mirror of
https://github.com/aler9/gortsplib
synced 2025-10-05 07:06:58 +08:00
add h264 utilities
This commit is contained in:
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module github.com/aler9/gortsplib
|
|||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/asticode/go-astits v1.9.0
|
||||||
github.com/icza/bitio v1.0.0
|
github.com/icza/bitio v1.0.0
|
||||||
github.com/pion/rtcp v1.2.4
|
github.com/pion/rtcp v1.2.4
|
||||||
github.com/pion/rtp v1.6.1
|
github.com/pion/rtp v1.6.1
|
||||||
|
7
go.sum
7
go.sum
@@ -1,3 +1,7 @@
|
|||||||
|
github.com/asticode/go-astikit v0.20.0 h1:+7N+J4E4lWx2QOkRdOf6DafWJMv6O4RRfgClwQokrH8=
|
||||||
|
github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
|
github.com/asticode/go-astits v1.9.0 h1:69cilL0/7uwsxdGNQgwmVBu6JP0aMicXSm91ukJDjgQ=
|
||||||
|
github.com/asticode/go-astits v1.9.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8=
|
github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8=
|
||||||
@@ -12,9 +16,11 @@ github.com/pion/rtp v1.6.1 h1:2Y2elcVBrahYnHKN2X7rMHX/r1R4TEBMP1LaVu/wNhk=
|
|||||||
github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||||
github.com/pion/sdp/v3 v3.0.2 h1:UNnSPVaMM+Pdu/mR9UvAyyo6zkdYbKeuOooCwZvTl/g=
|
github.com/pion/sdp/v3 v3.0.2 h1:UNnSPVaMM+Pdu/mR9UvAyyo6zkdYbKeuOooCwZvTl/g=
|
||||||
github.com/pion/sdp/v3 v3.0.2/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
|
github.com/pion/sdp/v3 v3.0.2/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
|
||||||
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
|
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
|
||||||
@@ -27,5 +33,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
79
pkg/h264/annexb.go
Normal file
79
pkg/h264/annexb.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeAnnexB decodes NALUs from the Annex-B stream format.
|
||||||
|
func DecodeAnnexB(byts []byte) ([][]byte, error) {
|
||||||
|
bl := len(byts)
|
||||||
|
|
||||||
|
// check initial delimiter
|
||||||
|
n := func() int {
|
||||||
|
if bl < 3 || byts[0] != 0x00 || byts[1] != 0x00 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if byts[2] == 0x01 {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
if bl < 4 || byts[2] != 0x00 || byts[3] != 0x01 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 4
|
||||||
|
}()
|
||||||
|
if n < 0 {
|
||||||
|
return nil, fmt.Errorf("input doesn't start with a delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret [][]byte
|
||||||
|
zeros := 0
|
||||||
|
start := n
|
||||||
|
delimStart := 0
|
||||||
|
|
||||||
|
for i := n; i < bl; i++ {
|
||||||
|
switch byts[i] {
|
||||||
|
case 0:
|
||||||
|
if zeros == 0 {
|
||||||
|
delimStart = i
|
||||||
|
}
|
||||||
|
zeros++
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
if zeros == 2 || zeros == 3 {
|
||||||
|
nalu := byts[start:delimStart]
|
||||||
|
if len(nalu) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty NALU")
|
||||||
|
}
|
||||||
|
ret = append(ret, nalu)
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
zeros = 0
|
||||||
|
|
||||||
|
default:
|
||||||
|
zeros = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nalu := byts[start:bl]
|
||||||
|
if len(nalu) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty NALU")
|
||||||
|
}
|
||||||
|
ret = append(ret, nalu)
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeAnnexB encodes NALUs into the Annex-B stream format.
|
||||||
|
func EncodeAnnexB(nalus [][]byte) ([]byte, error) {
|
||||||
|
var ret []byte
|
||||||
|
|
||||||
|
for _, nalu := range nalus {
|
||||||
|
ret = append(ret, []byte{0x00, 0x00, 0x00, 0x01}...)
|
||||||
|
ret = append(ret, nalu...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
115
pkg/h264/annexb_test.go
Normal file
115
pkg/h264/annexb_test.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var casesAnnexB = []struct {
|
||||||
|
name string
|
||||||
|
encin []byte
|
||||||
|
encout []byte
|
||||||
|
dec [][]byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"2 zeros, single",
|
||||||
|
[]byte{0x00, 0x00, 0x01, 0xaa, 0xbb},
|
||||||
|
[]byte{0x00, 0x00, 0x00, 0x01, 0xaa, 0xbb},
|
||||||
|
[][]byte{
|
||||||
|
{0xaa, 0xbb},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"2 zeros, multiple",
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x01, 0xaa, 0xbb, 0x00, 0x00, 0x01,
|
||||||
|
0xcc, 0xdd, 0x00, 0x00, 0x01, 0xee, 0xff,
|
||||||
|
},
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0xaa, 0xbb, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0xcc, 0xdd, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0xee, 0xff,
|
||||||
|
},
|
||||||
|
[][]byte{
|
||||||
|
{0xaa, 0xbb},
|
||||||
|
{0xcc, 0xdd},
|
||||||
|
{0xee, 0xff},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"3 zeros, single",
|
||||||
|
[]byte{0x00, 0x00, 0x00, 0x01, 0xaa, 0xbb},
|
||||||
|
[]byte{0x00, 0x00, 0x00, 0x01, 0xaa, 0xbb},
|
||||||
|
[][]byte{
|
||||||
|
{0xaa, 0xbb},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"3 zeros, multiple",
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0xaa, 0xbb, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0xcc, 0xdd, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0xee, 0xff,
|
||||||
|
},
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0xaa, 0xbb, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0xcc, 0xdd, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0xee, 0xff,
|
||||||
|
},
|
||||||
|
[][]byte{
|
||||||
|
{0xaa, 0xbb},
|
||||||
|
{0xcc, 0xdd},
|
||||||
|
{0xee, 0xff},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnnexBDecode(t *testing.T) {
|
||||||
|
for _, ca := range casesAnnexB {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
dec, err := DecodeAnnexB(ca.encin)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ca.dec, dec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnnexBEncode(t *testing.T) {
|
||||||
|
for _, ca := range casesAnnexB {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
enc, err := EncodeAnnexB(ca.dec)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ca.encout, enc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnnexBDecodeError(t *testing.T) {
|
||||||
|
for _, ca := range []struct {
|
||||||
|
name string
|
||||||
|
enc []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty",
|
||||||
|
[]byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"missing initial delimiter",
|
||||||
|
[]byte{0xaa, 0xbb},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty initial",
|
||||||
|
[]byte{0x00, 0x00, 0x01},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"empty 2nd",
|
||||||
|
[]byte{0x00, 0x00, 0x01, 0xaa, 0x00, 0x00, 0x01},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
_, err := DecodeAnnexB(ca.enc)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
90
pkg/h264/anticompetition.go
Normal file
90
pkg/h264/anticompetition.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
// AntiCompetitionAdd adds the anti-competition bytes to a NALU.
|
||||||
|
func AntiCompetitionAdd(nalu []byte) []byte {
|
||||||
|
var ret []byte
|
||||||
|
step := 0
|
||||||
|
start := 0
|
||||||
|
|
||||||
|
for i, b := range nalu {
|
||||||
|
switch step {
|
||||||
|
case 0:
|
||||||
|
if b == 0 {
|
||||||
|
step++
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
if b == 0 {
|
||||||
|
step++
|
||||||
|
} else {
|
||||||
|
step = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
switch b {
|
||||||
|
case 3, 2, 1, 0:
|
||||||
|
ret = append(ret, nalu[start:i-2]...)
|
||||||
|
ret = append(ret, []byte{0x00, 0x00, 0x03, b}...)
|
||||||
|
step = 0
|
||||||
|
start = i + 1
|
||||||
|
|
||||||
|
default:
|
||||||
|
step = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, nalu[start:]...)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// AntiCompetitionRemove removes the anti-competition bytes from a NALU.
|
||||||
|
func AntiCompetitionRemove(nalu []byte) []byte {
|
||||||
|
// 0x00 0x00 0x03 0x00 -> 0x00 0x00 0x00
|
||||||
|
// 0x00 0x00 0x03 0x01 -> 0x00 0x00 0x01
|
||||||
|
// 0x00 0x00 0x03 0x02 -> 0x00 0x00 0x02
|
||||||
|
// 0x00 0x00 0x03 0x03 -> 0x00 0x00 0x03
|
||||||
|
|
||||||
|
var ret []byte
|
||||||
|
step := 0
|
||||||
|
start := 0
|
||||||
|
|
||||||
|
for i, b := range nalu {
|
||||||
|
switch step {
|
||||||
|
case 0:
|
||||||
|
if b == 0 {
|
||||||
|
step++
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
if b == 0 {
|
||||||
|
step++
|
||||||
|
} else {
|
||||||
|
step = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
if b == 3 {
|
||||||
|
step++
|
||||||
|
} else {
|
||||||
|
step = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
switch b {
|
||||||
|
case 3, 2, 1, 0:
|
||||||
|
ret = append(ret, nalu[start:i-3]...)
|
||||||
|
ret = append(ret, []byte{0x00, 0x00, b}...)
|
||||||
|
step = 0
|
||||||
|
start = i + 1
|
||||||
|
|
||||||
|
default:
|
||||||
|
step = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, nalu[start:]...)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
47
pkg/h264/anticompetition_test.go
Normal file
47
pkg/h264/anticompetition_test.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var casesAntiCompetition = []struct {
|
||||||
|
name string
|
||||||
|
unproc []byte
|
||||||
|
proc []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"base",
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x02,
|
||||||
|
0x00, 0x00, 0x03,
|
||||||
|
},
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x03, 0x00,
|
||||||
|
0x00, 0x00, 0x03, 0x01,
|
||||||
|
0x00, 0x00, 0x03, 0x02,
|
||||||
|
0x00, 0x00, 0x03, 0x03,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAntiCompetitionAdd(t *testing.T) {
|
||||||
|
for _, ca := range casesAntiCompetition {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
proc := AntiCompetitionAdd(ca.unproc)
|
||||||
|
require.Equal(t, ca.proc, proc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAntiCompetitionRemove(t *testing.T) {
|
||||||
|
for _, ca := range casesAntiCompetition {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
unproc := AntiCompetitionRemove(ca.proc)
|
||||||
|
require.Equal(t, ca.unproc, unproc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
55
pkg/h264/avcc.go
Normal file
55
pkg/h264/avcc.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeAVCC decodes NALUs from the AVCC stream format.
|
||||||
|
func DecodeAVCC(byts []byte) ([][]byte, error) {
|
||||||
|
var ret [][]byte
|
||||||
|
|
||||||
|
for len(byts) > 0 {
|
||||||
|
if len(byts) < 4 {
|
||||||
|
return nil, fmt.Errorf("invalid length")
|
||||||
|
}
|
||||||
|
|
||||||
|
le := binary.BigEndian.Uint32(byts)
|
||||||
|
byts = byts[4:]
|
||||||
|
|
||||||
|
if len(byts) < int(le) {
|
||||||
|
return nil, fmt.Errorf("invalid length")
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, byts[:le])
|
||||||
|
byts = byts[le:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, fmt.Errorf("no NALUs decoded")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeAVCC encodes NALUs into the AVCC stream format.
|
||||||
|
func EncodeAVCC(nalus [][]byte) ([]byte, error) {
|
||||||
|
le := 0
|
||||||
|
for _, nalu := range nalus {
|
||||||
|
le += 4 + len(nalu)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make([]byte, le)
|
||||||
|
pos := 0
|
||||||
|
|
||||||
|
for _, nalu := range nalus {
|
||||||
|
ln := len(nalu)
|
||||||
|
binary.BigEndian.PutUint32(ret[pos:], uint32(ln))
|
||||||
|
pos += 4
|
||||||
|
|
||||||
|
copy(ret[pos:], nalu)
|
||||||
|
pos += ln
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
85
pkg/h264/avcc_test.go
Normal file
85
pkg/h264/avcc_test.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var casesAVCC = []struct {
|
||||||
|
name string
|
||||||
|
enc []byte
|
||||||
|
dec [][]byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"single",
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x00, 0x03,
|
||||||
|
0xaa, 0xbb, 0xcc,
|
||||||
|
},
|
||||||
|
[][]byte{
|
||||||
|
{0xaa, 0xbb, 0xcc},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple",
|
||||||
|
[]byte{
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0xaa, 0xbb,
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0xcc, 0xdd,
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0xee, 0xff,
|
||||||
|
},
|
||||||
|
[][]byte{
|
||||||
|
{0xaa, 0xbb},
|
||||||
|
{0xcc, 0xdd},
|
||||||
|
{0xee, 0xff},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAVCCDecode(t *testing.T) {
|
||||||
|
for _, ca := range casesAVCC {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
dec, err := DecodeAVCC(ca.enc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ca.dec, dec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAVCCEncode(t *testing.T) {
|
||||||
|
for _, ca := range casesAVCC {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
enc, err := EncodeAVCC(ca.dec)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ca.enc, enc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAVCCDecodeError(t *testing.T) {
|
||||||
|
for _, ca := range []struct {
|
||||||
|
name string
|
||||||
|
enc []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"empty",
|
||||||
|
[]byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid length",
|
||||||
|
[]byte{0x01},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid length",
|
||||||
|
[]byte{0x00, 0x00, 0x00, 0x03},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
_, err := DecodeAVCC(ca.enc)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
61
pkg/h264/dtsestimator.go
Normal file
61
pkg/h264/dtsestimator.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DTSEstimator is a DTS estimator.
|
||||||
|
type DTSEstimator struct {
|
||||||
|
initializing int
|
||||||
|
prevDTS time.Duration
|
||||||
|
prevPTS time.Duration
|
||||||
|
prevPrevPTS time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDTSEstimator allocates a DTSEstimator.
|
||||||
|
func NewDTSEstimator() *DTSEstimator {
|
||||||
|
return &DTSEstimator{
|
||||||
|
initializing: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed provides PTS to the estimator, and returns the estimated DTS.
|
||||||
|
func (d *DTSEstimator) Feed(pts time.Duration) time.Duration {
|
||||||
|
switch d.initializing {
|
||||||
|
case 2:
|
||||||
|
d.initializing--
|
||||||
|
return 0
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
d.initializing--
|
||||||
|
d.prevPTS = pts
|
||||||
|
d.prevDTS = time.Millisecond
|
||||||
|
return time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
dts := func() time.Duration {
|
||||||
|
// P or I frame
|
||||||
|
if pts > d.prevPTS {
|
||||||
|
// previous frame was B
|
||||||
|
// use the DTS of the previous frame
|
||||||
|
if d.prevPTS < d.prevPrevPTS {
|
||||||
|
return d.prevPTS
|
||||||
|
}
|
||||||
|
|
||||||
|
// previous frame was P or I
|
||||||
|
// use two frames ago plus a small quantity
|
||||||
|
// to avoid non-monotonous DTS with B-frames
|
||||||
|
return d.prevPrevPTS + time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
// B Frame
|
||||||
|
// increase by a small quantity
|
||||||
|
return d.prevDTS + time.Millisecond
|
||||||
|
}()
|
||||||
|
|
||||||
|
d.prevPrevPTS = d.prevPTS
|
||||||
|
d.prevPTS = pts
|
||||||
|
d.prevDTS = dts
|
||||||
|
|
||||||
|
return dts
|
||||||
|
}
|
32
pkg/h264/dtsestimator_test.go
Normal file
32
pkg/h264/dtsestimator_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDTSEstimator(t *testing.T) {
|
||||||
|
est := NewDTSEstimator()
|
||||||
|
|
||||||
|
// initial state
|
||||||
|
dts := est.Feed(0)
|
||||||
|
require.Equal(t, time.Duration(0), dts)
|
||||||
|
|
||||||
|
// b-frame
|
||||||
|
dts = est.Feed(1*time.Second - 200*time.Millisecond)
|
||||||
|
require.Equal(t, time.Millisecond, dts)
|
||||||
|
|
||||||
|
// b-frame
|
||||||
|
dts = est.Feed(1*time.Second - 400*time.Millisecond)
|
||||||
|
require.Equal(t, 2*time.Millisecond, dts)
|
||||||
|
|
||||||
|
// p-frame
|
||||||
|
dts = est.Feed(1 * time.Second)
|
||||||
|
require.Equal(t, 1*time.Second-400*time.Millisecond, dts)
|
||||||
|
|
||||||
|
// p-frame
|
||||||
|
dts = est.Feed(1*time.Second + 200*time.Millisecond)
|
||||||
|
require.Equal(t, 1*time.Second-399*time.Millisecond, dts)
|
||||||
|
}
|
2
pkg/h264/h264.go
Normal file
2
pkg/h264/h264.go
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Package h264 contains utilities to work with H264 streams.
|
||||||
|
package h264
|
69
pkg/h264/nalutype.go
Normal file
69
pkg/h264/nalutype.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NALUType is the type of a NALU.
|
||||||
|
type NALUType uint8
|
||||||
|
|
||||||
|
// standard NALU types.
|
||||||
|
const (
|
||||||
|
NALUTypeNonIDR NALUType = 1
|
||||||
|
NALUTypeDataPartitionA NALUType = 2
|
||||||
|
NALUTypeDataPartitionB NALUType = 3
|
||||||
|
NALUTypeDataPartitionC NALUType = 4
|
||||||
|
NALUTypeIDR NALUType = 5
|
||||||
|
NALUTypeSEI NALUType = 6
|
||||||
|
NALUTypeSPS NALUType = 7
|
||||||
|
NALUTypePPS NALUType = 8
|
||||||
|
NALUTypeAccessUnitDelimiter NALUType = 9
|
||||||
|
NALUTypeEndOfSequence NALUType = 10
|
||||||
|
NALUTypeEndOfStream NALUType = 11
|
||||||
|
NALUTypeFillerData NALUType = 12
|
||||||
|
NALUTypeSPSExtension NALUType = 13
|
||||||
|
NALUTypePrefix NALUType = 14
|
||||||
|
NALUTypeSubsetSPS NALUType = 15
|
||||||
|
NALUTypeReserved16 NALUType = 16
|
||||||
|
NALUTypeReserved17 NALUType = 17
|
||||||
|
NALUTypeReserved18 NALUType = 18
|
||||||
|
NALUTypeSliceLayerWithoutPartitioning NALUType = 19
|
||||||
|
NALUTypeSliceExtension NALUType = 20
|
||||||
|
NALUTypeSliceExtensionDepth NALUType = 21
|
||||||
|
NALUTypeReserved22 NALUType = 22
|
||||||
|
NALUTypeReserved23 NALUType = 23
|
||||||
|
)
|
||||||
|
|
||||||
|
var naluTypelabels = map[NALUType]string{
|
||||||
|
NALUTypeNonIDR: "NonIDR",
|
||||||
|
NALUTypeDataPartitionA: "DataPartitionA",
|
||||||
|
NALUTypeDataPartitionB: "DataPartitionB",
|
||||||
|
NALUTypeDataPartitionC: "DataPartitionC",
|
||||||
|
NALUTypeIDR: "IDR",
|
||||||
|
NALUTypeSEI: "SEI",
|
||||||
|
NALUTypeSPS: "SPS",
|
||||||
|
NALUTypePPS: "PPS",
|
||||||
|
NALUTypeAccessUnitDelimiter: "AccessUnitDelimiter",
|
||||||
|
NALUTypeEndOfSequence: "EndOfSequence",
|
||||||
|
NALUTypeEndOfStream: "EndOfStream",
|
||||||
|
NALUTypeFillerData: "FillerData",
|
||||||
|
NALUTypeSPSExtension: "SPSExtension",
|
||||||
|
NALUTypePrefix: "Prefix",
|
||||||
|
NALUTypeSubsetSPS: "SubsetSPS",
|
||||||
|
NALUTypeReserved16: "Reserved16",
|
||||||
|
NALUTypeReserved17: "Reserved17",
|
||||||
|
NALUTypeReserved18: "Reserved18",
|
||||||
|
NALUTypeSliceLayerWithoutPartitioning: "SliceLayerWithoutPartitioning",
|
||||||
|
NALUTypeSliceExtension: "SliceExtension",
|
||||||
|
NALUTypeSliceExtensionDepth: "SliceExtensionDepth",
|
||||||
|
NALUTypeReserved22: "Reserved22",
|
||||||
|
NALUTypeReserved23: "Reserved23",
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer.
|
||||||
|
func (nt NALUType) String() string {
|
||||||
|
if l, ok := naluTypelabels[nt]; ok {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("unknown (%d)", nt)
|
||||||
|
}
|
13
pkg/h264/nalutype_test.go
Normal file
13
pkg/h264/nalutype_test.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package h264
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNALUType(t *testing.T) {
|
||||||
|
require.NotEqual(t, true, strings.HasPrefix(NALUType(10).String(), "unknown"))
|
||||||
|
require.Equal(t, true, strings.HasPrefix(NALUType(50).String(), "unknown"))
|
||||||
|
}
|
@@ -38,7 +38,7 @@ func (d *Decoder) decodeTimestamp(ts uint32) time.Duration {
|
|||||||
// It returns the AUs and the PTS of the first AU.
|
// It returns the AUs and the PTS of the first AU.
|
||||||
// The PTS of subsequent AUs can be calculated by adding time.Second*1000/clockRate.
|
// The PTS of subsequent AUs can be calculated by adding time.Second*1000/clockRate.
|
||||||
func (d *Decoder) Decode(byts []byte) ([][]byte, time.Duration, error) {
|
func (d *Decoder) Decode(byts []byte) ([][]byte, time.Duration, error) {
|
||||||
pkt := rtp.Packet{}
|
var pkt rtp.Packet
|
||||||
err := pkt.Unmarshal(byts)
|
err := pkt.Unmarshal(byts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.isDecodingFragmented = false
|
d.isDecodingFragmented = false
|
||||||
|
@@ -9,6 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
|
||||||
|
"github.com/aler9/gortsplib/pkg/h264"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrMorePacketsNeeded is returned when more packets are needed.
|
// ErrMorePacketsNeeded is returned when more packets are needed.
|
||||||
@@ -55,7 +57,7 @@ func (d *Decoder) decodeTimestamp(ts uint32) time.Duration {
|
|||||||
// Decode decodes NALUs from a RTP/H264 packet.
|
// Decode decodes NALUs from a RTP/H264 packet.
|
||||||
// It returns the decoded NALUs and their PTS.
|
// It returns the decoded NALUs and their PTS.
|
||||||
func (d *Decoder) Decode(byts []byte) ([][]byte, time.Duration, error) {
|
func (d *Decoder) Decode(byts []byte) ([][]byte, time.Duration, error) {
|
||||||
pkt := rtp.Packet{}
|
var pkt rtp.Packet
|
||||||
err := pkt.Unmarshal(byts)
|
err := pkt.Unmarshal(byts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.isDecodingFragmented = false
|
d.isDecodingFragmented = false
|
||||||
@@ -197,13 +199,13 @@ func (d *Decoder) ReadSPSPPS(r io.Reader) ([]byte, []byte, error) {
|
|||||||
|
|
||||||
for _, nalu := range nalus {
|
for _, nalu := range nalus {
|
||||||
switch naluType(nalu[0] & 0x1F) {
|
switch naluType(nalu[0] & 0x1F) {
|
||||||
case naluTypeSPS:
|
case naluType(h264.NALUTypeSPS):
|
||||||
sps = append([]byte(nil), nalu...)
|
sps = append([]byte(nil), nalu...)
|
||||||
if sps != nil && pps != nil {
|
if sps != nil && pps != nil {
|
||||||
return sps, pps, nil
|
return sps, pps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
case naluTypePPS:
|
case naluType(h264.NALUTypePPS):
|
||||||
pps = append([]byte(nil), nalu...)
|
pps = append([]byte(nil), nalu...)
|
||||||
if sps != nil && pps != nil {
|
if sps != nil && pps != nil {
|
||||||
return sps, pps, nil
|
return sps, pps, nil
|
||||||
|
@@ -1,33 +1,16 @@
|
|||||||
package rtph264
|
package rtph264
|
||||||
|
|
||||||
// naluType is the type of a NALU.
|
import (
|
||||||
type naluType uint8
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
// NALU types, augmented for RTP.
|
"github.com/aler9/gortsplib/pkg/h264"
|
||||||
|
)
|
||||||
|
|
||||||
|
type naluType h264.NALUType
|
||||||
|
|
||||||
|
// additional NALU types for RTP/H264.
|
||||||
const (
|
const (
|
||||||
naluTypeNonIDR naluType = 1
|
|
||||||
naluTypeDataPartitionA naluType = 2
|
|
||||||
naluTypeDataPartitionB naluType = 3
|
|
||||||
naluTypeDataPartitionC naluType = 4
|
|
||||||
naluTypeIDR naluType = 5
|
|
||||||
naluTypeSEI naluType = 6
|
|
||||||
naluTypeSPS naluType = 7
|
|
||||||
naluTypePPS naluType = 8
|
|
||||||
naluTypeAccessUnitDelimiter naluType = 9
|
|
||||||
naluTypeEndOfSequence naluType = 10
|
|
||||||
naluTypeEndOfStream naluType = 11
|
|
||||||
naluTypeFillerData naluType = 12
|
|
||||||
naluTypeSPSExtension naluType = 13
|
|
||||||
naluTypePrefix naluType = 14
|
|
||||||
naluTypeSubsetSPS naluType = 15
|
|
||||||
naluTypeReserved16 naluType = 16
|
|
||||||
naluTypeReserved17 naluType = 17
|
|
||||||
naluTypeReserved18 naluType = 18
|
|
||||||
naluTypeSliceLayerWithoutPartitioning naluType = 19
|
|
||||||
naluTypeSliceExtension naluType = 20
|
|
||||||
naluTypeSliceExtensionDepth naluType = 21
|
|
||||||
naluTypeReserved22 naluType = 22
|
|
||||||
naluTypeReserved23 naluType = 23
|
|
||||||
naluTypeSTAPA naluType = 24
|
naluTypeSTAPA naluType = 24
|
||||||
naluTypeSTAPB naluType = 25
|
naluTypeSTAPB naluType = 25
|
||||||
naluTypeMTAP16 naluType = 26
|
naluTypeMTAP16 naluType = 26
|
||||||
@@ -37,41 +20,24 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var naluLabels = map[naluType]string{
|
var naluLabels = map[naluType]string{
|
||||||
naluTypeNonIDR: "NonIDR",
|
naluTypeSTAPA: "STAP-A",
|
||||||
naluTypeDataPartitionA: "DataPartitionA",
|
naluTypeSTAPB: "STAP-B",
|
||||||
naluTypeDataPartitionB: "DataPartitionB",
|
naluTypeMTAP16: "MTAP-16",
|
||||||
naluTypeDataPartitionC: "DataPartitionC",
|
naluTypeMTAP24: "MTAP-24",
|
||||||
naluTypeIDR: "IDR",
|
naluTypeFUA: "FU-A",
|
||||||
naluTypeSEI: "SEI",
|
naluTypeFUB: "FU-B",
|
||||||
naluTypeSPS: "SPS",
|
|
||||||
naluTypePPS: "PPS",
|
|
||||||
naluTypeAccessUnitDelimiter: "AccessUnitDelimiter",
|
|
||||||
naluTypeEndOfSequence: "EndOfSequence",
|
|
||||||
naluTypeEndOfStream: "EndOfStream",
|
|
||||||
naluTypeFillerData: "FillerData",
|
|
||||||
naluTypeSPSExtension: "SPSExtension",
|
|
||||||
naluTypePrefix: "Prefix",
|
|
||||||
naluTypeSubsetSPS: "SubsetSPS",
|
|
||||||
naluTypeReserved16: "Reserved16",
|
|
||||||
naluTypeReserved17: "Reserved17",
|
|
||||||
naluTypeReserved18: "Reserved18",
|
|
||||||
naluTypeSliceLayerWithoutPartitioning: "SliceLayerWithoutPartitioning",
|
|
||||||
naluTypeSliceExtension: "SliceExtension",
|
|
||||||
naluTypeSliceExtensionDepth: "SliceExtensionDepth",
|
|
||||||
naluTypeReserved22: "Reserved22",
|
|
||||||
naluTypeReserved23: "Reserved23",
|
|
||||||
naluTypeSTAPA: "STAPA",
|
|
||||||
naluTypeSTAPB: "STAPB",
|
|
||||||
naluTypeMTAP16: "MTAP16",
|
|
||||||
naluTypeMTAP24: "MTAP24",
|
|
||||||
naluTypeFUA: "FUA",
|
|
||||||
naluTypeFUB: "FUB",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
// String implements fmt.Stringer.
|
||||||
func (nt naluType) String() string {
|
func (nt naluType) String() string {
|
||||||
|
p := h264.NALUType(nt).String()
|
||||||
|
if !strings.HasPrefix(p, "unknown") {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
if l, ok := naluLabels[nt]; ok {
|
if l, ok := naluLabels[nt]; ok {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
return "unknown"
|
|
||||||
|
return fmt.Sprintf("unknown (%d)", nt)
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
package rtph264
|
package rtph264
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNALUType(t *testing.T) {
|
func TestNALUType(t *testing.T) {
|
||||||
require.NotEqual(t, "unknown", naluType(10).String())
|
require.NotEqual(t, true, strings.HasPrefix(naluType(10).String(), "unknown"))
|
||||||
require.Equal(t, "unknown", naluType(50).String())
|
require.NotEqual(t, true, strings.HasPrefix(naluType(26).String(), "unknown"))
|
||||||
|
require.Equal(t, true, strings.HasPrefix(naluType(50).String(), "unknown"))
|
||||||
}
|
}
|
||||||
|
@@ -431,7 +431,7 @@ func TestDecodeErrors(t *testing.T) {
|
|||||||
0x80, 0xe0, 0x44, 0xed, 0x88, 0x77, 0x6a, 0x15,
|
0x80, 0xe0, 0x44, 0xed, 0x88, 0x77, 0x6a, 0x15,
|
||||||
0x9d, 0xbb, 0x78, 0x12, byte(naluTypeMTAP16),
|
0x9d, 0xbb, 0x78, 0x12, byte(naluTypeMTAP16),
|
||||||
}},
|
}},
|
||||||
"packet type not supported (MTAP16)",
|
"packet type not supported (MTAP-16)",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(ca.name, func(t *testing.T) {
|
t.Run(ca.name, func(t *testing.T) {
|
||||||
|
Reference in New Issue
Block a user