import Foundation import LiveKitWebRTC import os /// For the gate only (spec §15.22): a receiver whose screen decoder has /// wedged. Packets keep arriving, frames keep being handed to the decoder, /// and nothing comes out — which no keyframe mends, because it is this /// decoder instance that is stuck, and which a new transceiver does mend, /// because the far end's new stream gets a decoder of its own. That is the /// shape of a stall that "keyframes only" (0.4.3) left sitting at /// "recovering". /// /// Off unless the interop executable turns it on before the call is /// built; the apps never do, and then the call's factory is WebRTC's own. @_spi(Fixtures) public enum DecoderWedge { /// Wrap the call's decoders so they can be wedged. Read once, when the /// call's factory is made. nonisolated(unsafe) public static var enabled = false private static let live = OSAllocatedUnfairLock(initialState: [WeakDecoder]()) private static let sticky = OSAllocatedUnfairLock(initialState: false) static func register(_ decoder: WedgeableDecoder) { live.withLock { list in list.removeAll { $0.decoder == nil } list.append(WeakDecoder(decoder: decoder)) } } /// Wedges the decoder now putting out the largest picture, which in the /// gate is always the screen (its cameras are 320×240 to 640×480, and a /// screen even at its smallest under a thin link is wider). `sticky` /// wedges every decoder made afterwards too — only a screen's rebuild or /// share again makes one mid-call — for a receiver no rebuild can mend, /// the gate's "could not restore". Returns how many were wedged. @discardableResult public static func wedgeScreens(sticky: Bool = false) -> Int { self.sticky.withLock { $0 = sticky } let decoders = live.withLock { $0.compactMap(\.decoder) } guard let largest = decoders.map(\.area).max(), largest > 0 else { return 0 } var count = 0 for d in decoders where d.area == largest { d.wedge() count += 1 } return count } /// Every wedge lifted, and nothing wedged from now on. public static func release() { sticky.withLock { $0 = false } for d in live.withLock({ $0.compactMap(\.decoder) }) { d.unwedge() } } static var isSticky: Bool { sticky.withLock { $0 } } static func factory() -> RTCVideoDecoderFactory { WedgeableDecoderFactory() } } private struct WeakDecoder { weak var decoder: WedgeableDecoder? } final class WedgeableDecoderFactory: NSObject, RTCVideoDecoderFactory { private let inner = RTCDefaultVideoDecoderFactory() func createDecoder(_ info: RTCVideoCodecInfo) -> RTCVideoDecoder? { guard let decoder = inner.createDecoder(info) else { return nil } // Only the VideoToolbox decoders are Objective-C all the way down. // WebRTC's own VP8, VP9 and AV1 come back as wrapped native decoders, // which the factory unwraps and never calls through this protocol, so // they are handed over as they are and cannot be wedged. guard ["H264", "H265"].contains(info.name.uppercased()) else { return decoder } let wrapped = WedgeableDecoder(decoder) if DecoderWedge.isSticky { wrapped.wedge() } DecoderWedge.register(wrapped) return wrapped } func supportedCodecs() -> [RTCVideoCodecInfo] { inner.supportedCodecs() } } final class WedgeableDecoder: NSObject, RTCVideoDecoder, @unchecked Sendable { private let inner: RTCVideoDecoder private struct State { var wedged = false var area = 0 } private let state = OSAllocatedUnfairLock(initialState: State()) init(_ inner: RTCVideoDecoder) { self.inner = inner } var area: Int { state.withLock { $0.area } } func wedge() { state.withLock { $0.wedged = true } } func unwedge() { state.withLock { $0.wedged = false } } func setCallback(_ callback: @escaping RTCVideoDecoderCallback) { inner.setCallback { [weak self] frame in guard let self else { return } let area = Int(frame.width) * Int(frame.height) let wedged = self.state.withLock { s -> Bool in s.area = area return s.wedged } if !wedged { callback(frame) } } } func startDecode(withNumberOfCores numberOfCores: Int32) -> Int { inner.startDecode(withNumberOfCores: numberOfCores) } func release() -> Int { inner.release() } func decode( _ encodedImage: RTCEncodedImage, missingFrames: Bool, codecSpecificInfo info: RTCCodecSpecificInfo?, renderTimeMs: Int64 ) -> Int { inner.decode(encodedImage, missingFrames: missingFrames, codecSpecificInfo: info, renderTimeMs: renderTimeMs) } func implementationName() -> String { inner.implementationName() } }