streaming

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

commit ab15f9243df870d2643b2c31f6da896fe2adcec9
parent 259fb85f2b3254aa6e5683141dde31026b211372
Author: Oliver Lowe <o@olowe.co>
Date:   Tue, 14 May 2024 14:45:17 +1000

scte35: publish to root of repo

By exporting just two functions, for now: Decode/Encode splices.
Still more documentation and more testing to be done.

Diffstat:
Dinternal/scte35/README.md | 24------------------------
Dinternal/scte35/splice_descriptor.go | 381-------------------------------------------------------------------------------
Dinternal/scte35/splice_info.go | 265-------------------------------------------------------------------------------
Dinternal/scte35/splice_info_test.go | 167-------------------------------------------------------------------------------
Rinternal/scte35/LICENSE -> scte35/LICENSE | 0
Rinternal/scte35/LICENSE.ISC -> scte35/LICENSE.ISC | 0
Rinternal/scte35/NOTICE.md -> scte35/NOTICE.md | 0
Ascte35/README.md | 10++++++++++
Rinternal/scte35/break_duration.go -> scte35/break_duration.go | 0
Rinternal/scte35/break_duration_test.go -> scte35/break_duration_test.go | 0
Rinternal/scte35/cipher.go -> scte35/cipher.go | 0
Rinternal/scte35/cipher_test.go -> scte35/cipher_test.go | 0
Rinternal/scte35/command.go -> scte35/command.go | 0
Rinternal/scte35/crc_32.go -> scte35/crc_32.go | 0
Rinternal/scte35/pts.go -> scte35/pts.go | 0
Rinternal/scte35/pts_test.go -> scte35/pts_test.go | 0
Rinternal/scte35/scte35_test.go -> scte35/scte35_test.go | 0
Ascte35/splice_descriptor.go | 381+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascte35/splice_info.go | 265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascte35/splice_info_test.go | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rinternal/scte35/splice_schedule_test.go -> scte35/splice_schedule_test.go | 0
21 files changed, 823 insertions(+), 837 deletions(-)

