diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 4c3250429..de3f64f48 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -356,6 +356,10 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { webRTCSession.attachLocalRenderer(renderer) } + func removeLocalVideoRenderer(_ renderer: RTCVideoRenderer) { + webRTCSession.removeLocalRenderer(renderer) + } + // MARK: - Delegate public func webRTCIsConnected() { diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index 49152e981..f98901d45 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -8,6 +8,9 @@ import SessionMessagingKit import SessionUtilitiesKit final class CallVC: UIViewController, VideoPreviewDelegate { + static let floatingVideoViewWidth: CGFloat = UIDevice.current.isIPad ? 160 : 80 + static let floatingVideoViewHeight: CGFloat = UIDevice.current.isIPad ? 346: 173 + let call: SessionCall var latestKnownAudioOutputDeviceName: String? var durationTimer: Timer? @@ -21,27 +24,90 @@ final class CallVC: UIViewController, VideoPreviewDelegate { return result }() + enum FloatingViewVideoSource { + case local + case remote + } + + var floatingViewVideoSource: FloatingViewVideoSource = .local + // MARK: - UI Components - private lazy var localVideoView: LocalVideoView = { + private lazy var floatingLocalVideoView: LocalVideoView = { let result = LocalVideoView() - result.clipsToBounds = true + result.alpha = 0 result.themeBackgroundColor = .backgroundSecondary - result.isHidden = !call.isVideoEnabled - result.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10 - result.layer.masksToBounds = true - result.set(.width, to: LocalVideoView.width) - result.set(.height, to: LocalVideoView.height) - result.makeViewDraggable() + result.set(.width, to: Self.floatingVideoViewWidth) + result.set(.height, to: Self.floatingVideoViewHeight) + + return result + }() + + private lazy var floatingRemoteVideoView: RemoteVideoView = { + let result = RemoteVideoView() + result.alpha = 0 + result.themeBackgroundColor = .backgroundSecondary + result.set(.width, to: Self.floatingVideoViewWidth) + result.set(.height, to: Self.floatingVideoViewHeight) + + return result + }() + + private lazy var fullScreenLocalVideoView: LocalVideoView = { + let result = LocalVideoView() + result.alpha = 0 + result.themeBackgroundColor = .backgroundPrimary + result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleFullScreenVideoViewTapped))) return result }() - private lazy var remoteVideoView: RemoteVideoView = { + private lazy var fullScreenRemoteVideoView: RemoteVideoView = { let result = RemoteVideoView() result.alpha = 0 result.themeBackgroundColor = .backgroundPrimary - result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleRemoteVieioViewTapped))) + result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleFullScreenVideoViewTapped))) + + return result + }() + + private lazy var floatingViewContainer: UIView = { + let result = UIView() + result.isHidden = true + result.clipsToBounds = true + result.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10 + result.layer.masksToBounds = true + result.themeBackgroundColor = .backgroundSecondary + result.makeViewDraggable() + + let noVideoIcon: UIImageView = UIImageView( + image: UIImage(systemName: "video.slash")? + .withRenderingMode(.alwaysTemplate) + ) + noVideoIcon.themeTintColor = .textPrimary + noVideoIcon.set(.width, to: 34) + noVideoIcon.set(.height, to: 28) + result.addSubview(noVideoIcon) + noVideoIcon.center(in: result) + + result.addSubview(floatingLocalVideoView) + floatingLocalVideoView.pin(to: result) + + result.addSubview(floatingRemoteVideoView) + floatingRemoteVideoView.pin(to: result) + + let swappingVideoIcon: UIImageView = UIImageView( + image: UIImage(systemName: "arrow.2.squarepath")? + .withRenderingMode(.alwaysTemplate) + ) + swappingVideoIcon.themeTintColor = .textPrimary + swappingVideoIcon.set(.width, to: 16) + swappingVideoIcon.set(.height, to: 12) + result.addSubview(swappingVideoIcon) + swappingVideoIcon.pin(.top, to: .top, of: result, withInset: Values.smallSpacing) + swappingVideoIcon.pin(.trailing, to: .trailing, of: result, withInset: -Values.smallSpacing) + + result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(switchVideo))) return result }() @@ -265,7 +331,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate { self.call.remoteVideoStateDidChange = { isEnabled in DispatchQueue.main.async { UIView.animate(withDuration: 0.25) { - self.remoteVideoView.alpha = isEnabled ? 1 : 0 + let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView + remoteVideoView.alpha = isEnabled ? 1 : 0 } if self.callInfoLabel.alpha < 0.5 { @@ -375,13 +442,16 @@ final class CallVC: UIViewController, VideoPreviewDelegate { view.addSubview(profilePictureContainer) // Remote video view - call.attachRemoteVideoRenderer(remoteVideoView) - view.addSubview(remoteVideoView) - remoteVideoView.translatesAutoresizingMaskIntoConstraints = false - remoteVideoView.pin(to: view) + call.attachRemoteVideoRenderer(fullScreenRemoteVideoView) + view.addSubview(fullScreenRemoteVideoView) + fullScreenRemoteVideoView.translatesAutoresizingMaskIntoConstraints = false + fullScreenRemoteVideoView.pin(to: view) // Local video view - call.attachLocalVideoRenderer(localVideoView) + call.attachLocalVideoRenderer(floatingLocalVideoView) + view.addSubview(fullScreenLocalVideoView) + fullScreenLocalVideoView.translatesAutoresizingMaskIntoConstraints = false + fullScreenLocalVideoView.pin(to: view) // Fade view view.addSubview(fadeView) @@ -431,12 +501,12 @@ final class CallVC: UIViewController, VideoPreviewDelegate { callDurationLabel.center(in: callInfoLabelContainer) } - private func addLocalVideoView() { + private func addFloatingVideoView() { let safeAreaInsets = UIApplication.shared.keyWindow?.safeAreaInsets - CurrentAppContext().mainWindow?.addSubview(localVideoView) - localVideoView.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing) + CurrentAppContext().mainWindow?.addSubview(floatingViewContainer) + floatingViewContainer.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing) let topMargin = (safeAreaInsets?.top ?? 0) + Values.veryLargeSpacing - localVideoView.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin) + floatingViewContainer.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin) } override func viewDidAppear(_ animated: Bool) { @@ -445,7 +515,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate { if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() } shouldRestartCamera = true - addLocalVideoView() + addFloatingVideoView() + let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView remoteVideoView.alpha = (call.isRemoteVideoEnabled ? 1 : 0) } @@ -454,7 +525,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() } - localVideoView.removeFromSuperview() + floatingViewContainer.removeFromSuperview() } // MARK: - Orientation @@ -501,7 +572,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate { self.callInfoLabel.text = "Call Ended" UIView.animate(withDuration: 0.25) { - self.remoteVideoView.alpha = 0 + let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView + remoteVideoView.alpha = 0 self.operationPanel.alpha = 1 self.responsePanel.alpha = 1 self.callInfoLabel.alpha = 1 @@ -559,7 +631,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { @objc private func operateCamera() { if (call.isVideoEnabled) { - localVideoView.isHidden = true + floatingViewContainer.isHidden = true cameraManager.stop() videoButton.themeTintColor = .textPrimary videoButton.themeBackgroundColor = .backgroundSecondary @@ -575,7 +647,9 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } func cameraDidConfirmTurningOn() { - localVideoView.isHidden = false + floatingViewContainer.isHidden = false + let localVideoView: LocalVideoView = self.floatingViewVideoSource == .local ? self.floatingLocalVideoView : self.fullScreenLocalVideoView + localVideoView.alpha = 1 cameraManager.prepare() cameraManager.start() videoButton.themeTintColor = .backgroundSecondary @@ -584,6 +658,34 @@ final class CallVC: UIViewController, VideoPreviewDelegate { call.isVideoEnabled = true } + @objc private func switchVideo() { + if self.floatingViewVideoSource == .remote { + call.removeRemoteVideoRenderer(self.floatingRemoteVideoView) + call.removeLocalVideoRenderer(self.fullScreenLocalVideoView) + + self.floatingRemoteVideoView.alpha = 0 + self.floatingLocalVideoView.alpha = call.isVideoEnabled ? 1 : 0 + self.fullScreenRemoteVideoView.alpha = call.isRemoteVideoEnabled ? 1 : 0 + self.fullScreenLocalVideoView.alpha = 0 + + self.floatingViewVideoSource = .local + call.attachRemoteVideoRenderer(self.fullScreenRemoteVideoView) + call.attachLocalVideoRenderer(self.floatingLocalVideoView) + } else { + call.removeRemoteVideoRenderer(self.fullScreenRemoteVideoView) + call.removeLocalVideoRenderer(self.floatingLocalVideoView) + + self.floatingRemoteVideoView.alpha = call.isRemoteVideoEnabled ? 1 : 0 + self.floatingLocalVideoView.alpha = 0 + self.fullScreenRemoteVideoView.alpha = 0 + self.fullScreenLocalVideoView.alpha = call.isVideoEnabled ? 1 : 0 + + self.floatingViewVideoSource = .remote + call.attachRemoteVideoRenderer(self.floatingRemoteVideoView) + call.attachLocalVideoRenderer(self.fullScreenLocalVideoView) + } + } + @objc private func switchCamera() { cameraManager.switchCamera() } @@ -645,7 +747,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } } - @objc private func handleRemoteVieioViewTapped(gesture: UITapGestureRecognizer) { + @objc private func handleFullScreenVideoViewTapped(gesture: UITapGestureRecognizer) { let isHidden = callDurationLabel.alpha < 0.5 UIView.animate(withDuration: 0.5) { diff --git a/Session/Calls/Views & Modals/CallVideoView.swift b/Session/Calls/Views & Modals/CallVideoView.swift index 899732f66..d2e8894c0 100644 --- a/Session/Calls/Views & Modals/CallVideoView.swift +++ b/Session/Calls/Views & Modals/CallVideoView.swift @@ -87,10 +87,7 @@ class RemoteVideoView: TargetView { // MARK: LocalVideoView class LocalVideoView: TargetView { - - static let width: CGFloat = UIDevice.current.isIPad ? 160 : 80 - static let height: CGFloat = UIDevice.current.isIPad ? 346: 173 - + override func renderFrame(_ frame: RTCVideoFrame?) { super.renderFrame(frame) DispatchMainThreadSafe { diff --git a/SessionMessagingKit/Calls/WebRTCSession+UI.swift b/SessionMessagingKit/Calls/WebRTCSession+UI.swift index 305d1bfb7..2594d8a15 100644 --- a/SessionMessagingKit/Calls/WebRTCSession+UI.swift +++ b/SessionMessagingKit/Calls/WebRTCSession+UI.swift @@ -6,6 +6,10 @@ extension WebRTCSession { localVideoTrack.add(renderer) } + public func removeLocalRenderer(_ renderer: RTCVideoRenderer) { + localVideoTrack.remove(renderer) + } + public func attachRemoteRenderer(_ renderer: RTCVideoRenderer) { remoteVideoTrack?.add(renderer) }