import Foundation import UIKit import SwiftSignalKit import TelegramCore import SyncCore import Postbox import TelegramUIPreferences import TgVoip import TgVoipWebrtc private func callConnectionDescription(_ connection: CallSessionConnection) -> OngoingCallConnectionDescription? { switch connection { case let .reflector(reflector): return OngoingCallConnectionDescription(connectionId: reflector.id, ip: reflector.ip, ipv6: reflector.ipv6, port: reflector.port, peerTag: reflector.peerTag) case .webRtcReflector: return nil } } private func callConnectionDescriptionsWebrtc(_ connection: CallSessionConnection) -> [OngoingCallConnectionDescriptionWebrtc] { switch connection { case .reflector: return [] case let .webRtcReflector(reflector): var result: [OngoingCallConnectionDescriptionWebrtc] = [] if !reflector.ip.isEmpty { result.append(OngoingCallConnectionDescriptionWebrtc(connectionId: reflector.id, hasStun: reflector.hasStun, hasTurn: reflector.hasTurn, ip: reflector.ip, port: reflector.port, username: reflector.username, password: reflector.password)) } if !reflector.ipv6.isEmpty { result.append(OngoingCallConnectionDescriptionWebrtc(connectionId: reflector.id, hasStun: reflector.hasStun, hasTurn: reflector.hasTurn, ip: reflector.ipv6, port: reflector.port, username: reflector.username, password: reflector.password)) } return result } } public func callLogNameForId(id: Int64, account: Account) -> String? { let path = callLogsPath(account: account) let namePrefix = "\(id)_" if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { for url in enumerator { if let url = url as? URL { if url.lastPathComponent.hasPrefix(namePrefix) { return url.lastPathComponent } } } } return nil } public func callLogsPath(account: Account) -> String { return account.basePath + "/calls" } private func cleanupCallLogs(account: Account) { let path = callLogsPath(account: account) let fileManager = FileManager.default if !fileManager.fileExists(atPath: path, isDirectory: nil) { try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) } var oldest: [(URL, Date)] = [] var count = 0 if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { for url in enumerator { if let url = url as? URL { if let date = (try? url.resourceValues(forKeys: Set([.contentModificationDateKey])))?.contentModificationDate { oldest.append((url, date)) count += 1 } } } } let callLogsLimit = 40 if count > callLogsLimit { oldest.sort(by: { $0.1 > $1.1 }) while oldest.count > callLogsLimit { try? fileManager.removeItem(atPath: oldest[oldest.count - 1].0.path) oldest.removeLast() } } } private let setupLogs: Bool = { OngoingCallThreadLocalContext.setupLoggingFunction({ value in if let value = value { Logger.shared.log("TGVOIP", value) } }) OngoingCallThreadLocalContextWebrtc.setupLoggingFunction({ value in if let value = value { Logger.shared.log("TGVOIP", value) } }) /*OngoingCallThreadLocalContextWebrtcCustom.setupLoggingFunction({ value in if let value = value { Logger.shared.log("TGVOIP", value) } })*/ return true }() public struct OngoingCallContextState: Equatable { public enum State { case initializing case connected case reconnecting case failed } public enum VideoState: Equatable { case notAvailable case inactive case active case paused } public enum RemoteVideoState: Equatable { case inactive case active case paused } public enum RemoteAudioState: Equatable { case active case muted } public enum RemoteBatteryLevel: Equatable { case normal case low } public let state: State public let videoState: VideoState public let remoteVideoState: RemoteVideoState public let remoteAudioState: RemoteAudioState public let remoteBatteryLevel: RemoteBatteryLevel } private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc /*, OngoingCallThreadLocalContextQueueWebrtcCustom*/ { private let queue: Queue init(queue: Queue) { self.queue = queue super.init() } func dispatch(_ f: @escaping () -> Void) { self.queue.async { f() } } func dispatch(after seconds: Double, block f: @escaping () -> Void) { self.queue.after(seconds, f) } func isCurrent() -> Bool { return self.queue.isCurrent() } } private func ongoingNetworkTypeForType(_ type: NetworkType) -> OngoingCallNetworkType { switch type { case .none: return .wifi case .wifi: return .wifi case let .cellular(cellular): switch cellular { case .edge: return .cellularEdge case .gprs: return .cellularGprs case .thirdG, .unknown: return .cellular3g case .lte: return .cellularLte } } } private func ongoingNetworkTypeForTypeWebrtc(_ type: NetworkType) -> OngoingCallNetworkTypeWebrtc { switch type { case .none: return .wifi case .wifi: return .wifi case let .cellular(cellular): switch cellular { case .edge: return .cellularEdge case .gprs: return .cellularGprs case .thirdG, .unknown: return .cellular3g case .lte: return .cellularLte } } } /*private func ongoingNetworkTypeForTypeWebrtcCustom(_ type: NetworkType) -> OngoingCallNetworkTypeWebrtcCustom { switch type { case .none: return .wifi case .wifi: return .wifi case let .cellular(cellular): switch cellular { case .edge: return .cellularEdge case .gprs: return .cellularGprs case .thirdG, .unknown: return .cellular3g case .lte: return .cellularLte } } }*/ private func ongoingDataSavingForType(_ type: VoiceCallDataSaving) -> OngoingCallDataSaving { switch type { case .never: return .never case .cellular: return .cellular case .always: return .always default: return .never } } private func ongoingDataSavingForTypeWebrtc(_ type: VoiceCallDataSaving) -> OngoingCallDataSavingWebrtc { switch type { case .never: return .never case .cellular: return .cellular case .always: return .always default: return .never } } /*private func ongoingDataSavingForTypeWebrtcCustom(_ type: VoiceCallDataSaving) -> OngoingCallDataSavingWebrtcCustom { switch type { case .never: return .never case .cellular: return .cellular case .always: return .always default: return .never } }*/ private protocol OngoingCallThreadLocalContextProtocol: class { func nativeSetNetworkType(_ type: NetworkType) func nativeSetIsMuted(_ value: Bool) func nativeSetIsLowBatteryLevel(_ value: Bool) func nativeRequestVideo(_ capturer: OngoingCallVideoCapturer) func nativeSetRequestedVideoAspect(_ aspect: Float) func nativeDisableVideo() func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void) func nativeBeginTermination() func nativeDebugInfo() -> String func nativeVersion() -> String func nativeGetDerivedState() -> Data } private final class OngoingCallThreadLocalContextHolder { let context: OngoingCallThreadLocalContextProtocol init(_ context: OngoingCallThreadLocalContextProtocol) { self.context = context } } extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol { func nativeSetNetworkType(_ type: NetworkType) { self.setNetworkType(ongoingNetworkTypeForType(type)) } func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void) { self.stop(completion) } func nativeBeginTermination() { } func nativeSetIsMuted(_ value: Bool) { self.setIsMuted(value) } func nativeSetIsLowBatteryLevel(_ value: Bool) { } func nativeRequestVideo(_ capturer: OngoingCallVideoCapturer) { } func nativeSetRequestedVideoAspect(_ aspect: Float) { } func nativeDisableVideo() { } func nativeSwitchVideoCamera() { } func nativeDebugInfo() -> String { return self.debugInfo() ?? "" } func nativeVersion() -> String { return self.version() ?? "" } func nativeGetDerivedState() -> Data { return self.getDerivedState() } } public final class OngoingCallVideoCapturer { fileprivate let impl: OngoingCallThreadLocalContextVideoCapturer public init() { self.impl = OngoingCallThreadLocalContextVideoCapturer() } public func switchCamera() { self.impl.switchVideoCamera() } public func makeOutgoingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { self.impl.makeOutgoingVideoView { view in if let view = view { completion(OngoingCallContextPresentationCallVideoView( view: view, setOnFirstFrameReceived: { [weak view] f in view?.setOnFirstFrameReceived(f) }, getOrientation: { [weak view] in if let view = view { return OngoingCallVideoOrientation(view.orientation) } else { return .rotation0 } }, getAspect: { [weak view] in if let view = view { return view.aspect } else { return 0.0 } }, setOnOrientationUpdated: { [weak view] f in view?.setOnOrientationUpdated { value, aspect in f?(OngoingCallVideoOrientation(value), aspect) } }, setOnIsMirroredUpdated: { [weak view] f in view?.setOnIsMirroredUpdated(f) } )) } else { completion(nil) } } } public func setIsVideoEnabled(_ value: Bool) { self.impl.setIsVideoEnabled(value) } } extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProtocol { func nativeSetNetworkType(_ type: NetworkType) { self.setNetworkType(ongoingNetworkTypeForTypeWebrtc(type)) } func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void) { self.stop(completion) } func nativeBeginTermination() { self.beginTermination() } func nativeSetIsMuted(_ value: Bool) { self.setIsMuted(value) } func nativeSetIsLowBatteryLevel(_ value: Bool) { self.setIsLowBatteryLevel(value) } func nativeRequestVideo(_ capturer: OngoingCallVideoCapturer) { self.requestVideo(capturer.impl) } func nativeSetRequestedVideoAspect(_ aspect: Float) { self.setRequestedVideoAspect(aspect) } func nativeDisableVideo() { self.disableVideo() } func nativeDebugInfo() -> String { return self.debugInfo() ?? "" } func nativeVersion() -> String { return self.version() ?? "" } func nativeGetDerivedState() -> Data { return self.getDerivedState() } } private extension OngoingCallContextState.State { init(_ state: OngoingCallState) { switch state { case .initializing: self = .initializing case .connected: self = .connected case .failed: self = .failed case .reconnecting: self = .reconnecting default: self = .failed } } } private extension OngoingCallContextState.State { init(_ state: OngoingCallStateWebrtc) { switch state { case .initializing: self = .initializing case .connected: self = .connected case .failed: self = .failed case .reconnecting: self = .reconnecting default: self = .failed } } } public enum OngoingCallVideoOrientation { case rotation0 case rotation90 case rotation180 case rotation270 } private extension OngoingCallVideoOrientation { init(_ orientation: OngoingCallVideoOrientationWebrtc) { switch orientation { case .orientation0: self = .rotation0 case .orientation90: self = .rotation90 case .orientation180: self = .rotation180 case .orientation270: self = .rotation270 @unknown default: self = .rotation0 } } } public final class OngoingCallContextPresentationCallVideoView { public let view: UIView public let setOnFirstFrameReceived: (((Float) -> Void)?) -> Void public let getOrientation: () -> OngoingCallVideoOrientation public let getAspect: () -> CGFloat public let setOnOrientationUpdated: (((OngoingCallVideoOrientation, CGFloat) -> Void)?) -> Void public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void public init( view: UIView, setOnFirstFrameReceived: @escaping (((Float) -> Void)?) -> Void, getOrientation: @escaping () -> OngoingCallVideoOrientation, getAspect: @escaping () -> CGFloat, setOnOrientationUpdated: @escaping (((OngoingCallVideoOrientation, CGFloat) -> Void)?) -> Void, setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void ) { self.view = view self.setOnFirstFrameReceived = setOnFirstFrameReceived self.getOrientation = getOrientation self.getAspect = getAspect self.setOnOrientationUpdated = setOnOrientationUpdated self.setOnIsMirroredUpdated = setOnIsMirroredUpdated } } public final class OngoingCallContext { public struct AuxiliaryServer { public enum Connection { case stun case turn(username: String, password: String) } public let host: String public let port: Int public let connection: Connection public init( host: String, port: Int, connection: Connection ) { self.host = host self.port = port self.connection = connection } } public let internalId: CallSessionInternalId private let queue = Queue() private let account: Account private let callSessionManager: CallSessionManager private let logPath: String private var contextRef: Unmanaged? private let contextState = Promise(nil) public var state: Signal { return self.contextState.get() } private var didReportCallAsVideo: Bool = false private var signalingDataDisposable: Disposable? private let receptionPromise = Promise(nil) public var reception: Signal { return self.receptionPromise.get() } private let audioSessionDisposable = MetaDisposable() private var networkTypeDisposable: Disposable? public static var maxLayer: Int32 { return OngoingCallThreadLocalContext.maxLayer() } private let tempLogFile: TempBoxFile private let tempStatsLogFile: TempBoxFile public static func versions(includeExperimental: Bool, includeReference: Bool) -> [(version: String, supportsVideo: Bool)] { var result: [(version: String, supportsVideo: Bool)] = [(OngoingCallThreadLocalContext.version(), false)] if includeExperimental { result.append(contentsOf: OngoingCallThreadLocalContextWebrtc.versions(withIncludeReference: includeReference).map { version -> (version: String, supportsVideo: Bool) in return (version, true) }) } return result } public init(account: Account, callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal, serializedData: String?, dataSaving: VoiceCallDataSaving, derivedState: VoipDerivedState, key: Data, isOutgoing: Bool, video: OngoingCallVideoCapturer?, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, allowP2P: Bool, enableStunMarking: Bool, audioSessionActive: Signal, logName: String, preferredVideoCodec: String?) { let _ = setupLogs OngoingCallThreadLocalContext.applyServerConfig(serializedData) self.internalId = internalId self.account = account self.callSessionManager = callSessionManager self.logPath = logName.isEmpty ? "" : callLogsPath(account: self.account) + "/" + logName + ".log" let logPath = self.logPath self.tempLogFile = TempBox.shared.tempFile(fileName: "CallLog.txt") let tempLogPath = self.tempLogFile.path self.tempStatsLogFile = TempBox.shared.tempFile(fileName: "CallStats.json") let tempStatsLogPath = self.tempStatsLogFile.path let queue = self.queue cleanupCallLogs(account: account) self.audioSessionDisposable.set((audioSessionActive |> filter { $0 } |> take(1) |> deliverOn(queue)).start(next: { [weak self] _ in if let strongSelf = self { if OngoingCallThreadLocalContextWebrtc.versions(withIncludeReference: true).contains(version) { var voipProxyServer: VoipProxyServerWebrtc? if let proxyServer = proxyServer { switch proxyServer.connection { case let .socks5(username, password): voipProxyServer = VoipProxyServerWebrtc(host: proxyServer.host, port: proxyServer.port, username: username, password: password) case .mtp: break } } let unfilteredConnections = [connections.primary] + connections.alternatives var processedConnections: [CallSessionConnection] = [] var filteredConnections: [OngoingCallConnectionDescriptionWebrtc] = [] for connection in unfilteredConnections { if processedConnections.contains(connection) { continue } processedConnections.append(connection) filteredConnections.append(contentsOf: callConnectionDescriptionsWebrtc(connection)) } let context = OngoingCallThreadLocalContextWebrtc(version: version, queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForTypeWebrtc(initialNetworkType), dataSaving: ongoingDataSavingForTypeWebrtc(dataSaving), derivedState: derivedState.data, key: key, isOutgoing: isOutgoing, connections: filteredConnections, maxLayer: maxLayer, allowP2P: allowP2P, enableStunMarking: enableStunMarking, logPath: tempLogPath, statsLogPath: tempStatsLogPath, sendSignalingData: { [weak callSessionManager] data in callSessionManager?.sendSignalingData(internalId: internalId, data: data) }, videoCapturer: video?.impl, preferredVideoCodec: preferredVideoCodec) strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) context.stateChanged = { [weak callSessionManager] state, videoState, remoteVideoState, remoteAudioState, remoteBatteryLevel, _ in queue.async { guard let strongSelf = self else { return } let mappedState = OngoingCallContextState.State(state) let mappedVideoState: OngoingCallContextState.VideoState switch videoState { case .inactive: mappedVideoState = .inactive case .active: mappedVideoState = .active case .paused: mappedVideoState = .paused @unknown default: mappedVideoState = .notAvailable } let mappedRemoteVideoState: OngoingCallContextState.RemoteVideoState switch remoteVideoState { case .inactive: mappedRemoteVideoState = .inactive case .active: mappedRemoteVideoState = .active case .paused: mappedRemoteVideoState = .paused @unknown default: mappedRemoteVideoState = .inactive } let mappedRemoteAudioState: OngoingCallContextState.RemoteAudioState switch remoteAudioState { case .active: mappedRemoteAudioState = .active case .muted: mappedRemoteAudioState = .muted @unknown default: mappedRemoteAudioState = .active } let mappedRemoteBatteryLevel: OngoingCallContextState.RemoteBatteryLevel switch remoteBatteryLevel { case .normal: mappedRemoteBatteryLevel = .normal case .low: mappedRemoteBatteryLevel = .low @unknown default: mappedRemoteBatteryLevel = .normal } if case .active = mappedVideoState, !strongSelf.didReportCallAsVideo { strongSelf.didReportCallAsVideo = true callSessionManager?.updateCallType(internalId: internalId, type: .video) } strongSelf.contextState.set(.single(OngoingCallContextState(state: mappedState, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel))) } } strongSelf.receptionPromise.set(.single(4)) context.signalBarsChanged = { signalBars in self?.receptionPromise.set(.single(signalBars)) } strongSelf.networkTypeDisposable = (updatedNetworkType |> deliverOn(queue)).start(next: { networkType in self?.withContext { context in context.nativeSetNetworkType(networkType) } }) } else { var voipProxyServer: VoipProxyServer? if let proxyServer = proxyServer { switch proxyServer.connection { case let .socks5(username, password): voipProxyServer = VoipProxyServer(host: proxyServer.host, port: proxyServer.port, username: username, password: password) case .mtp: break } } let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForType(initialNetworkType), dataSaving: ongoingDataSavingForType(dataSaving), derivedState: derivedState.data, key: key, isOutgoing: isOutgoing, primaryConnection: callConnectionDescription(connections.primary)!, alternativeConnections: connections.alternatives.compactMap(callConnectionDescription), maxLayer: maxLayer, allowP2P: allowP2P, logPath: logPath) strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) context.stateChanged = { state in self?.contextState.set(.single(OngoingCallContextState(state: OngoingCallContextState.State(state), videoState: .notAvailable, remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal))) } context.signalBarsChanged = { signalBars in self?.receptionPromise.set(.single(signalBars)) } strongSelf.networkTypeDisposable = (updatedNetworkType |> deliverOn(queue)).start(next: { networkType in self?.withContext { context in context.nativeSetNetworkType(networkType) } }) } } })) self.signalingDataDisposable = (callSessionManager.callSignalingData(internalId: internalId)).start(next: { [weak self] data in print("data received") queue.async { self?.withContext { context in if let context = context as? OngoingCallThreadLocalContextWebrtc { context.addSignaling(data) } } } }) } deinit { let contextRef = self.contextRef self.queue.async { contextRef?.release() } self.audioSessionDisposable.dispose() self.networkTypeDisposable?.dispose() } private func withContext(_ f: @escaping (OngoingCallThreadLocalContextProtocol) -> Void) { self.queue.async { if let contextRef = self.contextRef { let context = contextRef.takeUnretainedValue() f(context.context) } } } private func withContextThenDeallocate(_ f: @escaping (OngoingCallThreadLocalContextProtocol) -> Void) { self.queue.async { if let contextRef = self.contextRef { let context = contextRef.takeUnretainedValue() f(context.context) self.contextRef?.release() self.contextRef = nil } } } public func beginTermination() { self.withContext { context in context.nativeBeginTermination() } } public func stop(callId: CallId? = nil, sendDebugLogs: Bool = false, debugLogValue: Promise) { let account = self.account let logPath = self.logPath var statsLogPath = "" if !logPath.isEmpty { statsLogPath = logPath + ".json" } let tempLogPath = self.tempLogFile.path let tempStatsLogPath = self.tempStatsLogFile.path self.withContextThenDeallocate { context in context.nativeStop { debugLog, bytesSentWifi, bytesReceivedWifi, bytesSentMobile, bytesReceivedMobile in let delta = NetworkUsageStatsConnectionsEntry( cellular: NetworkUsageStatsDirectionsEntry( incoming: bytesReceivedMobile, outgoing: bytesSentMobile), wifi: NetworkUsageStatsDirectionsEntry( incoming: bytesReceivedWifi, outgoing: bytesSentWifi)) updateAccountNetworkUsageStats(account: self.account, category: .call, delta: delta) if !logPath.isEmpty { let logsPath = callLogsPath(account: account) let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) let _ = try? FileManager.default.moveItem(atPath: tempLogPath, toPath: logPath) } if !statsLogPath.isEmpty { let logsPath = callLogsPath(account: account) let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) let _ = try? FileManager.default.moveItem(atPath: tempStatsLogPath, toPath: statsLogPath) } if let callId = callId, !statsLogPath.isEmpty, let data = try? Data(contentsOf: URL(fileURLWithPath: statsLogPath)), let dataString = String(data: data, encoding: .utf8) { debugLogValue.set(.single(dataString)) if sendDebugLogs { let _ = saveCallDebugLog(network: self.account.network, callId: callId, log: dataString).start() } } } let derivedState = context.nativeGetDerivedState() let _ = updateVoipDerivedStateInteractively(postbox: self.account.postbox, { _ in return VoipDerivedState(data: derivedState) }).start() } } public func setIsMuted(_ value: Bool) { self.withContext { context in context.nativeSetIsMuted(value) } } public func setIsLowBatteryLevel(_ value: Bool) { self.withContext { context in context.nativeSetIsLowBatteryLevel(value) } } public func requestVideo(_ capturer: OngoingCallVideoCapturer) { self.withContext { context in context.nativeRequestVideo(capturer) } } public func setRequestedVideoAspect(_ aspect: Float) { self.withContext { context in context.nativeSetRequestedVideoAspect(aspect) } } public func disableVideo() { self.withContext { context in context.nativeDisableVideo() } } public func debugInfo() -> Signal<(String, String), NoError> { let poll = Signal<(String, String), NoError> { subscriber in self.withContext { context in let version = context.nativeVersion() let debugInfo = context.nativeDebugInfo() subscriber.putNext((version, debugInfo)) subscriber.putCompletion() } return EmptyDisposable } return (poll |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue()))) |> restart } public func makeIncomingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { self.withContext { context in if let context = context as? OngoingCallThreadLocalContextWebrtc { context.makeIncomingVideoView { view in if let view = view { completion(OngoingCallContextPresentationCallVideoView( view: view, setOnFirstFrameReceived: { [weak view] f in view?.setOnFirstFrameReceived(f) }, getOrientation: { [weak view] in if let view = view { return OngoingCallVideoOrientation(view.orientation) } else { return .rotation0 } }, getAspect: { [weak view] in if let view = view { return view.aspect } else { return 0.0 } }, setOnOrientationUpdated: { [weak view] f in view?.setOnOrientationUpdated { value, aspect in f?(OngoingCallVideoOrientation(value), aspect) } }, setOnIsMirroredUpdated: { [weak view] f in view?.setOnIsMirroredUpdated { value in f?(value) } } )) } else { completion(nil) } } } else { completion(nil) } } } }