Skip to content

Commit

Permalink
Support application-defined packet
Browse files Browse the repository at this point in the history
  • Loading branch information
samitAtsyna authored and Sean-Der committed Aug 16, 2024
1 parent 878e0b0 commit e1b695e
Show file tree
Hide file tree
Showing 5 changed files with 408 additions and 0 deletions.
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

0 comments on commit e1b695e

Please sign in to comment.