Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support application-defined packet (rfc3550#section-6.7) #181

Merged
merged 1 commit into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions application_defined.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package rtcp

import (
"encoding/binary"
)

// ApplicationDefined represents an RTCP application-defined packet.
type ApplicationDefined struct {
SubType uint8
SSRC uint32
Name string
Data []byte
}

// DestinationSSRC returns the SSRC value for this packet.
func (a ApplicationDefined) DestinationSSRC() []uint32 {
return []uint32{a.SSRC}
}

// Marshal serializes the application-defined struct into a byte slice with padding.
func (a ApplicationDefined) Marshal() ([]byte, error) {
dataLength := len(a.Data)
if dataLength > 0xFFFF-12 {
return nil, errAppDefinedDataTooLarge
}
if len(a.Name) != 4 {
return nil, errAppDefinedInvalidName
}
// Calculate the padding size to be added to make the packet length a multiple of 4 bytes.
paddingSize := 4 - (dataLength % 4)
if paddingSize == 4 {
paddingSize = 0
}

packetSize := a.MarshalSize()
header := Header{
Type: TypeApplicationDefined,
Length: uint16((packetSize / 4) - 1),
Padding: paddingSize != 0,
Count: a.SubType,
}

headerBytes, err := header.Marshal()
if err != nil {
return nil, err
}

rawPacket := make([]byte, packetSize)
copy(rawPacket, headerBytes)
binary.BigEndian.PutUint32(rawPacket[4:8], a.SSRC)
copy(rawPacket[8:12], a.Name)
copy(rawPacket[12:], a.Data)

// Add padding if necessary.
if paddingSize > 0 {
for i := 0; i < paddingSize; i++ {
rawPacket[12+dataLength+i] = byte(paddingSize)
}
}

return rawPacket, nil
}

// Unmarshal parses the given raw packet into an application-defined struct, handling padding.
func (a *ApplicationDefined) 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| subtype | PT=APP=204 | length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC/CSRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| name (ASCII) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| application-dependent data ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
header := Header{}
err := header.Unmarshal(rawPacket)
if err != nil {
return err
}
if len(rawPacket) < 12 {
return errPacketTooShort
}

if int(header.Length+1)*4 != len(rawPacket) {
return errAppDefinedInvalidLength
}

a.SubType = header.Count
a.SSRC = binary.BigEndian.Uint32(rawPacket[4:8])
a.Name = string(rawPacket[8:12])

// Check for padding.
paddingSize := 0
if header.Padding {
paddingSize = int(rawPacket[len(rawPacket)-1])
if paddingSize > len(rawPacket)-12 {
return errWrongPadding
}
}

a.Data = rawPacket[12 : len(rawPacket)-paddingSize]

return nil
}

// MarshalSize returns the size of the packet once marshaled
func (a *ApplicationDefined) MarshalSize() int {
dataLength := len(a.Data)
// Calculate the padding size to be added to make the packet length a multiple of 4 bytes.
paddingSize := 4 - (dataLength % 4)
if paddingSize == 4 {
paddingSize = 0
}
return 12 + dataLength + paddingSize
}
266 changes: 266 additions & 0 deletions application_defined_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package rtcp

import (
"errors"
"reflect"
"testing"
)

func TestTApplicationPacketUnmarshal(t *testing.T) {
for _, test := range []struct {
Name string
Data []byte
Want ApplicationDefined
WantError error
}{
{
Name: "valid",
Data: []byte{
// Application Packet Type + Length(0x0003)
0x80, 0xcc, 0x00, 0x03,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='NAME'
0x4E, 0x41, 0x4D, 0x45,
// data='ABCD'
0x41, 0x42, 0x43, 0x44,
},
Want: ApplicationDefined{
SubType: 0,
SSRC: 0x4baae1ab,
Name: "NAME",
Data: []byte{0x41, 0x42, 0x43, 0x44},
},
},
{
Name: "validCustomSsubType",
Data: []byte{
// Application Packet Type (SubType 31) + Length(0x0003)
0x9f, 0xcc, 0x00, 0x03,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='NAME'
0x4E, 0x41, 0x4D, 0x45,
// data='ABCD'
0x41, 0x42, 0x43, 0x44,
},
Want: ApplicationDefined{
SubType: 31,
SSRC: 0x4baae1ab,
Name: "NAME",
Data: []byte{0x41, 0x42, 0x43, 0x44},
},
},
{
Name: "validWithPadding",
Data: []byte{
// Application Packet Type + Length(0x0002) (0xA0 has padding bit set)
0xA0, 0xcc, 0x00, 0x04,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='NAME'
0x4E, 0x41, 0x4D, 0x45,
// data='ABCDE'
0x41, 0x42, 0x43, 0x44, 0x45,
// 3 bytes padding as packet length must be a division of 4
0x03, 0x03, 0x03,
},
Want: ApplicationDefined{
SubType: 0,
SSRC: 0x4baae1ab,
Name: "NAME",
Data: []byte{0x41, 0x42, 0x43, 0x44, 0x45},
},
},
{
Name: "invalidAppPacketLengthField",
Data: []byte{
// Application Packet Type + invalid Length(0x00FF)
0x80, 0xcc, 0x00, 0xFF,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='NAME'
0x4E, 0x41, 0x4D, 0x45,
// data='ABCD'
0x41, 0x42, 0x43, 0x44,
},
WantError: errAppDefinedInvalidLength,
},
{
Name: "invalidPacketLengthTooShort",
Data: []byte{
// Application Packet Type + Length(0x0002). Total packet length is less than 12 bytes
0x80, 0xcc, 0x00, 0x2,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='SUI'
0x53, 0x55, 0x49,
},
WantError: errPacketTooShort,
},
{
Name: "wrongPaddingSize",
Data: []byte{
// Application Packet Type + Length(0x0002) (0xA0 has padding bit set)
0xA0, 0xcc, 0x00, 0x04,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='NAME'
0x4E, 0x41, 0x4D, 0x45,
// data='ABCDE'
0x41, 0x42, 0x43, 0x44, 0x45,
// 3 bytes padding as packet length must be a division of 4
0x03, 0x03, 0x09, // last byte has padding size 0x09 which is more than the data + padding bytes
},
WantError: errWrongPadding,
},
{
Name: "invalidHeader",
Data: []byte{
// Application Packet Type + invalid Length(0x00FF)
0xFF,
},
WantError: errPacketTooShort,
},
} {
var apk ApplicationDefined
err := apk.Unmarshal(test.Data)
if got, want := err, test.WantError; !errors.Is(got, want) {
t.Fatalf("Unmarshal %q result: got = %v, want %v", test.Name, got, want)
}
if err != nil {
continue
}

if got, want := apk, test.Want; !reflect.DeepEqual(got, want) {
t.Fatalf("Unmarshal %q result: got %v, want %v", test.Name, got, want)
}

// Check SSRC is matching
if apk.SSRC != 0x4baae1ab {
t.Fatalf("SSRC %q result: got packet SSRC %x instead of %x", test.Name, apk.SSRC, 0x4baae1ab)
}
if apk.SSRC != apk.DestinationSSRC()[0] {
t.Fatalf("SSRC %q result: DestinationSSRC() %x doesn't match SSRC field %x", test.Name, apk.DestinationSSRC()[0], apk.SSRC)
}
}
}

