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.
161 lines
5.9 KiB
Swift
161 lines
5.9 KiB
Swift
import Foundation
|
|
|
|
/// Result of feeding one packet to the depacketizer.
|
|
struct DepacketizeResult {
|
|
/// Completed access unit (Annex-B with 3-byte start codes) when the frame closed undamaged.
|
|
var accessUnit: [UInt8]?
|
|
/// True when this call discarded a frame as damaged (packet loss or unsupported packetization).
|
|
var frameDropped = false
|
|
/// True when the completed access unit carries SPS/PPS (a keyframe).
|
|
var isKeyFrame = false
|
|
}
|
|
|
|
/// Reassembles RFC 6184 packet streams (single NAL unit packets and FU-A)
|
|
/// into Annex-B access units. Packets must arrive in order; frames damaged by
|
|
/// sequence gaps or missing fragments are reported via DepacketizeResult.
|
|
///
|
|
/// Mirrors the C++ `H264Depacketizer` (same state machine and start codes).
|
|
final class H264Depacketizer {
|
|
static let fuA = 28
|
|
|
|
private var lastSequenceNumber: Int?
|
|
private var frameStarted = false
|
|
private var frameDamaged = false
|
|
private var frameTimestamp = 0
|
|
// Growable byte accumulators: keyframes reach hundreds of KB.
|
|
private var accessUnit: [UInt8] = []
|
|
private var fuActive = false
|
|
private var fuNal: [UInt8] = []
|
|
|
|
/// Feed one packet (in sequence order, from the jitter buffer).
|
|
func depacketize(_ packet: RtpPacket) -> DepacketizeResult {
|
|
var result = DepacketizeResult()
|
|
|
|
// Track sequence continuity: a gap means packets were lost.
|
|
if let last = lastSequenceNumber {
|
|
let expected = (last + 1) & 0xFFFF
|
|
if packet.header.sequenceNumber != expected {
|
|
fuActive = false
|
|
fuNal.removeAll(keepingCapacity: true)
|
|
if frameStarted { frameDamaged = true }
|
|
}
|
|
}
|
|
lastSequenceNumber = packet.header.sequenceNumber
|
|
|
|
// A timestamp change without a closing marker means the previous frame
|
|
// lost its tail and can no longer be recovered.
|
|
if frameStarted && packet.header.timestamp != frameTimestamp {
|
|
dropFrame()
|
|
result.frameDropped = true
|
|
}
|
|
if !frameStarted {
|
|
frameStarted = true
|
|
frameDamaged = false
|
|
frameTimestamp = packet.header.timestamp
|
|
accessUnit.removeAll(keepingCapacity: true)
|
|
}
|
|
|
|
let payload = packet.payload
|
|
if !payload.isEmpty {
|
|
let type = Int(payload[0]) & 0x1F
|
|
if type >= 1 && type <= 23 {
|
|
// Single NAL unit packet.
|
|
if fuActive {
|
|
frameDamaged = true
|
|
fuActive = false
|
|
fuNal.removeAll(keepingCapacity: true)
|
|
}
|
|
appendStartCode()
|
|
accessUnit.append(contentsOf: payload)
|
|
} else if type == Self.fuA {
|
|
if payload.count < 2 {
|
|
frameDamaged = true
|
|
} else {
|
|
let fuHeader = Int(payload[1])
|
|
let start = fuHeader & 0x80 != 0
|
|
let end = fuHeader & 0x40 != 0
|
|
let fragment = Array(payload[2...])
|
|
if start {
|
|
if fuActive {
|
|
// The previous fragmented NAL lost its end packet.
|
|
frameDamaged = true
|
|
}
|
|
fuActive = true
|
|
fuNal.removeAll(keepingCapacity: true)
|
|
// The FU indicator keeps the original NAL's F bit (0) and NRI,
|
|
// and declares type 28; the FU header carries S/E plus the real type.
|
|
fuNal.append(UInt8((Int(payload[0]) & 0xE0) | (fuHeader & 0x1F)))
|
|
fuNal.append(contentsOf: fragment)
|
|
} else if !fuActive {
|
|
// Continuation without a start: the head of the NAL is lost.
|
|
frameDamaged = true
|
|
} else {
|
|
fuNal.append(contentsOf: fragment)
|
|
if end {
|
|
appendStartCode()
|
|
accessUnit.append(contentsOf: fuNal)
|
|
fuActive = false
|
|
fuNal.removeAll(keepingCapacity: true)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Unsupported packetization mode (STAP-A, MTAP, FU-B).
|
|
frameDamaged = true
|
|
}
|
|
}
|
|
|
|
if !packet.header.marker {
|
|
return result
|
|
}
|
|
|
|
if fuActive {
|
|
// The marker arrived while a NAL was still fragmented.
|
|
frameDamaged = true
|
|
fuActive = false
|
|
fuNal.removeAll(keepingCapacity: true)
|
|
}
|
|
|
|
if !frameDamaged && !accessUnit.isEmpty {
|
|
let unit = accessUnit
|
|
result.accessUnit = unit
|
|
result.isKeyFrame = Self.containsParameterSets(unit)
|
|
} else {
|
|
// The frame that just ended is unusable.
|
|
result.frameDropped = true
|
|
}
|
|
dropFrame()
|
|
return result
|
|
}
|
|
|
|
private func appendStartCode() {
|
|
accessUnit.append(0)
|
|
accessUnit.append(0)
|
|
accessUnit.append(1)
|
|
}
|
|
|
|
/// The sender repeats SPS/PPS in-band at every keyframe; sniff for NAL types 7/8.
|
|
private static func containsParameterSets(_ unit: [UInt8]) -> Bool {
|
|
guard unit.count >= 4 else { return false }
|
|
var i = 0
|
|
while i <= unit.count - 4 {
|
|
if unit[i] == 0 && unit[i + 1] == 0 && unit[i + 2] == 1 {
|
|
let nalType = Int(unit[i + 3]) & 0x1F
|
|
if nalType == 7 || nalType == 8 {
|
|
return true
|
|
}
|
|
}
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func dropFrame() {
|
|
frameStarted = false
|
|
frameDamaged = false
|
|
accessUnit.removeAll(keepingCapacity: true)
|
|
fuActive = false
|
|
fuNal.removeAll(keepingCapacity: true)
|
|
}
|
|
}
|