Files
screen_cast/ios/Receiver/Decode/RenderSink.swift
T
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

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)
}
}