/* * Copyright (c) 2010-2020 Belledonne Communications SARL. * * This file is part of linhome * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import Foundation import linphonesw import AVFoundation class ControlsViewModel { var core : Core { get { Core.get() } } let isSpeakerSelected = MutableLiveData() let isMicrophoneMuted = MutableLiveData() let isMuteMicrophoneEnabled = MutableLiveData() let isBluetoothHeadsetSelected = MutableLiveData() let nonEarpieceOutputAudioDevice = MutableLiveData() let audioRoutesSelected = MutableLiveData() let audioRoutesEnabled = MutableLiveData() let isVideoUpdateInProgress = MutableLiveData() let isVideoEnabled = MutableLiveData() let isVideoAvailable = MutableLiveData() let fullScreenMode = MutableLiveData(false) let numpadVisible = MutableLiveData(false) let callStatsVisible = MutableLiveData(false) let goToConferenceLayoutSettings = MutableLiveData(false) let goToConferenceParticipantsListEvent = MutableLiveData(false) let goToChatEvent = MutableLiveData(false) let goToCallsListEvent = MutableLiveData(false) let hideExtraButtons = MutableLiveData(true) let proximitySensorEnabled = MutableLiveData() static let shared = ControlsViewModel() private var coreDelegate : CoreDelegateStub? private var previousCallState = Call.State.Idle init () { coreDelegate = CoreDelegateStub( onCallStateChanged : { (core: Core, call: Call, state: Call.State, message:String) -> Void in Log.i("[Call Controls] Call state changed: \(call) : \(state)") if (state == Call.State.StreamsRunning) { self.isVideoUpdateInProgress.value = false } self.updateUI() self.setAudioRoutes(call,state) self.previousCallState = state }, onAudioDeviceChanged : { (core: Core, audioDevice: AudioDevice) -> Void in Log.i("[Call Controls] Audio device changed: \(audioDevice.deviceName)") self.nonEarpieceOutputAudioDevice.value = audioDevice.type != AudioDeviceType.Microphone // on iOS Earpiece = Microphone self.updateSpeakerState() self.updateBluetoothHeadsetState() } ) Core.get().addDelegate(delegate: coreDelegate!) proximitySensorEnabled.value = shouldProximitySensorBeEnabled() isVideoEnabled.readCurrentAndObserve { _ in self.proximitySensorEnabled.value = self.shouldProximitySensorBeEnabled() } nonEarpieceOutputAudioDevice.readCurrentAndObserve { _ in self.proximitySensorEnabled.value = self.shouldProximitySensorBeEnabled() } proximitySensorEnabled.readCurrentAndObserve { (enabled) in UIDevice.current.isProximityMonitoringEnabled = enabled == true } updateUI() ConferenceViewModel.shared.conferenceDisplayMode.readCurrentAndObserve { _ in self.updateVideoAvailable() } } private func setAudioRoutes(_ call:Call,_ state:Call.State) { if (state == .OutgoingProgress) { if (core.callsNb == 1 && ConfigManager.instance().lpConfigBoolForKey(key: "route_audio_to_bluetooth_if_available",defaultValue:true)) { AudioRouteUtils.routeAudioToBluetooth(call: call) } } if (state == .StreamsRunning) { if (core.callsNb == 1) { // Only try to route bluetooth / headphone / headset when the call is in StreamsRunning for the first time if (previousCallState == Call.State.Connected) { Log.i("[Context] First call going into StreamsRunning state for the first time, trying to route audio to headset or bluetooth if available") if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) { AudioRouteUtils.routeAudioToHeadset(call: call) } else if (ConfigManager.instance().lpConfigBoolForKey(key: "route_audio_to_bluetooth_if_available",defaultValue:true) && AudioRouteUtils.isBluetoothAudioRouteAvailable()) { AudioRouteUtils.routeAudioToBluetooth(call: call) } } } if (ConfigManager.instance().lpConfigBoolForKey(key: "route_audio_to_speaker_when_video_enabled",defaultValue:true) && call.currentParams?.videoEnabled == true) { // Do not turn speaker on when video is enabled if headset or bluetooth is used if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() && !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(call: call) ) { Log.i("[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker") AudioRouteUtils.routeAudioToSpeaker(call: call) } } } } private func shouldProximitySensorBeEnabled() -> Bool { return core.callsNb > 0 && isVideoEnabled.value != true && nonEarpieceOutputAudioDevice.value != true } func hangUp() { if (core.currentCall != nil) { try?core.currentCall?.terminate() } else if (core.conference?.isIn == true) { try?core.terminateConference() } else { try?core.terminateAllCalls() } } func toggleVideo() { if let currentCall = core.currentCall { if (currentCall.conference != nil) { if let params = try?core.createCallParams(call: currentCall) { isVideoUpdateInProgress.value = true params.videoDirection = params.videoDirection == MediaDirection.RecvOnly ? MediaDirection.SendRecv : MediaDirection.RecvOnly try?currentCall.update(params: params) } } else { let state = currentCall.state if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { return } isVideoUpdateInProgress.value = true if let params = try?core.createCallParams(call: currentCall) { params.videoEnabled = !(currentCall.currentParams?.videoEnabled == true) try?currentCall.update(params: params) if (params.videoEnabled) { currentCall.requestNotifyNextVideoFrameDecoded() } } } } } func updateUI() { updateVideoAvailable() updateVideoEnabled() updateMicState() updateSpeakerState() updateAudioRoutesState() proximitySensorEnabled.value = shouldProximitySensorBeEnabled() } private func updateAudioRoutesState() { let bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable() audioRoutesEnabled.value = bluetoothDeviceAvailable if (!bluetoothDeviceAvailable) { audioRoutesSelected.value = false audioRoutesEnabled.value = false } } private func updateSpeakerState() { isSpeakerSelected.value = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed() } private func updateBluetoothHeadsetState() { isBluetoothHeadsetSelected.value = AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed() } private func updateVideoAvailable() { let currentCall = core.currentCall isVideoAvailable.value = (core.videoCaptureEnabled || core.videoPreviewEnabled) && currentCall?.state != .Paused && currentCall?.state != .PausedByRemote && ((currentCall != nil && currentCall?.mediaInProgress() != true) || (core.conference?.isIn == true)) && (ConferenceViewModel.shared.conferenceExists.value != true || ConferenceViewModel.shared.conferenceDisplayMode.value != .AudioOnly) } private func updateVideoEnabled() { let enabled = isVideoCallOrConferenceActive() isVideoEnabled.value = enabled } func updateMicState() { isMicrophoneMuted.value = !micAuthorized() || !core.micEnabled isMuteMicrophoneEnabled.value = CallsViewModel.shared.currentCallData.value??.call != nil } func micAuthorized() -> Bool { return AVCaptureDevice.authorizationStatus(for: .audio) == .authorized } func isVideoCallOrConferenceActive() -> Bool { if let currentCall = core.currentCall, let params = currentCall.params { return params.videoEnabled && (currentCall.conference == nil || params.videoDirection == MediaDirection.SendRecv) } else { return false } } func toggleFullScreen() { fullScreenMode.value = fullScreenMode.value != true } func toggleMuteMicrophone() { if (!micAuthorized()) { askMicrophoneAccess() } core.micEnabled = !core.micEnabled updateMicState() } var microphoneAsking = false func askMicrophoneAccess() { microphoneAsking = true let settings = ButtonAttributes(text:VoipTexts.system_app_settings, action: { self.microphoneAsking = false try! self.core.terminateAllCalls() UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) }, isDestructive:false) let cancel = ButtonAttributes(text:VoipTexts.cancel, action: {self.microphoneAsking = false}, isDestructive:true) VoipDialog(message:VoipTexts.microphone_non_authorized_warning, givenButtons: [cancel,settings]).show() } func forceEarpieceAudioRoute() { if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) { Log.i("[Call Controls] Headset found, route audio to it instead of earpiece") AudioRouteUtils.routeAudioToHeadset() } else { AudioRouteUtils.routeAudioToEarpiece() } } func forceSpeakerAudioRoute() { AudioRouteUtils.routeAudioToSpeaker() } func forceBluetoothAudioRoute() { AudioRouteUtils.routeAudioToBluetooth() } func toggleSpeaker() { if (AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()) { forceEarpieceAudioRoute() } else { forceSpeakerAudioRoute() } } func toggleRoutesMenu() { audioRoutesSelected.value = audioRoutesSelected.value != true } } @objc class ControlsViewModelBridge: NSObject { @objc static func showParticipants() { ControlsViewModel.shared.goToConferenceParticipantsListEvent.value = true } @objc static func toggleStatsVisibility() { ControlsViewModel.shared.callStatsVisible.value = !(ControlsViewModel.shared.callStatsVisible.value ?? false) } }