Metadata fetching and chunk url extraction
This commit is contained in:
parent
001c761fe8
commit
f5da1941f6
20
main.go
20
main.go
@ -7,9 +7,21 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
master_url := "https://d17cyqyz9yhmep.cloudfront.net/streams/234945/playlist_1752291107574_1752292056713.m3u8"
|
masterUrl := "https://d17cyqyz9yhmep.cloudfront.net/streams/234945/playlist_1752291107574_1752292056713.m3u8"
|
||||||
|
|
||||||
streams := media.GetStreamMetadata(master_url)
|
stream, err := media.ParseMasterPlaylist(masterUrl)
|
||||||
|
if err != nil {
|
||||||
fmt.Println(streams)
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
audio, video, err := stream.FetchSegmentPlaylists()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
videoPlaylist := media.ParseMediaPlaylist(video)
|
||||||
|
audioPlaylist := media.ParseMediaPlaylist(audio)
|
||||||
|
|
||||||
|
fmt.Println(videoPlaylist)
|
||||||
|
fmt.Println(audioPlaylist)
|
||||||
}
|
}
|
||||||
|
|||||||
103
media/client.go
Normal file
103
media/client.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package media
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultTimeout = 30 * time.Second
|
||||||
|
UserAgent = "M3U8-Downloader/1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPClient struct {
|
||||||
|
client *http.Client
|
||||||
|
headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPClient() *HTTPClient {
|
||||||
|
return &HTTPClient{
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: DefaultTimeout,
|
||||||
|
},
|
||||||
|
headers: map[string]string{
|
||||||
|
"User-Agent": UserAgent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPClient) SetTimeout(timeout time.Duration) {
|
||||||
|
c.client.Timeout = timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPClient) SetHeader(key, value string) {
|
||||||
|
c.headers[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPClient) Get(url string) ([]byte, error) {
|
||||||
|
return c.GetWithContext(context.Background(), url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPClient) GetWithContext(ctx context.Context, url string) ([]byte, error) {
|
||||||
|
if !ValidateURL(url) {
|
||||||
|
return nil, errors.New("invalid URL format")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
for key, value := range c.headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global client instance for convenience functions
|
||||||
|
var defaultClient = NewHTTPClient()
|
||||||
|
|
||||||
|
func FetchPlaylistContent(url string) (string, error) {
|
||||||
|
data, err := defaultClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchPlaylistContentWithContext(ctx context.Context, url string) (string, error) {
|
||||||
|
data, err := defaultClient.GetWithContext(ctx, url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDefaultTimeout(timeout time.Duration) {
|
||||||
|
defaultClient.SetTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDefaultUserAgent(userAgent string) {
|
||||||
|
defaultClient.SetHeader("User-Agent", userAgent)
|
||||||
|
}
|
||||||
17
media/constants.go
Normal file
17
media/constants.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package media
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExtXStreamInf = "#EXT-X-STREAM-INF"
|
||||||
|
ExtXMedia = "#EXT-X-MEDIA"
|
||||||
|
ExtXTargetDuration = "#EXT-X-TARGETDURATION"
|
||||||
|
ExtInf = "#EXTINF"
|
||||||
|
ExtXEndList = "#EXT-X-ENDLIST"
|
||||||
|
ExtXPlaylistType = "#EXT-X-PLAYLIST-TYPE"
|
||||||
|
HTTPPrefix = "http://"
|
||||||
|
HTTPSPrefix = "https://"
|
||||||
|
ClientDefaultTimeout = 30 * time.Second
|
||||||
|
HTTPUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0"
|
||||||
|
PlaylistTypeVOD = "VOD"
|
||||||
|
)
|
||||||
88
media/parser.go
Normal file
88
media/parser.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package media
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseAttribute(attribute string) string {
|
||||||
|
parts := strings.Split(attribute, "=")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
return parts[1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func StripQuotes(input string) string {
|
||||||
|
return strings.Trim(input, "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolutionToPixels(resolution string) int {
|
||||||
|
parts := strings.Split(resolution, "x")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
width, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
height, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return width * height
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMediaAttributes(mediaInfo string) map[string]string {
|
||||||
|
attributes := make(map[string]string)
|
||||||
|
|
||||||
|
// Remove the #EXT-X-MEDIA: prefix
|
||||||
|
content := strings.TrimPrefix(mediaInfo, ExtXMedia+":")
|
||||||
|
|
||||||
|
// Split by comma, but respect quoted values
|
||||||
|
parts := splitRespectingQuotes(content, ',')
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
if kv := strings.SplitN(strings.TrimSpace(part), "=", 2); len(kv) == 2 {
|
||||||
|
attributes[kv[0]] = kv[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitRespectingQuotes(input string, delimiter rune) []string {
|
||||||
|
var result []string
|
||||||
|
var current strings.Builder
|
||||||
|
inQuotes := false
|
||||||
|
|
||||||
|
for _, char := range input {
|
||||||
|
switch char {
|
||||||
|
case '"':
|
||||||
|
inQuotes = !inQuotes
|
||||||
|
current.WriteRune(char)
|
||||||
|
case delimiter:
|
||||||
|
if inQuotes {
|
||||||
|
current.WriteRune(char)
|
||||||
|
} else {
|
||||||
|
result = append(result, current.String())
|
||||||
|
current.Reset()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
current.WriteRune(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.Len() > 0 {
|
||||||
|
result = append(result, current.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateURL(url string) bool {
|
||||||
|
return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
|
||||||
|
}
|
||||||
71
media/playlist.go
Normal file
71
media/playlist.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package media
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlaylistMetadata struct {
|
||||||
|
URL string
|
||||||
|
Domain string
|
||||||
|
StreamID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlaylistMetadata(masterURL string) *PlaylistMetadata {
|
||||||
|
strippedURL := strings.Replace(masterURL, HTTPSPrefix, "", 1)
|
||||||
|
urlPrefix := strings.Split(strippedURL, "/")[0]
|
||||||
|
|
||||||
|
return &PlaylistMetadata{
|
||||||
|
URL: masterURL,
|
||||||
|
Domain: urlPrefix,
|
||||||
|
StreamID: strings.Split(strippedURL, "/")[2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamSet struct {
|
||||||
|
Metadata *PlaylistMetadata
|
||||||
|
VideoStreams []VideoStream
|
||||||
|
AudioStreams []AudioStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StreamSet) BuildStreamURL(endpoint string) string {
|
||||||
|
return fmt.Sprintf("%s%s/streams/%s/%s",
|
||||||
|
HTTPSPrefix, s.Metadata.Domain, s.Metadata.StreamID, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMasterPlaylist(masterURL string) (*StreamSet, error) {
|
||||||
|
metadata := NewPlaylistMetadata(masterURL)
|
||||||
|
|
||||||
|
content, err := FetchPlaylistContent(masterURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
videoStreams, audioStreams := parsePlaylistLines(content)
|
||||||
|
|
||||||
|
return &StreamSet{
|
||||||
|
Metadata: metadata,
|
||||||
|
VideoStreams: videoStreams,
|
||||||
|
AudioStreams: audioStreams,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePlaylistLines(content string) ([]VideoStream, []AudioStream) {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
var videoStreams []VideoStream
|
||||||
|
var audioStreams []AudioStream
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
if strings.HasPrefix(line, ExtXStreamInf) && i+1 < len(lines) {
|
||||||
|
if video, err := NewVideoStream(line, lines[i+1]); err == nil {
|
||||||
|
videoStreams = append(videoStreams, *video)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(line, ExtXMedia) {
|
||||||
|
if audio, err := NewAudioStream(line); err == nil {
|
||||||
|
audioStreams = append(audioStreams, *audio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoStreams, audioStreams
|
||||||
|
}
|
||||||
107
media/segment.go
Normal file
107
media/segment.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package media
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Segment struct {
|
||||||
|
URL string
|
||||||
|
Duration float64
|
||||||
|
Sequence int
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaPlaylist struct {
|
||||||
|
Segments []Segment
|
||||||
|
TargetDuration float64
|
||||||
|
IsLive bool
|
||||||
|
HasEndList bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StreamSet) FetchSegmentPlaylists() (videoPlaylist, audioPlaylist string, err error) {
|
||||||
|
video, audio := s.SelectBestQualityStreams()
|
||||||
|
if video == nil {
|
||||||
|
return "", "", errors.New("no video streams available")
|
||||||
|
}
|
||||||
|
|
||||||
|
videoURL := s.BuildStreamURL(video.URL)
|
||||||
|
|
||||||
|
videoContent, err := FetchPlaylistContent(StripQuotes(videoURL))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var audioContent string
|
||||||
|
if audio != nil {
|
||||||
|
audioURL := s.BuildStreamURL(StripQuotes(audio.URL))
|
||||||
|
audioContent, err = FetchPlaylistContent(StripQuotes(audioURL))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoContent, audioContent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMediaPlaylist(content string) *MediaPlaylist {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
var segments []Segment
|
||||||
|
|
||||||
|
playlist := &MediaPlaylist{
|
||||||
|
IsLive: true,
|
||||||
|
HasEndList: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSequence := 0
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, ExtXTargetDuration):
|
||||||
|
playlist.TargetDuration = parseTargetDuration(line)
|
||||||
|
|
||||||
|
case strings.HasPrefix(line, ExtInf):
|
||||||
|
if i+1 < len(lines) {
|
||||||
|
duration := parseSegmentDuration(line)
|
||||||
|
segments = append(segments, Segment{
|
||||||
|
URL: lines[i+1],
|
||||||
|
Duration: duration,
|
||||||
|
Sequence: mediaSequence,
|
||||||
|
})
|
||||||
|
mediaSequence++
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.HasPrefix(line, ExtXPlaylistType):
|
||||||
|
if strings.Contains(line, PlaylistTypeVOD) {
|
||||||
|
playlist.IsLive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.HasPrefix(line, ExtXEndList):
|
||||||
|
playlist.HasEndList = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playlist.Segments = segments
|
||||||
|
return playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTargetDuration(line string) float64 {
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
if duration, err := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64); err == nil {
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSegmentDuration(line string) float64 {
|
||||||
|
parts := strings.Split(line, ":")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
durationPart := strings.Split(parts[1], ",")[0]
|
||||||
|
if duration, err := strconv.ParseFloat(durationPart, 64); err == nil {
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
98
media/stream.go
Normal file
98
media/stream.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package media
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VideoStream struct {
|
||||||
|
URL string
|
||||||
|
Bandwidth int
|
||||||
|
Codecs string
|
||||||
|
Resolution string
|
||||||
|
FrameRate string
|
||||||
|
AudioGroup string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVideoStream(streamInfo, url string) (*VideoStream, error) {
|
||||||
|
if !strings.HasPrefix(streamInfo, ExtXStreamInf) {
|
||||||
|
return nil, errors.New("invalid stream info line")
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := regexp.MustCompile(`([A-Z0-9-]+)=(".*?"|[^,]*)`)
|
||||||
|
matches := reg.FindAllStringSubmatch(streamInfo, -1)
|
||||||
|
|
||||||
|
if len(matches) < 5 {
|
||||||
|
return nil, errors.New("insufficient stream attributes")
|
||||||
|
}
|
||||||
|
|
||||||
|
bandwidth, err := strconv.Atoi(matches[0][2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &VideoStream{
|
||||||
|
URL: url,
|
||||||
|
Bandwidth: bandwidth,
|
||||||
|
Codecs: StripQuotes(matches[1][2]),
|
||||||
|
Resolution: StripQuotes(matches[2][2]),
|
||||||
|
FrameRate: StripQuotes(matches[3][2]),
|
||||||
|
AudioGroup: StripQuotes(matches[4][2]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AudioStream struct {
|
||||||
|
URL string
|
||||||
|
MediaType string
|
||||||
|
GroupID string
|
||||||
|
Name string
|
||||||
|
IsDefault bool
|
||||||
|
AutoSelect bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAudioStream(mediaInfo string) (*AudioStream, error) {
|
||||||
|
if !strings.HasPrefix(mediaInfo, ExtXMedia) {
|
||||||
|
return nil, errors.New("invalid media info line")
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes := parseMediaAttributes(mediaInfo)
|
||||||
|
|
||||||
|
return &AudioStream{
|
||||||
|
URL: StripQuotes(attributes["URI"]),
|
||||||
|
MediaType: StripQuotes(attributes["TYPE"]),
|
||||||
|
GroupID: StripQuotes(attributes["GROUP-ID"]),
|
||||||
|
Name: StripQuotes(attributes["NAME"]),
|
||||||
|
IsDefault: attributes["DEFAULT"] == "YES",
|
||||||
|
AutoSelect: attributes["AUTOSELECT"] == "YES",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StreamSet) SelectBestQualityStreams() (*VideoStream, *AudioStream) {
|
||||||
|
if len(s.VideoStreams) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bestVideo := s.VideoStreams[0]
|
||||||
|
maxPixels := ResolutionToPixels(bestVideo.Resolution)
|
||||||
|
|
||||||
|
for _, video := range s.VideoStreams {
|
||||||
|
pixels := ResolutionToPixels(video.Resolution)
|
||||||
|
if video.Bandwidth > bestVideo.Bandwidth || pixels > maxPixels {
|
||||||
|
bestVideo = video
|
||||||
|
maxPixels = pixels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &bestVideo, s.FindAudioStreamByGroup(bestVideo.AudioGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StreamSet) FindAudioStreamByGroup(groupID string) *AudioStream {
|
||||||
|
for _, audio := range s.AudioStreams {
|
||||||
|
if audio.GroupID == groupID {
|
||||||
|
return &audio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
113
media/types.go
113
media/types.go
@ -1,113 +0,0 @@
|
|||||||
package media
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StreamSet struct {
|
|
||||||
PlaylistURL string
|
|
||||||
VideoURLs []VideoURL
|
|
||||||
AudioURLs []AudioURL
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetStreamMetadata(master_url string) *StreamSet {
|
|
||||||
resp, err := http.Get(master_url)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
panic(resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
content := string(data)
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
video_urls := []VideoURL{}
|
|
||||||
audio_urls := []AudioURL{}
|
|
||||||
for i, line := range lines {
|
|
||||||
if strings.HasPrefix(line, "#EXT-X-STREAM-INF") {
|
|
||||||
video := NewVideoURL(lines[i], lines[i+1])
|
|
||||||
video_urls = append(video_urls, *video)
|
|
||||||
} else if strings.HasPrefix(line, "#EXT-X-MEDIA") {
|
|
||||||
audio := NewAudioURL(lines[i])
|
|
||||||
audio_urls = append(audio_urls, *audio)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &StreamSet{
|
|
||||||
PlaylistURL: master_url,
|
|
||||||
VideoURLs: video_urls,
|
|
||||||
AudioURLs: audio_urls,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type VideoURL struct {
|
|
||||||
URL string
|
|
||||||
Bandwidth int
|
|
||||||
Codecs string
|
|
||||||
Resolution string
|
|
||||||
FrameRate string
|
|
||||||
Audio string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewVideoURL(input string, url string) *VideoURL {
|
|
||||||
if strings.HasPrefix(input, "#EXT-X-STREAM-INF") {
|
|
||||||
reg := regexp.MustCompile(`([A-Z0-9-]+)=(".*?"|[^,]*)`)
|
|
||||||
split := reg.FindAllStringSubmatch(input, -1)
|
|
||||||
bandwidth := split[0][2]
|
|
||||||
bandwidth_int, err := strconv.Atoi(bandwidth)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
video := VideoURL{
|
|
||||||
URL: url,
|
|
||||||
Bandwidth: bandwidth_int,
|
|
||||||
Codecs: split[1][2],
|
|
||||||
Resolution: split[2][2],
|
|
||||||
FrameRate: split[3][2],
|
|
||||||
Audio: split[4][2],
|
|
||||||
}
|
|
||||||
return &video
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type AudioURL struct {
|
|
||||||
URL string
|
|
||||||
MediaType string
|
|
||||||
GroupId string
|
|
||||||
Name string
|
|
||||||
Default string
|
|
||||||
Autoselect string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAudioURL(input string) *AudioURL {
|
|
||||||
if strings.HasPrefix(input, "#EXT-X-MEDIA") {
|
|
||||||
split := strings.Split(strings.Split(input, ":")[1], ",")
|
|
||||||
url := ParseAttribute(split[5])
|
|
||||||
mediaType := ParseAttribute(split[0])
|
|
||||||
groupId := ParseAttribute(split[1])
|
|
||||||
name := ParseAttribute(split[2])
|
|
||||||
default_ := ParseAttribute(split[3])
|
|
||||||
autoselect := ParseAttribute(split[4])
|
|
||||||
audio := AudioURL{
|
|
||||||
URL: url,
|
|
||||||
MediaType: mediaType,
|
|
||||||
GroupId: groupId,
|
|
||||||
Name: name,
|
|
||||||
Default: default_,
|
|
||||||
Autoselect: autoselect,
|
|
||||||
}
|
|
||||||
return &audio
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
package media
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseAttribute(attribute string) string {
|
|
||||||
split := strings.Split(attribute, "=")
|
|
||||||
return split[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResolutionToPixels(resolution string) int {
|
|
||||||
split := strings.Split(resolution, "x")
|
|
||||||
width, err := strconv.Atoi(split[0])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
height, err := strconv.Atoi(split[1])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return width * height
|
|
||||||
}
|
|
||||||
327
scratch.txt
Normal file
327
scratch.txt
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
#EXTM3U
|
||||||
|
#FLOSPORTS:56b3742
|
||||||
|
#EXT-X-VERSION:7
|
||||||
|
#EXT-X-TARGETDURATION:7
|
||||||
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
|
#EXT-X-PROGRAM-DATE-TIME:2025-07-12T03:31:41.699Z
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3225_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3226_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3227_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3228_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3229_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3230_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3231_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3232_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3233_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3234_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3235_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3236_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3237_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3238_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3239_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3240_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3241_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3242_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3243_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3244_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3245_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3246_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3247_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3248_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3249_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3250_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3251_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3252_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3253_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3254_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3255_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3256_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3257_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3258_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3259_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3260_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3261_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3262_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3263_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3264_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3265_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3266_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3267_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3268_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3269_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3270_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3271_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3272_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3273_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3274_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3275_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3276_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3277_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3278_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3279_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3280_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3281_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3282_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3283_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3284_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3285_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3286_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3287_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3288_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3289_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3290_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3291_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3292_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3293_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3294_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3295_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3296_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3297_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3298_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3299_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3300_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3301_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3302_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3303_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3304_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3305_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3306_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3307_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3308_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3309_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3310_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3311_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3312_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3313_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3314_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3315_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3316_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3317_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3318_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3319_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3320_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3321_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3322_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3323_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3324_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3325_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3326_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3327_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3328_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3329_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3330_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3331_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3332_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3333_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3334_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3335_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3336_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3337_1752271771877.ts
|
||||||
|
#EXTINF:6.005,
|
||||||
|
media_vo_3338_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3339_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3340_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3341_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3342_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3343_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3344_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3345_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3346_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3347_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3348_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3349_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3350_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3351_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3352_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3353_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3354_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3355_1752271771877.ts
|
||||||
|
#EXTINF:6.005,
|
||||||
|
media_vo_3356_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3357_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3358_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3359_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3360_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3361_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3362_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3363_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3364_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3365_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3366_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3367_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3368_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3369_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3370_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3371_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3372_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3373_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3374_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3375_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3376_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3377_1752271771877.ts
|
||||||
|
#EXTINF:5.99,
|
||||||
|
media_vo_3378_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3379_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3380_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3381_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3382_1752271771877.ts
|
||||||
|
#EXTINF:5.989,
|
||||||
|
media_vo_3383_1752271771877.ts
|
||||||
|
#EXTINF:6.006,
|
||||||
|
media_vo_3384_1752271771877.ts
|
||||||
|
#EXT-X-ENDLIST
|
||||||
Loading…
x
Reference in New Issue
Block a user