| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
- // SPDX-License-Identifier: MIT
- // Package oggreader implements the Ogg media container reader
- package oggreader
- import (
- "encoding/binary"
- "errors"
- "io"
- )
- const (
- pageHeaderTypeBeginningOfStream = 0x02
- pageHeaderSignature = "OggS"
- idPageSignature = "OpusHead"
- pageHeaderLen = 27
- idPagePayloadLength = 19
- )
- 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")
- )
- // OggReader is used to read Ogg files and return page payloads
- type OggReader struct {
- stream io.Reader
- bytesReadSuccesfully int64
- checksumTable *[256]uint32
- doChecksum bool
- }
- // OggHeader is the metadata from the first two pages
- // in the file (ID and Comment)
- //
- // https://tools.ietf.org/html/rfc7845.html#section-3
- type OggHeader struct {
- ChannelMap uint8
- Channels uint8
- OutputGain uint16
- PreSkip uint16
- SampleRate uint32
- Version uint8
- }
- // OggPageHeader is the metadata for a Page
- // Pages are the fundamental unit of multiplexing in an Ogg stream
- //
- // https://tools.ietf.org/html/rfc7845.html#section-1
- type OggPageHeader struct {
- GranulePosition uint64
- sig [4]byte
- version uint8
- headerType uint8
- serial uint32
- index uint32
- segmentsCount uint8
- }
- // NewWith returns a new Ogg reader and Ogg header
- // with an io.Reader input
- func NewWith(in io.Reader) (*OggReader, *OggHeader, error) {
- return newWith(in /* doChecksum */, true)
- }
- func newWith(in io.Reader, doChecksum bool) (*OggReader, *OggHeader, error) {
- if in == nil {
- return nil, nil, errNilStream
- }
- reader := &OggReader{
- stream: in,
- checksumTable: generateChecksumTable(),
- doChecksum: doChecksum,
- }
- header, err := reader.readHeaders()
- if err != nil {
- return nil, nil, err
- }
- return reader, header, nil
- }
- func (o *OggReader) readHeaders() (*OggHeader, error) {
- payload, pageHeader, err := o.ParseNextPage()
- if err != nil {
- return nil, err
- }
- header := &OggHeader{}
- if string(pageHeader.sig[:]) != pageHeaderSignature {
- return nil, errBadIDPageSignature
- }
- if pageHeader.headerType != pageHeaderTypeBeginningOfStream {
- return nil, errBadIDPageType
- }
- if len(payload) != idPagePayloadLength {
- return nil, errBadIDPageLength
- }
- if s := string(payload[:8]); s != idPageSignature {
- return nil, errBadIDPagePayloadSignature
- }
- header.Version = payload[8]
- header.Channels = payload[9]
- header.PreSkip = binary.LittleEndian.Uint16(payload[10:12])
- header.SampleRate = binary.LittleEndian.Uint32(payload[12:16])
- header.OutputGain = binary.LittleEndian.Uint16(payload[16:18])
- header.ChannelMap = payload[18]
- return header, nil
- }
- // ParseNextPage reads from stream and returns Ogg page payload, header,
- // and an error if there is incomplete page data.
- func (o *OggReader) ParseNextPage() ([]byte, *OggPageHeader, error) {
- h := make([]byte, pageHeaderLen)
- n, err := io.ReadFull(o.stream, h)
- if err != nil {
- return nil, nil, err
- } else if n < len(h) {
- return nil, nil, errShortPageHeader
- }
- pageHeader := &OggPageHeader{
- sig: [4]byte{h[0], h[1], h[2], h[3]},
- }
- pageHeader.version = h[4]
- pageHeader.headerType = h[5]
- pageHeader.GranulePosition = binary.LittleEndian.Uint64(h[6 : 6+8])
- pageHeader.serial = binary.LittleEndian.Uint32(h[14 : 14+4])
- pageHeader.index = binary.LittleEndian.Uint32(h[18 : 18+4])
- pageHeader.segmentsCount = h[26]
- sizeBuffer := make([]byte, pageHeader.segmentsCount)
- if _, err = io.ReadFull(o.stream, sizeBuffer); err != nil {
- return nil, nil, err
- }
- payloadSize := 0
- for _, s := range sizeBuffer {
- payloadSize += int(s)
- }
- payload := make([]byte, payloadSize)
- if _, err = io.ReadFull(o.stream, payload); err != nil {
- return nil, nil, err
- }
- if o.doChecksum {
- var checksum uint32
- updateChecksum := func(v byte) {
- checksum = (checksum << 8) ^ o.checksumTable[byte(checksum>>24)^v]
- }
- for index := range h {
- // Don't include expected checksum in our generation
- if index > 21 && index < 26 {
- updateChecksum(0)
- continue
- }
- updateChecksum(h[index])
- }
- for _, s := range sizeBuffer {
- updateChecksum(s)
- }
- for index := range payload {
- updateChecksum(payload[index])
- }
- if binary.LittleEndian.Uint32(h[22:22+4]) != checksum {
- return nil, nil, errChecksumMismatch
- }
- }
- return payload, pageHeader, nil
- }
- // ResetReader resets the internal stream of OggReader. This is useful
- // for live streams, where the end of the file might be read without the
- // data being finished.
- func (o *OggReader) ResetReader(reset func(bytesRead int64) io.Reader) {
- o.stream = reset(o.bytesReadSuccesfully)
- }
- func generateChecksumTable() *[256]uint32 {
- var table [256]uint32
- const poly = 0x04c11db7
- for i := range table {
- r := uint32(i) << 24
- for j := 0; j < 8; j++ {
- if (r & 0x80000000) != 0 {
- r = (r << 1) ^ poly
- } else {
- r <<= 1
- }
- table[i] = (r & 0xffffffff)
- }
- }
- return &table
- }
|