diff --git a/cmd/revid-cli/main.go b/cmd/revid-cli/main.go index 53f38833..5eca3ff7 100644 --- a/cmd/revid-cli/main.go +++ b/cmd/revid-cli/main.go @@ -106,32 +106,30 @@ func handleFlags() revid.Config { var cfg revid.Config var ( - cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") - - inputCodecPtr = flag.String("InputCodec", "H264", "The codec of the input: H264, Mjpeg, PCM, ADPCM") - inputPtr = flag.String("Input", "", "The input type: Raspivid, File, v4l, Audio, RTSP") - rtspURLPtr = flag.String("RTSPURL", "", "The URL for an RTSP server.") - verbosityPtr = flag.String("Verbosity", "Info", "Verbosity: Debug, Info, Warning, Error, Fatal") - rtpAddrPtr = flag.String("RtpAddr", "", "Rtp destination address: : (port is generally 6970-6999)") - logPathPtr = flag.String("LogPath", defaultLogPath, "The log path") - configFilePtr = flag.String("ConfigFile", "", "NetSender config file") - rtmpUrlPtr = flag.String("RtmpUrl", "", "Url of rtmp endpoint") - outputPathPtr = flag.String("OutputPath", "", "The directory of the output file") - inputFilePtr = flag.String("InputPath", "", "The directory of the input file") - httpAddressPtr = flag.String("HttpAddress", "", "Destination address of http posts") - verticalFlipPtr = flag.Bool("VerticalFlip", false, "Flip video vertically: Yes, No") - horizontalFlipPtr = flag.Bool("HorizontalFlip", false, "Flip video horizontally: Yes, No") - bitratePtr = flag.Uint("Bitrate", 0, "Bitrate of recorded video") - heightPtr = flag.Uint("Height", 0, "Height in pixels") - widthPtr = flag.Uint("Width", 0, "Width in pixels") - 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("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[:], ",")+")") - autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(revid.AutoWhiteBalanceModes[:], ",")+")") + cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") + inputCodecPtr = flag.String("InputCodec", "H264", "The codec of the input: H264, Mjpeg, PCM, ADPCM") + inputPtr = flag.String("Input", "", "The input type: Raspivid, File, v4l, Audio, RTSP") + rtspURLPtr = flag.String("RTSPURL", "", "The URL for an RTSP server.") + verbosityPtr = flag.String("Verbosity", "Info", "Verbosity: Debug, Info, Warning, Error, Fatal") + rtpAddrPtr = flag.String("RtpAddr", "", "Rtp destination address: : (port is generally 6970-6999)") + logPathPtr = flag.String("LogPath", defaultLogPath, "The log path") + configFilePtr = flag.String("ConfigFile", "", "NetSender config file") + rtmpUrlPtr = flag.String("RtmpUrl", "", "Url of rtmp endpoint") + outputPathPtr = flag.String("OutputPath", "", "The directory of the output file") + inputFilePtr = flag.String("InputPath", "", "The directory of the input file") + httpAddressPtr = flag.String("HttpAddress", "", "Destination address of http posts") + verticalFlipPtr = flag.Bool("VerticalFlip", false, "Flip video vertically: Yes, No") + horizontalFlipPtr = flag.Bool("HorizontalFlip", false, "Flip video horizontally: Yes, No") + bitratePtr = flag.Uint("Bitrate", 0, "Bitrate of recorded video") + heightPtr = flag.Uint("Height", 0, "Height in pixels") + widthPtr = flag.Uint("Width", 0, "Width in pixels") + frameRatePtr = flag.Uint("FrameRate", 0, "Frame rate of captured video") + quantizationPtr = flag.Uint("Quantization", 0, "Desired quantization value: 0-40") + 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[:], ",")+")") + autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(revid.AutoWhiteBalanceModes[:], ",")+")") // Audio specific flags. sampleRatePtr = flag.Int("SampleRate", 48000, "Sample rate of recorded audio") @@ -246,7 +244,6 @@ func handleFlags() revid.Config { cfg.FrameRate = *frameRatePtr cfg.HTTPAddress = *httpAddressPtr cfg.Quantization = *quantizationPtr - cfg.IntraRefreshPeriod = *intraRefreshPeriodPtr cfg.RTPAddress = *rtpAddrPtr cfg.Brightness = *brightnessPtr cfg.Saturation = *saturationPtr diff --git a/codec/adpcm/adpcm.go b/codec/adpcm/adpcm.go index ce8ae9f7..479f6261 100644 --- a/codec/adpcm/adpcm.go +++ b/codec/adpcm/adpcm.go @@ -40,10 +40,11 @@ import ( const ( byteDepth = 2 // We are working with 16-bit samples. TODO(Trek): make configurable. initSamps = 2 // Number of samples used to initialise the encoder. - initBytes = initSamps * byteDepth - headBytes = 4 // Number of bytes in the header of ADPCM. + initSize = initSamps * byteDepth + headSize = 8 // Number of bytes in the header of ADPCM. samplesPerEnc = 2 // Number of sample encoded at a time eg. 2 16-bit samples get encoded into 1 byte. bytesPerEnc = samplesPerEnc * byteDepth + chunkLenSize = 4 // Size of the chunk length in bytes, chunk length is a 32 bit number. compFact = 4 // In general ADPCM compresses by a factor of 4. ) @@ -176,7 +177,7 @@ func (e *Encoder) calcHead(sample []byte, pad bool) (int, error) { // The suitable step size is the closest step size in the stepTable to half the absolute difference of the first two samples. func (e *Encoder) init(samples []byte) { int1 := int16(binary.LittleEndian.Uint16(samples[:byteDepth])) - int2 := int16(binary.LittleEndian.Uint16(samples[byteDepth:initBytes])) + int2 := int16(binary.LittleEndian.Uint16(samples[byteDepth:initSize])) e.est = int1 halfDiff := math.Abs(math.Abs(float64(int1)) - math.Abs(float64(int2))/2) @@ -197,8 +198,8 @@ func (e *Encoder) init(samples []byte) { func (e *Encoder) Write(b []byte) (int, error) { // Check that pcm has enough data to initialize Decoder. pcmLen := len(b) - if pcmLen < initBytes { - return 0, fmt.Errorf("length of given byte array must be >= %v", initBytes) + if pcmLen < initSize { + return 0, fmt.Errorf("length of given byte array must be >= %v", initSize) } // Determine if there will be a byte that won't contain two full nibbles and will need padding. @@ -207,8 +208,18 @@ func (e *Encoder) Write(b []byte) (int, error) { pad = true } - e.init(b[:initBytes]) - n, err := e.calcHead(b[:byteDepth], pad) + // Write the first 4 bytes of the adpcm chunk, which represent its length, ie. the number of bytes following the chunk length. + chunkLen := EncBytes(pcmLen) + chunkLenBytes := make([]byte, chunkLenSize) + binary.LittleEndian.PutUint32(chunkLenBytes, uint32(chunkLen)) + n, err := e.dst.Write(chunkLenBytes) + if err != nil { + return n, err + } + + e.init(b[:initSize]) + _n, err := e.calcHead(b[:byteDepth], pad) + n += _n if err != nil { return n, err } @@ -284,47 +295,59 @@ func (d *Decoder) decodeSample(nibble byte) int16 { // It writes its output to the Decoder's dst. // The number of bytes written out is returned along with any error that occured. func (d *Decoder) Write(b []byte) (int, error) { - // Initialize Decoder with first 4 bytes of b. - d.est = int16(binary.LittleEndian.Uint16(b[:byteDepth])) - d.idx = int16(b[byteDepth]) - d.step = stepTable[d.idx] - n, err := d.dst.Write(b[:byteDepth]) - if err != nil { - return n, err - } + // Iterate over each chunk and decode it. + var n int + var chunkLen int + for off := 0; off+headSize <= len(b); off += chunkLen { + // Read length of chunk and check if whole chunk exists. + chunkLen = int(binary.LittleEndian.Uint32(b[off : off+chunkLenSize])) + if off+chunkLen > len(b) { + break + } - // For each byte, seperate it into two nibbles (each nibble is a compressed sample), - // then decode each nibble and output the resulting 16-bit samples. - // If padding flag is true (Adpcm[3]), only decode up until the last byte, then decode that separately. - for i := headBytes; i < len(b)-int(b[3]); i++ { - twoNibs := b[i] - nib2 := byte(twoNibs >> 4) - nib1 := byte((nib2 << 4) ^ twoNibs) - - firstBytes := make([]byte, byteDepth) - binary.LittleEndian.PutUint16(firstBytes, uint16(d.decodeSample(nib1))) - _n, err := d.dst.Write(firstBytes) + // Initialize Decoder with header of b. + d.est = int16(binary.LittleEndian.Uint16(b[off+chunkLenSize : off+chunkLenSize+byteDepth])) + d.idx = int16(b[off+chunkLenSize+byteDepth]) + d.step = stepTable[d.idx] + _n, err := d.dst.Write(b[off+chunkLenSize : off+chunkLenSize+byteDepth]) n += _n if err != nil { return n, err } - secondBytes := make([]byte, byteDepth) - binary.LittleEndian.PutUint16(secondBytes, uint16(d.decodeSample(nib2))) - _n, err = d.dst.Write(secondBytes) - n += _n - if err != nil { - return n, err + // For each byte, seperate it into two nibbles (each nibble is a compressed sample), + // then decode each nibble and output the resulting 16-bit samples. + // If padding flag is true only decode up until the last byte, then decode that separately. + for i := off + headSize; i < off+chunkLen-int(b[off+chunkLenSize+3]); i++ { + twoNibs := b[i] + nib2 := byte(twoNibs >> 4) + nib1 := byte((nib2 << 4) ^ twoNibs) + + firstBytes := make([]byte, byteDepth) + binary.LittleEndian.PutUint16(firstBytes, uint16(d.decodeSample(nib1))) + _n, err := d.dst.Write(firstBytes) + n += _n + if err != nil { + return n, err + } + + secondBytes := make([]byte, byteDepth) + binary.LittleEndian.PutUint16(secondBytes, uint16(d.decodeSample(nib2))) + _n, err = d.dst.Write(secondBytes) + n += _n + if err != nil { + return n, err + } } - } - if b[3] == 0x01 { - padNib := b[len(b)-1] - samp := make([]byte, byteDepth) - binary.LittleEndian.PutUint16(samp, uint16(d.decodeSample(padNib))) - _n, err := d.dst.Write(samp) - n += _n - if err != nil { - return n, err + if b[off+chunkLenSize+3] == 0x01 { + padNib := b[off+chunkLen-1] + samp := make([]byte, byteDepth) + binary.LittleEndian.PutUint16(samp, uint16(d.decodeSample(padNib))) + _n, err := d.dst.Write(samp) + n += _n + if err != nil { + return n, err + } } } return n, nil @@ -349,7 +372,7 @@ func EncBytes(n int) int { // and a start index and padding-flag byte are added. // Also if there are an even number of samples, there will be half a byte of padding added to the last byte. if n%bytesPerEnc == 0 { - return (n-byteDepth)/compFact + headBytes + 1 + return (n-byteDepth)/compFact + headSize + 1 } - return (n-byteDepth)/compFact + headBytes + return (n-byteDepth)/compFact + headSize } diff --git a/codec/adpcm/adpcm_test.go b/codec/adpcm/adpcm_test.go index 8b825696..0afde670 100644 --- a/codec/adpcm/adpcm_test.go +++ b/codec/adpcm/adpcm_test.go @@ -51,7 +51,7 @@ func TestEncodeBlock(t *testing.T) { } // Read expected adpcm file. - exp, err := ioutil.ReadFile("../../../test/test-data/av/output/encoded_8kHz_adpcm_test.adpcm") + exp, err := ioutil.ReadFile("../../../test/test-data/av/output/encoded_8kHz_adpcm_test2.adpcm") if err != nil { t.Errorf("Unable to read expected ADPCM file: %v", err) } @@ -65,7 +65,7 @@ func TestEncodeBlock(t *testing.T) { // resulting PCM with the expected decoded PCM. func TestDecodeBlock(t *testing.T) { // Read adpcm. - comp, err := ioutil.ReadFile("../../../test/test-data/av/input/encoded_8kHz_adpcm_test.adpcm") + comp, err := ioutil.ReadFile("../../../test/test-data/av/input/encoded_8kHz_adpcm_test2.adpcm") if err != nil { t.Errorf("Unable to read input ADPCM file: %v", err) } @@ -79,7 +79,7 @@ func TestDecodeBlock(t *testing.T) { } // Read expected pcm file. - exp, err := ioutil.ReadFile("../../../test/test-data/av/output/decoded_8kHz_adpcm_test.pcm") + exp, err := ioutil.ReadFile("../../../test/test-data/av/output/decoded_8kHz_adpcm_test2.pcm") if err != nil { t.Errorf("Unable to read expected PCM file: %v", err) } diff --git a/codec/h264/h264dec/cabac.go b/codec/h264/h264dec/cabac.go index b5940a00..7b28414e 100644 --- a/codec/h264/h264dec/cabac.go +++ b/codec/h264/h264dec/cabac.go @@ -28,8 +28,6 @@ LICENSE package h264dec import ( - "math" - "bitbucket.org/ausocean/av/codec/h264/h264dec/bits" "github.com/pkg/errors" ) @@ -306,80 +304,6 @@ var ( } ) -// Errors used by mbTypeBinarization. -var ( - errBadMbType = errors.New("macroblock type outside of valid range") - errBadMbSliceType = errors.New("bad slice type for macroblock") -) - -// mbTypeBinarization returns the macroblock type binarization for the given -// macroblock type value and slice type using the process defined in section -// 9.3.2.5 of the specifications. -func mbTypeBinarization(v, slice int) ([]int, error) { - switch slice { - case sliceTypeI: - if v < minIMbType || v > maxIMbType { - return nil, errBadMbType - } - return binOfIMBTypes[v], nil - - case sliceTypeSI: - if v < minSIMbType || v > maxSIMbType { - return nil, errBadMbType - } - if v == sliceTypeSI { - return []int{0}, nil - } - return append([]int{1}, binOfIMBTypes[v-1]...), nil - - case sliceTypeP, sliceTypeSP: - if v < minPOrSPMbType || v > maxPOrSPMbType || v == P8x8ref0 { - return nil, errBadMbType - } - if v < 5 { - return binOfPOrSPMBTypes[v], nil - } - return append([]int{1}, binOfIMBTypes[v-5]...), nil - - case sliceTypeB: - if v < minBMbType || v > maxBMbType { - return nil, errBadMbType - } - if v < 23 { - return binOfBMBTypes[v], nil - } - return append([]int{1, 1, 1, 1, 0, 1}, binOfIMBTypes[v-23]...), nil - - default: - return nil, errBadMbSliceType - } -} - -// Error used by subMbTypeBinarization. -var errBadSubMbSliceType = errors.New("bad slice type for sub-macroblock") - -// subMbTypeBinarization returns the binarization of a sub-macroblock type -// given the slice in which it is in using the process defined in section -// 9.3.2.5 of the specifications. -func subMbTypeBinarization(v, slice int) ([]int, error) { - switch slice { - case sliceTypeP, sliceTypeSP: - if v < minPOrSPSubMbType || v > maxPOrSPSubMbType { - return nil, errBadMbType - } - return binOfPOrSPSubMBTypes[v], nil - - case sliceTypeB: - if v < minBSubMbType || v > maxBSubMbType { - return nil, errBadMbType - } - return binOfBSubMBTypes[v], nil - - default: - return nil, errBadSubMbSliceType - } -} - // Table 9-34 type MaxBinIdxCtx struct { // When false, Prefix is the MaxBinIdxCtx @@ -762,119 +686,3 @@ func CtxIdx(binIdx, maxBinIdxCtx, ctxIdxOffset int) int { return ctxIdx } - -// Error used by unaryBinarization. -var errNegativeSyntaxVal = errors.New("cannot get unary binarization of negative value") - -// unaryBinarization returns the unary binarization of a syntax element having -// value v, as specified by setion 9.3.2.1 in the specifications. -func unaryBinarization(v int) ([]int, error) { - if v < 0 { - return nil, errNegativeSyntaxVal - } - r := make([]int, v+1) - for i := 0; i <= v; i++ { - if i < v { - r[i] = 1 - } - } - return r, nil -} - -// Error used by truncUnaryBinarization. -var errInvalidSyntaxVal = errors.New("syntax value cannot be greater than cMax") - -// truncUnaryBinarization returns the truncated unary binarization of a syntax -// element v given a cMax as specified in section 9.3.2.2 of the specifications. -func truncUnaryBinarization(v, cMax int) ([]int, error) { - if v < 0 { - return nil, errNegativeSyntaxVal - } - - if v > cMax { - return nil, errInvalidSyntaxVal - } - - if v == cMax { - b, _ := unaryBinarization(v) - return b[:len(b)-1], nil - } - return unaryBinarization(v) -} - -// Error used by unaryExpGolombBinarization. -var errInvalidUCoff = errors.New("uCoff cannot be less than or equal to zero") - -// unaryExpGolombBinarization returns the concatendated unary/k-th order -// Exp-Golomb (UEGk) binarization of a syntax element using the process defined -// in section 9.3.2.3 of the specifications. -func unaryExpGolombBinarization(v, uCoff, k int, signedValFlag bool) ([]int, error) { - if uCoff <= 0 { - return nil, errInvalidUCoff - } - - prefix, err := truncUnaryBinarization(mini(uCoff, absi(v)), uCoff) - if err != nil { - return nil, err - } - - return append(prefix, suffix(v, uCoff, k, signedValFlag)...), nil -} - -// suffix returns the suffix part of a unary k-th Exp-Golomb Binarization -// using the the algorithm as described by pseudo code 9-6 in section 9.3.2.3. -// TODO: could probably reduce allocations. -func suffix(v, uCoff, k int, signedValFlag bool) []int { - var s []int - - if absi(v) >= uCoff { - sufS := absi(v) - uCoff - var stop bool - - for { - if sufS >= (1 << uint(k)) { - s = append(s, 1) - sufS = sufS - (1 << uint(k)) - k++ - } else { - s = append(s, 0) - for k = k - 1; k >= 0; k-- { - s = append(s, (sufS>>uint(k))&1) - } - stop = true - } - if stop { - break - } - } - } - - if signedValFlag && v != 0 { - if v > 0 { - s = append(s, 0) - } else { - s = append(s, 1) - } - } - - return s -} - -// Error used by fixedLenBinariztion. -var errNegativeValue = errors.New("cannot get fixed length binarization of negative value") - -// fixedLenBinarization returns the fixed-length (FL) binarization of the syntax -// element v, given cMax to determine bin length, as specified by section 9.3.2.4 -// of the specifications. -func fixedLenBinarization(v, cMax int) ([]int, error) { - if v < 0 { - return nil, errNegativeValue - } - l := int(math.Ceil(math.Log2(float64(cMax + 1)))) - r := make([]int, l) - for i := l - 1; i >= 0; i-- { - r[i] = v % 2 - v = v / 2 - } - return r, nil -} diff --git a/codec/h264/h264dec/cabac_test.go b/codec/h264/h264dec/cabac_test.go index 5ca17ce8..20f1e066 100644 --- a/codec/h264/h264dec/cabac_test.go +++ b/codec/h264/h264dec/cabac_test.go @@ -26,74 +26,9 @@ LICENSE package h264dec import ( - "reflect" "testing" ) -func TestMbTypeBinarization(t *testing.T) { - tests := []struct { - v, slice int - want []int - err error - }{ - {v: 6, slice: sliceTypeI, want: []int{1, 0, 0, 1, 0, 0, 1}}, - {v: 26, slice: sliceTypeI, err: errBadMbType}, - {v: -1, slice: sliceTypeI, err: errBadMbType}, - {v: 4, slice: sliceTypeSI, want: []int{0}}, - {v: 6, slice: sliceTypeSI, want: []int{1, 1, 0, 0, 1, 0, 0, 0}}, - {v: 0, slice: sliceTypeSI, err: errBadMbType}, - {v: 27, slice: sliceTypeSI, err: errBadMbType}, - {v: 2, slice: sliceTypeP, want: []int{0, 1, 0}}, - {v: 3, slice: sliceTypeSP, want: []int{0, 0, 1}}, - {v: 7, slice: sliceTypeP, want: []int{1, 1, 0, 0, 0, 0, 1}}, - {v: 7, slice: sliceTypeSP, want: []int{1, 1, 0, 0, 0, 0, 1}}, - {v: -1, slice: sliceTypeP, err: errBadMbType}, - {v: 31, slice: sliceTypeP, err: errBadMbType}, - {v: 8, slice: sliceTypeB, want: []int{1, 1, 0, 1, 0, 1}}, - {v: 30, slice: sliceTypeB, want: []int{1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0}}, - {v: -1, slice: sliceTypeB, err: errBadMbType}, - {v: 49, slice: sliceTypeB, err: errBadMbType}, - {v: 6, slice: 20, err: errBadMbSliceType}, - } - - for i, test := range tests { - got, err := mbTypeBinarization(test.v, test.slice) - if err != test.err { - t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v", i, err, test.err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v", i, got, test.want) - } - } -} - -func TestSubMbTypeBinarization(t *testing.T) { - tests := []struct { - v, slice int - want []int - err error - }{ - {v: 2, slice: sliceTypeP, want: []int{0, 1, 1}}, - {v: 2, slice: sliceTypeSP, want: []int{0, 1, 1}}, - {v: -1, slice: sliceTypeSP, err: errBadMbType}, - {v: 4, slice: sliceTypeSP, err: errBadMbType}, - {v: 9, slice: sliceTypeB, want: []int{1, 1, 1, 0, 1, 0}}, - {v: 9, slice: 40, err: errBadSubMbSliceType}, - } - - for i, test := range tests { - got, err := subMbTypeBinarization(test.v, test.slice) - if err != test.err { - t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v", i, err, test.err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v", i, got, test.want) - } - } -} - var ctxIdxTests = []struct { binIdx int maxBinIdxCtx int @@ -211,144 +146,3 @@ func TestCtxIdx(t *testing.T) { } } } - -func TestUnaryBinarization(t *testing.T) { - // Test data has been extracted from table 9-35 of the specifications. - tests := []struct { - in int - want []int - err error - }{ - {in: 0, want: []int{0}, err: nil}, - {in: 1, want: []int{1, 0}, err: nil}, - {in: 2, want: []int{1, 1, 0}, err: nil}, - {in: 3, want: []int{1, 1, 1, 0}, err: nil}, - {in: 4, want: []int{1, 1, 1, 1, 0}, err: nil}, - {in: 5, want: []int{1, 1, 1, 1, 1, 0}, err: nil}, - {in: -3, want: nil, err: errNegativeSyntaxVal}, - } - - for i, test := range tests { - got, err := unaryBinarization(test.in) - if err != test.err { - t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v", i, err, test.err) - } - - if !reflect.DeepEqual(test.want, got) { - t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) - } - } -} - -func TestFixedLengthBinarization(t *testing.T) { - tests := []struct { - v int - cMax int - want []int - err error - }{ - {v: 0, cMax: 7, want: []int{0, 0, 0}}, - {v: 1, cMax: 7, want: []int{0, 0, 1}}, - {v: 2, cMax: 7, want: []int{0, 1, 0}}, - {v: 3, cMax: 7, want: []int{0, 1, 1}}, - {v: 4, cMax: 7, want: []int{1, 0, 0}}, - {v: 5, cMax: 7, want: []int{1, 0, 1}}, - {v: 6, cMax: 7, want: []int{1, 1, 0}}, - {v: 7, cMax: 7, want: []int{1, 1, 1}}, - {v: -1, cMax: 7, want: nil, err: errNegativeValue}, - } - - for i, test := range tests { - got, err := fixedLenBinarization(test.v, test.cMax) - if err != test.err { - t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v\n", i, err, test.err) - } - - if !reflect.DeepEqual(test.want, got) { - t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) - } - } -} - -func TestTruncUnaryBinarization(t *testing.T) { - tests := []struct { - v int - cMax int - want []int - err error - }{ - {v: 0, cMax: 10, want: []int{0}, err: nil}, - {v: 1, cMax: 10, want: []int{1, 0}, err: nil}, - {v: 2, cMax: 10, want: []int{1, 1, 0}, err: nil}, - {v: 0, cMax: 0, want: []int{}, err: nil}, - {v: 4, cMax: 4, want: []int{1, 1, 1, 1}, err: nil}, - {v: 1, cMax: 10, want: []int{1, 0}, err: nil}, - {v: 2, cMax: 10, want: []int{1, 1, 0}, err: nil}, - {v: -3, cMax: 10, want: nil, err: errNegativeSyntaxVal}, - {v: 5, cMax: 4, want: nil, err: errInvalidSyntaxVal}, - } - - for i, test := range tests { - got, err := truncUnaryBinarization(test.v, test.cMax) - if err != test.err { - t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v", i, err, test.err) - } - - if !reflect.DeepEqual(test.want, got) { - t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) - } - } -} - -func TestUEGkSuffix(t *testing.T) { - // Data from https://patents.google.com/patent/US20070092150 - tests := []struct { - v, uCoff, k int - signedValFlag bool - want []int - }{ - 0: {v: 14, uCoff: 14, want: []int{0}}, - 1: {v: 15, uCoff: 14, want: []int{1, 0, 0}}, - 2: {v: 16, uCoff: 14, want: []int{1, 0, 1}}, - 3: {v: 17, uCoff: 14, want: []int{1, 1, 0, 0, 0}}, - 4: {v: 18, uCoff: 14, want: []int{1, 1, 0, 0, 1}}, - 5: {v: 19, uCoff: 14, want: []int{1, 1, 0, 1, 0}}, - 6: {v: 20, uCoff: 14, want: []int{1, 1, 0, 1, 1}}, - 7: {v: 21, uCoff: 14, want: []int{1, 1, 1, 0, 0, 0, 0}}, - 8: {v: 22, uCoff: 14, want: []int{1, 1, 1, 0, 0, 0, 1}}, - 9: {v: 23, uCoff: 14, want: []int{1, 1, 1, 0, 0, 1, 0}}, - 10: {v: 24, uCoff: 14, want: []int{1, 1, 1, 0, 0, 1, 1}}, - 11: {v: 25, uCoff: 14, want: []int{1, 1, 1, 0, 1, 0, 0}}, - } - - for i, test := range tests { - got := suffix(test.v, test.uCoff, test.k, test.signedValFlag) - if !reflect.DeepEqual(got, test.want) { - t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) - } - } -} - -func TestUnaryExpGolombBinarization(t *testing.T) { - tests := []struct { - v, uCoff, k int - signedValFlag bool - want []int - }{ - 0: {v: 7, uCoff: 14, want: []int{1, 1, 1, 1, 1, 1, 1, 0}}, - 1: {v: 17, uCoff: 14, want: []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0}}, - 2: {v: 15, uCoff: 14, signedValFlag: true, want: []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0}}, - 3: {v: -15, uCoff: 14, signedValFlag: true, want: []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1}}, - } - - for i, test := range tests { - got, err := unaryExpGolombBinarization(test.v, test.uCoff, test.k, test.signedValFlag) - if err != nil { - t.Errorf("did not expect error %v for test %d", err, i) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) - } - } -} diff --git a/codec/h264/h264dec/cabacenc.go b/codec/h264/h264dec/cabacenc.go new file mode 100644 index 00000000..f9b0cca5 --- /dev/null +++ b/codec/h264/h264dec/cabacenc.go @@ -0,0 +1,243 @@ +/* +TODO: this file should really be in a 'h264enc' package. + +DESCRIPTION + cabacenc.go provides functionality for CABAC encoding. + +AUTHORS + Saxon A. 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 + in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package h264dec + +import ( + "errors" + "fmt" + "math" +) + +// Error used by unaryBinString. +var errNegativeSyntaxVal = errors.New("cannot get unary binary string of negative value") + +// unaryBinString returns the unary binary string of a syntax element having +// value v, as specified by setion 9.3.2.1 in the specifications. +func unaryBinString(v int) ([]int, error) { + if v < 0 { + return nil, errNegativeSyntaxVal + } + r := make([]int, v+1) + for i := 0; i <= v; i++ { + if i < v { + r[i] = 1 + } + } + return r, nil +} + +// Error used by truncUnaryBinString. +var errInvalidSyntaxVal = errors.New("syntax value cannot be greater than cMax") + +// truncUnaryBinString returns the truncated unary binary string of a syntax +// element v given a cMax as specified in section 9.3.2.2 of the specifications. +func truncUnaryBinString(v, cMax int) ([]int, error) { + if v < 0 { + return nil, errNegativeSyntaxVal + } + + if v > cMax { + return nil, errInvalidSyntaxVal + } + + if v == cMax { + b, _ := unaryBinString(v) + return b[:len(b)-1], nil + } + return unaryBinString(v) +} + +// Error used by unaryExpGolombBinString. +var errInvalidUCoff = errors.New("uCoff cannot be less than or equal to zero") + +// unaryExpGolombBinString returns the concatendated unary/k-th order +// Exp-Golomb (UEGk) binary string of a syntax element using the process defined +// in section 9.3.2.3 of the specifications. +func unaryExpGolombBinString(v, uCoff, k int, signedValFlag bool) ([]int, error) { + if uCoff <= 0 { + return nil, errInvalidUCoff + } + + prefix, err := truncUnaryBinString(mini(uCoff, absi(v)), uCoff) + if err != nil { + return nil, err + } + + return append(prefix, suffix(v, uCoff, k, signedValFlag)...), nil +} + +// suffix returns the suffix part of a unary k-th Exp-Golomb binar string +// using the the algorithm as described by pseudo code 9-6 in section 9.3.2.3. +// TODO: could probably reduce allocations. +func suffix(v, uCoff, k int, signedValFlag bool) []int { + var s []int + + if absi(v) >= uCoff { + sufS := absi(v) - uCoff + var stop bool + + for { + if sufS >= (1 << uint(k)) { + s = append(s, 1) + sufS = sufS - (1 << uint(k)) + k++ + } else { + s = append(s, 0) + for k = k - 1; k >= 0; k-- { + s = append(s, (sufS>>uint(k))&1) + } + stop = true + } + if stop { + break + } + } + } + + if signedValFlag && v != 0 { + if v > 0 { + s = append(s, 0) + } else { + s = append(s, 1) + } + } + + return s +} + +// Error used by fixedLenBinString. +var errNegativeValue = errors.New("cannot get fixed length binary string of negative value") + +// fixedLenBinString returns the fixed-length (FL) binary string of the syntax +// element v, given cMax to determine bin length, as specified by section 9.3.2.4 +// of the specifications. +func fixedLenBinString(v, cMax int) ([]int, error) { + if v < 0 { + return nil, errNegativeValue + } + l := int(math.Ceil(math.Log2(float64(cMax + 1)))) + r := make([]int, l) + for i := l - 1; i >= 0; i-- { + r[i] = v % 2 + v = v / 2 + } + return r, nil +} + +// Errors used by mbTypeBinString. +var ( + errBadMbType = errors.New("macroblock type outside of valid range") + errBadMbSliceType = errors.New("bad slice type for macroblock") +) + +// mbTypeBinString returns the macroblock type binary string for the given +// macroblock type value and slice type using the process defined in section +// 9.3.2.5 of the specifications. +func mbTypeBinString(v, slice int) ([]int, error) { + switch slice { + case sliceTypeI: + if v < minIMbType || v > maxIMbType { + return nil, errBadMbType + } + return binOfIMBTypes[v], nil + + case sliceTypeSI: + if v < minSIMbType || v > maxSIMbType { + return nil, errBadMbType + } + if v == sliceTypeSI { + return []int{0}, nil + } + return append([]int{1}, binOfIMBTypes[v-1]...), nil + + case sliceTypeP, sliceTypeSP: + if v < minPOrSPMbType || v > maxPOrSPMbType || v == P8x8ref0 { + return nil, errBadMbType + } + if v < 5 { + return binOfPOrSPMBTypes[v], nil + } + return append([]int{1}, binOfIMBTypes[v-5]...), nil + + case sliceTypeB: + if v < minBMbType || v > maxBMbType { + return nil, errBadMbType + } + if v < 23 { + return binOfBMBTypes[v], nil + } + return append([]int{1, 1, 1, 1, 0, 1}, binOfIMBTypes[v-23]...), nil + + default: + return nil, errBadMbSliceType + } +} + +// Error used by subMbTypeBinString. +var errBadSubMbSliceType = errors.New("bad slice type for sub-macroblock") + +// subMbTypeBinString returns the binary string of a sub-macroblock type +// given the slice in which it is in using the process defined in section +// 9.3.2.5 of the specifications. +func subMbTypeBinString(v, slice int) ([]int, error) { + switch slice { + case sliceTypeP, sliceTypeSP: + if v < minPOrSPSubMbType || v > maxPOrSPSubMbType { + return nil, errBadMbType + } + return binOfPOrSPSubMBTypes[v], nil + + case sliceTypeB: + if v < minBSubMbType || v > maxBSubMbType { + return nil, errBadMbType + } + return binOfBSubMBTypes[v], nil + + default: + return nil, errBadSubMbSliceType + } +} + +// codedBlockPatternBinString returns the binarization for the syntax element +// coded_block_pattern as defined by section 9.3.2.6 in specifications. +func codedBlockPatternBinString(luma, chroma, arrayType int) ([]int, error) { + p, err := fixedLenBinString(luma, 15) + if err != nil { + return nil, fmt.Errorf("fixed length binarization failed with error: %v", err) + } + + if arrayType == 0 || arrayType == 3 { + return p, nil + } + + s, err := truncUnaryBinString(chroma, 2) + if err != nil { + return nil, fmt.Errorf("truncated unary binarization failed with error: %v", err) + } + + return append(p, s...), nil +} diff --git a/codec/h264/h264dec/cabacenc_test.go b/codec/h264/h264dec/cabacenc_test.go new file mode 100644 index 00000000..d27b1624 --- /dev/null +++ b/codec/h264/h264dec/cabacenc_test.go @@ -0,0 +1,237 @@ +/* +TODO: this file should really be in a 'h264enc' package. + +DESCRIPTION + cabacenc_test.go provides testing for functionality found in cabacenc.go. + +AUTHORS + Saxon A. 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 + in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package h264dec + +import ( + "reflect" + "testing" +) + +func TestMbTypeBinString(t *testing.T) { + tests := []struct { + v, slice int + want []int + err error + }{ + {v: 6, slice: sliceTypeI, want: []int{1, 0, 0, 1, 0, 0, 1}}, + {v: 26, slice: sliceTypeI, err: errBadMbType}, + {v: -1, slice: sliceTypeI, err: errBadMbType}, + {v: 4, slice: sliceTypeSI, want: []int{0}}, + {v: 6, slice: sliceTypeSI, want: []int{1, 1, 0, 0, 1, 0, 0, 0}}, + {v: 0, slice: sliceTypeSI, err: errBadMbType}, + {v: 27, slice: sliceTypeSI, err: errBadMbType}, + {v: 2, slice: sliceTypeP, want: []int{0, 1, 0}}, + {v: 3, slice: sliceTypeSP, want: []int{0, 0, 1}}, + {v: 7, slice: sliceTypeP, want: []int{1, 1, 0, 0, 0, 0, 1}}, + {v: 7, slice: sliceTypeSP, want: []int{1, 1, 0, 0, 0, 0, 1}}, + {v: -1, slice: sliceTypeP, err: errBadMbType}, + {v: 31, slice: sliceTypeP, err: errBadMbType}, + {v: 8, slice: sliceTypeB, want: []int{1, 1, 0, 1, 0, 1}}, + {v: 30, slice: sliceTypeB, want: []int{1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0}}, + {v: -1, slice: sliceTypeB, err: errBadMbType}, + {v: 49, slice: sliceTypeB, err: errBadMbType}, + {v: 6, slice: 20, err: errBadMbSliceType}, + } + + for i, test := range tests { + got, err := mbTypeBinString(test.v, test.slice) + if err != test.err { + t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v", i, err, test.err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v", i, got, test.want) + } + } +} + +func TestSubMbTypeBinString(t *testing.T) { + tests := []struct { + v, slice int + want []int + err error + }{ + {v: 2, slice: sliceTypeP, want: []int{0, 1, 1}}, + {v: 2, slice: sliceTypeSP, want: []int{0, 1, 1}}, + {v: -1, slice: sliceTypeSP, err: errBadMbType}, + {v: 4, slice: sliceTypeSP, err: errBadMbType}, + {v: 9, slice: sliceTypeB, want: []int{1, 1, 1, 0, 1, 0}}, + {v: 9, slice: 40, err: errBadSubMbSliceType}, + } + + for i, test := range tests { + got, err := subMbTypeBinString(test.v, test.slice) + if err != test.err { + t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v", i, err, test.err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v", i, got, test.want) + } + } +} + +func TestUnaryBinString(t *testing.T) { + // Test data has been extracted from table 9-35 of the specifications. + tests := []struct { + in int + want []int + err error + }{ + {in: 0, want: []int{0}, err: nil}, + {in: 1, want: []int{1, 0}, err: nil}, + {in: 2, want: []int{1, 1, 0}, err: nil}, + {in: 3, want: []int{1, 1, 1, 0}, err: nil}, + {in: 4, want: []int{1, 1, 1, 1, 0}, err: nil}, + {in: 5, want: []int{1, 1, 1, 1, 1, 0}, err: nil}, + {in: -3, want: nil, err: errNegativeSyntaxVal}, + } + + for i, test := range tests { + got, err := unaryBinString(test.in) + if err != test.err { + t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v", i, err, test.err) + } + + if !reflect.DeepEqual(test.want, got) { + t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) + } + } +} + +func TestFixedLengthBinString(t *testing.T) { + tests := []struct { + v int + cMax int + want []int + err error + }{ + {v: 0, cMax: 7, want: []int{0, 0, 0}}, + {v: 1, cMax: 7, want: []int{0, 0, 1}}, + {v: 2, cMax: 7, want: []int{0, 1, 0}}, + {v: 3, cMax: 7, want: []int{0, 1, 1}}, + {v: 4, cMax: 7, want: []int{1, 0, 0}}, + {v: 5, cMax: 7, want: []int{1, 0, 1}}, + {v: 6, cMax: 7, want: []int{1, 1, 0}}, + {v: 7, cMax: 7, want: []int{1, 1, 1}}, + {v: -1, cMax: 7, want: nil, err: errNegativeValue}, + } + + for i, test := range tests { + got, err := fixedLenBinString(test.v, test.cMax) + if err != test.err { + t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v\n", i, err, test.err) + } + + if !reflect.DeepEqual(test.want, got) { + t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) + } + } +} + +func TestTruncUnaryBinString(t *testing.T) { + tests := []struct { + v int + cMax int + want []int + err error + }{ + {v: 0, cMax: 10, want: []int{0}, err: nil}, + {v: 1, cMax: 10, want: []int{1, 0}, err: nil}, + {v: 2, cMax: 10, want: []int{1, 1, 0}, err: nil}, + {v: 0, cMax: 0, want: []int{}, err: nil}, + {v: 4, cMax: 4, want: []int{1, 1, 1, 1}, err: nil}, + {v: 1, cMax: 10, want: []int{1, 0}, err: nil}, + {v: 2, cMax: 10, want: []int{1, 1, 0}, err: nil}, + {v: -3, cMax: 10, want: nil, err: errNegativeSyntaxVal}, + {v: 5, cMax: 4, want: nil, err: errInvalidSyntaxVal}, + } + + for i, test := range tests { + got, err := truncUnaryBinString(test.v, test.cMax) + if err != test.err { + t.Errorf("did not get expected error for test %d\nGot: %v\nWant: %v", i, err, test.err) + } + + if !reflect.DeepEqual(test.want, got) { + t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) + } + } +} + +func TestUEGkSuffix(t *testing.T) { + // Data from https://patents.google.com/patent/US20070092150 + tests := []struct { + v, uCoff, k int + signedValFlag bool + want []int + }{ + 0: {v: 14, uCoff: 14, want: []int{0}}, + 1: {v: 15, uCoff: 14, want: []int{1, 0, 0}}, + 2: {v: 16, uCoff: 14, want: []int{1, 0, 1}}, + 3: {v: 17, uCoff: 14, want: []int{1, 1, 0, 0, 0}}, + 4: {v: 18, uCoff: 14, want: []int{1, 1, 0, 0, 1}}, + 5: {v: 19, uCoff: 14, want: []int{1, 1, 0, 1, 0}}, + 6: {v: 20, uCoff: 14, want: []int{1, 1, 0, 1, 1}}, + 7: {v: 21, uCoff: 14, want: []int{1, 1, 1, 0, 0, 0, 0}}, + 8: {v: 22, uCoff: 14, want: []int{1, 1, 1, 0, 0, 0, 1}}, + 9: {v: 23, uCoff: 14, want: []int{1, 1, 1, 0, 0, 1, 0}}, + 10: {v: 24, uCoff: 14, want: []int{1, 1, 1, 0, 0, 1, 1}}, + 11: {v: 25, uCoff: 14, want: []int{1, 1, 1, 0, 1, 0, 0}}, + } + + for i, test := range tests { + got := suffix(test.v, test.uCoff, test.k, test.signedValFlag) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) + } + } +} + +func TestUnaryExpGolombBinString(t *testing.T) { + tests := []struct { + v, uCoff, k int + signedValFlag bool + want []int + }{ + 0: {v: 7, uCoff: 14, want: []int{1, 1, 1, 1, 1, 1, 1, 0}}, + 1: {v: 17, uCoff: 14, want: []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0}}, + 2: {v: 15, uCoff: 14, signedValFlag: true, want: []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0}}, + 3: {v: -15, uCoff: 14, signedValFlag: true, want: []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1}}, + } + + for i, test := range tests { + got, err := unaryExpGolombBinString(test.v, test.uCoff, test.k, test.signedValFlag) + if err != nil { + t.Errorf("did not expect error %v for test %d", err, i) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("did not get expected result for test %d\nGot: %v\nWant: %v\n", i, got, test.want) + } + } +} diff --git a/codec/h264/h264dec/cavlc.go b/codec/h264/h264dec/cavlc.go new file mode 100644 index 00000000..c477a490 --- /dev/null +++ b/codec/h264/h264dec/cavlc.go @@ -0,0 +1,46 @@ +/* +DESCRIPTION + cavlc.go provides utilities for context-adaptive variable-length coding + for the parsing of H.264 syntax structure fields. + +AUTHORS + Saxon A. 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 + in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package h264dec + +import ( + "fmt" + + "bitbucket.org/ausocean/av/codec/h264/h264dec/bits" +) + +// parseLevelPrefix parses the level_prefix variable as specified by the process +// outlined in section 9.2.2.1 in the specifications. +func parseLevelPrefix(br *bits.BitReader) (int, error) { + zeros := -1 + for b := 0; b != 1; zeros++ { + _b, err := br.ReadBits(1) + if err != nil { + return -1, fmt.Errorf("could not read bit, failed with error: %v", err) + } + b = int(_b) + } + return zeros, nil +} diff --git a/codec/h264/h264dec/cavlc_test.go b/codec/h264/h264dec/cavlc_test.go new file mode 100644 index 00000000..a6740858 --- /dev/null +++ b/codec/h264/h264dec/cavlc_test.go @@ -0,0 +1,55 @@ +/* +DESCRIPTION + cavlc_test.go provides testing for functionality in cavlc.go. + +AUTHORS + Saxon A. 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 + in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package h264dec + +import ( + "bytes" + "testing" + + "bitbucket.org/ausocean/av/codec/h264/h264dec/bits" +) + +func TestParseLevelPrefix(t *testing.T) { + tests := []struct { + in string + want int + }{ + {in: "00001", want: 4}, + {in: "0000001", want: 6}, + {in: "1", want: 0}, + } + + for i, test := range tests { + s, _ := binToSlice(test.in) + l, err := parseLevelPrefix(bits.NewBitReader(bytes.NewReader(s))) + if err != nil { + t.Errorf("did not expect error: %v, for test %d", err, i) + } + + if l != test.want { + t.Errorf("did not get expected result for test %d\nGot: %d\nWant: %d\n", i, l, test.want) + } + } +} diff --git a/codec/h264/h264dec/frame.go b/codec/h264/h264dec/frame.go index 82b32ae5..ca85982b 100644 --- a/codec/h264/h264dec/frame.go +++ b/codec/h264/h264dec/frame.go @@ -9,7 +9,7 @@ const ( naluTypeSlicePartC naluTypeSliceIDRPicture naluTypeSEI - naluTypeSPS + NALTypeSPS naluTypePPS naluTypeAccessUnitDelimiter naluTypeEndOfSequence diff --git a/codec/h264/h264dec/read.go b/codec/h264/h264dec/read.go index c98d46ca..9b283c92 100644 --- a/codec/h264/h264dec/read.go +++ b/codec/h264/h264dec/read.go @@ -61,7 +61,7 @@ func (h *H264Reader) Start() { // TODO: need to handle error from this. nalUnit, _, _ := h.readNalUnit() switch nalUnit.Type { - case naluTypeSPS: + case NALTypeSPS: // TODO: handle this error sps, _ := NewSPS(nalUnit.RBSP, false) h.VideoStreams = append( diff --git a/codec/h264/parse.go b/codec/h264/parse.go new file mode 100644 index 00000000..9465fb1c --- /dev/null +++ b/codec/h264/parse.go @@ -0,0 +1,71 @@ +/* +DESCRIPTION + parse.go provides H.264 NAL unit parsing utilities for the extraction of + syntax elements. + +AUTHORS + Saxon A. Nelson-Milton + Dan Kortschak + +LICENSE + Copyright (C) 2017-2018 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 http://www.gnu.org/licenses. +*/ + +package h264 + +import "errors" + +var errNotEnoughBytes = errors.New("not enough bytes to read") + +// NALType returns the NAL type of the given NAL unit bytes. The given NAL unit +// may be in byte stream or packet format. +func NALType(n []byte) (int, error) { + sc := frameScanner{buf: n} + b, ok := sc.readByte() + if !ok { + return 0, errNotEnoughBytes + } + for i := 1; b == 0x00 && i != 4; i++ { + b, ok = sc.readByte() + if !ok { + return 0, errNotEnoughBytes + } + if b != 0x01 || (i != 2 && i != 3) { + continue + } + + b, ok = sc.readByte() + if !ok { + return 0, errNotEnoughBytes + } + return int(b & 0x1f), nil + } + return int(b & 0x1f), nil +} + +type frameScanner struct { + off int + buf []byte +} + +func (s *frameScanner) readByte() (b byte, ok bool) { + if s.off >= len(s.buf) { + return 0, false + } + b = s.buf[s.off] + s.off++ + return b, true +} diff --git a/container/mts/encoder.go b/container/mts/encoder.go index 5d5533cb..7d968d5f 100644 --- a/container/mts/encoder.go +++ b/container/mts/encoder.go @@ -26,14 +26,53 @@ LICENSE package mts import ( + "fmt" "io" + "strconv" "time" + "bitbucket.org/ausocean/av/codec/h264" + "bitbucket.org/ausocean/av/codec/h264/h264dec" "bitbucket.org/ausocean/av/container/mts/meta" "bitbucket.org/ausocean/av/container/mts/pes" "bitbucket.org/ausocean/av/container/mts/psi" + "bitbucket.org/ausocean/utils/realtime" ) +// Media type values. +// TODO: reference relevant specifications. +const ( + H264ID = 27 + H265ID = 36 + audioStreamID = 0xc0 // First audio stream ID. +) + +// Constants used to communicate which media codec will be packetized. +const ( + EncodeH264 = iota + EncodeH265 + EncodeAudio +) + +// Time-related constants. +const ( + // ptsOffset is the offset added to the clock to determine + // the current presentation timestamp. + ptsOffset = 700 * time.Millisecond + + // PCRFrequency is the base Program Clock Reference frequency in Hz. + PCRFrequency = 90000 + + // PTSFrequency is the presentation timestamp frequency in Hz. + PTSFrequency = 90000 + + // MaxPTS is the largest PTS value (i.e., for a 33-bit unsigned integer). + MaxPTS = (1 << 33) - 1 +) + +// If we are not using NAL based PSI intervals then we will send PSI every 7 packets. +const psiSendCount = 7 + // Some common manifestations of PSI. var ( // StandardPAT is a minimal PAT. @@ -72,51 +111,20 @@ var ( } ) -const ( - psiInterval = 1 * time.Second - psiSendCount = 7 -) - // Meta allows addition of metadata to encoded mts from outside of this pkg. // See meta pkg for usage. // // TODO: make this not global. var Meta *meta.Data +// This will help us obtain a realtime for timestamp meta encoding. +var RealTime = realtime.NewRealTime() + var ( patTable = StandardPAT.Bytes() pmtTable []byte ) -const ( - H264ID = 27 - H265ID = 36 - audioStreamID = 0xc0 // First audio stream ID. -) - -// Constants used to communicate which media codec will be packetized. -const ( - EncodeH264 = iota - EncodeH265 - EncodeAudio -) - -// Time-related constants. -const ( - // ptsOffset is the offset added to the clock to determine - // the current presentation timestamp. - ptsOffset = 700 * time.Millisecond - - // PCRFrequency is the base Program Clock Reference frequency in Hz. - PCRFrequency = 90000 - - // PTSFrequency is the presentation timestamp frequency in Hz. - PTSFrequency = 90000 - - // MaxPTS is the largest PTS value (i.e., for a 33-bit unsigned integer). - MaxPTS = (1 << 33) - 1 -) - // Encoder encapsulates properties of an MPEG-TS generator. type Encoder struct { dst io.WriteCloser @@ -130,13 +138,11 @@ type Encoder struct { continuity map[int]byte - timeBasedPsi bool + nalBasedPSI bool pktCount int psiSendCount int mediaPid int streamID byte - - psiLastTime time.Time } // NewEncoder returns an Encoder with the specified media type and rate eg. if a video stream @@ -174,7 +180,7 @@ func NewEncoder(dst io.WriteCloser, rate float64, mediaType int) *Encoder { writePeriod: time.Duration(float64(time.Second) / rate), ptsOffset: ptsOffset, - timeBasedPsi: true, + nalBasedPSI: true, pktCount: 8, @@ -199,12 +205,8 @@ const ( hasPTS = 0x2 ) -// TimeBasedPsi allows for the setting of the PSI writing method, therefore, if -// PSI is written based on some time duration, or based on a packet count. -// If b is true, then time based PSI is used, otherwise the PSI is written -// every sendCount. -func (e *Encoder) TimeBasedPsi(b bool, sendCount int) { - e.timeBasedPsi = b +func (e *Encoder) NALBasedPSI(b bool, sendCount int) { + e.nalBasedPSI = b e.psiSendCount = sendCount e.pktCount = e.psiSendCount } @@ -212,14 +214,24 @@ func (e *Encoder) TimeBasedPsi(b bool, sendCount int) { // Write implements io.Writer. Write takes raw video or audio data and encodes into MPEG-TS, // then sending it to the encoder's io.Writer destination. 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)) { + if e.nalBasedPSI { + nalType, err := h264.NALType(data) + if err != nil { + return 0, fmt.Errorf("could not get type from NAL unit, failed with error: %v", err) + } + + if nalType == h264dec.NALTypeSPS { + err := e.writePSI() + if err != nil { + return 0, err + } + } + } else if e.pktCount >= e.psiSendCount { e.pktCount = 0 err := e.writePSI() if err != nil { return 0, err } - e.psiLastTime = now } // Prepare PES data. @@ -328,6 +340,9 @@ func (e *Encoder) ccFor(pid int) byte { // contained in the global Meta struct. func updateMeta(b []byte) ([]byte, error) { p := psi.PSIBytes(b) + if RealTime.IsSet() { + Meta.Add("ts", strconv.Itoa(int(RealTime.Get().Unix()))) + } err := p.AddDescriptor(psi.MetadataTag, Meta.Encode()) return []byte(p), err } diff --git a/container/mts/mpegts.go b/container/mts/mpegts.go index 8ca509ba..fe31b5ce 100644 --- a/container/mts/mpegts.go +++ b/container/mts/mpegts.go @@ -650,8 +650,11 @@ func SegmentForMeta(d []byte, key, val string) ([][]byte, error) { } // pid returns the packet identifier for the given packet. -func pid(p []byte) uint16 { - return uint16(p[1]&0x1f)<<8 | uint16(p[2]) +func PID(p []byte) (uint16, error) { + if len(p) < PacketSize { + return 0, errors.New("packet length less than 188") + } + return uint16(p[1]&0x1f)<<8 | uint16(p[2]), nil } // Programs returns a map of program numbers and corresponding PMT PIDs for a @@ -683,10 +686,14 @@ func Streams(p []byte) ([]gotspsi.PmtElementaryStream, error) { // but this program may contain different streams, i.e. a video stream + audio // stream. func MediaStreams(p []byte) ([]gotspsi.PmtElementaryStream, error) { + if len(p) < 2*PacketSize { + return nil, errors.New("PSI is not two packets or more long") + } pat := p[:PacketSize] pmt := p[PacketSize : 2*PacketSize] - if pid(pat) != PatPid { + pid, _ := PID(pat) + if pid != PatPid { return nil, errors.New("first packet is not a PAT") } @@ -703,7 +710,8 @@ func MediaStreams(p []byte) ([]gotspsi.PmtElementaryStream, error) { return nil, ErrMultiplePrograms } - if pid(pmt) != pmtPIDs(m)[0] { + pid, _ = PID(pmt) + if pid != pmtPIDs(m)[0] { return nil, errors.New("second packet is not desired PMT") } diff --git a/revid/config.go b/revid/config.go index 42ab2b5d..87fb9139 100644 --- a/revid/config.go +++ b/revid/config.go @@ -27,6 +27,7 @@ package revid import ( "errors" + "time" "bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/utils/logger" @@ -102,15 +103,16 @@ const ( defaultBurstPeriod = 10 // Seconds // Raspivid video defaults. - defaultBrightness = 50 - defaultExposure = "auto" - defaultAutoWhiteBalance = "auto" - defaultRotation = 0 // Degrees - defaultWidth = 1280 - defaultHeight = 720 - defaultIntraRefreshPeriod = 100 - defaultQuantization = 30 - defaultBitrate = 400000 + defaultBrightness = 50 + defaultExposure = "auto" + defaultAutoWhiteBalance = "auto" + defaultRotation = 0 // Degrees + defaultWidth = 1280 + defaultHeight = 720 + defaultMinFrames = 100 + defaultClipDuration = 0 + defaultQuantization = 30 + defaultBitrate = 400000 // Audio defaults. defaultAudioInputCodec = codecutil.ADPCM @@ -202,9 +204,16 @@ type Config struct { // are using Raspivid input. Quantization uint - // IntraRefreshPeriod defines the frequency of video parameter NAL units for - // Raspivid input. - IntraRefreshPeriod uint + // MinFrames defines the frequency of key NAL units SPS, PPS and IDR in + // number of NAL units. This will also determine the frequency of PSI if the + // output container is MPEG-TS. If ClipDuration is less than MinFrames, + // ClipDuration will default to MinFrames. + MinFrames uint + + // ClipDuration is the duration of MTS data that is sent using HTTP or RTP + // output. This defaults to 0, therefore MinFrames will determine the length of + // clips by default. + ClipDuration time.Duration // Logger holds an implementation of the Logger interface as defined in revid.go. // This must be set for revid to work correctly. @@ -383,9 +392,18 @@ func (c *Config) Validate() error { return errors.New("invalid bitrate") } - if c.IntraRefreshPeriod == 0 { - c.Logger.Log(logger.Info, pkg+"no intra refresh defined, defaulting", "intraRefresh", defaultIntraRefreshPeriod) - c.IntraRefreshPeriod = defaultIntraRefreshPeriod + if c.MinFrames == 0 { + c.Logger.Log(logger.Info, pkg+"no min period defined, defaulting", "MinFrames", defaultMinFrames) + c.MinFrames = defaultMinFrames + } else if c.MinFrames < 0 { + return errors.New("refresh period is less than 0") + } + + if c.ClipDuration == 0 { + c.Logger.Log(logger.Info, pkg+"no clip duration defined, defaulting", "ClipDuration", defaultClipDuration) + c.ClipDuration = defaultClipDuration + } else if c.ClipDuration < 0 { + return errors.New("clip duration is less than 0") } if c.Quantization != 0 && (c.Quantization < 10 || c.Quantization > 40) { diff --git a/revid/revid.go b/revid/revid.go index 9e11844a..fa1156b1 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -51,6 +51,7 @@ import ( "bitbucket.org/ausocean/iot/pi/netsender" "bitbucket.org/ausocean/utils/ioext" "bitbucket.org/ausocean/utils/logger" + "bitbucket.org/ausocean/utils/ring" ) // RTMP connection properties. @@ -230,9 +231,8 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. w = newMtsSender( newHttpSender(r.ns, r.config.Logger.Log), r.config.Logger.Log, - r.config.MTSRBSize, - r.config.MTSRBElementSize, - 0, + ring.NewBuffer(r.config.MTSRBSize, r.config.MTSRBElementSize, 0), + r.config.ClipDuration, ) mtsSenders = append(mtsSenders, w) case RTP: @@ -456,13 +456,21 @@ func (r *Revid) Update(vars map[string]string) error { break } r.config.Quantization = uint(v) - case "IntraRefreshPeriod": - p, err := strconv.ParseUint(value, 10, 0) + case "MinFrames": + v, err := strconv.Atoi(value) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid intrarefreshperiod param", "value", value) + r.config.Logger.Log(logger.Warning, pkg+"invalid MinFrames param", "value", value) break } - r.config.IntraRefreshPeriod = uint(p) + r.config.MinFrames = uint(v) + + case "ClipDuration": + v, err := strconv.Atoi(value) + if err != nil { + r.config.Logger.Log(logger.Warning, pkg+"invalid ClipDuration param", "value", value) + break + } + r.config.ClipDuration = time.Duration(v) * time.Second case "HorizontalFlip": switch strings.ToLower(value) { @@ -577,7 +585,7 @@ func (r *Revid) startRaspivid() (func() error, error) { args = append(args, "--codec", "H264", "--inline", - "--intra", fmt.Sprint(r.config.IntraRefreshPeriod), + "--intra", fmt.Sprint(r.config.MinFrames), ) if r.config.Quantization != 0 { args = append(args, "-qp", fmt.Sprint(r.config.Quantization)) diff --git a/revid/senders.go b/revid/senders.go index 303663ec..a6a593bd 100644 --- a/revid/senders.go +++ b/revid/senders.go @@ -29,11 +29,11 @@ LICENSE package revid import ( + "errors" "fmt" "io" "net" "os" - "strconv" "sync" "time" @@ -118,7 +118,7 @@ func extractMeta(r string, log func(lvl int8, msg string, args ...interface{})) log(logger.Warning, pkg+"No timestamp in reply") } else { log(logger.Debug, fmt.Sprintf("%v got timestamp: %v", pkg, t)) - mts.Meta.Add("ts", strconv.Itoa(t)) + mts.RealTime.Set(time.Unix(int64(t), 0)) } // Extract location from reply @@ -156,8 +156,8 @@ func (s *fileSender) Close() error { return s.file.Close() } // mtsSender implements io.WriteCloser and provides sending capability specifically // for use with MPEGTS packetization. It handles the construction of appropriately -// lengthed clips based on PSI. It also accounts for discontinuities by -// setting the discontinuity indicator for the first packet of a clip. +// lengthed clips based on clip duration and PSI. It also accounts for +// discontinuities by setting the discontinuity indicator for the first packet of a clip. type mtsSender struct { dst io.WriteCloser buf []byte @@ -166,19 +166,22 @@ type mtsSender struct { pkt packet.Packet repairer *mts.DiscontinuityRepairer curPid int + clipDur time.Duration + prev time.Time done chan struct{} log func(lvl int8, msg string, args ...interface{}) wg sync.WaitGroup } // newMtsSender returns a new mtsSender. -func newMtsSender(dst io.WriteCloser, log func(lvl int8, msg string, args ...interface{}), ringSize int, ringElementSize int, wTimeout time.Duration) *mtsSender { +func newMtsSender(dst io.WriteCloser, log func(lvl int8, msg string, args ...interface{}), rb *ring.Buffer, clipDur time.Duration) *mtsSender { s := &mtsSender{ dst: dst, repairer: mts.NewDiscontinuityRepairer(), log: log, - ring: ring.NewBuffer(ringSize, ringElementSize, wTimeout), + ring: rb, done: make(chan struct{}), + clipDur: clipDur, } s.wg.Add(1) go s.output() @@ -229,15 +232,20 @@ func (s *mtsSender) output() { // Write implements io.Writer. func (s *mtsSender) Write(d []byte) (int, error) { + if len(d) < mts.PacketSize { + return 0, errors.New("do not have full MTS packet") + } + if s.next != nil { s.buf = append(s.buf, s.next...) } bytes := make([]byte, len(d)) copy(bytes, d) s.next = bytes - copy(s.pkt[:], bytes) - s.curPid = s.pkt.PID() - if s.curPid == mts.PatPid && len(s.buf) > 0 { + p, _ := mts.PID(bytes) + s.curPid = int(p) + if time.Now().Sub(s.prev) >= s.clipDur && s.curPid == mts.PatPid && len(s.buf) > 0 { + s.prev = time.Now() _, err := s.ring.Write(s.buf) if err != nil { s.log(logger.Warning, pkg+"mtsSender: ringBuffer write error", "error", err.Error()) diff --git a/revid/senders_test.go b/revid/senders_test.go index d92f19f4..3fe2f291 100644 --- a/revid/senders_test.go +++ b/revid/senders_test.go @@ -39,6 +39,7 @@ import ( "bitbucket.org/ausocean/av/container/mts" "bitbucket.org/ausocean/av/container/mts/meta" "bitbucket.org/ausocean/utils/logger" + "bitbucket.org/ausocean/utils/ring" ) var ( @@ -133,12 +134,12 @@ func TestMtsSenderSegment(t *testing.T) { // Create ringBuffer, sender, sender and the MPEGTS encoder. const numberOfClips = 11 dst := &destination{t: t, done: make(chan struct{}), doneAt: numberOfClips} - sender := newMtsSender(dst, (*dummyLogger)(t).log, defaultMTSRBSize, defaultMTSRBElementSize, 0) + sender := newMtsSender(dst, (*dummyLogger)(t).log, ring.NewBuffer(defaultMTSRBSize, defaultMTSRBElementSize, 0), 0) encoder := mts.NewEncoder(sender, 25, mts.EncodeH264) // Turn time based PSI writing off for encoder. const psiSendCount = 10 - encoder.TimeBasedPsi(false, psiSendCount) + encoder.NALBasedPSI(false, psiSendCount) // Write the packets to the encoder, which will in turn write to the mtsSender. // Payload will just be packet number. @@ -211,12 +212,12 @@ func TestMtsSenderFailedSend(t *testing.T) { // Create destination, the mtsSender and the mtsEncoder const clipToFailAt = 3 dst := &destination{t: t, testFails: true, failAt: clipToFailAt, done: make(chan struct{})} - sender := newMtsSender(dst, (*dummyLogger)(t).log, defaultMTSRBSize, defaultMTSRBElementSize, 0) + sender := newMtsSender(dst, (*dummyLogger)(t).log, ring.NewBuffer(defaultMTSRBSize, defaultMTSRBElementSize, 0), 0) encoder := mts.NewEncoder(sender, 25, mts.EncodeH264) // Turn time based PSI writing off for encoder and send PSI every 10 packets. const psiSendCount = 10 - encoder.TimeBasedPsi(false, psiSendCount) + encoder.NALBasedPSI(false, psiSendCount) // Write the packets to the encoder, which will in turn write to the mtsSender. // Payload will just be packet number. @@ -291,12 +292,12 @@ func TestMtsSenderDiscontinuity(t *testing.T) { // Create destination, the mtsSender and the mtsEncoder. const clipToDelay = 3 dst := &destination{t: t, sendDelay: 10 * time.Millisecond, delayAt: clipToDelay, done: make(chan struct{})} - sender := newMtsSender(dst, (*dummyLogger)(t).log, 1, defaultMTSRBElementSize, 0) + sender := newMtsSender(dst, (*dummyLogger)(t).log, ring.NewBuffer(1, defaultMTSRBElementSize, 0), 0) encoder := mts.NewEncoder(sender, 25, mts.EncodeH264) // Turn time based PSI writing off for encoder. const psiSendCount = 10 - encoder.TimeBasedPsi(false, psiSendCount) + encoder.NALBasedPSI(false, psiSendCount) // Write the packets to the encoder, which will in turn write to the mtsSender. // Payload will just be packet number.