@@ -21,23 +21,14 @@ import MetalKit
2121/// A ``NativeViewType`` that conforms to ``RTCVideoRenderer``.
2222public typealias NativeRendererView = NativeViewType & RTCVideoRenderer
2323
24- public class VideoView : NativeView , Loggable {
24+ public class VideoView : NativeView , MulticastDelegateCapable , Loggable {
2525
26- private static let mirrorTransform = CATransform3DMakeScale ( - 1.0 , 1.0 , 1.0 )
26+ public var delegates = MulticastDelegate < DelegateType > ( )
2727
28- /// A set of bool values describing the state of rendering.
29- public struct RenderState : OptionSet {
30- public let rawValue : Int
31- public init ( rawValue: Int ) {
32- self . rawValue = rawValue
33- }
28+ public typealias DelegateType = VideoViewDelegate
3429
35- /// Received first frame and already rendered to the ``VideoView``.
36- /// This can be used to trigger smooth transition of the UI.
37- static let didRenderFirstFrame = RenderState ( rawValue: 1 << 0 )
38- /// ``VideoView`` skipped rendering of a frame that could lead to crashes.
39- static let didSkipUnsafeFrame = RenderState ( rawValue: 1 << 1 )
40- }
30+ private static let mirrorTransform = CATransform3DMakeScale ( - 1.0 , 1.0 , 1.0 )
31+ private static let _freezeDetectThreshold = 2.0
4132
4233 public enum LayoutMode : String , Codable , CaseIterable {
4334 case fit
@@ -69,7 +60,9 @@ public class VideoView: NativeView, Loggable {
6960 _state. mutate {
7061 // reset states if track updated
7162 if !Self. track ( $0. track, isEqualWith: newValue) {
72- $0. renderState = [ ]
63+ $0. renderDate = nil
64+ $0. didRenderFirstFrame = false
65+ $0. isRendering = false
7366 $0. rendererSize = nil
7467 }
7568 $0. track = newValue
@@ -96,6 +89,9 @@ public class VideoView: NativeView, Loggable {
9689 set { _state. mutate { $0. debugMode = newValue } }
9790 }
9891
92+ public var isRendering : Bool { _state. isRendering }
93+ public var didRenderFirstFrame : Bool { _state. didRenderFirstFrame }
94+
9995 private var nativeRenderer : NativeRendererView ?
10096
10197 private var _debugTextView : TextView ?
@@ -113,15 +109,21 @@ public class VideoView: NativeView, Loggable {
113109 var rendererSize : CGSize ?
114110 var didLayout : Bool = false
115111 var layoutMode : LayoutMode = . fill
116-
117112 var mirrorMode : MirrorMode = . auto
118- var renderState = RenderState ( )
119113
120114 var debugMode : Bool = false
115+
116+ // render states
117+ var renderDate : Date ?
118+ var didRenderFirstFrame : Bool = false
119+ var isRendering : Bool = false
121120 }
122121
123122 internal var _state : StateSync < State >
124123
124+ // used for stats timer
125+ private lazy var _renderTimer = DispatchQueueTimer ( timeInterval: 0.1 )
126+
125127 public override init ( frame: CGRect = . zero) {
126128
127129 // should always be on main thread
@@ -203,14 +205,23 @@ public class VideoView: NativeView, Loggable {
203205 }
204206 }
205207
206- // renderState updated
207- if state. renderState != oldState. renderState, let track = state. track {
208- track. notify { $0. track ( track, videoView: self , didUpdate: state. renderState) }
208+ // isRendering updated
209+ if state. isRendering != oldState. isRendering {
210+
211+ self . log ( " isRendering \( oldState. isRendering) -> \( state. isRendering) " )
212+
213+ if state. isRendering {
214+ self . _renderTimer. restart ( )
215+ } else {
216+ self . _renderTimer. suspend ( )
217+ }
218+
219+ self . notify { $0. videoView ( self , didUpdate: state. isRendering) }
209220 }
210221
211222 // viewSize updated
212- if state. viewSize != oldState. viewSize, let track = state . track {
213- track . notify { $0. track ( track , videoView: self , didUpdate: state. viewSize) }
223+ if state. viewSize != oldState. viewSize {
224+ self . notify { $0. videoView ( self , didUpdate: state. viewSize) }
214225 }
215226
216227 // toggle MTKView's isPaused property
@@ -222,6 +233,7 @@ public class VideoView: NativeView, Loggable {
222233 if state. debugMode != oldState. debugMode ||
223234 state. layoutMode != oldState. layoutMode ||
224235 state. mirrorMode != oldState. mirrorMode ||
236+ state. didRenderFirstFrame != oldState. didRenderFirstFrame ||
225237 shouldRenderDidUpdate || trackDidUpdate {
226238
227239 // must be on main
@@ -230,6 +242,18 @@ public class VideoView: NativeView, Loggable {
230242 }
231243 }
232244 }
245+
246+ _renderTimer. handler = { [ weak self] in
247+
248+ guard let self = self else { return }
249+
250+ if self . _state. isRendering, let renderDate = self . _state. renderDate {
251+ let diff = Date ( ) . timeIntervalSince ( renderDate)
252+ if diff >= Self . _freezeDetectThreshold {
253+ self . _state. mutate { $0. isRendering = false }
254+ }
255+ }
256+ }
233257 }
234258
235259 required init ? ( coder: NSCoder ) {
@@ -261,11 +285,12 @@ public class VideoView: NativeView, Loggable {
261285 if _state. debugMode {
262286 let _trackSid = _state. track? . sid ?? " nil "
263287 let _dimensions = _state. track? . dimensions ?? . zero
264- let _didRenderFirstFrame = _state. renderState. contains ( . didRenderFirstFrame) ? " true " : " false "
288+ let _didRenderFirstFrame = _state. didRenderFirstFrame ? " true " : " false "
289+ let _isRendering = _state. isRendering ? " true " : " false "
265290 let _viewCount = _state. track? . videoViews. count ?? 0
266291 let _didLayout = _state. didLayout
267292 let debugView = ensureDebugTextView ( )
268- debugView. text = " # \( hashValue) \n " + " \( _trackSid) \n " + " \( _dimensions. width) x \( _dimensions. height) \n " + " enabled: \( isEnabled) \n " + " firstFrame: \( _didRenderFirstFrame) \n " + " viewCount: \( _viewCount) \n " + " layout: \( _didLayout) "
293+ debugView. text = " # \( hashValue) \n " + " \( _trackSid) \n " + " \( _dimensions. width) x \( _dimensions. height) \n " + " enabled: \( isEnabled) \n " + " firstFrame: \( _didRenderFirstFrame) \n " + " isRendering: \( _isRendering ) \n " + " viewCount: \( _viewCount) \n " + " layout: \( _didLayout) "
269294 debugView. frame = bounds
270295 #if os(iOS)
271296 debugView. layer. borderColor = ( _state. shouldRender ? UIColor . green : UIColor . red) . withAlphaComponent ( 0.5 ) . cgColor
@@ -456,10 +481,10 @@ extension VideoView: RTCVideoRenderer {
456481 // cache last rendered frame
457482 track? . set ( videoFrame: frame)
458483
459- if ! _state. renderState . contains ( . didRenderFirstFrame ) {
460- _state . mutate { $0. renderState . insert ( . didRenderFirstFrame) }
461- self . log ( " did render first frame, track: \( String ( describing : track ) ) " )
462- _needsLayout = true
484+ _state. mutateAsync {
485+ $0. didRenderFirstFrame = true
486+ $0 . isRendering = true
487+ $0 . renderDate = Date ( )
463488 }
464489 }
465490}
0 commit comments