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  }