Files
fegger c43dd3ca4a feat(ios): native receiver app (Swift, min iOS 17)
A native iOS receiver so an iPhone can act as the second receiver, speaking the existing signaling + RTP protocol (no C++ changes) and mirroring the Android receiver (Phase 8) source-to-source.

- RTP core (header/packet, jitter buffer, H.264 depacketizer) ported from the Android receiver
- BSD-socket signaling server (dual-stack, most-recent-peer, never-throwing sends) + NSBonjourServices
- VideoToolbox H.264 decode (in-band SPS/PPS, real-time, rebuilds on size change) -> AVSampleBufferDisplayLayer
- PLI keyframe recovery (500 ms) + pendingOffer for late surface attach
- XcodeGen project + bootstrap.sh; XCTest port of the Android suite + new coverage
- .gitignore for generated artifacts; CHANGELOG; PHASES + MEMORY updated

Status: authored; on-device validation pending a Mac + Xcode 26 + iPhone 16.
2026-09-10 21:31:53 +02:00

88 lines
3.4 KiB
Swift

import XCTest
@testable import Receiver
final class RtpHeaderTests: XCTestCase {
func testRoundtrip() {
let header = RtpHeader(version: 2, padding: false, extensionHeader: false, csrcCount: 0,
marker: true, payloadType: 96, sequenceNumber: 0xABCD,
timestamp: 0xDEADBEEF, ssrc: 0x12345678)
let wire = header.serialize()
XCTAssertEqual(wire.count, 12)
XCTAssertEqual(RtpHeader.parse(wire), header)
}
func testRejectsBadVersion() {
var wire = RtpHeader().serialize()
wire[0] = (wire[0] & 0x3F) | (1 << 6)
XCTAssertNil(RtpHeader.parse(wire))
}
func testRejectsShortInput() {
let header = RtpHeader()
XCTAssertNil(RtpHeader.parse(Array(header.serialize().prefix(11))))
XCTAssertNil(RtpHeader.parse([]))
}
func testPreservesFlags() {
let header = RtpHeader(padding: true, csrcCount: 2, marker: true, payloadType: 63)
let parsed = RtpHeader.parse(header.serialize())
XCTAssertEqual(parsed?.padding, true)
XCTAssertEqual(parsed?.csrcCount, 2)
XCTAssertEqual(parsed?.marker, true)
XCTAssertEqual(parsed?.payloadType, 63)
}
}
final class RtpPacketTests: XCTestCase {
private func header(seq: Int, marker: Bool = false) -> RtpHeader {
RtpHeader(sequenceNumber: seq, payloadType: 96, marker: marker)
}
func testRoundtripWithPayload() {
let packet = RtpPacket(header: header(seq: 7), payload: [0x11, 0x22, 0x33])
let wire = packet.header.serialize() + packet.payload
let parsed = RtpPacket.parse(wire)
XCTAssertEqual(parsed?.header, header(seq: 7))
XCTAssertEqual(parsed?.payload, [0x11, 0x22, 0x33])
}
func testSkipsCsrcList() {
let header = RtpHeader(csrcCount: 1, sequenceNumber: 3)
let wire = header.serialize() + [0x0A, 0x00, 0x00, 0x01] + [0x99]
let parsed = RtpPacket.parse(wire)
XCTAssertEqual(parsed?.header.csrcCount, 1)
XCTAssertEqual(parsed?.payload, [0x99])
}
func testSkipsExtensionHeader() {
let header = RtpHeader(extensionHeader: true, sequenceNumber: 4)
// profile=0x0001, length=1 word, one word of data.
let wire = header.serialize() + [0x00, 0x01, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF] + [0x77]
let parsed = RtpPacket.parse(wire)
XCTAssertEqual(parsed?.payload, [0x77])
}
func testStripsPadding() {
let header = RtpHeader(padding: true, sequenceNumber: 5)
// Payload byte, one padding zero, size byte (2 = padding incl. itself).
let wire = header.serialize() + [0x55, 0x00, 0x02]
let parsed = RtpPacket.parse(wire)
XCTAssertEqual(parsed?.payload, [0x55])
}
func testRejectsTruncatedCsrcAndExtension() {
let csrc = RtpHeader(csrcCount: 1).serialize()
XCTAssertNil(RtpPacket.parse(csrc)) // 12 bytes, needs 16
let ext = RtpHeader(extensionHeader: true).serialize() + [0x00, 0x01]
XCTAssertNil(RtpPacket.parse(ext)) // extension length field cut off
}
func testRejectsBadPadding() {
let zeroPad = RtpHeader(padding: true).serialize() + [0x00]
XCTAssertNil(RtpPacket.parse(zeroPad))
let oversized = RtpHeader(padding: true).serialize() + [0x00, 0x00, 0x05]
XCTAssertNil(RtpPacket.parse(oversized))
XCTAssertNil(RtpPacket.parse([UInt8](repeating: 0, count: 11)))
}
}