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.
60 lines
2.1 KiB
Swift
60 lines
2.1 KiB
Swift
import Foundation
|
|
#if canImport(Darwin)
|
|
import Darwin
|
|
#endif
|
|
|
|
/// A UDP socket for receiving RTP. Binds to a preferred port (or an ephemeral
|
|
/// port when it is busy), then delivers complete datagrams one at a time.
|
|
/// Mirrors the C++ `UdpRtpTransport` receive path and the Kotlin `DatagramSocket`
|
|
/// reader. Uses poll(2) with a timeout so `close()` from another thread cannot
|
|
/// strand a blocked recv (a close does not reliably unblock a POSIX recvfrom).
|
|
final class UdpTransport {
|
|
private var fd: Int32 = -1
|
|
private(set) var port: UInt16 = 0
|
|
|
|
/// Binds to [preferredPort] (or an ephemeral port when it is busy).
|
|
@discardableResult
|
|
func bind(preferredPort: UInt16, receiveBufferSize: Int32) -> Bool {
|
|
guard fd < 0 else { return true }
|
|
for p in [preferredPort, UInt16(0)] {
|
|
if let f = SocketUtils.makeUdpReceiver(port: p, receiveBufferSize: receiveBufferSize), f >= 0 {
|
|
fd = f
|
|
port = SocketUtils.boundPort(fd)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Waits up to [timeoutMs] for a datagram. Returns 1 when one is ready to
|
|
/// read, 0 on timeout, and -1 when the socket is closed/errored.
|
|
func poll(timeoutMs: Int32) -> Int32 {
|
|
guard fd >= 0 else { return -1 }
|
|
var pfd = pollfd(fd: fd, events: poll_events_t(POLLIN), revents: 0)
|
|
let r = withUnsafeMutablePointer(to: &pfd) { poll($0, 1, timeoutMs) }
|
|
if r > 0 {
|
|
if pfd.revents & poll_events_t(POLLERR) != 0 { return -1 }
|
|
return 1
|
|
}
|
|
return r == 0 ? 0 : -1
|
|
}
|
|
|
|
/// Reads one ready datagram. Returns nil on error or empty read.
|
|
func receiveDatagram() -> [UInt8]? {
|
|
guard fd >= 0 else { return nil }
|
|
var buf = [UInt8](repeating: 0, count: 4096)
|
|
let read = buf.withUnsafeMutableBytes { raw in
|
|
recvfrom(fd, raw.baseAddress, raw.count, 0, nil, nil)
|
|
}
|
|
guard read > 0 else { return nil }
|
|
return Array(buf.prefix(read))
|
|
}
|
|
|
|
func close() {
|
|
if fd >= 0 {
|
|
close(fd)
|
|
fd = -1
|
|
}
|
|
}
|
|
}
|