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..e5cad0773 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 renderers when the video is paused + 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 {