RTCP: Add BYE packet type

Relates to #119
This commit is contained in:
Max Hawkins
2018-09-15 17:47:48 -07:00
committed by Sean DuBois
parent 178fb16977
commit 426b2a1fbc
7 changed files with 377 additions and 19 deletions

16
pkg/rtcp/errors.go Normal file
View File

@@ -0,0 +1,16 @@
package rtcp
import "errors"
var (
errInvalidTotalLost = errors.New("rtcp: invalid total lost count")
errInvalidHeader = errors.New("rtcp: invalid header")
errTooManyReports = errors.New("rtcp: too many reports")
errTooManyChunks = errors.New("rtcp: too many chunks")
errTooManySources = errors.New("rtcp: too many sources")
errPacketTooShort = errors.New("rtcp: packet too short")
errWrongType = errors.New("rtcp: wrong packet type")
errSDESTextTooLong = errors.New("rtcp: sdes must be < 255 octets long")
errSDESMissingType = errors.New("rtcp: sdes item missing type")
errReasonTooLong = errors.New("rtcp: reason must be < 255 octets long")
)

133
pkg/rtcp/goodbye.go Normal file
View File

@@ -0,0 +1,133 @@
package rtcp
import (
"encoding/binary"
)
// The Goodbye packet indicates that one or more sources are no longer active.
type Goodbye struct {
// The SSRC/CSRC identifiers that are no longer active
Sources []uint32
// Optional text indicating the reason for leaving, e.g., "camera malfunction" or "RTP loop detected"
Reason string
}
// Marshal encodes the Goodbye packet in binary
func (g Goodbye) Marshal() ([]byte, error) {
/*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |V=2|P| SC | PT=BYE=203 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC/CSRC |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* : ... :
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* (opt) | length | reason for leaving ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
rawPacket := make([]byte, len(g.Sources)*ssrcLength)
if len(g.Sources) > countMax {
return nil, errTooManySources
}
for i, s := range g.Sources {
binary.BigEndian.PutUint32(rawPacket[i*ssrcLength:], s)
}
if g.Reason != "" {
reason := []byte(g.Reason)
if len(reason) > sdesMaxOctetCount {
return nil, errReasonTooLong
}
rawPacket = append(rawPacket, uint8(len(reason)))
rawPacket = append(rawPacket, reason...)
// align to 32-bit boundary
if len(rawPacket)%4 != 0 {
padCount := 4 - len(rawPacket)%4
rawPacket = append(rawPacket, make([]byte, padCount)...)
}
}
h := Header{
Version: 2,
Padding: false,
Count: uint8(len(g.Sources)),
Type: TypeGoodbye,
Length: uint16(headerLength + len(rawPacket)),
}
hData, err := h.Marshal()
if err != nil {
return nil, err
}
rawPacket = append(hData, rawPacket...)
return rawPacket, nil
}
// Unmarshal decodes the Goodbye packet from binary
func (g *Goodbye) Unmarshal(rawPacket []byte) error {
/*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |V=2|P| SC | PT=BYE=203 | length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SSRC/CSRC |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* : ... :
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* (opt) | length | reason for leaving ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
var header Header
if err := header.Unmarshal(rawPacket); err != nil {
return err
}
if header.Type != TypeGoodbye {
return errWrongType
}
if len(rawPacket)%4 != 0 {
return errPacketTooShort
}
g.Sources = make([]uint32, header.Count)
reasonOffset := int(headerLength + header.Count*ssrcLength)
if reasonOffset > len(rawPacket) {
return errPacketTooShort
}
for i := 0; i < int(header.Count); i++ {
offset := headerLength + i*ssrcLength
if offset > len(rawPacket) {
return errPacketTooShort
}
g.Sources[i] = binary.BigEndian.Uint32(rawPacket[offset:])
}
if reasonOffset < len(rawPacket) {
reasonLen := int(rawPacket[reasonOffset])
reasonEnd := reasonOffset + 1 + reasonLen
if reasonEnd > len(rawPacket) {
return errPacketTooShort
}
g.Reason = string(rawPacket[reasonOffset+1 : reasonEnd])
}
return nil
}

207
pkg/rtcp/goodbye_test.go Normal file
View File

@@ -0,0 +1,207 @@
package rtcp
import (
"reflect"
"testing"
)
func TestGoodbyeUnmarshal(t *testing.T) {
for _, test := range []struct {
Name string
Data []byte
Want Goodbye
WantError error
}{
{
Name: "valid",
Data: []byte{
// v=2, p=0, count=1, BYE, len=12
0x81, 0xcb, 0x00, 0x0c,
// ssrc=0x902f9e2e
0x90, 0x2f, 0x9e, 0x2e,
// len=3, text=FOO
0x03, 0x46, 0x4f, 0x4f,
},
Want: Goodbye{
Sources: []uint32{0x902f9e2e},
Reason: "FOO",
},
},
{
Name: "invalid octet count",
Data: []byte{
// v=2, p=0, count=1, BYE, len=12
0x81, 0xcb, 0x00, 0x0c,
// ssrc=0x902f9e2e
0x90, 0x2f, 0x9e, 0x2e,
// len=4, text=FOO
0x04, 0x46, 0x4f, 0x4f,
},
WantError: errPacketTooShort,
},
{
Name: "wrong type",
Data: []byte{
// v=2, p=0, count=1, SDES, len=12
0x81, 0xca, 0x00, 0x0c,
// ssrc=0x902f9e2e
0x90, 0x2f, 0x9e, 0x2e,
// len=3, text=FOO
0x03, 0x46, 0x4f, 0x4f,
},
WantError: errWrongType,
},
{
Name: "short reason",
Data: []byte{
// v=2, p=0, count=1, BYE, len=12
0x81, 0xcb, 0x00, 0x0c,
// ssrc=0x902f9e2e
0x90, 0x2f, 0x9e, 0x2e,
// len=3, text=F + padding
0x01, 0x46, 0x00, 0x00,
},
Want: Goodbye{
Sources: []uint32{0x902f9e2e},
Reason: "F",
},
},
{
Name: "not byte aligned",
Data: []byte{
// v=2, p=0, count=1, BYE, len=10
0x81, 0xcb, 0x00, 0x0a,
// ssrc=0x902f9e2e
0x90, 0x2f, 0x9e, 0x2e,
// len=1, text=F
0x01, 0x46,
},
WantError: errPacketTooShort,
},
{
Name: "bad count in header",
Data: []byte{
// v=2, p=0, count=2, BYE, len=8
0x82, 0xcb, 0x00, 0x0c,
// ssrc=0x902f9e2e
0x90, 0x2f, 0x9e, 0x2e,
},
WantError: errPacketTooShort,
},
{
Name: "empty packet",
Data: []byte{
// v=2, p=0, count=0, BYE, len=4
0x80, 0xcb, 0x00, 0x04,
},
Want: Goodbye{
Sources: []uint32{},
Reason: "",
},
},
{
Name: "nil",
Data: nil,
WantError: errInvalidHeader,
},
} {
var bye Goodbye
err := bye.Unmarshal(test.Data)
if got, want := err, test.WantError; got != want {
t.Fatalf("Unmarshal %q bye: err = %v, want %v", test.Name, got, want)
}
if err != nil {
continue
}
if got, want := bye, test.Want; !reflect.DeepEqual(got, want) {
t.Fatalf("Unmarshal %q bye: got %v, want %v", test.Name, got, want)
}
}
}
func TestGoodbyeRoundTrip(t *testing.T) {
for _, test := range []struct {
Name string
Bye Goodbye
WantError error
}{
{
Name: "empty",
Bye: Goodbye{
Sources: []uint32{},
},
},
{
Name: "valid",
Bye: Goodbye{
Sources: []uint32{
0x01020304,
0x05060708,
},
Reason: "because",
},
},
{
Name: "empty reason",
Bye: Goodbye{
Sources: []uint32{0x01020304},
Reason: "",
},
},
{
Name: "reason no source",
Bye: Goodbye{
Sources: []uint32{},
Reason: "foo",
},
},
{
Name: "short reason",
Bye: Goodbye{
Sources: []uint32{},
Reason: "f",
},
},
{
Name: "count overflow",
Bye: Goodbye{
Sources: tooManySources,
},
WantError: errTooManySources,
},
{
Name: "reason too long",
Bye: Goodbye{
Sources: []uint32{},
Reason: tooLongText,
},
WantError: errReasonTooLong,
},
} {
data, err := test.Bye.Marshal()
if got, want := err, test.WantError; got != want {
t.Fatalf("Marshal %q: err = %v, want %v", test.Name, got, want)
}
if err != nil {
continue
}
var bye Goodbye
if err := bye.Unmarshal(data); err != nil {
t.Fatalf("Unmarshal %q: %v", test.Name, err)
}
if got, want := bye, test.Bye; !reflect.DeepEqual(got, want) {
t.Fatalf("%q sdes round trip: got %#v, want %#v", test.Name, got, want)
}
}
}
// a slice with enough sources to overflow an 5-bit int
var tooManySources []uint32
func init() {
for i := 0; i < (1 << 5); i++ {
tooManySources = append(tooManySources, 0x00)
}
}

View File

@@ -2,8 +2,6 @@ package rtcp
import ( import (
"encoding/binary" "encoding/binary"
"github.com/pkg/errors"
) )
// PacketType specifies the type of an RTCP packet // PacketType specifies the type of an RTCP packet
@@ -55,15 +53,6 @@ type Header struct {
Length uint16 Length uint16
} }
var (
errInvalidTotalLost = errors.New("rtcp: invalid total lost count")
errInvalidHeader = errors.New("rtcp: invalid header")
errTooManyReports = errors.New("rtcp: too many reports")
errTooManyChunks = errors.New("rtcp: too many chunks")
errPacketTooShort = errors.New("rtcp: packet too short")
errWrongType = errors.New("rtcp: wrong packet type")
)
const ( const (
headerLength = 4 headerLength = 4
versionShift = 6 versionShift = 6

View File

@@ -107,4 +107,20 @@ func TestUnmarshal(t *testing.T) {
if got, want := sdes, wantSdes; !reflect.DeepEqual(got, want) { if got, want := sdes, wantSdes; !reflect.DeepEqual(got, want) {
t.Errorf("Unmarshal sdes: got %#v, want %#v", got, want) t.Errorf("Unmarshal sdes: got %#v, want %#v", got, want)
} }
// Goodbye
_, packet, err = r.ReadPacket()
if err != nil {
t.Fatalf("Read bye: %v", err)
}
var bye Goodbye
if err := bye.Unmarshal(packet); err != nil {
t.Errorf("Unmarshal bye: %v", err)
}
wantBye := Goodbye{
Sources: []uint32{0x902f9e2e},
}
if got, want := bye, wantBye; !reflect.DeepEqual(got, want) {
t.Errorf("Unmarshal bye: got %#v, want %#v", got, want)
}
} }

View File

@@ -2,8 +2,6 @@ package rtcp
import ( import (
"encoding/binary" "encoding/binary"
"github.com/pkg/errors"
) )
// SDESType is the item type used in the RTCP SDES control packet. // SDESType is the item type used in the RTCP SDES control packet.
@@ -47,11 +45,6 @@ func (s SDESType) String() string {
} }
} }
var (
errSDESTextTooLong = errors.New("session description must be < 255 octets long")
errSDESMissingType = errors.New("session description item missing type")
)
const ( const (
sdesSourceLen = 4 sdesSourceLen = 4
sdesTypeLen = 1 sdesTypeLen = 1

View File

@@ -323,7 +323,7 @@ func TestSourceDescriptionRoundTrip(t *testing.T) {
Chunks: []SourceDescriptionChunk{{ Chunks: []SourceDescriptionChunk{{
Items: []SourceDescriptionItem{{ Items: []SourceDescriptionItem{{
Type: SDESCNAME, Type: SDESCNAME,
Text: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", Text: tooLongText,
}}, }},
}}, }},
}, },
@@ -358,9 +358,13 @@ func TestSourceDescriptionRoundTrip(t *testing.T) {
// a slice with enough SourceDescriptionChunks to overflow an 5-bit int // a slice with enough SourceDescriptionChunks to overflow an 5-bit int
var tooManyChunks []SourceDescriptionChunk var tooManyChunks []SourceDescriptionChunk
var tooLongText string
func init() { func init() {
for i := 0; i < (1 << 5); i++ { for i := 0; i < (1 << 5); i++ {
tooManyChunks = append(tooManyChunks, SourceDescriptionChunk{}) tooManyChunks = append(tooManyChunks, SourceDescriptionChunk{})
} }
for i := 0; i < (1 << 8); i++ {
tooLongText += "x"
}
} }