From c24a6da36ebcb3855edecbdc8ebcc0b8d7c41b60 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sat, 13 Jan 2024 01:50:47 +0100 Subject: [PATCH] Improve video call UI TODO: missing camera switching --- Monal/Classes/AVCallUI.swift | 606 ++++++++++++++++--------------- Monal/Classes/MLCall.h | 1 + Monal/Classes/MLCall.m | 5 + Monal/Classes/WebRTCClient.swift | 22 ++ 4 files changed, 349 insertions(+), 285 deletions(-) diff --git a/Monal/Classes/AVCallUI.swift b/Monal/Classes/AVCallUI.swift index 176e2c7edd..aa97ddb9a3 100644 --- a/Monal/Classes/AVCallUI.swift +++ b/Monal/Classes/AVCallUI.swift @@ -22,6 +22,7 @@ struct VideoView: UIViewRepresentable { } func updateUIView(_ renderer: RTCMTLVideoView, context: Context) { + DDLogDebug("updateUIView called...") //do nothing } } @@ -32,6 +33,7 @@ struct AVCallUI: View { @StateObject private var contact: ObservableKVOWrapper @State private var showMicAlert = false @State private var showSecurityHelpAlert: MLCallEncryptionState? = nil + @State private var controlsVisible = true private var ringingPlayer: AVAudioPlayer! private var busyPlayer: AVAudioPlayer! private var errorPlayer: AVAudioPlayer! @@ -50,20 +52,27 @@ struct AVCallUI: View { self.formatter.unitsStyle = .positional self.formatter.zeroFormattingBehavior = .pad - //use the complete screen and resize later using swiftui - self.localRenderer = RTCMTLVideoView(frame: CGRect( - origin: CGPoint.zero, - size: CGSize(width:320, height:200) - )) + //use the complete screen for remote video self.remoteRenderer = RTCMTLVideoView(frame: UIScreen.main.bounds) - self.localRenderer.videoContentMode = .scaleAspectFill self.remoteRenderer.videoContentMode = .scaleAspectFill + self.localRenderer = RTCMTLVideoView(frame: UIScreen.main.bounds) + self.localRenderer.videoContentMode = .scaleAspectFill + self.localRenderer.transform = CGAffineTransformMakeScale(-1.0, 1.0) //local video should be displayed as "mirrored" + self.ringingPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"ringing", withExtension:"wav", subdirectory:"CallSounds")!) self.busyPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"busy", withExtension:"wav", subdirectory:"CallSounds")!) self.errorPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"error", withExtension:"wav", subdirectory:"CallSounds")!) } + func maybeStartRenderer() { + if MLCallType(rawValue:call.callType) == .video && MLCallState(rawValue:call.state) == .connected { + DDLogError("Starting renderer...") + call.obj.startCaptureLocalVideo(withRenderer: self.localRenderer) + call.obj.renderRemoteVideo(withRenderer: self.remoteRenderer) + } + } + func handleStateChange(_ state:MLCallState, _ audioState:MLAudioState) { switch state { case .unknown: @@ -93,10 +102,10 @@ struct AVCallUI: View { errorPlayer.stop() case .connected: DDLogDebug("state: connected") - if MLCallType(rawValue:call.callType) == .video { - call.obj.startCaptureLocalVideo(withRenderer: self.localRenderer) - call.obj.renderRemoteVideo(withRenderer: self.remoteRenderer) - } + maybeStartRenderer() + //we want our controls to disappear when we first connected, but want them to be visible when returning to a call + //--> don't set controlsVisible to false in maybeStartRenderer(), but only here + controlsVisible = false case .finished: DDLogDebug("state: finished: \(String(describing:call.finishReason as NSNumber))") //check audio state before trying to play anything (if we are still in state .call, @@ -162,363 +171,382 @@ struct AVCallUI: View { Color.background .edgesIgnoringSafeArea(.all) - if MLCallType(rawValue:call.callType) == .video { + if MLCallType(rawValue:call.callType) == .video && MLCallState(rawValue:call.state) == .connected { if MLCallState(rawValue:call.state) == .connected { VideoView(renderer:self.remoteRenderer) - .border(.green) } - if MLCallState(rawValue:call.state) == .connected { - VideoView(renderer:self.localRenderer) - .frame(width: 320.0, height: 200.0) - .border(.red) - } - } - - VStack { - Group { - Spacer().frame(height: 24) + VStack { + Spacer().frame(height: 16) - HStack(alignment: .top) { - Spacer().frame(width:20) + HStack { + Spacer() - VStack { - Spacer().frame(height: 8) - switch MLCallDirection(rawValue:call.direction) { - case .incoming: - Image(systemName: "phone.arrow.down.left") - .resizable() - .frame(width: 20.0, height: 20.0) - .foregroundColor(.primary) - case .outgoing: - Image(systemName: "phone.arrow.up.right") - .resizable() - .frame(width: 20.0, height: 20.0) - .foregroundColor(.primary) - default: //should never be reached - Text("") - } + if MLCallState(rawValue:call.state) == .connected { + VideoView(renderer:self.localRenderer) + //this will sometimes only honor the width and ignore the height + .frame(width: UIScreen.main.bounds.size.width/5.0, height: UIScreen.main.bounds.size.height/5.0) } - VStack { - Spacer().frame(height: 8) - Button(action: { - //show dialog explaining different encryption states - self.showSecurityHelpAlert = MLCallEncryptionState(rawValue:call.encryptionState) - }, label: { - switch MLCallEncryptionState(rawValue:call.encryptionState) { - case .unknown: - Text("") - case .clear: - Spacer().frame(width: 10) - Image(systemName: "xmark.shield.fill") - .resizable() - .frame(width: 20.0, height: 20.0) - .foregroundColor(.red) - case .toFU: - Spacer().frame(width: 10) - Image(systemName: "checkmark.shield.fill") + Spacer().frame(width: 24) + } + + Spacer() + } + } + + if MLCallType(rawValue:call.callType) == .audio || + (MLCallType(rawValue:call.callType) == .video && (MLCallState(rawValue:call.state) != .connected || controlsVisible)) { + VStack { + Group { + Spacer().frame(height: 24) + + HStack(alignment: .top) { + Spacer().frame(width:20) + + VStack { + Spacer().frame(height: 8) + switch MLCallDirection(rawValue:call.direction) { + case .incoming: + Image(systemName: "phone.arrow.down.left") .resizable() .frame(width: 20.0, height: 20.0) - .foregroundColor(.yellow) - case .trusted: - Spacer().frame(width: 10) - Image(systemName: "checkmark.shield.fill") + .foregroundColor(.primary) + case .outgoing: + Image(systemName: "phone.arrow.up.right") .resizable() .frame(width: 20.0, height: 20.0) - .foregroundColor(.green) + .foregroundColor(.primary) default: //should never be reached Text("") } - }) + } + + VStack { + Spacer().frame(height: 8) + Button(action: { + //show dialog explaining different encryption states + self.showSecurityHelpAlert = MLCallEncryptionState(rawValue:call.encryptionState) + }, label: { + switch MLCallEncryptionState(rawValue:call.encryptionState) { + case .unknown: + Text("") + case .clear: + Spacer().frame(width: 10) + Image(systemName: "xmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.red) + case .toFU: + Spacer().frame(width: 10) + Image(systemName: "checkmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.yellow) + case .trusted: + Spacer().frame(width: 10) + Image(systemName: "checkmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.green) + default: //should never be reached + Text("") + } + }) + } + + Spacer() + + Text(contact.contactDisplayName as String) + .font(.largeTitle) + .foregroundColor(.primary) + + Spacer() + + VStack { + Spacer().frame(height: 8) + Button(action: { + self.delegate.dismissWithoutAnimation() + if let activeChats = self.appDelegate.obj.activeChats { + activeChats.presentChat(with:self.contact.obj) + } + }, label: { + Image(systemName: "text.bubble") + .resizable() + .frame(width: 28.0, height: 28.0) + .foregroundColor(.primary) + }) + } + + Spacer().frame(width:20) } - Spacer() - - Text(contact.contactDisplayName as String) - .font(.largeTitle) - .foregroundColor(.primary) + Spacer().frame(height: 16) - Spacer() - - VStack { - Spacer().frame(height: 8) - Button(action: { - self.delegate.dismissWithoutAnimation() - if let activeChats = self.appDelegate.obj.activeChats { - activeChats.presentChat(with:self.contact.obj) - } - }, label: { - Image(systemName: "text.bubble") - .resizable() - .frame(width: 28.0, height: 28.0) - .foregroundColor(.primary) - }) - } - - Spacer().frame(width:20) - } - - Spacer().frame(height: 16) - - //this is needed because ObservableKVOWrapper somehow extracts an NSNumber? from it's wrapped object - //which results in a runtime error when trying to cast NSNumber? to MLCallState - switch MLCallState(rawValue:call.state) { - case .discovering: - Text("Discovering devices...") - .bold() - .foregroundColor(.primary) - case .ringing: - Text("Ringing...") - .bold() - .foregroundColor(.primary) - case .connecting: - Text("Connecting...") - .bold() - .foregroundColor(.primary) - case .reconnecting: - Text("Reconnecting...") - .bold() - .foregroundColor(.primary) - case .connected: - Text("Connected: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)") - .bold() - .foregroundColor(.primary) - case .finished: - switch MLCallFinishReason(rawValue:call.finishReason) { - case .unknown: - Text("Call ended for an unknown reason") - .bold() - .foregroundColor(.primary) - case .normal: - if call.wasConnectedOnce { - Text("Call ended, duration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)") + //this is needed because ObservableKVOWrapper somehow extracts an NSNumber? from it's wrapped object + //which results in a runtime error when trying to cast NSNumber? to MLCallState + switch MLCallState(rawValue:call.state) { + case .discovering: + Text("Discovering devices...") + .bold() + .foregroundColor(.primary) + case .ringing: + Text("Ringing...") + .bold() + .foregroundColor(.primary) + case .connecting: + Text("Connecting...") + .bold() + .foregroundColor(.primary) + case .reconnecting: + Text("Reconnecting...") + .bold() + .foregroundColor(.primary) + case .connected: + Text("Connected: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)") + .bold() + .foregroundColor(.primary) + case .finished: + switch MLCallFinishReason(rawValue:call.finishReason) { + case .unknown: + Text("Call ended for an unknown reason") .bold() .foregroundColor(.primary) - } else { - Text("Call ended") + case .normal: + if call.wasConnectedOnce { + Text("Call ended, duration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)") + .bold() + .foregroundColor(.primary) + } else { + Text("Call ended") + .bold() + .foregroundColor(.primary) + } + case .connectivityError: + if call.wasConnectedOnce { + Text("Call ended: connection failed\nDuration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)") + .bold() + .foregroundColor(.primary) + } else { + Text("Call ended: connection failed") + .bold() + .foregroundColor(.primary) + } + case .securityError: + Text("Call ended: couldn't establish encryption") .bold() .foregroundColor(.primary) - } - case .connectivityError: - if call.wasConnectedOnce { - Text("Call ended: connection failed\nDuration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)") + case .unanswered: + Text("Call was not answered") .bold() .foregroundColor(.primary) - } else { - Text("Call ended: connection failed") + case .answeredElsewhere: + Text("Call ended: answered with other device") .bold() .foregroundColor(.primary) - } - case .securityError: - Text("Call ended: couldn't establish encryption") - .bold() - .foregroundColor(.primary) - case .unanswered: - Text("Call was not answered") - .bold() - .foregroundColor(.primary) - case .answeredElsewhere: - Text("Call ended: answered with other device") - .bold() - .foregroundColor(.primary) - case .retracted: - //this will only be displayed for timer-induced retractions, - //reflect that in our text instead of using some generic "hung up" - //Text("Call ended: hung up") - Text("Call ended: remote busy") - .bold() - .foregroundColor(.primary) - case .rejected: - Text("Call ended: remote busy") - .bold() - .foregroundColor(.primary) - case .declined: - Text("Call ended: declined") - .bold() - .foregroundColor(.primary) - case .error: - Text("Call ended: application error") - .bold() - .foregroundColor(.primary) - default: //should never be reached - Text("") - } - default: //should never be reached - Text("") - } - - Spacer().frame(height: 48) - - Image(uiImage: contact.avatar) - .resizable() - .frame(minWidth: 100, idealWidth: 150, maxWidth: 200, minHeight: 100, idealHeight: 150, maxHeight: 200, alignment: .center) - .scaledToFit() - .shadow(radius: 7) - - Spacer() - } - - if MLCallState(rawValue:call.state) == .finished { - HStack() { - Spacer() - - Button(action: { - self.delegate.dismissWithoutAnimation() - if let activeChats = self.appDelegate.obj.activeChats { - activeChats.call(contact.obj) - } - }) { - if #available(iOS 15, *) { - Image(systemName: "arrow.clockwise.circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .green) - .shadow(radius: 7) - } else { - ZStack { - Image(systemName: "circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .accentColor(.white) - Image(systemName: "arrow.clockwise.circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .accentColor(.green) - .shadow(radius: 7) + case .retracted: + //this will only be displayed for timer-induced retractions, + //reflect that in our text instead of using some generic "hung up" + //Text("Call ended: hung up") + Text("Call ended: remote busy") + .bold() + .foregroundColor(.primary) + case .rejected: + Text("Call ended: remote busy") + .bold() + .foregroundColor(.primary) + case .declined: + Text("Call ended: declined") + .bold() + .foregroundColor(.primary) + case .error: + Text("Call ended: application error") + .bold() + .foregroundColor(.primary) + default: //should never be reached + Text("") } - } + default: //should never be reached + Text("") } - .buttonStyle(BorderlessButtonStyle()) - Spacer().frame(width: 64) - - Button(action: { - delegate.dismissWithoutAnimation() - }) { - if #available(iOS 15, *) { - Image(systemName: "x.circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .red) - .shadow(radius: 7) - } else { - ZStack { - Image(systemName: "circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .accentColor(.white) - Image(systemName: "x.circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .accentColor(.red) - .shadow(radius: 7) - } - } + Spacer().frame(height: 48) + + if MLCallType(rawValue:call.callType) == .audio || MLCallState(rawValue:call.state) != .connected { + Image(uiImage: contact.avatar) + .resizable() + .frame(minWidth: 100, idealWidth: 150, maxWidth: 200, minHeight: 100, idealHeight: 150, maxHeight: 200, alignment: .center) + .scaledToFit() + .shadow(radius: 7) } - .buttonStyle(BorderlessButtonStyle()) Spacer() } - } else { - HStack() { - Spacer() - - if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting { + + if MLCallState(rawValue:call.state) == .finished { + HStack() { + Spacer() + Button(action: { - call.muted = !call.muted + self.delegate.dismissWithoutAnimation() + if let activeChats = self.appDelegate.obj.activeChats { + activeChats.call(contact.obj) + } }) { if #available(iOS 15, *) { - Image(systemName: call.muted ? "mic.circle.fill" : "mic.slash.circle.fill") + Image(systemName: "arrow.clockwise.circle.fill") .resizable() .frame(width: 64.0, height: 64.0) .symbolRenderingMode(.palette) - .foregroundStyle(call.muted ? .black : .white, call.muted ? .white : .black) + .foregroundStyle(.white, .green) .shadow(radius: 7) } else { ZStack { Image(systemName: "circle.fill") .resizable() .frame(width: 64.0, height: 64.0) - .accentColor(call.muted ? .black : .white) - Image(systemName: call.muted ? "mic.circle.fill" : "mic.circle.fill") + .accentColor(.white) + Image(systemName: "arrow.clockwise.circle.fill") .resizable() .frame(width: 64.0, height: 64.0) - .accentColor(call.muted ? .white : .black) + .accentColor(.green) .shadow(radius: 7) } } } .buttonStyle(BorderlessButtonStyle()) - Spacer().frame(width: 32) - } - - Button(action: { - call.obj.end() - self.delegate.dismissWithoutAnimation() - }) { - if #available(iOS 15, *) { - Image(systemName: "phone.down.circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .red) - .shadow(radius: 7) - } else { - ZStack(alignment: .center) { - Image(systemName: "circle.fill") - .resizable() - .frame(width: 64.0, height: 64.0) - .accentColor(.white) - Image(systemName: "phone.down.circle.fill") + Spacer().frame(width: 64) + + Button(action: { + delegate.dismissWithoutAnimation() + }) { + if #available(iOS 15, *) { + Image(systemName: "x.circle.fill") .resizable() .frame(width: 64.0, height: 64.0) - .accentColor(.red) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) .shadow(radius: 7) + } else { + ZStack { + Image(systemName: "circle.fill") + .resizable() + .frame(width: 64.0, height: 64.0) + .accentColor(.white) + Image(systemName: "x.circle.fill") + .resizable() + .frame(width: 64.0, height: 64.0) + .accentColor(.red) + .shadow(radius: 7) + } } } + .buttonStyle(BorderlessButtonStyle()) + + Spacer() } - .buttonStyle(BorderlessButtonStyle()) - - if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting { - Spacer().frame(width: 32) + } else { + HStack() { + Spacer() + + if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting { + Button(action: { + call.muted = !call.muted + }) { + if #available(iOS 15, *) { + Image(systemName: call.muted ? "mic.circle.fill" : "mic.slash.circle.fill") + .resizable() + .frame(width: 64.0, height: 64.0) + .symbolRenderingMode(.palette) + .foregroundStyle(call.muted ? .black : .white, call.muted ? .white : .black) + .shadow(radius: 7) + } else { + ZStack { + Image(systemName: "circle.fill") + .resizable() + .frame(width: 64.0, height: 64.0) + .accentColor(call.muted ? .black : .white) + Image(systemName: call.muted ? "mic.circle.fill" : "mic.circle.fill") + .resizable() + .frame(width: 64.0, height: 64.0) + .accentColor(call.muted ? .white : .black) + .shadow(radius: 7) + } + } + } + .buttonStyle(BorderlessButtonStyle()) + + Spacer().frame(width: 32) + } + Button(action: { - call.speaker = !call.speaker + call.obj.end() + self.delegate.dismissWithoutAnimation() }) { if #available(iOS 15, *) { - Image(systemName: "speaker.wave.2.circle.fill") + Image(systemName: "phone.down.circle.fill") .resizable() .frame(width: 64.0, height: 64.0) .symbolRenderingMode(.palette) - .foregroundStyle(call.speaker ? .black : .white, call.speaker ? .white : .black) + .foregroundStyle(.white, .red) .shadow(radius: 7) } else { - ZStack { + ZStack(alignment: .center) { Image(systemName: "circle.fill") .resizable() .frame(width: 64.0, height: 64.0) - .accentColor(call.speaker ? .black : .white) - Image(systemName: "speaker.wave.2.circle.fill") + .accentColor(.white) + Image(systemName: "phone.down.circle.fill") .resizable() .frame(width: 64.0, height: 64.0) - .accentColor(call.speaker ? .white : .black) + .accentColor(.red) .shadow(radius: 7) } } } .buttonStyle(BorderlessButtonStyle()) + + if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting { + Spacer().frame(width: 32) + Button(action: { + call.speaker = !call.speaker + }) { + if #available(iOS 15, *) { + Image(systemName: "speaker.wave.2.circle.fill") + .resizable() + .frame(width: 64.0, height: 64.0) + .symbolRenderingMode(.palette) + .foregroundStyle(call.speaker ? .black : .white, call.speaker ? .white : .black) + .shadow(radius: 7) + } else { + ZStack { + Image(systemName: "circle.fill") + .resizable() + .frame(width: 64.0, height: 64.0) + .accentColor(call.speaker ? .black : .white) + Image(systemName: "speaker.wave.2.circle.fill") + .resizable() + .frame(width: 64.0, height: 64.0) + .accentColor(call.speaker ? .white : .black) + .shadow(radius: 7) + } + } + } + .buttonStyle(BorderlessButtonStyle()) + } + + Spacer() } - - Spacer() } + + Spacer().frame(height: 32) } - - Spacer().frame(height: 32) } } + .onTapGesture(count: 1) { + controlsVisible = !controlsVisible + } .alert(isPresented: $showMicAlert) { Alert( title: Text("Missing permission"), @@ -566,6 +594,8 @@ struct AVCallUI: View { //force portrait mode and lock ui there UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") self.appDelegate.obj.orientationLock = .portrait + UIApplication.shared.isIdleTimerDisabled = true + self.ringingPlayer.numberOfLoops = -1 self.busyPlayer.numberOfLoops = -1 self.errorPlayer.numberOfLoops = -1 @@ -576,13 +606,19 @@ struct AVCallUI: View { showMicAlert = true } } + + maybeStartRenderer() } .onDisappear { //allow all orientations again self.appDelegate.obj.orientationLock = .all + UIApplication.shared.isIdleTimerDisabled = false + ringingPlayer.stop() busyPlayer.stop() errorPlayer.stop() + + call.obj.stopCaptureLocalVideo() } .onChange(of: MLCallState(rawValue:call.state)) { state in DDLogVerbose("call state changed: \(String(describing:call.state as NSNumber))") diff --git a/Monal/Classes/MLCall.h b/Monal/Classes/MLCall.h index f366ec93f1..1b264aa39c 100644 --- a/Monal/Classes/MLCall.h +++ b/Monal/Classes/MLCall.h @@ -82,6 +82,7 @@ typedef NS_ENUM(NSUInteger, MLCallEncryptionState) { //RTCVideoRenderer will not be visible to swift until we have swift 5.9 (feature flag ImportObjcForwardDeclarations) or swift 6.0 support //see https://github.com/apple/swift-evolution/blob/main/proposals/0384-importing-forward-declared-objc-interfaces-and-protocols.md -(void) startCaptureLocalVideoWithRenderer:(id) renderer; +-(void) stopCaptureLocalVideo; -(void) renderRemoteVideoWithRenderer:(id) renderer; -(BOOL) isEqualToContact:(MLContact*) contact; diff --git a/Monal/Classes/MLCall.m b/Monal/Classes/MLCall.m index 27b1853415..453de9ff9f 100644 --- a/Monal/Classes/MLCall.m +++ b/Monal/Classes/MLCall.m @@ -142,6 +142,11 @@ -(void) startCaptureLocalVideoWithRenderer:(id) renderer [self.webRTCClient startCaptureLocalVideoWithRenderer:renderer]; } +-(void) stopCaptureLocalVideo +{ + [self.webRTCClient stopCaptureLocalVideo]; +} + -(void) renderRemoteVideoWithRenderer:(id) renderer { [self.webRTCClient renderRemoteVideoTo:renderer]; diff --git a/Monal/Classes/WebRTCClient.swift b/Monal/Classes/WebRTCClient.swift index 0629ffa809..142ac445ce 100644 --- a/Monal/Classes/WebRTCClient.swift +++ b/Monal/Classes/WebRTCClient.swift @@ -169,6 +169,15 @@ final class WebRTCClient: NSObject { guard let capturer = self.videoCapturer as? RTCCameraVideoCapturer else { return } + + //see https://bugs.chromium.org/p/webrtc/issues/detail?id=10006#c2 + let aClass: AnyClass! = object_getClass(capturer) + let bClass: AnyClass! = object_getClass(self) + if capturer.responds(to: NSSelectorFromString("updateOrientation")) { + let swizzledMethod = class_getInstanceMethod(aClass, NSSelectorFromString("updateOrientation")) + let originalMethod = class_getInstanceMethod(bClass, NSSelectorFromString("myUpdateOrientation")) + method_exchangeImplementations(originalMethod!, swizzledMethod!) + } guard let frontCamera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == .front }), @@ -192,6 +201,19 @@ final class WebRTCClient: NSObject { self.localVideoTrack?.add(renderer) } + @objc + func myUpdateOrientation() { + DDLogDebug("Ignoring device orientation change in webrtc...") + } + + @objc + func stopCaptureLocalVideo() { + guard let capturer = self.videoCapturer as? RTCCameraVideoCapturer else { + return + } + capturer.stopCapture() + } + @objc func renderRemoteVideo(to renderer: RTCVideoRenderer) { self.remoteVideoTrack?.add(renderer)