diff --git a/codec/mjpeg/extract.go b/codec/mjpeg/extract.go index 4181639c..96d5ba15 100644 --- a/codec/mjpeg/extract.go +++ b/codec/mjpeg/extract.go @@ -1,12 +1,13 @@ /* DESCRIPTION - extract.go provides an Extractor to get JPEG from RTP. + extract.go provides an Extractor to get JPEG images from an RTP/JPEG stream + defined by RFC 2435 (see https://tools.ietf.org/html/rfc2435). AUTHOR Saxon Nelson-Milton LICENSE - Copyright (C) 2017 the Australian Ocean Lab (AusOcean) + Copyright (C) 2019 the Australian Ocean Lab (AusOcean) It is free software: you can redistribute it and/or modify them under the terms of the GNU General Public License as published by the @@ -25,71 +26,29 @@ LICENSE package mjpeg import ( - "bytes" "fmt" "io" "time" - "errors" "bitbucket.org/ausocean/av/protocol/rtp" ) -// Buffer sizes. -const ( - maxRTPSize = 1500 // Max ethernet transmission unit in bytes. -) - -var ( - errNoQTable = errors.New("no quantization table") - errReservedQ = errors.New("q value is reserved") -) - -var defaultQuantisers = []byte{ - // Luma table. - 16, 11, 12, 14, 12, 10, 16, 14, - 13, 14, 18, 17, 16, 19, 24, 40, - 26, 24, 22, 22, 24, 49, 35, 37, - 29, 40, 58, 51, 61, 60, 57, 51, - 56, 55, 64, 72, 92, 78, 64, 68, - 87, 69, 55, 56, 80, 109, 81, 87, - 95, 98, 103, 104, 103, 62, 77, 113, - 121, 112, 100, 120, 92, 101, 103, 99, - - /* chroma table */ - 17, 18, 18, 24, 21, 24, 47, 26, - 26, 47, 99, 66, 56, 66, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - -} +const maxRTPSize = 1500 // Max ethernet transmission unit in bytes. // Extractor is an Extractor for extracting JPEG from an RTP stream. type Extractor struct { - buf *bytes.Buffer // Holds the current JPEG image. - dst io.Writer // The destination we'll be writing extracted NALUs to. + dst io.Writer // The destination we'll be writing extracted NALUs to. } // NewExtractor returns a new Extractor. -func NewExtractor() *Extractor { - return &Extractor{ - buf: new(bytes.Buffer), - } -} +func NewExtractor() *Extractor { return &Extractor{} } // Extract will continously read RTP packets from src containing JPEG (in RTP // payload format) and extract the JPEG images, sending them to dst. This // function expects that each read from src will provide a single RTP packet. func (e *Extractor) Extract(dst io.Writer, src io.Reader, delay time.Duration) error { buf := make([]byte, maxRTPSize) - - var ( - qTables [128][128]byte - qTablesLen [128]byte - ) + c := NewCtx(dst) for { n, err := src.Read(buf) @@ -107,186 +66,15 @@ func (e *Extractor) Extract(dst io.Writer, src io.Reader, delay time.Duration) e return fmt.Errorf("could not get RTP payload, failed with err: %v\n", err) } - b := newByteStream(p) - _ = b.get8() // Ignore type-specific flag - - var ( - off = b.get24() // Fragment offset. - t = b.get8() // Type. - q = b.get8() // Quantization value. - width = b.get8() // Picture width. - height = b.get8() // Picture height. - dri int // Restart interval. - ) - - if t&0x40 != 0 { - dri = b.get16() - _ = b.get16() // Ignore restart count. - t &= ^0x40 - } - - if t > 1 { - panic("unimplemented RTP/JPEG type") - } - - // Parse quantization table if our offset is 0. - if off == 0 { - var qTable []byte - var qLen int - - if q > 127 { - _ = b.get8() // Ignore first byte (reserved for future use). - prec := b.get8() // The size of coefficients. - qLen = b.get16() // The length of the quantization table. - - if prec != 0 { - panic("unsupported precision") - } - - if qLen > 0 { - qTable = b.getBuf(qLen) - - if q < 255 { - if qTablesLen[q-128] == 0 && qLen <= 128 { - copy(qTables[q-128][:],qTable) - qTablesLen[q-128] = byte(qLen) - } - } - } else { - if q == 255 { - return errNoQTable - } - - if qTablesLen[q-128] == 0 { - return fmt.Errorf("no quantization tables known for q %d yet",q) - } - - qTable = qTables[q-128][:] - qLen = int(qTablesLen[q-128]) - } - } else { // q <= 127 - if q == 0 || q > 99 { - return errReservedQ - } - qTable = defaultQTable(q) - qLen = len(qTable) - } - - e.buf.Reset() - - err = writeHeader(e.buf, t, width, height, qLen / 64, dri, qTable) - if err != nil { - return fmt.Errorf("could not write JPEG header: %w",err) - } - } - - if e.buf.Len() == 0 { - // Must have missed start of frame? So ignore and wait for start. - continue - } - - // TODO: check that timestamp is consistent - // This will need expansion to RTP package to create Timestamp parsing func. - - // TODO: could also check offset with how many bytes we currently have - // to determine if there are missing frames. - - // Write frame data - err = b.writeTo(e.buf,b.remaining()) - if err != nil { - return fmt.Errorf("could not write remaining frame data to output buffer: %w",err) - } - + // Also grab the marker so that we know when the JPEG is finished. m, err := rtp.Marker(buf[:n]) if err != nil { - return fmt.Errorf("could not read RTP marker: %w",err) + return fmt.Errorf("could not read RTP marker: %w", err) } - if m { - _,err = e.buf.Write([]byte{0xff,codeEOI}) - if err != nil { - return fmt.Errorf("could not write EOI marker: %w",err) - } - - _,err = e.buf.WriteTo(dst) - if err != nil { - return fmt.Errorf("could not write JPEG to dst: %w",err) - } + err = c.ParseScan(p, m) + if err != nil { + return fmt.Errorf("could not parse JPEG scan: %w", err) } } } - -type byteStream struct { - bytes []byte - i int -} - -func newByteStream(b []byte) *byteStream { return &byteStream{bytes: b} } - -func (b *byteStream) get24() int { - v := int(b.bytes[b.i])<<16 | int(b.bytes[b.i+1])<<8 | int(b.bytes[b.i+2]) - b.i += 3 - return v -} - -func (b *byteStream) get8() int { - v := int(b.bytes[b.i]) - b.i++ - return v -} - -func (b *byteStream) get16() int { - v := int(b.bytes[b.i])<<8 | int(b.bytes[b.i+1]) - b.i += 2 - return v -} - -func (b *byteStream) getBuf(n int) []byte { - v := b.bytes[b.i:b.i+n] - b.i += n - return v -} - -func (b *byteStream) remaining() int { - return len(b.bytes) - b.i -} - -func (b *byteStream) writeTo(w io.Writer, n int) error { - _n,err := w.Write(b.bytes[b.i:b.i+n]) - b.i += _n - if err != nil { - return err - } - return nil -} - -func defaultQTable(q int) []byte { - f := clip(q,q,99) - const tabLen = 128 - tab := make([]byte,tabLen) - - if q < 50 { - q = 5000 / f - } else { - q = 200 - f*2 - } - - for i := 0; i < tabLen; i++ { - v := (int(defaultQuantisers[i])*q + 50) / 100 - v = clip(v,1,255) - tab[i] = byte(v) - } - return tab -} - -func clip(v, min, max int) int { - if v < min { - return min - } - - if v > max { - return max - } - - return v -} diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go index fd44a623..2f4821f8 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -1,36 +1,45 @@ /* DESCRIPTION - jpeg.go contains constants, structure and functions specific to the JPEG. + jpeg.go contains code ported from FFmpeg's C implementation of an RTP + JPEG-compressed Video Depacketizer following RFC 2435. See + https://ffmpeg.org/doxygen/2.6/rtpdec__jpeg_8c_source.html and + https://tools.ietf.org/html/rfc2435). + + This code can be used to build JPEG images from an RTP/JPEG stream. AUTHOR Saxon Nelson-Milton LICENSE - Copyright (C) 2017 the Australian Ocean Lab (AusOcean) + Copyright (c) 2012 Samuel Pitoiset. - It is free software: you can redistribute it and/or modify them - under the terms of the GNU General Public License as published by the - Free Software Foundation, either version 3 of the License, or (at your - option) any later version. + This file is part of FFmpeg. - It is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - for more details. + FFmpeg is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. - You should have received a copy of the GNU General Public License - along with revid in gpl.txt. If not, see http://www.gnu.org/licenses. + FFmpeg is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with FFmpeg; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package mjpeg import ( - "bytes" - "encoding/binary" + "errors" "fmt" "io" ) +const maxJPEG = 1000000 // 1 MB (arbitrary) + // JPEG marker codes. const ( codeSOI = 0xd8 // Start of image. @@ -43,6 +52,11 @@ const ( codeEOI = 0xd9 // End of image. ) +var ( + errNoQTable = errors.New("no quantization table") + errReservedQ = errors.New("q value is reserved") +) + var ( bitsDCLum = []byte{0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0} bitsDCChr = []byte{0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0} @@ -96,139 +110,197 @@ var ( 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, } + + defaultQuantisers = []byte{ + // Luma table. + 16, 11, 12, 14, 12, 10, 16, 14, + 13, 14, 18, 17, 16, 19, 24, 40, + 26, 24, 22, 22, 24, 49, 35, 37, + 29, 40, 58, 51, 61, 60, 57, 51, + 56, 55, 64, 72, 92, 78, 64, 68, + 87, 69, 55, 56, 80, 109, 81, 87, + 95, 98, 103, 104, 103, 62, 77, 113, + 121, 112, 100, 120, 92, 101, 103, 99, + + /* chroma table */ + 17, 18, 18, 24, 21, 24, 47, 26, + 26, 47, 99, 66, 56, 66, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + } ) -type multiError []error - -func (me multiError) Error() string { - return fmt.Sprintf("%v", []error(me)) +// Ctx describes a RTP/JPEG parsing context that will keep track of the current +// JPEG (help by p), and the state of the quantization tables. +type Ctx struct { + qTables [128][128]byte + qTablesLen [128]byte + p *putBuffer + d io.Writer } -func (me multiError) add(e error) { - me = append(me, e) +// NewCTX will return a new Ctx. +func NewCtx(d io.Writer) *Ctx { + return &Ctx{ + d: d, + p: newPutBuffer(make([]byte, maxJPEG)), + } } -type putter struct { - idx int -} +// ParseScan will parse a JPEG scan from an RTP/JPEG payload and append +func (c *Ctx) ParseScan(p []byte, m bool) error { + b := newByteStream(p) + _ = b.get8() // Ignore type-specific flag -func (p *putter) put16(b []byte, v uint16) { - binary.BigEndian.PutUint16(b[p.idx:], v) - p.idx += 2 -} + off := b.get24() // Fragment offset. + t := b.get8() // Type. + q := b.get8() // Quantization value. + width := b.get8() // Picture width. + height := b.get8() // Picture height. -func (p *putter) put8(b []byte, v uint8) { - b[p.idx] = byte(v) - p.idx++ -} + var dri int // Restart interval. -func (p *putter) putBuf(dst, src []byte, l int) { - copy(dst[p.idx:], src) - p.idx++ + if t&0x40 != 0 { + dri = b.get16() + _ = b.get16() // Ignore restart count. + t &= ^0x40 + } + + if t > 1 { + panic("unimplemented RTP/JPEG type") + } + + // Parse quantization table if our offset is 0. + if off == 0 { + var qTable []byte + var qLen int + + if q > 127 { + _ = b.get8() // Ignore first byte (reserved for future use). + prec := b.get8() // The size of coefficients. + qLen = b.get16() // The length of the quantization table. + + if prec != 0 { + panic("unsupported precision") + } + + if qLen > 0 { + qTable = b.getBuf(qLen) + + if q < 255 { + if c.qTablesLen[q-128] == 0 && qLen <= 128 { + copy(c.qTables[q-128][:], qTable) + c.qTablesLen[q-128] = byte(qLen) + } + } + } else { + if q == 255 { + return errNoQTable + } + + if c.qTablesLen[q-128] == 0 { + return fmt.Errorf("no quantization tables known for q %d yet", q) + } + + qTable = c.qTables[q-128][:] + qLen = int(c.qTablesLen[q-128]) + } + } else { // q <= 127 + if q == 0 || q > 99 { + return errReservedQ + } + qTable = defaultQTable(q) + qLen = len(qTable) + } + + c.p.reset() + + writeHeader(c.p, t, width, height, qLen/64, dri, qTable) + } + + if c.p.len() == 0 { + // Must have missed start of frame? So ignore and wait for start. + return nil + } + + // TODO: check that timestamp is consistent + // This will need expansion to RTP package to create Timestamp parsing func. + + // TODO: could also check offset with how many bytes we currently have + // to determine if there are missing frames. + + // Write frame data + err := b.writeTo(c.p, b.remaining()) + if err != nil { + return fmt.Errorf("could not write remaining frame data to output buffer: %w", err) + } + + if m { + // End of image marker. + mark(c.p, codeEOI) + + _, err = c.p.writeTo(c.d) + if err != nil { + return fmt.Errorf("could not write JPEG to dst: %w", err) + } + } + return nil } // writeHeader writes a JPEG header to the writer w. -func writeHeader(w io.Writer, _type, width, height, nbqTab, dri int, qtable []byte) error { +func writeHeader(p *putBuffer, _type, width, height, nbqTab, dri int, qtable []byte) { width <<= 3 height <<= 3 // Indicate start of image. - err := writeMarker(w, codeSOI) - if err != nil { - return fmt.Errorf("could not write SOI marker: %w", err) - } - - err = writeMarker(w, codeAPP0) - if err != nil { - return fmt.Errorf("could not write APP0 marker: %w", err) - } + mark(p, codeSOI) // Write JFIF header. - b := make([]byte, 16) - p := putter{} - p.put16(b, 16) - p.putBuf(b, []byte("JFIF"), 5) - p.put16(b, 0x0201) - p.put8(b, 0) - p.put16(b, 1) - p.put16(b, 1) - p.put8(b, 0) - p.put8(b, 0) - _, err = w.Write(b) - if err != nil { - return fmt.Errorf("could not write JFIF header: %w", err) - } + mark(p, codeAPP0) + p.put16(16) + p.putBuf([]byte("JFIF\000")) + p.put16(0x0201) + p.put8(0) + p.put16(1) + p.put16(1) + p.put8(0) + p.put8(0) - // If we want to define restart interval. + // If we want to define restart interval then write that. if dri != 0 { - err = writeMarker(w, codeDRI) - if err != nil { - return fmt.Errorf("could not write DRI marker code: %w", err) - } - - _, err := w.Write([]byte{0x00, 0x04, byte(dri >> 8), byte(dri)}) - if err != nil { - return fmt.Errorf("could not write restart interval value: %w", err) - } + mark(p, codeDRI) + p.put16(4) + p.put16(uint16(dri)) } // Define quantization tables. - err = writeMarker(w, codeDQT) - if err != nil { - return fmt.Errorf("could not write DQI marker code: %w", err) - } + mark(p, codeDQT) // Calculate table size and create slice for table. ts := 2 + nbqTab*(1+64) - _, err = w.Write([]byte{byte(ts >> 8), byte(ts)}) - if err != nil { - return fmt.Errorf("could not write quantization table size: %w", err) - } + p.put16(uint16(ts)) for i := 0; i < nbqTab; i++ { - _, err = w.Write([]byte{byte(i)}) - if err != nil { - return fmt.Errorf("could not write quantization table entry no.: %w", err) - } - - _, err = w.Write(qtable[64*i : (64*i)+64]) - if err != nil { - return fmt.Errorf("could not write quantization table entry: %w", err) - } + p.put8(uint8(i)) + p.putBuf(qtable[64*i : (64*i)+64]) } // Define huffman table. - err = writeMarker(w, codeDHT) - if err != nil { - return fmt.Errorf("could not write DHT marker code: %w", err) - } - - var me multiError - buf := new(bytes.Buffer) - me.add(writeHuffman(buf, 0, 0, bitsDCLum, valDC)) - me.add(writeHuffman(buf, 0, 1, bitsDCChr, valDC)) - me.add(writeHuffman(buf, 1, 0, bitsACLum, valACLum)) - me.add(writeHuffman(buf, 1, 1, bitsACChr, valACChr)) - if me != nil { - return fmt.Errorf("error writing huffman tables: %w", err) - } - - l := buf.Len() + 2 - _, err = w.Write([]byte{byte(l >> 8), byte(l)}) - if err != nil { - return fmt.Errorf("could not write quantization table entry: %w", err) - } - - _, err = buf.WriteTo(w) - if err != nil { - return fmt.Errorf("could not write huffman tables: %w", err) - } + mark(p, codeDHT) + lenIdx := p.len() + p.put16(0) + writeHuffman(p, 0, 0, bitsDCLum, valDC) + writeHuffman(p, 0, 1, bitsDCChr, valDC) + writeHuffman(p, 1, 0, bitsACLum, valACLum) + writeHuffman(p, 1, 1, bitsACChr, valACChr) + p.put16At(uint16(p.len()-lenIdx), lenIdx) // Start of frame. - err = writeMarker(w, codeSOF0) - if err != nil { - return fmt.Errorf("could not write SOF0 marker code: %w", err) - } + mark(p, codeSOF0) // Derive sample type. sample := 1 @@ -242,84 +314,84 @@ func writeHeader(w io.Writer, _type, width, height, nbqTab, dri int, qtable []by mtxNo = 1 } - b = make([]byte, 17) - p = putter{} - p.put16(b, 17) - p.put8(b, 8) - p.put16(b, uint16(height)) - p.put16(b, uint16(width)) - p.put8(b, 3) - p.put8(b, 1) - p.put8(b, uint8((2<<4)|sample)) - p.put8(b, 0) - p.put8(b, 2) - p.put8(b, 1<<4|1) - p.put8(b, uint8(mtxNo)) - p.put8(b, 3) - p.put8(b, 1<<4|1) - p.put8(b, uint8(mtxNo)) - _, err = w.Write(b) - if err != nil { - return fmt.Errorf("could not write SOF0 info: %w", err) - } + p.put16(17) + p.put8(8) + p.put16(uint16(height)) + p.put16(uint16(width)) + p.put8(3) + p.put8(1) + p.put8(uint8((2 << 4) | sample)) + p.put8(0) + p.put8(2) + p.put8(1<<4 | 1) + p.put8(uint8(mtxNo)) + p.put8(3) + p.put8(1<<4 | 1) + p.put8(uint8(mtxNo)) // Write start of scan. - err = writeMarker(w, codeSOS) - if err != nil { - return fmt.Errorf("could not write SOS marker code: %w", err) - } - - b = make([]byte, 12) - p = putter{} - p.put16(b, 12) - p.put8(b, 3) - p.put8(b, 1) - p.put8(b, 0) - p.put8(b, 2) - p.put8(b, 17) - p.put8(b, 3) - p.put8(b, 17) - p.put8(b, 0) - p.put8(b, 63) - p.put8(b, 0) - _, err = w.Write(b) - if err != nil { - return fmt.Errorf("could not write SOS info: %w", err) - } - - return nil + mark(p, codeSOS) + p.put16(12) + p.put8(3) + p.put8(1) + p.put8(0) + p.put8(2) + p.put8(17) + p.put8(3) + p.put8(17) + p.put8(0) + p.put8(63) + p.put8(0) } -// writeMarker writes an JPEG marker with code to w. -func writeMarker(w io.Writer, code byte) error { - _, err := w.Write([]byte{0xff, code}) - if err != nil { - return err - } - return nil +// mark writes a marker with code c to the putBuffer p. +func mark(p *putBuffer, c byte) { + p.put8(0xff) + p.put8(c) } // writeHuffman write a JPEG huffman table to w. -func writeHuffman(w io.Writer, class, id int, bits, values []byte) error { - _, err := w.Write([]byte{byte(class<<4 | id)}) - if err != nil { - return fmt.Errorf("could not write class and id: %w", err) - } +func writeHuffman(p *putBuffer, class, id int, bits, values []byte) { + p.put8(uint8(class<<4 | id)) var n int for i := 1; i <= 16; i++ { n += int(bits[i]) } - _, err = w.Write(bits[1:17]) - if err != nil { - return fmt.Errorf("could not write first lot of huffman bytes: %w", err) - } - - _, err = w.Write(values[0:n]) - if err != nil { - return fmt.Errorf("could not write second lot of huffman bytes: %w", err) - } - - return nil + p.putBuf(bits[1:17]) + p.putBuf(values[0:n]) +} + +// defaultQTable returns a default quantization table. +func defaultQTable(q int) []byte { + f := clip(q, q, 99) + const tabLen = 128 + tab := make([]byte, tabLen) + + if q < 50 { + q = 5000 / f + } else { + q = 200 - f*2 + } + + for i := 0; i < tabLen; i++ { + v := (int(defaultQuantisers[i])*q + 50) / 100 + v = clip(v, 1, 255) + tab[i] = byte(v) + } + return tab +} + +// clip clips the value v to the bounds defined by min and max. +func clip(v, min, max int) int { + if v < min { + return min + } + + if v > max { + return max + } + + return v } diff --git a/codec/mjpeg/utils.go b/codec/mjpeg/utils.go new file mode 100644 index 00000000..23c0fb3e --- /dev/null +++ b/codec/mjpeg/utils.go @@ -0,0 +1,120 @@ +/* +DESCRIPTION + utils.go provides buffer utilities used by jpeg.go. + +AUTHOR + Saxon Nelson-Milton + +LICENSE + Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + along with revid in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package mjpeg + +import ( + "encoding/binary" + "io" +) + +type putBuffer struct { + i int + b []byte +} + +func newPutBuffer(b []byte) *putBuffer { return &putBuffer{b: b} } + +func (p *putBuffer) Write(b []byte) (int, error) { + copy(p.b[p.i:], b) + p.i += len(b) + return len(b), nil +} + +func (p *putBuffer) writeTo(d io.Writer) (int, error) { + n, err := d.Write(p.b[0:p.i]) + p.i -= n + return n, err +} + +func (p *putBuffer) put16(v uint16) { + binary.BigEndian.PutUint16(p.b[p.i:], v) + p.i += 2 +} + +func (p *putBuffer) put8(v uint8) { + p.b[p.i] = byte(v) + p.i++ +} + +func (p *putBuffer) putBuf(src []byte) { + copy(p.b[p.i:], src) + p.i += len(src) +} + +func (p *putBuffer) put16At(v uint16, i int) { + binary.BigEndian.PutUint16(p.b[i:], v) +} + +func (p *putBuffer) reset() { + p.i = 0 +} + +func (p *putBuffer) len() int { + return p.i +} + +type byteStream struct { + bytes []byte + i int +} + +func newByteStream(b []byte) *byteStream { return &byteStream{bytes: b} } + +func (b *byteStream) get24() int { + v := int(b.bytes[b.i])<<16 | int(b.bytes[b.i+1])<<8 | int(b.bytes[b.i+2]) + b.i += 3 + return v +} + +func (b *byteStream) get8() int { + v := int(b.bytes[b.i]) + b.i++ + return v +} + +func (b *byteStream) get16() int { + v := int(b.bytes[b.i])<<8 | int(b.bytes[b.i+1]) + b.i += 2 + return v +} + +func (b *byteStream) getBuf(n int) []byte { + v := b.bytes[b.i : b.i+n] + b.i += n + return v +} + +func (b *byteStream) remaining() int { + return len(b.bytes) - b.i +} + +func (b *byteStream) writeTo(w io.Writer, n int) error { + _n, err := w.Write(b.bytes[b.i : b.i+n]) + b.i += _n + if err != nil { + return err + } + return nil +}