github.com/pion/webrtc/v4@v4.0.1/pkg/media/oggreader/oggreader.go (about) 1 // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> 2 // SPDX-License-Identifier: MIT 3 4 // Package oggreader implements the Ogg media container reader 5 package oggreader 6 7 import ( 8 "encoding/binary" 9 "errors" 10 "io" 11 ) 12 13 const ( 14 pageHeaderTypeBeginningOfStream = 0x02 15 pageHeaderSignature = "OggS" 16 17 idPageSignature = "OpusHead" 18 19 pageHeaderLen = 27 20 idPagePayloadLength = 19 21 ) 22 23 var ( 24 errNilStream = errors.New("stream is nil") 25 errBadIDPageSignature = errors.New("bad header signature") 26 errBadIDPageType = errors.New("wrong header, expected beginning of stream") 27 errBadIDPageLength = errors.New("payload for id page must be 19 bytes") 28 errBadIDPagePayloadSignature = errors.New("bad payload signature") 29 errShortPageHeader = errors.New("not enough data for payload header") 30 errChecksumMismatch = errors.New("expected and actual checksum do not match") 31 ) 32 33 // OggReader is used to read Ogg files and return page payloads 34 type OggReader struct { 35 stream io.Reader 36 bytesReadSuccesfully int64 37 checksumTable *[256]uint32 38 doChecksum bool 39 } 40 41 // OggHeader is the metadata from the first two pages 42 // in the file (ID and Comment) 43 // 44 // https://tools.ietf.org/html/rfc7845.html#section-3 45 type OggHeader struct { 46 ChannelMap uint8 47 Channels uint8 48 OutputGain uint16 49 PreSkip uint16 50 SampleRate uint32 51 Version uint8 52 } 53 54 // OggPageHeader is the metadata for a Page 55 // Pages are the fundamental unit of multiplexing in an Ogg stream 56 // 57 // https://tools.ietf.org/html/rfc7845.html#section-1 58 type OggPageHeader struct { 59 GranulePosition uint64 60 61 sig [4]byte 62 version uint8 63 headerType uint8 64 serial uint32 65 index uint32 66 segmentsCount uint8 67 } 68 69 // NewWith returns a new Ogg reader and Ogg header 70 // with an io.Reader input 71 func NewWith(in io.Reader) (*OggReader, *OggHeader, error) { 72 return newWith(in /* doChecksum */, true) 73 } 74 75 func newWith(in io.Reader, doChecksum bool) (*OggReader, *OggHeader, error) { 76 if in == nil { 77 return nil, nil, errNilStream 78 } 79 80 reader := &OggReader{ 81 stream: in, 82 checksumTable: generateChecksumTable(), 83 doChecksum: doChecksum, 84 } 85 86 header, err := reader.readHeaders() 87 if err != nil { 88 return nil, nil, err 89 } 90 91 return reader, header, nil 92 } 93 94 func (o *OggReader) readHeaders() (*OggHeader, error) { 95 payload, pageHeader, err := o.ParseNextPage() 96 if err != nil { 97 return nil, err 98 } 99 100 header := &OggHeader{} 101 if string(pageHeader.sig[:]) != pageHeaderSignature { 102 return nil, errBadIDPageSignature 103 } 104 105 if pageHeader.headerType != pageHeaderTypeBeginningOfStream { 106 return nil, errBadIDPageType 107 } 108 109 if len(payload) != idPagePayloadLength { 110 return nil, errBadIDPageLength 111 } 112 113 if s := string(payload[:8]); s != idPageSignature { 114 return nil, errBadIDPagePayloadSignature 115 } 116 117 header.Version = payload[8] 118 header.Channels = payload[9] 119 header.PreSkip = binary.LittleEndian.Uint16(payload[10:12]) 120 header.SampleRate = binary.LittleEndian.Uint32(payload[12:16]) 121 header.OutputGain = binary.LittleEndian.Uint16(payload[16:18]) 122 header.ChannelMap = payload[18] 123 124 return header, nil 125 } 126 127 // ParseNextPage reads from stream and returns Ogg page payload, header, 128 // and an error if there is incomplete page data. 129 func (o *OggReader) ParseNextPage() ([]byte, *OggPageHeader, error) { 130 h := make([]byte, pageHeaderLen) 131 132 n, err := io.ReadFull(o.stream, h) 133 if err != nil { 134 return nil, nil, err 135 } else if n < len(h) { 136 return nil, nil, errShortPageHeader 137 } 138 139 pageHeader := &OggPageHeader{ 140 sig: [4]byte{h[0], h[1], h[2], h[3]}, 141 } 142 143 pageHeader.version = h[4] 144 pageHeader.headerType = h[5] 145 pageHeader.GranulePosition = binary.LittleEndian.Uint64(h[6 : 6+8]) 146 pageHeader.serial = binary.LittleEndian.Uint32(h[14 : 14+4]) 147 pageHeader.index = binary.LittleEndian.Uint32(h[18 : 18+4]) 148 pageHeader.segmentsCount = h[26] 149 150 sizeBuffer := make([]byte, pageHeader.segmentsCount) 151 if _, err = io.ReadFull(o.stream, sizeBuffer); err != nil { 152 return nil, nil, err 153 } 154 155 payloadSize := 0 156 for _, s := range sizeBuffer { 157 payloadSize += int(s) 158 } 159 160 payload := make([]byte, payloadSize) 161 if _, err = io.ReadFull(o.stream, payload); err != nil { 162 return nil, nil, err 163 } 164 165 if o.doChecksum { 166 var checksum uint32 167 updateChecksum := func(v byte) { 168 checksum = (checksum << 8) ^ o.checksumTable[byte(checksum>>24)^v] 169 } 170 171 for index := range h { 172 // Don't include expected checksum in our generation 173 if index > 21 && index < 26 { 174 updateChecksum(0) 175 continue 176 } 177 178 updateChecksum(h[index]) 179 } 180 for _, s := range sizeBuffer { 181 updateChecksum(s) 182 } 183 for index := range payload { 184 updateChecksum(payload[index]) 185 } 186 187 if binary.LittleEndian.Uint32(h[22:22+4]) != checksum { 188 return nil, nil, errChecksumMismatch 189 } 190 } 191 192 o.bytesReadSuccesfully += int64(len(h) + len(sizeBuffer) + len(payload)) 193 194 return payload, pageHeader, nil 195 } 196 197 // ResetReader resets the internal stream of OggReader. This is useful 198 // for live streams, where the end of the file might be read without the 199 // data being finished. 200 func (o *OggReader) ResetReader(reset func(bytesRead int64) io.Reader) { 201 o.stream = reset(o.bytesReadSuccesfully) 202 } 203 204 func generateChecksumTable() *[256]uint32 { 205 var table [256]uint32 206 const poly = 0x04c11db7 207 208 for i := range table { 209 r := uint32(i) << 24 210 for j := 0; j < 8; j++ { 211 if (r & 0x80000000) != 0 { 212 r = (r << 1) ^ poly 213 } else { 214 r <<= 1 215 } 216 table[i] = (r & 0xffffffff) 217 } 218 } 219 return &table 220 }