diff --git a/Sources/RTCP/ReceiverReport.swift b/Sources/RTCP/ReceiverReport.swift index e60e294..c2b0ecf 100644 --- a/Sources/RTCP/ReceiverReport.swift +++ b/Sources/RTCP/ReceiverReport.swift @@ -13,3 +13,213 @@ //===----------------------------------------------------------------------===// import NIOCore import Shared + +let rrSsrcOffset: Int = headerLength +let rrReportOffset: Int = rrSsrcOffset + ssrcLength + +/// A ReceiverReport (RR) packet provides reception quality feedback for an RTP stream +public struct ReceiverReport: Equatable { + /// The synchronization source identifier for the originator of this RR packet. + public var 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. + public var reports: [ReceptionReport] + /// Extension contains additional, payload-specific information that needs to + /// be reported regularly about the receiver. + public var profileExtensions: ByteBuffer + + public init() { + self.ssrc = 0 + self.reports = [] + self.profileExtensions = ByteBuffer() + } + + public init(ssrc: UInt32, reports: [ReceptionReport], profileExtensions: ByteBuffer) { + self.ssrc = ssrc + self.reports = reports + self.profileExtensions = profileExtensions + } +} + +extension ReceiverReport: CustomStringConvertible { + public var description: String { + var out = "ReceiverReport from \(self.ssrc)\n" + out += "\tSSRC \tLost\tLastSequence\n" + for rep in self.reports { + out += String( + format: + "\t%x\t%d/%d\t%d\n", + rep.ssrc, rep.fractionLost, rep.totalLost, rep.lastSequenceNumber + ) + } + out += "\tProfile Extension Data: \(self.profileExtensions)\n" + + return out + } +} + +extension ReceiverReport: Packet { + /// Header returns the Header associated with this packet. + public func header() -> Header { + Header( + padding: getPadding(self.rawSize()) != 0, + count: UInt8(self.reports.count), + packetType: PacketType.receiverReport, + length: UInt16((self.marshalSize() / 4) - 1) + ) + } + + /// destination_ssrc returns an array of SSRC values that this packet refers to. + public func destinationSsrc() -> [UInt32] { + self.reports.map { $0.ssrc } + } + + public func rawSize() -> Int { + var repsLength = 0 + for rep in self.reports { + repsLength += rep.marshalSize() + } + + return headerLength + ssrcLength + repsLength + self.profileExtensions.readableBytes + } +} + +extension ReceiverReport: Unmarshal { + /// Unmarshal decodes the ReceiverReport from binary + public static func unmarshal(_ buf: ByteBuffer) throws -> (Self, 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 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * header |V=2|P| RC | PT=RR=201 | length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | SSRC of packet sender | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * report | SSRC_1 (SSRC of first source) | + * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * 1 | fraction lost | cumulative number of packets lost | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | extended highest sequence number received | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | interarrival jitter | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | last SR (LSR) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | delay since last SR (DLSR) | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * report | SSRC_2 (SSRC of second source) | + * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * 2 : ... : + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * | profile-specific extensions | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + let rawPacketLen = buf.readableBytes + if rawPacketLen < (headerLength + ssrcLength) { + throw RtcpError.errPacketTooShort + } + + let (header, headerLen) = try Header.unmarshal(buf) + if header.packetType != PacketType.receiverReport { + throw RtcpError.errWrongType + } + + var reader = buf.slice() + reader.moveReaderIndex(forwardBy: headerLen) + + guard let ssrc: UInt32 = reader.readInteger() else { + throw RtcpError.errPacketTooShort + } + + var offset = rrReportOffset + var reports: [ReceptionReport] = [] + reports.reserveCapacity(Int(header.count)) + for _ in 0.. rawPacketLen { + throw RtcpError.errPacketTooShort + } + let (reception_report, receptionReportLen) = try ReceptionReport.unmarshal(reader) + reader.moveReaderIndex(forwardBy: receptionReportLen) + reports.append(reception_report) + offset += receptionReportLen + } + let profileExtensions = reader.readSlice(length: reader.readableBytes) ?? ByteBuffer() + /* + if header.padding && raw_packet.has_remaining() { + raw_packet.advance(raw_packet.remaining()); + } + */ + + return ( + ReceiverReport( + ssrc: ssrc, + reports: reports, + profileExtensions: profileExtensions + ), + reader.readerIndex + ) + } +} + +extension ReceiverReport: MarshalSize { + public func marshalSize() -> Int { + let l = self.rawSize() + // align to 32-bit boundary + return l + getPadding(l) + } +} + +extension ReceiverReport: Marshal { + /// marshal_to encodes the packet in binary. + public func marshalTo(_ buf: inout ByteBuffer) throws -> Int { + if self.reports.count > countMax { + throw RtcpError.errTooManyReports + } + + /* + * 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 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * header |V=2|P| RC | PT=RR=201 | length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | SSRC of packet sender | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * report | SSRC_1 (SSRC of first source) | + * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * 1 | fraction lost | cumulative number of packets lost | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | extended highest sequence number received | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | interarrival jitter | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | last SR (LSR) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | delay since last SR (DLSR) | + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * report | SSRC_2 (SSRC of second source) | + * block +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * 2 : ... : + * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ + * | profile-specific extensions | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + let h = self.header() + let _ = try h.marshalTo(&buf) + + buf.writeInteger(self.ssrc) + + for report in self.reports { + let _ = try report.marshalTo(&buf) + } + + buf.writeImmutableBuffer(self.profileExtensions) + + if h.padding { + putPadding(&buf, self.rawSize()) + } + + return self.marshalSize() + } +} diff --git a/Sources/RTCP/ReceptionReport.swift b/Sources/RTCP/ReceptionReport.swift index bcbff4e..5132a39 100644 --- a/Sources/RTCP/ReceptionReport.swift +++ b/Sources/RTCP/ReceptionReport.swift @@ -14,7 +14,7 @@ import NIOCore import Shared -let recptionReportLength: Int = 24 +let receptionReportLength: Int = 24 let fractionLostOffset: Int = 4 let totalLostOffset: Int = 5 let lastSeqOffset: Int = 8 @@ -51,6 +51,29 @@ public struct ReceptionReport: Equatable { /// 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. public var delay: UInt32 + + public init() { + self.ssrc = 0 + self.fractionLost = 0 + self.totalLost = 0 + self.lastSequenceNumber = 0 + self.jitter = 0 + self.lastSenderReport = 0 + self.delay = 0 + } + + public init( + ssrc: UInt32, fractionLost: UInt8, totalLost: UInt32, lastSequenceNumber: UInt32, + jitter: UInt32, lastSenderReport: UInt32, delay: UInt32 + ) { + self.ssrc = ssrc + self.fractionLost = fractionLost + self.totalLost = totalLost + self.lastSequenceNumber = lastSequenceNumber + self.jitter = jitter + self.lastSenderReport = lastSenderReport + self.delay = delay + } } extension ReceptionReport: Packet { @@ -63,14 +86,14 @@ extension ReceptionReport: Packet { } public func rawSize() -> Int { - recptionReportLength + receptionReportLength } } extension ReceptionReport: Unmarshal { /// unmarshal decodes the ReceptionReport from binary public static func unmarshal(_ buf: ByteBuffer) throws -> (Self, Int) { - if buf.readableBytes < recptionReportLength { + if buf.readableBytes < receptionReportLength { throw RtcpError.errPacketTooShort } diff --git a/Tests/RTCPTests/ReceiverReportTests.swift b/Tests/RTCPTests/ReceiverReportTests.swift new file mode 100644 index 0000000..0eb2fae --- /dev/null +++ b/Tests/RTCPTests/ReceiverReportTests.swift @@ -0,0 +1,264 @@ +import NIOCore +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftRTC open source project +// +// Copyright (c) 2024 ngRTC and the SwiftRTC project authors +// Licensed under MIT License +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftRTC project authors +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// +import XCTest + +@testable import RTCP + +final class ReceiverReportTests: XCTestCase { + func testReceiverReportUnmarshal() throws { + let tests = [ + ( + "valid", + ByteBuffer(bytes: [ + 0x81, 0xc9, 0x0, 0x7, // v=2, p=0, count=1, RR, len=7 + 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e + 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 + 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 + 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 + 0x0, 0x0, 0x1, 0x11, // jitter=273 + 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 + 0x0, 0x2, 0x4a, 0x79, // delay=150137 + ]), + ReceiverReport( + ssrc: 0x902f_9e2e, + reports: [ + ReceptionReport( + ssrc: 0xbc5e_9a40, + fractionLost: 0, + totalLost: 0, + lastSequenceNumber: 0x46e1, + jitter: 273, + lastSenderReport: 0x9f36432, + delay: 150137 + ) + ], + profileExtensions: ByteBuffer() + ), + nil + ), + ( + "valid with extension data", + ByteBuffer(bytes: [ + 0x81, 0xc9, 0x0, 0x9, // v=2, p=0, count=1, RR, len=9 + 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e + 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 + 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 + 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 + 0x0, 0x0, 0x1, 0x11, // jitter=273 + 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 + 0x0, 0x2, 0x4a, 0x79, // delay=150137 + 0x54, 0x45, 0x53, 0x54, 0x44, 0x41, 0x54, + 0x41, // profile-specific extension data + ]), + ReceiverReport( + ssrc: 0x902f_9e2e, + reports: [ + ReceptionReport( + ssrc: 0xbc5e_9a40, + fractionLost: 0, + totalLost: 0, + lastSequenceNumber: 0x46e1, + jitter: 273, + lastSenderReport: 0x9f36432, + delay: 150137 + ) + ], + profileExtensions: ByteBuffer(bytes: [ + 0x54, 0x45, 0x53, 0x54, 0x44, 0x41, 0x54, 0x41, + ]) + ), + nil + ), + ( + "short report", + ByteBuffer(bytes: [ + 0x81, 0xc9, 0x00, 0x0c, // v=2, p=0, count=1, RR, len=7 + 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e + 0x00, 0x00, 0x00, + 0x00, // fracLost=0, totalLost=0 + // report ends early + ]), + ReceiverReport(), + RtcpError.errPacketTooShort + ), + ( + "wrong type", + ByteBuffer(bytes: [ + // v=2, p=0, count=1, SR, len=7 + 0x81, 0xc8, 0x0, 0x7, // ssrc=0x902f9e2e + 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0xbc5e9a40 + 0xbc, 0x5e, 0x9a, 0x40, // fracLost=0, totalLost=0 + 0x0, 0x0, 0x0, 0x0, // lastSeq=0x46e1 + 0x0, 0x0, 0x46, 0xe1, // jitter=273 + 0x0, 0x0, 0x1, 0x11, // lsr=0x9f36432 + 0x9, 0xf3, 0x64, 0x32, // delay=150137 + 0x0, 0x2, 0x4a, 0x79, + ]), + ReceiverReport(), + RtcpError.errWrongType + ), + ( + "bad count in header", + ByteBuffer(bytes: [ + 0x82, 0xc9, 0x0, 0x7, // v=2, p=0, count=2, RR, len=7 + 0x90, 0x2f, 0x9e, 0x2e, // ssrc=0x902f9e2e + 0xbc, 0x5e, 0x9a, 0x40, // ssrc=0xbc5e9a40 + 0x0, 0x0, 0x0, 0x0, // fracLost=0, totalLost=0 + 0x0, 0x0, 0x46, 0xe1, // lastSeq=0x46e1 + 0x0, 0x0, 0x1, 0x11, // jitter=273 + 0x9, 0xf3, 0x64, 0x32, // lsr=0x9f36432 + 0x0, 0x2, 0x4a, 0x79, // delay=150137 + ]), + ReceiverReport(), + RtcpError.errPacketTooShort + ), + ( + "nil", + ByteBuffer(bytes: []), + ReceiverReport(), + RtcpError.errPacketTooShort + ), + ] + + for (name, data, want, wantError) in tests { + if let wantError { + do { + let _ = try ReceiverReport.unmarshal(data) + XCTFail("expect error") + } catch let gotErr as RtcpError { + XCTAssertEqual(gotErr, wantError) + } catch { + XCTFail("expect RtcpError") + } + } else { + let got = try? ReceiverReport.unmarshal(data) + XCTAssertTrue(got != nil) + let (actual, _) = got! + XCTAssertEqual( + actual, want, + "Unmarshal \(name)" + ) + } + } + } + + func testReceiverReportRoundtrip() throws { + var tooManyReports: [ReceptionReport] = [] + for _ in 0..<(1 << 5) { + tooManyReports.append( + ReceptionReport( + ssrc: 2, + fractionLost: 2, + totalLost: 3, + lastSequenceNumber: 4, + jitter: 5, + lastSenderReport: 6, + delay: 7 + )) + } + + let tests = [ + ( + "valid", + ReceiverReport( + ssrc: 1, + reports: [ + ReceptionReport( + ssrc: 2, + fractionLost: 2, + totalLost: 3, + lastSequenceNumber: 4, + jitter: 5, + lastSenderReport: 6, + delay: 7 + ), + ReceptionReport(), + ], + profileExtensions: ByteBuffer() + ), + nil + ), + ( + "also valid", + ReceiverReport( + ssrc: 2, + reports: [ + ReceptionReport( + ssrc: 999, + fractionLost: 30, + totalLost: 12345, + lastSequenceNumber: 99, + jitter: 22, + lastSenderReport: 92, + delay: 46 + ) + ], + profileExtensions: ByteBuffer() + ), + nil + ), + ( + "totallost overflow", + ReceiverReport( + ssrc: 1, + reports: [ + ReceptionReport( + ssrc: 0, + fractionLost: 0, + totalLost: 1 << 25, + lastSequenceNumber: 0, + jitter: 0, + lastSenderReport: 0, + delay: 0 + ) + ], + profileExtensions: ByteBuffer() + ), + RtcpError.errInvalidTotalLost + ), + ( + "count overflow", + ReceiverReport( + ssrc: 1, + reports: tooManyReports, + profileExtensions: ByteBuffer() + ), + RtcpError.errTooManyReports + ), + ] + + for (name, want, wantError) in tests { + if let wantError { + do { + let _ = try want.marshal() + XCTFail("expect error") + } catch let gotErr as RtcpError { + XCTAssertEqual(gotErr, wantError) + } catch { + XCTFail("expect RtcpError") + } + } else { + let got = try? want.marshal() + XCTAssertTrue(got != nil) + let data = got! + let (actual, _) = try ReceiverReport.unmarshal(data) + XCTAssertEqual( + actual, want, + "Unmarshal \(name)" + ) + } + } + } +}