diff --git a/header.go b/header.go index 0ee8a62..0f05dca 100644 --- a/header.go +++ b/header.go @@ -38,6 +38,10 @@ const ( // https://tools.ietf.org/html/draft-holmer-rmcat-transport-wide-cc-extensions-01#page-5 FormatTCC uint8 = 15 + // https://www.rfc-editor.org/rfc/rfc5104.html#section-4.2.1 + FormatTMMBR = 3 + // https://www.rfc-editor.org/rfc/rfc5104.html#section-4.2.2 + FormatTMMBN = 4 ) func (p PacketType) String() string { diff --git a/packet.go b/packet.go index 637d9cd..ab53de3 100644 --- a/packet.go +++ b/packet.go @@ -97,6 +97,10 @@ func unmarshal(rawData []byte) (packet Packet, bytesprocessed int, err error) { packet = new(TransportLayerCC) case FormatCCFB: packet = new(CCFeedbackReport) + case FormatTMMBR: + packet = new(TMMBR) + case FormatTMMBN: + packet = new(TMMBN) default: packet = new(RawPacket) } diff --git a/packet_stringifier_test.go b/packet_stringifier_test.go index 2491a59..86e8190 100644 --- a/packet_stringifier_test.go +++ b/packet_stringifier_test.go @@ -484,6 +484,24 @@ func TestPrint(t *testing.T) { "\t\t\tType: 1\n" + "\t\t\tDelta: 37000\n", }, + { + &TMMBR{ + SenderSSRC: 0x902f9e2e, + Entries: []TMMBREntry{ + {0x902f9e2e, 9812743}, + {0xdeadbeef, 8435793}, + }, + }, + "rtcp.TMMBR:\n" + + "\tSenderSSRC: 2419039790\n" + + "\tEntries:\n" + + "\t\t0:\n" + + "\t\t\tMediaSSRC: 2419039790\n" + + "\t\t\tBitrate: 9.812743e+06\n" + + "\t\t1:\n" + + "\t\t\tMediaSSRC: 3735928559\n" + + "\t\t\tBitrate: 8.435793e+06\n", + }, { &TransportLayerNack{ SenderSSRC: 0x902f9e2e, diff --git a/receiver_estimated_maximum_bitrate.go b/receiver_estimated_maximum_bitrate.go index 91aa832..533b964 100644 --- a/receiver_estimated_maximum_bitrate.go +++ b/receiver_estimated_maximum_bitrate.go @@ -7,7 +7,6 @@ import ( "bytes" "encoding/binary" "fmt" - "math" ) // ReceiverEstimatedMaximumBitrate contains the receiver's estimated maximum bitrate. @@ -49,7 +48,6 @@ func (p ReceiverEstimatedMaximumBitrate) MarshalSize() int { // MarshalTo serializes the packet to the given byte slice. func (p ReceiverEstimatedMaximumBitrate) MarshalTo(buf []byte) (n int, err error) { - const bitratemax = 0x3FFFFp+63 /* 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 @@ -93,35 +91,11 @@ func (p ReceiverEstimatedMaximumBitrate) MarshalTo(buf []byte) (n int, err error // Write the length of the ssrcs to follow at the end buf[16] = byte(len(p.SSRCs)) - exp := 0 - bitrate := p.Bitrate - - if bitrate >= bitratemax { - bitrate = bitratemax - } - - if bitrate < 0 { - return 0, errInvalidBitrate - } - - for bitrate >= (1 << 18) { - bitrate /= 2.0 - exp++ - } - - if exp >= (1 << 6) { - return 0, errInvalidBitrate + err = putBitrate(p.Bitrate, buf[17:20]) + if err != nil { + return 0, err } - mantissa := uint(math.Floor(float64(bitrate))) - - // We can't quite use the binary package because - // a) it's a uint24 and b) the exponent is only 6-bits - // Just trust me; this is big-endian encoding. - buf[17] = byte(exp<<2) | byte(mantissa>>16) - buf[18] = byte(mantissa >> 8) - buf[19] = byte(mantissa) - // Write the SSRCs at the very end. n = 20 for _, ssrc := range p.SSRCs { @@ -136,7 +110,6 @@ func (p ReceiverEstimatedMaximumBitrate) MarshalTo(buf []byte) (n int, err error // //nolint:cyclop func (p *ReceiverEstimatedMaximumBitrate) Unmarshal(buf []byte) (err error) { - const mantissamax = 0x7FFFFF /* 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 @@ -220,24 +193,7 @@ func (p *ReceiverEstimatedMaximumBitrate) Unmarshal(buf []byte) (err error) { return errSSRCNumAndLengthMismatch } - // Get the 6-bit exponent value. - exp := buf[17] >> 2 - exp += 127 // bias for IEEE754 - exp += 23 // IEEE754 biases the decimal to the left, abs-send-time biases it to the right - - // The remaining 2-bits plus the next 16-bits are the mantissa. - mantissa := uint32(buf[17]&3)<<16 | uint32(buf[18])<<8 | uint32(buf[19]) - - if mantissa != 0 { - // ieee754 requires an implicit leading bit - for (mantissa & (mantissamax + 1)) == 0 { - exp-- - mantissa *= 2 - } - } - - // bitrate = mantissa * 2^exp - p.Bitrate = math.Float32frombits((uint32(exp) << 23) | (mantissa & mantissamax)) + p.Bitrate = loadBitrate(buf[17:20]) // Clear any existing SSRCs p.SSRCs = nil @@ -263,22 +219,8 @@ func (p *ReceiverEstimatedMaximumBitrate) Header() Header { // String prints the REMB packet in a human-readable format. func (p *ReceiverEstimatedMaximumBitrate) String() string { - // Keep a table of powers to units for fast conversion. - bitUnits := []string{"b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb"} - - // Do some unit conversions because b/s is far too difficult to read. - bitrate := p.Bitrate - powers := 0 - - // Keep dividing the bitrate until it's under 1000 - for bitrate >= 1000.0 && powers < len(bitUnits) { - bitrate /= 1000.0 - powers++ - } - - unit := bitUnits[powers] //nolint:gosec // powers is bounded by loop condition - - return fmt.Sprintf("ReceiverEstimatedMaximumBitrate %x %.2f %s/s", p.SenderSSRC, bitrate, unit) + unit := bitrateUnit(p.Bitrate) + return fmt.Sprintf("ReceiverEstimatedMaximumBitrate %x %.2f %s/s", p.SenderSSRC, p.Bitrate, unit) } // DestinationSSRC returns an array of SSRC values that this packet refers to. diff --git a/tmmbn.go b/tmmbn.go new file mode 100644 index 0000000..968751d --- /dev/null +++ b/tmmbn.go @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package rtcp + +import ( + "encoding/binary" + "fmt" + "strings" +) + +// TMMBN represents a Temporary Maximum Media Stream Bit Rate Notification packet +// as defined in RFC 5104, section 4.2.2. +type TMMBN struct { + // SSRC of the sender + SenderSSRC uint32 + + // List of TMMBN entries + Entries []TMMBNEntry +} + +// TMMBNEntry represents a single entry in TMMBN packet +type TMMBNEntry struct { + // SSRC of media source this entry applies to + MediaSSRC uint32 + + // Estimated maximum bitrate + Bitrate float32 +} + +// Marshal encodes the TMMBN packet in binary format +func (p TMMBN) Marshal() ([]byte, error) { + /* + TMMBN packet format (RFC 5104): + 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| FMT=4 | PT = 205 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of RTCP packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of media source | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | FCI SSRC | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | MxTBR Exp | MxTBR Mantissa |Measured Overhead| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ... | + */ + + packetSize := p.MarshalSize() + rawPacket := make([]byte, packetSize) + + header := p.Header() + headerBuf, err := header.Marshal() + if err != nil { + return nil, err + } + copy(rawPacket, headerBuf) + + body := rawPacket[headerLength:] + binary.BigEndian.PutUint32(body, p.SenderSSRC) + // Media SSRC is always 0 + // https://www.rfc-editor.org/rfc/rfc5104.html#section-4.2.1.2 + binary.BigEndian.PutUint32(body[ssrcLength:], 0) + + // Write each FCI entry + for i, entry := range p.Entries { + offset := ssrcLength*2 + i*(2*ssrcLength) + binary.BigEndian.PutUint32(body[offset:], entry.MediaSSRC) + + err = putBitrate(entry.Bitrate, body[offset+ssrcLength:]) + if err != nil { + return nil, err + } + } + + return rawPacket, nil +} + +// Unmarshal decodes the TMMBN packet from binary data +func (p *TMMBN) Unmarshal(rawPacket []byte) error { + if len(rawPacket) < headerLength+ssrcLength*2 { + return errPacketTooShort + } + + var header Header + if err := header.Unmarshal(rawPacket); err != nil { + return err + } + + expectedSize := int((header.Length + 1) * 4) + if len(rawPacket) < expectedSize { + return errBadLength + } + + if header.Type != TypeTransportSpecificFeedback || header.Count != FormatTMMBN { + return errWrongType + } + + body := rawPacket[headerLength:] + p.SenderSSRC = binary.BigEndian.Uint32(body) + + entryCount := int((header.Length - 2) / 2) + p.Entries = make([]TMMBNEntry, entryCount) + + for i := 0; i < entryCount; i++ { + offset := ssrcLength*2 + i*(2*ssrcLength) + entry := &p.Entries[i] + entry.MediaSSRC = binary.BigEndian.Uint32(body[offset:]) + entry.Bitrate = loadBitrate(body[offset+ssrcLength:]) + } + + return nil +} + +// MarshalSize returns the size of the packet when marshaled +func (p *TMMBN) MarshalSize() int { + return headerLength + ssrcLength*2 + len(p.Entries)*(2*ssrcLength) +} + +func (p *TMMBN) Header() Header { + return Header{ + Count: FormatTMMBN, + Type: TypeTransportSpecificFeedback, + Length: uint16((p.MarshalSize() / 4) - 1), //nolint:gosec // G115 + } +} + +// String prints the TMMBN packet in a human-readable format. +func (p *TMMBN) String() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("TMMBN from %x:\n", p.SenderSSRC)) + for i, entry := range p.Entries { + unit := bitrateUnit(entry.Bitrate) + sb.WriteString(fmt.Sprintf(" entry %d: media=%x, bitrate=%.2f %s/s\n", i, entry.MediaSSRC, entry.Bitrate, unit)) + } + return sb.String() +} + +// DestinationSSRC returns SSRCs this packet applies to +func (p *TMMBN) DestinationSSRC() []uint32 { + ssrcs := make([]uint32, len(p.Entries)) + for i, entry := range p.Entries { + ssrcs[i] = entry.MediaSSRC + } + return ssrcs +} diff --git a/tmmbn_test.go b/tmmbn_test.go new file mode 100644 index 0000000..f972414 --- /dev/null +++ b/tmmbn_test.go @@ -0,0 +1,270 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package rtcp + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +var _ Packet = (*TMMBN)(nil) // assert is a Packet + +func TestTMMBNMarshal(t *testing.T) { + assert := assert.New(t) + + input := TMMBN{ + SenderSSRC: 1, + Entries: []TMMBNEntry{ + { + MediaSSRC: 1215622422, + Bitrate: 8927168.0, + }, + }, + } + + // Expected packet structure: + // Header: V=2, P=0, FMT=4, PT=205, Length=4 + // SenderSSRC: 0x00000001 + // MediaSSRC: 0x00000000 (always 0 per RFC 5104) + // FCI Entry: + // - SSRC: 0x48746ED6 (1215622422) + // - Bitrate: exp=6, mantissa=139487 (0x0220DF) -> 0x1A20DF + // - Overhead: 0x00 + expected := []byte{132, 205, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 72, 116, 237, 22, 26, 32, 223, 0} + + output, err := input.Marshal() + assert.NoError(err) + assert.Equal(expected, output) +} + +func TestTMMBNUnmarshal(t *testing.T) { + assert := assert.New(t) + + // Real TMMBN packet with bitrate 8927168 (8.9 Mb/s) + input := []byte{132, 205, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 72, 116, 237, 22, 26, 32, 223, 0} + + // mantissa = []byte{26 & 3, 32, 223} = []byte{2, 32, 223} = 139487 + // exp = 26 >> 2 = 6 + // bitrate = 139487 * 2^6 = 139487 * 64 = 8927168 = 8.9 Mb/s + expected := TMMBN{ + SenderSSRC: 1, + Entries: []TMMBNEntry{ + { + MediaSSRC: 1215622422, + Bitrate: 8927168, + }, + }, + } + + packet := TMMBN{} + err := packet.Unmarshal(input) + assert.NoError(err) + assert.Equal(expected, packet) +} + +func TestTMMBNTruncate(t *testing.T) { + assert := assert.New(t) + + input := []byte{132, 205, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 72, 116, 237, 22, 26, 32, 223, 0} + + // Make sure that we're interpreting the bitrate correctly. + // For the above example, we have: + + // mantissa = 139487 + // exp = 6 + // bitrate = 8927168 + + packet := TMMBN{} + err := packet.Unmarshal(input) + assert.NoError(err) + assert.Equal(float32(8927168), packet.Entries[0].Bitrate) + + // Just verify marshal produces the same input. + output, err := packet.Marshal() + assert.NoError(err) + assert.Equal(input, output) + + // If we subtract the bitrate by 1, we'll round down a lower mantissa + packet.Entries[0].Bitrate-- + + // bitrate = 8927167 + // mantissa = 139486 + // exp = 6 + + output, err = packet.Marshal() + assert.NoError(err) + assert.NotEqual(input, output) + + // Which if we actually unmarshal again, we'll find that it's actually decreased by 64 (which is 2^exp) + // mantissa = 139486 + // exp = 6 + // bitrate = 8927104 + + err = packet.Unmarshal(output) + assert.NoError(err) + assert.Equal(float32(8927104), packet.Entries[0].Bitrate) +} + +func TestTMMBNOverflow(t *testing.T) { + assert := assert.New(t) + + // Marshal a packet with the maximum possible bitrate. + packet := TMMBN{ + Entries: []TMMBNEntry{ + { + Bitrate: math.MaxFloat32, + }, + }, + } + + // mantissa = 262143 = 0x3FFFF + // exp = 63 + + expected := []byte{132, 205, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0} + + output, err := packet.Marshal() + assert.NoError(err) + assert.Equal(expected, output) + + // mantissa = 262143 + // exp = 63 + // bitrate = 0xFFFFC00000000000 + + err = packet.Unmarshal(output) + assert.NoError(err) + assert.Equal(math.Float32frombits(0x67FFFFC0), packet.Entries[0].Bitrate) + + // Make sure we marshal to the same result again. + output, err = packet.Marshal() + assert.NoError(err) + assert.Equal(expected, output) + + // Finally, try unmarshalling one number higher than we used to be able to handle. + input := []byte{132, 205, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 188, 0, 0, 0} + err = packet.Unmarshal(input) + assert.NoError(err) + assert.Equal(math.Float32frombits(0x62800000), packet.Entries[0].Bitrate) +} + +func TestTMMBNMultipleEntries(t *testing.T) { + assert := assert.New(t) + + input := TMMBN{ + SenderSSRC: 12345, + Entries: []TMMBNEntry{ + { + MediaSSRC: 1000, + Bitrate: 1000000.0, + }, + { + MediaSSRC: 2000, + Bitrate: 2000000.0, + }, + }, + } + + output, err := input.Marshal() + assert.NoError(err) + + packet := TMMBN{} + err = packet.Unmarshal(output) + assert.NoError(err) + + assert.Equal(input.SenderSSRC, packet.SenderSSRC) + assert.Equal(len(input.Entries), len(packet.Entries)) + + for i := range input.Entries { + assert.Equal(input.Entries[i].MediaSSRC, packet.Entries[i].MediaSSRC) + // Allow small floating point differences due to encoding/decoding + assert.InDelta(input.Entries[i].Bitrate, packet.Entries[i].Bitrate, 1000) + } +} + +func TestTMMBNDestinationSSRC(t *testing.T) { + assert := assert.New(t) + + packet := TMMBN{ + Entries: []TMMBNEntry{ + {MediaSSRC: 1000}, + {MediaSSRC: 2000}, + {MediaSSRC: 3000}, + }, + } + + ssrcs := packet.DestinationSSRC() + assert.Equal([]uint32{1000, 2000, 3000}, ssrcs) +} + +func TestTMMBNMarshalSize(t *testing.T) { + assert := assert.New(t) + + // Test with no entries + packet := TMMBN{} + assert.Equal(12, packet.MarshalSize()) + + // Test with one entry + packet.Entries = []TMMBNEntry{{}} + assert.Equal(20, packet.MarshalSize()) + + // Test with multiple entries + packet.Entries = []TMMBNEntry{{}, {}, {}} + assert.Equal(36, packet.MarshalSize()) +} + +func TestTMMBNHeader(t *testing.T) { + assert := assert.New(t) + + packet := TMMBN{ + SenderSSRC: 1, + Entries: []TMMBNEntry{ + {MediaSSRC: 1000, Bitrate: 1000000}, + }, + } + + header := packet.Header() + assert.Equal(FormatTMMBN, int(header.Count)) + assert.Equal(TypeTransportSpecificFeedback, header.Type) + assert.Equal(4, int(header.Length)) +} + +func TestTMMBNString(t *testing.T) { + assert := assert.New(t) + + packet := TMMBN{ + SenderSSRC: 0x12345678, + Entries: []TMMBNEntry{ + { + MediaSSRC: 0xABCDEF00, + Bitrate: 8927168.0, + }, + }, + } + + str := packet.String() + assert.Contains(str, "TMMBN") + assert.Contains(str, "12345678") + assert.Contains(str, "abcdef00") +} + +func TestTMMBNUnmarshalErrors(t *testing.T) { + assert := assert.New(t) + + // Test packet too short + packet := TMMBN{} + err := packet.Unmarshal([]byte{1, 2, 3}) + assert.Error(err) + + // Test wrong packet type + wrongType := []byte{132, 200, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + err = packet.Unmarshal(wrongType) + assert.Error(err) + + // Test wrong format + wrongFormat := []byte{132, 205, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + wrongFormat[0] = 135 // Change FMT to 7 + err = packet.Unmarshal(wrongFormat) + assert.Error(err) +} diff --git a/tmmbr.go b/tmmbr.go new file mode 100644 index 0000000..9eedb00 --- /dev/null +++ b/tmmbr.go @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package rtcp + +import ( + "encoding/binary" + "fmt" + "strings" +) + +// TMMBR represents a Temporary Maximum Media Stream Bit Rate Request packet +// as defined in RFC 5104, section 4.2.1. +type TMMBR struct { + // SSRC of the sender + SenderSSRC uint32 + + // List of TMMBR entries + Entries []TMMBREntry +} + +// TMMBREntry represents a single entry in TMMBR packet +type TMMBREntry struct { + // SSRC of media source this entry applies to + MediaSSRC uint32 + + // Estimated maximum bitrate + Bitrate float32 +} + +// Marshal encodes the TMMBR packet in binary format +func (p TMMBR) Marshal() ([]byte, error) { + /* + TMMBR packet format (RFC 5104): + 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| FMT=3 | PT = 205 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of RTCP packet sender | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC of media source | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | FCI SSRC | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | MxTBR Exp | MxTBR Mantissa |Measured Overhead| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ... | + */ + + packetSize := p.MarshalSize() + rawPacket := make([]byte, packetSize) + + header := p.Header() + headerBuf, err := header.Marshal() + if err != nil { + return nil, err + } + copy(rawPacket, headerBuf) + + body := rawPacket[headerLength:] + binary.BigEndian.PutUint32(body, p.SenderSSRC) + // Media SSRC is always 0 + // https://www.rfc-editor.org/rfc/rfc5104.html#section-4.2.1.2 + binary.BigEndian.PutUint32(body[ssrcLength:], 0) + + // Write each FCI entry + for i, entry := range p.Entries { + offset := ssrcLength*2 + i*(2*ssrcLength) + binary.BigEndian.PutUint32(body[offset:], entry.MediaSSRC) + + err = putBitrate(entry.Bitrate, body[offset+ssrcLength:]) + if err != nil { + return nil, err + } + } + + return rawPacket, nil +} + +// Unmarshal decodes the TMMBR packet from binary data +func (p *TMMBR) Unmarshal(rawPacket []byte) error { + if len(rawPacket) < headerLength+ssrcLength*2 { + return errPacketTooShort + } + + var header Header + if err := header.Unmarshal(rawPacket); err != nil { + return err + } + + expectedSize := int((header.Length + 1) * 4) + if len(rawPacket) < expectedSize { + return errBadLength + } + + if header.Type != TypeTransportSpecificFeedback || header.Count != FormatTMMBR { + return errWrongType + } + + body := rawPacket[headerLength:] + p.SenderSSRC = binary.BigEndian.Uint32(body) + + entryCount := int((header.Length - 2) / 2) + p.Entries = make([]TMMBREntry, entryCount) + + for i := 0; i < entryCount; i++ { + offset := ssrcLength*2 + i*(2*ssrcLength) + entry := &p.Entries[i] + entry.MediaSSRC = binary.BigEndian.Uint32(body[offset:]) + entry.Bitrate = loadBitrate(body[offset+ssrcLength:]) + } + + return nil +} + +// MarshalSize returns the size of the packet when marshaled +func (p *TMMBR) MarshalSize() int { + return headerLength + ssrcLength*2 + len(p.Entries)*(2*ssrcLength) +} + +func (p *TMMBR) Header() Header { + return Header{ + Count: FormatTMMBR, + Type: TypeTransportSpecificFeedback, + Length: uint16((p.MarshalSize() / 4) - 1), //nolint:gosec // G115 + } +} + +// String prints the TMMBR packet in a human-readable format. +func (p *TMMBR) String() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("TMMBR from %x:\n", p.SenderSSRC)) + for i, entry := range p.Entries { + unit := bitrateUnit(entry.Bitrate) + sb.WriteString(fmt.Sprintf(" entry %d: media=%x, bitrate=%.2f %s/s\n", i, entry.MediaSSRC, entry.Bitrate, unit)) + } + return sb.String() +} + +// DestinationSSRC returns SSRCs this packet applies to +func (p *TMMBR) DestinationSSRC() []uint32 { + ssrcs := make([]uint32, len(p.Entries)) + for i, entry := range p.Entries { + ssrcs[i] = entry.MediaSSRC + } + return ssrcs +} diff --git a/tmmbr_test.go b/tmmbr_test.go new file mode 100644 index 0000000..728e742 --- /dev/null +++ b/tmmbr_test.go @@ -0,0 +1,270 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package rtcp + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +var _ Packet = (*TMMBR)(nil) // assert is a Packet + +func TestTMMBRMarshal(t *testing.T) { + assert := assert.New(t) + + input := TMMBR{ + SenderSSRC: 1, + Entries: []TMMBREntry{ + { + MediaSSRC: 1215622422, + Bitrate: 8927168.0, + }, + }, + } + + // Expected packet structure: + // Header: V=2, P=0, FMT=3, PT=205, Length=4 + // SenderSSRC: 0x00000001 + // MediaSSRC: 0x00000000 (always 0 per RFC 5104) + // FCI Entry: + // - SSRC: 0x48746ED6 (1215622422) + // - Bitrate: exp=6, mantissa=139487 (0x0220DF) -> 0x1A20DF + // - Overhead: 0x00 + expected := []byte{131, 205, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 72, 116, 237, 22, 26, 32, 223, 0} + + output, err := input.Marshal() + assert.NoError(err) + assert.Equal(expected, output) +} + +func TestTMMBRUnmarshal(t *testing.T) { + assert := assert.New(t) + + // Real TMMBR packet with bitrate 8927168 (8.9 Mb/s) + input := []byte{131, 205, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 72, 116, 237, 22, 26, 32, 223, 0} + + // mantissa = []byte{26 & 3, 32, 223} = []byte{2, 32, 223} = 139487 + // exp = 26 >> 2 = 6 + // bitrate = 139487 * 2^6 = 139487 * 64 = 8927168 = 8.9 Mb/s + expected := TMMBR{ + SenderSSRC: 1, + Entries: []TMMBREntry{ + { + MediaSSRC: 1215622422, + Bitrate: 8927168, + }, + }, + } + + packet := TMMBR{} + err := packet.Unmarshal(input) + assert.NoError(err) + assert.Equal(expected, packet) +} + +func TestTMMBRTruncate(t *testing.T) { + assert := assert.New(t) + + input := []byte{131, 205, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 72, 116, 237, 22, 26, 32, 223, 0} + + // Make sure that we're interpreting the bitrate correctly. + // For the above example, we have: + + // mantissa = 139487 + // exp = 6 + // bitrate = 8927168 + + packet := TMMBR{} + err := packet.Unmarshal(input) + assert.NoError(err) + assert.Equal(float32(8927168), packet.Entries[0].Bitrate) + + // Just verify marshal produces the same input. + output, err := packet.Marshal() + assert.NoError(err) + assert.Equal(input, output) + + // If we subtract the bitrate by 1, we'll round down a lower mantissa + packet.Entries[0].Bitrate-- + + // bitrate = 8927167 + // mantissa = 139486 + // exp = 6 + + output, err = packet.Marshal() + assert.NoError(err) + assert.NotEqual(input, output) + + // Which if we actually unmarshal again, we'll find that it's actually decreased by 64 (which is 2^exp) + // mantissa = 139486 + // exp = 6 + // bitrate = 8927104 + + err = packet.Unmarshal(output) + assert.NoError(err) + assert.Equal(float32(8927104), packet.Entries[0].Bitrate) +} + +func TestTMMBROverflow(t *testing.T) { + assert := assert.New(t) + + // Marshal a packet with the maximum possible bitrate. + packet := TMMBR{ + Entries: []TMMBREntry{ + { + Bitrate: math.MaxFloat32, + }, + }, + } + + // mantissa = 262143 = 0x3FFFF + // exp = 63 + + expected := []byte{131, 205, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0} + + output, err := packet.Marshal() + assert.NoError(err) + assert.Equal(expected, output) + + // mantissa = 262143 + // exp = 63 + // bitrate = 0xFFFFC00000000000 + + err = packet.Unmarshal(output) + assert.NoError(err) + assert.Equal(math.Float32frombits(0x67FFFFC0), packet.Entries[0].Bitrate) + + // Make sure we marshal to the same result again. + output, err = packet.Marshal() + assert.NoError(err) + assert.Equal(expected, output) + + // Finally, try unmarshalling one number higher than we used to be able to handle. + input := []byte{131, 205, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 188, 0, 0, 0} + err = packet.Unmarshal(input) + assert.NoError(err) + assert.Equal(math.Float32frombits(0x62800000), packet.Entries[0].Bitrate) +} + +func TestTMMBRMultipleEntries(t *testing.T) { + assert := assert.New(t) + + input := TMMBR{ + SenderSSRC: 12345, + Entries: []TMMBREntry{ + { + MediaSSRC: 1000, + Bitrate: 1000000.0, + }, + { + MediaSSRC: 2000, + Bitrate: 2000000.0, + }, + }, + } + + output, err := input.Marshal() + assert.NoError(err) + + packet := TMMBR{} + err = packet.Unmarshal(output) + assert.NoError(err) + + assert.Equal(input.SenderSSRC, packet.SenderSSRC) + assert.Equal(len(input.Entries), len(packet.Entries)) + + for i := range input.Entries { + assert.Equal(input.Entries[i].MediaSSRC, packet.Entries[i].MediaSSRC) + // Allow small floating point differences due to encoding/decoding + assert.InDelta(input.Entries[i].Bitrate, packet.Entries[i].Bitrate, 1000) + } +} + +func TestTMMBRDestinationSSRC(t *testing.T) { + assert := assert.New(t) + + packet := TMMBR{ + Entries: []TMMBREntry{ + {MediaSSRC: 1000}, + {MediaSSRC: 2000}, + {MediaSSRC: 3000}, + }, + } + + ssrcs := packet.DestinationSSRC() + assert.Equal([]uint32{1000, 2000, 3000}, ssrcs) +} + +func TestTMMBRMarshalSize(t *testing.T) { + assert := assert.New(t) + + // Test with no entries + packet := TMMBR{} + assert.Equal(12, packet.MarshalSize()) + + // Test with one entry + packet.Entries = []TMMBREntry{{}} + assert.Equal(20, packet.MarshalSize()) + + // Test with multiple entries + packet.Entries = []TMMBREntry{{}, {}, {}} + assert.Equal(36, packet.MarshalSize()) +} + +func TestTMMBRHeader(t *testing.T) { + assert := assert.New(t) + + packet := TMMBR{ + SenderSSRC: 1, + Entries: []TMMBREntry{ + {MediaSSRC: 1000, Bitrate: 1000000}, + }, + } + + header := packet.Header() + assert.Equal(FormatTMMBR, int(header.Count)) + assert.Equal(TypeTransportSpecificFeedback, header.Type) + assert.Equal(4, int(header.Length)) +} + +func TestTMMBRString(t *testing.T) { + assert := assert.New(t) + + packet := TMMBR{ + SenderSSRC: 0x12345678, + Entries: []TMMBREntry{ + { + MediaSSRC: 0xABCDEF00, + Bitrate: 8927168.0, + }, + }, + } + + str := packet.String() + assert.Contains(str, "TMMBR") + assert.Contains(str, "12345678") + assert.Contains(str, "abcdef00") +} + +func TestTMMBRUnmarshalErrors(t *testing.T) { + assert := assert.New(t) + + // Test packet too short + packet := TMMBR{} + err := packet.Unmarshal([]byte{1, 2, 3}) + assert.Error(err) + + // Test wrong packet type + wrongType := []byte{131, 200, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + err = packet.Unmarshal(wrongType) + assert.Error(err) + + // Test wrong format + wrongFormat := []byte{131, 205, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + wrongFormat[0] = 135 // Change FMT to 7 + err = packet.Unmarshal(wrongFormat) + assert.Error(err) +} diff --git a/util.go b/util.go index 705b290..dc448bf 100644 --- a/util.go +++ b/util.go @@ -3,6 +3,8 @@ package rtcp +import "math" + // getPadding Returns the padding required to make the length a multiple of 4. func getPadding(packetLen int) int { if packetLen%4 == 0 { @@ -41,3 +43,74 @@ func getNBitsFromByte(b byte, begin, n uint16) uint16 { func get24BitsFromBytes(b []byte) uint32 { return uint32(b[0])<<16 + uint32(b[1])<<8 + uint32(b[2]) } + +func putBitrate(bitrate float32, buf []byte) (err error) { + const bitratemax = 0x3FFFFp+63 + if bitrate >= bitratemax { + bitrate = bitratemax + } + + if bitrate < 0 { + return errInvalidBitrate + } + + exp := 0 + + for bitrate >= (1 << 18) { + bitrate /= 2.0 + exp++ + } + + if exp >= (1 << 6) { + return errInvalidBitrate + } + + mantissa := uint(math.Floor(float64(bitrate))) + + // We can't quite use the binary package because + // a) it's a uint24 and b) the exponent is only 6-bits + // Just trust me; this is big-endian encoding. + buf[0] = byte(exp<<2) | byte(mantissa>>16) + buf[1] = byte(mantissa >> 8) + buf[2] = byte(mantissa) + return nil +} + +func loadBitrate(buf []byte) float32 { + const mantissamax = 0x7FFFFF + // Get the 6-bit exponent value. + exp := buf[0] >> 2 + exp += 127 // bias for IEEE754 + exp += 23 // IEEE754 biases the decimal to the left, abs-send-time biases it to the right + + // The remaining 2-bits plus the next 16-bits are the mantissa. + mantissa := uint32(buf[0]&3)<<16 | uint32(buf[1])<<8 | uint32(buf[2]) + + if mantissa != 0 { + // ieee754 requires an implicit leading bit + for (mantissa & (mantissamax + 1)) == 0 { + exp-- + mantissa *= 2 + } + } + + // bitrate = mantissa * 2^exp + bitrate := math.Float32frombits((uint32(exp) << 23) | (mantissa & mantissamax)) + return bitrate +} + +func bitrateUnit(bitrate float32) string { + // Keep a table of powers to units for fast conversion. + bitUnits := []string{"b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb"} + + // Do some unit conversions because b/s is far too difficult to read. + powers := 0 + + // Keep dividing the bitrate until it's under 1000 + for bitrate >= 1000.0 && powers < len(bitUnits) { + bitrate /= 1000.0 + powers++ + } + + return bitUnits[powers] //nolint:gosec // powers is bounded by loop condition +}