mirror of
https://github.com/pion/webrtc.git
synced 2025-12-24 11:51:03 +08:00
Parse opus mappings
This commit is contained in:
@@ -7,6 +7,7 @@ package oggreader
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
@@ -14,20 +15,20 @@ const (
|
||||
pageHeaderTypeBeginningOfStream = 0x02
|
||||
pageHeaderSignature = "OggS"
|
||||
|
||||
idPageSignature = "OpusHead"
|
||||
|
||||
pageHeaderLen = 27
|
||||
idPagePayloadLength = 19
|
||||
idPageSignature = "OpusHead"
|
||||
idPageBasePayloadLength = 19
|
||||
pageHeaderLen = 27
|
||||
)
|
||||
|
||||
var (
|
||||
errNilStream = errors.New("stream is nil")
|
||||
errBadIDPageSignature = errors.New("bad header signature")
|
||||
errBadIDPageType = errors.New("wrong header, expected beginning of stream")
|
||||
errBadIDPageLength = errors.New("payload for id page must be 19 bytes")
|
||||
errBadIDPagePayloadSignature = errors.New("bad payload signature")
|
||||
errShortPageHeader = errors.New("not enough data for payload header")
|
||||
errChecksumMismatch = errors.New("expected and actual checksum do not match")
|
||||
errNilStream = errors.New("stream is nil")
|
||||
errBadIDPageSignature = errors.New("bad header signature")
|
||||
errBadIDPageType = errors.New("wrong header, expected beginning of stream")
|
||||
errBadIDPageLength = errors.New("payload for id page must be 19 bytes")
|
||||
errBadIDPagePayloadSignature = errors.New("bad payload signature")
|
||||
errShortPageHeader = errors.New("not enough data for payload header")
|
||||
errChecksumMismatch = errors.New("expected and actual checksum do not match")
|
||||
errUnsupportedChannelMappingFamily = errors.New("unsupported channel mapping family")
|
||||
)
|
||||
|
||||
// OggReader is used to read Ogg files and return page payloads.
|
||||
@@ -43,12 +44,17 @@ type OggReader struct {
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7845.html#section-3
|
||||
type OggHeader struct {
|
||||
ChannelMap uint8
|
||||
Channels uint8
|
||||
OutputGain uint16
|
||||
PreSkip uint16
|
||||
SampleRate uint32
|
||||
Version uint8
|
||||
ChannelMap uint8
|
||||
Channels uint8
|
||||
OutputGain uint16
|
||||
PreSkip uint16
|
||||
SampleRate uint32
|
||||
Version uint8
|
||||
StreamCount uint8
|
||||
CoupledCount uint8
|
||||
// ChannelMapping we store it as a string to be comparable (maps/struct equality)
|
||||
// while still holding raw bytes.
|
||||
ChannelMapping string
|
||||
}
|
||||
|
||||
// OggPageHeader is the metadata for a Page
|
||||
@@ -97,23 +103,40 @@ func (o *OggReader) readHeaders() (*OggHeader, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header := &OggHeader{}
|
||||
if err := validatePageHeader(pageHeader, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header := parseBasicHeaderFields(payload)
|
||||
if err := parseChannelMapping(header, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return header, nil
|
||||
}
|
||||
|
||||
func validatePageHeader(pageHeader *OggPageHeader, payload []byte) error {
|
||||
if string(pageHeader.sig[:]) != pageHeaderSignature {
|
||||
return nil, errBadIDPageSignature
|
||||
return errBadIDPageSignature
|
||||
}
|
||||
|
||||
if pageHeader.headerType != pageHeaderTypeBeginningOfStream {
|
||||
return nil, errBadIDPageType
|
||||
return errBadIDPageType
|
||||
}
|
||||
|
||||
if len(payload) != idPagePayloadLength {
|
||||
return nil, errBadIDPageLength
|
||||
if len(payload) < idPageBasePayloadLength {
|
||||
return errBadIDPageLength
|
||||
}
|
||||
|
||||
if s := string(payload[:8]); s != idPageSignature {
|
||||
return nil, errBadIDPagePayloadSignature
|
||||
return errBadIDPagePayloadSignature
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBasicHeaderFields(payload []byte) *OggHeader {
|
||||
header := &OggHeader{}
|
||||
header.Version = payload[8]
|
||||
header.Channels = payload[9]
|
||||
header.PreSkip = binary.LittleEndian.Uint16(payload[10:12])
|
||||
@@ -121,7 +144,44 @@ func (o *OggReader) readHeaders() (*OggHeader, error) {
|
||||
header.OutputGain = binary.LittleEndian.Uint16(payload[16:18])
|
||||
header.ChannelMap = payload[18]
|
||||
|
||||
return header, nil
|
||||
return header
|
||||
}
|
||||
|
||||
// parseChannelMapping parses channel mapping data based on the channel map family.
|
||||
// https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1
|
||||
// family mapping of 2 and 3 are defined in https://datatracker.ietf.org/doc/html/rfc8486
|
||||
func parseChannelMapping(header *OggHeader, payload []byte) error {
|
||||
switch header.ChannelMap {
|
||||
case 0:
|
||||
return validatePayloadLength(payload, idPageBasePayloadLength)
|
||||
case 1, 2, 255:
|
||||
return parseExtendedChannelMapping(header, payload)
|
||||
case 3:
|
||||
return fmt.Errorf("%w: ambisonics family type 3 is not supported", errUnsupportedChannelMappingFamily)
|
||||
default:
|
||||
return errUnsupportedChannelMappingFamily
|
||||
}
|
||||
}
|
||||
|
||||
func validatePayloadLength(payload []byte, expectedLen int) error {
|
||||
if len(payload) != expectedLen {
|
||||
return errBadIDPageLength
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseExtendedChannelMapping(header *OggHeader, payload []byte) error {
|
||||
expectedPayloadLen := 21 + int(header.Channels)
|
||||
if err := validatePayloadLength(payload, expectedPayloadLen); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.StreamCount = payload[19]
|
||||
header.CoupledCount = payload[20]
|
||||
header.ChannelMapping = string(payload[21 : 21+header.Channels])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseNextPage reads from stream and returns Ogg page payload, header,
|
||||
|
||||
@@ -26,6 +26,90 @@ func buildOggContainer() []byte {
|
||||
}
|
||||
}
|
||||
|
||||
// buildSurroundOggContainerShort has mapping family 1 but omits the mapping table (invalid length).
|
||||
func buildSurroundOggContainerShort() []byte {
|
||||
return []byte{
|
||||
0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x58, 0x49, 0xac, 0xe2, 0x00, 0x00,
|
||||
0x00, 0x00, 0xc1, 0xa8, 0x7d, 0x4e, 0x01, 0x13, 0x4f, 0x70,
|
||||
0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x06, 0x38, 0x01,
|
||||
0x80, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
}
|
||||
}
|
||||
|
||||
// buildUnknownMappingFamilyContainer creates an ID page with an unrecognized channel mapping family.
|
||||
func buildUnknownMappingFamilyContainer(mappingFamily, channels uint8) []byte {
|
||||
payload := []byte{
|
||||
0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, // "OpusHead"
|
||||
0x01, // version
|
||||
channels, // channel count
|
||||
0x38, 0x01, // preskip (0x0138)
|
||||
0x80, 0xbb, 0x00, 0x00, // sample rate (48000)
|
||||
0x00, 0x00, // output gain
|
||||
mappingFamily,
|
||||
}
|
||||
|
||||
segmentTable := []byte{byte(len(payload))}
|
||||
|
||||
header := []byte{
|
||||
0x4f, 0x67, 0x67, 0x53, // "OggS"
|
||||
0x00, // version
|
||||
0x02, // beginning of stream
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // granule position
|
||||
0x00, 0x00, 0x00, 0x00, // bitstream serial number
|
||||
0x00, 0x00, 0x00, 0x00, // page sequence number
|
||||
0x00, 0x00, 0x00, 0x00, // checksum (ignored with checksum disabled)
|
||||
0x01, // page segments
|
||||
}
|
||||
|
||||
packet := make([]byte, 0, len(header)+len(segmentTable)+len(payload))
|
||||
packet = append(packet, header...)
|
||||
packet = append(packet, segmentTable...)
|
||||
packet = append(packet, payload...)
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
// buildChannelMappingFamilyContainer builds an Opus ID page for mapping families that
|
||||
// follow the Figure 3 layout (families 1, 2, 3, 255).
|
||||
func buildChannelMappingFamilyContainer(
|
||||
mappingFamily, channels, streamCount, coupledCount uint8,
|
||||
mapping []byte,
|
||||
) []byte {
|
||||
payload := []byte{
|
||||
0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, // "OpusHead"
|
||||
0x01, // version
|
||||
channels, // channel count
|
||||
0x38, 0x01, // preskip (0x0138)
|
||||
0x80, 0xbb, 0x00, 0x00, // sample rate (48000)
|
||||
0x00, 0x00, // output gain
|
||||
mappingFamily,
|
||||
streamCount,
|
||||
coupledCount,
|
||||
}
|
||||
payload = append(payload, mapping...)
|
||||
|
||||
segmentTable := []byte{byte(len(payload))}
|
||||
|
||||
header := []byte{
|
||||
0x4f, 0x67, 0x67, 0x53, // "OggS"
|
||||
0x00, // version
|
||||
0x02, // header type (beginning of stream)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // granule position
|
||||
0x00, 0x00, 0x00, 0x00, // bitstream serial number
|
||||
0x00, 0x00, 0x00, 0x00, // page sequence number
|
||||
0x00, 0x00, 0x00, 0x00, // checksum (ignored when checksum disabled)
|
||||
0x01, // page segments
|
||||
}
|
||||
|
||||
packet := make([]byte, 0, len(header)+len(segmentTable)+len(payload))
|
||||
packet = append(packet, header...)
|
||||
packet = append(packet, segmentTable...)
|
||||
packet = append(packet, payload...)
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
func TestOggReader_ParseValidHeader(t *testing.T) {
|
||||
reader, header, err := NewWith(bytes.NewReader(buildOggContainer()))
|
||||
assert.NoError(t, err)
|
||||
@@ -102,4 +186,131 @@ func TestOggReader_ParseErrors(t *testing.T) {
|
||||
_, _, err := NewWith(bytes.NewReader(ogg))
|
||||
assert.Equal(t, err, errChecksumMismatch)
|
||||
})
|
||||
|
||||
t.Run("Invalid Multichannel ID Page Payload Length", func(t *testing.T) {
|
||||
_, _, err := newWith(bytes.NewReader(buildSurroundOggContainerShort()), false)
|
||||
assert.Equal(t, err, errBadIDPageLength)
|
||||
})
|
||||
|
||||
t.Run("Unsupported Channel Mapping Family", func(t *testing.T) {
|
||||
_, _, err := newWith(bytes.NewReader(buildUnknownMappingFamilyContainer(4, 2)), false)
|
||||
assert.Equal(t, err, errUnsupportedChannelMappingFamily)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOggReader_ChannelMappingFamily1(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
channels uint8
|
||||
streams uint8
|
||||
coupled uint8
|
||||
channelMap []byte
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{name: "1-mono", channels: 1, streams: 1, coupled: 0, channelMap: []byte{0}},
|
||||
{name: "2-stereo", channels: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}},
|
||||
{name: "3-linear-surround", channels: 3, streams: 2, coupled: 1, channelMap: []byte{0, 2, 1}},
|
||||
{name: "4-quad", channels: 4, streams: 2, coupled: 2, channelMap: []byte{0, 1, 2, 3}},
|
||||
{name: "5-5.0", channels: 5, streams: 3, coupled: 2, channelMap: []byte{0, 1, 2, 3, 4}},
|
||||
{name: "6-5.1", channels: 6, streams: 4, coupled: 2, channelMap: []byte{0, 4, 1, 2, 3, 5}},
|
||||
{name: "7-6.1", channels: 7, streams: 4, coupled: 3, channelMap: []byte{0, 1, 2, 3, 4, 5, 6}},
|
||||
{name: "8-7.1", channels: 8, streams: 5, coupled: 3, channelMap: []byte{0, 1, 2, 3, 4, 5, 6, 7}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reader, header, err := newWith(bytes.NewReader(
|
||||
buildChannelMappingFamilyContainer(1, tc.channels, tc.streams, tc.coupled, tc.channelMap),
|
||||
), false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, reader)
|
||||
assert.NotNil(t, header)
|
||||
|
||||
assert.EqualValues(t, 1, header.Version)
|
||||
assert.EqualValues(t, tc.channels, header.Channels)
|
||||
assert.EqualValues(t, 0x138, header.PreSkip)
|
||||
assert.EqualValues(t, 48e3, header.SampleRate)
|
||||
assert.EqualValues(t, 0, header.OutputGain)
|
||||
assert.EqualValues(t, 1, header.ChannelMap)
|
||||
assert.EqualValues(t, tc.streams, header.StreamCount)
|
||||
assert.EqualValues(t, tc.coupled, header.CoupledCount)
|
||||
assert.Equal(t, string(tc.channelMap), header.ChannelMapping)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOggReader_KnownChannelMappingFamilies(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mappingFamily uint8
|
||||
channels uint8
|
||||
streams uint8
|
||||
coupled uint8
|
||||
channelMap []byte
|
||||
}{
|
||||
{name: "family-2", mappingFamily: 2, channels: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}},
|
||||
{name: "family-255", mappingFamily: 255, channels: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reader, header, err := newWith(bytes.NewReader(
|
||||
buildChannelMappingFamilyContainer(tc.mappingFamily, tc.channels, tc.streams, tc.coupled, tc.channelMap),
|
||||
), false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, reader)
|
||||
assert.NotNil(t, header)
|
||||
|
||||
assert.EqualValues(t, tc.mappingFamily, header.ChannelMap)
|
||||
assert.EqualValues(t, tc.channels, header.Channels)
|
||||
assert.EqualValues(t, 0x138, header.PreSkip)
|
||||
assert.EqualValues(t, 48e3, header.SampleRate)
|
||||
assert.EqualValues(t, 0, header.OutputGain)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOggReader_ParseExtraFieldsForNonZeroMappingFamily(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mappingFamily uint8
|
||||
channels uint8
|
||||
streams uint8
|
||||
coupled uint8
|
||||
channelMap []byte
|
||||
}{
|
||||
{name: "family-1-stereo", mappingFamily: 1, channels: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}},
|
||||
{name: "family-1-5.1", mappingFamily: 1, channels: 6, streams: 4, coupled: 2, channelMap: []byte{0, 4, 1, 2, 3, 5}},
|
||||
{
|
||||
name: "family-1-7.1",
|
||||
mappingFamily: 1,
|
||||
channels: 8,
|
||||
streams: 5,
|
||||
coupled: 3,
|
||||
channelMap: []byte{0, 1, 2, 3, 4, 5, 6, 7},
|
||||
},
|
||||
{name: "family-2", mappingFamily: 2, channels: 4, streams: 2, coupled: 2, channelMap: []byte{0, 1, 2, 3}},
|
||||
{name: "family-255", mappingFamily: 255, channels: 5, streams: 3, coupled: 2, channelMap: []byte{0, 1, 2, 3, 4}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
reader, header, err := newWith(bytes.NewReader(
|
||||
buildChannelMappingFamilyContainer(tc.mappingFamily, tc.channels, tc.streams, tc.coupled, tc.channelMap),
|
||||
), false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, reader)
|
||||
assert.NotNil(t, header)
|
||||
|
||||
assert.EqualValues(t, tc.mappingFamily, header.ChannelMap)
|
||||
assert.EqualValues(t, tc.channels, header.Channels)
|
||||
assert.EqualValues(t, tc.streams, header.StreamCount)
|
||||
assert.EqualValues(t, tc.coupled, header.CoupledCount)
|
||||
assert.Equal(t, string(tc.channelMap), header.ChannelMapping)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user