commit 8a3d06f2a309055b4e11c8a0796c93e473cf314f from: Oliver Lowe date: Mon Jun 9 06:13:35 2025 UTC scte35: fix AudioDescriptor marshalling Add missing unmarshalAudioDescriptor function and TagAudio case in unmarshalSpliceDescriptor. Fix bit packing in Data method where Count field was not shifted correctly. Fixes round-trip encoding for SCTE35 audio descriptors. Co-Authored-By: sketch Change-ID: sb9edfc16eeea6362k commit - 62d865db8655e6d23b4fccc3280d73c3cdda8ce9 commit + 8a3d06f2a309055b4e11c8a0796c93e473cf314f blob - 6a360bdd85a88ac4a54558990481a227ade66c3c blob + d422847b6bd1d0a1739e00a5b1c0dce02e7a1283 --- scte35/splice_descriptor.go +++ scte35/splice_descriptor.go @@ -314,16 +314,53 @@ func (d AudioDescriptor) Data() []byte { 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 + c |= (ch.BitstreamMode << 5) // set left 3 bits (7-5) + c |= ((ch.Count & 0x0f) << 1) // 4 bits shifted left for bits 4-1 if ch.FullService { - c |= 0x01 // set last remaining bit + c |= 0x01 // set last remaining bit (0) } b = append(b, c) } return b } +func unmarshalAudioDescriptor(buf []byte) AudioDescriptor { + if len(buf) == 0 { + return AudioDescriptor{} + } + + // Extract channel count from upper 4 bits of first byte + count := int(buf[0] >> 4) + if count == 0 { + return AudioDescriptor{} + } + + // Each channel requires 5 bytes (1 + 3 + 1) + expectedLen := 1 + (count * 5) + if len(buf) < expectedLen { + return AudioDescriptor{} + } + + channels := make([]AudioChannel, count) + buf = buf[1:] // skip first byte with count + + for i := 0; i < count; i++ { + ch := &channels[i] + ch.ComponentTag = buf[0] + copy(ch.Language[:], buf[1:4]) + + // Decode the packed byte: BitstreamMode (3 bits) + Count (4 bits) + FullService (1 bit) + packed := buf[4] + ch.BitstreamMode = (packed >> 5) & 0x07 // upper 3 bits + ch.Count = (packed >> 1) & 0x0F // middle 4 bits + ch.FullService = (packed & 0x01) != 0 // lowest bit + + buf = buf[5:] // move to next channel + } + + return AudioDescriptor(channels) +} + func decodeAllDescriptors(buf []byte) ([]SpliceDescriptor, error) { var sds []SpliceDescriptor for len(buf) >= 6 { @@ -365,6 +402,8 @@ func unmarshalSpliceDescriptor(buf []byte) (SpliceDesc return unmarshalSegDescriptor(buf), nil case TagDTMF: return unmarshalDTMF(buf), nil + case TagAudio: + return unmarshalAudioDescriptor(buf), nil } return nil, fmt.Errorf("unmarshal %d unsupported", tag) } blob - /dev/null blob + d3d08df7bb6e5f0efb4e7cb077430d111c069ff2 (mode 644) --- /dev/null +++ scte35/splice_descriptor_test.go @@ -0,0 +1,139 @@ +package scte35 + +import ( + "reflect" + "testing" +) + +func TestAudioDescriptorRoundTrip(t *testing.T) { + tests := []struct { + name string + desc AudioDescriptor + }{ + { + name: "empty", + desc: AudioDescriptor{}, + }, + { + name: "single channel", + desc: AudioDescriptor{ + { + ComponentTag: 0x12, + Language: [3]byte{'e', 'n', 'g'}, + BitstreamMode: 0x05, + Count: 6, + FullService: true, + }, + }, + }, + { + name: "multiple channels", + desc: AudioDescriptor{ + { + ComponentTag: 0x12, + Language: [3]byte{'e', 'n', 'g'}, + BitstreamMode: 0x05, + Count: 6, + FullService: true, + }, + { + ComponentTag: 0x34, + Language: [3]byte{'s', 'p', 'a'}, + BitstreamMode: 0x03, + Count: 2, + FullService: false, + }, + { + ComponentTag: 0x56, + Language: [3]byte{'f', 'r', 'a'}, + BitstreamMode: 0x07, + Count: 8, + FullService: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test basic properties + if got := tt.desc.Tag(); got != TagAudio { + t.Errorf("Tag() = %v, want %v", got, TagAudio) + } + if got := tt.desc.ID(); got != descriptorIDCUEI { + t.Errorf("ID() = %v, want %v", got, descriptorIDCUEI) + } + + // Test round-trip marshalling/unmarshalling + data := tt.desc.Data() + unmarshaled := unmarshalAudioDescriptor(data) + + if !reflect.DeepEqual(tt.desc, unmarshaled) { + t.Errorf("Round-trip failed:\noriginal: %+v\nunmarshaled: %+v", tt.desc, unmarshaled) + } + }) + } +} + +func TestAudioDescriptorUnmarshalSpliceDescriptor(t *testing.T) { + // Test that AudioDescriptor can be unmarshalled via unmarshalSpliceDescriptor + desc := AudioDescriptor{ + { + ComponentTag: 0x12, + Language: [3]byte{'e', 'n', 'g'}, + BitstreamMode: 0x05, + Count: 6, + FullService: true, + }, + } + + // Encode as a full splice descriptor + encoded := encodeSpliceDescriptor(desc) + + // Unmarshal via the main function + unmarshaled, err := unmarshalSpliceDescriptor(encoded) + if err != nil { + t.Fatalf("unmarshalSpliceDescriptor failed: %v", err) + } + + // Check that we got back an AudioDescriptor + audioDesc, ok := unmarshaled.(AudioDescriptor) + if !ok { + t.Fatalf("Expected AudioDescriptor, got %T", unmarshaled) + } + + if !reflect.DeepEqual(desc, audioDesc) { + t.Errorf("Round-trip via unmarshalSpliceDescriptor failed:\noriginal: %+v\nunmarshaled: %+v", desc, audioDesc) + } +} + +func TestAudioDescriptorDataFormat(t *testing.T) { + // Test specific encoding format + desc := AudioDescriptor{ + { + ComponentTag: 0x12, + Language: [3]byte{'e', 'n', 'g'}, + BitstreamMode: 0x05, // 3 bits: 101 + Count: 6, // 4 bits: 0110 + FullService: true, // 1 bit: 1 + }, + } + + data := desc.Data() + expected := []byte{ + 0x10, // count=1 in upper 4 bits: 00010000 + 0x12, // ComponentTag + 'e', 'n', 'g', // Language + 0xAD, // BitstreamMode(101) + Count(0110) + FullService(1) = 10101101 = 0xAD + } + + if !reflect.DeepEqual(data, expected) { + t.Errorf("Data() encoding mismatch:\ngot: %x\nexpected: %x", data, expected) + } + + // Test unmarshalling the expected format + unmarshaled := unmarshalAudioDescriptor(expected) + if !reflect.DeepEqual(desc, unmarshaled) { + t.Errorf("Unmarshalling expected format failed:\noriginal: %+v\nunmarshaled: %+v", desc, unmarshaled) + } +}