Files
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

1361 lines
34 KiB
Go

// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package oggwriter implements OGG media container writer
package oggwriter
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"os"
"sync"
"unicode/utf8"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/webrtc/v4/internal/util"
)
const (
pageHeaderTypeContinuationOfStream = 0x00
pageHeaderTypeContinuationOfPacket = 0x01
pageHeaderTypeBeginningOfStream = 0x02
pageHeaderTypeEndOfStream = 0x04
defaultPreSkip = 3840 // 3840 recommended in the RFC
defaultSampleRate = 48000
opusGranuleSampleRate = 48000
maxOpusPacketSamples = opusGranuleSampleRate * 120 / 1000
defaultChannelCount = 2
channelMappingFamily0 = 0
channelMappingFamily1 = 1
channelMappingFamily2 = 2
channelMappingFamily255 = 255
idPageSignature = "OpusHead"
commentPageSignature = "OpusTags"
defaultVendor = "pion"
pageHeaderSignature = "OggS"
pageHeaderSize = 27
maxOggPageSegments = 255
noGranulePosition = ^uint64(0)
maxUint32Length = uint64(1<<32 - 1)
)
var (
errFileNotOpened = errors.New("file not opened")
errOutputNotOpened = errors.New("output not opened")
errInvalidNilPacket = errors.New("invalid nil packet")
errDuplicateTrackSSRC = errors.New("duplicate Ogg track SSRC")
errDuplicateTrackSerial = errors.New("duplicate Ogg track serial")
errTracksStarted = errors.New("cannot add Ogg tracks after writing has started")
errPacketSSRCMismatch = errors.New("RTP packet SSRC does not match Ogg track SSRC")
errInvalidOpusPacket = errors.New("invalid Opus packet")
errInvalidChannelCount = errors.New("invalid channel count")
errInvalidChannelMap = errors.New("invalid channel mapping")
errInvalidOpusTags = errors.New("invalid OpusTags")
)
type pageRewriter interface {
io.Seeker
io.WriterAt
}
type writerConfig struct {
sampleRate uint32
channelMapping channelMapping
pageRewriter pageRewriter
opusTags OpusTags
}
type trackConfig struct {
sampleRate uint32
channelMapping channelMapping
channelMappingSet bool
serial uint32
serialSet bool
opusTags OpusTags
}
type channelMapping struct {
family uint8
channelCount uint8
streamCount uint8
coupledCount uint8
mapping []byte
}
// WriterOption configures a multiplexed Ogg writer.
type WriterOption interface {
applyWriterConfig(*writerConfig) error
}
// TrackOption configures a logical Opus stream in a multiplexed Ogg container.
type TrackOption interface {
applyTrackConfig(*trackConfig) error
}
// WriterTrackOption configures both Writer defaults and per-track overrides.
type WriterTrackOption interface {
WriterOption
TrackOption
}
type writerOptionFunc func(*writerConfig) error
func (f writerOptionFunc) applyWriterConfig(config *writerConfig) error {
return f(config)
}
type trackOptionFunc func(*trackConfig) error
func (f trackOptionFunc) applyTrackConfig(config *trackConfig) error {
return f(config)
}
type sampleRateOption uint32
func (o sampleRateOption) applyWriterConfig(config *writerConfig) error {
config.sampleRate = uint32(o)
return nil
}
func (o sampleRateOption) applyTrackConfig(config *trackConfig) error {
config.sampleRate = uint32(o)
return nil
}
type (
channelCountOption uint16
channelMapOption struct {
family uint8
streamCount uint8
coupledCount uint8
mapping []byte
}
vendorOption string
userCommentsOption []UserComment
)
func (o channelCountOption) applyWriterConfig(config *writerConfig) error {
mapping, err := defaultChannelMapping(uint16(o))
if err != nil {
return err
}
config.channelMapping = mapping
return nil
}
func (o channelCountOption) applyTrackConfig(config *trackConfig) error {
mapping, err := defaultChannelMapping(uint16(o))
if err != nil {
return err
}
config.channelMapping = mapping
config.channelMappingSet = true
return nil
}
func (o channelMapOption) applyWriterConfig(config *writerConfig) error {
mapping, err := validateChannelMapping(o.family, o.streamCount, o.coupledCount, o.mapping)
if err != nil {
return err
}
config.channelMapping = mapping
return nil
}
func (o channelMapOption) applyTrackConfig(config *trackConfig) error {
mapping, err := validateChannelMapping(o.family, o.streamCount, o.coupledCount, o.mapping)
if err != nil {
return err
}
config.channelMapping = mapping
config.channelMappingSet = true
return nil
}
func (o vendorOption) applyWriterConfig(config *writerConfig) error {
return applyVendor(&config.opusTags, string(o))
}
func (o vendorOption) applyTrackConfig(config *trackConfig) error {
return applyVendor(&config.opusTags, string(o))
}
func (o userCommentsOption) applyWriterConfig(config *writerConfig) error {
return applyUserComments(&config.opusTags, []UserComment(o))
}
func (o userCommentsOption) applyTrackConfig(config *trackConfig) error {
return applyUserComments(&config.opusTags, []UserComment(o))
}
// OpusTags is the metadata for an OpusTags page.
// https://datatracker.ietf.org/doc/html/rfc7845#section-5.2
type OpusTags struct {
Vendor string
UserComments []UserComment
}
// UserComment is a key-value pair of a Vorbis comment.
type UserComment struct {
Comment string
Value string
}
type oggTrack struct {
sampleRate uint32
channelMapping channelMapping
preSkip uint16
serial uint32
opusTags OpusTags
pageIndex uint32
previousGranulePosition uint64
lastPayload []byte
lastGranulePosition uint64
lastPageIndex uint32
lastPageOffset int64
lastPageHeaderType uint8
lastPageWritten bool
}
type oggPage struct {
data []byte
payload []byte
headerType uint8
granulePos uint64
pageIndex uint32
}
// OggWriter is used to take RTP packets and write them to a single-track OGG.
type OggWriter struct {
mu sync.Mutex
stream io.Writer
fd *os.File
checksumTable *[256]uint32
track *oggTrack
}
// Writer is used to write multiple logical Opus streams into one OGG.
// Tracks are added before writing starts, and each returned Track owns WriteRTP.
type Writer struct {
mu sync.Mutex
stream io.Writer
pageRewriter pageRewriter
sampleRate uint32
channelMapping channelMapping
opusTags OpusTags
checksumTable *[256]uint32
tracks map[uint32]*Track
trackOrder []*Track
started bool
}
// Track writes RTP packets for a single logical Opus stream in a Writer.
type Track struct {
parent *Writer
ssrc uint32
track *oggTrack
}
// WithSerial sets the Ogg bitstream serial number for a track.
func WithSerial(serial uint32) TrackOption {
return trackOptionFunc(func(config *trackConfig) error {
config.serial = serial
config.serialSet = true
return nil
})
}
// WithSampleRate sets the default sample rate for a Writer or overrides it for a Track.
func WithSampleRate(sampleRate uint32) WriterTrackOption {
return sampleRateOption(sampleRate)
}
// WithChannelCount sets the default channel count for a Writer or overrides it for a Track.
// Mono and stereo are written with Opus channel mapping family 0.
// For non-zero single-stream channel mapping families, use WithChannelMapping.
func WithChannelCount(channelCount uint16) WriterTrackOption {
return channelCountOption(channelCount)
}
// WithChannelMapping sets an explicit Opus channel mapping family for a Writer or Track.
// The channel count is derived from len(mapping). Only single-stream mappings are supported
// because WriteRTP accepts one Opus packet per Ogg packet. Family 1 supports mono/stereo
// Vorbis mappings, family 2 supports zero-order ambisonics, and family 255 supports
// arbitrary single-stream mappings.
func WithChannelMapping(family, streamCount, coupledCount uint8, mapping []byte) WriterTrackOption {
return channelMapOption{
family: family,
streamCount: streamCount,
coupledCount: coupledCount,
mapping: append([]byte(nil), mapping...),
}
}
// WithVendor sets the OpusTags vendor string for a Writer or overrides it for a Track.
func WithVendor(vendor string) WriterTrackOption {
return vendorOption(vendor)
}
// WithUserComments adds OpusTags user comments for a Writer or Track.
func WithUserComments(comments ...UserComment) WriterTrackOption {
return userCommentsOption(cloneUserComments(comments))
}
// WithSeekableOutput enables close-time Ogg page rewrites for outputs that support Seek and WriteAt.
func WithSeekableOutput(output interface {
io.Seeker
io.WriterAt
},
) WriterOption {
return writerOptionFunc(func(config *writerConfig) error {
config.pageRewriter = output
return nil
})
}
// New builds a new single-track OGG Opus writer.
func New(fileName string, sampleRate uint32, channelCount uint16) (*OggWriter, error) {
config, err := newTrackConfig(sampleRate, channelCount)
if err != nil {
return nil, err
}
file, err := os.Create(fileName) //nolint:gosec
if err != nil {
return nil, err
}
writer, err := newWith(file, file, config)
if err != nil {
return nil, errors.Join(err, file.Close())
}
return writer, nil
}
// NewWith initializes a new single-track OGG Opus writer with an io.Writer output.
func NewWith(out io.Writer, sampleRate uint32, channelCount uint16) (*OggWriter, error) {
config, err := newTrackConfig(sampleRate, channelCount)
if err != nil {
return nil, err
}
return newWith(out, nil, config)
}
func newWith(out io.Writer, fd *os.File, config *trackConfig) (*OggWriter, error) {
if out == nil {
return nil, errFileNotOpened
}
serial := config.serial
if !config.serialSet {
serial = util.RandUint32()
}
writer := &OggWriter{
stream: out,
fd: fd,
checksumTable: generateChecksumTable(),
track: newTrackState(config.sampleRate, config.channelMapping, serial, config.opusTags),
}
if err := writer.writeTrackHeaders(writer.track); err != nil {
return nil, err
}
return writer, nil
}
// NewWriter creates a new multi-track OGG Opus writer with an io.Writer output.
// Close finalizes each logical stream with EOS. Without WithSeekableOutput,
// Close appends nil EOS pages because previously written pages cannot be rewritten.
func NewWriter(out io.Writer, opts ...WriterOption) (*Writer, error) {
if out == nil {
return nil, errOutputNotOpened
}
mapping, err := defaultChannelMapping(defaultChannelCount)
if err != nil {
return nil, err
}
config := &writerConfig{
sampleRate: defaultSampleRate,
channelMapping: mapping,
opusTags: defaultOpusTags(),
}
for _, opt := range opts {
if err := opt.applyWriterConfig(config); err != nil {
return nil, err
}
}
if err := validateOpusTags(config.opusTags); err != nil {
return nil, err
}
writer := &Writer{
stream: out,
pageRewriter: config.pageRewriter,
sampleRate: config.sampleRate,
channelMapping: cloneChannelMapping(config.channelMapping),
opusTags: cloneOpusTags(config.opusTags),
checksumTable: generateChecksumTable(),
tracks: map[uint32]*Track{},
}
return writer, nil
}
// NewTrack registers a logical Opus stream and returns its Track.
// It must be called before the first packet is written.
func (w *Writer) NewTrack(ssrc uint32, opts ...TrackOption) (*Track, error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.stream == nil {
return nil, errOutputNotOpened
}
if w.started {
return nil, errTracksStarted
}
if _, ok := w.tracks[ssrc]; ok {
return nil, errDuplicateTrackSSRC
}
config := &trackConfig{
sampleRate: w.sampleRate,
channelMapping: cloneChannelMapping(w.channelMapping),
channelMappingSet: true,
opusTags: cloneOpusTags(w.opusTags),
}
for _, opt := range opts {
if err := opt.applyTrackConfig(config); err != nil {
return nil, err
}
}
if err := validateOpusTags(config.opusTags); err != nil {
return nil, err
}
serial := config.serial
if !config.serialSet {
serial = w.allocateSerial()
} else if w.serialInUse(serial) {
return nil, errDuplicateTrackSerial
}
track := &Track{
parent: w,
ssrc: ssrc,
track: newTrackState(config.sampleRate, config.channelMapping, serial, config.opusTags),
}
w.tracks[ssrc] = track
w.trackOrder = append(w.trackOrder, track)
return track, nil
}
// WriteRTP adds a new packet and writes the appropriate headers for it.
func (w *OggWriter) WriteRTP(packet *rtp.Packet) error {
w.mu.Lock()
defer w.mu.Unlock()
if w.stream == nil {
return errFileNotOpened
}
return w.writeRTP(w.track, packet)
}
// WriteRTP adds a packet to a single logical stream inside a Writer.
func (w *Track) WriteRTP(packet *rtp.Packet) error {
parent := w.parent
parent.mu.Lock()
defer parent.mu.Unlock()
if parent.stream == nil {
return errOutputNotOpened
}
if packet == nil {
return errInvalidNilPacket
}
if packet.SSRC != w.ssrc {
return errPacketSSRCMismatch
}
payload, shouldWrite, err := opusPayloadFromPacket(packet)
if err != nil {
return err
}
if !shouldWrite {
return nil
}
if err := parent.startLocked(); err != nil {
return err
}
return parent.writeOpusPayload(w.track, payload)
}
// Close stops single-track recording.
func (w *OggWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
defer func() {
w.fd = nil
w.stream = nil
}()
if w.fd == nil {
if closer, ok := w.stream.(io.Closer); ok {
return closer.Close()
}
return nil
}
closeErr := markTrackEndOfStream(w.fd, w.checksumTable, w.track)
return errors.Join(closeErr, w.fd.Close())
}
// Close stops multi-track recording.
func (w *Writer) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
defer func() {
w.pageRewriter = nil
w.stream = nil
}()
if w.stream == nil {
return nil
}
closeErr := w.startLocked()
if w.pageRewriter != nil {
for _, track := range w.trackOrder {
if err := markTrackEndOfStream(w.pageRewriter, w.checksumTable, track.track); err != nil {
closeErr = errors.Join(closeErr, err)
}
}
} else {
for _, track := range w.trackOrder {
if err := writeNilEndOfStreamPage(w.stream, w.checksumTable, track.track); err != nil {
closeErr = errors.Join(closeErr, err)
}
}
}
if closer, ok := w.stream.(io.Closer); ok {
closeErr = errors.Join(closeErr, closer.Close())
}
return closeErr
}
func newTrackConfig(sampleRate uint32, channelCount uint16) (*trackConfig, error) {
config := &trackConfig{
sampleRate: sampleRate,
opusTags: defaultOpusTags(),
}
mapping, channelMappingErr := defaultChannelMapping(channelCount)
if channelMappingErr == nil {
config.channelMapping = mapping
config.channelMappingSet = true
}
if !config.channelMappingSet {
return nil, channelMappingErr
}
if err := validateOpusTags(config.opusTags); err != nil {
return nil, err
}
return config, nil
}
func newTrackState(sampleRate uint32, mapping channelMapping, serial uint32, opusTags OpusTags) *oggTrack {
return &oggTrack{
sampleRate: sampleRate,
channelMapping: cloneChannelMapping(mapping),
preSkip: defaultPreSkip,
serial: serial,
opusTags: cloneOpusTags(opusTags),
}
}
func defaultChannelMapping(channelCount uint16) (channelMapping, error) {
switch channelCount {
case 1:
return channelMapping{
family: channelMappingFamily0,
channelCount: 1,
streamCount: 1,
coupledCount: 0,
}, nil
case 2:
return channelMapping{
family: channelMappingFamily0,
channelCount: 2,
streamCount: 1,
coupledCount: 1,
}, nil
default:
return channelMapping{}, errInvalidChannelCount
}
}
func validateChannelMapping(family, streamCount, coupledCount uint8, mapping []byte) (channelMapping, error) {
if err := validateChannelMappingFamily(family); err != nil {
return channelMapping{}, err
}
if err := validateChannelMappingLayout(streamCount, coupledCount, mapping); err != nil {
return channelMapping{}, err
}
if err := validateFamilySpecificChannelMapping(family, streamCount, coupledCount, mapping); err != nil {
return channelMapping{}, err
}
return channelMapping{
family: family,
channelCount: uint8(len(mapping)), //nolint:gosec // validated <= MaxUint8.
streamCount: streamCount,
coupledCount: coupledCount,
mapping: append([]byte(nil), mapping...),
}, nil
}
func validateChannelMappingFamily(family uint8) error {
switch family {
case channelMappingFamily1, channelMappingFamily2, channelMappingFamily255:
return nil
default:
return fmt.Errorf("%w: unsupported family %d", errInvalidChannelMap, family)
}
}
func validateChannelMappingLayout(streamCount, coupledCount uint8, mapping []byte) error {
if len(mapping) == 0 || len(mapping) > math.MaxUint8 {
return errInvalidChannelCount
}
if streamCount != 1 {
return fmt.Errorf("%w: stream count must be one", errInvalidChannelMap)
}
if coupledCount > streamCount {
return fmt.Errorf("%w: coupled count exceeds stream count", errInvalidChannelMap)
}
decodedChannels := int(streamCount) + int(coupledCount)
for _, channel := range mapping {
if channel != math.MaxUint8 && int(channel) >= decodedChannels {
return fmt.Errorf("%w: channel map entry is out of range", errInvalidChannelMap)
}
}
return nil
}
func validateFamilySpecificChannelMapping(
family uint8,
streamCount uint8,
coupledCount uint8,
mapping []byte,
) error {
switch family {
case channelMappingFamily1:
return validateVorbisChannelMapping(streamCount, coupledCount, mapping)
case channelMappingFamily2:
if len(mapping) != 1 || streamCount != 1 || coupledCount != 0 || mapping[0] != 0 {
return fmt.Errorf("%w: invalid family 2 mapping", errInvalidChannelMap)
}
}
return nil
}
func validateVorbisChannelMapping(streamCount, coupledCount uint8, mapping []byte) error {
var wantStreams, wantCoupled uint8
var wantMapping []byte
switch len(mapping) {
case 1:
wantStreams = 1
wantCoupled = 0
wantMapping = []byte{0}
case 2:
wantStreams = 1
wantCoupled = 1
wantMapping = []byte{0, 1}
default:
return fmt.Errorf("%w: unsupported family 1 channel count", errInvalidChannelMap)
}
if streamCount != wantStreams || coupledCount != wantCoupled || !bytes.Equal(mapping, wantMapping) {
return fmt.Errorf("%w: invalid family 1 mapping", errInvalidChannelMap)
}
return nil
}
func cloneChannelMapping(mapping channelMapping) channelMapping {
mapping.mapping = append([]byte(nil), mapping.mapping...)
return mapping
}
func defaultOpusTags() OpusTags {
return OpusTags{Vendor: defaultVendor}
}
func cloneOpusTags(opusTags OpusTags) OpusTags {
opusTags.UserComments = cloneUserComments(opusTags.UserComments)
return opusTags
}
func cloneUserComments(comments []UserComment) []UserComment {
if len(comments) == 0 {
return nil
}
return append([]UserComment(nil), comments...)
}
func applyVendor(opusTags *OpusTags, vendor string) error {
if err := validateOpusTagString("vendor", vendor); err != nil {
return err
}
opusTags.Vendor = vendor
return nil
}
func applyUserComments(opusTags *OpusTags, comments []UserComment) error {
if err := validateUserComments(comments); err != nil {
return err
}
opusTags.UserComments = append(opusTags.UserComments, comments...)
return nil
}
func validateOpusTags(opusTags OpusTags) error {
return validateOpusTagsWithMaxHeaderLen(opusTags, uint64(math.MaxInt))
}
func validateOpusTagsWithMaxHeaderLen(opusTags OpusTags, maxHeaderLen uint64) error {
if err := validateOpusTagString("vendor", opusTags.Vendor); err != nil {
return err
}
if uint64(len(opusTags.UserComments)) > maxUint32Length {
return fmt.Errorf("%w: too many user comments", errInvalidOpusTags)
}
if err := validateUserComments(opusTags.UserComments); err != nil {
return err
}
return validateOpusTagsHeaderLen(opusTags, maxHeaderLen)
}
func validateOpusTagsHeaderLen(opusTags OpusTags, maxHeaderLen uint64) error {
headerLen := uint64(len(commentPageSignature)) + 4 + uint64(len(opusTags.Vendor)) + 4
if headerLen > maxHeaderLen {
return fmt.Errorf("%w: header is too long", errInvalidOpusTags)
}
for _, comment := range opusTags.UserComments {
userCommentLen := uint64(len(comment.Comment)) + 1 + uint64(len(comment.Value))
commentFieldLen := 4 + userCommentLen
if commentFieldLen > maxHeaderLen-headerLen {
return fmt.Errorf("%w: header is too long", errInvalidOpusTags)
}
headerLen += commentFieldLen
}
return nil
}
func validateUserComments(comments []UserComment) error {
for _, comment := range comments {
if err := validateUserComment(comment); err != nil {
return err
}
}
return nil
}
func validateUserComment(comment UserComment) error {
if !isValidCommentName(comment.Comment) {
return fmt.Errorf("%w: invalid user comment name", errInvalidOpusTags)
}
if err := validateOpusTagString("user comment value", comment.Value); err != nil {
return err
}
if uint64(len(comment.Comment))+1+uint64(len(comment.Value)) > maxUint32Length {
return fmt.Errorf("%w: user comment is too long", errInvalidOpusTags)
}
return nil
}
func validateOpusTagString(field, value string) error {
if !utf8.ValidString(value) {
return fmt.Errorf("%w: %s is not valid UTF-8", errInvalidOpusTags, field)
}
if uint64(len(value)) > maxUint32Length {
return fmt.Errorf("%w: %s is too long", errInvalidOpusTags, field)
}
return nil
}
func isValidCommentName(comment string) bool {
if comment == "" {
return false
}
for i := 0; i < len(comment); i++ {
b := comment[i]
if b < 0x20 || b > 0x7d || b == '=' {
return false
}
}
return true
}
func (w *Writer) serialInUse(serial uint32) bool {
for _, track := range w.trackOrder {
if track.track.serial == serial {
return true
}
}
return false
}
func (w *Writer) allocateSerial() uint32 {
serial := util.RandUint32()
for w.serialInUse(serial) {
serial = util.RandUint32()
}
return serial
}
func (w *OggWriter) writeTrackHeaders(track *oggTrack) error {
return writeTrackHeaders(track, func(track *oggTrack, payload []byte, headerType uint8, granulePos uint64) error {
return writePage(w.stream, pageRewriterForFile(w.fd), w.checksumTable, track, payload, headerType, granulePos)
})
}
func (w *Writer) writeTrackIDHeader(track *oggTrack) error {
return writeTrackIDHeader(track, func(track *oggTrack, payload []byte, headerType uint8, granulePos uint64) error {
return writePage(w.stream, w.pageRewriter, w.checksumTable, track, payload, headerType, granulePos)
})
}
func (w *Writer) writeTrackCommentHeader(track *oggTrack) error {
return writeTrackCommentHeader(
track,
func(track *oggTrack, payload []byte, headerType uint8, granulePos uint64) error {
return writePage(w.stream, w.pageRewriter, w.checksumTable, track, payload, headerType, granulePos)
},
)
}
func writeTrackHeaders(track *oggTrack, writePageFunc func(*oggTrack, []byte, uint8, uint64) error) error {
if err := writeTrackIDHeader(track, writePageFunc); err != nil {
return err
}
return writeTrackCommentHeader(track, writePageFunc)
}
func writeTrackIDHeader(track *oggTrack, writePageFunc func(*oggTrack, []byte, uint8, uint64) error) error {
return writePageFunc(track, buildIDHeader(track), pageHeaderTypeBeginningOfStream, 0)
}
func writeTrackCommentHeader(track *oggTrack, writePageFunc func(*oggTrack, []byte, uint8, uint64) error) error {
return writePageFunc(track, buildCommentHeader(track.opusTags), pageHeaderTypeContinuationOfStream, 0)
}
func buildIDHeader(track *oggTrack) []byte {
channelMapping := track.channelMapping
headerLen := 19
if channelMapping.family != channelMappingFamily0 {
headerLen += 2 + int(channelMapping.channelCount)
}
oggIDHeader := make([]byte, headerLen)
copy(oggIDHeader[0:], idPageSignature) // Magic Signature 'OpusHead'
oggIDHeader[8] = 1 // Version
oggIDHeader[9] = channelMapping.channelCount // Channel count
binary.LittleEndian.PutUint16(oggIDHeader[10:], track.preSkip) // pre-skip
binary.LittleEndian.PutUint32(oggIDHeader[12:], track.sampleRate)
binary.LittleEndian.PutUint16(oggIDHeader[16:], 0) // output gain
oggIDHeader[18] = channelMapping.family
if channelMapping.family != channelMappingFamily0 {
oggIDHeader[19] = channelMapping.streamCount
oggIDHeader[20] = channelMapping.coupledCount
copy(oggIDHeader[21:], channelMapping.mapping)
}
return oggIDHeader
}
func buildCommentHeader(opusTags OpusTags) []byte {
payloadLen := len(commentPageSignature) + 4 + len(opusTags.Vendor) + 4
for _, comment := range opusTags.UserComments {
payloadLen += 4 + len(comment.Comment) + 1 + len(comment.Value)
}
oggCommentHeader := make([]byte, payloadLen)
copy(oggCommentHeader[0:], commentPageSignature) // Magic Signature 'OpusTags'
pos := len(commentPageSignature)
binary.LittleEndian.PutUint32(
oggCommentHeader[pos:], uint32(len(opusTags.Vendor)), //nolint:gosec // validated to fit in uint32.
)
pos += 4
copy(oggCommentHeader[pos:], opusTags.Vendor)
pos += len(opusTags.Vendor)
binary.LittleEndian.PutUint32(
oggCommentHeader[pos:], uint32(len(opusTags.UserComments)), //nolint:gosec // validated to fit in uint32.
)
pos += 4
for _, comment := range opusTags.UserComments {
userCommentLen := len(comment.Comment) + 1 + len(comment.Value)
binary.LittleEndian.PutUint32(
oggCommentHeader[pos:], uint32(userCommentLen), //nolint:gosec // validated to fit in uint32.
)
pos += 4
copy(oggCommentHeader[pos:], comment.Comment)
pos += len(comment.Comment)
oggCommentHeader[pos] = '='
pos++
copy(oggCommentHeader[pos:], comment.Value)
pos += len(comment.Value)
}
return oggCommentHeader
}
func writePage(
stream io.Writer,
rewriter pageRewriter,
checksumTable *[256]uint32,
track *oggTrack,
payload []byte,
headerType uint8,
granulePos uint64,
) error {
pages := createPagesForSerial(checksumTable, payload, headerType, granulePos, track.serial, track.pageIndex)
var offset int64
if rewriter != nil {
var err error
offset, err = rewriter.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
}
for i, page := range pages {
pageOffset := offset
if err := writeToStream(stream, page.data); err != nil {
return err
}
if rewriter != nil {
if i == len(pages)-1 {
track.lastPageOffset = pageOffset
track.lastPayload = append(track.lastPayload[:0], page.payload...)
track.lastGranulePosition = page.granulePos
track.lastPageIndex = page.pageIndex
track.lastPageHeaderType = page.headerType
track.lastPageWritten = true
}
offset += int64(len(page.data))
}
}
track.pageIndex += uint32(len(pages)) //nolint:gosec // Page counts are bounded by the packet size in memory.
return nil
}
func createPagesForSerial(
checksumTable *[256]uint32,
payload []byte,
headerType uint8,
granulePos uint64,
serial uint32,
pageIndex uint32,
) []oggPage {
pages := []oggPage{}
payloadOffset := 0
remainingPayload := len(payload)
firstPage := true
for {
segmentTable := make([]byte, 0, maxOggPageSegments)
pagePayloadSize := 0
packetComplete := false
for len(segmentTable) < maxOggPageSegments {
if remainingPayload >= 255 {
segmentTable = append(segmentTable, 255)
pagePayloadSize += 255
remainingPayload -= 255
continue
}
segmentTable = append(segmentTable, byte(remainingPayload)) //nolint:gosec // remainingPayload is < 255 here.
pagePayloadSize += remainingPayload
remainingPayload = 0
packetComplete = true
break
}
pagePayload := payload[payloadOffset : payloadOffset+pagePayloadSize]
pageHeaderType := packetPageHeaderType(headerType, firstPage, packetComplete)
pageGranulePos := noGranulePosition
if packetComplete {
pageGranulePos = granulePos
}
pages = append(pages, oggPage{
data: createPageForSerialWithSegments(
checksumTable,
pagePayload,
segmentTable,
pageHeaderType,
pageGranulePos,
serial,
pageIndex,
),
payload: pagePayload,
headerType: pageHeaderType,
granulePos: pageGranulePos,
pageIndex: pageIndex,
})
payloadOffset += pagePayloadSize
pageIndex++
firstPage = false
if packetComplete {
break
}
}
return pages
}
func packetPageHeaderType(headerType uint8, firstPage, packetComplete bool) uint8 {
if firstPage {
if packetComplete {
return headerType
}
return headerType &^ pageHeaderTypeEndOfStream
}
pageHeaderType := uint8(pageHeaderTypeContinuationOfPacket)
if packetComplete {
pageHeaderType |= headerType & pageHeaderTypeEndOfStream
}
return pageHeaderType
}
func createPageForSerial(
checksumTable *[256]uint32,
payload []byte,
headerType uint8,
granulePos uint64,
serial uint32,
pageIndex uint32,
) []byte {
pages := createPagesForSerial(checksumTable, payload, headerType, granulePos, serial, pageIndex)
pageSize := 0
for _, page := range pages {
pageSize += len(page.data)
}
data := make([]byte, 0, pageSize)
for _, page := range pages {
data = append(data, page.data...)
}
return data
}
func createPageForSerialWithSegments(
checksumTable *[256]uint32,
payload []byte,
segmentTable []byte,
headerType uint8,
granulePos uint64,
serial uint32,
pageIndex uint32,
) []byte {
page := make([]byte, pageHeaderSize+len(segmentTable)+len(payload))
copy(page[0:], pageHeaderSignature) // page headers starts with 'OggS'
page[4] = 0 // Version
page[5] = headerType // 1 = continuation, 2 = beginning of stream, 4 = end of stream
binary.LittleEndian.PutUint64(page[6:], granulePos) // granule position
binary.LittleEndian.PutUint32(page[14:], serial) // Bitstream serial number
binary.LittleEndian.PutUint32(page[18:], pageIndex) // Page sequence number
page[26] = uint8(len(segmentTable)) //nolint:gosec // segmentTable is capped at maxOggPageSegments.
copy(page[pageHeaderSize:], segmentTable)
copy(page[pageHeaderSize+len(segmentTable):], payload)
var checksum uint32
for index := range page {
checksum = (checksum << 8) ^ checksumTable[byte(checksum>>24)^page[index]]
}
binary.LittleEndian.PutUint32(page[22:], checksum)
return page
}
func (w *OggWriter) writeRTP(track *oggTrack, packet *rtp.Packet) error {
return writeRTP(w.stream, pageRewriterForFile(w.fd), w.checksumTable, track, packet)
}
func (w *Writer) writeOpusPayload(track *oggTrack, payload []byte) error {
return writeOpusPayload(w.stream, w.pageRewriter, w.checksumTable, track, payload)
}
func writeRTP(
stream io.Writer,
rewriter pageRewriter,
checksumTable *[256]uint32,
track *oggTrack,
packet *rtp.Packet,
) error {
payload, shouldWrite, err := opusPayloadFromPacket(packet)
if err != nil || !shouldWrite {
return err
}
return writeOpusPayload(stream, rewriter, checksumTable, track, payload)
}
func opusPayloadFromPacket(packet *rtp.Packet) ([]byte, bool, error) {
if packet == nil {
return nil, false, errInvalidNilPacket
}
if len(packet.Payload) == 0 {
return nil, false, nil
}
opusPacket := codecs.OpusPacket{}
if _, err := opusPacket.Unmarshal(packet.Payload); err != nil {
return nil, false, err
}
return opusPacket.Payload, true, nil
}
func writeOpusPayload(
stream io.Writer,
rewriter pageRewriter,
checksumTable *[256]uint32,
track *oggTrack,
payload []byte,
) error {
sampleCount, err := opusPacketSampleCount(payload)
if err != nil {
return err
}
track.previousGranulePosition += sampleCount
return writePage(
stream, rewriter, checksumTable, track, payload, pageHeaderTypeContinuationOfStream, track.previousGranulePosition,
)
}
func opusPacketSampleCount(payload []byte) (uint64, error) {
if len(payload) == 0 {
return 0, errInvalidOpusPacket
}
frameCount, err := opusPacketFrameCount(payload)
if err != nil {
return 0, err
}
sampleCount := uint64(opusSamplesPerFrame(payload[0])) * uint64(frameCount)
if sampleCount > maxOpusPacketSamples {
return 0, errInvalidOpusPacket
}
return sampleCount, nil
}
func opusPacketFrameCount(payload []byte) (uint8, error) {
switch payload[0] & 0x03 {
case 0:
return 1, nil
case 1, 2:
return 2, nil
case 3:
if len(payload) < 2 {
return 0, errInvalidOpusPacket
}
frameCount := payload[1] & 0x3f
if frameCount == 0 {
return 0, errInvalidOpusPacket
}
return frameCount, nil
default:
return 0, errInvalidOpusPacket
}
}
func opusSamplesPerFrame(toc byte) uint32 {
if toc&0x80 != 0 {
return (opusGranuleSampleRate << ((toc >> 3) & 0x03)) / 400
}
if toc&0x60 == 0x60 {
if toc&0x08 != 0 {
return opusGranuleSampleRate / 50
}
return opusGranuleSampleRate / 100
}
frameSize := (toc >> 3) & 0x03
if frameSize == 3 {
return opusGranuleSampleRate * 60 / 1000
}
return (opusGranuleSampleRate << frameSize) / 100
}
func (w *Writer) startLocked() error {
if w.started {
return nil
}
for _, track := range w.trackOrder {
if err := w.writeTrackIDHeader(track.track); err != nil {
return err
}
}
for _, track := range w.trackOrder {
if err := w.writeTrackCommentHeader(track.track); err != nil {
return err
}
}
w.started = true
return nil
}
func markTrackEndOfStream(rewriter pageRewriter, checksumTable *[256]uint32, track *oggTrack) error {
if track == nil || !track.lastPageWritten {
return nil
}
data := createPageForSerial(
checksumTable,
track.lastPayload,
track.lastPageHeaderType|pageHeaderTypeEndOfStream,
track.lastGranulePosition,
track.serial,
track.lastPageIndex,
)
_, err := rewriter.WriteAt(data, track.lastPageOffset)
return err
}
func writeNilEndOfStreamPage(stream io.Writer, checksumTable *[256]uint32, track *oggTrack) error {
if track == nil || track.pageIndex == 0 {
return nil
}
data := createPageForSerialWithSegments(
checksumTable,
nil,
nil,
pageHeaderTypeEndOfStream,
track.previousGranulePosition,
track.serial,
track.pageIndex,
)
if err := writeToStream(stream, data); err != nil {
return err
}
track.pageIndex++
return nil
}
func pageRewriterForFile(fd *os.File) pageRewriter {
if fd == nil {
return nil
}
return fd
}
func writeToStream(stream io.Writer, p []byte) error {
if stream == nil {
return errFileNotOpened
}
_, err := stream.Write(p)
return err
}
func generateChecksumTable() *[256]uint32 {
var table [256]uint32
const poly = 0x04c11db7
for i := range table {
remainder := uint32(i) << 24 //nolint:gosec // G115
for range 8 {
if (remainder & 0x80000000) != 0 {
remainder = (remainder << 1) ^ poly
} else {
remainder <<= 1
}
}
table[i] = (remainder & 0xffffffff) //nolint:gosec // no out of bounds access here.
}
return &table
}