diff --git a/pkg/rtcp/packet.go b/pkg/rtcp/header.go similarity index 95% rename from pkg/rtcp/packet.go rename to pkg/rtcp/header.go index 9eb6e4d5..b9d8d693 100644 --- a/pkg/rtcp/packet.go +++ b/pkg/rtcp/header.go @@ -33,6 +33,13 @@ type Header struct { Length uint16 } +var ( + errInvalidVersion = errors.New("invalid version") + errInvalidReportCount = errors.New("invalid report count") + errInvalidTotalLost = errors.New("invalid total lost count") + errPacketTooShort = errors.New("packet too short") +) + const ( headerLength = 4 versionShift = 6 @@ -43,12 +50,6 @@ const ( reportCountMask = 0x1f ) -var ( - errInvalidVersion = errors.New("invalid version") - errInvalidReportCount = errors.New("invalid report count") - errHeaderTooShort = errors.New("rtcp header too short") -) - // Marshal encodes the Header in binary func (h Header) Marshal() ([]byte, error) { /* @@ -84,7 +85,7 @@ func (h Header) Marshal() ([]byte, error) { // Unmarshal decodes the Header from binary func (h *Header) Unmarshal(rawPacket []byte) error { if len(rawPacket) < headerLength { - return errHeaderTooShort + return errPacketTooShort } /* diff --git a/pkg/rtcp/packet_test.go b/pkg/rtcp/header_test.go similarity index 94% rename from pkg/rtcp/packet_test.go rename to pkg/rtcp/header_test.go index 58cabbb8..23b6ee15 100644 --- a/pkg/rtcp/packet_test.go +++ b/pkg/rtcp/header_test.go @@ -33,8 +33,8 @@ func TestHeaderUnmarshal(t *testing.T) { func TestHeaderUnmarshalNil(t *testing.T) { var header Header err := header.Unmarshal(nil) - if got, want := err, errHeaderTooShort; got != want { - t.Errorf("unmarshal nil header: err = %v, want %v", got, want) + if got, want := err, errPacketTooShort; got != want { + t.Fatalf("unmarshal nil header: err = %v, want %v", got, want) } } func TestHeaderRoundTrip(t *testing.T) { diff --git a/pkg/rtcp/receiver_report.go b/pkg/rtcp/receiver_report.go new file mode 100644 index 00000000..d66eb497 --- /dev/null +++ b/pkg/rtcp/receiver_report.go @@ -0,0 +1,50 @@ +package rtcp + +import "encoding/binary" + +// A ReceiverReport (RR) packet provides reception quality feedback for an RTP stream +type ReceiverReport struct { + // The synchronization source identifier for the originator of this RR packet. + SSRC uint32 + // Zero or more reception report blocks depending on the number of other + // sources heard by this sender since the last report. Each reception report + // block conveys statistics on the reception of RTP packets from a + // single synchronization source. + Reports []ReceptionReport +} + +// Marshal encodes the ReceiverReport in binary +func (r ReceiverReport) Marshal() ([]byte, error) { + rawPacket := make([]byte, 4) + + binary.BigEndian.PutUint32(rawPacket, r.SSRC) + + for _, rp := range r.Reports { + data, err := rp.Marshal() + if err != nil { + return nil, err + } + rawPacket = append(rawPacket, data...) + } + + return rawPacket, nil +} + +// Unmarshal decodes the ReceiverReport from binary +func (r *ReceiverReport) Unmarshal(rawPacket []byte) error { + if len(rawPacket) < 4 { + return errPacketTooShort + } + + r.SSRC = binary.BigEndian.Uint32(rawPacket) + + for i := 4; i < len(rawPacket); i += receptionReportLength { + var rr ReceptionReport + if err := rr.Unmarshal(rawPacket[i:]); err != nil { + return err + } + r.Reports = append(r.Reports, rr) + } + + return nil +} diff --git a/pkg/rtcp/receiver_report_test.go b/pkg/rtcp/receiver_report_test.go new file mode 100644 index 00000000..b42e5c6d --- /dev/null +++ b/pkg/rtcp/receiver_report_test.go @@ -0,0 +1,87 @@ +package rtcp + +import ( + "reflect" + "testing" +) + +func TestReceiverReportUnmarshalNil(t *testing.T) { + var rr ReceiverReport + err := rr.Unmarshal(nil) + if got, want := err, errPacketTooShort; got != want { + t.Fatalf("unmarshal nil rr: err = %v, want %v", got, want) + } +} + +func TestReceiverReportRoundTrip(t *testing.T) { + for _, test := range []struct { + Name string + Report ReceiverReport + WantError error + }{ + { + Name: "valid", + Report: ReceiverReport{ + SSRC: 1, + Reports: []ReceptionReport{ + { + SSRC: 2, + FractionLost: 2, + TotalLost: 3, + LastSeq: 4, + Jitter: 5, + LastSR: 6, + Delay: 7, + }, + { + SSRC: 0, + }, + }, + }, + }, + { + Name: "also valid", + Report: ReceiverReport{ + SSRC: 2, + Reports: []ReceptionReport{ + { + SSRC: 999, + FractionLost: 30, + TotalLost: 12345, + LastSeq: 99, + Jitter: 22, + LastSR: 92, + Delay: 46, + }, + }, + }, + }, + { + Name: "totallost overflow", + Report: ReceiverReport{ + SSRC: 1, + Reports: []ReceptionReport{{ + TotalLost: 1 << 25, + }}, + }, + WantError: errInvalidTotalLost, + }, + } { + data, err := test.Report.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 ReceiverReport + if err := decoded.Unmarshal(data); err != nil { + t.Fatalf("Unmarshal %q: %v", test.Name, err) + } + + if got, want := decoded, test.Report; !reflect.DeepEqual(got, want) { + t.Fatalf("%q rr round trip: got %#v, want %#v", test.Name, got, want) + } + } +} diff --git a/pkg/rtcp/reception_report.go b/pkg/rtcp/reception_report.go new file mode 100644 index 00000000..d212d3ee --- /dev/null +++ b/pkg/rtcp/reception_report.go @@ -0,0 +1,126 @@ +package rtcp + +import "encoding/binary" + +// A ReceptionReport block conveys statistics on the reception of RTP packets +// from a single synchronization source. +type ReceptionReport struct { + // The SSRC identifier of the source to which the information in this + // reception report block pertains. + SSRC uint32 + // The fraction of RTP data packets from source SSRC lost since the + // previous SR or RR packet was sent, expressed as a fixed point + // number with the binary point at the left edge of the field. + FractionLost uint8 + // The total number of RTP data packets from source SSRC that have + // been lost since the beginning of reception. + TotalLost uint32 + // The low 16 bits contain the highest sequence number received in an + // RTP data packet from source SSRC, and the most significant 16 + // bits extend that sequence number with the corresponding count of + // sequence number cycles. + LastSeq uint32 + // An estimate of the statistical variance of the RTP data packet + // interarrival time, measured in timestamp units and expressed as an + // unsigned integer. + Jitter uint32 + // The middle 32 bits out of 64 in the NTP timestamp received as part of + // the most recent RTCP sender report (SR) packet from source SSRC. If no + // SR has been received yet, the field is set to zero. + LastSR uint32 + // The delay, expressed in units of 1/65536 seconds, between receiving the + // last SR packet from source SSRC and sending this reception report block. + // If no SR packet has been received yet from SSRC, the field is set to zero. + Delay uint32 +} + +var ( + receptionReportLength = 24 + fractionLostOffset = 4 + totalLostOffset = 5 + lastSeqOffset = 8 + jitterOffset = 12 + lastSROffset = 16 + delayOffset = 20 +) + +// Marshal encodes the ReceptionReport in binary +func (r ReceptionReport) 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 + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * | SSRC | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | fraction lost | cumulative number of packets lost | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | extended highest sequence number received | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | interarrival jitter | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | last SR (LSR) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | delay since last SR (DLSR) | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + */ + + rawPacket := make([]byte, receptionReportLength) + + binary.BigEndian.PutUint32(rawPacket, r.SSRC) + + rawPacket[fractionLostOffset] = r.FractionLost + + // pack TotalLost into 24 bits + if r.TotalLost >= (1 << 25) { + return nil, errInvalidTotalLost + } + tlBytes := rawPacket[totalLostOffset:] + tlBytes[0] = byte(r.TotalLost >> 16) + tlBytes[1] = byte(r.TotalLost >> 8) + tlBytes[2] = byte(r.TotalLost) + + binary.BigEndian.PutUint32(rawPacket[lastSeqOffset:], r.LastSeq) + binary.BigEndian.PutUint32(rawPacket[jitterOffset:], r.Jitter) + binary.BigEndian.PutUint32(rawPacket[lastSROffset:], r.LastSR) + binary.BigEndian.PutUint32(rawPacket[delayOffset:], r.Delay) + + return rawPacket, nil +} + +// Unmarshal decodes the ReceptionReport from binary +func (r *ReceptionReport) Unmarshal(rawPacket []byte) error { + if len(rawPacket) < receptionReportLength { + return errPacketTooShort + } + + /* + * 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 + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * | SSRC | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | fraction lost | cumulative number of packets lost | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | extended highest sequence number received | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | interarrival jitter | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | last SR (LSR) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | delay since last SR (DLSR) | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + */ + + r.SSRC = binary.BigEndian.Uint32(rawPacket) + r.FractionLost = rawPacket[fractionLostOffset] + + tlBytes := rawPacket[totalLostOffset:] + r.TotalLost = uint32(tlBytes[2]) | uint32(tlBytes[1])<<8 | uint32(tlBytes[0])<<16 + + r.LastSeq = binary.BigEndian.Uint32(rawPacket[lastSeqOffset:]) + r.Jitter = binary.BigEndian.Uint32(rawPacket[jitterOffset:]) + r.LastSR = binary.BigEndian.Uint32(rawPacket[lastSROffset:]) + r.Delay = binary.BigEndian.Uint32(rawPacket[delayOffset:]) + + return nil +} diff --git a/pkg/rtcp/sender_report.go b/pkg/rtcp/sender_report.go new file mode 100644 index 00000000..318e6d5b --- /dev/null +++ b/pkg/rtcp/sender_report.go @@ -0,0 +1,86 @@ +package rtcp + +import "encoding/binary" + +// A SenderReport (SR) packet provides reception quality feedback for an RTP stream +type SenderReport struct { + // The synchronization source identifier for the originator of this SR packet. + SSRC uint32 + // The wallclock time when this report was sent so that it may be used in + // combination with timestamps returned in reception reports from other + // receivers to measure round-trip propagation to those receivers. + NTPTime uint64 + // Corresponds to the same time as the NTP timestamp (above), but in + // the same units and with the same random offset as the RTP + // timestamps in data packets. This correspondence may be used for + // intra- and inter-media synchronization for sources whose NTP + // timestamps are synchronized, and may be used by media-independent + // receivers to estimate the nominal RTP clock frequency. + RTPTime uint32 + // The total number of RTP data packets transmitted by the sender + // since starting transmission up until the time this SR packet was + // generated. + PacketCount uint32 + // The total number of payload octets (i.e., not including header or + // padding) transmitted in RTP data packets by the sender since + // starting transmission up until the time this SR packet was + // generated. + OctetCount uint32 + // Zero or more reception report blocks depending on the number of other + // sources heard by this sender since the last report. Each reception report + // block conveys statistics on the reception of RTP packets from a + // single synchronization source. + Reports []ReceptionReport +} + +var ( + senderReportLength = 24 + ntpTimeOffset = 4 + rtpTimeOffset = 12 + packetCountOffset = 16 + octetCountOffset = 20 +) + +// Marshal encodes the SenderReport in binary +func (r SenderReport) Marshal() ([]byte, error) { + rawPacket := make([]byte, senderReportLength) + + binary.BigEndian.PutUint32(rawPacket, r.SSRC) + binary.BigEndian.PutUint64(rawPacket[ntpTimeOffset:], r.NTPTime) + binary.BigEndian.PutUint32(rawPacket[rtpTimeOffset:], r.RTPTime) + binary.BigEndian.PutUint32(rawPacket[packetCountOffset:], r.PacketCount) + binary.BigEndian.PutUint32(rawPacket[octetCountOffset:], r.OctetCount) + + for _, rp := range r.Reports { + data, err := rp.Marshal() + if err != nil { + return nil, err + } + rawPacket = append(rawPacket, data...) + } + + return rawPacket, nil +} + +// Unmarshal decodes the SenderReport from binary +func (r *SenderReport) Unmarshal(rawPacket []byte) error { + if len(rawPacket) < senderReportLength { + return errPacketTooShort + } + + r.SSRC = binary.BigEndian.Uint32(rawPacket) + r.NTPTime = binary.BigEndian.Uint64(rawPacket[ntpTimeOffset:]) + r.RTPTime = binary.BigEndian.Uint32(rawPacket[rtpTimeOffset:]) + r.PacketCount = binary.BigEndian.Uint32(rawPacket[packetCountOffset:]) + r.OctetCount = binary.BigEndian.Uint32(rawPacket[octetCountOffset:]) + + for i := senderReportLength; i < len(rawPacket); i += receptionReportLength { + var rr ReceptionReport + if err := rr.Unmarshal(rawPacket[i:]); err != nil { + return err + } + r.Reports = append(r.Reports, rr) + } + + return nil +} diff --git a/pkg/rtcp/sender_report_test.go b/pkg/rtcp/sender_report_test.go new file mode 100644 index 00000000..1648c2a2 --- /dev/null +++ b/pkg/rtcp/sender_report_test.go @@ -0,0 +1,81 @@ +package rtcp + +import ( + "reflect" + "testing" +) + +func TestSenderReportUnmarshalNil(t *testing.T) { + var sr SenderReport + err := sr.Unmarshal(nil) + if got, want := err, errPacketTooShort; got != want { + t.Fatalf("unmarshal nil sr: err = %v, want %v", got, want) + } +} + +func TestSenderReportRoundTrip(t *testing.T) { + for _, test := range []struct { + Name string + Report SenderReport + WantError error + }{ + { + Name: "valid", + Report: SenderReport{ + SSRC: 1, + NTPTime: 999, + RTPTime: 555, + PacketCount: 32, + OctetCount: 11, + Reports: []ReceptionReport{ + { + SSRC: 2, + FractionLost: 2, + TotalLost: 3, + LastSeq: 4, + Jitter: 5, + LastSR: 6, + Delay: 7, + }, + { + SSRC: 0, + }, + }, + }, + }, + { + Name: "also valid", + Report: SenderReport{ + SSRC: 2, + Reports: []ReceptionReport{ + { + SSRC: 999, + FractionLost: 30, + TotalLost: 12345, + LastSeq: 99, + Jitter: 22, + LastSR: 92, + Delay: 46, + }, + }, + }, + }, + } { + data, err := test.Report.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 SenderReport + if err := decoded.Unmarshal(data); err != nil { + t.Fatalf("Unmarshal %q: %v", test.Name, err) + } + + if got, want := decoded, test.Report; !reflect.DeepEqual(got, want) { + t.Fatalf("%q sr round trip: got %#v, want %#v", test.Name, got, want) + } + } +}