// SPDX-FileCopyrightText: 2026 The Pion community // SPDX-License-Identifier: MIT package oggwriter import ( "bytes" "encoding/binary" "errors" "io" "os" "path/filepath" "testing" "github.com/pion/rtp" "github.com/pion/webrtc/v4/pkg/media/oggreader" "github.com/stretchr/testify/assert" ) var ( errUnexpectedSeek = errors.New("unexpected seek") errUnexpectedWriteAt = errors.New("unexpected write-at") errRewriteFailed = errors.New("rewrite failed") errCloseFailed = errors.New("close failed") _ func(string, uint32, uint16) (*OggWriter, error) = New _ func(io.Writer, uint32, uint16) (*OggWriter, error) = NewWith ) type oggWriterPacketTest struct { buffer io.Writer message string messageClose string packet *rtp.Packet writer *OggWriter err error closeErr error } type noSeekWriter struct { bytes.Buffer closed bool seekCalled bool writeAtCalled bool } type rewriteFailingCloser struct { bytes.Buffer closed bool writeAtCalls int writeAtErr error closeErr error } type rawOggPage struct { header []byte segmentTable []byte payload []byte } func (w *noSeekWriter) Seek(int64, int) (int64, error) { w.seekCalled = true return 0, errUnexpectedSeek } func (w *noSeekWriter) WriteAt([]byte, int64) (int, error) { w.writeAtCalled = true return 0, errUnexpectedWriteAt } func (w *noSeekWriter) Close() error { w.closed = true return nil } func (w *rewriteFailingCloser) Seek(offset int64, whence int) (int64, error) { if offset != 0 || whence != io.SeekCurrent { return 0, errUnexpectedSeek } return int64(w.Len()), nil } func (w *rewriteFailingCloser) WriteAt(p []byte, _ int64) (int, error) { w.writeAtCalls++ if w.writeAtErr != nil { return 0, w.writeAtErr } return len(p), nil } func (w *rewriteFailingCloser) Close() error { w.closed = true return w.closeErr } func TestOggWriter_AddPacketAndClose(t *testing.T) { rawPkt := []byte{ 0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64, 0x27, 0x82, 0x00, 0x01, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x36, 0xbe, 0x88, 0x9e, } validPacket := &rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: true, ExtensionProfile: 1, Version: 2, PayloadType: 111, SequenceNumber: 27023, Timestamp: 3653407706, SSRC: 476325762, CSRC: []uint32{}, }, Payload: rawPkt[20:], } assert.NoError(t, validPacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF})) assert := assert.New(t) // The linter misbehave and thinks this code is the same as the tests in ivf-writer_test // nolint:dupl addPacketTestCase := []oggWriterPacketTest{ { buffer: &bytes.Buffer{}, message: "OggWriter shouldn't be able to write something to a closed file", messageClose: "OggWriter should be able to close an already closed file", packet: validPacket, err: errFileNotOpened, closeErr: nil, }, { buffer: &bytes.Buffer{}, message: "OggWriter shouldn't be able to write a nil packet", messageClose: "OggWriter should be able to close the file", packet: nil, err: errInvalidNilPacket, closeErr: nil, }, { buffer: &bytes.Buffer{}, message: "OggWriter should be able to write an Opus packet", messageClose: "OggWriter should be able to close the file", packet: validPacket, err: nil, closeErr: nil, }, { buffer: nil, message: "OggWriter shouldn't be able to write something to a closed file", messageClose: "OggWriter should be able to close an already closed file", packet: nil, err: errFileNotOpened, closeErr: nil, }, } // First test case has a 'nil' file descriptor writer, err := NewWith(addPacketTestCase[0].buffer, 48000, 2) assert.Nil(err, "OggWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") err = writer.Close() assert.Nil(err, "OggWriter should be able to close the file descriptor") writer.stream = nil addPacketTestCase[0].writer = writer // Second test writes tries to write an empty packet writer, err = NewWith(addPacketTestCase[1].buffer, 48000, 2) assert.Nil(err, "OggWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") addPacketTestCase[1].writer = writer // Third test writes tries to write a valid Opus packet writer, err = NewWith(addPacketTestCase[2].buffer, 48000, 2) assert.Nil(err, "OggWriter should be created") assert.NotNil(writer, "Writer shouldn't be nil") addPacketTestCase[2].writer = writer // Fourth test tries to write to a nil stream writer, err = NewWith(addPacketTestCase[3].buffer, 4800, 2) assert.NotNil(err, "IVFWriter shouldn't be created") assert.Nil(writer, "Writer should be nil") addPacketTestCase[3].writer = writer for _, t := range addPacketTestCase { if t.writer != nil { res := t.writer.WriteRTP(t.packet) assert.Equal(t.err, res, t.message) } } for _, t := range addPacketTestCase { if t.writer != nil { res := t.writer.Close() assert.Equal(t.closeErr, res, t.messageClose) } } } func TestOggWriter_EmptyPayload(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWith(buffer, 48000, 2) assert.NoError(t, err) assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{}})) } func TestOggWriter_RejectsUnsupportedChannelCount(t *testing.T) { tests := []struct { name string channelCount uint16 }{ { name: "zero", channelCount: 0, }, { name: "three", channelCount: 3, }, { name: "nine", channelCount: 9, }, { name: "wraps to zero", channelCount: 256, }, { name: "max uint16", channelCount: 65535, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fileName := filepath.Join(t.TempDir(), "invalid.ogg") writer, err := New(fileName, 48000, tt.channelCount) assert.ErrorIs(t, err, errInvalidChannelCount) assert.Nil(t, writer) _, statErr := os.Stat(fileName) assert.ErrorIs(t, statErr, os.ErrNotExist) writer, err = NewWith(&bytes.Buffer{}, 48000, tt.channelCount) assert.ErrorIs(t, err, errInvalidChannelCount) assert.Nil(t, writer) multiWriter, err := NewWriter(&bytes.Buffer{}, WithChannelCount(tt.channelCount)) assert.ErrorIs(t, err, errInvalidChannelCount) assert.Nil(t, multiWriter) multiWriter, err = NewWriter(&bytes.Buffer{}) assert.NoError(t, err) assert.NotNil(t, multiWriter) track, err := multiWriter.NewTrack(1111, WithChannelCount(tt.channelCount)) assert.ErrorIs(t, err, errInvalidChannelCount) assert.Nil(t, track) }) } } func TestOggWriter_ChannelMappingFamily0Defaults(t *testing.T) { tests := []struct { name string channels uint16 mappingFamily uint8 streams uint8 coupled uint8 channelMap []byte }{ {name: "family-0-mono", channels: 1, mappingFamily: 0}, {name: "family-0-stereo", channels: 2, mappingFamily: 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWith(buffer, 48000, tt.channels) assert.NoError(t, err) assert.NotNil(t, writer) header := firstOpusHeadForTest(t, buffer.Bytes()) assert.EqualValues(t, tt.channels, header.Channels) assert.EqualValues(t, tt.mappingFamily, header.ChannelMap) assert.EqualValues(t, tt.streams, header.StreamCount) assert.EqualValues(t, tt.coupled, header.CoupledCount) assert.Equal(t, string(tt.channelMap), header.ChannelMapping) }) } } func TestOggWriter_ExplicitChannelMappings(t *testing.T) { tests := []struct { name string mappingFamily uint8 streams uint8 coupled uint8 channelMap []byte }{ {name: "family-1-mono", mappingFamily: 1, streams: 1, coupled: 0, channelMap: []byte{0}}, {name: "family-1-stereo", mappingFamily: 1, streams: 1, coupled: 1, channelMap: []byte{0, 1}}, {name: "family-2-zero-order", mappingFamily: 2, streams: 1, coupled: 0, channelMap: []byte{0}}, {name: "family-255", mappingFamily: 255, streams: 1, coupled: 1, channelMap: []byte{0, 255, 1}}, { name: "family-255-fewer-output-channels-than-decoded", mappingFamily: 255, streams: 1, coupled: 1, channelMap: []byte{0}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWriter(buffer) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack( 1111, WithChannelMapping(tt.mappingFamily, tt.streams, tt.coupled, tt.channelMap), ) assert.NoError(t, err) assert.NotNil(t, track) assert.NoError(t, track.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) header := firstOpusHeadForTest(t, buffer.Bytes()) assert.EqualValues(t, tt.mappingFamily, header.ChannelMap) assert.EqualValues(t, len(tt.channelMap), header.Channels) assert.EqualValues(t, tt.streams, header.StreamCount) assert.EqualValues(t, tt.coupled, header.CoupledCount) assert.Equal(t, string(tt.channelMap), header.ChannelMapping) }) } } func TestOggWriter_ExplicitChannelMappingOverridesWriterDefaultChannelCount(t *testing.T) { channelMap := []byte{0, 255, 1} buffer := &bytes.Buffer{} writer, err := NewWriter(buffer, WithChannelCount(1)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithChannelMapping(255, 1, 1, channelMap)) assert.NoError(t, err) assert.NotNil(t, track) assert.NoError(t, track.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) header := firstOpusHeadForTest(t, buffer.Bytes()) assert.EqualValues(t, 255, header.ChannelMap) assert.EqualValues(t, len(channelMap), header.Channels) assert.EqualValues(t, 1, header.StreamCount) assert.EqualValues(t, 1, header.CoupledCount) assert.Equal(t, string(channelMap), header.ChannelMapping) } func TestOggWriter_RejectsInvalidChannelMappings(t *testing.T) { tests := []struct { name string mappingFamily uint8 streams uint8 coupled uint8 channelMap []byte wantErr error }{ { name: "family-0", mappingFamily: 0, streams: 1, coupled: 1, channelMap: []byte{0, 1}, wantErr: errInvalidChannelMap, }, { name: "family-3", mappingFamily: 3, streams: 1, coupled: 1, channelMap: []byte{0, 1}, wantErr: errInvalidChannelMap, }, { name: "family-4", mappingFamily: 4, streams: 1, coupled: 1, channelMap: []byte{0, 1}, wantErr: errInvalidChannelMap, }, { name: "empty mapping", mappingFamily: 1, streams: 1, coupled: 1, channelMap: nil, wantErr: errInvalidChannelCount, }, { name: "zero streams", mappingFamily: 1, streams: 0, coupled: 0, channelMap: []byte{0}, wantErr: errInvalidChannelMap, }, { name: "multiple streams", mappingFamily: 255, streams: 2, coupled: 0, channelMap: []byte{0, 1}, wantErr: errInvalidChannelMap, }, { name: "too many coupled streams", mappingFamily: 1, streams: 1, coupled: 2, channelMap: []byte{0, 1, 2}, wantErr: errInvalidChannelMap, }, { name: "map entry out of range", mappingFamily: 1, streams: 1, coupled: 1, channelMap: []byte{0, 2}, wantErr: errInvalidChannelMap, }, { name: "family-1 unsupported channel count", mappingFamily: 1, streams: 1, coupled: 1, channelMap: []byte{0, 1, 1}, wantErr: errInvalidChannelMap, }, { name: "family-1 non-vorbis mapping", mappingFamily: 1, streams: 1, coupled: 1, channelMap: []byte{1, 0}, wantErr: errInvalidChannelMap, }, { name: "family-2 unsupported channel count", mappingFamily: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}, wantErr: errInvalidChannelMap, }, } for _, tt := range tests { t.Run(tt.name+"/writer-default", func(t *testing.T) { writer, err := NewWriter( &bytes.Buffer{}, WithChannelMapping(tt.mappingFamily, tt.streams, tt.coupled, tt.channelMap), ) assert.ErrorIs(t, err, tt.wantErr) assert.Nil(t, writer) }) t.Run(tt.name+"/track", func(t *testing.T) { writer, err := NewWriter(&bytes.Buffer{}) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack( 1111, WithChannelMapping(tt.mappingFamily, tt.streams, tt.coupled, tt.channelMap), ) assert.ErrorIs(t, err, tt.wantErr) assert.Nil(t, track) }) } } func TestOggWriter_NewCloseWithoutRTPPreservesHeaders(t *testing.T) { fileName := t.TempDir() + "/empty.ogg" writer, err := New(fileName, 48000, 2) assert.NoError(t, err) assert.NoError(t, writer.Close()) file, err := os.Open(fileName) //nolint:gosec assert.NoError(t, err) defer func() { assert.NoError(t, file.Close()) }() reader, header, err := oggreader.NewWith(file) assert.NoError(t, err) assert.NotNil(t, reader) assert.NotNil(t, header) payload, pageHeader, err := reader.ParseNextPage() assert.NoError(t, err) headerType, ok := pageHeader.HeaderType(payload) assert.True(t, ok) assert.Equal(t, oggreader.HeaderOpusTags, headerType) } func TestOggWriter_LargePayload(t *testing.T) { rawPkt := bytes.Repeat([]byte{0x45}, 1000) validPacket := &rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: true, ExtensionProfile: 1, Version: 2, PayloadType: 111, SequenceNumber: 27023, Timestamp: 3653407706, SSRC: 476325762, CSRC: []uint32{}, }, Payload: rawPkt, } assert.NoError(t, validPacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF})) writer, err := NewWith(&bytes.Buffer{}, 48000, 2) assert.NoError(t, err, "OggWriter should be created") assert.NotNil(t, writer, "Writer shouldn't be nil") err = writer.WriteRTP(validPacket) assert.NoError(t, err) data := createPageForSerial( writer.checksumTable, rawPkt, pageHeaderTypeContinuationOfStream, 0, writer.track.serial, 1, ) assert.Equal(t, uint8(4), data[26]) } func TestOggWriter_BoundaryPayloadSplitsTerminatingLace(t *testing.T) { payload := bytes.Repeat([]byte{0x45}, 65025) mapping, err := defaultChannelMapping(2) assert.NoError(t, err) track := newTrackState(48000, mapping, 0x01020304, defaultOpusTags()) track.pageIndex = 7 buffer := &bytes.Buffer{} err = writePage( buffer, nil, generateChecksumTable(), track, payload, pageHeaderTypeContinuationOfStream, 960, ) assert.NoError(t, err) assert.Equal(t, uint32(9), track.pageIndex) pages := parseRawOggPages(t, buffer.Bytes()) if !assert.Len(t, pages, 2) { return } assert.Equal(t, uint8(pageHeaderTypeContinuationOfStream), pages[0].header[5]) assert.Equal(t, noGranulePosition, binary.LittleEndian.Uint64(pages[0].header[6:14])) assert.Equal(t, uint32(7), binary.LittleEndian.Uint32(pages[0].header[18:22])) assert.Equal(t, uint8(maxOggPageSegments), pages[0].header[26]) assert.Equal(t, bytes.Repeat([]byte{255}, maxOggPageSegments), pages[0].segmentTable) assert.Len(t, pages[0].payload, 65025) assert.Equal(t, uint8(pageHeaderTypeContinuationOfPacket), pages[1].header[5]) assert.Equal(t, uint64(960), binary.LittleEndian.Uint64(pages[1].header[6:14])) assert.Equal(t, uint32(8), binary.LittleEndian.Uint32(pages[1].header[18:22])) assert.Equal(t, uint8(1), pages[1].header[26]) assert.Equal(t, []byte{0}, pages[1].segmentTable) assert.Empty(t, pages[1].payload) } func TestOggWriter_BoundaryPayloadCloseMarksEmptyContinuationPageEOS(t *testing.T) { fileName := filepath.Join(t.TempDir(), "boundary.ogg") writer, err := New(fileName, 48000, 2) assert.NoError(t, err) assert.NotNil(t, writer) payload := bytes.Repeat([]byte{0x45}, 65025) assert.NoError(t, writer.WriteRTP(opusRTPPacketForTest(t, 1000, writer.track.serial, payload))) assert.NoError(t, writer.Close()) data, err := os.ReadFile(fileName) //nolint:gosec assert.NoError(t, err) pages := parseRawOggPages(t, data) if !assert.Len(t, pages, 4) { return } assert.Equal(t, uint8(pageHeaderTypeContinuationOfPacket|pageHeaderTypeEndOfStream), pages[3].header[5]) assert.Equal(t, []byte{0}, pages[3].segmentTable) assert.Empty(t, pages[3].payload) } func TestOggWriter_CloseClosesFileAfterEOSRewriteFailure(t *testing.T) { fileName := filepath.Join(t.TempDir(), "append.ogg") file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) //nolint:gosec assert.NoError(t, err) config, err := newTrackConfig(48000, 2) assert.NoError(t, err) writer, err := newWith(file, file, config) assert.NoError(t, err) assert.NotNil(t, writer) assert.NoError(t, writer.WriteRTP(validOpusPacketForTest(t, 1000, writer.track.serial))) err = writer.Close() assert.Error(t, err) _, err = file.Write([]byte{0}) assert.ErrorIs(t, err, os.ErrClosed) } func parseRawOggPages(t *testing.T, data []byte) []rawOggPage { t.Helper() var pages []rawOggPage for offset := 0; offset < len(data); { if offset+pageHeaderSize > len(data) { assert.Failf(t, "short Ogg page header", "at offset %d", offset) return nil } header := data[offset : offset+pageHeaderSize] segmentCount := int(header[26]) segmentTableOffset := offset + pageHeaderSize payloadOffset := segmentTableOffset + segmentCount if payloadOffset > len(data) { assert.Failf(t, "short Ogg segment table", "at offset %d", segmentTableOffset) return nil } segmentTable := data[segmentTableOffset:payloadOffset] payloadSize := 0 for _, segmentSize := range segmentTable { payloadSize += int(segmentSize) } nextOffset := payloadOffset + payloadSize if nextOffset > len(data) { assert.Failf(t, "short Ogg page payload", "at offset %d", payloadOffset) return nil } pages = append(pages, rawOggPage{ header: header, segmentTable: segmentTable, payload: data[payloadOffset:nextOffset], }) offset = nextOffset } return pages } func validOpusPacketForTest(t *testing.T, timestamp, ssrc uint32) *rtp.Packet { t.Helper() return opusRTPPacketForTest(t, timestamp, ssrc, []byte{0x98, 0x36, 0xbe, 0x88, 0x9e}) } func opusRTPPacketForTest(t *testing.T, timestamp, ssrc uint32, payload []byte) *rtp.Packet { t.Helper() packet := &rtp.Packet{ Header: rtp.Header{ Marker: true, Extension: true, ExtensionProfile: 1, Version: 2, PayloadType: 111, SequenceNumber: 27023, Timestamp: timestamp, SSRC: ssrc, CSRC: []uint32{}, }, Payload: append([]byte(nil), payload...), } assert.NoError(t, packet.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF})) return packet } func firstOpusHeadForTest(t *testing.T, data []byte) *oggreader.OggHeader { t.Helper() _, header, err := oggreader.NewWith(bytes.NewReader(data)) assert.NoError(t, err) assert.NotNil(t, header) return header } func TestOggWriter_OpusPacketSampleCount(t *testing.T) { tests := []struct { name string payload []byte want uint64 wantErr error }{ { name: "silk 60ms", payload: []byte{0x18}, want: 2880, }, { name: "hybrid 10ms", payload: []byte{0x60}, want: 480, }, { name: "hybrid 20ms", payload: []byte{0x68}, want: 960, }, { name: "celt 2.5ms", payload: []byte{0x80}, want: 120, }, { name: "celt 20ms one frame", payload: []byte{0x98}, want: 960, }, { name: "code 1 two frames", payload: []byte{0x99}, want: 1920, }, { name: "code 2 two frames", payload: []byte{0x9a}, want: 1920, }, { name: "code 3 frame count", payload: []byte{0x9b, 0x03}, want: 2880, }, { name: "maximum packet duration", payload: []byte{0x1b, 0x02}, want: 5760, }, { name: "missing code 3 count", payload: []byte{0x9b}, wantErr: errInvalidOpusPacket, }, { name: "zero code 3 frame count", payload: []byte{0x9b, 0x00}, wantErr: errInvalidOpusPacket, }, { name: "over maximum packet duration", payload: []byte{0x1b, 0x03}, wantErr: errInvalidOpusPacket, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := opusPacketSampleCount(tt.payload) if tt.wantErr != nil { assert.ErrorIs(t, err, tt.wantErr) return } assert.NoError(t, err) assert.Equal(t, tt.want, got) }) } } func TestOggWriter_GranulePositionUsesOpusDuration(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWith(buffer, 48000, 2) assert.NoError(t, err) assert.NotNil(t, writer) assert.NoError(t, writer.WriteRTP(opusRTPPacketForTest(t, 1000, 1111, []byte{0x98}))) assert.NoError(t, writer.WriteRTP(opusRTPPacketForTest(t, 3880, 1111, []byte{0x98}))) assert.NoError(t, writer.WriteRTP(opusRTPPacketForTest(t, 4840, 1111, []byte{0x9b, 0x02}))) assert.Equal(t, []uint64{960, 1920, 3840}, audioGranulePositions(t, buffer.Bytes())) } func audioGranulePositions(t *testing.T, data []byte) []uint64 { t.Helper() reader, err := oggreader.NewWithOptions(bytes.NewReader(data)) assert.NoError(t, err) assert.NotNil(t, reader) var granules []uint64 for { payload, pageHeader, parseErr := reader.ParseNextPage() if errors.Is(parseErr, io.EOF) { break } if !assert.NoError(t, parseErr) { break } if _, ok := pageHeader.HeaderType(payload); ok { continue } granules = append(granules, pageHeader.GranulePosition) } return granules } func opusTagsBySerial(t *testing.T, data []byte) map[uint32]*oggreader.OpusTags { t.Helper() reader, err := oggreader.NewWithOptions(bytes.NewReader(data)) assert.NoError(t, err) assert.NotNil(t, reader) tagsBySerial := map[uint32]*oggreader.OpusTags{} for { payload, pageHeader, parseErr := reader.ParseNextPage() if errors.Is(parseErr, io.EOF) { break } if !assert.NoError(t, parseErr) { break } headerType, ok := pageHeader.HeaderType(payload) if !ok || headerType != oggreader.HeaderOpusTags { continue } opusTags, err := oggreader.ParseOpusTags(payload) if !assert.NoError(t, err) { continue } tagsBySerial[pageHeader.Serial] = opusTags } return tagsBySerial } func TestOggWriter_NewWriterRequiresOutput(t *testing.T) { writer, err := NewWriter(nil) assert.ErrorIs(t, err, errOutputNotOpened) assert.Nil(t, writer) } func TestOggWriter_NewWriter(t *testing.T) { buffer := &bytes.Buffer{} serial1 := uint32(0x01020304) serial2 := uint32(0x05060708) writer, err := NewWriter(buffer, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track1, err := writer.NewTrack(1111, WithSerial(serial1), WithChannelCount(1)) assert.NoError(t, err) assert.NotNil(t, track1) track2, err := writer.NewTrack(2222, WithSerial(serial2), WithSampleRate(16000)) assert.NoError(t, err) assert.NotNil(t, track2) assert.NoError(t, track1.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) assert.NoError(t, track2.WriteRTP(validOpusPacketForTest(t, 2000, 2222))) type pageOrderEntry struct { kind string serial uint32 } reader, err := oggreader.NewWithOptions(bytes.NewReader(buffer.Bytes())) assert.NoError(t, err) assert.NotNil(t, reader) var pageOrder []pageOrderEntry var opusHeadSerials []uint32 var opusTagsSerials []uint32 var audioSerials []uint32 var audioGranules []uint64 opusHeadSampleRates := map[uint32]uint32{} opusHeadChannelCounts := map[uint32]uint8{} for { payload, pageHeader, parseErr := reader.ParseNextPage() if errors.Is(parseErr, io.EOF) { break } if !assert.NoError(t, parseErr) { break } headerType, ok := pageHeader.HeaderType(payload) if ok { pageOrder = append(pageOrder, pageOrderEntry{ kind: string(headerType), serial: pageHeader.Serial, }) switch headerType { case oggreader.HeaderOpusID: opusHeadSerials = append(opusHeadSerials, pageHeader.Serial) opusHeadSampleRates[pageHeader.Serial] = binary.LittleEndian.Uint32(payload[12:]) opusHeadChannelCounts[pageHeader.Serial] = payload[9] case oggreader.HeaderOpusTags: opusTagsSerials = append(opusTagsSerials, pageHeader.Serial) default: } continue } pageOrder = append(pageOrder, pageOrderEntry{ kind: "audio", serial: pageHeader.Serial, }) audioSerials = append(audioSerials, pageHeader.Serial) audioGranules = append(audioGranules, pageHeader.GranulePosition) } assert.Equal(t, []pageOrderEntry{ {kind: string(oggreader.HeaderOpusID), serial: serial1}, {kind: string(oggreader.HeaderOpusID), serial: serial2}, {kind: string(oggreader.HeaderOpusTags), serial: serial1}, {kind: string(oggreader.HeaderOpusTags), serial: serial2}, {kind: "audio", serial: serial1}, {kind: "audio", serial: serial2}, }, pageOrder) assert.Equal(t, []uint32{serial1, serial2}, opusHeadSerials) assert.Equal(t, []uint32{serial1, serial2}, opusTagsSerials) assert.Equal(t, []uint32{serial1, serial2}, audioSerials) assert.Equal(t, []uint64{960, 960}, audioGranules) assert.Equal(t, map[uint32]uint32{ serial1: 48000, serial2: 16000, }, opusHeadSampleRates) assert.Equal(t, map[uint32]uint8{ serial1: 1, serial2: 2, }, opusHeadChannelCounts) } func TestOggWriter_NewWriterWritesOpusTags(t *testing.T) { buffer := &bytes.Buffer{} serial1 := uint32(0x01020304) serial2 := uint32(0x05060708) writer, err := NewWriter( buffer, WithVendor("pion-test-suite"), WithUserComments(UserComment{Comment: "ENCODER", Value: "pion-oggwriter"}), ) assert.NoError(t, err) assert.NotNil(t, writer) track1, err := writer.NewTrack( 1111, WithSerial(serial1), WithUserComments( UserComment{Comment: "ARTIST", Value: "Julia"}, UserComment{Comment: "TITLE", Value: "First Track"}, ), ) assert.NoError(t, err) assert.NotNil(t, track1) track2, err := writer.NewTrack( 2222, WithSerial(serial2), WithVendor("track-two-vendor"), WithUserComments( UserComment{Comment: "ARTIST", Value: "Jade Bob"}, UserComment{Comment: "ALBUM", Value: "Fanta"}, ), ) assert.NoError(t, err) assert.NotNil(t, track2) assert.NoError(t, track1.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) assert.NoError(t, track2.WriteRTP(validOpusPacketForTest(t, 2000, 2222))) tagsBySerial := opusTagsBySerial(t, buffer.Bytes()) if !assert.Contains(t, tagsBySerial, serial1) || !assert.Contains(t, tagsBySerial, serial2) { return } assert.Equal(t, "pion-test-suite", tagsBySerial[serial1].Vendor) assert.Equal(t, []oggreader.UserComment{ {Comment: "ENCODER", Value: "pion-oggwriter"}, {Comment: "ARTIST", Value: "Julia"}, {Comment: "TITLE", Value: "First Track"}, }, tagsBySerial[serial1].UserComments) assert.Equal(t, "track-two-vendor", tagsBySerial[serial2].Vendor) assert.Equal(t, []oggreader.UserComment{ {Comment: "ENCODER", Value: "pion-oggwriter"}, {Comment: "ARTIST", Value: "Jade Bob"}, {Comment: "ALBUM", Value: "Fanta"}, }, tagsBySerial[serial2].UserComments) } func TestOggWriter_RejectsInvalidOpusTags(t *testing.T) { tests := []struct { name string option WriterTrackOption errContains string }{ { name: "invalid vendor UTF-8", option: WithVendor(string([]byte{0xff})), errContains: "vendor is not valid UTF-8", }, { name: "empty user comment name", option: WithUserComments(UserComment{Comment: "", Value: "Alice"}), errContains: "invalid user comment name", }, { name: "user comment name with equals", option: WithUserComments(UserComment{Comment: "ART=IST", Value: "Alice"}), errContains: "invalid user comment name", }, { name: "invalid user comment value UTF-8", option: WithUserComments(UserComment{Comment: "ARTIST", Value: string([]byte{0xff})}), errContains: "user comment value is not valid UTF-8", }, } for _, tt := range tests { t.Run(tt.name+"/writer", func(t *testing.T) { writer, err := NewWriter(&bytes.Buffer{}, tt.option) assert.ErrorIs(t, err, errInvalidOpusTags) assert.ErrorContains(t, err, tt.errContains) assert.Nil(t, writer) }) t.Run(tt.name+"/track", func(t *testing.T) { writer, err := NewWriter(&bytes.Buffer{}) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, tt.option) assert.ErrorIs(t, err, errInvalidOpusTags) assert.ErrorContains(t, err, tt.errContains) assert.Nil(t, track) }) } } func TestOggWriter_RejectsOpusTagsHeaderLongerThanMaxInt(t *testing.T) { opusTags := OpusTags{ Vendor: "pion", UserComments: []UserComment{ {Comment: "TITLE", Value: "A long enough title"}, {Comment: "ARTIST", Value: "Pion"}, }, } headerLen := uint64(len(commentPageSignature) + 4 + len(opusTags.Vendor) + 4) for _, comment := range opusTags.UserComments { headerLen += 4 + uint64(len(comment.Comment)) + 1 + uint64(len(comment.Value)) } assert.NoError(t, validateOpusTagsWithMaxHeaderLen(opusTags, headerLen)) err := validateOpusTagsWithMaxHeaderLen(opusTags, headerLen-1) assert.ErrorIs(t, err, errInvalidOpusTags) assert.ErrorContains(t, err, "header is too long") } func TestOggWriter_NewWriterAcceptsZeroSerial(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWriter(buffer, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithSerial(0)) assert.NoError(t, err) assert.NotNil(t, track) assert.Equal(t, uint32(0), track.track.serial) assert.NoError(t, track.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) reader, err := oggreader.NewWithOptions(bytes.NewReader(buffer.Bytes())) assert.NoError(t, err) assert.NotNil(t, reader) var serials []uint32 for { _, pageHeader, parseErr := reader.ParseNextPage() if errors.Is(parseErr, io.EOF) { break } if !assert.NoError(t, parseErr) { break } serials = append(serials, pageHeader.Serial) } assert.Equal(t, []uint32{0, 0, 0}, serials) } func TestOggWriter_MultiTrackRejectsDuplicateTrackSSRC(t *testing.T) { writer, err := NewWriter(&bytes.Buffer{}, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithSerial(0x01020304)) assert.NoError(t, err) assert.NotNil(t, track) track, err = writer.NewTrack(1111, WithSerial(0x05060708)) assert.ErrorIs(t, err, errDuplicateTrackSSRC) assert.Nil(t, track) } func TestOggWriter_MultiTrackRejectsPacketSSRCMismatch(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWriter(buffer, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithSerial(0x01020304)) assert.NoError(t, err) assert.NotNil(t, track) err = track.WriteRTP(validOpusPacketForTest(t, 1000, 2222)) assert.ErrorIs(t, err, errPacketSSRCMismatch) assert.False(t, writer.started) assert.Empty(t, buffer.Bytes()) track, err = writer.NewTrack(2222, WithSerial(0x05060708)) assert.NoError(t, err) assert.NotNil(t, track) } func TestOggWriter_MultiTrackDoesNotStartUntilPacketWillBeWritten(t *testing.T) { tests := []struct { name string packet *rtp.Packet wantErr error }{ { name: "nil packet", packet: nil, wantErr: errInvalidNilPacket, }, { name: "nil payload", packet: &rtp.Packet{ Header: rtp.Header{SSRC: 1111}, }, wantErr: nil, }, { name: "empty payload", packet: &rtp.Packet{ Header: rtp.Header{SSRC: 1111}, Payload: []byte{}, }, wantErr: nil, }, { name: "wrong SSRC", packet: validOpusPacketForTest(t, 1000, 2222), wantErr: errPacketSSRCMismatch, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWriter(buffer, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track1, err := writer.NewTrack(1111, WithSerial(0x01020304)) assert.NoError(t, err) assert.NotNil(t, track1) err = track1.WriteRTP(tt.packet) if tt.wantErr == nil { assert.NoError(t, err) } else { assert.ErrorIs(t, err, tt.wantErr) } assert.False(t, writer.started) assert.Empty(t, buffer.Bytes()) track2, err := writer.NewTrack(2222, WithSerial(0x05060708)) assert.NoError(t, err) assert.NotNil(t, track2) assert.NoError(t, track2.WriteRTP(validOpusPacketForTest(t, 2000, 2222))) assert.True(t, writer.started) assert.NotEmpty(t, buffer.Bytes()) }) } } func TestOggWriter_MultiTrackRejectsDuplicateTrackSerial(t *testing.T) { writer, err := NewWriter(&bytes.Buffer{}, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithSerial(0x01020304)) assert.NoError(t, err) assert.NotNil(t, track) track, err = writer.NewTrack(2222, WithSerial(0x01020304)) assert.ErrorIs(t, err, errDuplicateTrackSerial) assert.Nil(t, track) } func TestOggWriter_MultiTrackRejectsDuplicateZeroTrackSerial(t *testing.T) { writer, err := NewWriter(&bytes.Buffer{}, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithSerial(0)) assert.NoError(t, err) assert.NotNil(t, track) track, err = writer.NewTrack(2222, WithSerial(0)) assert.ErrorIs(t, err, errDuplicateTrackSerial) assert.Nil(t, track) } func TestOggWriter_MultiTrackRejectsTrackCreationAfterWrite(t *testing.T) { writer, err := NewWriter(&bytes.Buffer{}, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track1, err := writer.NewTrack(1111, WithSerial(0x01020304)) assert.NoError(t, err) assert.NotNil(t, track1) assert.NoError(t, track1.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) track2, err := writer.NewTrack(2222, WithSerial(0x05060708)) assert.ErrorIs(t, err, errTracksStarted) assert.Nil(t, track2) } func TestOggWriter_NewWriterDoesNotSeekWithoutOption(t *testing.T) { output := &noSeekWriter{} writer, err := NewWriter(output, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithSerial(0x01020304)) assert.NoError(t, err) assert.NotNil(t, track) assert.NoError(t, track.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) assert.NoError(t, writer.Close()) assert.True(t, output.closed) assert.False(t, output.seekCalled) assert.False(t, output.writeAtCalled) } func TestOggWriter_NewWriterFinalizesUnseekableOutputWithEOS(t *testing.T) { output := &noSeekWriter{} serial1 := uint32(0x01020304) serial2 := uint32(0x05060708) writer, err := NewWriter(output, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track1, err := writer.NewTrack(1111, WithSerial(serial1)) assert.NoError(t, err) assert.NotNil(t, track1) track2, err := writer.NewTrack(2222, WithSerial(serial2)) assert.NoError(t, err) assert.NotNil(t, track2) assert.NoError(t, track1.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) assert.NoError(t, track2.WriteRTP(validOpusPacketForTest(t, 2000, 2222))) assert.NoError(t, writer.Close()) pages := parseRawOggPages(t, output.Bytes()) if !assert.Len(t, pages, 8) { return } eosPages := map[uint32]rawOggPage{} for _, page := range pages { if page.header[5]&pageHeaderTypeEndOfStream != 0 { eosPages[binary.LittleEndian.Uint32(page.header[14:18])] = page } } if !assert.Len(t, eosPages, 2) { return } for _, serial := range []uint32{serial1, serial2} { page := eosPages[serial] assert.Equal(t, uint8(pageHeaderTypeEndOfStream), page.header[5]) assert.Equal(t, uint64(960), binary.LittleEndian.Uint64(page.header[6:14])) assert.Empty(t, page.segmentTable) assert.Empty(t, page.payload) } assert.True(t, output.closed) assert.False(t, output.seekCalled) assert.False(t, output.writeAtCalled) } func TestOggWriter_NewWriterCloseWithoutRTPFinalizesUnseekableOutputWithEOS(t *testing.T) { buffer := &bytes.Buffer{} serial := uint32(0x01020304) writer, err := NewWriter(buffer, WithSampleRate(48000), WithChannelCount(2)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithSerial(serial)) assert.NoError(t, err) assert.NotNil(t, track) assert.NoError(t, writer.Close()) pages := parseRawOggPages(t, buffer.Bytes()) if !assert.Len(t, pages, 3) { return } assert.Equal(t, uint8(pageHeaderTypeBeginningOfStream), pages[0].header[5]) assert.Equal(t, uint8(pageHeaderTypeContinuationOfStream), pages[1].header[5]) assert.Equal(t, uint8(pageHeaderTypeEndOfStream), pages[2].header[5]) assert.Equal(t, serial, binary.LittleEndian.Uint32(pages[2].header[14:18])) assert.Equal(t, uint32(2), binary.LittleEndian.Uint32(pages[2].header[18:22])) assert.Zero(t, binary.LittleEndian.Uint64(pages[2].header[6:14])) assert.Empty(t, pages[2].segmentTable) assert.Empty(t, pages[2].payload) } func TestOggWriter_NewWriterCloseClosesOutputAfterStartFailure(t *testing.T) { output := &noSeekWriter{} writer, err := NewWriter(output, WithSampleRate(48000), WithChannelCount(2), WithSeekableOutput(output)) assert.NoError(t, err) assert.NotNil(t, writer) track, err := writer.NewTrack(1111, WithSerial(0x01020304)) assert.NoError(t, err) assert.NotNil(t, track) err = writer.Close() assert.ErrorIs(t, err, errUnexpectedSeek) assert.True(t, output.closed) assert.True(t, output.seekCalled) } func TestOggWriter_NewWriterCloseClosesOutputAfterEOSRewriteFailure(t *testing.T) { output := &rewriteFailingCloser{ writeAtErr: errRewriteFailed, closeErr: errCloseFailed, } writer, err := NewWriter(output, WithSampleRate(48000), WithChannelCount(2), WithSeekableOutput(output)) assert.NoError(t, err) assert.NotNil(t, writer) track1, err := writer.NewTrack(1111, WithSerial(0x01020304)) assert.NoError(t, err) assert.NotNil(t, track1) track2, err := writer.NewTrack(2222, WithSerial(0x05060708)) assert.NoError(t, err) assert.NotNil(t, track2) assert.NoError(t, track1.WriteRTP(validOpusPacketForTest(t, 1000, 1111))) assert.NoError(t, track2.WriteRTP(validOpusPacketForTest(t, 2000, 2222))) err = writer.Close() assert.ErrorIs(t, err, errRewriteFailed) assert.ErrorIs(t, err, errCloseFailed) assert.Equal(t, 2, output.writeAtCalls) assert.True(t, output.closed) } func TestOggWriter_NewWithPreservesLegacyStreamMode(t *testing.T) { file, err := os.CreateTemp(t.TempDir(), "oggwriter-*.ogg") assert.NoError(t, err) writer, err := NewWith(file, 48000, 2) assert.NoError(t, err) assert.NotNil(t, writer) assert.Nil(t, writer.fd) assert.NoError(t, writer.Close()) } func TestOggWriter_NewWithDoesNotRetainLastPayload(t *testing.T) { buffer := &bytes.Buffer{} writer, err := NewWith(buffer, 48000, 2) assert.NoError(t, err) assert.NotNil(t, writer) assert.Nil(t, writer.track.lastPayload) assert.NoError(t, writer.WriteRTP(validOpusPacketForTest(t, 1000, writer.track.serial))) assert.Nil(t, writer.track.lastPayload) assert.Zero(t, writer.track.lastGranulePosition) assert.Zero(t, writer.track.lastPageIndex) assert.Zero(t, writer.track.lastPageOffset) }