commit a30e111bb1d9e62aac3810e51749241f488e046a
parent 1146e3cfa554e40c95835cd5fd7c65eb7afb40aa
Author: Oliver Lowe <o@olowe.co>
Date: Thu, 15 May 2025 22:27:39 +1000
m3u8: support parsing segment keys
Diffstat:
3 files changed, 82 insertions(+), 1 deletion(-)
diff --git a/m3u8/m3u8.go b/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
diff --git a/m3u8/segment.go b/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) (*Segment, error) {
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) (*Segment, error) {
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, error) {
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()
diff --git a/m3u8/segment_test.go b/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",
}