grafov / m3u8

Parser and generator of M3U8-playlists for Apple HLS. Library for Go language. :cinema:
http://tools.ietf.org/html/draft-pantos-http-live-streaming
BSD 3-Clause "New" or "Revised" License
1.23k stars 313 forks source link

EXT-X-MEDIA audio missing from VariantParams.Alternatives for master playlist decode #160

Open elv-peter opened 4 years ago

elv-peter commented 4 years ago

For the master playlist below, the MasterPlaylist object (after decoding) is missing the audio stream with GROUP-ID="audio-aacl-128". That is, MasterPlaylist.Variants[9].VariantParams.Alternatives is nil.

Also, only the first variant's Alternatives is populated, which I believe is this ticket: https://github.com/grafov/m3u8/issues/96. However, I can't work around this by accessing the audio-aacl-128 stream using the first variant, because it specifies AUDIO="audio-aacl-64".

#EXTM3U
#EXT-X-VERSION:5
## Created with Unified Streaming Platform(version=1.9.5)

# AUDIO groups
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aacl-64",NAME="English",LANGUAGE="en",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="avc_decrypted_global-stream_audio_eng_64000=64000.m3u8"

# variants
#EXT-X-STREAM-INF:BANDWIDTH=202000,AVERAGE-BANDWIDTH=184000,CODECS="mp4a.40.2,avc1.42C01E",RESOLUTION=192x108,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=112000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=407000,AVERAGE-BANDWIDTH=370000,CODECS="mp4a.40.2,avc1.4D401F",RESOLUTION=480x270,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=288000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=631000,AVERAGE-BANDWIDTH=574000,CODECS="mp4a.40.2,avc1.4D401F",RESOLUTION=640x360,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=480000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1051000,AVERAGE-BANDWIDTH=956000,CODECS="mp4a.40.2,avc1.4D401F",RESOLUTION=960x540,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=840000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1821000,AVERAGE-BANDWIDTH=1655000,CODECS="mp4a.40.2,avc1.4D401F",RESOLUTION=960x540,FRAME-RATE=25,AUDIO="audio-aacl-64",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=1499968.m3u8

# AUDIO groups
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aacl-128",NAME="English",LANGUAGE="en",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="avc_decrypted_global-stream_audio_eng_128000=128000.m3u8"

# variants
#EXT-X-STREAM-INF:BANDWIDTH=2824000,AVERAGE-BANDWIDTH=2568000,CODECS="mp4a.40.2,avc1.4D401F",RESOLUTION=1280x720,FRAME-RATE=25,AUDIO="audio-aacl-128",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=2299968.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3641000,AVERAGE-BANDWIDTH=3310000,CODECS="mp4a.40.2,avc1.4D401F",RESOLUTION=1280x720,FRAME-RATE=25,AUDIO="audio-aacl-128",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=3000000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5273000,AVERAGE-BANDWIDTH=4794000,CODECS="mp4a.40.2,avc1.640020",RESOLUTION=1280x720,FRAME-RATE=50,AUDIO="audio-aacl-128",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=4400000.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7722000,AVERAGE-BANDWIDTH=7020000,CODECS="mp4a.40.2,avc1.640020",RESOLUTION=1280x720,FRAME-RATE=50,AUDIO="audio-aacl-128",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=6499968.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=9471000,AVERAGE-BANDWIDTH=8610000,CODECS="mp4a.40.2,avc1.640020",RESOLUTION=1280x720,FRAME-RATE=50,AUDIO="audio-aacl-128",CLOSED-CAPTIONS=NONE
avc_decrypted_global-video=8000000.m3u8

# keyframes
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=17000,CODECS="avc1.42C01E",RESOLUTION=192x108,URI="keyframes/avc_decrypted_global-video=112000.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=42000,CODECS="avc1.4D401F",RESOLUTION=480x270,URI="keyframes/avc_decrypted_global-video=288000.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=70000,CODECS="avc1.4D401F",RESOLUTION=640x360,URI="keyframes/avc_decrypted_global-video=480000.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=123000,CODECS="avc1.4D401F",RESOLUTION=960x540,URI="keyframes/avc_decrypted_global-video=840000.m3u8"
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=336000,CODECS="avc1.4D401F",RESOLUTION=1280x720,URI="keyframes/avc_decrypted_global-video=2299968.m3u8"
slugalisk commented 3 years ago

#EXT-X-MEDIA tags can be parsed using custom tags ex:

type ExtXMediaTagValue struct {
    Type       string
    GroupID    string
    Channels   string
    Name       string
    Language   string
    Default    bool
    AutoSelect bool
    Forced     bool
    URI        string
}

type ExtXMediaTag struct {
    Values []*ExtXMediaTagValue
}

func (tag *ExtXMediaTag) TagName() string {
    return "#EXT-X-MEDIA:"
}

func (tag *ExtXMediaTag) Decode(line string) (m3u8.CustomTag, error) {
    val := &ExtXMediaTagValue{}

    for k, v := range m3u8.DecodeAttributeList(line) {
        switch k {
        case "TYPE":
            val.Type = v
        case "GROUP-ID":
            val.GroupID = v
        case "CHANNELS":
            val.Channels = v
        case "NAME":
            val.Name = v
        case "LANGUAGE":
            val.Language = v
        case "DEFAULT":
            val.Default = v == "YES"
        case "AUTOSELECT":
            val.AutoSelect = v == "YES"
        case "FORCED":
            val.Forced = v == "YES"
        case "URI":
            val.URI = v
        }
    }

    tag.Values = append(tag.Values, val)

    return tag, nil
}

func (tag *ExtXMediaTag) SegmentTag() bool {
    return false
}

func (tag *ExtXMediaTag) Encode() *bytes.Buffer {
    b := bytes.NewBuffer(nil)

    for i, val := range tag.Values {
        if i != 0 {
            b.WriteByte('\n')
        }
        b.WriteString(tag.TagName())
        writeStringTagAttr(b, "TYPE", val.Type, false)
        b.WriteRune(',')
        writeStringTagAttr(b, "GROUP-ID", val.GroupID, true)
        if val.Type == "AUDIO" {
            b.WriteRune(',')
            writeStringTagAttr(b, "CHANNELS", val.Channels, true)
        }
        b.WriteRune(',')
        writeStringTagAttr(b, "NAME", val.Name, true)
        b.WriteRune(',')
        writeStringTagAttr(b, "LANGUAGE", val.Language, true)
        b.WriteRune(',')
        writeBoolTagAttr(b, "DEFAULT", val.Default)
        b.WriteRune(',')
        writeBoolTagAttr(b, "AUTOSELECT", val.AutoSelect)
        if val.Type == "SUBTITLES" {
            b.WriteRune(',')
            writeBoolTagAttr(b, "FORCED", val.Forced)
        }
        b.WriteRune(',')
        writeStringTagAttr(b, "URI", val.URI, true)
    }

    return b
}

func (tag *ExtXMediaTag) String() string {
    return tag.Encode().String()
}

func writeStringTagAttr(b *bytes.Buffer, k string, v string, quote bool) {
    b.WriteString(k)
    b.WriteRune('=')
    if quote {
        b.WriteRune('"')
    }
    b.WriteString(v)
    if quote {
        b.WriteRune('"')
    }
}

func writeBoolTagAttr(b *bytes.Buffer, k string, v bool) {
    b.WriteString(k)
    b.WriteRune('=')
    if v {
        b.WriteString("YES")
    } else {
        b.WriteString("NO")
    }
}