commit a30e111bb1d9e62aac3810e51749241f488e046a from: Oliver Lowe date: Thu May 15 12:27:39 2025 UTC m3u8: support parsing segment keys commit - 1146e3cfa554e40c95835cd5fd7c65eb7afb40aa commit + a30e111bb1d9e62aac3810e51749241f488e046a blob - 1944ff91c17ec4b247f369030d6be9974a145ba2 blob + b524a68ce5470a6e0137bb827ed7145edd7d5f99 --- m3u8/m3u8.go +++ m3u8/m3u8.go @@ -99,6 +99,7 @@ const ( EncryptMethodNone EncryptMethod = 0 + iota EncryptMethodAES128 EncryptMethodSampleAES + encryptMethodInvalid EncryptMethod = 255 ) func (m EncryptMethod) String() string { @@ -113,6 +114,18 @@ func (m EncryptMethod) String() string { return "invalid" } +func parseEncryptMethod(s string) EncryptMethod { + switch s { + case EncryptMethodNone.String(): + return EncryptMethodNone + case EncryptMethodAES128.String(): + return EncryptMethodAES128 + case EncryptMethodSampleAES.String(): + return EncryptMethodSampleAES + } + return encryptMethodInvalid +} + type Map struct { URI string ByteRange ByteRange blob - 22fc0b281c644f6e25183ceb6de969d198189b80 blob + d019ec22dc533455dad16f1fa9b75bc93bb9732f --- m3u8/segment.go +++ m3u8/segment.go @@ -2,6 +2,7 @@ package m3u8 import ( "bytes" + "encoding/hex" "errors" "fmt" "io" @@ -38,6 +39,12 @@ func parseSegment(items chan item, leading item) (*Seg return nil, fmt.Errorf("parse segment duration: %w", err) } seg.Duration = dur + case tagKey: + key, err := parseKey(items) + if err != nil { + return nil, fmt.Errorf("parse key: %w", err) + } + seg.Key = &key default: return nil, fmt.Errorf("parse leading item %s: unsupported", leading) } @@ -75,6 +82,12 @@ func parseSegment(items chan item, leading item) (*Seg seg.Range = r case tagDiscontinuity: seg.Discontinuity = true + case tagKey: + key, err := parseKey(items) + if err != nil { + return nil, fmt.Errorf("parse key: %w", err) + } + seg.Key = &key default: return nil, fmt.Errorf("parsing %s unsupported", it) } @@ -123,6 +136,61 @@ func parseSegmentDuration(it item) (time.Duration, err return time.Duration(microseconds) * time.Microsecond, nil } +func parseKey(items chan item) (Key, error) { + var key Key + for it := range items { + switch it.typ { + case itemError: + return key, errors.New(it.val) + case itemNewline: + return key, nil + case itemAttrName: + v := <-items + if v.typ != itemEquals { + return key, fmt.Errorf("expected %q after %s, got %s", "=", it.typ, v) + } + switch it.val { + case "METHOD": + v = <-items + key.Method = parseEncryptMethod(v.val) + if key.Method == encryptMethodInvalid { + return key, fmt.Errorf("bad encrypt method %q", v.val) + } + case "URI": + v = <-items + key.URI = strings.Trim(v.val, `"`) + case "IV": + v = <-items + b, err := hex.DecodeString(strings.TrimPrefix(v.val, "0x")) + if err != nil { + return key, fmt.Errorf("parse initialisation vector: %w", err) + } + if len(b) != len(key.IV) { + return key, fmt.Errorf("bad initialisation length %d, want %d", len(b), len(key.IV)) + } + copy(key.IV[:], b) + case "KEYFORMAT": + v = <-items + key.Format = strings.Trim(v.val, `"`) + case "KEYFORMATVERSIONS": + v = <-items + ss := strings.Split(v.val, "/") + key.FormatVersions = make([]uint32, len(ss)) + for i := range ss { + n, err := strconv.Atoi(ss[i]) + if err != nil { + return key, fmt.Errorf("parse key format version: %w", err) + } + key.FormatVersions[i] = uint32(n) + } + default: + return key, fmt.Errorf("TODO %s", it.val) + } + } + } + return key, fmt.Errorf("TODO") +} + func writeSegments(w io.Writer, segments []Segment) (n int, err error) { for i, seg := range segments { b, err := seg.MarshalText() blob - 40d13ffe7d9e39b9073e50c523d8d9341d1e6072 blob + c9c777d8c62b46c04c954a12fc84294e87fd250b --- m3u8/segment_test.go +++ m3u8/segment_test.go @@ -113,7 +113,7 @@ func TestParseSegment(t *testing.T) { Key: &Key{ Method: EncryptMethodAES128, URI: "key1.json?f=1041&s=0&p=1822767&m=1506045858", - IV: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x1B, 0xD0, 0x2F}, + IV: [...]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x1B, 0xD0, 0x2F}, }, URI: "1041_6_1822767.ts?m=1506045858", }