From f5b6af559fe675dab253e37fef92c1749ebe7728 Mon Sep 17 00:00:00 2001 From: Saxon Date: Fri, 15 Nov 2019 15:11:53 +1030 Subject: [PATCH 01/14] codec/mjpeg: added jpeg.go file to hold JPEG specific stuff and added some JPEG marker codes. --- codec/mjpeg/jpeg.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 codec/mjpeg/jpeg.go diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go new file mode 100644 index 00000000..da878d83 --- /dev/null +++ b/codec/mjpeg/jpeg.go @@ -0,0 +1,36 @@ +/* +DESCRIPTION + jpeg.go contains constants, structure and functions specific to the JPEG. + +AUTHOR + Saxon Nelson-Milton + +LICENSE + Copyright (C) 2017 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 + +// JPEG marker codes. +const ( + soi = 0xd8 // Start of image. + dri = 0xdd // Define restart interval. + dqt = 0xdb // Define quantization tables. + dht = 0xde // Define hierarchical progression. + sos = 0xda // Start of scan. + app0 = 0xe0 // TODO: find out what this is. + sof0 = 0xc0 // Baseline +) From 7577cfa0c4ca86ff5c46e427f07b05b3330e46c4 Mon Sep 17 00:00:00 2001 From: Saxon Date: Fri, 15 Nov 2019 15:41:02 +1030 Subject: [PATCH 02/14] codec/mjpeg/jpeg.go: added putMarker function to write JPEG marker codes to an io.Writer --- codec/mjpeg/jpeg.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go index da878d83..c5c42c06 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -24,6 +24,8 @@ LICENSE package mjpeg +import "io" + // JPEG marker codes. const ( soi = 0xd8 // Start of image. @@ -34,3 +36,12 @@ const ( app0 = 0xe0 // TODO: find out what this is. sof0 = 0xc0 // Baseline ) + +// putMarker writes an JPEG marker with code to w. +func putMarker(w io.Writer, code byte) error { + _, err := w.Write([]byte{0xff, code}) + if err != nil { + return err + } + return nil +} From a63cf5a1b7b97328127d5469810763e740495a95 Mon Sep 17 00:00:00 2001 From: Saxon Date: Fri, 15 Nov 2019 16:25:35 +1030 Subject: [PATCH 03/14] codec/mjpeg/jpeg.go: added writeHuffman function to write JPEG huffman tables to an io.Writer. --- codec/mjpeg/jpeg.go | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go index c5c42c06..570fa769 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -24,7 +24,10 @@ LICENSE package mjpeg -import "io" +import ( + "fmt" + "io" +) // JPEG marker codes. const ( @@ -37,11 +40,36 @@ const ( sof0 = 0xc0 // Baseline ) -// putMarker writes an JPEG marker with code to w. -func putMarker(w io.Writer, code byte) error { +// 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 } + +// 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) + } + + 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 +} From eaac50f33915dbb5eac906f84bd548c8dab23e33 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 16 Nov 2019 23:12:08 +1030 Subject: [PATCH 04/14] codec/mjpeg/jpeg.go: added writeHeader function to write JPEG header This also included the addition of some lunimance and chrominance tables, a multiError type (implements error) and a putter type, that will put uint16s, bytes and "buffers" into a byte slice. --- codec/mjpeg/jpeg.go | 251 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 244 insertions(+), 7 deletions(-) diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go index 570fa769..be4c7604 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -25,19 +25,75 @@ LICENSE package mjpeg import ( + "encoding/binary" "fmt" "io" ) // JPEG marker codes. const ( - soi = 0xd8 // Start of image. - dri = 0xdd // Define restart interval. - dqt = 0xdb // Define quantization tables. - dht = 0xde // Define hierarchical progression. - sos = 0xda // Start of scan. - app0 = 0xe0 // TODO: find out what this is. - sof0 = 0xc0 // Baseline + codeSOI = 0xd8 // Start of image. + codeDRI = 0xdd // Define restart interval. + codeDQT = 0xdb // Define quantization tables. + codeDHT = 0xde // Define hierarchical progression. + codeSOS = 0xda // Start of scan. + codeAPP0 = 0xe0 // TODO: find out what this is. + codeSOF0 = 0xc0 // Baseline +) + +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} + bitsACLum = []byte{0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d} + bitsACChr = []byte{0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77} + valDC = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} + valACLum = []byte{ + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, + 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, + 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, + 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, + 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, + 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, + 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, + 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, + 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, + 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, + 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa, + } + + valACChr = []byte{ + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, + 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, + 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, + 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, + 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, + 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, + 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, + 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, + 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, + 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, + 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, + 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, + 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, + 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa, + } ) // writeMarker writes an JPEG marker with code to w. @@ -73,3 +129,184 @@ func writeHuffman(w io.Writer, class, id int, bits, values []byte) error { return nil } + +// writeHeader writes a JPEG header to the writer w. +func writeHeader(w io.Writer, size, _type, width, height, nbqTab, dri int, qtable []byte) error { + 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) + } + + // 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) + } + + // If we want to define restart interval. + 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 >> 4), byte(dri)}) + if err != nil { + return fmt.Errorf("could not write restart interval value: %w", err) + } + } + + // Define quantization tables. + err = writeMarker(w, codeDQT) + if err != nil { + return fmt.Errorf("could not write DQI marker code: %w", err) + } + + // Calculate table size and create slice for table. + ts := 2 + nbqTab*(1+64) + _, err = w.Write([]byte{byte(ts >> 4), byte(ts)}) + if err != nil { + return fmt.Errorf("could not write quantization table size: %w", err) + } + + 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) + } + } + + // Define huffman table. + err = writeMarker(w, codeDHT) + if err != nil { + return fmt.Errorf("could not write DHT marker code: %w", err) + } + + var me multiError + me.add(writeHuffman(w, 0, 0, bitsDCLum, valDC)) + me.add(writeHuffman(w, 0, 1, bitsDCChr, valDC)) + me.add(writeHuffman(w, 1, 0, bitsACLum, valACLum)) + me.add(writeHuffman(w, 1, 1, bitsACChr, valACChr)) + if me != nil { + return fmt.Errorf("error writing huffman tables: %w", err) + } + return nil + + // Start of frame. + err = writeMarker(w, codeSOF0) + if err != nil { + return fmt.Errorf("could not write SOF0 marker code: %w", err) + } + + // Derive sample type. + sample := 1 + if _type != 0 { + sample = 2 + } + + // Derive matrix number. + mtxNo := 0 + if nbqTab == 2 { + 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) + } + + // 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 +} + +type multiError []error + +func (me multiError) Error() string { + return fmt.Sprintf("%v", []error(me)) +} + +func (me multiError) add(e error) { + me = append(me, e) +} + +type putter struct { + idx int +} + +func (p *putter) put16(b []byte, v uint16) { + binary.BigEndian.PutUint16(b[p.idx:], v) + p.idx += 2 +} + +func (p *putter) put8(b []byte, v uint8) { + b[p.idx] = byte(v) + p.idx++ +} + +func (p *putter) putBuf(dst, src []byte, l int) { + copy(dst[p.idx:], src) + p.idx++ +} From bee8cd270cc175c74620952176572c0d4b68ee36 Mon Sep 17 00:00:00 2001 From: Saxon Date: Wed, 20 Nov 2019 13:40:07 +1030 Subject: [PATCH 05/14] codec/mjpeg/extract.go: wrote Extractor type Wrote extractor type that provides an Extract function to extract JPEG frames from an RTP/MJPEG stream and writes them to a destination. --- codec/mjpeg/extract.go | 288 +++++++++++++++++++++++++++++++++++++++++ codec/mjpeg/jpeg.go | 97 +++++++------- 2 files changed, 337 insertions(+), 48 deletions(-) create mode 100644 codec/mjpeg/extract.go diff --git a/codec/mjpeg/extract.go b/codec/mjpeg/extract.go new file mode 100644 index 00000000..d0f2e791 --- /dev/null +++ b/codec/mjpeg/extract.go @@ -0,0 +1,288 @@ +/* +DESCRIPTION + extract.go provides an Extractor to get JPEG from RTP. + +AUTHOR + Saxon Nelson-Milton + +LICENSE + Copyright (C) 2017 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 ( + "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, + +} + +// 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. +} + +// NewExtractor returns a new Extractor. +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 + ) + + for { + n, err := src.Read(buf) + switch err { + case nil: // Do nothing. + case io.EOF: + return nil + default: + return fmt.Errorf("source read error: %v\n", err) + } + + // Get payload from RTP packet. + p, err := rtp.Payload(buf[:n]) + if err != nil { + 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) + } + + m, err := rtp.Marker(buf[:n]) + if err != nil { + 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) + } + } + } +} + +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: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 be4c7604..72daf6f6 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -39,6 +39,7 @@ const ( codeSOS = 0xda // Start of scan. codeAPP0 = 0xe0 // TODO: find out what this is. codeSOF0 = 0xc0 // Baseline + codeEOI = 0xd9 // End of image. ) var ( @@ -96,42 +97,37 @@ var ( } ) -// 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 +type multiError []error + +func (me multiError) Error() string { + return fmt.Sprintf("%v", []error(me)) } -// 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 (me multiError) add(e error) { + me = append(me, e) +} - var n int - for i := 1; i <= 16; i++ { - n += int(bits[i]) - } +type putter struct { + idx int +} - _, err = w.Write(bits[1:17]) - if err != nil { - return fmt.Errorf("could not write first lot of huffman bytes: %w", err) - } +func (p *putter) put16(b []byte, v uint16) { + binary.BigEndian.PutUint16(b[p.idx:], v) + p.idx += 2 +} - _, err = w.Write(values[0:n]) - if err != nil { - return fmt.Errorf("could not write second lot of huffman bytes: %w", err) - } +func (p *putter) put8(b []byte, v uint8) { + b[p.idx] = byte(v) + p.idx++ +} - return nil +func (p *putter) putBuf(dst, src []byte, l int) { + copy(dst[p.idx:], src) + p.idx++ } // writeHeader writes a JPEG header to the writer w. -func writeHeader(w io.Writer, size, _type, width, height, nbqTab, dri int, qtable []byte) error { +func writeHeader(w io.Writer, _type, width, height, nbqTab, dri int, qtable []byte) error { width <<= 3 height <<= 3 @@ -282,31 +278,36 @@ func writeHeader(w io.Writer, size, _type, width, height, nbqTab, dri int, qtabl return nil } -type multiError []error - -func (me multiError) Error() string { - return fmt.Sprintf("%v", []error(me)) +// 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 } -func (me multiError) add(e error) { - me = append(me, e) -} +// 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) + } -type putter struct { - idx int -} + var n int + for i := 1; i <= 16; i++ { + n += int(bits[i]) + } -func (p *putter) put16(b []byte, v uint16) { - binary.BigEndian.PutUint16(b[p.idx:], v) - p.idx += 2 -} + _, err = w.Write(bits[1:17]) + if err != nil { + return fmt.Errorf("could not write first lot of huffman bytes: %w", err) + } -func (p *putter) put8(b []byte, v uint8) { - b[p.idx] = byte(v) - p.idx++ -} + _, err = w.Write(values[0:n]) + if err != nil { + return fmt.Errorf("could not write second lot of huffman bytes: %w", err) + } -func (p *putter) putBuf(dst, src []byte, l int) { - copy(dst[p.idx:], src) - p.idx++ + return nil } From e467c7792dc0bef82e1d89a836661d24f942a015 Mon Sep 17 00:00:00 2001 From: Saxon Date: Fri, 22 Nov 2019 13:05:11 +1030 Subject: [PATCH 06/14] Fixed bugs, now working --- codec/mjpeg/extract.go | 8 ++++++-- codec/mjpeg/jpeg.go | 28 ++++++++++++++++++++-------- device/geovision/geovision.go | 7 ++++--- revid/revid.go | 2 +- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/codec/mjpeg/extract.go b/codec/mjpeg/extract.go index d0f2e791..4181639c 100644 --- a/codec/mjpeg/extract.go +++ b/codec/mjpeg/extract.go @@ -74,7 +74,11 @@ type Extractor struct { } // NewExtractor returns a new Extractor. -func NewExtractor() *Extractor { return &Extractor{} } +func NewExtractor() *Extractor { + return &Extractor{ + buf: new(bytes.Buffer), + } +} // Extract will continously read RTP packets from src containing JPEG (in RTP // payload format) and extract the JPEG images, sending them to dst. This @@ -248,7 +252,7 @@ func (b *byteStream) remaining() int { } func (b *byteStream) writeTo(w io.Writer, n int) error { - _n,err := w.Write(b.bytes[b.i:n]) + _n,err := w.Write(b.bytes[b.i:b.i+n]) b.i += _n if err != nil { return err diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go index 72daf6f6..fd44a623 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -25,6 +25,7 @@ LICENSE package mjpeg import ( + "bytes" "encoding/binary" "fmt" "io" @@ -35,7 +36,7 @@ const ( codeSOI = 0xd8 // Start of image. codeDRI = 0xdd // Define restart interval. codeDQT = 0xdb // Define quantization tables. - codeDHT = 0xde // Define hierarchical progression. + codeDHT = 0xc4 // Define huffman tables. codeSOS = 0xda // Start of scan. codeAPP0 = 0xe0 // TODO: find out what this is. codeSOF0 = 0xc0 // Baseline @@ -165,7 +166,7 @@ func writeHeader(w io.Writer, _type, width, height, nbqTab, dri int, qtable []by return fmt.Errorf("could not write DRI marker code: %w", err) } - _, err := w.Write([]byte{0x00, 0x04, byte(dri >> 4), byte(dri)}) + _, 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) } @@ -179,7 +180,7 @@ func writeHeader(w io.Writer, _type, width, height, nbqTab, dri int, qtable []by // Calculate table size and create slice for table. ts := 2 + nbqTab*(1+64) - _, err = w.Write([]byte{byte(ts >> 4), byte(ts)}) + _, err = w.Write([]byte{byte(ts >> 8), byte(ts)}) if err != nil { return fmt.Errorf("could not write quantization table size: %w", err) } @@ -203,14 +204,25 @@ func writeHeader(w io.Writer, _type, width, height, nbqTab, dri int, qtable []by } var me multiError - me.add(writeHuffman(w, 0, 0, bitsDCLum, valDC)) - me.add(writeHuffman(w, 0, 1, bitsDCChr, valDC)) - me.add(writeHuffman(w, 1, 0, bitsACLum, valACLum)) - me.add(writeHuffman(w, 1, 1, bitsACChr, valACChr)) + 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) } - return nil + + 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) + } // Start of frame. err = writeMarker(w, codeSOF0) diff --git a/device/geovision/geovision.go b/device/geovision/geovision.go index 492a05fb..7dce1f39 100644 --- a/device/geovision/geovision.go +++ b/device/geovision/geovision.go @@ -67,7 +67,7 @@ const ( defaultFrameRate = 25 defaultBitrate = 400 defaultVBRBitrate = 400 - defaultMinFrames = 100 + defaultMinFrames = 3 defaultVBRQuality = avconfig.QualityStandard defaultCameraChan = 2 ) @@ -130,9 +130,10 @@ func (g *GeoVision) Set(c avconfig.Config) error { c.Bitrate = defaultBitrate } - if c.MinFrames <= 0 { + refresh := float64(c.MinFrames) / float64(c.FrameRate) + if refresh < 1 || refresh > 5 { errs = append(errs, errGVBadMinFrames) - c.MinFrames = defaultMinFrames + c.MinFrames = 4 * c.FrameRate } switch c.VBRQuality { diff --git a/revid/revid.go b/revid/revid.go index 4885bc1b..1863cb02 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -344,7 +344,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case codecutil.H265: r.lexTo = h265.NewLexer(false).Lex case codecutil.MJPEG: - panic("not implemented") + r.lexTo = mjpeg.NewExtractor().Extract } case config.InputAudio: From 82d9e5e8bdc074fd3cc13bb7133ab7bd60e41a6b Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 23 Nov 2019 14:23:35 +1030 Subject: [PATCH 07/14] codec/mjpeg: tidying up Separated my code from code that was ported from ffmpeg (differen copyright). Also added utils.go file to house the putBuffer and bytestream types. Reduced copying and use of bytes.Buffer. Instead expanded putBuffer functionality so that I can use this throughout process (reduce copying from buffer to buffer). --- codec/mjpeg/extract.go | 236 ++-------------------- codec/mjpeg/jpeg.go | 434 ++++++++++++++++++++++++----------------- codec/mjpeg/utils.go | 120 ++++++++++++ 3 files changed, 385 insertions(+), 405 deletions(-) create mode 100644 codec/mjpeg/utils.go 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 +} From 5b3988a5e0d49d68d2f79ffee736ecca470a3942 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 23 Nov 2019 15:33:41 +1030 Subject: [PATCH 08/14] codec/mjpeg/extract.go: corrected comment for Extractor.dst field --- codec/mjpeg/extract.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codec/mjpeg/extract.go b/codec/mjpeg/extract.go index 96d5ba15..857b7b37 100644 --- a/codec/mjpeg/extract.go +++ b/codec/mjpeg/extract.go @@ -37,7 +37,7 @@ const maxRTPSize = 1500 // Max ethernet transmission unit in bytes. // Extractor is an Extractor for extracting JPEG from an RTP stream. type Extractor struct { - dst io.Writer // The destination we'll be writing extracted NALUs to. + dst io.Writer // The destination we'll be writing extracted JPEGs to. } // NewExtractor returns a new Extractor. From 870c0bc3fa181e64efba93829626993c01969461 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 23 Nov 2019 15:34:59 +1030 Subject: [PATCH 09/14] codec/mjpeg/jpeg.go: fixed indentation in file header --- codec/mjpeg/jpeg.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go index 2f4821f8..3c5f3a53 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -1,11 +1,11 @@ /* DESCRIPTION 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). + 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. + This code can be used to build JPEG images from an RTP/JPEG stream. AUTHOR Saxon Nelson-Milton From 6407f24d906036fb02bac508940d8712db67a18e Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 23 Dec 2019 12:48:08 +1030 Subject: [PATCH 10/14] codec/mjpeg/jpeg.go: fixed indentation on file header --- codec/mjpeg/extract.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codec/mjpeg/extract.go b/codec/mjpeg/extract.go index 857b7b37..58b91367 100644 --- a/codec/mjpeg/extract.go +++ b/codec/mjpeg/extract.go @@ -1,7 +1,7 @@ /* DESCRIPTION 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). + defined by RFC 2435 (see https://tools.ietf.org/html/rfc2435). AUTHOR Saxon Nelson-Milton From 6c379458d794827f5b18c233c5954043812a9be1 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 23 Dec 2019 12:51:21 +1030 Subject: [PATCH 11/14] codec/mjpeg/extract.go: simplified error message when can't get RTP payload --- codec/mjpeg/extract.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codec/mjpeg/extract.go b/codec/mjpeg/extract.go index 58b91367..9174074f 100644 --- a/codec/mjpeg/extract.go +++ b/codec/mjpeg/extract.go @@ -63,7 +63,7 @@ func (e *Extractor) Extract(dst io.Writer, src io.Reader, delay time.Duration) e // Get payload from RTP packet. p, err := rtp.Payload(buf[:n]) if err != nil { - return fmt.Errorf("could not get RTP payload, failed with err: %v\n", err) + return fmt.Errorf("could not get RTP payload: %w\n", err) } // Also grab the marker so that we know when the JPEG is finished. From 4df5f11364a21b1bd0933e4bcc04b357d6a24c74 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 23 Dec 2019 12:54:31 +1030 Subject: [PATCH 12/14] codec/mjpeg/jpeg.go: Fixed comment for Ctx struct --- codec/mjpeg/jpeg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go index 3c5f3a53..804119fc 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -135,7 +135,7 @@ var ( ) // 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. +// JPEG (held by p), and the state of the quantization tables. type Ctx struct { qTables [128][128]byte qTablesLen [128]byte From 7ee35f650fe02c5a52a352fc29dca40aad218172 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 23 Dec 2019 12:59:25 +1030 Subject: [PATCH 13/14] codec/mjpeg/jpeg.go: renamed ParseScan to ParsePayload, updated call and comment --- codec/mjpeg/extract.go | 2 +- codec/mjpeg/jpeg.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codec/mjpeg/extract.go b/codec/mjpeg/extract.go index 9174074f..6fb987bd 100644 --- a/codec/mjpeg/extract.go +++ b/codec/mjpeg/extract.go @@ -72,7 +72,7 @@ func (e *Extractor) Extract(dst io.Writer, src io.Reader, delay time.Duration) e return fmt.Errorf("could not read RTP marker: %w", err) } - err = c.ParseScan(p, m) + err = c.ParsePayload(p, m) if err != nil { return fmt.Errorf("could not parse JPEG scan: %w", err) } diff --git a/codec/mjpeg/jpeg.go b/codec/mjpeg/jpeg.go index 804119fc..41f9e529 100644 --- a/codec/mjpeg/jpeg.go +++ b/codec/mjpeg/jpeg.go @@ -151,8 +151,8 @@ func NewCtx(d io.Writer) *Ctx { } } -// ParseScan will parse a JPEG scan from an RTP/JPEG payload and append -func (c *Ctx) ParseScan(p []byte, m bool) error { +// ParsePayload will parse an RTP/JPEG payload and append to current image. +func (c *Ctx) ParsePayload(p []byte, m bool) error { b := newByteStream(p) _ = b.get8() // Ignore type-specific flag From 39c66bdfd6fe245d2fd6ad5f086833e8395f72cc Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 23 Dec 2019 13:59:27 +1030 Subject: [PATCH 14/14] codec/mjpeg/utils.go: using BigEndian.Uint16 in get16 --- codec/mjpeg/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codec/mjpeg/utils.go b/codec/mjpeg/utils.go index 23c0fb3e..511a9da3 100644 --- a/codec/mjpeg/utils.go +++ b/codec/mjpeg/utils.go @@ -95,7 +95,7 @@ func (b *byteStream) get8() int { } func (b *byteStream) get16() int { - v := int(b.bytes[b.i])<<8 | int(b.bytes[b.i+1]) + v := int(binary.BigEndian.Uint16(b.bytes[b.i:])) b.i += 2 return v }