From ee27d22dc855d39e976ca30098df5ca2e6557fe3 Mon Sep 17 00:00:00 2001 From: Bogdan Vatamanu Date: Wed, 28 Aug 2024 17:10:20 +0300 Subject: [PATCH 1/2] Add video pausing option on VideoView --- .../LiveKit/SwiftUI/SwiftUIVideoView.swift | 4 ++ Sources/LiveKit/Views/VideoView.swift | 46 ++++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/Sources/LiveKit/SwiftUI/SwiftUIVideoView.swift b/Sources/LiveKit/SwiftUI/SwiftUIVideoView.swift index 1a41fe33f..ef5e94693 100644 --- a/Sources/LiveKit/SwiftUI/SwiftUIVideoView.swift +++ b/Sources/LiveKit/SwiftUI/SwiftUIVideoView.swift @@ -29,6 +29,7 @@ public struct SwiftUIVideoView: NativeViewRepresentable { let renderMode: VideoView.RenderMode let rotationOverride: VideoRotation? let pinchToZoomOptions: VideoView.PinchToZoomOptions + let isPaused: Bool let isDebugMode: Bool let videoViewDelegateReceiver: VideoViewDelegateReceiver @@ -40,6 +41,7 @@ public struct SwiftUIVideoView: NativeViewRepresentable { rotationOverride: VideoRotation? = nil, pinchToZoomOptions: VideoView.PinchToZoomOptions = [], isDebugMode: Bool = false, + isPaused: Bool = false, isRendering: Binding? = nil) { self.track = track @@ -48,6 +50,7 @@ public struct SwiftUIVideoView: NativeViewRepresentable { self.renderMode = renderMode self.rotationOverride = rotationOverride self.isDebugMode = isDebugMode + self.isPaused = isPaused self.pinchToZoomOptions = pinchToZoomOptions videoViewDelegateReceiver = VideoViewDelegateReceiver(isRendering: isRendering) @@ -66,6 +69,7 @@ public struct SwiftUIVideoView: NativeViewRepresentable { videoView.renderMode = renderMode videoView.rotationOverride = rotationOverride videoView.pinchToZoomOptions = pinchToZoomOptions + videoView.isPaused = isPaused videoView.isDebugMode = isDebugMode Task.detached { @MainActor in diff --git a/Sources/LiveKit/Views/VideoView.swift b/Sources/LiveKit/Views/VideoView.swift index 2e50002e4..23683bf7a 100644 --- a/Sources/LiveKit/Views/VideoView.swift +++ b/Sources/LiveKit/Views/VideoView.swift @@ -118,6 +118,14 @@ public class VideoView: NativeView, Loggable { get { _state.isEnabled } set { _state.mutate { $0.isEnabled = newValue } } } + + @objc + public var isPaused: Bool { + get { _state.isPaused } + set { _state.mutate { $0.isPaused = newValue } } + } + + private var pausedVideoFrame: VideoFrame? @objc override public var isHidden: Bool { @@ -190,6 +198,7 @@ public class VideoView: NativeView, Loggable { struct State { weak var track: Track? var isEnabled: Bool = true + var isPaused: Bool = false var isHidden: Bool = false // layout related @@ -221,9 +230,11 @@ public class VideoView: NativeView, Loggable { var captureOptions: VideoCaptureOptions? = nil var captureDevice: AVCaptureDevice? = nil + var lastRenderedFrame: VideoFrame? + // whether if current state should be rendering var shouldRender: Bool { - track != nil && isEnabled && !isHidden + track != nil && isEnabled && !isHidden && !isPaused } } @@ -284,15 +295,18 @@ public class VideoView: NativeView, Loggable { // clean up old track if let track = oldState.track as? VideoTrack { track.remove(videoRenderer: self) - - if let r = self._primaryRenderer { - r.removeFromSuperview() - self._primaryRenderer = nil - } - - if let r = self._secondaryRenderer { - r.removeFromSuperview() - self._secondaryRenderer = nil + + // avoid destroying and re-creating the + if !newState.isPaused { + if let r = self._primaryRenderer { + r.removeFromSuperview() + self._primaryRenderer = nil + } + + if let r = self._secondaryRenderer { + r.removeFromSuperview() + self._secondaryRenderer = nil + } } } @@ -305,7 +319,7 @@ public class VideoView: NativeView, Loggable { track.add(videoRenderer: self) if let frame = track._state.videoFrame { - self.log("rendering cached frame tack: \(String(describing: track._state.sid))") + self.log("rendering cached frame track: \(String(describing: track._state.sid))") nr.renderFrame(frame.toRTCType()) self.setNeedsLayout() } @@ -313,7 +327,14 @@ public class VideoView: NativeView, Loggable { } if renderModeDidUpdate, !didReCreateNativeRenderer { - self.recreatePrimaryRenderer(for: newState.renderMode) + let nr = self.recreatePrimaryRenderer(for: newState.renderMode) + + // re-render last rendered frame before pause + if let frame = newState.lastRenderedFrame, newState.isPaused { + self.log("rendering last rendered frame before pause") + nr.renderFrame(frame.toRTCType()) + self.setNeedsLayout() + } } } } @@ -625,6 +646,7 @@ extension VideoView: VideoRenderer { $0.didRenderFirstFrame = true $0.isRendering = true $0.renderDate = Date() + $0.lastRenderedFrame = frame // Update renderTarget if capture position changes if let oldCaptureDevicePosition, oldCaptureDevicePosition != captureDevice?.position { From f093ccd348f70cdae3b3ae439df9210b95870c4d Mon Sep 17 00:00:00 2001 From: Bogdan Vatamanu Date: Wed, 28 Aug 2024 18:14:04 +0300 Subject: [PATCH 2/2] Fix unfinished comment --- Sources/LiveKit/Views/VideoView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/LiveKit/Views/VideoView.swift b/Sources/LiveKit/Views/VideoView.swift index 23683bf7a..e5cad0773 100644 --- a/Sources/LiveKit/Views/VideoView.swift +++ b/Sources/LiveKit/Views/VideoView.swift @@ -296,7 +296,7 @@ public class VideoView: NativeView, Loggable { if let track = oldState.track as? VideoTrack { track.remove(videoRenderer: self) - // avoid destroying and re-creating the + // avoid destroying and re-creating the renderers when the video is paused if !newState.isPaused { if let r = self._primaryRenderer { r.removeFromSuperview()