220 lines
5.3 KiB
Go
220 lines
5.3 KiB
Go
// 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) {
|
|
segments, 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(segments[0]) != idPagePayloadLength {
|
|
return nil, errBadIDPageLength
|
|
}
|
|
|
|
if s := string(segments[0][:8]); s != idPageSignature {
|
|
return nil, errBadIDPagePayloadSignature
|
|
}
|
|
|
|
header.Version = segments[0][8]
|
|
header.Channels = segments[0][9]
|
|
header.PreSkip = binary.LittleEndian.Uint16(segments[0][10:12])
|
|
header.SampleRate = binary.LittleEndian.Uint32(segments[0][12:16])
|
|
header.OutputGain = binary.LittleEndian.Uint16(segments[0][16:18])
|
|
header.ChannelMap = segments[0][18]
|
|
|
|
return header, nil
|
|
}
|
|
|
|
// ParseNextPage reads from stream and returns Ogg page segments, 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
|
|
}
|
|
|
|
segments := [][]byte{}
|
|
|
|
for _, s := range sizeBuffer {
|
|
segment := make([]byte, int(s))
|
|
if _, err = io.ReadFull(o.stream, segment); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
segments = append(segments, segment)
|
|
}
|
|
|
|
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 i := range segments {
|
|
for index := range segments[i] {
|
|
updateChecksum(segments[i][index])
|
|
}
|
|
}
|
|
|
|
if binary.LittleEndian.Uint32(h[22:22+4]) != checksum {
|
|
return nil, nil, errChecksumMismatch
|
|
}
|
|
}
|
|
|
|
return segments, 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
|
|
}
|