import Foundation import UIKit import Postbox import TelegramCore import SwiftSignalKit import Display import AVFoundation import TelegramVoip import TelegramAudio import TelegramUIPreferences import TelegramPresentationData import DeviceAccess import UniversalMediaPlayer import AccountContext import DeviceProximity import PhoneNumberFormat public final class SharedCallAudioContext { let audioDevice: OngoingCallContext.AudioDevice? let callKitIntegration: CallKitIntegration? private let defaultToSpeaker: Bool private var audioSessionDisposable: Disposable? private var audioSessionShouldBeActiveDisposable: Disposable? private var isAudioSessionActiveDisposable: Disposable? private var audioOutputStateDisposable: Disposable? private(set) var audioSessionControl: ManagedAudioSessionControl? private let isAudioSessionActivePromise = Promise(false) private var isAudioSessionActive: Signal { return self.isAudioSessionActivePromise.get() } private let audioOutputStatePromise = Promise<([AudioSessionOutput], AudioSessionOutput?)>(([], nil)) private var audioOutputStateValue: ([AudioSessionOutput], AudioSessionOutput?) = ([], nil) public private(set) var currentAudioOutputValue: AudioSessionOutput = .builtin private var didSetCurrentAudioOutputValue: Bool = false var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { return self.audioOutputStatePromise.get() } private let audioSessionShouldBeActive = Promise(true) private var initialSetupTimer: Foundation.Timer? init(audioSession: ManagedAudioSession, callKitIntegration: CallKitIntegration?, defaultToSpeaker: Bool = false) { self.callKitIntegration = callKitIntegration self.audioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false) self.defaultToSpeaker = defaultToSpeaker if defaultToSpeaker { self.didSetCurrentAudioOutputValue = true self.currentAudioOutputValue = .speaker } var didReceiveAudioOutputs = false self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in Queue.mainQueue().async { guard let self else { return } let previousControl = self.audioSessionControl self.audioSessionControl = control if previousControl == nil, let audioSessionControl = self.audioSessionControl { if let callKitIntegration = self.callKitIntegration { if self.didSetCurrentAudioOutputValue { callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) } } else { audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) audioSessionControl.setup(synchronous: true) } let audioSessionActive: Signal if let callKitIntegration = self.callKitIntegration { audioSessionActive = callKitIntegration.audioSessionActive } else { audioSessionControl.activate({ _ in }) audioSessionActive = .single(true) } self.isAudioSessionActivePromise.set(audioSessionActive) self.initialSetupTimer?.invalidate() self.initialSetupTimer = Foundation.Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in guard let self else { return } if self.defaultToSpeaker, let audioSessionControl = self.audioSessionControl { self.currentAudioOutputValue = .speaker self.didSetCurrentAudioOutputValue = true if let callKitIntegration = self.callKitIntegration { if self.didSetCurrentAudioOutputValue { callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) } } else { audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) audioSessionControl.setup(synchronous: true) } } }) } } }, deactivate: { [weak self] _ in return Signal { subscriber in Queue.mainQueue().async { if let self { self.isAudioSessionActivePromise.set(.single(false)) self.audioSessionControl = nil } subscriber.putCompletion() } return EmptyDisposable } }, availableOutputsChanged: { [weak self] availableOutputs, currentOutput in Queue.mainQueue().async { guard let self else { return } self.audioOutputStateValue = (availableOutputs, currentOutput) if let currentOutput = currentOutput { self.currentAudioOutputValue = currentOutput self.didSetCurrentAudioOutputValue = true } var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput)) if !didReceiveAudioOutputs { didReceiveAudioOutputs = true if currentOutput == .speaker { signal = .single((availableOutputs, .builtin)) |> then( signal |> delay(1.0, queue: Queue.mainQueue()) ) } } self.audioOutputStatePromise.set(signal) } }) self.audioSessionShouldBeActive.set(.single(true)) self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get() |> deliverOnMainQueue).start(next: { [weak self] value in guard let self else { return } if value { if let audioSessionControl = self.audioSessionControl { let audioSessionActive: Signal if let callKitIntegration = self.callKitIntegration { audioSessionActive = callKitIntegration.audioSessionActive } else { audioSessionControl.activate({ _ in }) audioSessionActive = .single(true) } self.isAudioSessionActivePromise.set(audioSessionActive) } else { self.isAudioSessionActivePromise.set(.single(false)) } } else { self.isAudioSessionActivePromise.set(.single(false)) } }) self.isAudioSessionActiveDisposable = (self.isAudioSessionActive |> deliverOnMainQueue).start(next: { [weak self] value in guard let self else { return } self.audioDevice?.setIsAudioSessionActive(value) }) self.audioOutputStateDisposable = (self.audioOutputStatePromise.get() |> deliverOnMainQueue).start(next: { [weak self] value in guard let self else { return } self.audioOutputStateValue = value if let currentOutput = value.1 { self.currentAudioOutputValue = currentOutput } }) } deinit { self.audioSessionDisposable?.dispose() self.audioSessionShouldBeActiveDisposable?.dispose() self.isAudioSessionActiveDisposable?.dispose() self.audioOutputStateDisposable?.dispose() self.initialSetupTimer?.invalidate() } func setCurrentAudioOutput(_ output: AudioSessionOutput) { self.initialSetupTimer?.invalidate() self.initialSetupTimer = nil guard self.currentAudioOutputValue != output else { return } self.currentAudioOutputValue = output self.didSetCurrentAudioOutputValue = true self.audioOutputStatePromise.set(.single((self.audioOutputStateValue.0, output)) |> then( .single(self.audioOutputStateValue) |> delay(1.0, queue: Queue.mainQueue()) )) if let audioSessionControl = self.audioSessionControl { if let callKitIntegration = self.callKitIntegration { callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) } else { audioSessionControl.setOutputMode(.custom(output)) } } } } public final class PresentationCallImpl: PresentationCall { public let context: AccountContext private let audioSession: ManagedAudioSession private let callSessionManager: CallSessionManager private let callKitIntegration: CallKitIntegration? public var isIntegratedWithCallKit: Bool { return self.callKitIntegration != nil } private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void) public let internalId: CallSessionInternalId public let peerId: EnginePeer.Id public let isOutgoing: Bool private let incomingConferenceSource: EngineMessage.Id? public var isVideo: Bool public var isVideoPossible: Bool private let enableStunMarking: Bool private let enableTCP: Bool public let preferredVideoCodec: String? public let peer: EnginePeer? private let serializedData: String? private let dataSaving: VoiceCallDataSaving private let proxyServer: ProxyServerSettings? private let auxiliaryServers: [OngoingCallContext.AuxiliaryServer] private let currentNetworkType: NetworkType private let updatedNetworkType: Signal public private(set) var sharedAudioContext: SharedCallAudioContext? private var sessionState: CallSession? private var callContextState: OngoingCallContextState? private var ongoingContext: OngoingCallContext? private var ongoingContextStateDisposable: Disposable? private var ongoingContextIsFailedDisposable: Disposable? private var ongoingContextIsDroppedDisposable: Disposable? private var didDropCall = false private var requestedVideoAspect: Float? private var reception: Int32? private var receptionDisposable: Disposable? private var audioLevelDisposable: Disposable? private var reportedIncomingCall = false private var batteryLevelDisposable: Disposable? private var callWasActive = false private var shouldPresentCallRating = false private var previousVideoState: PresentationCallState.VideoState? private var previousRemoteVideoState: PresentationCallState.RemoteVideoState? private var previousRemoteAudioState: PresentationCallState.RemoteAudioState? private var previousRemoteBatteryLevel: PresentationCallState.RemoteBatteryLevel? private var sessionStateDisposable: Disposable? private let statePromise = ValuePromise() public var state: Signal { return self.statePromise.get() } private let audioLevelPromise = ValuePromise(0.0) public var audioLevel: Signal { return self.audioLevelPromise.get() } private let isMutedPromise = ValuePromise(false) private var isMutedValue = false public var isMuted: Signal { return self.isMutedPromise.get() } private let audioOutputStatePromise = Promise<([AudioSessionOutput], AudioSessionOutput?)>(([], nil)) private var audioOutputStateValue: ([AudioSessionOutput], AudioSessionOutput?) = ([], nil) private var currentAudioOutputValue: AudioSessionOutput = .builtin private var didSetCurrentAudioOutputValue: Bool = false public var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { if let sharedAudioContext = self.sharedAudioContext { return sharedAudioContext.audioOutputState } return self.audioOutputStatePromise.get() } private let debugInfoValue = Promise<(String, String)>(("", "")) private let canBeRemovedPromise = Promise(false) private var didSetCanBeRemoved = false public var canBeRemoved: Signal { return self.canBeRemovedPromise.get() } private let hungUpPromise = ValuePromise() private var activeTimestamp: Double? private var audioSessionControl: ManagedAudioSessionControl? private var audioSessionDisposable: Disposable? private let audioSessionShouldBeActive = ValuePromise(false, ignoreRepeated: true) private var audioSessionShouldBeActiveDisposable: Disposable? private let audioSessionActive = Promise(false) private var audioSessionActiveDisposable: Disposable? private var isAudioSessionActive = false private var currentTone: PresentationCallTone? private var droppedCall = false private var dropCallKitCallTimer: SwiftSignalKit.Timer? private var useFrontCamera: Bool = true private var videoCapturer: OngoingCallVideoCapturer? public var hasVideo: Bool { return self.videoCapturer != nil } private var screencastBufferServerContext: IpcGroupCallBufferAppContext? private var screencastCapturer: OngoingCallVideoCapturer? private var isScreencastActive: Bool = false public var hasScreencast: Bool { return self.screencastCapturer != nil } private var proximityManagerIndex: Int? private let screencastFramesDisposable = MetaDisposable() private let screencastAudioDataDisposable = MetaDisposable() private let screencastStateDisposable = MetaDisposable() private var conferenceCallImpl: PresentationGroupCallImpl? public var conferenceCall: PresentationGroupCall? { return self.conferenceCallImpl } private var conferenceCallDisposable: Disposable? private var upgradedToConferenceCompletions = Bag<(PresentationGroupCall) -> Void>() private var isAcceptingIncomingConference: Bool = false private var waitForConferenceCallReadyDisposable: Disposable? private let conferenceStatePromise = ValuePromise(nil) public private(set) var conferenceStateValue: PresentationCallConferenceState? { didSet { if self.conferenceStateValue != oldValue { self.conferenceStatePromise.set(self.conferenceStateValue) } } } public var conferenceState: Signal { return self.conferenceStatePromise.get() } public private(set) var pendingInviteToConferencePeerIds: [EnginePeer.Id] = [] private var localVideoEndpointId: String? private var remoteVideoEndpointId: String? private var isMovedToConference: Bool = false init( context: AccountContext, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, serializedData: String?, dataSaving: VoiceCallDataSaving, getDeviceAccessData: @escaping () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void), initialState: CallSession?, internalId: CallSessionInternalId, peerId: EnginePeer.Id, isOutgoing: Bool, incomingConferenceSource: EngineMessage.Id?, peer: EnginePeer?, proxyServer: ProxyServerSettings?, auxiliaryServers: [CallAuxiliaryServer], currentNetworkType: NetworkType, updatedNetworkType: Signal, startWithVideo: Bool, isVideoPossible: Bool, enableStunMarking: Bool, enableTCP: Bool, preferredVideoCodec: String? ) { self.context = context self.audioSession = audioSession self.callSessionManager = callSessionManager self.callKitIntegration = callKitIntegration self.getDeviceAccessData = getDeviceAccessData self.auxiliaryServers = auxiliaryServers.map { server -> OngoingCallContext.AuxiliaryServer in let mappedConnection: OngoingCallContext.AuxiliaryServer.Connection switch server.connection { case .stun: mappedConnection = .stun case let .turn(username, password): mappedConnection = .turn(username: username, password: password) } return OngoingCallContext.AuxiliaryServer( host: server.host, port: server.port, connection: mappedConnection ) } self.internalId = internalId self.peerId = peerId self.isOutgoing = isOutgoing self.incomingConferenceSource = incomingConferenceSource self.isVideo = initialState?.type == .video self.isVideoPossible = isVideoPossible self.enableStunMarking = enableStunMarking self.enableTCP = enableTCP self.preferredVideoCodec = preferredVideoCodec self.peer = peer self.isVideo = startWithVideo if self.isVideo { self.videoCapturer = OngoingCallVideoCapturer() self.statePromise.set(PresentationCallState(state: isOutgoing ? .waiting : .ringing, videoState: .active(isScreencast: self.isScreencastActive, endpointId: ""), remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal)) } else { self.statePromise.set(PresentationCallState(state: isOutgoing ? .waiting : .ringing, videoState: self.isVideoPossible ? .inactive : .notAvailable, remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal)) } self.serializedData = serializedData self.dataSaving = dataSaving self.proxyServer = proxyServer self.currentNetworkType = currentNetworkType self.updatedNetworkType = updatedNetworkType var didReceiveAudioOutputs = false var callSessionState: Signal = .complete() if let initialState = initialState { callSessionState = .single(initialState) } callSessionState = callSessionState |> then(callSessionManager.callState(internalId: internalId)) self.sessionStateDisposable = (callSessionState |> deliverOnMainQueue).start(next: { [weak self] sessionState in if let strongSelf = self { strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl) } }) if let data = context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_call_device"] { self.sharedAudioContext = nil } else { self.sharedAudioContext = SharedCallAudioContext(audioSession: audioSession, callKitIntegration: callKitIntegration, defaultToSpeaker: startWithVideo || initialState?.type == .video) } if let _ = self.sharedAudioContext { } else { self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in Queue.mainQueue().async { if let strongSelf = self { if let sessionState = strongSelf.sessionState { strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception, audioSessionControl: control) } else { strongSelf.audioSessionControl = control } } } }, deactivate: { [weak self] _ in return Signal { subscriber in Queue.mainQueue().async { if let strongSelf = self { strongSelf.updateIsAudioSessionActive(false) if let sessionState = strongSelf.sessionState { strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception, audioSessionControl: nil) } else { strongSelf.audioSessionControl = nil } } subscriber.putCompletion() } return EmptyDisposable } }, availableOutputsChanged: { [weak self] availableOutputs, currentOutput in Queue.mainQueue().async { guard let strongSelf = self else { return } strongSelf.audioOutputStateValue = (availableOutputs, currentOutput) if let currentOutput = currentOutput { strongSelf.currentAudioOutputValue = currentOutput strongSelf.didSetCurrentAudioOutputValue = true } var signal: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> = .single((availableOutputs, currentOutput)) if !didReceiveAudioOutputs { didReceiveAudioOutputs = true if currentOutput == .speaker { signal = .single((availableOutputs, .builtin)) |> then( signal |> delay(1.0, queue: Queue.mainQueue()) ) } } strongSelf.audioOutputStatePromise.set(signal) } }) self.audioSessionShouldBeActiveDisposable = (self.audioSessionShouldBeActive.get() |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { if value { if let audioSessionControl = strongSelf.audioSessionControl { let audioSessionActive: Signal if let callKitIntegration = strongSelf.callKitIntegration { audioSessionActive = callKitIntegration.audioSessionActive } else { audioSessionControl.activate({ _ in }) audioSessionActive = .single(true) } strongSelf.audioSessionActive.set(audioSessionActive) } else { strongSelf.audioSessionActive.set(.single(false)) } } else { strongSelf.audioSessionActive.set(.single(false)) } } }) self.audioSessionActiveDisposable = (self.audioSessionActive.get() |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { strongSelf.updateIsAudioSessionActive(value) } }) } let screencastCapturer = OngoingCallVideoCapturer(isCustom: true) self.screencastCapturer = screencastCapturer self.resetScreencastContext() if callKitIntegration == nil { self.proximityManagerIndex = DeviceProximityManager.shared().add { _ in } } } deinit { self.audioSessionShouldBeActiveDisposable?.dispose() self.audioSessionActiveDisposable?.dispose() self.sessionStateDisposable?.dispose() self.ongoingContextStateDisposable?.dispose() self.receptionDisposable?.dispose() self.audioLevelDisposable?.dispose() self.batteryLevelDisposable?.dispose() self.audioSessionDisposable?.dispose() self.screencastFramesDisposable.dispose() self.screencastAudioDataDisposable.dispose() self.screencastStateDisposable.dispose() self.conferenceCallDisposable?.dispose() self.ongoingContextStateDisposable?.dispose() self.ongoingContextIsFailedDisposable?.dispose() self.ongoingContextIsDroppedDisposable?.dispose() self.waitForConferenceCallReadyDisposable?.dispose() if let dropCallKitCallTimer = self.dropCallKitCallTimer { dropCallKitCallTimer.invalidate() if !self.droppedCall { self.callKitIntegration?.dropCall(uuid: self.internalId) } } if let proximityManagerIndex = self.proximityManagerIndex { DeviceProximityManager.shared().remove(proximityManagerIndex) } } public func resetAsMovedToConference() { if self.isMovedToConference { return } self.isMovedToConference = true self.sharedAudioContext = nil self.sessionState = nil self.callContextState = nil self.ongoingContext = nil self.ongoingContextStateDisposable?.dispose() self.ongoingContextStateDisposable = nil self.ongoingContextIsFailedDisposable?.dispose() self.ongoingContextIsFailedDisposable = nil self.ongoingContextIsDroppedDisposable?.dispose() self.ongoingContextIsDroppedDisposable = nil self.didDropCall = false self.requestedVideoAspect = nil self.reception = nil self.receptionDisposable?.dispose() self.receptionDisposable = nil self.audioLevelDisposable?.dispose() self.audioLevelDisposable = nil self.reportedIncomingCall = false self.batteryLevelDisposable?.dispose() self.batteryLevelDisposable = nil self.callWasActive = false self.shouldPresentCallRating = false self.previousVideoState = nil self.previousRemoteVideoState = nil self.previousRemoteAudioState = nil self.previousRemoteBatteryLevel = nil self.sessionStateDisposable?.dispose() self.sessionStateDisposable = nil self.activeTimestamp = nil self.audioSessionControl = nil self.audioSessionDisposable?.dispose() self.audioSessionDisposable = nil self.audioSessionShouldBeActiveDisposable?.dispose() self.audioSessionShouldBeActiveDisposable = nil self.audioSessionActiveDisposable?.dispose() self.audioSessionActiveDisposable = nil self.isAudioSessionActive = false self.currentTone = nil self.dropCallKitCallTimer?.invalidate() self.dropCallKitCallTimer = nil self.droppedCall = true self.videoCapturer = nil self.screencastBufferServerContext = nil self.screencastCapturer = nil self.isScreencastActive = false if let proximityManagerIndex = self.proximityManagerIndex { DeviceProximityManager.shared().remove(proximityManagerIndex) self.proximityManagerIndex = nil } self.screencastFramesDisposable.set(nil) self.screencastAudioDataDisposable.set(nil) self.screencastStateDisposable.set(nil) self.conferenceCallImpl = nil self.conferenceCallDisposable?.dispose() self.conferenceCallDisposable = nil self.upgradedToConferenceCompletions.removeAll() self.waitForConferenceCallReadyDisposable?.dispose() self.waitForConferenceCallReadyDisposable = nil self.pendingInviteToConferencePeerIds.removeAll() self.localVideoEndpointId = nil self.remoteVideoEndpointId = nil self.callKitIntegration?.updateCallIsConference(uuid: self.internalId) } func internal_markAsCanBeRemoved() { if !self.didSetCanBeRemoved { self.didSetCanBeRemoved = true self.canBeRemovedPromise.set(.single(true)) } } private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) { if self.isMovedToConference { return } self.reception = reception if let ongoingContext = self.ongoingContext { if self.receptionDisposable == nil, case .active = sessionState.state { self.reception = 4 var canUpdate = false self.receptionDisposable = (ongoingContext.reception |> delay(1.0, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] reception in if let strongSelf = self { if let sessionState = strongSelf.sessionState { if canUpdate { strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl) } else { strongSelf.reception = reception } } else { strongSelf.reception = reception } } }) canUpdate = true } } if case .video = sessionState.type { self.isVideo = true } let previous = self.sessionState let previousControl = self.audioSessionControl self.sessionState = sessionState self.callContextState = callContextState self.audioSessionControl = audioSessionControl let reception = self.reception /*if previousControl != nil && audioSessionControl == nil { print("updateSessionState \(sessionState.state) \(audioSessionControl != nil)") }*/ var presentationState: PresentationCallState? var wasActive = false var wasTerminated = false if let previous = previous { switch previous.state { case .active: wasActive = true case let .terminated(_, reason, _): if case .ended(.switchedToConference) = reason { } else { wasTerminated = true } default: break } } if self.sharedAudioContext == nil { if let audioSessionControl = audioSessionControl, previous == nil || previousControl == nil { if let callKitIntegration = self.callKitIntegration { if self.didSetCurrentAudioOutputValue { callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) } } else { audioSessionControl.setOutputMode(.custom(self.currentAudioOutputValue)) audioSessionControl.setup(synchronous: true) } } } let mappedVideoState: PresentationCallState.VideoState let mappedRemoteVideoState: PresentationCallState.RemoteVideoState let mappedRemoteAudioState: PresentationCallState.RemoteAudioState let mappedRemoteBatteryLevel: PresentationCallState.RemoteBatteryLevel if let callContextState = callContextState { switch callContextState.videoState { case .notAvailable: mappedVideoState = .notAvailable case .active: mappedVideoState = .active(isScreencast: self.isScreencastActive, endpointId: "") case .inactive: mappedVideoState = .inactive case .paused: mappedVideoState = .paused(isScreencast: self.isScreencastActive, endpointId: "") } switch callContextState.remoteVideoState { case .inactive: mappedRemoteVideoState = .inactive case .active: mappedRemoteVideoState = .active(endpointId: "") case .paused: mappedRemoteVideoState = .paused(endpointId: "") } switch callContextState.remoteAudioState { case .active: mappedRemoteAudioState = .active case .muted: mappedRemoteAudioState = .muted } switch callContextState.remoteBatteryLevel { case .normal: mappedRemoteBatteryLevel = .normal case .low: mappedRemoteBatteryLevel = .low } self.previousVideoState = mappedVideoState self.previousRemoteVideoState = mappedRemoteVideoState self.previousRemoteAudioState = mappedRemoteAudioState self.previousRemoteBatteryLevel = mappedRemoteBatteryLevel } else { if let previousVideoState = self.previousVideoState { mappedVideoState = previousVideoState } else { if self.isVideo { mappedVideoState = .active(isScreencast: self.isScreencastActive, endpointId: "") } else if self.isVideoPossible && sessionState.isVideoPossible { mappedVideoState = .inactive } else { mappedVideoState = .notAvailable } } mappedRemoteVideoState = .inactive if let previousRemoteAudioState = self.previousRemoteAudioState { mappedRemoteAudioState = previousRemoteAudioState } else { mappedRemoteAudioState = .active } if let previousRemoteBatteryLevel = self.previousRemoteBatteryLevel { mappedRemoteBatteryLevel = previousRemoteBatteryLevel } else { mappedRemoteBatteryLevel = .normal } } switch sessionState.state { case .ringing: presentationState = PresentationCallState(state: .ringing, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) if previous == nil || previousControl == nil { if !self.reportedIncomingCall, let stableId = sessionState.stableId { self.reportedIncomingCall = true var phoneNumber: String? if case let .user(peer) = self.peer, let phone = peer.phone { phoneNumber = formatPhoneNumber(context: self.context, number: phone) } self.callKitIntegration?.reportIncomingCall( uuid: self.internalId, stableId: stableId, handle: "\(self.peerId.id._internalGetInt64Value())", phoneNumber: phoneNumber, isVideo: sessionState.type == .video, displayTitle: self.peer?.debugDisplayTitle ?? "Unknown", completion: { [weak self] error in if let error = error { if error.domain == "com.apple.CallKit.error.incomingcall" && (error.code == -3 || error.code == 3) { Logger.shared.log("PresentationCall", "reportIncomingCall device in DND mode") Queue.mainQueue().async { /*if let strongSelf = self { strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .busy, debugLog: .single(nil)) }*/ } } else { Logger.shared.log("PresentationCall", "reportIncomingCall error \(error)") Queue.mainQueue().async { if let strongSelf = self { strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp, debugLog: .single(nil)) } } } } } ) } } case .accepting: self.callWasActive = true presentationState = PresentationCallState(state: .connecting(nil), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) case let .dropping(reason): if case .ended(.switchedToConference) = reason { } else { presentationState = PresentationCallState(state: .terminating(reason), videoState: mappedVideoState, remoteVideoState: .inactive, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) } case let .terminated(id, reason, options): presentationState = PresentationCallState(state: .terminated(id, reason, self.callWasActive && (options.contains(.reportRating) || self.shouldPresentCallRating)), videoState: mappedVideoState, remoteVideoState: .inactive, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) case let .requesting(ringing): presentationState = PresentationCallState(state: .requesting(ringing), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) case .active(_, _, _, _, _, _, _, _), .switchedToConference: self.callWasActive = true var isConference = false if case .switchedToConference = sessionState.state { isConference = true } if let callContextState = callContextState, !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _) = sessionState.state { switch callContextState.state { case .initializing: presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) case .failed: presentationState = PresentationCallState(state: .terminating(.error(.disconnected)), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) case .connected: let timestamp: Double if let activeTimestamp = self.activeTimestamp { timestamp = activeTimestamp } else { timestamp = CFAbsoluteTimeGetCurrent() self.activeTimestamp = timestamp } presentationState = PresentationCallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) case .reconnecting: let timestamp: Double if let activeTimestamp = self.activeTimestamp { timestamp = activeTimestamp } else { timestamp = CFAbsoluteTimeGetCurrent() self.activeTimestamp = timestamp } presentationState = PresentationCallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) } } else if !isConference, case let .active(_, _, keyVisualHash, _, _, _, _, _) = sessionState.state { presentationState = PresentationCallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel) } } var conferenceCallData: InternalGroupCallReference? if let incomingConferenceSource = self.incomingConferenceSource { if self.isAcceptingIncomingConference { conferenceCallData = .message(id: incomingConferenceSource) } } else { switch sessionState.state { case let .switchedToConference(slug): conferenceCallData = .link(slug: slug) default: break } } if let conferenceCallData { if self.conferenceCallDisposable == nil { let conferenceCallSignal = self.context.engine.calls.getCurrentGroupCall(reference: conferenceCallData) self.conferenceCallDisposable = (conferenceCallSignal |> deliverOnMainQueue).startStrict(next: { [weak self] groupCall in guard let self else { return } let keyPair: TelegramKeyPair? = TelegramE2EEncryptionProviderImpl.shared.generateKeyPair() guard let keyPair, let groupCall else { self.updateSessionState(sessionState: CallSession( id: self.internalId, stableId: nil, isOutgoing: false, type: .audio, state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), isVideoPossible: true ), callContextState: nil, reception: nil, audioSessionControl: self.audioSessionControl) return } let conferenceCall = PresentationGroupCallImpl( accountContext: self.context, audioSession: self.audioSession, callKitIntegration: self.callKitIntegration, getDeviceAccessData: self.getDeviceAccessData, initialCall: (EngineGroupCallDescription( id: groupCall.info.id, accessHash: groupCall.info.accessHash, title: nil, scheduleTimestamp: nil, subscribedToScheduled: false, isStream: false ), conferenceCallData), internalId: CallSessionInternalId(), peerId: nil, isChannel: false, invite: nil, joinAsPeerId: nil, isStream: false, keyPair: keyPair, conferenceSourceId: self.internalId, isConference: true, sharedAudioContext: self.sharedAudioContext ) self.conferenceCallImpl = conferenceCall conferenceCall.upgradedConferenceCall = self conferenceCall.setConferenceInvitedPeers(self.pendingInviteToConferencePeerIds) for peerId in self.pendingInviteToConferencePeerIds { let _ = conferenceCall.invitePeer(peerId) } conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted) if let videoCapturer = self.videoCapturer { conferenceCall.requestVideo(capturer: videoCapturer) } let waitForLocalVideo = self.videoCapturer != nil let waitForRemotePeerId: EnginePeer.Id? = self.peerId var waitForRemoteVideo: EnginePeer.Id? if let callContextState = self.callContextState { switch callContextState.remoteVideoState { case .active, .paused: waitForRemoteVideo = self.peerId case .inactive: break } } self.conferenceStateValue = .preparing self.waitForConferenceCallReadyDisposable?.dispose() self.waitForConferenceCallReadyDisposable = (combineLatest(queue: .mainQueue(), conferenceCall.state, conferenceCall.members ) |> filter { state, members in if state.networkState != .connected { return false } if let waitForRemotePeerId { var found = false if let members { for participant in members.participants { if participant.peer.id == waitForRemotePeerId { found = true break } } } if !found { return false } } if waitForLocalVideo { if let members { for participant in members.participants { if participant.peer.id == state.myPeerId { if participant.videoDescription == nil { return false } } } } } if let waitForRemoteVideo { if let members { for participant in members.participants { if participant.peer.id == waitForRemoteVideo { if participant.videoDescription == nil { return false } } } } } return true } |> map { _, _ -> Void in return Void() } |> take(1) |> timeout(10.0, queue: .mainQueue(), alternate: .single(Void()))).start(next: { [weak self] _ in guard let self else { return } self.ongoingContextStateDisposable?.dispose() self.conferenceStateValue = .ready let upgradedToConferenceCompletions = self.upgradedToConferenceCompletions.copyItems() self.upgradedToConferenceCompletions.removeAll() for f in upgradedToConferenceCompletions { f(conferenceCall) } }) }, error: { [weak self] _ in guard let self else { return } self.updateSessionState(sessionState: CallSession( id: self.internalId, stableId: nil, isOutgoing: false, type: .audio, state: .terminated(id: nil, reason: .error(.generic), options: CallTerminationOptions()), isVideoPossible: true ), callContextState: nil, reception: nil, audioSessionControl: self.audioSessionControl) }) } } switch sessionState.state { case .requesting: if let _ = audioSessionControl { self.audioSessionShouldBeActive.set(true) } case let .active(id, key, _, connections, maxLayer, version, customParameters, allowsP2P): self.audioSessionShouldBeActive.set(true) if conferenceCallData != nil { if sessionState.isOutgoing { self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date()) } } else { if (self.sharedAudioContext != nil || audioSessionControl != nil), !wasActive || (self.sharedAudioContext == nil && previousControl == nil) { let logName = "\(id.id)_\(id.accessHash)" let updatedConnections = connections let contextAudioSessionActive: Signal if self.sharedAudioContext != nil { contextAudioSessionActive = .single(true) } else { contextAudioSessionActive = self.audioSessionActive.get() } let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, callId: id, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: updatedConnections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: contextAudioSessionActive, logName: logName, preferredVideoCodec: self.preferredVideoCodec, audioDevice: self.sharedAudioContext?.audioDevice) self.ongoingContext = ongoingContext ongoingContext.setIsMuted(self.isMutedValue) if let requestedVideoAspect = self.requestedVideoAspect { ongoingContext.setRequestedVideoAspect(requestedVideoAspect) } self.debugInfoValue.set(ongoingContext.debugInfo()) self.ongoingContextStateDisposable = (ongoingContext.state |> deliverOnMainQueue).start(next: { [weak self] contextState in if let strongSelf = self { if let sessionState = strongSelf.sessionState { strongSelf.updateSessionState(sessionState: sessionState, callContextState: contextState, reception: strongSelf.reception, audioSessionControl: strongSelf.audioSessionControl) } else { strongSelf.callContextState = contextState } } }) self.audioLevelDisposable = (ongoingContext.audioLevel |> deliverOnMainQueue).start(next: { [weak self] level in if let strongSelf = self { strongSelf.audioLevelPromise.set(level) } }) func batteryLevelIsLowSignal() -> Signal { return Signal { subscriber in let device = UIDevice.current device.isBatteryMonitoringEnabled = true var previousBatteryLevelIsLow = false let timer = SwiftSignalKit.Timer(timeout: 30.0, repeat: true, completion: { let batteryLevelIsLow = device.batteryLevel >= 0.0 && device.batteryLevel < 0.1 && device.batteryState != .charging if batteryLevelIsLow != previousBatteryLevelIsLow { previousBatteryLevelIsLow = batteryLevelIsLow subscriber.putNext(batteryLevelIsLow) } }, queue: Queue.mainQueue()) timer.start() return ActionDisposable { device.isBatteryMonitoringEnabled = false timer.invalidate() } } } self.batteryLevelDisposable = (batteryLevelIsLowSignal() |> deliverOnMainQueue).start(next: { [weak self] batteryLevelIsLow in if let strongSelf = self, let ongoingContext = strongSelf.ongoingContext { ongoingContext.setIsLowBatteryLevel(batteryLevelIsLow) } }) } } case .switchedToConference: self.audioSessionShouldBeActive.set(true) case let .terminated(_, _, options): self.audioSessionShouldBeActive.set(true) if wasActive { let debugLogValue = Promise() self.ongoingContext?.stop(sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue) } case .dropping: break default: self.audioSessionShouldBeActive.set(false) if wasActive { let debugLogValue = Promise() self.ongoingContext?.stop(debugLogValue: debugLogValue) } } var terminating = false if case .terminated = sessionState.state { terminating = true } else if case let .dropping(reason) = sessionState.state { switch reason { case .ended(.switchedToConference): break default: terminating = true } } if terminating, !wasTerminated { if !self.didSetCanBeRemoved { self.didSetCanBeRemoved = true self.canBeRemovedPromise.set(.single(true) |> delay(2.0, queue: Queue.mainQueue())) } self.hungUpPromise.set(true) if sessionState.isOutgoing { if !self.droppedCall && self.dropCallKitCallTimer == nil { let dropCallKitCallTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in if let strongSelf = self { strongSelf.dropCallKitCallTimer = nil if !strongSelf.droppedCall { strongSelf.droppedCall = true strongSelf.callKitIntegration?.dropCall(uuid: strongSelf.internalId) } } }, queue: Queue.mainQueue()) self.dropCallKitCallTimer = dropCallKitCallTimer dropCallKitCallTimer.start() } } else { self.callKitIntegration?.dropCall(uuid: self.internalId) } } var isConference = false if case .switchedToConference = sessionState.state { isConference = true } if self.conferenceCallImpl != nil { isConference = true } if self.conferenceStateValue != nil { isConference = true } if self.incomingConferenceSource != nil { isConference = true } if isConference { if self.currentTone != nil { self.currentTone = nil self.sharedAudioContext?.audioDevice?.setTone(tone: nil) } } else { if let presentationState { self.statePromise.set(presentationState) self.updateTone(presentationState, callContextState: callContextState, previous: previous) } } } private func updateTone(_ state: PresentationCallState, callContextState: OngoingCallContextState?, previous: CallSession?) { if self.isMovedToConference { return } var tone: PresentationCallTone? if let callContextState = callContextState, case .reconnecting = callContextState.state { if !self.isVideo { tone = .connecting } } else if let previous = previous { switch previous.state { case .accepting, .active, .dropping, .requesting: switch state.state { case .connecting: if case .requesting = previous.state { tone = .ringing } else { if !self.isVideo { tone = .connecting } } case .requesting(true): tone = .ringing case let .terminated(_, reason, _): if let reason = reason { switch reason { case let .ended(type): switch type { case .busy: tone = .busy case .hungUp, .missed: tone = .ended case .switchedToConference: tone = nil } case .error: tone = .failed } } default: break } default: break } } if tone != self.currentTone { self.currentTone = tone self.sharedAudioContext?.audioDevice?.setTone(tone: tone.flatMap(presentationCallToneData).flatMap { data in return OngoingCallContext.Tone(samples: data, sampleRate: 48000, loopCount: tone?.loopCount ?? 1000000) }) } } private func updateIsAudioSessionActive(_ value: Bool) { if self.isMovedToConference { return } if self.isAudioSessionActive != value { self.isAudioSessionActive = value } } public func answer() { if self.isMovedToConference { return } self.answer(fromCallKitAction: false) } func answer(fromCallKitAction: Bool) { if self.isMovedToConference { return } let (presentationData, present, openSettings) = self.getDeviceAccessData() DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in present(c, a) }, openSettings: { openSettings() }, { [weak self] value in guard let strongSelf = self else { return } if value { if strongSelf.isVideo { DeviceAccess.authorizeAccess(to: .camera(.videoCall), presentationData: presentationData, present: { c, a in present(c, a) }, openSettings: { openSettings() }, { [weak strongSelf] value in guard let strongSelf else { return } if value { if strongSelf.incomingConferenceSource != nil { strongSelf.conferenceStateValue = .preparing strongSelf.isAcceptingIncomingConference = true strongSelf.updateSessionState(sessionState: CallSession( id: strongSelf.internalId, stableId: nil, isOutgoing: false, type: .audio, state: .ringing, isVideoPossible: true ), callContextState: nil, reception: nil, audioSessionControl: strongSelf.audioSessionControl) } else { strongSelf.callSessionManager.accept(internalId: strongSelf.internalId) } if !fromCallKitAction { strongSelf.callKitIntegration?.answerCall(uuid: strongSelf.internalId) } } else { let _ = strongSelf.hangUp().start() } }) } else { if strongSelf.incomingConferenceSource != nil { strongSelf.conferenceStateValue = .preparing strongSelf.isAcceptingIncomingConference = true strongSelf.updateSessionState(sessionState: CallSession( id: strongSelf.internalId, stableId: nil, isOutgoing: false, type: .audio, state: .ringing, isVideoPossible: true ), callContextState: nil, reception: nil, audioSessionControl: strongSelf.audioSessionControl) } else { strongSelf.callSessionManager.accept(internalId: strongSelf.internalId) } if !fromCallKitAction { strongSelf.callKitIntegration?.answerCall(uuid: strongSelf.internalId) } } } else { let _ = strongSelf.hangUp().start() } }) } public func hangUp() -> Signal { if self.isMovedToConference { return .single(true) } let debugLogValue = Promise() self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: debugLogValue.get()) self.ongoingContext?.stop(debugLogValue: debugLogValue) return self.hungUpPromise.get() } public func rejectBusy() { if self.isMovedToConference { return } self.callSessionManager.drop(internalId: self.internalId, reason: .busy, debugLog: .single(nil)) let debugLog = Promise() self.ongoingContext?.stop(debugLogValue: debugLog) } public func toggleIsMuted() { if self.isMovedToConference { return } self.setIsMuted(!self.isMutedValue) } public func setIsMuted(_ value: Bool) { if self.isMovedToConference { return } self.isMutedValue = value self.isMutedPromise.set(self.isMutedValue) self.ongoingContext?.setIsMuted(self.isMutedValue) } public func requestVideo() { if self.isMovedToConference { return } if self.videoCapturer == nil { let videoCapturer = OngoingCallVideoCapturer() self.videoCapturer = videoCapturer } if let videoCapturer = self.videoCapturer { if let ongoingContext = self.ongoingContext { ongoingContext.requestVideo(videoCapturer) } } } public func requestVideo(capturer: OngoingCallVideoCapturer) { if self.isMovedToConference { return } if self.videoCapturer == nil { self.videoCapturer = capturer } if let videoCapturer = self.videoCapturer { if let ongoingContext = self.ongoingContext { ongoingContext.requestVideo(videoCapturer) } } } public func setRequestedVideoAspect(_ aspect: Float) { if self.isMovedToConference { return } self.requestedVideoAspect = aspect self.ongoingContext?.setRequestedVideoAspect(aspect) } public func disableVideo() { if self.isMovedToConference { return } if let _ = self.videoCapturer { self.videoCapturer = nil if let ongoingContext = self.ongoingContext { ongoingContext.disableVideo() } } } private func resetScreencastContext() { if self.isMovedToConference { return } let basePath = self.context.sharedContext.basePath + "/broadcast-coordination" let screencastBufferServerContext = IpcGroupCallBufferAppContext(basePath: basePath) self.screencastBufferServerContext = screencastBufferServerContext self.screencastFramesDisposable.set((screencastBufferServerContext.frames |> deliverOnMainQueue).start(next: { [weak screencastCapturer] screencastFrame in guard let screencastCapturer = screencastCapturer else { return } guard let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: screencastFrame.0) else { return } screencastCapturer.injectSampleBuffer(sampleBuffer, rotation: screencastFrame.1, completion: {}) })) self.screencastAudioDataDisposable.set((screencastBufferServerContext.audioData |> deliverOnMainQueue).start(next: { [weak self] data in guard let strongSelf = self else { return } strongSelf.ongoingContext?.addExternalAudioData(data: data) })) self.screencastStateDisposable.set((screencastBufferServerContext.isActive |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] isActive in guard let strongSelf = self else { return } if isActive { strongSelf.requestScreencast() } else { strongSelf.disableScreencast(reset: false) } })) } private func requestScreencast() { if self.isMovedToConference { return } self.disableVideo() if let screencastCapturer = self.screencastCapturer { self.isScreencastActive = true if let ongoingContext = self.ongoingContext { ongoingContext.requestVideo(screencastCapturer) } } } func disableScreencast(reset: Bool = true) { if self.isMovedToConference { return } if self.isScreencastActive { if let _ = self.videoCapturer { self.videoCapturer = nil } self.isScreencastActive = false self.ongoingContext?.disableVideo() self.conferenceCallImpl?.disableVideo() if reset { self.resetScreencastContext() } } } public func setOutgoingVideoIsPaused(_ isPaused: Bool) { if self.isMovedToConference { return } self.videoCapturer?.setIsVideoEnabled(!isPaused) } public func upgradeToConference(invitePeerIds: [EnginePeer.Id], completion: @escaping (PresentationGroupCall) -> Void) -> Disposable { if self.isMovedToConference { return EmptyDisposable } if let conferenceCall = self.conferenceCall { completion(conferenceCall) return EmptyDisposable } self.pendingInviteToConferencePeerIds = invitePeerIds let index = self.upgradedToConferenceCompletions.add({ call in completion(call) }) self.conferenceStateValue = .preparing self.callSessionManager.createConferenceIfNecessary(internalId: self.internalId) return ActionDisposable { [weak self] in Queue.mainQueue().async { guard let self else { return } self.upgradedToConferenceCompletions.remove(index) } } } public func setCurrentAudioOutput(_ output: AudioSessionOutput) { if self.isMovedToConference { return } if let sharedAudioContext = self.sharedAudioContext { sharedAudioContext.setCurrentAudioOutput(output) return } guard self.currentAudioOutputValue != output else { return } self.currentAudioOutputValue = output self.didSetCurrentAudioOutputValue = true self.audioOutputStatePromise.set(.single((self.audioOutputStateValue.0, output)) |> then( .single(self.audioOutputStateValue) |> delay(1.0, queue: Queue.mainQueue()) )) if let audioSessionControl = self.audioSessionControl { if let callKitIntegration = self.callKitIntegration { callKitIntegration.applyVoiceChatOutputMode(outputMode: .custom(self.currentAudioOutputValue)) } else { audioSessionControl.setOutputMode(.custom(output)) } } } public func debugInfo() -> Signal<(String, String), NoError> { return self.debugInfoValue.get() } func video(isIncoming: Bool) -> Signal? { if self.isMovedToConference { return nil } if isIncoming { if let ongoingContext = self.ongoingContext { return ongoingContext.video(isIncoming: isIncoming) } else { return nil } } else if let videoCapturer = self.videoCapturer { return videoCapturer.video() } else { return nil } } public func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) { if self.isMovedToConference { completion(nil) return } if self.videoCapturer == nil { let videoCapturer = OngoingCallVideoCapturer() self.videoCapturer = videoCapturer } self.videoCapturer?.makeOutgoingVideoView(requestClone: false, completion: { view, _ in if let view = view { let setOnFirstFrameReceived = view.setOnFirstFrameReceived let setOnOrientationUpdated = view.setOnOrientationUpdated let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated let updateIsEnabled = view.updateIsEnabled completion(PresentationCallVideoView( holder: view, view: view.view, setOnFirstFrameReceived: { f in setOnFirstFrameReceived(f) }, getOrientation: { [weak view] in if let view = view { let mappedValue: PresentationCallVideoView.Orientation switch view.getOrientation() { case .rotation0: mappedValue = .rotation0 case .rotation90: mappedValue = .rotation90 case .rotation180: mappedValue = .rotation180 case .rotation270: mappedValue = .rotation270 } return mappedValue } else { return .rotation0 } }, getAspect: { [weak view] in if let view = view { return view.getAspect() } else { return 0.0 } }, setOnOrientationUpdated: { f in setOnOrientationUpdated { value, aspect in let mappedValue: PresentationCallVideoView.Orientation switch value { case .rotation0: mappedValue = .rotation0 case .rotation90: mappedValue = .rotation90 case .rotation180: mappedValue = .rotation180 case .rotation270: mappedValue = .rotation270 } f?(mappedValue, aspect) } }, setOnIsMirroredUpdated: { f in setOnIsMirroredUpdated { value in f?(value) } }, updateIsEnabled: { value in updateIsEnabled(value) } )) } else { completion(nil) } }) } public func switchVideoCamera() { if self.isMovedToConference { return } self.useFrontCamera = !self.useFrontCamera self.videoCapturer?.switchVideoInput(isFront: self.useFrontCamera) } public func playRemoteCameraTone() { let name: String name = "voip_group_recording_started.mp3" self.beginTone(tone: .custom(name: name, loopCount: 1)) } private func beginTone(tone: PresentationCallTone?) { if let tone, let toneData = presentationCallToneData(tone) { if let sharedAudioContext = self.sharedAudioContext { sharedAudioContext.audioDevice?.setTone(tone: OngoingCallContext.Tone( samples: toneData, sampleRate: 48000, loopCount: tone.loopCount ?? 100000 )) } } else { if let sharedAudioContext = self.sharedAudioContext { sharedAudioContext.audioDevice?.setTone(tone: nil) } } } func deactivateIncomingAudio() { self.ongoingContext?.deactivateIncomingAudio() } } func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? { var maybeFormat: CMVideoFormatDescription? let status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &maybeFormat) if status != noErr { return nil } guard let format = maybeFormat else { return nil } var timingInfo = CMSampleTimingInfo( duration: CMTimeMake(value: 1, timescale: 30), presentationTimeStamp: CMTimeMake(value: 0, timescale: 30), decodeTimeStamp: CMTimeMake(value: 0, timescale: 30) ) var maybeSampleBuffer: CMSampleBuffer? let bufferStatus = CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &maybeSampleBuffer) if (bufferStatus != noErr) { return nil } guard let sampleBuffer = maybeSampleBuffer else { return nil } let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber return sampleBuffer }