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 }