func TestTApplicationPacketMarshal(t *testing.T) {
for _, test := range []struct {
Name string
Want []byte
Packet ApplicationDefined
WantError error
}{
{
Name: "valid",
Want: []byte{
// Application Packet Type + Length(0x0003)
0x80, 0xcc, 0x00, 0x03,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='NAME'
0x4E, 0x41, 0x4D, 0x45,
// data='ABCD'
0x41, 0x42, 0x43, 0x44,
},
Packet: ApplicationDefined{
SSRC: 0x4baae1ab,
Name: "NAME",
Data: []byte{0x41, 0x42, 0x43, 0x44},
},
},
{
Name: "validCustomSubType",
Want: []byte{
// Application Packet Type (SubType 31) + Length(0x0003)
0x9f, 0xcc, 0x00, 0x03,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='NAME'
0x4E, 0x41, 0x4D, 0x45,
// data='ABCD'
0x41, 0x42, 0x43, 0x44,
},
Packet: ApplicationDefined{
SubType: 31,
SSRC: 0x4baae1ab,
Name: "NAME",
Data: []byte{0x41, 0x42, 0x43, 0x44},
},
},
{
Name: "validWithPadding",
Want: []byte{
// Application Packet Type + Length(0x0002) (0xA0 has padding bit set)
0xA0, 0xcc, 0x00, 0x04,
// sender=0x4baae1ab
0x4b, 0xaa, 0xe1, 0xab,
// name='NAME'
0x4E, 0x41, 0x4D, 0x45,
// data='ABCDE'
0x41, 0x42, 0x43, 0x44, 0x45,
// 3 bytes padding as packet length must be a division of 4
0x03, 0x03, 0x03,
},
Packet: ApplicationDefined{
SSRC: 0x4baae1ab,
Name: "NAME",
Data: []byte{0x41, 0x42, 0x43, 0x44, 0x45},
},
},
{
Name: "invalidDataTooLarge",
WantError: errAppDefinedDataTooLarge,
Packet: ApplicationDefined{
SSRC: 0x4baae1ab,
Name: "NAME",
Data: make([]byte, 0xFFFF-12+1), // total max packet size is 0xFFFF including header and other fields.
},
},
{
Name: "invalidName",
WantError: errAppDefinedInvalidName,
Packet: ApplicationDefined{
SSRC: 0x4baae1ab,
Name: "NOT4CHARS",
Data: []byte{0x41, 0x42, 0x43, 0x44},
},
},
{
Name: "InvalidSubType",
WantError: errInvalidHeader,
Packet: ApplicationDefined{
SubType: 32, // Must be up to 31
SSRC: 0x4baae1ab,
Name: "NAME",
Data: []byte{0x41, 0x42, 0x43, 0x44},
},
},
} {
rawPacket, err := test.Packet.Marshal()

// Check for expected errors
if got, want := err, test.WantError; !errors.Is(got, want) {
t.Fatalf("Marshal %q result: got = %v, want %v", test.Name, got, want)
}
if err != nil {
continue
}

// Check for expected successful result
if got, want := rawPacket, test.Want; !reflect.DeepEqual(got, want) {
t.Fatalf("Marshal %q result: got %v, want %v", test.Name, got, want)
}

// Check if MarshalSize() is matching the marshaled bytes
marshalSize := test.Packet.MarshalSize()
if marshalSize != len(rawPacket) {
t.Fatalf("MarshalSize %q result: got %d bytes instead of %d", test.Name, len(rawPacket), marshalSize)
}
}
}
3 changes: 3 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ var (
errWrongChunkType = errors.New("rtcp: wrong chunk type")
errBadStructMemberType = errors.New("rtcp: struct contains unexpected member type")
errBadReadParameter = errors.New("rtcp: cannot read into non-pointer")
errAppDefinedInvalidLength = errors.New("rtcp: application defined type invalid length")
errAppDefinedDataTooLarge = errors.New("rtcp: application defined data is too large")
errAppDefinedInvalidName = errors.New("rtcp: application defined name must be 4 ASCII chars")
)
Loading
Loading