287 lines
6.8 KiB
Go
287 lines
6.8 KiB
Go
package parser
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"govd/enums"
|
|
"govd/models"
|
|
|
|
"github.com/grafov/m3u8"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var httpClient = &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
func ParseM3U8Content(
|
|
content []byte,
|
|
baseURL string,
|
|
) ([]*models.MediaFormat, error) {
|
|
baseURLObj, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid base url: %w", err)
|
|
}
|
|
|
|
buf := bytes.NewBuffer(content)
|
|
playlist, listType, err := m3u8.DecodeFrom(buf, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed parsing m3u8: %w", err)
|
|
}
|
|
|
|
switch listType {
|
|
case m3u8.MASTER:
|
|
return parseMasterPlaylist(
|
|
playlist.(*m3u8.MasterPlaylist),
|
|
baseURLObj,
|
|
)
|
|
case m3u8.MEDIA:
|
|
return parseMediaPlaylist(
|
|
playlist.(*m3u8.MediaPlaylist),
|
|
baseURLObj,
|
|
)
|
|
}
|
|
|
|
return nil, errors.New("unsupported m3u8 playlist type")
|
|
}
|
|
|
|
func parseMasterPlaylist(
|
|
playlist *m3u8.MasterPlaylist,
|
|
baseURL *url.URL,
|
|
) ([]*models.MediaFormat, error) {
|
|
var formats []*models.MediaFormat
|
|
|
|
seenAlternatives := make(map[string]bool)
|
|
for _, variant := range playlist.Variants {
|
|
if variant == nil || variant.URI == "" {
|
|
continue
|
|
}
|
|
for _, alt := range variant.Alternatives {
|
|
if _, ok := seenAlternatives[alt.GroupId]; ok {
|
|
continue
|
|
}
|
|
seenAlternatives[alt.GroupId] = true
|
|
format := parseAlternative(
|
|
playlist.Variants,
|
|
alt, baseURL,
|
|
)
|
|
if format == nil {
|
|
continue
|
|
}
|
|
formats = append(formats, format)
|
|
}
|
|
width, height := getResolution(variant.Resolution)
|
|
mediaType, videoCodec, audioCodec := parseVariantType(variant)
|
|
variantURL := resolveURL(baseURL, variant.URI)
|
|
if variant.Audio != "" {
|
|
audioCodec = ""
|
|
}
|
|
format := &models.MediaFormat{
|
|
FormatID: fmt.Sprintf("hls-%d", variant.Bandwidth/1000),
|
|
Type: mediaType,
|
|
VideoCodec: videoCodec,
|
|
AudioCodec: audioCodec,
|
|
Bitrate: int64(variant.Bandwidth),
|
|
Width: width,
|
|
Height: height,
|
|
URL: []string{variantURL},
|
|
}
|
|
variantContent, err := fetchContent(variantURL)
|
|
if err == nil {
|
|
variantFormats, err := ParseM3U8Content(variantContent, variantURL)
|
|
if err == nil && len(variantFormats) > 0 {
|
|
format.Segments = variantFormats[0].Segments
|
|
if variantFormats[0].Duration > 0 {
|
|
format.Duration = variantFormats[0].Duration
|
|
}
|
|
}
|
|
}
|
|
formats = append(formats, format)
|
|
}
|
|
return formats, nil
|
|
}
|
|
|
|
func parseMediaPlaylist(
|
|
playlist *m3u8.MediaPlaylist,
|
|
baseURL *url.URL,
|
|
) ([]*models.MediaFormat, error) {
|
|
var segments []string
|
|
var totalDuration float64
|
|
initSegment := playlist.Map
|
|
if initSegment != nil && initSegment.URI != "" {
|
|
initSegmentURL := resolveURL(baseURL, initSegment.URI)
|
|
segments = append(segments, initSegmentURL)
|
|
}
|
|
for _, segment := range playlist.Segments {
|
|
if segment != nil && segment.URI != "" {
|
|
segmentURL := resolveURL(baseURL, segment.URI)
|
|
segments = append(segments, segmentURL)
|
|
totalDuration += segment.Duration
|
|
if segment.Limit > 0 {
|
|
// byterange not supported
|
|
break
|
|
}
|
|
}
|
|
}
|
|
format := &models.MediaFormat{
|
|
FormatID: "hls",
|
|
Duration: int64(totalDuration),
|
|
URL: []string{baseURL.String()},
|
|
Segments: segments,
|
|
}
|
|
return []*models.MediaFormat{format}, nil
|
|
}
|
|
|
|
func parseAlternative(
|
|
variants []*m3u8.Variant,
|
|
alternative *m3u8.Alternative,
|
|
baseURL *url.URL,
|
|
) *models.MediaFormat {
|
|
if alternative == nil || alternative.URI == "" {
|
|
return nil
|
|
}
|
|
if alternative.Type != "AUDIO" {
|
|
return nil
|
|
}
|
|
altURL := resolveURL(baseURL, alternative.URI)
|
|
audioCodec := getAudioAlternativeCodec(variants, alternative)
|
|
format := &models.MediaFormat{
|
|
FormatID: "hls" + alternative.GroupId,
|
|
Type: enums.MediaTypeAudio,
|
|
AudioCodec: audioCodec,
|
|
URL: []string{altURL},
|
|
}
|
|
altContent, err := fetchContent(altURL)
|
|
if err == nil {
|
|
altFormats, err := ParseM3U8Content(altContent, altURL)
|
|
if err == nil && len(altFormats) > 0 {
|
|
format.Segments = altFormats[0].Segments
|
|
if altFormats[0].Duration > 0 {
|
|
format.Duration = altFormats[0].Duration
|
|
}
|
|
}
|
|
}
|
|
return format
|
|
}
|
|
|
|
func getAudioAlternativeCodec(
|
|
variants []*m3u8.Variant,
|
|
alt *m3u8.Alternative,
|
|
) enums.MediaCodec {
|
|
if alt == nil || alt.URI == "" {
|
|
return ""
|
|
}
|
|
if alt.Type != "AUDIO" {
|
|
return ""
|
|
}
|
|
for _, variant := range variants {
|
|
if variant == nil || variant.URI == "" {
|
|
continue
|
|
}
|
|
if variant.Audio != alt.GroupId {
|
|
continue
|
|
}
|
|
audioCodec := getAudioCodec(variant.Codecs)
|
|
if audioCodec != "" {
|
|
return audioCodec
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func ParseM3U8FromURL(url string) ([]*models.MediaFormat, error) {
|
|
body, err := fetchContent(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch m3u8 content: %w", err)
|
|
}
|
|
return ParseM3U8Content(body, url)
|
|
}
|
|
|
|
func fetchContent(url string) ([]byte, error) {
|
|
resp, err := httpClient.Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch content: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("server returned status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
|
|
func getResolution(
|
|
resolution string,
|
|
) (int64, int64) {
|
|
var width, height int
|
|
if _, err := fmt.Sscanf(resolution, "%dx%d", &width, &height); err == nil {
|
|
return int64(width), int64(height)
|
|
}
|
|
return 0, 0
|
|
}
|
|
|
|
func parseVariantType(
|
|
variant *m3u8.Variant,
|
|
) (enums.MediaType, enums.MediaCodec, enums.MediaCodec) {
|
|
var mediaType enums.MediaType
|
|
var videoCodec, audioCodec enums.MediaCodec
|
|
|
|
videoCodec = getVideoCodec(variant.Codecs)
|
|
audioCodec = getAudioCodec(variant.Codecs)
|
|
|
|
if videoCodec != "" {
|
|
mediaType = enums.MediaTypeVideo
|
|
} else if audioCodec != "" {
|
|
mediaType = enums.MediaTypeAudio
|
|
}
|
|
|
|
return mediaType, videoCodec, audioCodec
|
|
}
|
|
|
|
func getVideoCodec(codecs string) enums.MediaCodec {
|
|
if strings.Contains(codecs, "avc") || strings.Contains(codecs, "h264") {
|
|
return enums.MediaCodecAVC
|
|
} else if strings.Contains(codecs, "hvc") || strings.Contains(codecs, "h265") {
|
|
return enums.MediaCodecHEVC
|
|
} else if strings.Contains(codecs, "av01") {
|
|
return enums.MediaCodecAV1
|
|
} else if strings.Contains(codecs, "vp9") {
|
|
return enums.MediaCodecVP9
|
|
} else if strings.Contains(codecs, "vp8") {
|
|
return enums.MediaCodecVP8
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getAudioCodec(codecs string) enums.MediaCodec {
|
|
if strings.Contains(codecs, "mp4a") {
|
|
return enums.MediaCodecAAC
|
|
} else if strings.Contains(codecs, "opus") {
|
|
return enums.MediaCodecOpus
|
|
} else if strings.Contains(codecs, "mp3") {
|
|
return enums.MediaCodecMP3
|
|
} else if strings.Contains(codecs, "flac") {
|
|
return enums.MediaCodecFLAC
|
|
} else if strings.Contains(codecs, "vorbis") {
|
|
return enums.MediaCodecVorbis
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func resolveURL(base *url.URL, uri string) string {
|
|
if strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://") {
|
|
return uri
|
|
}
|
|
ref, err := url.Parse(uri)
|
|
if err != nil {
|
|
return uri
|
|
}
|
|
return base.ResolveReference(ref).String()
|
|
}
|