c43dd3ca4a
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.
104 lines
4.6 KiB
Swift
104 lines
4.6 KiB
Swift
import XCTest
|
|
@testable import Receiver
|
|
|
|
final class H264DepacketizerTests: XCTestCase {
|
|
private let start: [UInt8] = [0x00, 0x00, 0x01]
|
|
|
|
private func packet(seq: Int, ts: Int, payload: [UInt8], marker: Bool = false) -> RtpPacket {
|
|
RtpPacket(header: RtpHeader(sequenceNumber: seq, timestamp: ts, marker: marker), payload: payload)
|
|
}
|
|
|
|
func testSingleNalTwoPackets() {
|
|
let d = H264Depacketizer()
|
|
// SPS (type 7) then a slice (type 5), closed by the marker.
|
|
let first = d.depacketize(packet(seq: 1, ts: 100, payload: [0x67, 0xAA, 0xBB]))
|
|
XCTAssertNil(first.accessUnit)
|
|
let second = d.depacketize(packet(seq: 2, ts: 100, payload: [0x41, 0x01, 0x02], marker: true))
|
|
XCTAssertEqual(second.accessUnit, start + [0x67, 0xAA, 0xBB] + start + [0x41, 0x01, 0x02])
|
|
XCTAssertTrue(second.isKeyFrame)
|
|
XCTAssertFalse(second.frameDropped)
|
|
}
|
|
|
|
func testFuAReassembly() {
|
|
let d = H264Depacketizer()
|
|
// NAL: header 0x41 (type 1, NRI 2) + payload 0x11 0x22 0x33 0x44.
|
|
// FU indicator = (0x41 & 0xE0) | 28 = 0x5C.
|
|
let indicator: UInt8 = 0x5C
|
|
_ = d.depacketize(packet(seq: 1, ts: 100, payload: [indicator, 0x81, 0x11]))
|
|
_ = d.depacketize(packet(seq: 2, ts: 100, payload: [indicator, 0x01, 0x22]))
|
|
let done = d.depacketize(packet(seq: 3, ts: 100, payload: [indicator, 0x41, 0x33, 0x44], marker: true))
|
|
XCTAssertEqual(done.accessUnit, start + [0x41, 0x11, 0x22, 0x33, 0x44])
|
|
XCTAssertFalse(done.frameDropped)
|
|
}
|
|
|
|
func testDropsGappedFrames() {
|
|
let d = H264Depacketizer()
|
|
_ = d.depacketize(packet(seq: 1, ts: 100, payload: [0x41, 0x01]))
|
|
// seq 2 is missing; the frame must be reported dropped, not delivered.
|
|
let tail = d.depacketize(packet(seq: 3, ts: 100, payload: [0x41, 0x02], marker: true))
|
|
XCTAssertNil(tail.accessUnit)
|
|
XCTAssertTrue(tail.frameDropped)
|
|
}
|
|
|
|
func testSeparateFramesByMarker() {
|
|
let d = H264Depacketizer()
|
|
let au1 = d.depacketize(packet(seq: 1, ts: 100, payload: [0x41, 0xAA], marker: true))
|
|
XCTAssertEqual(au1.accessUnit, start + [0x41, 0xAA])
|
|
let au2 = d.depacketize(packet(seq: 2, ts: 200, payload: [0x41, 0xBB], marker: true))
|
|
XCTAssertEqual(au2.accessUnit, start + [0x41, 0xBB])
|
|
}
|
|
|
|
func testDropsFuWithoutStart() {
|
|
let d = H264Depacketizer()
|
|
// Continuation (no S bit) without any start packet.
|
|
let result = d.depacketize(packet(seq: 1, ts: 100, payload: [0x7C, 0x41, 0x11], marker: true))
|
|
XCTAssertNil(result.accessUnit)
|
|
XCTAssertTrue(result.frameDropped)
|
|
}
|
|
|
|
func testDropsStillFragmentedAtMarker() {
|
|
let d = H264Depacketizer()
|
|
_ = d.depacketize(packet(seq: 1, ts: 100, payload: [0x7C, 0x81, 0x11]))
|
|
// Marker arrives while the FU-A NAL is still open.
|
|
let result = d.depacketize(packet(seq: 2, ts: 100, payload: [0x41, 0x01], marker: true))
|
|
XCTAssertNil(result.accessUnit)
|
|
XCTAssertTrue(result.frameDropped)
|
|
}
|
|
|
|
func testDropsUnsupportedPacketization() {
|
|
let d = H264Depacketizer()
|
|
let stapA: UInt8 = 24 // STAP-A
|
|
let result = d.depacketize(packet(seq: 1, ts: 100, payload: [stapA, 0x00, 0x05, 0x41, 0x01], marker: true))
|
|
XCTAssertNil(result.accessUnit)
|
|
XCTAssertTrue(result.frameDropped)
|
|
}
|
|
|
|
func testDropsTimestampChangeWithoutMarker() {
|
|
let d = H264Depacketizer()
|
|
_ = d.depacketize(packet(seq: 1, ts: 100, payload: [0x41, 0x01]))
|
|
// The stale frame is reported dropped, but this packet starts (and
|
|
// closes) the next frame — matching the C++ depacketizer.
|
|
let result = d.depacketize(packet(seq: 2, ts: 200, payload: [0x41, 0x02], marker: true))
|
|
XCTAssertEqual(result.accessUnit, start + [0x41, 0x02])
|
|
XCTAssertTrue(result.frameDropped)
|
|
}
|
|
|
|
func testKeyframeDetectionRequiresParameterSets() {
|
|
let d = H264Depacketizer()
|
|
let plain = d.depacketize(packet(seq: 1, ts: 100, payload: [0x41, 0x01], marker: true))
|
|
XCTAssertFalse(plain.isKeyFrame)
|
|
|
|
let d2 = H264Depacketizer()
|
|
let withSps = d2.depacketize(packet(seq: 1, ts: 100, payload: [0x67, 0xAA, 0x88, 0x68, 0xBB, 0x41, 0x01], marker: true))
|
|
XCTAssertTrue(withSps.isKeyFrame)
|
|
}
|
|
|
|
func testDropsShortFuPackets() {
|
|
let d = H264Depacketizer()
|
|
// FU-A packet without its FU header byte.
|
|
let result = d.depacketize(packet(seq: 1, ts: 100, payload: [0x7C], marker: true))
|
|
XCTAssertNil(result.accessUnit)
|
|
XCTAssertTrue(result.frameDropped)
|
|
}
|
|
}
|