github.com/pion/webrtc/v3@v3.2.24/pkg/media/oggwriter/oggwriter.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
     2  // SPDX-License-Identifier: MIT
     3  
     4  // Package oggwriter implements OGG media container writer
     5  package oggwriter
     6  
     7  import (
     8  	"encoding/binary"
     9  	"errors"
    10  	"io"
    11  	"os"
    12  
    13  	"github.com/pion/randutil"
    14  	"github.com/pion/rtp"
    15  	"github.com/pion/rtp/codecs"
    16  )
    17  
    18  const (
    19  	pageHeaderTypeContinuationOfStream = 0x00
    20  	pageHeaderTypeBeginningOfStream    = 0x02
    21  	pageHeaderTypeEndOfStream          = 0x04
    22  	defaultPreSkip                     = 3840 // 3840 recommended in the RFC
    23  	idPageSignature                    = "OpusHead"
    24  	commentPageSignature               = "OpusTags"
    25  	pageHeaderSignature                = "OggS"
    26  )
    27  
    28  var (
    29  	errFileNotOpened    = errors.New("file not opened")
    30  	errInvalidNilPacket = errors.New("invalid nil packet")
    31  )
    32  
    33  // OggWriter is used to take RTP packets and write them to an OGG on disk
    34  type OggWriter struct {
    35  	stream                  io.Writer
    36  	fd                      *os.File
    37  	sampleRate              uint32
    38  	channelCount            uint16
    39  	serial                  uint32
    40  	pageIndex               uint32
    41  	checksumTable           *[256]uint32
    42  	previousGranulePosition uint64
    43  	previousTimestamp       uint32
    44  	lastPayloadSize         int
    45  }
    46  
    47  // New builds a new OGG Opus writer
    48  func New(fileName string, sampleRate uint32, channelCount uint16) (*OggWriter, error) {
    49  	f, err := os.Create(fileName) //nolint:gosec
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	writer, err := NewWith(f, sampleRate, channelCount)
    54  	if err != nil {
    55  		return nil, f.Close()
    56  	}
    57  	writer.fd = f
    58  	return writer, nil
    59  }
    60  
    61  // NewWith initialize a new OGG Opus writer with an io.Writer output
    62  func NewWith(out io.Writer, sampleRate uint32, channelCount uint16) (*OggWriter, error) {
    63  	if out == nil {
    64  		return nil, errFileNotOpened
    65  	}
    66  
    67  	writer := &OggWriter{
    68  		stream:        out,
    69  		sampleRate:    sampleRate,
    70  		channelCount:  channelCount,
    71  		serial:        randutil.NewMathRandomGenerator().Uint32(),
    72  		checksumTable: generateChecksumTable(),
    73  
    74  		// Timestamp and Granule MUST start from 1
    75  		// Only headers can have 0 values
    76  		previousTimestamp:       1,
    77  		previousGranulePosition: 1,
    78  	}
    79  	if err := writer.writeHeaders(); err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	return writer, nil
    84  }
    85  
    86  /*
    87      ref: https://tools.ietf.org/html/rfc7845.html
    88      https://git.xiph.org/?p=opus-tools.git;a=blob;f=src/opus_header.c#l219
    89  
    90         Page 0         Pages 1 ... n        Pages (n+1) ...
    91      +------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +--
    92      |            | |   | |   |     |   | |           | |         | |
    93      |+----------+| |+-----------------+| |+-------------------+ +-----
    94      |||ID Header|| ||  Comment Header || ||Audio Data Packet 1| | ...
    95      |+----------+| |+-----------------+| |+-------------------+ +-----
    96      |            | |   | |   |     |   | |           | |         | |
    97      +------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +--
    98      ^      ^                           ^
    99      |      |                           |
   100      |      |                           Mandatory Page Break
   101      |      |
   102      |      ID header is contained on a single page
   103      |
   104      'Beginning Of Stream'
   105  
   106     Figure 1: Example Packet Organization for a Logical Ogg Opus Stream
   107  */
   108  
   109  func (i *OggWriter) writeHeaders() error {
   110  	// ID Header
   111  	oggIDHeader := make([]byte, 19)
   112  
   113  	copy(oggIDHeader[0:], idPageSignature)                          // Magic Signature 'OpusHead'
   114  	oggIDHeader[8] = 1                                              // Version
   115  	oggIDHeader[9] = uint8(i.channelCount)                          // Channel count
   116  	binary.LittleEndian.PutUint16(oggIDHeader[10:], defaultPreSkip) // pre-skip
   117  	binary.LittleEndian.PutUint32(oggIDHeader[12:], i.sampleRate)   // original sample rate, any valid sample e.g 48000
   118  	binary.LittleEndian.PutUint16(oggIDHeader[16:], 0)              // output gain
   119  	oggIDHeader[18] = 0                                             // channel map 0 = one stream: mono or stereo
   120  
   121  	// Reference: https://tools.ietf.org/html/rfc7845.html#page-6
   122  	// RFC specifies that the ID Header page should have a granule position of 0 and a Header Type set to 2 (StartOfStream)
   123  	data := i.createPage(oggIDHeader, pageHeaderTypeBeginningOfStream, 0, i.pageIndex)
   124  	if err := i.writeToStream(data); err != nil {
   125  		return err
   126  	}
   127  	i.pageIndex++
   128  
   129  	// Comment Header
   130  	oggCommentHeader := make([]byte, 21)
   131  	copy(oggCommentHeader[0:], commentPageSignature)        // Magic Signature 'OpusTags'
   132  	binary.LittleEndian.PutUint32(oggCommentHeader[8:], 5)  // Vendor Length
   133  	copy(oggCommentHeader[12:], "pion")                     // Vendor name 'pion'
   134  	binary.LittleEndian.PutUint32(oggCommentHeader[17:], 0) // User Comment List Length
   135  
   136  	// RFC specifies that the page where the CommentHeader completes should have a granule position of 0
   137  	data = i.createPage(oggCommentHeader, pageHeaderTypeContinuationOfStream, 0, i.pageIndex)
   138  	if err := i.writeToStream(data); err != nil {
   139  		return err
   140  	}
   141  	i.pageIndex++
   142  
   143  	return nil
   144  }
   145  
   146  const (
   147  	pageHeaderSize = 27
   148  )
   149  
   150  func (i *OggWriter) createPage(payload []uint8, headerType uint8, granulePos uint64, pageIndex uint32) []byte {
   151  	i.lastPayloadSize = len(payload)
   152  	nSegments := (len(payload) / 255) + 1 // A segment can be at most 255 bytes long.
   153  
   154  	page := make([]byte, pageHeaderSize+i.lastPayloadSize+nSegments)
   155  
   156  	copy(page[0:], pageHeaderSignature)                 // page headers starts with 'OggS'
   157  	page[4] = 0                                         // Version
   158  	page[5] = headerType                                // 1 = continuation, 2 = beginning of stream, 4 = end of stream
   159  	binary.LittleEndian.PutUint64(page[6:], granulePos) // granule position
   160  	binary.LittleEndian.PutUint32(page[14:], i.serial)  // Bitstream serial number
   161  	binary.LittleEndian.PutUint32(page[18:], pageIndex) // Page sequence number
   162  	page[26] = uint8(nSegments)                         // Number of segments in page.
   163  
   164  	// Filling segment table with the lacing values.
   165  	// First (nSegments - 1) values will always be 255.
   166  	for i := 0; i < nSegments-1; i++ {
   167  		page[pageHeaderSize+i] = 255
   168  	}
   169  	// The last value will be the remainder.
   170  	page[pageHeaderSize+nSegments-1] = uint8(len(payload) % 255)
   171  
   172  	copy(page[pageHeaderSize+nSegments:], payload) // Payload goes after the segment table, so at pageHeaderSize+nSegments.
   173  
   174  	var checksum uint32
   175  	for index := range page {
   176  		checksum = (checksum << 8) ^ i.checksumTable[byte(checksum>>24)^page[index]]
   177  	}
   178  
   179  	binary.LittleEndian.PutUint32(page[22:], checksum) // Checksum - generating for page data and inserting at 22th position into 32 bits
   180  
   181  	return page
   182  }
   183  
   184  // WriteRTP adds a new packet and writes the appropriate headers for it
   185  func (i *OggWriter) WriteRTP(packet *rtp.Packet) error {
   186  	if packet == nil {
   187  		return errInvalidNilPacket
   188  	}
   189  	if len(packet.Payload) == 0 {
   190  		return nil
   191  	}
   192  
   193  	opusPacket := codecs.OpusPacket{}
   194  	if _, err := opusPacket.Unmarshal(packet.Payload); err != nil {
   195  		// Only handle Opus packets
   196  		return err
   197  	}
   198  
   199  	payload := opusPacket.Payload[0:]
   200  
   201  	// Should be equivalent to sampleRate * duration
   202  	if i.previousTimestamp != 1 {
   203  		increment := packet.Timestamp - i.previousTimestamp
   204  		i.previousGranulePosition += uint64(increment)
   205  	}
   206  	i.previousTimestamp = packet.Timestamp
   207  
   208  	data := i.createPage(payload, pageHeaderTypeContinuationOfStream, i.previousGranulePosition, i.pageIndex)
   209  	i.pageIndex++
   210  	return i.writeToStream(data)
   211  }
   212  
   213  // Close stops the recording
   214  func (i *OggWriter) Close() error {
   215  	defer func() {
   216  		i.fd = nil
   217  		i.stream = nil
   218  	}()
   219  
   220  	// Returns no error has it may be convenient to call
   221  	// Close() multiple times
   222  	if i.fd == nil {
   223  		// Close stream if we are operating on a stream
   224  		if closer, ok := i.stream.(io.Closer); ok {
   225  			return closer.Close()
   226  		}
   227  		return nil
   228  	}
   229  
   230  	// Seek back one page, we need to update the header and generate new CRC
   231  	pageOffset, err := i.fd.Seek(-1*int64(i.lastPayloadSize+pageHeaderSize+1), 2)
   232  	if err != nil {
   233  		return err
   234  	}
   235  
   236  	payload := make([]byte, i.lastPayloadSize)
   237  	if _, err := i.fd.ReadAt(payload, pageOffset+pageHeaderSize+1); err != nil {
   238  		return err
   239  	}
   240  
   241  	data := i.createPage(payload, pageHeaderTypeEndOfStream, i.previousGranulePosition, i.pageIndex-1)
   242  	if err := i.writeToStream(data); err != nil {
   243  		return err
   244  	}
   245  
   246  	// Update the last page if we are operating on files
   247  	// to mark it as the EOS
   248  	return i.fd.Close()
   249  }
   250  
   251  // Wraps writing to the stream and maintains state
   252  // so we can set values for EOS
   253  func (i *OggWriter) writeToStream(p []byte) error {
   254  	if i.stream == nil {
   255  		return errFileNotOpened
   256  	}
   257  
   258  	_, err := i.stream.Write(p)
   259  	return err
   260  }
   261  
   262  func generateChecksumTable() *[256]uint32 {
   263  	var table [256]uint32
   264  	const poly = 0x04c11db7
   265  
   266  	for i := range table {
   267  		r := uint32(i) << 24
   268  		for j := 0; j < 8; j++ {
   269  			if (r & 0x80000000) != 0 {
   270  				r = (r << 1) ^ poly
   271  			} else {
   272  				r <<= 1
   273  			}
   274  			table[i] = (r & 0xffffffff)
   275  		}
   276  	}
   277  	return &table
   278  }