diff --git a/internal/scte35/README.md b/internal/scte35/README.md @@ -1,24 +0,0 @@ -# scte35-go: ANSI/SCTE 35 Decoder/Encoder - -This repository is a fork of [github.com/Comcast/scte35-go@ba13aa8](https://github.com/Comcast/scte35-go) - -Notable differences from the original works are: - -- third party dependencies removed -- refactored test code -- supports only binary/base64 encoding, decoding - -## License - -Unless otherwise noted, this software is -Copyright 2021 Comcast Cable Communications Management, LLC -licensed under the Apache License, Version 2.0. -See NOTICE.md. - -Derivative work by The Untangled Authors is licensed under the ISC License. -See LICENSE.ISC. - -## Code of Conduct - -Contributors to this repository agree to the -[Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.txt) diff --git a/internal/scte35/splice_descriptor.go b/internal/scte35/splice_descriptor.go @@ -1,381 +0,0 @@ -package scte35 - -import ( - "encoding/binary" - "fmt" -) - -const DescriptorIDCUEI = "CUEI" - -// DescriptorIDCUEI in ASCII -const descriptorIDCUEI uint32 = 0x43554549 - -const ( - TagAvail uint8 = iota - TagDTMF - TagSegmentation - TagTime - TagAudio -) - -type SpliceDescriptor interface { - // Tag identifies the type of descriptor. If ID is - // DescriptorIDCUEI, then the the values [TagAvail] et al. may be used. - Tag() uint8 - // For private descriptors, this value must not be DescriptorIDCUEI. - ID() uint32 - // Data is the encoded splice descriptor implementation. - // If Tag is one of [TagAvail] et al., then the corresponding - // types (AvailDescriptor, DTMFDescriptor...) may be used to - // decode/encode this field. - Data() []byte -} - -func encodeSpliceDescriptor(sd SpliceDescriptor) []byte { - var buf []byte - buf = append(buf, byte(sd.Tag())) - buf = append(buf, byte(len(sd.Data())+4)) // len(sd.ID()) == 4 - buf = binary.BigEndian.AppendUint32(buf, sd.ID()) - return append(buf, sd.Data()...) -} - -// AvailDescriptor is a type of splice descriptor described in SCTE 35 section 10.3.1. -// Its only value is a so-called "provider avail ID". -type AvailDescriptor uint32 - -func (d AvailDescriptor) Tag() uint8 { return TagAvail } -func (d AvailDescriptor) ID() uint32 { return descriptorIDCUEI } - -func (d AvailDescriptor) Data() []byte { - buf := make([]byte, 4) - binary.BigEndian.PutUint32(buf, uint32(d)) - return buf -} - -// DTMFDescriptor is a type of a splice descriptor as described in SCTE 35 10.3.2. -// DTMF stands for [Dual-tone multi-frequency signaling]. -// -// [Dual-tone multi-frequency signaling]: https://en.wikipedia.org/wiki/DTMF -type DTMFDescriptor struct { - Preroll uint8 - // Chars holds a DTMF sequence whose values may only - // consist of the ASCII values of '0' through '9', '*', and '#'. - Chars []byte -} - -func (d DTMFDescriptor) Tag() uint8 { return TagDTMF } -func (d DTMFDescriptor) ID() uint32 { return descriptorIDCUEI } - -func (d DTMFDescriptor) Data() []byte { - // preroll + char count + chars - b := make([]byte, 1+1+len(d.Chars)) - b[0] = byte(d.Preroll) - // set 3 bits, right-most 5 are reserved. - b[1] = byte(len(d.Chars)) << 5 - copy(b[2:], d.Chars) - return b -} - -func unmarshalDTMF(buf []byte) DTMFDescriptor { - // skip buf[1]; contains the length which we don't care about when using slices. - return DTMFDescriptor{ - Preroll: uint8(buf[0]), - Chars: buf[2:], - } -} - -type DeliveryRestrictions uint8 - -const ( - WebDeliveryAllowed DeliveryRestrictions = 1<<4 - iota - NoRegionalBlackout - ArchiveAllowed - DeviceRestrictGroup0 = 0x00 - DeviceRestrictGroup1 = 0x01 - DeviceRestrictGroup2 = 0x02 - DeviceRestrictionsNone = 0x03 -) - -// SegmentationDescriptor represents the segmentation_descriptor -// structure defined in SCTE 35 section 10.3.3. -type SegmentationDescriptor struct { - EventID uint32 - Cancel bool - EventIDCompliance bool - Restrictions DeliveryRestrictions - // 40-bit integer representing the number of ticks of a 90KHz clock. - Duration *uint64 - UPID UPID - // Valid types are specified in Table 23, SCTE 35 section 10.3.3.1. - Type uint8 - // The numbered index of this descriptor in a collection of descriptors. - Number uint8 - // Expected count of descriptors. - Expected uint8 - // Numbered index of any subsegment of this descriptor. - SubNumber uint8 - // Expected count of subsegments. - SubExpected uint8 -} - -func (d SegmentationDescriptor) Tag() uint8 { return TagSegmentation } -func (d SegmentationDescriptor) ID() uint32 { return descriptorIDCUEI } - -func (d SegmentationDescriptor) Data() []byte { - buf := make([]byte, 5) - binary.BigEndian.PutUint32(buf[:4], d.EventID) - if d.Cancel { - buf[4] |= (1 << 7) - } - if d.EventIDCompliance { - buf[4] |= (1 << 6) - } - // toggle next remaining 6 reserved bits. - buf[4] |= 0b00111111 - - if !d.Cancel { - buf = append(buf, segDescFlags(&d)) - if d.Duration != nil { - b := make([]byte, 8) // uint64 needs 8 - binary.BigEndian.PutUint64(b, *d.Duration<<24) // 40 bits - // append 40 bits (5 bytes) - buf = append(buf, b[:5]...) - } - - buf = append(buf, byte(d.UPID.Type)) - buf = append(buf, uint8(len(d.UPID.Value))) - buf = append(buf, d.UPID.Value...) - - buf = append(buf, byte(d.Type), byte(d.Number), byte(d.Expected)) - switch d.Type { - // TODO(otl): use named constants from section 10.3.3.1 Table 23 - segmentation_type_id - case 0x34, 0x30, 0x32, 0x36, 0x38, 0x3a, 0x44, 0x46: - if d.SubNumber > 0 { - buf = append(buf, d.SubNumber) - } - if d.SubExpected > 0 { - buf = append(buf, d.SubExpected) - } - } - } - return buf -} - -func unmarshalSegDescriptor(buf []byte) SegmentationDescriptor { - var desc SegmentationDescriptor - desc.EventID = binary.BigEndian.Uint32(buf[:4]) - if buf[4]&0b10000000 > 0 { - desc.Cancel = true - } - if buf[4]&0b01000000 > 0 { - desc.EventIDCompliance = true - } - // next 6 bits are reserved - - // always assume program_segmentation_flag is set at 0b10000000 - // we don't support the deprecated component mode. - - if !desc.Cancel { - // left-most 2 bits are flags for later. - desc.Restrictions = DeliveryRestrictions(buf[5] & 0b00111111) - - // is segmentation duration flag set? - if buf[5]&0b01000000 > 0 { - b := make([]byte, 3) - b = append(b, buf[6:11]...) - dur := binary.BigEndian.Uint64(b) - desc.Duration = &dur - buf = buf[11:] - } else { - buf = buf[6:] - } - - uplen := int(buf[1]) - desc.UPID = UPID{ - Type: UPIDType(buf[0]), - Value: buf[2 : 2+uplen], - } - buf = buf[2+uplen:] - - // TODO(otl): use named constants from section 10.3.3.1 Table 23 - segmentation_type_id - desc.Type = uint8(buf[0]) - desc.Number = uint8(buf[1]) - desc.Expected = uint8(buf[2]) - switch desc.Type { - // TODO(otl): use named constants from section 10.3.3.1 Table 23 - segmentation_type_id - case 0x34, 0x30, 0x32, 0x36, 0x38, 0x3a, 0x44, 0x46: - if len(buf[2:]) > 1 { - desc.SubNumber = uint8(buf[3]) - } - if len(buf[2:]) > 2 { - desc.SubExpected = uint8(buf[4]) - } - } - } - - return desc -} - -// UPID represents a segmentation_upid structure as specified in SCTE 35 section 10.3.3.1. -type UPID struct { - Type UPIDType - // Value holds the corresponding encoded contents for this UPID's Type. - // Possible values are given in Table 22 of section 10.3.3.1. - Value []byte -} - -// UPIDType represents a Segmentation UPID type as defined in SCTE 35 section 10.3.3.1. -type UPIDType uint8 - -// Valid UPIDType values defined in Table 22, SCTE 35 section 10.3.3.1. -const ( - UPIDNone UPIDType = 0 + iota - _ // User Defined, deprecated, use MPU. - _ // ISCI, deprecated, use AdID. - UPIDAdID - UPIDUMID - _ // ISAN, deprecated, use ISAN. - UPIDISAN - UPIDTID - UPIDTI - UPIDADI - UPIDEIDR - UPIDATSCContentID - UPIDMPU - UPIDMID - UPIDADSInfo - UPIDURI - UPIDUUID - UPIDSCR - UPIDReserved -) - -// TimeDescriptor represents a moment in time as used in the Precision -// Time Protocol (PTP). PTP uses International Atomic Time (TAI) rather -// than UTC time as in NTP. -type TimeDescriptor struct { - // A 48-bit integer of the number of seconds since the Unix - // epoch according to TAI. - Seconds uint64 - // Number of nanoseconds... - Nanoseconds uint32 - // The current number of seconds between NTP time and - // TAI for a single instance of time. - UTCOffset uint16 -} - -func (d TimeDescriptor) Tag() uint8 { return TagTime } -func (d TimeDescriptor) ID() uint32 { return descriptorIDCUEI } - -func (d TimeDescriptor) Data() []byte { - // 48 bits + 32 bits + 16 bits - b := make([]byte, 0, 6+4+2) - b = binary.BigEndian.AppendUint64(b, d.Seconds) - b = b[:6] // only want 48-bits - b = binary.BigEndian.AppendUint32(b, d.Nanoseconds) - return binary.BigEndian.AppendUint16(b, d.UTCOffset) -} - -type AudioChannel struct { - ComponentTag uint8 - // A language code from ISO 639-2. - Language [3]byte - // A 3-bit integer from ATSC A/52 Table 5.7. - BitstreamMode uint8 - // Number of channels as a 4-bit integer, from ATSC A/52 Table A4.5. - Count uint8 - FullService bool -} - -type AudioDescriptor []AudioChannel - -func (d AudioDescriptor) Tag() uint8 { return TagAudio } -func (d AudioDescriptor) ID() uint32 { return descriptorIDCUEI } - -func (d AudioDescriptor) Data() []byte { - var b []byte - count := len(d) - b = append(b, byte(count<<4)) // right-most 4 bits are reserved - for _, ch := range d { - b = append(b, ch.ComponentTag) - b = append(b, ch.Language[:]...) - var c byte - c |= (ch.BitstreamMode << 5) // set left 3 bits - c |= (ch.Count & 0x0f) // only want 4 bits - if ch.FullService { - c |= 0x01 // set last remaining bit - } - b = append(b, c) - } - return b -} - -func DecodeAllDescriptors(buf []byte) ([]SpliceDescriptor, error) { - var sds []SpliceDescriptor - for len(buf) >= 6 { - // first byte is tag, second is length of next descriptor. - dlen := uint8(buf[1]) - desc, err := UnmarshalSpliceDescriptor(buf[:2+dlen]) - if err != nil { - return sds, err - } - sds = append(sds, desc) - if int(dlen) >= len(buf) { - break - } - buf = buf[2+dlen:] - } - return sds, nil -} - -// UnmarshalSpliceDescriptor reads exactly one descriptor from buf. -func UnmarshalSpliceDescriptor(buf []byte) (SpliceDescriptor, error) { - if len(buf) < 6 { - return nil, fmt.Errorf("short slice: need at least 5 bytes") - } - tag := uint8(buf[0]) - length := uint8(buf[1]) - if len(buf[2:]) != int(length) { - return nil, fmt.Errorf("need %d bytes, have %d", int(length), len(buf[2:])) - } - buf = buf[2 : 2+length] - id := binary.BigEndian.Uint32(buf[:4]) - buf = buf[4:] - if id != descriptorIDCUEI { - return PrivateDescriptor{tag, id, buf}, nil - } - switch tag { - case TagAvail: - return AvailDescriptor(binary.BigEndian.Uint32(buf)), nil - case TagSegmentation: - return unmarshalSegDescriptor(buf), nil - case TagDTMF: - return unmarshalDTMF(buf), nil - } - return nil, fmt.Errorf("unmarshal %d unsupported", tag) -} - -type PrivateDescriptor struct { - PTag uint8 - PID uint32 - PData []byte -} - -func (d PrivateDescriptor) Tag() uint8 { return d.PTag } -func (d PrivateDescriptor) ID() uint32 { return d.PID } -func (d PrivateDescriptor) Data() []byte { return d.PData } - -func segDescFlags(seg *SegmentationDescriptor) uint8 { - var b uint8 - // assume program_segmentation is always set; we do not support the deprecated component mode. - b |= (1 << 7) - if seg.Duration != nil { - b |= (1 << 6) - } - if seg.Restrictions != 0 { - b |= byte(seg.Restrictions) - } else { - b |= (1 << 5) - } - return b -} diff --git a/internal/scte35/splice_info.go b/internal/scte35/splice_info.go @@ -1,265 +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(sis *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(sis.SAPType) - - // length, buf[1,2] set at the end - buf[3] = protocolVersion - - var b byte - if sis.Encrypted { - b |= (1 << 7) - if sis.Cipher > maxCipher { - return nil, fmt.Errorf("cipher %d larger than max %d", sis.Cipher, maxCipher) - } - // pack cipher, keeping 1 bit for PTSAdjustment. - b |= byte(sis.Cipher) << 1 - } - buf = append(buf, b) - buf = append(buf, 0, 0, 0, 0) - putPTS(buf[4:], sis.PTSAdjustment) - if sis.Encrypted { - buf = append(buf, sis.CWIndex) - } else { - // unused; toggle all bits as in the spec. - buf = append(buf, 0xff) - } - - if sis.Tier > maxTier { - return nil, fmt.Errorf("tier %d greater than max %d", sis.Tier, maxTier) - } - tier := sis.Tier & 0x0fff // just 12 bits - // right 4 bits are for command length - buf = binary.BigEndian.AppendUint16(buf, tier<<4) - if sis.Command == nil { - return nil, fmt.Errorf("nil command") - } - cmd, err := encodeCommand(sis.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(sis.Command.Type)) - buf = append(buf, cmd...) - - var buf1 []byte - for _, desc := range sis.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 := calculateCRC32(buf) - 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.EventIDCompliance = 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 -} diff --git a/internal/scte35/splice_info_test.go b/internal/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/internal/scte35/LICENSE b/scte35/LICENSE diff --git a/internal/scte35/LICENSE.ISC b/scte35/LICENSE.ISC diff --git a/internal/scte35/NOTICE.md b/scte35/NOTICE.md diff --git a/scte35/README.md b/scte35/README.md @@ -0,0 +1,10 @@ +# License + +Unless otherwise noted, this software is +by The Untangled Authors is licensed under the ISC License. +See LICENSE.ISC. + +The file crc_32.go is +Copyright 2021 Comcast Cable Communications Management, LLC +licensed under the Apache License, Version 2.0. +See NOTICE.md and LICENSE. diff --git a/internal/scte35/break_duration.go b/scte35/break_duration.go diff --git a/internal/scte35/break_duration_test.go b/scte35/break_duration_test.go diff --git a/internal/scte35/cipher.go b/scte35/cipher.go diff --git a/internal/scte35/cipher_test.go b/scte35/cipher_test.go diff --git a/internal/scte35/command.go b/scte35/command.go diff --git a/internal/scte35/crc_32.go b/scte35/crc_32.go diff --git a/internal/scte35/pts.go b/scte35/pts.go diff --git a/internal/scte35/pts_test.go b/scte35/pts_test.go diff --git a/internal/scte35/scte35_test.go b/scte35/scte35_test.go diff --git a/scte35/splice_descriptor.go b/scte35/splice_descriptor.go @@ -0,0 +1,381 @@ +package scte35 + +import ( + "encoding/binary" + "fmt" +) + +const DescriptorIDCUEI = "CUEI" + +// DescriptorIDCUEI in ASCII +const descriptorIDCUEI uint32 = 0x43554549 + +const ( + TagAvail uint8 = iota + TagDTMF + TagSegmentation + TagTime + TagAudio +) + +type SpliceDescriptor interface { + // Tag identifies the type of descriptor. If ID is + // DescriptorIDCUEI, then the the values [TagAvail] et al. may be used. + Tag() uint8 + // For private descriptors, this value must not be DescriptorIDCUEI. + ID() uint32 + // Data is the encoded splice descriptor implementation. + // If Tag is one of [TagAvail] et al., then the corresponding + // types (AvailDescriptor, DTMFDescriptor...) may be used to + // decode/encode this field. + Data() []byte +} + +func encodeSpliceDescriptor(sd SpliceDescriptor) []byte { + var buf []byte + buf = append(buf, byte(sd.Tag())) + buf = append(buf, byte(len(sd.Data())+4)) // len(sd.ID()) == 4 + buf = binary.BigEndian.AppendUint32(buf, sd.ID()) + return append(buf, sd.Data()...) +} + +// AvailDescriptor is a type of splice descriptor described in SCTE 35 section 10.3.1. +// Its only value is a so-called "provider avail ID". +type AvailDescriptor uint32 + +func (d AvailDescriptor) Tag() uint8 { return TagAvail } +func (d AvailDescriptor) ID() uint32 { return descriptorIDCUEI } + +func (d AvailDescriptor) Data() []byte { + buf := make([]byte, 4) + binary.BigEndian.PutUint32(buf, uint32(d)) + return buf +} + +// DTMFDescriptor is a type of a splice descriptor as described in SCTE 35 10.3.2. +// DTMF stands for [Dual-tone multi-frequency signaling]. +// +// [Dual-tone multi-frequency signaling]: https://en.wikipedia.org/wiki/DTMF +type DTMFDescriptor struct { + Preroll uint8 + // Chars holds a DTMF sequence whose values may only + // consist of the ASCII values of '0' through '9', '*', and '#'. + Chars []byte +} + +func (d DTMFDescriptor) Tag() uint8 { return TagDTMF } +func (d DTMFDescriptor) ID() uint32 { return descriptorIDCUEI } + +func (d DTMFDescriptor) Data() []byte { + // preroll + char count + chars + b := make([]byte, 1+1+len(d.Chars)) + b[0] = byte(d.Preroll) + // set 3 bits, right-most 5 are reserved. + b[1] = byte(len(d.Chars)) << 5 + copy(b[2:], d.Chars) + return b +} + +func unmarshalDTMF(buf []byte) DTMFDescriptor { + // skip buf[1]; contains the length which we don't care about when using slices. + return DTMFDescriptor{ + Preroll: uint8(buf[0]), + Chars: buf[2:], + } +} + +type DeliveryRestrictions uint8 + +const ( + WebDeliveryAllowed DeliveryRestrictions = 1<<4 - iota + NoRegionalBlackout + ArchiveAllowed + DeviceRestrictGroup0 = 0x00 + DeviceRestrictGroup1 = 0x01 + DeviceRestrictGroup2 = 0x02 + DeviceRestrictionsNone = 0x03 +) + +// SegmentationDescriptor represents the segmentation_descriptor +// structure defined in SCTE 35 section 10.3.3. +type SegmentationDescriptor struct { + EventID uint32 + Cancel bool + EventIDCompliance bool + Restrictions DeliveryRestrictions + // 40-bit integer representing the number of ticks of a 90KHz clock. + Duration *uint64 + UPID UPID + // Valid types are specified in Table 23, SCTE 35 section 10.3.3.1. + Type uint8 + // The numbered index of this descriptor in a collection of descriptors. + Number uint8 + // Expected count of descriptors. + Expected uint8 + // Numbered index of any subsegment of this descriptor. + SubNumber uint8 + // Expected count of subsegments. + SubExpected uint8 +} + +func (d SegmentationDescriptor) Tag() uint8 { return TagSegmentation } +func (d SegmentationDescriptor) ID() uint32 { return descriptorIDCUEI } + +func (d SegmentationDescriptor) Data() []byte { + buf := make([]byte, 5) + binary.BigEndian.PutUint32(buf[:4], d.EventID) + if d.Cancel { + buf[4] |= (1 << 7) + } + if d.EventIDCompliance { + buf[4] |= (1 << 6) + } + // toggle next remaining 6 reserved bits. + buf[4] |= 0b00111111 + + if !d.Cancel { + buf = append(buf, segDescFlags(&d)) + if d.Duration != nil { + b := make([]byte, 8) // uint64 needs 8 + binary.BigEndian.PutUint64(b, *d.Duration<<24) // 40 bits + // append 40 bits (5 bytes) + buf = append(buf, b[:5]...) + } + + buf = append(buf, byte(d.UPID.Type)) + buf = append(buf, uint8(len(d.UPID.Value))) + buf = append(buf, d.UPID.Value...) + + buf = append(buf, byte(d.Type), byte(d.Number), byte(d.Expected)) + switch d.Type { + // TODO(otl): use named constants from section 10.3.3.1 Table 23 - segmentation_type_id + case 0x34, 0x30, 0x32, 0x36, 0x38, 0x3a, 0x44, 0x46: + if d.SubNumber > 0 { + buf = append(buf, d.SubNumber) + } + if d.SubExpected > 0 { + buf = append(buf, d.SubExpected) + } + } + } + return buf +} + +func unmarshalSegDescriptor(buf []byte) SegmentationDescriptor { + var desc SegmentationDescriptor + desc.EventID = binary.BigEndian.Uint32(buf[:4]) + if buf[4]&0b10000000 > 0 { + desc.Cancel = true + } + if buf[4]&0b01000000 > 0 { + desc.EventIDCompliance = true + } + // next 6 bits are reserved + + // always assume program_segmentation_flag is set at 0b10000000 + // we don't support the deprecated component mode. + + if !desc.Cancel { + // left-most 2 bits are flags for later. + desc.Restrictions = DeliveryRestrictions(buf[5] & 0b00111111) + + // is segmentation duration flag set? + if buf[5]&0b01000000 > 0 { + b := make([]byte, 3) + b = append(b, buf[6:11]...) + dur := binary.BigEndian.Uint64(b) + desc.Duration = &dur + buf = buf[11:] + } else { + buf = buf[6:] + } + + uplen := int(buf[1]) + desc.UPID = UPID{ + Type: UPIDType(buf[0]), + Value: buf[2 : 2+uplen], + } + buf = buf[2+uplen:] + + // TODO(otl): use named constants from section 10.3.3.1 Table 23 - segmentation_type_id + desc.Type = uint8(buf[0]) + desc.Number = uint8(buf[1]) + desc.Expected = uint8(buf[2]) + switch desc.Type { + // TODO(otl): use named constants from section 10.3.3.1 Table 23 - segmentation_type_id + case 0x34, 0x30, 0x32, 0x36, 0x38, 0x3a, 0x44, 0x46: + if len(buf[2:]) > 1 { + desc.SubNumber = uint8(buf[3]) + } + if len(buf[2:]) > 2 { + desc.SubExpected = uint8(buf[4]) + } + } + } + + return desc +} + +// UPID represents a segmentation_upid structure as specified in SCTE 35 section 10.3.3.1. +type UPID struct { + Type UPIDType + // Value holds the corresponding encoded contents for this UPID's Type. + // Possible values are given in Table 22 of section 10.3.3.1. + Value []byte +} + +// UPIDType represents a Segmentation UPID type as defined in SCTE 35 section 10.3.3.1. +type UPIDType uint8 + +// Valid UPIDType values defined in Table 22, SCTE 35 section 10.3.3.1. +const ( + UPIDNone UPIDType = 0 + iota + _ // User Defined, deprecated, use MPU. + _ // ISCI, deprecated, use AdID. + UPIDAdID + UPIDUMID + _ // ISAN, deprecated, use ISAN. + UPIDISAN + UPIDTID + UPIDTI + UPIDADI + UPIDEIDR + UPIDATSCContentID + UPIDMPU + UPIDMID + UPIDADSInfo + UPIDURI + UPIDUUID + UPIDSCR + UPIDReserved +) + +// TimeDescriptor represents a moment in time as used in the Precision +// Time Protocol (PTP). PTP uses International Atomic Time (TAI) rather +// than UTC time as in NTP. +type TimeDescriptor struct { + // A 48-bit integer of the number of seconds since the Unix + // epoch according to TAI. + Seconds uint64 + // Number of nanoseconds... + Nanoseconds uint32 + // The current number of seconds between NTP time and + // TAI for a single instance of time. + UTCOffset uint16 +} + +func (d TimeDescriptor) Tag() uint8 { return TagTime } +func (d TimeDescriptor) ID() uint32 { return descriptorIDCUEI } + +func (d TimeDescriptor) Data() []byte { + // 48 bits + 32 bits + 16 bits + b := make([]byte, 0, 6+4+2) + b = binary.BigEndian.AppendUint64(b, d.Seconds) + b = b[:6] // only want 48-bits + b = binary.BigEndian.AppendUint32(b, d.Nanoseconds) + return binary.BigEndian.AppendUint16(b, d.UTCOffset) +} + +type AudioChannel struct { + ComponentTag uint8 + // A language code from ISO 639-2. + Language [3]byte + // A 3-bit integer from ATSC A/52 Table 5.7. + BitstreamMode uint8 + // Number of channels as a 4-bit integer, from ATSC A/52 Table A4.5. + Count uint8 + FullService bool +} + +type AudioDescriptor []AudioChannel + +func (d AudioDescriptor) Tag() uint8 { return TagAudio } +func (d AudioDescriptor) ID() uint32 { return descriptorIDCUEI } + +func (d AudioDescriptor) Data() []byte { + var b []byte + count := len(d) + b = append(b, byte(count<<4)) // right-most 4 bits are reserved + for _, ch := range d { + b = append(b, ch.ComponentTag) + b = append(b, ch.Language[:]...) + var c byte + c |= (ch.BitstreamMode << 5) // set left 3 bits + c |= (ch.Count & 0x0f) // only want 4 bits + if ch.FullService { + c |= 0x01 // set last remaining bit + } + b = append(b, c) + } + return b +} + +func decodeAllDescriptors(buf []byte) ([]SpliceDescriptor, error) { + var sds []SpliceDescriptor + for len(buf) >= 6 { + // first byte is tag, second is length of next descriptor. + dlen := uint8(buf[1]) + desc, err := unmarshalSpliceDescriptor(buf[:2+dlen]) + if err != nil { + return sds, err + } + sds = append(sds, desc) + if int(dlen) >= len(buf) { + break + } + buf = buf[2+dlen:] + } + return sds, nil +} + +// UnmarshalSpliceDescriptor reads exactly one descriptor from buf. +func unmarshalSpliceDescriptor(buf []byte) (SpliceDescriptor, error) { + if len(buf) < 6 { + return nil, fmt.Errorf("short slice: need at least 5 bytes") + } + tag := uint8(buf[0]) + length := uint8(buf[1]) + if len(buf[2:]) != int(length) { + return nil, fmt.Errorf("need %d bytes, have %d", int(length), len(buf[2:])) + } + buf = buf[2 : 2+length] + id := binary.BigEndian.Uint32(buf[:4]) + buf = buf[4:] + if id != descriptorIDCUEI { + return PrivateDescriptor{tag, id, buf}, nil + } + switch tag { + case TagAvail: + return AvailDescriptor(binary.BigEndian.Uint32(buf)), nil + case TagSegmentation: + return unmarshalSegDescriptor(buf), nil + case TagDTMF: + return unmarshalDTMF(buf), nil + } + return nil, fmt.Errorf("unmarshal %d unsupported", tag) +} + +type PrivateDescriptor struct { + PTag uint8 + PID uint32 + PData []byte +} + +func (d PrivateDescriptor) Tag() uint8 { return d.PTag } +func (d PrivateDescriptor) ID() uint32 { return d.PID } +func (d PrivateDescriptor) Data() []byte { return d.PData } + +func segDescFlags(seg *SegmentationDescriptor) uint8 { + var b uint8 + // assume program_segmentation is always set; we do not support the deprecated component mode. + b |= (1 << 7) + if seg.Duration != nil { + b |= (1 << 6) + } + if seg.Restrictions != 0 { + b |= byte(seg.Restrictions) + } else { + b |= (1 << 5) + } + return b +} diff --git a/scte35/splice_info.go b/scte35/splice_info.go @@ -0,0 +1,265 @@ +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 := calculateCRC32(buf) + 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.EventIDCompliance = 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 +} diff --git a/scte35/splice_info_test.go b/scte35/splice_info_test.go @@ -0,0 +1,167 @@ +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/internal/scte35/splice_schedule_test.go b/scte35/splice_schedule_test.go