Files
upstream-pion-webrtc/pkg/media/oggwriter/oggwriter_test.go
upstream-mirror 8acb1a3a29
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 20:06:07 +05:00

1449 lines
39 KiB
Go

// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// 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)
}