streaming

Media streaming and broadcast systems in Go
Log | Files | Refs | README | LICENSE

commit f2aae646ea349e196c882dea120da8243efb37f6
parent f06b871300334ace54d1d6c9678156a53cfcff65
Author: Oliver Lowe <o@olowe.co>
Date:   Fri, 31 May 2024 12:43:43 +1000

scte35, m3u8: rename SpliceInfo -> Splice

Unlike in std io/fs, there's no extra Info, there's just Splice. The
Info suffix in SpliceInfo doesn't tell us anything more than just
Splice.

Diffstat:
Mm3u8/m3u8.go | 6+++---
Mm3u8/write.go | 4++--
Mscte35/cipher_test.go | 6+++---
Mscte35/codec_test.go | 4++--
Mscte35/scte35_test.go | 12++++++------
Ascte35/splice.go | 267+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dscte35/splice_info.go | 267-------------------------------------------------------------------------------
Dscte35/splice_info_test.go | 167-------------------------------------------------------------------------------
Ascte35/splice_test.go | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 450 insertions(+), 450 deletions(-)

diff --git a/m3u8/m3u8.go b/m3u8/m3u8.go @@ -139,13 +139,13 @@ type DateRange struct { Planned time.Duration // value must be a string, float or hex sequence (int?) Custom map[string]any - CueCommand *scte35.SpliceInfo + CueCommand *scte35.Splice // Contains the first of the in/out cue pair. Command may be // TimeSignal or Insert, with OutOfNetwork set to true. - CueOut *scte35.SpliceInfo + CueOut *scte35.Splice // Contains the second of the cue in/out pair. The Command's // Type must match the "out" cue. - CueIn *scte35.SpliceInfo + CueIn *scte35.Splice EndOnNext bool } diff --git a/m3u8/write.go b/m3u8/write.go @@ -145,14 +145,14 @@ func writeDateRange(w io.Writer, dr *DateRange) error { // TODO(otl): dr.Custom. // TODO(otl): dr.CueCommand, when to write this versuse cuein, cueout. if dr.CueIn != nil { - b, err := scte35.EncodeSpliceInfo(dr.CueIn) + b, err := scte35.Encode(dr.CueIn) if err != nil { return fmt.Errorf("encode cue in: %w", err) } attrs = append(attrs, fmt.Sprintf("SCTE35-IN=0x%s", hex.EncodeToString(b))) } if dr.CueOut != nil { - b, err := scte35.EncodeSpliceInfo(dr.CueOut) + b, err := scte35.Encode(dr.CueOut) if err != nil { return fmt.Errorf("encode cue out: %w", err) } diff --git a/scte35/cipher_test.go b/scte35/cipher_test.go @@ -4,16 +4,16 @@ import "testing" func TestPackEncryption(t *testing.T) { type ptest struct { - sis SpliceInfo + sis Splice want uint8 } var tests = []ptest{ { - sis: SpliceInfo{Encrypted: true, Cipher: DES_CBC}, + sis: Splice{Encrypted: true, Cipher: DES_CBC}, want: 0b10000100, }, { - sis: SpliceInfo{Encrypted: true, Cipher: TripleDES}, + sis: Splice{Encrypted: true, Cipher: TripleDES}, want: 0b10000110, }, } diff --git a/scte35/codec_test.go b/scte35/codec_test.go @@ -13,7 +13,7 @@ func Example() { if err != nil { // handle error... } - splice, err := scte35.DecodeSpliceInfo(b) + splice, err := scte35.Decode(b) if err != nil { // handle error... } @@ -23,7 +23,7 @@ func Example() { *dur += 15 * 90000 // add 15 seconds as per 90KHz clock. fmt.Println(*splice.Descriptors[0].(scte35.SegmentationDescriptor).Duration) - bb, err := scte35.EncodeSpliceInfo(splice) + bb, err := scte35.Encode(splice) if err != nil { // handle error... } diff --git a/scte35/scte35_test.go b/scte35/scte35_test.go @@ -3,14 +3,14 @@ package scte35 type sample struct { name string encoded string - want SpliceInfo + want Splice } var samples = []sample{ { name: "14.1. time_signal", encoded: "/DA0AAAAAAAA///wBQb+cr0AUAAeAhxDVUVJSAAAjn/PAAGlmbAICAAAAAAsoKGKNAIAmsnRfg==", - want: SpliceInfo{ + want: Splice{ SAPType: SAPNone, Tier: 0x0fff, CWIndex: 0xff, @@ -39,7 +39,7 @@ var samples = []sample{ { name: "14.2. splice_insert", encoded: "/DAvAAAAAAAA///wFAVIAACPf+/+c2nALv4AUsz1AAAAAAAKAAhDVUVJAAABNWLbowo=", - want: SpliceInfo{ + want: Splice{ SAPType: SAPNone, CWIndex: 0xff, Tier: 0x0fff, @@ -65,7 +65,7 @@ var samples = []sample{ { name: "14.3. time_signal", encoded: "/DAvAAAAAAAA///wBQb+dGKQoAAZAhdDVUVJSAAAjn+fCAgAAAAALKChijUCAKnMZ1g=", - want: SpliceInfo{ + want: Splice{ SAPType: SAPNone, CWIndex: 0xff, Tier: 0x0fff, @@ -92,7 +92,7 @@ var samples = []sample{ { name: "14.4. time_signal", encoded: "/DBIAAAAAAAA///wBQb+ek2ItgAyAhdDVUVJSAAAGH+fCAgAAAAALMvDRBEAAAIXQ1VFSUgAABl/nwgIAAAAACyk26AQAACZcuND", - want: SpliceInfo{ + want: Splice{ SAPType: SAPNone, CWIndex: 0xff, Tier: 0x0fff, @@ -129,7 +129,7 @@ var samples = []sample{ // from "The Essential Guide to SCTE-35" by Bitmovin (https://bitmovin.com/scte-35-guide) name: "dtmf", encoded: "/DBcAAAAAAAAAP/wBQb//ciI8QBGAh1DVUVJXQk9EX+fAQ5FUDAxODAzODQwMDY2NiEEZAIZQ1VFSV0JPRF/3wABLit7AQVDMTQ2NDABAQEKQ1VFSQCAMTUwKnPhdcU=", - want: SpliceInfo{ + want: Splice{ SAPType: SAPNone, Tier: 0x0fff, Command: &Command{ diff --git a/scte35/splice.go b/scte35/splice.go @@ -0,0 +1,267 @@ +package scte35 + +import ( + "encoding/binary" + "fmt" +) + +// SAPType represents the two-bit field used to indicate that a Stream +// Access Point (SAP) in the stream +// as specified in SCTE 35 section 9.6.1. +type SAPType uint8 + +const ( + SAPClosedGOP SAPType = 0 + SAPClosedGOPLeading = 0x10 + SAPOpenGOP = 0x20 + SAPNone = 0x30 +) + +func (t SAPType) String() string { + switch t { + case SAPClosedGOP: + return "SAP Type 1 (closed GOP)" + case SAPClosedGOPLeading: + return "SAP Type 2 (closed GOP with leading pictures)" + case SAPOpenGOP: + return "SAP Type 3 (open GOP)" + case SAPNone: + return "none" + } + return "invalid" +} + +type Splice struct { + SAPType SAPType + Encrypted bool + Cipher Cipher + // Holds a 33-bit unsigned integer representing the number of ticks of a 90KHz clock. + PTSAdjustment uint64 + CWIndex uint8 + // Holds a 12-bit field representing an authorization tier. + Tier uint16 + Command *Command + Descriptors []SpliceDescriptor + CRC32 uint32 +} + +// fields of Splice Info Section which MUST have their values set... +// as specified in SCTE 35 section 9.6.1. +const ( + tableID uint8 = 0xfc + protocolVersion = 0x0 + sectionSyntax = false + privateIndicator = false +) + +// maximum 12-bit uint (2^12 - 1) +const maxTier uint16 = 0xfff + +func Encode(splice *Splice) ([]byte, error) { + buf := make([]byte, 4) + buf[0] = byte(tableID) + // next 2 bits (section_syntax_indicator, private_indicator) must be 0. + // 0b00000000 + buf[1] |= byte(splice.SAPType) + + // length, buf[1,2] set at the end + buf[3] = protocolVersion + + var b byte + if splice.Encrypted { + b |= (1 << 7) + if splice.Cipher > maxCipher { + return nil, fmt.Errorf("cipher %d larger than max %d", splice.Cipher, maxCipher) + } + // pack cipher, keeping 1 bit for PTSAdjustment. + b |= byte(splice.Cipher) << 1 + } + buf = append(buf, b) + buf = append(buf, 0, 0, 0, 0) + putPTS(buf[4:], splice.PTSAdjustment) + if splice.Encrypted { + buf = append(buf, splice.CWIndex) + } else { + // unused; toggle all bits as in the spec. + buf = append(buf, 0xff) + } + + if splice.Tier > maxTier { + return nil, fmt.Errorf("tier %d greater than max %d", splice.Tier, maxTier) + } + tier := splice.Tier & 0x0fff // just 12 bits + // right 4 bits are for command length + buf = binary.BigEndian.AppendUint16(buf, tier<<4) + if splice.Command == nil { + return nil, fmt.Errorf("nil command") + } + cmd, err := encodeCommand(splice.Command) + if err != nil { + return nil, fmt.Errorf("encode splice command: %w", err) + } + cmdlen := uint16(len(cmd)) & 0x0fff + // stuff remaining 4 bits into the last byte. + buf[len(buf)-1] |= byte(cmdlen >> 8) + buf = append(buf, byte(cmdlen)) + buf = append(buf, byte(splice.Command.Type)) + buf = append(buf, cmd...) + + var buf1 []byte + for _, desc := range splice.Descriptors { + buf1 = append(buf1, encodeSpliceDescriptor(desc)...) + } + buf = binary.BigEndian.AppendUint16(buf, uint16(len(buf1))) + buf = append(buf, buf1...) + + // want only 12 bits, left 4 bits are used by flags, saptype. + buflen := uint16(len(buf)) & 0x0fff + buflen++ // TODO(otl): is this required because of alignment stuffing? + buf[1] |= byte(buflen >> 8) + buf[2] = byte(buflen) + + crc := checksum(buf, &crctab) + return binary.BigEndian.AppendUint32(buf, crc), nil +} + +func Decode(buf []byte) (*Splice, error) { + if len(buf) < 3 { + return nil, fmt.Errorf("need at least 2 bytes") + } + // skip buf[0], we don't store table_id. + + var splice Splice + // skip 2 bits, straight to sap_type. + splice.SAPType = SAPType(buf[1] & 0b00110000) + length := binary.BigEndian.Uint16([]byte{buf[1], buf[2]}) + length &= 0x0fff // 12-bit field + buf = buf[3:] + if len(buf) != int(length) { + return nil, fmt.Errorf("message declares %d bytes but have %d", length, len(buf)) + } + + // skip version byte, we don't store version as it's constant. + if buf[1]&0b10000000 == 1 { + splice.Encrypted = true + // right-most bit is used by PTSAdjustment. + splice.Cipher = Cipher(buf[1] & 0b01111110) + } + + pts := make([]byte, 8) + pts[0] = buf[1] & (1 << 1) + pts[1] = buf[2] + pts[2] = buf[3] + pts[3] = buf[4] + pts[4] = buf[5] + splice.PTSAdjustment = binary.BigEndian.Uint64(pts) + splice.CWIndex = uint8(buf[6]) + + // want left-most 12 bits, remaining is used by command length. + // TODO(otl): still not getting expected values here; + // check TestDecode + tier := binary.BigEndian.Uint16([]byte{buf[7], buf[8] & 0xf0}) + splice.Tier = tier >> 4 + + // 4-bits out of buf[8], then all of buf[9] for a 12-bit integer. + cmdlen := binary.BigEndian.Uint16([]byte{buf[8] & 0x0f, buf[9]}) + cmd, err := decodeCommand(buf[10 : 10+cmdlen+1]) + if err != nil { + return nil, fmt.Errorf("decode command: %w", err) + } + splice.Command = cmd + buf = buf[10+cmdlen+1:] + + desclen := binary.BigEndian.Uint16([]byte{buf[0], buf[1]}) + descriptors, err := decodeAllDescriptors(buf[2 : 2+desclen]) + if err != nil { + return nil, fmt.Errorf("decode splice descriptors: %w", err) + } + splice.Descriptors = descriptors + + buf = buf[2+desclen:] + if splice.Encrypted { + // TODO(otl): handle alignment_stuffing for encrypted packets. + // skip past E_CRC_32; we don't store it. + buf = buf[1:] + } + splice.CRC32 = binary.BigEndian.Uint32(buf) + return &splice, nil +} + +func decodeCommand(buf []byte) (*Command, error) { + var cmd Command + cmd.Type = CommandType(buf[0]) + switch cmd.Type { + case SpliceNull, BandwidthReservation: + // nothing to decode + case TimeSignal: + // check if time specified flag is set. + // If so, extract the 33-bit integer timestamp. + if buf[1]&0x80 == 1<<7 { + b := make([]byte, 8) + b[3] = buf[1] & 0x01 // ignoring flag and reserved bits + b[4] = buf[2] + b[5] = buf[3] + b[6] = buf[4] + b[7] = buf[5] + t := binary.BigEndian.Uint64(b) + cmd.TimeSignal = &t + } + case SpliceInsert: + var ins Insert + ins.ID = binary.BigEndian.Uint32(buf[1:5]) + if buf[5]&0x80 > 0 { + ins.Cancel = true + cmd.Insert = &ins + // rebelelder told us to do this. + return &cmd, nil + } + if buf[6]&(1<<7) > 0 { + ins.OutOfNetwork = true + } + + // assume program_splice is set at bit 6; + + var durflag bool + if buf[6]&(1<<5) > 0 { + durflag = true + } + // we don't support deprecated component mode. + if buf[6]&(1<<4) > 0 { + ins.Immediate = true + } + if buf[6]&(1<<3) > 0 { + ins.idCompliance = true + } + // next 3 bits are reserved. + + if !ins.Immediate { + // is time_specified_flag set? if so, read the 33-bit time. + if buf[7]&(1<<7) > 0 { + b := make([]byte, 3) + b = append(b, buf[7]&0x01) // skip reserved bits. + b = append(b, buf[8:12]...) // read remaining 32 bits. + dur := binary.BigEndian.Uint64(b) + ins.SpliceTime = newuint64(dur) + buf = buf[12:] + } else { + buf = buf[8:] + } + } + + if durflag { + a := [5]byte{buf[0], buf[1], buf[2], buf[3], buf[4]} + ins.Duration = readBreakDuration(a) + buf = buf[5:] + } + + ins.ProgramID = binary.BigEndian.Uint16([]byte{buf[0], buf[1]}) + ins.AvailNum = uint8(buf[2]) + ins.AvailExpected = uint8(buf[3]) + cmd.Insert = &ins + default: + return nil, fmt.Errorf("TODO: cannot decode command type %s", cmd.Type) + } + return &cmd, nil +} + +func newuint64(i uint64) *uint64 { p := new(uint64); p = &i; return p } diff --git a/scte35/splice_info.go b/scte35/splice_info.go @@ -1,267 +0,0 @@ -package scte35 - -import ( - "encoding/binary" - "fmt" -) - -// SAPType represents the two-bit field used to indicate that a Stream -// Access Point (SAP) in the stream -// as specified in SCTE 35 section 9.6.1. -type SAPType uint8 - -const ( - SAPClosedGOP SAPType = 0 - SAPClosedGOPLeading = 0x10 - SAPOpenGOP = 0x20 - SAPNone = 0x30 -) - -func (t SAPType) String() string { - switch t { - case SAPClosedGOP: - return "SAP Type 1 (closed GOP)" - case SAPClosedGOPLeading: - return "SAP Type 2 (closed GOP with leading pictures)" - case SAPOpenGOP: - return "SAP Type 3 (open GOP)" - case SAPNone: - return "none" - } - return "invalid" -} - -type SpliceInfo struct { - SAPType SAPType - Encrypted bool - Cipher Cipher - // Holds a 33-bit unsigned integer representing the number of ticks of a 90KHz clock. - PTSAdjustment uint64 - CWIndex uint8 - // Holds a 12-bit field representing an authorization tier. - Tier uint16 - Command *Command - Descriptors []SpliceDescriptor - CRC32 uint32 -} - -// fields of Splice Info Section which MUST have their values set... -// as specified in SCTE 35 section 9.6.1. -const ( - tableID uint8 = 0xfc - protocolVersion = 0x0 - sectionSyntax = false - privateIndicator = false -) - -// maximum 12-bit uint (2^12 - 1) -const maxTier uint16 = 0xfff - -func EncodeSpliceInfo(info *SpliceInfo) ([]byte, error) { - buf := make([]byte, 4) - buf[0] = byte(tableID) - // next 2 bits (section_syntax_indicator, private_indicator) must be 0. - // 0b00000000 - buf[1] |= byte(info.SAPType) - - // length, buf[1,2] set at the end - buf[3] = protocolVersion - - var b byte - if info.Encrypted { - b |= (1 << 7) - if info.Cipher > maxCipher { - return nil, fmt.Errorf("cipher %d larger than max %d", info.Cipher, maxCipher) - } - // pack cipher, keeping 1 bit for PTSAdjustment. - b |= byte(info.Cipher) << 1 - } - buf = append(buf, b) - buf = append(buf, 0, 0, 0, 0) - putPTS(buf[4:], info.PTSAdjustment) - if info.Encrypted { - buf = append(buf, info.CWIndex) - } else { - // unused; toggle all bits as in the spec. - buf = append(buf, 0xff) - } - - if info.Tier > maxTier { - return nil, fmt.Errorf("tier %d greater than max %d", info.Tier, maxTier) - } - tier := info.Tier & 0x0fff // just 12 bits - // right 4 bits are for command length - buf = binary.BigEndian.AppendUint16(buf, tier<<4) - if info.Command == nil { - return nil, fmt.Errorf("nil command") - } - cmd, err := encodeCommand(info.Command) - if err != nil { - return nil, fmt.Errorf("encode splice command: %w", err) - } - cmdlen := uint16(len(cmd)) & 0x0fff - // stuff remaining 4 bits into the last byte. - buf[len(buf)-1] |= byte(cmdlen >> 8) - buf = append(buf, byte(cmdlen)) - buf = append(buf, byte(info.Command.Type)) - buf = append(buf, cmd...) - - var buf1 []byte - for _, desc := range info.Descriptors { - buf1 = append(buf1, encodeSpliceDescriptor(desc)...) - } - buf = binary.BigEndian.AppendUint16(buf, uint16(len(buf1))) - buf = append(buf, buf1...) - - // want only 12 bits, left 4 bits are used by flags, saptype. - buflen := uint16(len(buf)) & 0x0fff - buflen++ // TODO(otl): is this required because of alignment stuffing? - buf[1] |= byte(buflen >> 8) - buf[2] = byte(buflen) - - crc := checksum(buf, &crctab) - return binary.BigEndian.AppendUint32(buf, crc), nil -} - -func DecodeSpliceInfo(buf []byte) (*SpliceInfo, error) { - if len(buf) < 3 { - return nil, fmt.Errorf("need at least 2 bytes") - } - // skip buf[0], we don't store table_id. - - var info SpliceInfo - // skip 2 bits, straight to sap_type. - info.SAPType = SAPType(buf[1] & 0b00110000) - length := binary.BigEndian.Uint16([]byte{buf[1], buf[2]}) - length &= 0x0fff // 12-bit field - buf = buf[3:] - if len(buf) != int(length) { - return nil, fmt.Errorf("message declares %d bytes but have %d", length, len(buf)) - } - - // skip version byte, we don't store version as it's constant. - if buf[1]&0b10000000 == 1 { - info.Encrypted = true - // right-most bit is used by PTSAdjustment. - info.Cipher = Cipher(buf[1] & 0b01111110) - } - - pts := make([]byte, 8) - pts[0] = buf[1] & (1 << 1) - pts[1] = buf[2] - pts[2] = buf[3] - pts[3] = buf[4] - pts[4] = buf[5] - info.PTSAdjustment = binary.BigEndian.Uint64(pts) - info.CWIndex = uint8(buf[6]) - - // want left-most 12 bits, remaining is used by command length. - // TODO(otl): still not getting expected values here; - // check TestDecodeSpliceInfo - tier := binary.BigEndian.Uint16([]byte{buf[7], buf[8] & 0xf0}) - info.Tier = tier >> 4 - - // 4-bits out of buf[8], then all of buf[9] for a 12-bit integer. - cmdlen := binary.BigEndian.Uint16([]byte{buf[8] & 0x0f, buf[9]}) - cmd, err := decodeCommand(buf[10 : 10+cmdlen+1]) - if err != nil { - return nil, fmt.Errorf("decode command: %w", err) - } - info.Command = cmd - buf = buf[10+cmdlen+1:] - - desclen := binary.BigEndian.Uint16([]byte{buf[0], buf[1]}) - descriptors, err := decodeAllDescriptors(buf[2 : 2+desclen]) - if err != nil { - return nil, fmt.Errorf("decode splice descriptors: %w", err) - } - info.Descriptors = descriptors - - buf = buf[2+desclen:] - if info.Encrypted { - // TODO(otl): handle alignment_stuffing for encrypted packets. - // skip past E_CRC_32; we don't store it. - buf = buf[1:] - } - info.CRC32 = binary.BigEndian.Uint32(buf) - return &info, nil -} - -func decodeCommand(buf []byte) (*Command, error) { - var cmd Command - cmd.Type = CommandType(buf[0]) - switch cmd.Type { - case SpliceNull, BandwidthReservation: - // nothing to decode - case TimeSignal: - // check if time specified flag is set. - // If so, extract the 33-bit integer timestamp. - if buf[1]&0x80 == 1<<7 { - b := make([]byte, 8) - b[3] = buf[1] & 0x01 // ignoring flag and reserved bits - b[4] = buf[2] - b[5] = buf[3] - b[6] = buf[4] - b[7] = buf[5] - t := binary.BigEndian.Uint64(b) - cmd.TimeSignal = &t - } - case SpliceInsert: - var ins Insert - ins.ID = binary.BigEndian.Uint32(buf[1:5]) - if buf[5]&0x80 > 0 { - ins.Cancel = true - cmd.Insert = &ins - // rebelelder told us to do this. - return &cmd, nil - } - if buf[6]&(1<<7) > 0 { - ins.OutOfNetwork = true - } - - // assume program_splice is set at bit 6; - - var durflag bool - if buf[6]&(1<<5) > 0 { - durflag = true - } - // we don't support deprecated component mode. - if buf[6]&(1<<4) > 0 { - ins.Immediate = true - } - if buf[6]&(1<<3) > 0 { - ins.idCompliance = true - } - // next 3 bits are reserved. - - if !ins.Immediate { - // is time_specified_flag set? if so, read the 33-bit time. - if buf[7]&(1<<7) > 0 { - b := make([]byte, 3) - b = append(b, buf[7]&0x01) // skip reserved bits. - b = append(b, buf[8:12]...) // read remaining 32 bits. - dur := binary.BigEndian.Uint64(b) - ins.SpliceTime = newuint64(dur) - buf = buf[12:] - } else { - buf = buf[8:] - } - } - - if durflag { - a := [5]byte{buf[0], buf[1], buf[2], buf[3], buf[4]} - ins.Duration = readBreakDuration(a) - buf = buf[5:] - } - - ins.ProgramID = binary.BigEndian.Uint16([]byte{buf[0], buf[1]}) - ins.AvailNum = uint8(buf[2]) - ins.AvailExpected = uint8(buf[3]) - cmd.Insert = &ins - default: - return nil, fmt.Errorf("TODO: cannot decode command type %s", cmd.Type) - } - return &cmd, nil -} - -func newuint64(i uint64) *uint64 { p := new(uint64); p = &i; return p } diff --git a/scte35/splice_info_test.go b/scte35/splice_info_test.go @@ -1,167 +0,0 @@ -package scte35 - -import ( - "encoding/base64" - "fmt" - "reflect" - "strings" - "testing" - "time" -) - -func diffInfo(a, b SpliceInfo) string { - buf := &strings.Builder{} - if a.SAPType != b.SAPType { - fmt.Fprintln(buf, "SAP type differs") - fmt.Fprintf(buf, "< %s\n> %s\n", a.SAPType, b.SAPType) - } - if a.Cipher != b.Cipher { - fmt.Fprintln(buf, "cipher differs") - fmt.Fprintf(buf, "< %s\n> %s", a.Cipher, b.Cipher) - } - if a.PTSAdjustment != b.PTSAdjustment { - fmt.Fprintln(buf, "pts adjustment = ", a.PTSAdjustment, b.PTSAdjustment) - } - if a.CWIndex != b.CWIndex { - fmt.Fprintln(buf, "cw index differs") - fmt.Fprintf(buf, "< %v\n> %v\n", a.CWIndex, b.CWIndex) - } - if a.Tier != b.Tier { - fmt.Fprintln(buf, "tier differs") - fmt.Fprintf(buf, "< %#x\n> %#x\n", a.Tier, b.Tier) - } - if !reflect.DeepEqual(a.Command, b.Command) { - fmt.Fprintln(buf, "command = ", *a.Command, *b.Command) - } - for i := range a.Descriptors { - if !reflect.DeepEqual(a.Descriptors[i], b.Descriptors[i]) { - buf.WriteString(diffDescriptors(a.Descriptors[i], b.Descriptors[i])) - } - } - if a.CRC32 != b.CRC32 { - fmt.Fprintln(buf, "crc32 = ", a.CRC32, b.CRC32) - } - return buf.String() -} - -func diffDescriptors(a, b SpliceDescriptor) string { - buf := &strings.Builder{} - if a.Tag() != b.Tag() { - fmt.Fprintln(buf, "tag differs") - fmt.Fprintln(buf, "<", a.Tag()) - fmt.Fprintln(buf, ">", b.Tag()) - } - if a.ID() != b.ID() { - fmt.Fprintln(buf, "id differs") - fmt.Fprintf(buf, "< %d\n> %d\n", a.ID(), b.ID()) - } - if !reflect.DeepEqual(a.Data(), b.Data()) { - fmt.Fprintln(buf, "data differs") - fmt.Fprintf(buf, "< %v\n> %v\n", a.Data(), b.Data()) - } - fmt.Fprintf(buf, "< %T %v\n> %T %v\n", a, a, b, b) - return buf.String() -} - -func TestDecodeSpliceInfo(t *testing.T) { - for _, tt := range samples { - t.Run(tt.name, func(t *testing.T) { - b, err := base64.StdEncoding.DecodeString(tt.encoded) - if err != nil { - t.Fatal("decode example splice info:", err) - } - info, err := DecodeSpliceInfo(b) - if err != nil { - t.Fatalf("decode splice info: %v", err) - } - - // test each possible command - if tt.want.Command.TimeSignal != nil { - if *tt.want.Command.TimeSignal != *info.Command.TimeSignal { - t.Errorf("want timesig %x, got %x", *tt.want.Command.TimeSignal, *info.Command.TimeSignal) - } - } - if tt.want.Command.Insert != nil { - want := *tt.want.Command.Insert - got := *info.Command.Insert - if !reflect.DeepEqual(want, got) { - t.Errorf("info command: want %+v, got %+v", want, got) - if *want.SpliceTime != *got.SpliceTime { - t.Logf("want splice time %d, got %d", want.SpliceTime, got.SpliceTime) - } - if want.Duration.Duration != got.Duration.Duration { - t.Logf("want break duration %d, got %d", want.Duration.Duration, got.Duration.Duration) - } - } - } - - if !reflect.DeepEqual(tt.want, *info) { - t.Errorf("decode splice info: want %+v, got %+v", tt.want, *info) - t.Log(diffInfo(tt.want, *info)) - } - }) - } - - // these messages are from github.com/futzu/SCTE-35_threefive/examples/hls/ - inserts := map[string]time.Duration{ - "/DAlAAAAAAAAAP/wFAUAAAABf+/+ANgNkv4AFJlwAAEBAQAA5xULLA==": 15 * time.Second, - "/DAnAAAAAAAAAP/wBQb+AA27oAARAg9DVUVJAAAAAX+HCQA0AAE0xUZn": 10 * time.Second, - "/DAnAAAAAAAAAP/wBQb+AGb/MAARAg9DVUVJAAAAAn+HCQA0AALMua1L": 75 * time.Second, - } - for s, dur := range inserts { - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - t.Fatal(err) - } - splice, err := DecodeSpliceInfo(b) - if err != nil { - t.Fatalf("decode splice info: %v", err) - } - var got time.Duration - switch splice.Command.Type { - case TimeSignal: - got = time.Duration(*splice.Command.TimeSignal/90000) * time.Second - case SpliceInsert: - got = time.Duration(splice.Command.Insert.Duration.Duration/90000) * time.Second - default: - t.Fatalf("no duration test supported for %s", splice.Command.Type) - } - if got != dur { - t.Errorf("want %s, got %s", dur, got) - } - } -} - -func TestEncodeSpliceInfo(t *testing.T) { - for _, tt := range samples { - t.Run(tt.name, func(t *testing.T) { - b, err := EncodeSpliceInfo(&tt.want) - if err != nil { - t.Fatal(err) - } - bwant, err := base64.StdEncoding.DecodeString(tt.encoded) - if err != nil { - t.Fatal(err) - } - // If we're not encrypted, set the CWIndex to be - // the same as desired; its value is now undefined - // and should be ignored downstream. This lets our - // test pass even if our test encoded value has a - // different CWIndex set than what we encode. - if !tt.want.Encrypted { - b[9] = bwant[9] - } - got := base64.StdEncoding.EncodeToString(b) - if tt.encoded != got { - // as above, since the undefined CWIndex is encoded differently, - // our checksum could be different. - // Only error if the value of the message *without* the CRC32 is different. - if tt.encoded[:len(tt.encoded)-7] != got[:len(got)-7] { - t.Errorf("expected encoded splice info differs from calculated") - } - t.Logf("< %#x", bwant) - t.Logf("> %#x", b) - } - }) - } -} diff --git a/scte35/splice_test.go b/scte35/splice_test.go @@ -0,0 +1,167 @@ +package scte35 + +import ( + "encoding/base64" + "fmt" + "reflect" + "strings" + "testing" + "time" +) + +func diffInfo(a, b Splice) string { + buf := &strings.Builder{} + if a.SAPType != b.SAPType { + fmt.Fprintln(buf, "SAP type differs") + fmt.Fprintf(buf, "< %s\n> %s\n", a.SAPType, b.SAPType) + } + if a.Cipher != b.Cipher { + fmt.Fprintln(buf, "cipher differs") + fmt.Fprintf(buf, "< %s\n> %s", a.Cipher, b.Cipher) + } + if a.PTSAdjustment != b.PTSAdjustment { + fmt.Fprintln(buf, "pts adjustment = ", a.PTSAdjustment, b.PTSAdjustment) + } + if a.CWIndex != b.CWIndex { + fmt.Fprintln(buf, "cw index differs") + fmt.Fprintf(buf, "< %v\n> %v\n", a.CWIndex, b.CWIndex) + } + if a.Tier != b.Tier { + fmt.Fprintln(buf, "tier differs") + fmt.Fprintf(buf, "< %#x\n> %#x\n", a.Tier, b.Tier) + } + if !reflect.DeepEqual(a.Command, b.Command) { + fmt.Fprintln(buf, "command = ", *a.Command, *b.Command) + } + for i := range a.Descriptors { + if !reflect.DeepEqual(a.Descriptors[i], b.Descriptors[i]) { + buf.WriteString(diffDescriptors(a.Descriptors[i], b.Descriptors[i])) + } + } + if a.CRC32 != b.CRC32 { + fmt.Fprintln(buf, "crc32 = ", a.CRC32, b.CRC32) + } + return buf.String() +} + +func diffDescriptors(a, b SpliceDescriptor) string { + buf := &strings.Builder{} + if a.Tag() != b.Tag() { + fmt.Fprintln(buf, "tag differs") + fmt.Fprintln(buf, "<", a.Tag()) + fmt.Fprintln(buf, ">", b.Tag()) + } + if a.ID() != b.ID() { + fmt.Fprintln(buf, "id differs") + fmt.Fprintf(buf, "< %d\n> %d\n", a.ID(), b.ID()) + } + if !reflect.DeepEqual(a.Data(), b.Data()) { + fmt.Fprintln(buf, "data differs") + fmt.Fprintf(buf, "< %v\n> %v\n", a.Data(), b.Data()) + } + fmt.Fprintf(buf, "< %T %v\n> %T %v\n", a, a, b, b) + return buf.String() +} + +func TestDecode(t *testing.T) { + for _, tt := range samples { + t.Run(tt.name, func(t *testing.T) { + b, err := base64.StdEncoding.DecodeString(tt.encoded) + if err != nil { + t.Fatal("decode example splice:", err) + } + splice, err := Decode(b) + if err != nil { + t.Fatalf("decode splice: %v", err) + } + + // test each possible command + if tt.want.Command.TimeSignal != nil { + if *tt.want.Command.TimeSignal != *splice.Command.TimeSignal { + t.Errorf("want timesig %x, got %x", *tt.want.Command.TimeSignal, *splice.Command.TimeSignal) + } + } + if tt.want.Command.Insert != nil { + want := *tt.want.Command.Insert + got := *splice.Command.Insert + if !reflect.DeepEqual(want, got) { + t.Errorf("splice command: want %+v, got %+v", want, got) + if *want.SpliceTime != *got.SpliceTime { + t.Logf("want splice time %d, got %d", want.SpliceTime, got.SpliceTime) + } + if want.Duration.Duration != got.Duration.Duration { + t.Logf("want break duration %d, got %d", want.Duration.Duration, got.Duration.Duration) + } + } + } + + if !reflect.DeepEqual(tt.want, *splice) { + t.Errorf("decode splice splice: want %+v, got %+v", tt.want, *splice) + t.Log(diffInfo(tt.want, *splice)) + } + }) + } + + // these messages are from github.com/futzu/SCTE-35_threefive/examples/hls/ + inserts := map[string]time.Duration{ + "/DAlAAAAAAAAAP/wFAUAAAABf+/+ANgNkv4AFJlwAAEBAQAA5xULLA==": 15 * time.Second, + "/DAnAAAAAAAAAP/wBQb+AA27oAARAg9DVUVJAAAAAX+HCQA0AAE0xUZn": 10 * time.Second, + "/DAnAAAAAAAAAP/wBQb+AGb/MAARAg9DVUVJAAAAAn+HCQA0AALMua1L": 75 * time.Second, + } + for s, dur := range inserts { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + t.Fatal(err) + } + splice, err := Decode(b) + if err != nil { + t.Fatalf("decode splice splice: %v", err) + } + var got time.Duration + switch splice.Command.Type { + case TimeSignal: + got = time.Duration(*splice.Command.TimeSignal/90000) * time.Second + case SpliceInsert: + got = time.Duration(splice.Command.Insert.Duration.Duration/90000) * time.Second + default: + t.Fatalf("no duration test supported for %s", splice.Command.Type) + } + if got != dur { + t.Errorf("want %s, got %s", dur, got) + } + } +} + +func TestEncode(t *testing.T) { + for _, tt := range samples { + t.Run(tt.name, func(t *testing.T) { + b, err := Encode(&tt.want) + if err != nil { + t.Fatal(err) + } + bwant, err := base64.StdEncoding.DecodeString(tt.encoded) + if err != nil { + t.Fatal(err) + } + // If we're not encrypted, set the CWIndex to be + // the same as desired; its value is now undefined + // and should be ignored downstream. This lets our + // test pass even if our test encoded value has a + // different CWIndex set than what we encode. + if !tt.want.Encrypted { + b[9] = bwant[9] + } + got := base64.StdEncoding.EncodeToString(b) + if tt.encoded != got { + // as above, since the undefined CWIndex is encoded differently, + // our checksum could be different. + // Only error if the value of the message *without* the CRC32 is different. + if tt.encoded[:len(tt.encoded)-7] != got[:len(got)-7] { + t.Errorf("expected encoded splice differs from calculated") + } + t.Logf("< %#x", bwant) + t.Logf("> %#x", b) + } + }) + } +}