Skip to content

Instantly share code, notes, and snippets.

@gwik
Last active August 29, 2015 14:06
Show Gist options
  • Save gwik/e87162fdfb8449748afd to your computer and use it in GitHub Desktop.
Save gwik/e87162fdfb8449748afd to your computer and use it in GitHub Desktop.
ogg decoder for sniffing content type.
package main
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"strings"
)
const (
flagContinued = 0x01
flagFirst = 0x02
flagLast = 0x04
)
var capturePattern = []byte("OggS")
// pageHeader starts with OggS
type pageHeader struct {
version uint8 // stream_structure_version (4)
flags uint8 // header_type_flag (5)
pos uint64 // absolute granule position (6-13)
sn uint32 // stream serial number (14-17)
seqNo uint32 // page sequence no (18-21)
checksum uint32 // page checksum (22-25)
count uint8 // page_segments (26)
segments [256]uint8 // segment_table (27-n, n=page_segments+26)
}
func (ph *pageHeader) Len() int {
return int(27 + ph.count)
}
func (ph *pageHeader) PayloadLen() int {
l := 0
for i := uint8(0); i < ph.count; i++ {
l += int(ph.segments[i])
}
return l
}
// caller should ensure buf should be a least 27 bytes
func (ph *pageHeader) ReadHeader(p []byte) error {
if len(p) < 27 {
return errors.New("Invalid buffer size.")
}
if !bytes.Equal(p[:4], capturePattern) {
return errors.New("Capture pattern not found.")
}
ph.version = p[4]
if ph.version != 0 {
return fmt.Errorf("Unsupported version: %d", ph.version)
}
ph.flags = p[5]
ph.pos = binary.LittleEndian.Uint64(p[6:])
ph.sn = binary.LittleEndian.Uint32(p[14:])
ph.seqNo = binary.LittleEndian.Uint32(p[18:])
ph.checksum = binary.LittleEndian.Uint32(p[22:])
ph.count = p[26]
if ph.count > 255 {
return fmt.Errorf("Invalid packet count %d.", ph.count)
}
return nil
}
func (ph *pageHeader) ReadSegments(p []byte) error {
if len(p) < int(ph.count) {
return errors.New("Invalid buffer size.")
}
for i := uint8(0); i < ph.count; i++ {
ph.segments[i] = p[i]
}
return nil
}
const (
flagCodecVideo = 1 << iota
flagCodecAudio = 1 << iota
flagCodecText = 1 << iota
)
type codecInfo struct {
prefix []byte
name string
flag int
}
// http://wiki.xiph.org/index.php/MIMETypesCodecs
var codecTable = []codecInfo{
{[]byte("CELT "), "celt", flagCodecAudio},
{[]byte("CMML\x00\x00\x00\x00"), "cmml", flagCodecText},
{[]byte("BBCD\x00"), "dirac", flagCodecVideo},
{[]byte("\177FLAC"), "flac", flagCodecAudio},
{[]byte("\213JNG\r\n\032\n"), "jng", flagCodecVideo},
{[]byte("\x80kate\x00\x00\x00"), "kate", flagCodecText},
{[]byte("OggMIDI\x00"), "midi", flagCodecText},
{[]byte("\212MNG\r\n\032\n"), "mng", flagCodecVideo},
{[]byte("OpusHead"), "opus", flagCodecAudio},
{[]byte("PCM "), "pcm", flagCodecAudio},
{[]byte("\211PNG\r\n\032\n"), "png", flagCodecVideo},
{[]byte("Speex "), "speex", flagCodecAudio},
{[]byte("\x80theora"), "theora", flagCodecVideo},
{[]byte("\x01vorbis"), "vorbis", flagCodecAudio},
{[]byte("YUV4MPEG"), "yuv4mpeg", flagCodecVideo},
}
type streamInfo struct {
sn uint32
codec *codecInfo
}
func findCodecInfo(hdr []byte) *codecInfo {
hlen := len(hdr)
for _, pte := range codecTable {
plen := len(pte.prefix)
if hlen > plen && bytes.Equal(hdr[:plen], pte.prefix) {
return &pte
}
}
return nil
}
// peek looks at the first page for every streams and extract codec info.
func peek(in io.ReadSeeker) ([]streamInfo, error) {
buf := make([]byte, 256)
hbuf := buf[:27]
streams := make([]streamInfo, 0, 3)
ph := new(pageHeader)
for {
offset := 0
_, err := io.ReadFull(in, hbuf)
if err != nil {
return streams, err
}
err = ph.ReadHeader(hbuf)
if err != nil {
return streams, err
}
segbuf := buf[:ph.count]
_, err = io.ReadFull(in, segbuf)
if err != nil {
return streams, err
}
ph.ReadSegments(segbuf)
if ph.flags&flagFirst < 1 {
return streams, nil
}
offset += ph.PayloadLen()
if ph.count > 0 {
size := int(ph.segments[0])
_, err := io.ReadFull(in, buf[:size])
if err != nil {
return streams, err
}
codec := findCodecInfo(buf)
if codec != nil {
streams = append(streams, streamInfo{ph.sn, codec})
}
offset -= size
}
_, err = in.Seek(int64(offset), 1)
if err != nil {
return streams, err
}
}
}
// returns mimeType as defined in RFC5334
func mimeType(streams []streamInfo) string {
flags := 0
for _, stream := range streams {
flags |= stream.codec.flag
}
if flags&flagCodecVideo > 0 {
return "video/ogg"
}
if flags&flagCodecAudio > 0 {
return "audio/ogg"
}
return "application/ogg"
}
// returns mimeType + codecs attribute as defined in RFC5334
func contentType(streams []streamInfo) string {
flags := 0
mime := "application/ogg"
codecs := make([]string, 0, len(streams))
for _, stream := range streams {
flags |= stream.codec.flag
codecs = append(codecs, stream.codec.name)
}
if flags&flagCodecVideo > 0 {
mime = "video/ogg"
} else if flags&flagCodecAudio > 0 {
mime = "audio/ogg"
}
if len(codecs) > 0 {
return fmt.Sprintf(`%s codecs="%s"`, mime, strings.Join(codecs, ", "))
}
return mime
}
func main() {
// curl -LO http://techslides.com/demos/sample-videos/small.ogv
f, err := os.Open("small.ogv")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// net/http.DetectContentType only read the first 512 bytes
// it seems to work with this video but there is no warranty the
// coded infos are within the first 512 bytes.
buf := make([]byte, 512)
_, err = io.ReadFull(f, buf)
if err != nil {
os.Exit(128)
}
streams, err := peek(bytes.NewReader(buf))
if err != nil {
fmt.Println(err)
os.Exit(2)
}
for _, stream := range streams {
fmt.Printf("stream sn=%d codec=%s\n", stream.sn, stream.codec.name)
}
fmt.Println(mimeType(streams))
fmt.Println(contentType(streams))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment