diff --git a/stream/lex/lex.go b/stream/lex/lex.go new file mode 100644 index 00000000..115915c3 --- /dev/null +++ b/stream/lex/lex.go @@ -0,0 +1,248 @@ +/* +NAME + lex.go + +DESCRIPTION + See Readme.md + +AUTHOR + Dan Kortschak + +LICENSE + lex.go is 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 lex provides lexers for video encodings. +package lex + +import ( + "bufio" + "bytes" + "fmt" + "io" + "time" + + "bitbucket.org/ausocean/av/stream" +) + +var noDelay = make(chan time.Time) + +func init() { + close(noDelay) +} + +var h264Prefix = [...]byte{0x00, 0x00, 0x01, 0x09, 0xf0} + +// H264 lexes H.264 NAL units read from src into separate writes to dst with +// successive writes being performed not earlier than the specified delay. +// NAL units are split after type 1 (Coded slice of a non-IDR picture), 5 +// (Coded slice of a IDR picture) and 8 (Picture parameter set). +func H264(dst stream.Encoder, src io.Reader, delay time.Duration) error { + var tick <-chan time.Time + if delay == 0 { + tick = noDelay + } else { + ticker := time.NewTicker(delay) + defer ticker.Stop() + tick = ticker.C + } + + const bufSize = 8 << 10 + + c := newScanner(src, make([]byte, 4<<10)) // Standard file buffer size. + + buf := make([]byte, len(h264Prefix), bufSize) + copy(buf, h264Prefix[:]) + writeOut := false +outer: + for { + var b byte + var err error + buf, b, err = c.scanUntilZeroInto(buf) + if err != nil { + if err != io.EOF { + return err + } + break + } + + for n := 1; b == 0x0 && n < 4; n++ { + b, err = c.readByte() + if err != nil { + if err != io.EOF { + return err + } + break outer + } + buf = append(buf, b) + + if b != 0x1 || (n != 2 && n != 3) { + continue + } + + if writeOut { + <-tick + err := dst.Encode(buf[:len(buf)-(n+1)]) + if err != nil { + return err + } + buf = make([]byte, len(h264Prefix)+n, bufSize) + copy(buf, h264Prefix[:]) + buf = append(buf, 1) + writeOut = false + } + + b, err = c.readByte() + if err != nil { + if err != io.EOF { + return err + } + break outer + } + buf = append(buf, b) + + // http://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-200305-S!!PDF-E&type=items + // Table 7-1 NAL unit type codes + const ( + nonIdrPic = 1 + idrPic = 5 + paramSet = 8 + ) + switch nalTyp := b & 0x1f; nalTyp { + case nonIdrPic, idrPic, paramSet: + writeOut = true + } + } + } + if len(buf) == len(h264Prefix) { + return nil + } + <-tick + err := dst.Encode(buf) + return err +} + +// scanner is a byte scanner. +type scanner struct { + buf []byte + off int + + // r is the source of data for the scanner. + r io.Reader +} + +// newScanner returns a scanner initialised with an io.Reader and a read buffer. +func newScanner(r io.Reader, buf []byte) *scanner { + return &scanner{r: r, buf: buf[:0]} +} + +// scanUntilZeroInto scans the scanner's underlying io.Reader until a zero byte +// has been read, appending all read bytes to dst. The resulting appended data, +// the last read byte and whether the last read byte was zero are returned. +func (c *scanner) scanUntilZeroInto(dst []byte) (res []byte, b byte, err error) { +outer: + for { + var i int + for i, b = range c.buf[c.off:] { + if b != 0x0 { + continue + } + dst = append(dst, c.buf[c.off:c.off+i+1]...) + c.off += i + 1 + break outer + } + dst = append(dst, c.buf[c.off:]...) + err = c.reload() + if err != nil { + break + } + } + return dst, b, err +} + +// readByte is an unexported ReadByte. +func (c *scanner) readByte() (byte, error) { + if c.off >= len(c.buf) { + err := c.reload() + if err != nil { + return 0, err + } + } + b := c.buf[c.off] + c.off++ + return b, nil +} + +// reload re-fills the scanner's buffer. +func (c *scanner) reload() error { + n, err := c.r.Read(c.buf[:cap(c.buf)]) + c.buf = c.buf[:n] + if err != nil { + if err != io.EOF { + return err + } + if n == 0 { + return io.EOF + } + } + c.off = 0 + return nil +} + +// MJPEG parses MJPEG frames read from src into separate writes to dst with +// successive writes being performed not earlier than the specified delay. +func MJPEG(dst stream.Encoder, src io.Reader, delay time.Duration) error { + var tick <-chan time.Time + if delay == 0 { + tick = noDelay + } else { + ticker := time.NewTicker(delay) + defer ticker.Stop() + tick = ticker.C + } + + r := bufio.NewReader(src) + for { + buf := make([]byte, 2, 4<<10) + n, err := r.Read(buf) + if n < 2 { + return nil + } + if err != nil { + return err + } + if !bytes.Equal(buf, []byte{0xff, 0xd8}) { + return fmt.Errorf("parser: not MJPEG frame start: %#v", buf) + } + var last byte + for { + b, err := r.ReadByte() + if err != nil { + return err + } + buf = append(buf, b) + if last == 0xff && b == 0xd9 { + break + } + last = b + } + <-tick + err = dst.Encode(buf) + if err != nil { + return err + } + } +} diff --git a/stream/lex/lex_test.go b/stream/lex/lex_test.go new file mode 100644 index 00000000..34730227 --- /dev/null +++ b/stream/lex/lex_test.go @@ -0,0 +1,346 @@ +/* +NAME + lex_test.go + +DESCRIPTION + See Readme.md + +AUTHOR + Dan Kortschak + +LICENSE + lex_test.go is 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 lex + +import ( + "bytes" + "fmt" + "reflect" + "testing" + "time" +) + +var h264Tests = []struct { + name string + input []byte + delay time.Duration + want [][]byte + err error +}{ + { + name: "empty", + }, + { + name: "null short", + input: []byte{0x00, 0x00, 0x01, 0x0}, + delay: 0, + want: [][]byte{{0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x0}}, + }, + { + name: "null short delayed", + input: []byte{0x00, 0x00, 0x01, 0x0}, + delay: time.Millisecond, + want: [][]byte{{0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x0}}, + }, + { + name: "full short type 1", + input: []byte{ + 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x01, 0x41, 'w', 'i', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd', + }, + delay: 0, + want: [][]byte{ + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x01, 0x41, 'w', 'i', 't', 'h'}, + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd'}, + }, + }, + { + name: "full short type 5", + input: []byte{ + 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x01, 0x45, 'w', 'i', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd', + }, + delay: 0, + want: [][]byte{ + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x01, 0x45, 'w', 'i', 't', 'h'}, + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd'}, + }, + }, + { + name: "full short type 8", + input: []byte{ + 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x01, 0x48, 'w', 'i', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd', + }, + delay: 0, + want: [][]byte{ + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x01, 0x48, 'w', 'i', 't', 'h'}, + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd'}, + }, + }, + { + name: "full short delayed", + input: []byte{ + 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x01, 0x41, 'w', 'i', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd', + }, + delay: time.Millisecond, + want: [][]byte{ + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x01, 0x41, 'w', 'i', 't', 'h'}, + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd'}, + }, + }, + { + name: "full long type 1", + input: []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x00, 0x01, 0x41, 'w', 'i', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd', + }, + delay: 0, + want: [][]byte{ + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x00, 0x01, 0x41, 'w', 'i', 't', 'h'}, + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd'}, + }, + }, + { + name: "full long type 5", + input: []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x00, 0x01, 0x45, 'w', 'i', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd', + }, + delay: 0, + want: [][]byte{ + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x00, 0x01, 0x45, 'w', 'i', 't', 'h'}, + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd'}, + }, + }, + { + name: "full long type 8", + input: []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x00, 0x01, 0x48, 'w', 'i', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd', + }, + delay: 0, + want: [][]byte{ + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x00, 0x01, 0x48, 'w', 'i', 't', 'h'}, + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd'}, + }, + }, + { + name: "full long delayed", + input: []byte{ + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x00, 0x01, 0x41, 'w', 'i', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd', + }, + delay: time.Millisecond, + want: [][]byte{ + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'u', 'l', 'l', + 0x00, 0x00, 0x00, 0x01, 0x00, 'f', 'r', 'a', 'm', 'e', + 0x00, 0x00, 0x00, 0x01, 0x41, 'w', 'i', 't', 'h'}, + {0x0, 0x0, 0x1, 0x9, 0xf0, 0x00, 0x00, 0x00, 0x01, 0x00, 'l', 'e', 'n', 'g', 't', 'h', + 0x00, 0x00, 0x00, 0x01, 0x00, 's', 'p', 'r', 'e', 'a', 'd'}, + }, + }, +} + +func TestH264(t *testing.T) { + for _, test := range h264Tests { + var buf chunkEncoder + err := H264(&buf, bytes.NewReader(test.input), test.delay) + if fmt.Sprint(err) != fmt.Sprint(test.err) { + t.Errorf("unexpected error for %q: got:%v want:%v", test.name, err, test.err) + } + if err != nil { + continue + } + got := [][]byte(buf) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("unexpected result for %q:\ngot :%#v\nwant:%#v", test.name, got, test.want) + } + } +} + +var mjpegTests = []struct { + name string + input []byte + delay time.Duration + want [][]byte + err error +}{ + { + name: "empty", + }, + { + name: "null", + input: []byte{0xff, 0xd8, 0xff, 0xd9}, + delay: 0, + want: [][]byte{{0xff, 0xd8, 0xff, 0xd9}}, + }, + { + name: "null delayed", + input: []byte{0xff, 0xd8, 0xff, 0xd9}, + delay: time.Millisecond, + want: [][]byte{{0xff, 0xd8, 0xff, 0xd9}}, + }, + { + name: "full", + input: []byte{ + 0xff, 0xd8, 'f', 'u', 'l', 'l', 0xff, 0xd9, + 0xff, 0xd8, 'f', 'r', 'a', 'm', 'e', 0xff, 0xd9, + 0xff, 0xd8, 'w', 'i', 't', 'h', 0xff, 0xd9, + 0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9, + 0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9, + }, + delay: 0, + want: [][]byte{ + {0xff, 0xd8, 'f', 'u', 'l', 'l', 0xff, 0xd9}, + {0xff, 0xd8, 'f', 'r', 'a', 'm', 'e', 0xff, 0xd9}, + {0xff, 0xd8, 'w', 'i', 't', 'h', 0xff, 0xd9}, + {0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9}, + {0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9}, + }, + }, + { + name: "full delayed", + input: []byte{ + 0xff, 0xd8, 'f', 'u', 'l', 'l', 0xff, 0xd9, + 0xff, 0xd8, 'f', 'r', 'a', 'm', 'e', 0xff, 0xd9, + 0xff, 0xd8, 'w', 'i', 't', 'h', 0xff, 0xd9, + 0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9, + 0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9, + }, + delay: time.Millisecond, + want: [][]byte{ + {0xff, 0xd8, 'f', 'u', 'l', 'l', 0xff, 0xd9}, + {0xff, 0xd8, 'f', 'r', 'a', 'm', 'e', 0xff, 0xd9}, + {0xff, 0xd8, 'w', 'i', 't', 'h', 0xff, 0xd9}, + {0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9}, + {0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9}, + }, + }, +} + +func TestMJEG(t *testing.T) { + for _, test := range mjpegTests { + var buf chunkEncoder + err := MJPEG(&buf, bytes.NewReader(test.input), test.delay) + if fmt.Sprint(err) != fmt.Sprint(test.err) { + t.Errorf("unexpected error for %q: got:%v want:%v", test.name, err, test.err) + } + if err != nil { + continue + } + got := [][]byte(buf) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("unexpected result for %q:\ngot :%#v\nwant:%#v", test.name, got, test.want) + } + } +} + +type chunkEncoder [][]byte + +func (e *chunkEncoder) Encode(b []byte) error { + *e = append(*e, b) + return nil +} + +func (*chunkEncoder) Stream() <-chan []byte { panic("INVALID USE") } + +func TestScannerReadByte(t *testing.T) { + data := []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") + + for _, size := range []int{1, 2, 8, 1 << 10} { + r := newScanner(bytes.NewReader(data), make([]byte, size)) + var got []byte + for { + b, err := r.readByte() + if err != nil { + break + } + got = append(got, b) + } + if !bytes.Equal(got, data) { + t.Errorf("unexpected result for buffer size %d:\ngot :%q\nwant:%q", size, got, data) + } + } +} + +func TestScannerScanUntilZero(t *testing.T) { + data := []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit,\x00 sed do eiusmod tempor incididunt ut \x00labore et dolore magna aliqua.") + + for _, size := range []int{1, 2, 8, 1 << 10} { + r := newScanner(bytes.NewReader(data), make([]byte, size)) + var got [][]byte + for { + buf, _, err := r.scanUntilZeroInto(nil) + got = append(got, buf) + if err != nil { + break + } + } + want := bytes.SplitAfter(data, []byte{0}) + if !reflect.DeepEqual(got, want) { + t.Errorf("unexpected result for buffer zie %d:\ngot :%q\nwant:%q", size, got, want) + } + } +}