182 lines
4.5 KiB
Go
182 lines
4.5 KiB
Go
package parser
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"govd/enums"
|
|
"govd/models"
|
|
|
|
"github.com/grafov/m3u8"
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
var formats []*models.MediaFormat
|
|
|
|
if listType == m3u8.MASTER {
|
|
masterpl := playlist.(*m3u8.MasterPlaylist)
|
|
|
|
for _, variant := range masterpl.Variants {
|
|
if variant == nil || variant.URI == "" {
|
|
continue
|
|
}
|
|
|
|
width, height := int64(0), int64(0)
|
|
if variant.Resolution != "" {
|
|
var w, h int
|
|
if _, err := fmt.Sscanf(variant.Resolution, "%dx%d", &w, &h); err == nil {
|
|
width, height = int64(w), int64(h)
|
|
}
|
|
}
|
|
|
|
format := &models.MediaFormat{
|
|
Type: enums.MediaTypeVideo,
|
|
FormatID: fmt.Sprintf("hls-%d", variant.Bandwidth/1000),
|
|
VideoCodec: getCodecFromCodecs(variant.Codecs),
|
|
AudioCodec: getAudioCodecFromCodecs(variant.Codecs),
|
|
Bitrate: int64(variant.Bandwidth),
|
|
Width: width,
|
|
Height: height,
|
|
}
|
|
|
|
variantURL := resolveURL(baseURLObj, variant.URI)
|
|
format.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
|
|
}
|
|
|
|
if listType == m3u8.MEDIA {
|
|
mediapl := playlist.(*m3u8.MediaPlaylist)
|
|
|
|
var segments []string
|
|
var totalDuration float64
|
|
|
|
for _, segment := range mediapl.Segments {
|
|
if segment != nil && segment.URI != "" {
|
|
segmentURL := segment.URI
|
|
if !strings.HasPrefix(segmentURL, "http://") && !strings.HasPrefix(segmentURL, "https://") {
|
|
segmentURL = resolveURL(baseURLObj, segmentURL)
|
|
}
|
|
|
|
segments = append(segments, segmentURL)
|
|
totalDuration += segment.Duration
|
|
}
|
|
}
|
|
|
|
format := &models.MediaFormat{
|
|
Type: enums.MediaTypeVideo,
|
|
FormatID: "hls",
|
|
VideoCodec: enums.MediaCodecAVC,
|
|
AudioCodec: enums.MediaCodecAAC,
|
|
Duration: int64(totalDuration),
|
|
URL: []string{baseURL},
|
|
Segments: segments,
|
|
}
|
|
|
|
return []*models.MediaFormat{format}, nil
|
|
}
|
|
|
|
return nil, errors.New("unsupported m3u8 playlist type")
|
|
}
|
|
|
|
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 getCodecFromCodecs(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 enums.MediaCodecAVC
|
|
}
|
|
|
|
func getAudioCodecFromCodecs(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 enums.MediaCodecAAC
|
|
}
|
|
|
|
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()
|
|
}
|