From 7e4e2fe7c6fa5fb542aeb118122034ab271d4837 Mon Sep 17 00:00:00 2001 From: Trek H Date: Fri, 5 Apr 2019 16:28:25 +1030 Subject: [PATCH 01/15] mts: Added audio mts encoding and test --- container/mts/audio_test.go | 109 +++++++++++++++++++++++++++++++ container/mts/encoder.go | 76 ++++++++++++++------- container/mts/metaEncode_test.go | 4 +- container/mts/mpegts.go | 10 ++- container/mts/pes/pes.go | 2 +- 5 files changed, 170 insertions(+), 31 deletions(-) create mode 100644 container/mts/audio_test.go diff --git a/container/mts/audio_test.go b/container/mts/audio_test.go new file mode 100644 index 00000000..d03bd14e --- /dev/null +++ b/container/mts/audio_test.go @@ -0,0 +1,109 @@ +/* +NAME + audio_test.go + +DESCRIPTION + See Readme.md + +AUTHOR + Trek Hopton + +LICENSE + audio_test.go is Copyright (C) 2017-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 mts + +import ( + "bytes" + "io/ioutil" + "log" + "testing" + + "bitbucket.org/ausocean/av/container/mts/meta" + "github.com/Comcast/gots/packet" + "github.com/Comcast/gots/pes" +) + +// TestEncodePcm tests the mpegts encoder's ability to encode pcm audio data. +// It reads and encodes input pcm data into mpegts, then decodes the mpegts compares the result to the input pcm. +func TestEncodePcm(t *testing.T) { + Meta = meta.New() + + var b []byte + buf := bytes.NewBuffer(b) + sampleRate := 48000 + sampleSize := 2 + writeSize := 16000 + writeFreq := float64(sampleRate*sampleSize) / float64(writeSize) + e := NewEncoder(buf, writeFreq, Audio) + + inPath := "../../../test/test-data/av/input/sweep_400Hz_20000Hz_-3dBFS_5s_48khz.pcm" + inPcm, err := ioutil.ReadFile(inPath) + if err != nil { + log.Fatal(err) + } + + // Encode pcm to mts and get the resulting bytes. + _, err = e.Write(inPcm) + if err != nil { + log.Fatal(err) + } + clip := buf.Bytes() + + // Decode the mts packets to extract the original data + var pkt packet.Packet + pesPacket := make([]byte, 0, writeSize) + got := make([]byte, 0, len(inPcm)) + i := 0 + for i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + if pkt.PID() == audioPid { + if pkt.PayloadUnitStartIndicator() { + payload, err := pkt.Payload() + if err != nil { + t.Fatalf("Unexpected err: %v\n", err) + } + pesPacket = append(pesPacket, payload...) + i += PacketSize + first := true + for (first || !pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { + first = false + copy(pkt[:], clip[i:i+PacketSize]) + payload, err := pkt.Payload() + if err != nil { + t.Fatalf("Unexpected err: %v\n", err) + } + + pesPacket = append(pesPacket, payload...) + i += PacketSize + } + } + pesHeader, err := pes.NewPESHeader(pesPacket) + if err != nil { + t.Fatalf("Unexpected err: %v\n", err) + } + got = append(got, pesHeader.Data()...) + } else { + i += PacketSize + } + } + + // Compare encoded data with original data. + if !bytes.Equal(got, inPcm) { + t.Error("Error, unexpected output") + } +} diff --git a/container/mts/encoder.go b/container/mts/encoder.go index 92e87051..5824ca15 100644 --- a/container/mts/encoder.go +++ b/container/mts/encoder.go @@ -101,11 +101,20 @@ var ( ) const ( - sdtPid = 17 - patPid = 0 - pmtPid = 4096 - videoPid = 256 - streamID = 0xe0 // First video stream ID. + sdtPid = 17 + patPid = 0 + pmtPid = 4096 + videoPid = 256 + audioPid = 210 + videoStreamID = 0xe0 // First video stream ID. + audioStreamID = 0xc0 // First audio stream ID. +) + +// Video and Audio constants are used to communicate which media type will be encoded when creating a +// new encoder with NewEncoder. +const ( + Video = iota + Audio ) // Time related constants. @@ -122,38 +131,54 @@ const ( type Encoder struct { dst io.Writer - clock time.Duration - lastTime time.Time - frameInterval time.Duration - ptsOffset time.Duration - tsSpace [PacketSize]byte - pesSpace [pes.MaxPesSize]byte + clock time.Duration + lastTime time.Time + writePeriod time.Duration + ptsOffset time.Duration + tsSpace [PacketSize]byte + pesSpace [pes.MaxPesSize]byte continuity map[int]byte timeBasedPsi bool pktCount int psiSendCount int + mediaPid int + streamID byte psiLastTime time.Time } // NewEncoder returns an Encoder with the specified frame rate. -func NewEncoder(dst io.Writer, fps float64) *Encoder { +func NewEncoder(dst io.Writer, writeFreq float64, mediaType int) *Encoder { + var mPid int + var sid byte + switch mediaType { + case Audio: + mPid = audioPid + sid = audioStreamID + case Video: + mPid = videoPid + sid = videoStreamID + } + return &Encoder{ dst: dst, - frameInterval: time.Duration(float64(time.Second) / fps), - ptsOffset: ptsOffset, + writePeriod: time.Duration(float64(time.Second) / writeFreq), + ptsOffset: ptsOffset, timeBasedPsi: true, pktCount: 8, + mediaPid: mPid, + streamID: sid, + continuity: map[int]byte{ - patPid: 0, - pmtPid: 0, - videoPid: 0, + patPid: 0, + pmtPid: 0, + mPid: 0, }, } } @@ -180,7 +205,7 @@ func (e *Encoder) TimeBasedPsi(b bool, sendCount int) { // Write implements io.Writer. Write takes raw h264 and encodes into mpegts, // then sending it to the encoder's io.Writer destination. -func (e *Encoder) Write(nalu []byte) (int, error) { +func (e *Encoder) Write(data []byte) (int, error) { now := time.Now() if (e.timeBasedPsi && (now.Sub(e.psiLastTime) > psiInterval)) || (!e.timeBasedPsi && (e.pktCount >= e.psiSendCount)) { e.pktCount = 0 @@ -193,21 +218,22 @@ func (e *Encoder) Write(nalu []byte) (int, error) { // Prepare PES data. pesPkt := pes.Packet{ - StreamID: streamID, + StreamID: e.streamID, PDI: hasPTS, PTS: e.pts(), - Data: nalu, + Data: data, HeaderLength: 5, } + buf := pesPkt.Bytes(e.pesSpace[:pes.MaxPesSize]) pusi := true for len(buf) != 0 { pkt := Packet{ PUSI: pusi, - PID: videoPid, + PID: uint16(e.mediaPid), RAI: pusi, - CC: e.ccFor(videoPid), + CC: e.ccFor(e.mediaPid), AFC: hasAdaptationField | hasPayload, PCRF: pusi, } @@ -222,14 +248,14 @@ func (e *Encoder) Write(nalu []byte) (int, error) { } _, err := e.dst.Write(pkt.Bytes(e.tsSpace[:PacketSize])) if err != nil { - return len(nalu), err + return len(data), err } e.pktCount++ } e.tick() - return len(nalu), nil + return len(data), nil } // writePSI creates mpegts with pat and pmt tables - with pmt table having updated @@ -271,7 +297,7 @@ func (e *Encoder) writePSI() error { // tick advances the clock one frame interval. func (e *Encoder) tick() { - e.clock += e.frameInterval + e.clock += e.writePeriod } // pts retuns the current presentation timestamp. diff --git a/container/mts/metaEncode_test.go b/container/mts/metaEncode_test.go index 00f806b9..2bf94d64 100644 --- a/container/mts/metaEncode_test.go +++ b/container/mts/metaEncode_test.go @@ -49,7 +49,7 @@ func TestMetaEncode1(t *testing.T) { Meta = meta.New() var b []byte buf := bytes.NewBuffer(b) - e := NewEncoder(buf, fps) + e := NewEncoder(buf, fps, Video) Meta.Add("ts", "12345678") if err := e.writePSI(); err != nil { t.Errorf(errUnexpectedErr, err.Error()) @@ -78,7 +78,7 @@ func TestMetaEncode2(t *testing.T) { Meta = meta.New() var b []byte buf := bytes.NewBuffer(b) - e := NewEncoder(buf, fps) + e := NewEncoder(buf, fps, Video) Meta.Add("ts", "12345678") Meta.Add("loc", "1234,4321,1234") if err := e.writePSI(); err != nil { diff --git a/container/mts/mpegts.go b/container/mts/mpegts.go index 18fe8b5f..ed8cbe02 100644 --- a/container/mts/mpegts.go +++ b/container/mts/mpegts.go @@ -74,7 +74,7 @@ const ( ) /* -The below data struct encapsulates the fields of an MPEG-TS packet. Below is +Packet encapsulates the fields of an MPEG-TS packet. Below is the formatting of an MPEG-TS packet for reference! MPEG-TS Packet Formatting @@ -196,7 +196,11 @@ func FindPid(d []byte, pid uint16) (pkt []byte, i int, err error) { func (p *Packet) FillPayload(data []byte) int { currentPktLen := 6 + asInt(p.PCRF)*6 + asInt(p.OPCRF)*6 + asInt(p.SPF)*1 + asInt(p.TPDF)*1 + len(p.TPD) - p.Payload = make([]byte, PayloadSize-currentPktLen) + if len(data) > PayloadSize-currentPktLen { + p.Payload = make([]byte, PayloadSize-currentPktLen) + } else { + p.Payload = make([]byte, len(data)) + } return copy(p.Payload, data) } @@ -214,7 +218,7 @@ func asByte(b bool) byte { return 0 } -// ToByteSlice interprets the fields of the ts packet instance and outputs a +// Bytes interprets the fields of the ts packet instance and outputs a // corresponding byte slice func (p *Packet) Bytes(buf []byte) []byte { if buf == nil || cap(buf) != PacketSize { diff --git a/container/mts/pes/pes.go b/container/mts/pes/pes.go index 1e085166..50544aa2 100644 --- a/container/mts/pes/pes.go +++ b/container/mts/pes/pes.go @@ -26,7 +26,7 @@ LICENSE package pes -const MaxPesSize = 10000 +const MaxPesSize = 65536 /* The below data struct encapsulates the fields of an PES packet. Below is From d15d0ddb86c8251693c9c00382fd083c30c5c689 Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 9 Apr 2019 13:33:58 +0930 Subject: [PATCH 02/15] mts: Limited size of encoder writes and updated audio_test Previously the encoder would not care if a write was given that exceeded the max PES packet size because we were never using PES packets bigger than a frame of video. Now I have changed it so that the encoder will check the write length and create a new PES packet if needed. I have also restructured my test so that it can extract the data from PES packets that span accross multiple MTS packets. --- container/mts/audio_test.go | 52 ++++++++++++++++++++++++++++--------- container/mts/encoder.go | 4 +++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/container/mts/audio_test.go b/container/mts/audio_test.go index d03bd14e..36df7df8 100644 --- a/container/mts/audio_test.go +++ b/container/mts/audio_test.go @@ -47,8 +47,8 @@ func TestEncodePcm(t *testing.T) { buf := bytes.NewBuffer(b) sampleRate := 48000 sampleSize := 2 - writeSize := 16000 - writeFreq := float64(sampleRate*sampleSize) / float64(writeSize) + blockSize := 16000 + writeFreq := float64(sampleRate*sampleSize) / float64(blockSize) e := NewEncoder(buf, writeFreq, Audio) inPath := "../../../test/test-data/av/input/sweep_400Hz_20000Hz_-3dBFS_5s_48khz.pcm" @@ -58,19 +58,33 @@ func TestEncodePcm(t *testing.T) { } // Encode pcm to mts and get the resulting bytes. - _, err = e.Write(inPcm) - if err != nil { - log.Fatal(err) + for i := 0; i < len(inPcm); i += blockSize { + if len(inPcm)-i < blockSize { + block := inPcm[i:] + _, err = e.Write(block) + if err != nil { + log.Fatal(err) + } + } else { + block := inPcm[i : i+blockSize] + _, err = e.Write(block) + if err != nil { + log.Fatal(err) + } + } } clip := buf.Bytes() // Decode the mts packets to extract the original data var pkt packet.Packet - pesPacket := make([]byte, 0, writeSize) + pesPacket := make([]byte, 0, blockSize) got := make([]byte, 0, len(inPcm)) i := 0 - for i+PacketSize <= len(clip) { + if i+PacketSize <= len(clip) { copy(pkt[:], clip[i:i+PacketSize]) + } + + for i+PacketSize <= len(clip) { if pkt.PID() == audioPid { if pkt.PayloadUnitStartIndicator() { payload, err := pkt.Payload() @@ -78,18 +92,28 @@ func TestEncodePcm(t *testing.T) { t.Fatalf("Unexpected err: %v\n", err) } pesPacket = append(pesPacket, payload...) + i += PacketSize - first := true - for (first || !pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { - first = false + if i+PacketSize <= len(clip) { copy(pkt[:], clip[i:i+PacketSize]) - payload, err := pkt.Payload() + } + + for (!pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { + payload, err = pkt.Payload() if err != nil { t.Fatalf("Unexpected err: %v\n", err) } - pesPacket = append(pesPacket, payload...) + i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + } + } + } else { + i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) } } pesHeader, err := pes.NewPESHeader(pesPacket) @@ -97,8 +121,12 @@ func TestEncodePcm(t *testing.T) { t.Fatalf("Unexpected err: %v\n", err) } got = append(got, pesHeader.Data()...) + pesPacket = pesPacket[:0] } else { i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + } } } diff --git a/container/mts/encoder.go b/container/mts/encoder.go index 5824ca15..34cfc8de 100644 --- a/container/mts/encoder.go +++ b/container/mts/encoder.go @@ -29,6 +29,7 @@ LICENSE package mts import ( + "fmt" "io" "time" @@ -206,6 +207,9 @@ func (e *Encoder) TimeBasedPsi(b bool, sendCount int) { // Write implements io.Writer. Write takes raw h264 and encodes into mpegts, // then sending it to the encoder's io.Writer destination. func (e *Encoder) Write(data []byte) (int, error) { + if len(data) > pes.MaxPesSize { + return 0, fmt.Errorf("data size too large (Max is %v): %v", pes.MaxPesSize, len(data)) + } now := time.Now() if (e.timeBasedPsi && (now.Sub(e.psiLastTime) > psiInterval)) || (!e.timeBasedPsi && (e.pktCount >= e.psiSendCount)) { e.pktCount = 0 From b09a6e210ea3630aef1c770884d4c2b5d2c02adb Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 9 Apr 2019 13:53:14 +0930 Subject: [PATCH 03/15] mts: Changed uses of NewEncoder in revid and senders_test to use extra argument. --- revid/revid.go | 2 +- revid/senders_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/revid/revid.go b/revid/revid.go index adb60a92..6939399d 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -258,7 +258,7 @@ func (r *Revid) setupPipeline(mtsEnc func(io.Writer, int) io.Writer, flvEnc func } func newMtsEncoder(dst io.Writer, fps int) io.Writer { - e := mts.NewEncoder(dst, float64(fps)) + e := mts.NewEncoder(dst, float64(fps), mts.Video) return e } diff --git a/revid/senders_test.go b/revid/senders_test.go index 2f464de0..0186b356 100644 --- a/revid/senders_test.go +++ b/revid/senders_test.go @@ -111,7 +111,7 @@ func TestMtsSenderSegment(t *testing.T) { tstSender := &sender{} loadSender := newMtsSender(tstSender, log) rb := ring.NewBuffer(rbSize, rbElementSize, wTimeout) - encoder := mts.NewEncoder((*buffer)(rb), 25) + encoder := mts.NewEncoder((*buffer)(rb), 25, mts.Video) // Turn time based PSI writing off for encoder. const psiSendCount = 10 @@ -201,7 +201,7 @@ func TestMtsSenderDiscontinuity(t *testing.T) { tstSender := &sender{testDiscontinuities: true, discontinuityAt: clipWithDiscontinuity} loadSender := newMtsSender(tstSender, log) rb := ring.NewBuffer(rbSize, rbElementSize, wTimeout) - encoder := mts.NewEncoder((*buffer)(rb), 25) + encoder := mts.NewEncoder((*buffer)(rb), 25, mts.Video) // Turn time based PSI writing off for encoder. const psiSendCount = 10 From 78447ed495d3450c81629896329ef7f7309f9a40 Mon Sep 17 00:00:00 2001 From: Trek H Date: Fri, 5 Apr 2019 16:28:25 +1030 Subject: [PATCH 04/15] mts: Added audio mts encoding and test --- container/mts/audio_test.go | 109 +++++++++++++++++++++++++++++++ container/mts/encoder.go | 76 ++++++++++++++------- container/mts/metaEncode_test.go | 4 +- container/mts/mpegts.go | 10 ++- container/mts/pes/pes.go | 2 +- 5 files changed, 170 insertions(+), 31 deletions(-) create mode 100644 container/mts/audio_test.go diff --git a/container/mts/audio_test.go b/container/mts/audio_test.go new file mode 100644 index 00000000..d03bd14e --- /dev/null +++ b/container/mts/audio_test.go @@ -0,0 +1,109 @@ +/* +NAME + audio_test.go + +DESCRIPTION + See Readme.md + +AUTHOR + Trek Hopton + +LICENSE + audio_test.go is Copyright (C) 2017-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 mts + +import ( + "bytes" + "io/ioutil" + "log" + "testing" + + "bitbucket.org/ausocean/av/container/mts/meta" + "github.com/Comcast/gots/packet" + "github.com/Comcast/gots/pes" +) + +// TestEncodePcm tests the mpegts encoder's ability to encode pcm audio data. +// It reads and encodes input pcm data into mpegts, then decodes the mpegts compares the result to the input pcm. +func TestEncodePcm(t *testing.T) { + Meta = meta.New() + + var b []byte + buf := bytes.NewBuffer(b) + sampleRate := 48000 + sampleSize := 2 + writeSize := 16000 + writeFreq := float64(sampleRate*sampleSize) / float64(writeSize) + e := NewEncoder(buf, writeFreq, Audio) + + inPath := "../../../test/test-data/av/input/sweep_400Hz_20000Hz_-3dBFS_5s_48khz.pcm" + inPcm, err := ioutil.ReadFile(inPath) + if err != nil { + log.Fatal(err) + } + + // Encode pcm to mts and get the resulting bytes. + _, err = e.Write(inPcm) + if err != nil { + log.Fatal(err) + } + clip := buf.Bytes() + + // Decode the mts packets to extract the original data + var pkt packet.Packet + pesPacket := make([]byte, 0, writeSize) + got := make([]byte, 0, len(inPcm)) + i := 0 + for i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + if pkt.PID() == audioPid { + if pkt.PayloadUnitStartIndicator() { + payload, err := pkt.Payload() + if err != nil { + t.Fatalf("Unexpected err: %v\n", err) + } + pesPacket = append(pesPacket, payload...) + i += PacketSize + first := true + for (first || !pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { + first = false + copy(pkt[:], clip[i:i+PacketSize]) + payload, err := pkt.Payload() + if err != nil { + t.Fatalf("Unexpected err: %v\n", err) + } + + pesPacket = append(pesPacket, payload...) + i += PacketSize + } + } + pesHeader, err := pes.NewPESHeader(pesPacket) + if err != nil { + t.Fatalf("Unexpected err: %v\n", err) + } + got = append(got, pesHeader.Data()...) + } else { + i += PacketSize + } + } + + // Compare encoded data with original data. + if !bytes.Equal(got, inPcm) { + t.Error("Error, unexpected output") + } +} diff --git a/container/mts/encoder.go b/container/mts/encoder.go index 92e87051..5824ca15 100644 --- a/container/mts/encoder.go +++ b/container/mts/encoder.go @@ -101,11 +101,20 @@ var ( ) const ( - sdtPid = 17 - patPid = 0 - pmtPid = 4096 - videoPid = 256 - streamID = 0xe0 // First video stream ID. + sdtPid = 17 + patPid = 0 + pmtPid = 4096 + videoPid = 256 + audioPid = 210 + videoStreamID = 0xe0 // First video stream ID. + audioStreamID = 0xc0 // First audio stream ID. +) + +// Video and Audio constants are used to communicate which media type will be encoded when creating a +// new encoder with NewEncoder. +const ( + Video = iota + Audio ) // Time related constants. @@ -122,38 +131,54 @@ const ( type Encoder struct { dst io.Writer - clock time.Duration - lastTime time.Time - frameInterval time.Duration - ptsOffset time.Duration - tsSpace [PacketSize]byte - pesSpace [pes.MaxPesSize]byte + clock time.Duration + lastTime time.Time + writePeriod time.Duration + ptsOffset time.Duration + tsSpace [PacketSize]byte + pesSpace [pes.MaxPesSize]byte continuity map[int]byte timeBasedPsi bool pktCount int psiSendCount int + mediaPid int + streamID byte psiLastTime time.Time } // NewEncoder returns an Encoder with the specified frame rate. -func NewEncoder(dst io.Writer, fps float64) *Encoder { +func NewEncoder(dst io.Writer, writeFreq float64, mediaType int) *Encoder { + var mPid int + var sid byte + switch mediaType { + case Audio: + mPid = audioPid + sid = audioStreamID + case Video: + mPid = videoPid + sid = videoStreamID + } + return &Encoder{ dst: dst, - frameInterval: time.Duration(float64(time.Second) / fps), - ptsOffset: ptsOffset, + writePeriod: time.Duration(float64(time.Second) / writeFreq), + ptsOffset: ptsOffset, timeBasedPsi: true, pktCount: 8, + mediaPid: mPid, + streamID: sid, + continuity: map[int]byte{ - patPid: 0, - pmtPid: 0, - videoPid: 0, + patPid: 0, + pmtPid: 0, + mPid: 0, }, } } @@ -180,7 +205,7 @@ func (e *Encoder) TimeBasedPsi(b bool, sendCount int) { // Write implements io.Writer. Write takes raw h264 and encodes into mpegts, // then sending it to the encoder's io.Writer destination. -func (e *Encoder) Write(nalu []byte) (int, error) { +func (e *Encoder) Write(data []byte) (int, error) { now := time.Now() if (e.timeBasedPsi && (now.Sub(e.psiLastTime) > psiInterval)) || (!e.timeBasedPsi && (e.pktCount >= e.psiSendCount)) { e.pktCount = 0 @@ -193,21 +218,22 @@ func (e *Encoder) Write(nalu []byte) (int, error) { // Prepare PES data. pesPkt := pes.Packet{ - StreamID: streamID, + StreamID: e.streamID, PDI: hasPTS, PTS: e.pts(), - Data: nalu, + Data: data, HeaderLength: 5, } + buf := pesPkt.Bytes(e.pesSpace[:pes.MaxPesSize]) pusi := true for len(buf) != 0 { pkt := Packet{ PUSI: pusi, - PID: videoPid, + PID: uint16(e.mediaPid), RAI: pusi, - CC: e.ccFor(videoPid), + CC: e.ccFor(e.mediaPid), AFC: hasAdaptationField | hasPayload, PCRF: pusi, } @@ -222,14 +248,14 @@ func (e *Encoder) Write(nalu []byte) (int, error) { } _, err := e.dst.Write(pkt.Bytes(e.tsSpace[:PacketSize])) if err != nil { - return len(nalu), err + return len(data), err } e.pktCount++ } e.tick() - return len(nalu), nil + return len(data), nil } // writePSI creates mpegts with pat and pmt tables - with pmt table having updated @@ -271,7 +297,7 @@ func (e *Encoder) writePSI() error { // tick advances the clock one frame interval. func (e *Encoder) tick() { - e.clock += e.frameInterval + e.clock += e.writePeriod } // pts retuns the current presentation timestamp. diff --git a/container/mts/metaEncode_test.go b/container/mts/metaEncode_test.go index 00f806b9..2bf94d64 100644 --- a/container/mts/metaEncode_test.go +++ b/container/mts/metaEncode_test.go @@ -49,7 +49,7 @@ func TestMetaEncode1(t *testing.T) { Meta = meta.New() var b []byte buf := bytes.NewBuffer(b) - e := NewEncoder(buf, fps) + e := NewEncoder(buf, fps, Video) Meta.Add("ts", "12345678") if err := e.writePSI(); err != nil { t.Errorf(errUnexpectedErr, err.Error()) @@ -78,7 +78,7 @@ func TestMetaEncode2(t *testing.T) { Meta = meta.New() var b []byte buf := bytes.NewBuffer(b) - e := NewEncoder(buf, fps) + e := NewEncoder(buf, fps, Video) Meta.Add("ts", "12345678") Meta.Add("loc", "1234,4321,1234") if err := e.writePSI(); err != nil { diff --git a/container/mts/mpegts.go b/container/mts/mpegts.go index 18fe8b5f..ed8cbe02 100644 --- a/container/mts/mpegts.go +++ b/container/mts/mpegts.go @@ -74,7 +74,7 @@ const ( ) /* -The below data struct encapsulates the fields of an MPEG-TS packet. Below is +Packet encapsulates the fields of an MPEG-TS packet. Below is the formatting of an MPEG-TS packet for reference! MPEG-TS Packet Formatting @@ -196,7 +196,11 @@ func FindPid(d []byte, pid uint16) (pkt []byte, i int, err error) { func (p *Packet) FillPayload(data []byte) int { currentPktLen := 6 + asInt(p.PCRF)*6 + asInt(p.OPCRF)*6 + asInt(p.SPF)*1 + asInt(p.TPDF)*1 + len(p.TPD) - p.Payload = make([]byte, PayloadSize-currentPktLen) + if len(data) > PayloadSize-currentPktLen { + p.Payload = make([]byte, PayloadSize-currentPktLen) + } else { + p.Payload = make([]byte, len(data)) + } return copy(p.Payload, data) } @@ -214,7 +218,7 @@ func asByte(b bool) byte { return 0 } -// ToByteSlice interprets the fields of the ts packet instance and outputs a +// Bytes interprets the fields of the ts packet instance and outputs a // corresponding byte slice func (p *Packet) Bytes(buf []byte) []byte { if buf == nil || cap(buf) != PacketSize { diff --git a/container/mts/pes/pes.go b/container/mts/pes/pes.go index 1e085166..50544aa2 100644 --- a/container/mts/pes/pes.go +++ b/container/mts/pes/pes.go @@ -26,7 +26,7 @@ LICENSE package pes -const MaxPesSize = 10000 +const MaxPesSize = 65536 /* The below data struct encapsulates the fields of an PES packet. Below is From 634ecfdbb2a3627c9f457c6ea536c6f147ec57e7 Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 9 Apr 2019 13:33:58 +0930 Subject: [PATCH 05/15] mts: Limited size of encoder writes and updated audio_test Previously the encoder would not care if a write was given that exceeded the max PES packet size because we were never using PES packets bigger than a frame of video. Now I have changed it so that the encoder will check the write length and create a new PES packet if needed. I have also restructured my test so that it can extract the data from PES packets that span accross multiple MTS packets. --- container/mts/audio_test.go | 52 ++++++++++++++++++++++++++++--------- container/mts/encoder.go | 4 +++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/container/mts/audio_test.go b/container/mts/audio_test.go index d03bd14e..36df7df8 100644 --- a/container/mts/audio_test.go +++ b/container/mts/audio_test.go @@ -47,8 +47,8 @@ func TestEncodePcm(t *testing.T) { buf := bytes.NewBuffer(b) sampleRate := 48000 sampleSize := 2 - writeSize := 16000 - writeFreq := float64(sampleRate*sampleSize) / float64(writeSize) + blockSize := 16000 + writeFreq := float64(sampleRate*sampleSize) / float64(blockSize) e := NewEncoder(buf, writeFreq, Audio) inPath := "../../../test/test-data/av/input/sweep_400Hz_20000Hz_-3dBFS_5s_48khz.pcm" @@ -58,19 +58,33 @@ func TestEncodePcm(t *testing.T) { } // Encode pcm to mts and get the resulting bytes. - _, err = e.Write(inPcm) - if err != nil { - log.Fatal(err) + for i := 0; i < len(inPcm); i += blockSize { + if len(inPcm)-i < blockSize { + block := inPcm[i:] + _, err = e.Write(block) + if err != nil { + log.Fatal(err) + } + } else { + block := inPcm[i : i+blockSize] + _, err = e.Write(block) + if err != nil { + log.Fatal(err) + } + } } clip := buf.Bytes() // Decode the mts packets to extract the original data var pkt packet.Packet - pesPacket := make([]byte, 0, writeSize) + pesPacket := make([]byte, 0, blockSize) got := make([]byte, 0, len(inPcm)) i := 0 - for i+PacketSize <= len(clip) { + if i+PacketSize <= len(clip) { copy(pkt[:], clip[i:i+PacketSize]) + } + + for i+PacketSize <= len(clip) { if pkt.PID() == audioPid { if pkt.PayloadUnitStartIndicator() { payload, err := pkt.Payload() @@ -78,18 +92,28 @@ func TestEncodePcm(t *testing.T) { t.Fatalf("Unexpected err: %v\n", err) } pesPacket = append(pesPacket, payload...) + i += PacketSize - first := true - for (first || !pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { - first = false + if i+PacketSize <= len(clip) { copy(pkt[:], clip[i:i+PacketSize]) - payload, err := pkt.Payload() + } + + for (!pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { + payload, err = pkt.Payload() if err != nil { t.Fatalf("Unexpected err: %v\n", err) } - pesPacket = append(pesPacket, payload...) + i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + } + } + } else { + i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) } } pesHeader, err := pes.NewPESHeader(pesPacket) @@ -97,8 +121,12 @@ func TestEncodePcm(t *testing.T) { t.Fatalf("Unexpected err: %v\n", err) } got = append(got, pesHeader.Data()...) + pesPacket = pesPacket[:0] } else { i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + } } } diff --git a/container/mts/encoder.go b/container/mts/encoder.go index 5824ca15..34cfc8de 100644 --- a/container/mts/encoder.go +++ b/container/mts/encoder.go @@ -29,6 +29,7 @@ LICENSE package mts import ( + "fmt" "io" "time" @@ -206,6 +207,9 @@ func (e *Encoder) TimeBasedPsi(b bool, sendCount int) { // Write implements io.Writer. Write takes raw h264 and encodes into mpegts, // then sending it to the encoder's io.Writer destination. func (e *Encoder) Write(data []byte) (int, error) { + if len(data) > pes.MaxPesSize { + return 0, fmt.Errorf("data size too large (Max is %v): %v", pes.MaxPesSize, len(data)) + } now := time.Now() if (e.timeBasedPsi && (now.Sub(e.psiLastTime) > psiInterval)) || (!e.timeBasedPsi && (e.pktCount >= e.psiSendCount)) { e.pktCount = 0 From 9fe3de5d6548df23053ebb5109ab0a8f66c753ae Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 9 Apr 2019 13:53:14 +0930 Subject: [PATCH 06/15] mts: Changed uses of NewEncoder in revid and senders_test to use extra argument. --- revid/revid.go | 2 +- revid/senders_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/revid/revid.go b/revid/revid.go index 0828d7da..8636dba8 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -259,7 +259,7 @@ func (r *Revid) setupPipeline(mtsEnc, flvEnc func(dst io.Writer, rate int) (io.W } func newMtsEncoder(dst io.Writer, fps int) (io.Writer, error) { - e := mts.NewEncoder(dst, float64(fps)) + e := mts.NewEncoder(dst, float64(fps), mts.Video) return e, nil } diff --git a/revid/senders_test.go b/revid/senders_test.go index 95474922..aba06455 100644 --- a/revid/senders_test.go +++ b/revid/senders_test.go @@ -111,7 +111,7 @@ func TestMtsSenderSegment(t *testing.T) { tstDst := &destination{} loadSender := newMtsSender(tstDst, log) rb := ring.NewBuffer(rbSize, rbElementSize, wTimeout) - encoder := mts.NewEncoder((*buffer)(rb), 25) + encoder := mts.NewEncoder((*buffer)(rb), 25, mts.Video) // Turn time based PSI writing off for encoder. const psiSendCount = 10 @@ -195,7 +195,7 @@ func TestMtsSenderDiscontinuity(t *testing.T) { tstDst := &destination{testDiscontinuities: true, discontinuityAt: clipWithDiscontinuity} loadSender := newMtsSender(tstDst, log) rb := ring.NewBuffer(rbSize, rbElementSize, wTimeout) - encoder := mts.NewEncoder((*buffer)(rb), 25) + encoder := mts.NewEncoder((*buffer)(rb), 25, mts.Video) // Turn time based PSI writing off for encoder. const psiSendCount = 10 From 3c29ca554dd4ab6aa5fc2a1197711eb32c14dcb6 Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 9 Apr 2019 14:59:10 +0930 Subject: [PATCH 07/15] mts: removed readme reference, added comments to test --- container/mts/audio_test.go | 37 ++++++++++++++++++++----------------- container/mts/encoder.go | 3 --- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/container/mts/audio_test.go b/container/mts/audio_test.go index 36df7df8..f4aaf902 100644 --- a/container/mts/audio_test.go +++ b/container/mts/audio_test.go @@ -2,9 +2,6 @@ NAME audio_test.go -DESCRIPTION - See Readme.md - AUTHOR Trek Hopton @@ -21,8 +18,8 @@ LICENSE 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. + You should have received a copy of the GNU General Public License in gpl.txt. + If not, see http://www.gnu.org/licenses. */ package mts @@ -30,7 +27,6 @@ package mts import ( "bytes" "io/ioutil" - "log" "testing" "bitbucket.org/ausocean/av/container/mts/meta" @@ -39,7 +35,7 @@ import ( ) // TestEncodePcm tests the mpegts encoder's ability to encode pcm audio data. -// It reads and encodes input pcm data into mpegts, then decodes the mpegts compares the result to the input pcm. +// It reads and encodes input pcm data into mpegts, then decodes the mpegts and compares the result to the input pcm. func TestEncodePcm(t *testing.T) { Meta = meta.New() @@ -54,28 +50,28 @@ func TestEncodePcm(t *testing.T) { inPath := "../../../test/test-data/av/input/sweep_400Hz_20000Hz_-3dBFS_5s_48khz.pcm" inPcm, err := ioutil.ReadFile(inPath) if err != nil { - log.Fatal(err) + t.Errorf("unable to read file: %v", err) } - // Encode pcm to mts and get the resulting bytes. + // Break pcm into blocks and encode to mts and get the resulting bytes. for i := 0; i < len(inPcm); i += blockSize { if len(inPcm)-i < blockSize { block := inPcm[i:] _, err = e.Write(block) if err != nil { - log.Fatal(err) + t.Errorf("unable to write block: %v", err) } } else { block := inPcm[i : i+blockSize] _, err = e.Write(block) if err != nil { - log.Fatal(err) + t.Errorf("unable to write block: %v", err) } } } clip := buf.Bytes() - // Decode the mts packets to extract the original data + // Get the first MTS packet to check var pkt packet.Packet pesPacket := make([]byte, 0, blockSize) got := make([]byte, 0, len(inPcm)) @@ -84,12 +80,17 @@ func TestEncodePcm(t *testing.T) { copy(pkt[:], clip[i:i+PacketSize]) } + // Loop through MTS packets until all the audio data from PES packets has been retrieved for i+PacketSize <= len(clip) { + + // Check MTS packet if pkt.PID() == audioPid { if pkt.PayloadUnitStartIndicator() { + + // Copy the first MTS payload payload, err := pkt.Payload() if err != nil { - t.Fatalf("Unexpected err: %v\n", err) + t.Errorf("unable to get MTS payload: %v", err) } pesPacket = append(pesPacket, payload...) @@ -98,10 +99,11 @@ func TestEncodePcm(t *testing.T) { copy(pkt[:], clip[i:i+PacketSize]) } + // Copy the rest of the MTS payloads that are part of the same PES packet for (!pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { payload, err = pkt.Payload() if err != nil { - t.Fatalf("Unexpected err: %v\n", err) + t.Errorf("unable to get MTS payload: %v", err) } pesPacket = append(pesPacket, payload...) @@ -116,9 +118,10 @@ func TestEncodePcm(t *testing.T) { copy(pkt[:], clip[i:i+PacketSize]) } } + // Get the audio data from the current PES packet pesHeader, err := pes.NewPESHeader(pesPacket) if err != nil { - t.Fatalf("Unexpected err: %v\n", err) + t.Errorf("unable to read PES packet: %v", err) } got = append(got, pesHeader.Data()...) pesPacket = pesPacket[:0] @@ -130,8 +133,8 @@ func TestEncodePcm(t *testing.T) { } } - // Compare encoded data with original data. + // Compare data from MTS with original data. if !bytes.Equal(got, inPcm) { - t.Error("Error, unexpected output") + t.Error("data decoded from mts did not match input data") } } diff --git a/container/mts/encoder.go b/container/mts/encoder.go index 34cfc8de..6ccde63e 100644 --- a/container/mts/encoder.go +++ b/container/mts/encoder.go @@ -2,9 +2,6 @@ NAME encoder.go -DESCRIPTION - See Readme.md - AUTHOR Dan Kortschak Saxon Nelson-Milton From f597a04289ffca4778d298fa67a01b29b732ea69 Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 9 Apr 2019 15:42:44 +0930 Subject: [PATCH 08/15] pcm: changed location to codec --- codec/pcm/pcm.go | 145 ++++++++++++++++++++++++++++++++++++++++++ codec/pcm/pcm_test.go | 118 ++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 codec/pcm/pcm.go create mode 100644 codec/pcm/pcm_test.go diff --git a/codec/pcm/pcm.go b/codec/pcm/pcm.go new file mode 100644 index 00000000..5ead3143 --- /dev/null +++ b/codec/pcm/pcm.go @@ -0,0 +1,145 @@ +/* +NAME + pcm.go + +DESCRIPTION + pcm.go contains functions for processing pcm. + +AUTHOR + Trek Hopton + +LICENSE + pcm.go is 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 in gpl.txt. + If not, see [GNU licenses](http://www.gnu.org/licenses). +*/ +package pcm + +import ( + "encoding/binary" + "fmt" + + "github.com/yobert/alsa" +) + +// Resample takes an alsa.Buffer (b) and resamples the pcm audio data to 'rate' Hz and returns the resulting pcm. +// If an error occurs, an error will be returned along with the original b's data. +// Notes: +// - Currently only downsampling is implemented and b's rate must be divisible by 'rate' or an error will occur. +// - If the number of bytes in b.Data is not divisible by the decimation factor (ratioFrom), the remaining bytes will +// not be included in the result. Eg. input of length 480002 downsampling 6:1 will result in output length 80000. +func Resample(b alsa.Buffer, rate int) ([]byte, error) { + fromRate := b.Format.Rate + if fromRate == rate { + return b.Data, nil + } else if fromRate < 0 { + return nil, fmt.Errorf("Unable to convert from: %v Hz", fromRate) + } else if rate < 0 { + return nil, fmt.Errorf("Unable to convert to: %v Hz", rate) + } + + // The number of bytes in a sample. + var sampleLen int + switch b.Format.SampleFormat { + case alsa.S32_LE: + sampleLen = 4 * b.Format.Channels + case alsa.S16_LE: + sampleLen = 2 * b.Format.Channels + default: + return nil, fmt.Errorf("Unhandled ALSA format: %v", b.Format.SampleFormat) + } + inPcmLen := len(b.Data) + + // Calculate sample rate ratio ratioFrom:ratioTo. + rateGcd := gcd(rate, fromRate) + ratioFrom := fromRate / rateGcd + ratioTo := rate / rateGcd + + // ratioTo = 1 is the only number that will result in an even sampling. + if ratioTo != 1 { + return nil, fmt.Errorf("unhandled from:to rate ratio %v:%v: 'to' must be 1", ratioFrom, ratioTo) + } + + newLen := inPcmLen / ratioFrom + result := make([]byte, 0, newLen) + + // For each new sample to be generated, loop through the respective 'ratioFrom' samples in 'b.Data' to add them + // up and average them. The result is the new sample. + bAvg := make([]byte, sampleLen) + for i := 0; i < newLen/sampleLen; i++ { + var sum int + for j := 0; j < ratioFrom; j++ { + switch b.Format.SampleFormat { + case alsa.S32_LE: + sum += int(int32(binary.LittleEndian.Uint32(b.Data[(i*ratioFrom*sampleLen)+(j*sampleLen) : (i*ratioFrom*sampleLen)+((j+1)*sampleLen)]))) + case alsa.S16_LE: + sum += int(int16(binary.LittleEndian.Uint16(b.Data[(i*ratioFrom*sampleLen)+(j*sampleLen) : (i*ratioFrom*sampleLen)+((j+1)*sampleLen)]))) + } + } + avg := sum / ratioFrom + switch b.Format.SampleFormat { + case alsa.S32_LE: + binary.LittleEndian.PutUint32(bAvg, uint32(avg)) + case alsa.S16_LE: + binary.LittleEndian.PutUint16(bAvg, uint16(avg)) + } + result = append(result, bAvg...) + } + return result, nil +} + +// StereoToMono returns raw mono audio data generated from only the left channel from +// the given stereo recording (ALSA buffer) +// if an error occurs, an error will be returned along with the original stereo data. +func StereoToMono(b alsa.Buffer) ([]byte, error) { + if b.Format.Channels == 1 { + return b.Data, nil + } else if b.Format.Channels != 2 { + return nil, fmt.Errorf("Audio is not stereo or mono, it has %v channels", b.Format.Channels) + } + + var stereoSampleBytes int + switch b.Format.SampleFormat { + case alsa.S32_LE: + stereoSampleBytes = 8 + case alsa.S16_LE: + stereoSampleBytes = 4 + default: + return nil, fmt.Errorf("Unhandled ALSA format %v", b.Format.SampleFormat) + } + + recLength := len(b.Data) + mono := make([]byte, recLength/2) + + // Convert to mono: for each byte in the stereo recording, if it's in the first half of a stereo sample + // (left channel), add it to the new mono audio data. + var inc int + for i := 0; i < recLength; i++ { + if i%stereoSampleBytes < stereoSampleBytes/2 { + mono[inc] = b.Data[i] + inc++ + } + } + + return mono, nil +} + +// gcd is used for calculating the greatest common divisor of two positive integers, a and b. +// assumes given a and b are positive. +func gcd(a, b int) int { + for b != 0 { + a, b = b, a%b + } + return a +} diff --git a/codec/pcm/pcm_test.go b/codec/pcm/pcm_test.go new file mode 100644 index 00000000..713d01d8 --- /dev/null +++ b/codec/pcm/pcm_test.go @@ -0,0 +1,118 @@ +/* +NAME + pcm_test.go + +DESCRIPTION + pcm_test.go contains functions for testing the pcm package. + +AUTHOR + Trek Hopton + +LICENSE + pcm_test.go is 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 in gpl.txt. + If not, see [GNU licenses](http://www.gnu.org/licenses). +*/ +package pcm + +import ( + "bytes" + "io/ioutil" + "log" + "testing" + + "github.com/yobert/alsa" +) + +// TestResample tests the Resample function using a pcm file that contains audio of a freq. sweep. +// The output of the Resample function is compared with a file containing the expected result. +func TestResample(t *testing.T) { + inPath := "../../../test/test-data/av/input/sweep_400Hz_20000Hz_-3dBFS_5s_48khz.pcm" + expPath := "../../../test/test-data/av/output/sweep_400Hz_20000Hz_resampled_48to8kHz.pcm" + + // Read input pcm. + inPcm, err := ioutil.ReadFile(inPath) + if err != nil { + log.Fatal(err) + } + + format := alsa.BufferFormat{ + Channels: 1, + Rate: 48000, + SampleFormat: alsa.S16_LE, + } + + buf := alsa.Buffer{ + Format: format, + Data: inPcm, + } + + // Resample pcm. + resampled, err := Resample(buf, 8000) + if err != nil { + log.Fatal(err) + } + + // Read expected resampled pcm. + exp, err := ioutil.ReadFile(expPath) + if err != nil { + log.Fatal(err) + } + + // Compare result with expected. + if !bytes.Equal(resampled, exp) { + t.Error("Resampled data does not match expected result.") + } +} + +// TestStereoToMono tests the StereoToMono function using a pcm file that contains stereo audio. +// The output of the StereoToMono function is compared with a file containing the expected mono audio. +func TestStereoToMono(t *testing.T) { + inPath := "../../../test/test-data/av/input/stereo_DTMF_tones.pcm" + expPath := "../../../test/test-data/av/output/mono_DTMF_tones.pcm" + + // Read input pcm. + inPcm, err := ioutil.ReadFile(inPath) + if err != nil { + log.Fatal(err) + } + + format := alsa.BufferFormat{ + Channels: 2, + Rate: 44100, + SampleFormat: alsa.S16_LE, + } + + buf := alsa.Buffer{ + Format: format, + Data: inPcm, + } + + // Convert audio. + mono, err := StereoToMono(buf) + if err != nil { + log.Fatal(err) + } + + // Read expected mono pcm. + exp, err := ioutil.ReadFile(expPath) + if err != nil { + log.Fatal(err) + } + + // Compare result with expected. + if !bytes.Equal(mono, exp) { + t.Error("Converted data does not match expected result.") + } +} From cba85c169b6f5d8cc2b4b807276ffca5ae98e1c7 Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 9 Apr 2019 15:45:28 +0930 Subject: [PATCH 09/15] pcm: deleted files in old location --- audio/pcm/pcm.go | 145 ------------------------------------------ audio/pcm/pcm_test.go | 118 ---------------------------------- 2 files changed, 263 deletions(-) delete mode 100644 audio/pcm/pcm.go delete mode 100644 audio/pcm/pcm_test.go diff --git a/audio/pcm/pcm.go b/audio/pcm/pcm.go deleted file mode 100644 index 5ead3143..00000000 --- a/audio/pcm/pcm.go +++ /dev/null @@ -1,145 +0,0 @@ -/* -NAME - pcm.go - -DESCRIPTION - pcm.go contains functions for processing pcm. - -AUTHOR - Trek Hopton - -LICENSE - pcm.go is 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 in gpl.txt. - If not, see [GNU licenses](http://www.gnu.org/licenses). -*/ -package pcm - -import ( - "encoding/binary" - "fmt" - - "github.com/yobert/alsa" -) - -// Resample takes an alsa.Buffer (b) and resamples the pcm audio data to 'rate' Hz and returns the resulting pcm. -// If an error occurs, an error will be returned along with the original b's data. -// Notes: -// - Currently only downsampling is implemented and b's rate must be divisible by 'rate' or an error will occur. -// - If the number of bytes in b.Data is not divisible by the decimation factor (ratioFrom), the remaining bytes will -// not be included in the result. Eg. input of length 480002 downsampling 6:1 will result in output length 80000. -func Resample(b alsa.Buffer, rate int) ([]byte, error) { - fromRate := b.Format.Rate - if fromRate == rate { - return b.Data, nil - } else if fromRate < 0 { - return nil, fmt.Errorf("Unable to convert from: %v Hz", fromRate) - } else if rate < 0 { - return nil, fmt.Errorf("Unable to convert to: %v Hz", rate) - } - - // The number of bytes in a sample. - var sampleLen int - switch b.Format.SampleFormat { - case alsa.S32_LE: - sampleLen = 4 * b.Format.Channels - case alsa.S16_LE: - sampleLen = 2 * b.Format.Channels - default: - return nil, fmt.Errorf("Unhandled ALSA format: %v", b.Format.SampleFormat) - } - inPcmLen := len(b.Data) - - // Calculate sample rate ratio ratioFrom:ratioTo. - rateGcd := gcd(rate, fromRate) - ratioFrom := fromRate / rateGcd - ratioTo := rate / rateGcd - - // ratioTo = 1 is the only number that will result in an even sampling. - if ratioTo != 1 { - return nil, fmt.Errorf("unhandled from:to rate ratio %v:%v: 'to' must be 1", ratioFrom, ratioTo) - } - - newLen := inPcmLen / ratioFrom - result := make([]byte, 0, newLen) - - // For each new sample to be generated, loop through the respective 'ratioFrom' samples in 'b.Data' to add them - // up and average them. The result is the new sample. - bAvg := make([]byte, sampleLen) - for i := 0; i < newLen/sampleLen; i++ { - var sum int - for j := 0; j < ratioFrom; j++ { - switch b.Format.SampleFormat { - case alsa.S32_LE: - sum += int(int32(binary.LittleEndian.Uint32(b.Data[(i*ratioFrom*sampleLen)+(j*sampleLen) : (i*ratioFrom*sampleLen)+((j+1)*sampleLen)]))) - case alsa.S16_LE: - sum += int(int16(binary.LittleEndian.Uint16(b.Data[(i*ratioFrom*sampleLen)+(j*sampleLen) : (i*ratioFrom*sampleLen)+((j+1)*sampleLen)]))) - } - } - avg := sum / ratioFrom - switch b.Format.SampleFormat { - case alsa.S32_LE: - binary.LittleEndian.PutUint32(bAvg, uint32(avg)) - case alsa.S16_LE: - binary.LittleEndian.PutUint16(bAvg, uint16(avg)) - } - result = append(result, bAvg...) - } - return result, nil -} - -// StereoToMono returns raw mono audio data generated from only the left channel from -// the given stereo recording (ALSA buffer) -// if an error occurs, an error will be returned along with the original stereo data. -func StereoToMono(b alsa.Buffer) ([]byte, error) { - if b.Format.Channels == 1 { - return b.Data, nil - } else if b.Format.Channels != 2 { - return nil, fmt.Errorf("Audio is not stereo or mono, it has %v channels", b.Format.Channels) - } - - var stereoSampleBytes int - switch b.Format.SampleFormat { - case alsa.S32_LE: - stereoSampleBytes = 8 - case alsa.S16_LE: - stereoSampleBytes = 4 - default: - return nil, fmt.Errorf("Unhandled ALSA format %v", b.Format.SampleFormat) - } - - recLength := len(b.Data) - mono := make([]byte, recLength/2) - - // Convert to mono: for each byte in the stereo recording, if it's in the first half of a stereo sample - // (left channel), add it to the new mono audio data. - var inc int - for i := 0; i < recLength; i++ { - if i%stereoSampleBytes < stereoSampleBytes/2 { - mono[inc] = b.Data[i] - inc++ - } - } - - return mono, nil -} - -// gcd is used for calculating the greatest common divisor of two positive integers, a and b. -// assumes given a and b are positive. -func gcd(a, b int) int { - for b != 0 { - a, b = b, a%b - } - return a -} diff --git a/audio/pcm/pcm_test.go b/audio/pcm/pcm_test.go deleted file mode 100644 index 713d01d8..00000000 --- a/audio/pcm/pcm_test.go +++ /dev/null @@ -1,118 +0,0 @@ -/* -NAME - pcm_test.go - -DESCRIPTION - pcm_test.go contains functions for testing the pcm package. - -AUTHOR - Trek Hopton - -LICENSE - pcm_test.go is 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 in gpl.txt. - If not, see [GNU licenses](http://www.gnu.org/licenses). -*/ -package pcm - -import ( - "bytes" - "io/ioutil" - "log" - "testing" - - "github.com/yobert/alsa" -) - -// TestResample tests the Resample function using a pcm file that contains audio of a freq. sweep. -// The output of the Resample function is compared with a file containing the expected result. -func TestResample(t *testing.T) { - inPath := "../../../test/test-data/av/input/sweep_400Hz_20000Hz_-3dBFS_5s_48khz.pcm" - expPath := "../../../test/test-data/av/output/sweep_400Hz_20000Hz_resampled_48to8kHz.pcm" - - // Read input pcm. - inPcm, err := ioutil.ReadFile(inPath) - if err != nil { - log.Fatal(err) - } - - format := alsa.BufferFormat{ - Channels: 1, - Rate: 48000, - SampleFormat: alsa.S16_LE, - } - - buf := alsa.Buffer{ - Format: format, - Data: inPcm, - } - - // Resample pcm. - resampled, err := Resample(buf, 8000) - if err != nil { - log.Fatal(err) - } - - // Read expected resampled pcm. - exp, err := ioutil.ReadFile(expPath) - if err != nil { - log.Fatal(err) - } - - // Compare result with expected. - if !bytes.Equal(resampled, exp) { - t.Error("Resampled data does not match expected result.") - } -} - -// TestStereoToMono tests the StereoToMono function using a pcm file that contains stereo audio. -// The output of the StereoToMono function is compared with a file containing the expected mono audio. -func TestStereoToMono(t *testing.T) { - inPath := "../../../test/test-data/av/input/stereo_DTMF_tones.pcm" - expPath := "../../../test/test-data/av/output/mono_DTMF_tones.pcm" - - // Read input pcm. - inPcm, err := ioutil.ReadFile(inPath) - if err != nil { - log.Fatal(err) - } - - format := alsa.BufferFormat{ - Channels: 2, - Rate: 44100, - SampleFormat: alsa.S16_LE, - } - - buf := alsa.Buffer{ - Format: format, - Data: inPcm, - } - - // Convert audio. - mono, err := StereoToMono(buf) - if err != nil { - log.Fatal(err) - } - - // Read expected mono pcm. - exp, err := ioutil.ReadFile(expPath) - if err != nil { - log.Fatal(err) - } - - // Compare result with expected. - if !bytes.Equal(mono, exp) { - t.Error("Converted data does not match expected result.") - } -} From 8e9cbd5a7917b42e58d28c157f555b2d333f2dc9 Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 9 Apr 2019 15:48:54 +0930 Subject: [PATCH 10/15] pcm: updated import statements using pcm --- exp/pcm/resample/resample.go | 2 +- exp/pcm/stereo-to-mono/stereo-to-mono.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exp/pcm/resample/resample.go b/exp/pcm/resample/resample.go index aaa8f77c..eab7a342 100644 --- a/exp/pcm/resample/resample.go +++ b/exp/pcm/resample/resample.go @@ -32,7 +32,7 @@ import ( "io/ioutil" "log" - "bitbucket.org/ausocean/av/audio/pcm" + "bitbucket.org/ausocean/av/codec/pcm" "github.com/yobert/alsa" ) diff --git a/exp/pcm/stereo-to-mono/stereo-to-mono.go b/exp/pcm/stereo-to-mono/stereo-to-mono.go index 231591f0..ccbf87bf 100644 --- a/exp/pcm/stereo-to-mono/stereo-to-mono.go +++ b/exp/pcm/stereo-to-mono/stereo-to-mono.go @@ -32,7 +32,7 @@ import ( "io/ioutil" "log" - "bitbucket.org/ausocean/av/audio/pcm" + "bitbucket.org/ausocean/av/codec/pcm" "github.com/yobert/alsa" ) From af32f13932b7b1edccb97e117aa000cbe195eeaa Mon Sep 17 00:00:00 2001 From: scruzin Date: Wed, 10 Apr 2019 16:04:18 +0930 Subject: [PATCH 11/15] Removed obsolete directory. --- startup/rc.local | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100755 startup/rc.local diff --git a/startup/rc.local b/startup/rc.local deleted file mode 100755 index 7d39ed7c..00000000 --- a/startup/rc.local +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -e -# -# rc.local -# -# This script is executed at the end of each multiuser runlevel. -# Make sure that the script will "exit 0" on success or any other -# value on error. -# -# In order to enable or disable this script just change the execution -# bits. -# -# By default this script does nothing. - -# Print the IP address -_IP=$(hostname -I) || true -if [ "$_IP" ]; then - printf "My IP address is %s\n" "$_IP" -fi - -exit 0 From d4c6a8f2a3e03f221692c734d817bae0ac5be07b Mon Sep 17 00:00:00 2001 From: scruzin Date: Wed, 10 Apr 2019 16:06:36 +0930 Subject: [PATCH 12/15] Removed as grossly out of date. --- revid/Readme.md | 112 ------------------------------------------------ 1 file changed, 112 deletions(-) delete mode 100644 revid/Readme.md diff --git a/revid/Readme.md b/revid/Readme.md deleted file mode 100644 index a3c3d4e8..00000000 --- a/revid/Readme.md +++ /dev/null @@ -1,112 +0,0 @@ -# Readme - -revid is a testbed for re-muxing and re-directing video streams as -MPEG-TS over various protocols. - -# Description - -The mode (-m) determine the mode of operation: - -* h = send HTTP (as a POST) -* u = send UDP -* r = send RTP -* f = write to /tmp files -* d = inspect packets and dump to screen - -Flags (-f) determine video filtering and other actions. - -For example, to send as raw UDP to on the current host, passing the video and audio as is: - -revid -i -m u -o udp://0.0.0.0: - -Or, to post as HTTP to , fixing PTS and dropping the audio along the way: - -revid -i -m h -f 3 -o - -Note that revid appends the size of the video to the URL to supply a query param. -Append a ? to your if you don't need it - -List of flags: - -* filterFixPTS = 0x0001 -* filterDropAudio = 0x0002 -* filterScale640 = 0x0004 -* filterScale320 = 0x0008 -* filterFixContinuity = 0x0010 -* dumpProgramInfo = 0x0100 -* dumpPacketStats = 0x0200 -* dumpPacketHeader = 0x0400 -* dumpPacketPayload = 0x0800 - -Common flag combos: - -* 3: fix pts and drop audio -* 7: fix pts, drop audo and scale 640 -* 17: fix pts and fix continuity -* 256: dump program info -* 512: dump packet stats -* 513: fix pts, plus above -* 529: fix pts and fix continuity, plus above - -# Errors - -If you see "Error reading from ffmpeg: EOF" that means ffmpeg has -crashed for some reason, usually because of a bad parameter. Copy and -paste the ffmpeg command line into a terminal to see what is -happening. - -RTSP feeds from certain cameras (such as TechView ones) do not -generate presentation timestamps (PTS), resulting in errors such as -the following: - -* [mpegts @ 0xX] Timestamps are unset in a packet for stream 0... -* [mpegts @ 0xX] first pts value must be set - -This can be fixed with an ffmpeg video filter (specified by flag 0x0001). -Another issue is that MPEG-TS continuity counters may not be continuous. -You can fix this with the fix continuity flag (0x0010). - -FFmpeg will also complain if doesn't have the necessary audio codec -installed. If so, you can drop the audio (flag 0x0002). - -# MPEG-TS Notes - -MPEG2-TS stream clocks (PCR, PTS, and DTS) all have units of 1/90000 -second and header fields are read as big endian (like most protocols). - -* TEI = Transport Error Indicator -* PUSI = Payload Unit Start Indicator -* TP = Transport Priority -* TCS = Transport Scrambling Control -* AFC = Adapation Field Control -* CC = Continuity Counter (incremented per PID wen payload present) -* AFL = Adapation Field Length -* PCR = Program Clock Reference - -# Dependencies - -revid uses ffmpeg for video remuxing. -See [Ffmepg filters](https://ffmpeg.org/ffmpeg-filters.html). - -revid also uses [Comcast's gots package](https://github.com/Comcast/gots). - -# Author - -Alan Noble - -# License - -Revid 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 -or more details. - -You should have received a copy of the GNU General Public License -along with revid in gpl.txt. If not, see [GNU licenses](http://www.gnu.org/licenses/). From 863db58a843bd47b7c4745334032785e9f4e6d89 Mon Sep 17 00:00:00 2001 From: scruzin Date: Wed, 10 Apr 2019 16:32:07 +0930 Subject: [PATCH 13/15] Removed references to obsolete Session. --- protocol/rtmp/packet.go | 2 +- protocol/rtmp/rtmp_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/protocol/rtmp/packet.go b/protocol/rtmp/packet.go index 5f46851d..dda53d35 100644 --- a/protocol/rtmp/packet.go +++ b/protocol/rtmp/packet.go @@ -259,7 +259,7 @@ func (pkt *packet) resize(size uint32, ht uint8) { } // writeTo writes a packet to the RTMP connection. -// Packets are written in chunks which are Session.chunkSize in length (128 bytes in length). +// Packets are written in chunks which are c.chunkSize in length (128 bytes by default). // We defer sending small audio packets and combine consecutive small audio packets where possible to reduce I/O. // When queue is true, we expect a response to this request and cache the method on c.methodCalls. func (pkt *packet) writeTo(c *Conn, queue bool) error { diff --git a/protocol/rtmp/rtmp_test.go b/protocol/rtmp/rtmp_test.go index 8ccb2e12..f2d661c5 100644 --- a/protocol/rtmp/rtmp_test.go +++ b/protocol/rtmp/rtmp_test.go @@ -206,7 +206,7 @@ func TestFromFrame(t *testing.T) { err = c.Close() if err != nil { - t.Errorf("Session.Close failed with error: %v", err) + t.Errorf("Conn.Close failed with error: %v", err) } } @@ -256,6 +256,6 @@ func TestFromFile(t *testing.T) { err = c.Close() if err != nil { - t.Errorf("Session.Close failed with error: %v", err) + t.Errorf("Conn.Close failed with error: %v", err) } } From 7c990b3bb521312c7497ff21cb4d4ffe268573ea Mon Sep 17 00:00:00 2001 From: Trek H Date: Wed, 10 Apr 2019 17:18:42 +0930 Subject: [PATCH 14/15] mts: reordered, neatened and clarified code. --- container/mts/audio_test.go | 77 ++++++++++++++++++------------------- container/mts/encoder.go | 7 ++-- container/mts/pes/pes.go | 2 +- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/container/mts/audio_test.go b/container/mts/audio_test.go index f4aaf902..23ba16e6 100644 --- a/container/mts/audio_test.go +++ b/container/mts/audio_test.go @@ -29,9 +29,10 @@ import ( "io/ioutil" "testing" - "bitbucket.org/ausocean/av/container/mts/meta" "github.com/Comcast/gots/packet" "github.com/Comcast/gots/pes" + + "bitbucket.org/ausocean/av/container/mts/meta" ) // TestEncodePcm tests the mpegts encoder's ability to encode pcm audio data. @@ -39,13 +40,12 @@ import ( func TestEncodePcm(t *testing.T) { Meta = meta.New() - var b []byte - buf := bytes.NewBuffer(b) + var buf bytes.Buffer sampleRate := 48000 sampleSize := 2 blockSize := 16000 writeFreq := float64(sampleRate*sampleSize) / float64(blockSize) - e := NewEncoder(buf, writeFreq, Audio) + e := NewEncoder(&buf, writeFreq, Audio) inPath := "../../../test/test-data/av/input/sweep_400Hz_20000Hz_-3dBFS_5s_48khz.pcm" inPcm, err := ioutil.ReadFile(inPath) @@ -84,11 +84,34 @@ func TestEncodePcm(t *testing.T) { for i+PacketSize <= len(clip) { // Check MTS packet - if pkt.PID() == audioPid { - if pkt.PayloadUnitStartIndicator() { + if !(pkt.PID() == audioPid) { + i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + } + continue + } + if !pkt.PayloadUnitStartIndicator() { + i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + } + } else { + // Copy the first MTS payload + payload, err := pkt.Payload() + if err != nil { + t.Errorf("unable to get MTS payload: %v", err) + } + pesPacket = append(pesPacket, payload...) - // Copy the first MTS payload - payload, err := pkt.Payload() + i += PacketSize + if i+PacketSize <= len(clip) { + copy(pkt[:], clip[i:i+PacketSize]) + } + + // Copy the rest of the MTS payloads that are part of the same PES packet + for (!pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { + payload, err = pkt.Payload() if err != nil { t.Errorf("unable to get MTS payload: %v", err) } @@ -98,39 +121,15 @@ func TestEncodePcm(t *testing.T) { if i+PacketSize <= len(clip) { copy(pkt[:], clip[i:i+PacketSize]) } - - // Copy the rest of the MTS payloads that are part of the same PES packet - for (!pkt.PayloadUnitStartIndicator()) && i+PacketSize <= len(clip) { - payload, err = pkt.Payload() - if err != nil { - t.Errorf("unable to get MTS payload: %v", err) - } - pesPacket = append(pesPacket, payload...) - - i += PacketSize - if i+PacketSize <= len(clip) { - copy(pkt[:], clip[i:i+PacketSize]) - } - } - } else { - i += PacketSize - if i+PacketSize <= len(clip) { - copy(pkt[:], clip[i:i+PacketSize]) - } - } - // Get the audio data from the current PES packet - pesHeader, err := pes.NewPESHeader(pesPacket) - if err != nil { - t.Errorf("unable to read PES packet: %v", err) - } - got = append(got, pesHeader.Data()...) - pesPacket = pesPacket[:0] - } else { - i += PacketSize - if i+PacketSize <= len(clip) { - copy(pkt[:], clip[i:i+PacketSize]) } } + // Get the audio data from the current PES packet + pesHeader, err := pes.NewPESHeader(pesPacket) + if err != nil { + t.Errorf("unable to read PES packet: %v", err) + } + got = append(got, pesHeader.Data()...) + pesPacket = pesPacket[:0] } // Compare data from MTS with original data. diff --git a/container/mts/encoder.go b/container/mts/encoder.go index 6ccde63e..e72cfebf 100644 --- a/container/mts/encoder.go +++ b/container/mts/encoder.go @@ -147,8 +147,9 @@ type Encoder struct { psiLastTime time.Time } -// NewEncoder returns an Encoder with the specified frame rate. -func NewEncoder(dst io.Writer, writeFreq float64, mediaType int) *Encoder { +// NewEncoder returns an Encoder with the specified media type and rate eg. if a video stream +// calls write for every frame, the rate will be the frame rate of the video. +func NewEncoder(dst io.Writer, rate float64, mediaType int) *Encoder { var mPid int var sid byte switch mediaType { @@ -163,7 +164,7 @@ func NewEncoder(dst io.Writer, writeFreq float64, mediaType int) *Encoder { return &Encoder{ dst: dst, - writePeriod: time.Duration(float64(time.Second) / writeFreq), + writePeriod: time.Duration(float64(time.Second) / rate), ptsOffset: ptsOffset, timeBasedPsi: true, diff --git a/container/mts/pes/pes.go b/container/mts/pes/pes.go index 50544aa2..b0e40f86 100644 --- a/container/mts/pes/pes.go +++ b/container/mts/pes/pes.go @@ -26,7 +26,7 @@ LICENSE package pes -const MaxPesSize = 65536 +const MaxPesSize = 64 * 1 << 10 /* The below data struct encapsulates the fields of an PES packet. Below is From be0f8d00943e77161d7dfe9e2426d9847e394302 Mon Sep 17 00:00:00 2001 From: Alan Noble Date: Thu, 11 Apr 2019 22:17:01 +0000 Subject: [PATCH 15/15] Fix typo in Rotation param. --- cmd/revid-cli/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/revid-cli/main.go b/cmd/revid-cli/main.go index a1c1d95f..c7e3fffd 100644 --- a/cmd/revid-cli/main.go +++ b/cmd/revid-cli/main.go @@ -127,7 +127,7 @@ func handleFlags() revid.Config { frameRatePtr = flag.Uint("FrameRate", 0, "Frame rate of captured video") quantizationPtr = flag.Uint("Quantization", 0, "Desired quantization value: 0-40") intraRefreshPeriodPtr = flag.Uint("IntraRefreshPeriod", 0, "The IntraRefreshPeriod i.e. how many keyframes we send") - rotationPtr = flag.Uint("Rotatation", 0, "Rotate video output. (0-359 degrees)") + rotationPtr = flag.Uint("Rotation", 0, "Rotate video output. (0-359 degrees)") brightnessPtr = flag.Uint("Brightness", 50, "Set brightness. (0-100) ") saturationPtr = flag.Int("Saturation", 0, "Set Saturation. (100-100)") exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(revid.ExposureModes[:], ",")+")")