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.
62 lines
2.0 KiB
Swift
62 lines
2.0 KiB
Swift
import AVFoundation
|
|
import CoreMedia
|
|
|
|
/// A render target for decoded frames. The decoder enqueues a `CMSampleBuffer`
|
|
/// (wrapping a CVPixelBuffer) per frame; the sink renders it. Mirrors the role
|
|
/// of the Android `Surface` + MediaCodec surface-mode rendering.
|
|
protocol RenderSink: AnyObject {
|
|
var isAttached: Bool { get }
|
|
func attach()
|
|
func detach()
|
|
/// Updates the layer's video format description (called when the in-band
|
|
/// SPS/PPS establish a (new) stream size).
|
|
func setFormatDescription(_ formatDescription: CMVideoFormatDescription)
|
|
/// Enqueues one decoded frame for display.
|
|
func enqueue(_ sampleBuffer: CMSampleBuffer)
|
|
}
|
|
|
|
/// Renders decoded CVPixelBuffers via `AVSampleBufferDisplayLayer`, which does
|
|
/// the video-scaling for us. `.resizeAspect` gives letterbox directly, so the
|
|
/// host view can stay full-screen without a manual transform (the same lesson
|
|
/// as the Android TextureView sizing: size the surface to the aspect, not a
|
|
/// transform matrix).
|
|
final class AVSampleBufferRenderSink: RenderSink {
|
|
private let layer: AVSampleBufferDisplayLayer
|
|
private var session: AVSampleBufferDisplayLayerSession?
|
|
private let lock = NSLock()
|
|
private(set) var isAttached = false
|
|
|
|
init(layer: AVSampleBufferDisplayLayer) {
|
|
self.layer = layer
|
|
}
|
|
|
|
func attach() {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
guard session == nil else { return }
|
|
let s = AVSampleBufferDisplayLayerSession(layer)
|
|
s.start()
|
|
session = s
|
|
isAttached = true
|
|
}
|
|
|
|
func detach() {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
session?.stop()
|
|
session = nil
|
|
isAttached = false
|
|
}
|
|
|
|
func setFormatDescription(_ formatDescription: CMVideoFormatDescription) {
|
|
layer.formatDescription = formatDescription
|
|
}
|
|
|
|
func enqueue(_ sampleBuffer: CMSampleBuffer) {
|
|
lock.lock()
|
|
let s = session
|
|
lock.unlock()
|
|
s?.enqueue(sampleBuffer)
|
|
}
|
|
}
|