RTCP: add SDES packet type

Relates to #119
This commit is contained in:
Max Hawkins
2018-09-12 12:44:14 -07:00
committed by Sean DuBois
parent 3fee11e486
commit 485b4776b5
4 changed files with 593 additions and 30 deletions

View File

@@ -24,8 +24,8 @@ type Header struct {
// some additional padding octets at the end which are not part of
// the control information but are included in the length field.
Padding bool
// The number of reception report blocks contained in this packet.
ReportCount uint8
// The number of reception reports or sources contained in this packet (depending on the Type)
Count uint8
// The RTCP packet type for this packet
Type uint8
// The length of this RTCP packet in 32-bit words minus one,
@@ -35,7 +35,7 @@ type Header struct {
var (
errInvalidVersion = errors.New("invalid version")
errInvalidReportCount = errors.New("invalid report count")
errInvalidCount = errors.New("invalid count header")
errInvalidTotalLost = errors.New("invalid total lost count")
errPacketTooShort = errors.New("packet too short")
)
@@ -46,8 +46,8 @@ const (
versionMask = 0x3
paddingShift = 5
paddingMask = 0x1
reportCountShift = 0
reportCountMask = 0x1f
countShift = 0
countMask = 0x1f
)
// Marshal encodes the Header in binary
@@ -70,10 +70,10 @@ func (h Header) Marshal() ([]byte, error) {
rawPacket[0] |= 1 << paddingShift
}
if h.ReportCount > 31 {
return nil, errInvalidReportCount
if h.Count > 31 {
return nil, errInvalidCount
}
rawPacket[0] |= h.ReportCount << reportCountShift
rawPacket[0] |= h.Count << countShift
rawPacket[1] = h.Type
@@ -98,7 +98,7 @@ func (h *Header) Unmarshal(rawPacket []byte) error {
h.Version = rawPacket[0] >> versionShift & versionMask
h.Padding = (rawPacket[0] >> paddingShift & paddingMask) > 0
h.ReportCount = rawPacket[0] >> reportCountShift & reportCountMask
h.Count = rawPacket[0] >> countShift & countMask
h.Type = rawPacket[1]

View File

@@ -48,7 +48,7 @@ func TestHeaderRoundTrip(t *testing.T) {
Header: Header{
Version: 2,
Padding: true,
ReportCount: 31,
Count: 31,
Type: TypeSenderReport,
Length: 4,
},
@@ -58,7 +58,7 @@ func TestHeaderRoundTrip(t *testing.T) {
Header: Header{
Version: 1,
Padding: false,
ReportCount: 28,
Count: 28,
Type: TypeReceiverReport,
Length: 65535,
},
@@ -71,11 +71,11 @@ func TestHeaderRoundTrip(t *testing.T) {
WantError: errInvalidVersion,
},
{
Name: "invalid report count",
Name: "invalid count",
Header: Header{
ReportCount: 40,
Count: 40,
},
WantError: errInvalidReportCount,
WantError: errInvalidCount,
},
} {
data, err := test.Header.Marshal()

View File

@@ -0,0 +1,264 @@
package rtcp
import (
"encoding/binary"
"github.com/pkg/errors"
)
// RTP SDES item types registered with IANA. See: https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-5
const (
SDESEnd = iota // end of SDES list RFC 3550, 6.5
SDESCNAME // canonical name RFC 3550, 6.5.1
SDESName // user name RFC 3550, 6.5.2
SDESEmail // user's electronic mail address RFC 3550, 6.5.3
SDESPhone // user's phone number RFC 3550, 6.5.4
SDESLocation // geographic user location RFC 3550, 6.5.5
SDESTool // name of application or tool RFC 3550, 6.5.6
SDESNote // notice about the source RFC 3550, 6.5.7
SDESPrivate // private extensions RFC 3550, 6.5.8 (not implemented)
)
var (
errSDESTextTooLong = errors.New("session description must be < 255 octets long")
errSDESMissingType = errors.New("session description item missing type")
)
const (
sdesSourceLen = 4
sdesTypeLen = 1
sdesTypeOffset = 0
sdesOctetCountLen = 1
sdesOctetCountOffset = 1
sdesMaxOctetCount = (1 << 8) - 1
sdesTextOffset = 2
)
// A SourceDescription (SDES) packet describes the sources in an RTP stream.
type SourceDescription struct {
Chunks []SourceDescriptionChunk
}
// Marshal encodes the SourceDescription in binary
func (s SourceDescription) Marshal() ([]byte, error) {
/*
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* chunk | SSRC/CSRC_1 |
* 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SDES items |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* chunk | SSRC/CSRC_2 |
* 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SDES items |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
*/
rawPacket := make([]byte, 0)
for _, c := range s.Chunks {
data, err := c.Marshal()
if err != nil {
return nil, err
}
rawPacket = append(rawPacket, data...)
}
return rawPacket, nil
}
// Unmarshal decodes the SourceDescription from binary
func (s *SourceDescription) Unmarshal(rawPacket []byte) error {
/*
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* chunk | SSRC/CSRC_1 |
* 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SDES items |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* chunk | SSRC/CSRC_2 |
* 2 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SDES items |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
*/
for i := 0; i < len(rawPacket); {
var chunk SourceDescriptionChunk
if err := chunk.Unmarshal(rawPacket[i:]); err != nil {
return err
}
s.Chunks = append(s.Chunks, chunk)
i += chunk.len()
}
return nil
}
// A SourceDescriptionChunk contains items describing a single RTP source
type SourceDescriptionChunk struct {
// The source (ssrc) or contributing source (csrc) identifier this packet describes
Source uint32
Items []SourceDescriptionItem
}
// Marshal encodes the SourceDescriptionChunk in binary
func (s SourceDescriptionChunk) Marshal() ([]byte, error) {
/*
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | SSRC/CSRC_1 |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SDES items |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
*/
rawPacket := make([]byte, sdesSourceLen)
binary.BigEndian.PutUint32(rawPacket, s.Source)
for _, it := range s.Items {
data, err := it.Marshal()
if err != nil {
return nil, err
}
rawPacket = append(rawPacket, data...)
}
// The list of items in each chunk MUST be terminated by one or more null octets
rawPacket = append(rawPacket, SDESEnd)
// additional null octets MUST be included if needed to pad until the next 32-bit boundary
if size := len(rawPacket); size%4 != 0 {
padding := make([]byte, 4-size%4)
rawPacket = append(rawPacket, padding...)
}
return rawPacket, nil
}
// Unmarshal decodes the SourceDescriptionChunk from binary
func (s *SourceDescriptionChunk) Unmarshal(rawPacket []byte) error {
/*
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
* | SSRC/CSRC_1 |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | SDES items |
* | ... |
* +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
*/
if len(rawPacket) < (sdesSourceLen + sdesTypeLen) {
return errPacketTooShort
}
s.Source = binary.BigEndian.Uint32(rawPacket)
for i := 4; i < len(rawPacket); {
if pktType := rawPacket[i]; pktType == SDESEnd {
return nil
}
var it SourceDescriptionItem
if err := it.Unmarshal(rawPacket[i:]); err != nil {
return err
}
s.Items = append(s.Items, it)
i += it.len()
}
return errPacketTooShort
}
func (s SourceDescriptionChunk) len() int {
len := sdesSourceLen
for _, it := range s.Items {
len += it.len()
}
len += sdesTypeLen // for terminating null octet
// align to 32-bit boundary
if len%4 != 0 {
len += 4 - (len % 4)
}
return len
}
// A SourceDescriptionItem is a part of a SourceDescription that describes a stream.
type SourceDescriptionItem struct {
// The type identifier for this item. eg, SDESCNAME for canonical name description.
//
// Type zero or SDESEnd is interpreted as the end of an item list and cannot be used.
Type uint8
// Text is a unicode text blob associated with the item. Its meaning varies based on the item's Type.
Text string
}
func (s SourceDescriptionItem) len() int {
/*
* 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | CNAME=1 | length | user and domain name ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
return sdesTypeLen + sdesOctetCountLen + len([]byte(s.Text))
}
// Marshal encodes the SourceDescriptionItem in binary
func (s SourceDescriptionItem) 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | CNAME=1 | length | user and domain name ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
if s.Type == SDESEnd {
return nil, errSDESMissingType
}
rawPacket := make([]byte, sdesTypeLen+sdesOctetCountLen)
rawPacket[sdesTypeOffset] = s.Type
txtBytes := []byte(s.Text)
octetCount := len(txtBytes)
if octetCount > sdesMaxOctetCount {
return nil, errSDESTextTooLong
}
rawPacket[sdesOctetCountOffset] = uint8(octetCount)
rawPacket = append(rawPacket, txtBytes...)
return rawPacket, nil
}
// Unmarshal decodes the SourceDescriptionItem from binary
func (s *SourceDescriptionItem) 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
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | CNAME=1 | length | user and domain name ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
if len(rawPacket) < (sdesTypeLen + sdesOctetCountLen) {
return errPacketTooShort
}
s.Type = rawPacket[sdesTypeOffset]
octetCount := int(rawPacket[sdesOctetCountOffset])
if sdesTextOffset+octetCount > len(rawPacket) {
return errPacketTooShort
}
txtBytes := rawPacket[sdesTextOffset : sdesTextOffset+octetCount]
s.Text = string(txtBytes)
return nil
}

View File

@@ -0,0 +1,299 @@
package rtcp
import (
"reflect"
"testing"
)
func TestSourceDescriptionUnmarshal(t *testing.T) {
for _, test := range []struct {
Name string
Data []byte
Want SourceDescription
WantError error
}{
{
Name: "nil",
Data: nil,
Want: SourceDescription{
Chunks: nil,
},
},
{
Name: "missing type",
Data: []byte{
// ssrc=0x00000000
0x00, 0x00, 0x00, 0x00,
},
WantError: errPacketTooShort,
},
{
Name: "bad cname length",
Data: []byte{
// ssrc=0x00000000
0x00, 0x00, 0x00, 0x00,
// CNAME, len = 1
0x01, 0x01,
},
WantError: errPacketTooShort,
},
{
Name: "short cname",
Data: []byte{
// ssrc=0x00000000
0x00, 0x00, 0x00, 0x00,
// CNAME, Missing length
0x01,
},
WantError: errPacketTooShort,
},
{
Name: "no end",
Data: []byte{
// ssrc=0x00000000
0x00, 0x00, 0x00, 0x00,
// CNAME, len=1, content=A
0x01, 0x02, 0x41,
// Missing END
},
WantError: errPacketTooShort,
},
{
Name: "bad octet count",
Data: []byte{0, 0, 0, 0, 1, 1},
WantError: errPacketTooShort,
},
{
Name: "zero item chunk",
Data: []byte{
// ssrc=0x01020304
0x01, 0x02, 0x03, 0x04,
// END + padding
0x00, 0x00, 0x00, 0x00,
},
Want: SourceDescription{
Chunks: []SourceDescriptionChunk{{
Source: 0x01020304,
Items: nil,
}},
},
},
{
Name: "empty string",
Data: []byte{
// ssrc=0x01020304
0x01, 0x02, 0x03, 0x04,
// CNAME, len=0
0x01, 0x00,
// END + padding
0x00, 0x00,
},
Want: SourceDescription{
Chunks: []SourceDescriptionChunk{{
Source: 0x01020304,
Items: []SourceDescriptionItem{
{
Type: SDESCNAME,
Text: "",
},
},
}},
},
},
{
Name: "two items",
Data: []byte{
// ssrc=0x10000000
0x10, 0x00, 0x00, 0x00,
// CNAME, len=1, content=A
0x01, 0x01, 0x41,
// PHONE, len=1, content=B
0x04, 0x01, 0x42,
// END + padding
0x00, 0x00,
},
Want: SourceDescription{
Chunks: []SourceDescriptionChunk{
{
Source: 0x10000000,
Items: []SourceDescriptionItem{
{
Type: SDESCNAME,
Text: "A",
},
{
Type: SDESPhone,
Text: "B",
},
},
},
},
},
},
{
Name: "two chunks",
Data: []byte{
// ssrc=0x01020304
0x01, 0x02, 0x03, 0x04,
// Chunk 1
// CNAME, len=1, content=A
0x01, 0x01, 0x41,
// END
0x00,
// Chunk 2
// SSRC 0x05060708
0x05, 0x06, 0x07, 0x08,
// CNAME, len=3, content=BCD
0x01, 0x03, 0x42, 0x43, 0x44,
// END
0x00, 0x00, 0x00,
},
Want: SourceDescription{
Chunks: []SourceDescriptionChunk{
{
Source: 0x01020304,
Items: []SourceDescriptionItem{
{
Type: SDESCNAME,
Text: "A",
},
},
},
{
Source: 0x05060708,
Items: []SourceDescriptionItem{
{
Type: SDESCNAME,
Text: "BCD",
},
},
},
},
},
},
} {
var sdes SourceDescription
err := sdes.Unmarshal(test.Data)
if got, want := err, test.WantError; got != want {
t.Fatalf("Unmarshal %q: err = %v, want %v", test.Name, got, want)
}
if err != nil {
continue
}
if got, want := sdes, test.Want; !reflect.DeepEqual(got, want) {
t.Fatalf("Unmarshal %q: got %#v, want %#v", test.Name, got, want)
}
}
}
func TestSourceDescriptionRoundTrip(t *testing.T) {
for _, test := range []struct {
Name string
Desc SourceDescription
WantError error
}{
{
Name: "valid",
Desc: SourceDescription{
Chunks: []SourceDescriptionChunk{
{
Source: 1,
Items: []SourceDescriptionItem{
{
Type: SDESCNAME,
Text: "test@example.com",
},
},
},
{
Source: 2,
Items: []SourceDescriptionItem{
{
Type: SDESNote,
Text: "some note",
},
{
Type: SDESNote,
Text: "another note",
},
},
},
},
},
},
{
Name: "item without type",
Desc: SourceDescription{
Chunks: []SourceDescriptionChunk{{
Source: 1,
Items: []SourceDescriptionItem{{
Text: "test@example.com",
}},
}},
},
WantError: errSDESMissingType,
},
{
Name: "zero items",
Desc: SourceDescription{
Chunks: []SourceDescriptionChunk{{
Source: 1,
}},
},
},
{
Name: "email item",
Desc: SourceDescription{
Chunks: []SourceDescriptionChunk{{
Source: 1,
Items: []SourceDescriptionItem{{
Type: SDESEmail,
Text: "test@example.com",
}},
}},
},
},
{
Name: "empty text",
Desc: SourceDescription{
Chunks: []SourceDescriptionChunk{{
Source: 1,
Items: []SourceDescriptionItem{{
Type: SDESCNAME,
Text: "",
}},
}},
},
},
{
Name: "text too long",
Desc: SourceDescription{
Chunks: []SourceDescriptionChunk{{
Items: []SourceDescriptionItem{{
Type: SDESCNAME,
Text: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
}},
}},
},
WantError: errSDESTextTooLong,
},
} {
data, err := test.Desc.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 decoded SourceDescription
if err := decoded.Unmarshal(data); err != nil {
t.Fatalf("Unmarshal %q: %v", test.Name, err)
}
if got, want := decoded, test.Desc; !reflect.DeepEqual(got, want) {
t.Fatalf("%q sdes round trip: got %#v, want %#v", test.Name, got, want)
}
}
}