| 1 | import Foundation |
| 2 | import LiveKitWebRTC |
| 3 | import os |
| 4 | |
| 5 | /// For the gate only (spec §15.22): a receiver whose screen decoder has |
| 6 | /// wedged. Packets keep arriving, frames keep being handed to the decoder, |
| 7 | /// and nothing comes out — which no keyframe mends, because it is this |
| 8 | /// decoder instance that is stuck, and which a new transceiver does mend, |
| 9 | /// because the far end's new stream gets a decoder of its own. That is the |
| 10 | /// shape of a stall that "keyframes only" (0.4.3) left sitting at |
| 11 | /// "recovering". |
| 12 | /// |
| 13 | /// Off unless the interop executable turns it on before the call is |
| 14 | /// built; the apps never do, and then the call's factory is WebRTC's own. |
| 15 | @_spi(Fixtures) public enum DecoderWedge { |
| 16 | /// Wrap the call's decoders so they can be wedged. Read once, when the |
| 17 | /// call's factory is made. |
| 18 | nonisolated(unsafe) public static var enabled = false |
| 19 | |
| 20 | |
| 21 | private static let live = OSAllocatedUnfairLock(initialState: [WeakDecoder]()) |
| 22 | private static let sticky = OSAllocatedUnfairLock(initialState: false) |
| 23 | |
| 24 | static func register(_ decoder: WedgeableDecoder) { |
| 25 | live.withLock { list in |
| 26 | list.removeAll { $0.decoder == nil } |
| 27 | list.append(WeakDecoder(decoder: decoder)) |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | /// Wedges the decoder now putting out the largest picture, which in the |
| 32 | /// gate is always the screen (its cameras are 320×240 to 640×480, and a |
| 33 | /// screen even at its smallest under a thin link is wider). `sticky` |
| 34 | /// wedges every decoder made afterwards too — only a screen's rebuild or |
| 35 | /// share again makes one mid-call — for a receiver no rebuild can mend, |
| 36 | /// the gate's "could not restore". Returns how many were wedged. |
| 37 | @discardableResult public static func wedgeScreens(sticky: Bool = false) -> Int { |
| 38 | self.sticky.withLock { $0 = sticky } |
| 39 | let decoders = live.withLock { $0.compactMap(\.decoder) } |
| 40 | guard let largest = decoders.map(\.area).max(), largest > 0 else { return 0 } |
| 41 | var count = 0 |
| 42 | for d in decoders where d.area == largest { |
| 43 | d.wedge() |
| 44 | count += 1 |
| 45 | } |
| 46 | return count |
| 47 | } |
| 48 | |
| 49 | /// Every wedge lifted, and nothing wedged from now on. |
| 50 | public static func release() { |
| 51 | sticky.withLock { $0 = false } |
| 52 | for d in live.withLock({ $0.compactMap(\.decoder) }) { d.unwedge() } |
| 53 | } |
| 54 | |
| 55 | static var isSticky: Bool { sticky.withLock { $0 } } |
| 56 | |
| 57 | static func factory() -> RTCVideoDecoderFactory { WedgeableDecoderFactory() } |
| 58 | } |
| 59 | |
| 60 | private struct WeakDecoder { |
| 61 | weak var decoder: WedgeableDecoder? |
| 62 | } |
| 63 | |
| 64 | final class WedgeableDecoderFactory: NSObject, RTCVideoDecoderFactory { |
| 65 | private let inner = RTCDefaultVideoDecoderFactory() |
| 66 | |
| 67 | func createDecoder(_ info: RTCVideoCodecInfo) -> RTCVideoDecoder? { |
| 68 | guard let decoder = inner.createDecoder(info) else { return nil } |
| 69 | // Only the VideoToolbox decoders are Objective-C all the way down. |
| 70 | // WebRTC's own VP8, VP9 and AV1 come back as wrapped native decoders, |
| 71 | // which the factory unwraps and never calls through this protocol, so |
| 72 | // they are handed over as they are and cannot be wedged. |
| 73 | guard ["H264", "H265"].contains(info.name.uppercased()) else { return decoder } |
| 74 | let wrapped = WedgeableDecoder(decoder) |
| 75 | if DecoderWedge.isSticky { wrapped.wedge() } |
| 76 | DecoderWedge.register(wrapped) |
| 77 | return wrapped |
| 78 | } |
| 79 | |
| 80 | func supportedCodecs() -> [RTCVideoCodecInfo] { inner.supportedCodecs() } |
| 81 | } |
| 82 | |
| 83 | final class WedgeableDecoder: NSObject, RTCVideoDecoder, @unchecked Sendable { |
| 84 | private let inner: RTCVideoDecoder |
| 85 | private struct State { |
| 86 | var wedged = false |
| 87 | var area = 0 |
| 88 | } |
| 89 | private let state = OSAllocatedUnfairLock(initialState: State()) |
| 90 | |
| 91 | init(_ inner: RTCVideoDecoder) { self.inner = inner } |
| 92 | |
| 93 | var area: Int { state.withLock { $0.area } } |
| 94 | func wedge() { state.withLock { $0.wedged = true } } |
| 95 | func unwedge() { state.withLock { $0.wedged = false } } |
| 96 | |
| 97 | func setCallback(_ callback: @escaping RTCVideoDecoderCallback) { |
| 98 | inner.setCallback { [weak self] frame in |
| 99 | guard let self else { return } |
| 100 | let area = Int(frame.width) * Int(frame.height) |
| 101 | let wedged = self.state.withLock { s -> Bool in |
| 102 | s.area = area |
| 103 | return s.wedged |
| 104 | } |
| 105 | if !wedged { callback(frame) } |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | func startDecode(withNumberOfCores numberOfCores: Int32) -> Int { |
| 110 | inner.startDecode(withNumberOfCores: numberOfCores) |
| 111 | } |
| 112 | |
| 113 | func release() -> Int { inner.release() } |
| 114 | |
| 115 | func decode( |
| 116 | _ encodedImage: RTCEncodedImage, missingFrames: Bool, codecSpecificInfo info: RTCCodecSpecificInfo?, |
| 117 | renderTimeMs: Int64 |
| 118 | ) -> Int { |
| 119 | inner.decode(encodedImage, missingFrames: missingFrames, codecSpecificInfo: info, renderTimeMs: renderTimeMs) |
| 120 | } |
| 121 | |
| 122 | func implementationName() -> String { inner.implementationName() } |
| 123 | } |
| 124 | |