123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // Threema iOS Client
- // Copyright (c) 2019-2020 Threema GmbH
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License, version 3,
- // as published by the Free Software Foundation.
- //
- // 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 Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- import Foundation
- import WebRTC
- import ThreemaFramework
- class CallViewController: UIViewController {
- @IBOutlet private weak var backgroundImage: UIImageView!
- @IBOutlet private weak var contentView: UIView!
- @IBOutlet private weak var contactLabel: UILabel!
- @IBOutlet private weak var verificationLevel: UIImageView!
- @IBOutlet private weak var debugLabel: UILabel!
-
- @IBOutlet private weak var acceptButton: UIButton!
- @IBOutlet private weak var rejectButton: UIButton!
-
- @IBOutlet private weak var hideButton: UIButton!
- @IBOutlet private weak var timerLabel: UILabel!
-
- @IBOutlet private weak var localVideoView: UIView!
- @IBOutlet private weak var remoteVideoView: UIView!
-
- @IBOutlet weak var muteButton: UIButton!
- @IBOutlet weak var speakerButton: UIButton!
- @IBOutlet weak var endButton: UIButton!
- @IBOutlet weak var cameraButton: UIButton!
- @IBOutlet weak var cameraSwitchButton: UIButton!
-
- @IBOutlet weak var callInfoStackView: UIStackView!
- @IBOutlet weak var callInfoStackViewTopConstraint: NSLayoutConstraint!
- @IBOutlet weak var phoneButtonsStackView: UIStackView!
- @IBOutlet weak var phoneButtonsStackViewBottomConstraint: NSLayoutConstraint!
-
- @IBOutlet weak var localVideoViewConstraintHeight: NSLayoutConstraint!
- @IBOutlet weak var localVideoViewConstraintWidth: NSLayoutConstraint!
-
- @IBOutlet weak var localVideoViewConstraintLeft: NSLayoutConstraint!
- @IBOutlet weak var localVideoViewConstraintRight: NSLayoutConstraint!
- @IBOutlet weak var localVideoViewConstraintBottom: NSLayoutConstraint!
- @IBOutlet weak var localVideoViewConstraintBottomNavigation: NSLayoutConstraint!
- @IBOutlet weak var localVideoViewConstraintTop: NSLayoutConstraint!
- @IBOutlet weak var localVideoViewConstraintTopNavigation: NSLayoutConstraint!
- @IBOutlet weak var localVideoViewConstraintTopNavigationLabel: NSLayoutConstraint!
-
- @IBOutlet weak var phoneButtonsGradientView: UIView!
- @IBOutlet weak var callInfoGradientView: UIView!
- var contact: Contact?
- var alreadyAccepted: Bool = false
- var isCallInitiator: Bool = false
- var isTesting: Bool = false
- var viewWasHidden: Bool = false
- var threemaVideoCallAvailable: Bool = false
- var isLocalVideoActive: Bool = false
- var isReceivingRemoteVideo: Bool = false {
- didSet {
- if self.isReceivingRemoteVideo {
- self.startRemoteVideo()
- } else {
- self.endRemoteVideo()
- }
- }
- }
- private var statsTimer: Timer?
-
- private var useBackCamera: Bool = false
- private var myVolumeView: UIView?
-
- private var initiatorVideoCallShowcase: MaterialShowcase?
- private var remoteVideoActivatedShowcase: MaterialShowcase?
- private var speakerShowcase: MaterialShowcase?
- private var cameraDisabledShowcase: MaterialShowcase?
-
- private var didRotateDevice: Bool = false
-
- required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self)
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- modalPresentationCapturesStatusBarAppearance = true
-
- muteButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_mute")
- speakerButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_speaker")
- endButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_end")
- acceptButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_accept")
- rejectButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_reject")
- hideButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_hide_call")
-
- cameraButton.accessibilityLabel = BundleUtil.localizedString(forKey: self.isLocalVideoActive ? "call_camera_deactivate_button" : "call_camera_activate_button")
- cameraSwitchButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_camera_switch_to_back_button")
-
- NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: nil) { (n) in
- DispatchQueue.main.async {
- let currentRoute = AVAudioSession.sharedInstance().currentRoute
-
- for output in currentRoute.outputs {
- if self.isBeingDismissed {
- UIDevice.current.isProximityMonitoringEnabled = false
- } else {
- if output.portType == AVAudioSession.Port.builtInReceiver {
- if UserSettings.shared()?.disableProximityMonitoring == false && !UIDevice.current.isProximityMonitoringEnabled && VoIPCallStateManager.shared.currentCallState() != .idle {
- UIDevice.current.isProximityMonitoringEnabled = true
- }
- } else {
- if UIDevice.current.isProximityMonitoringEnabled {
- UIDevice.current.isProximityMonitoringEnabled = false
- }
- }
- }
-
- if output.portType == AVAudioSession.Port.builtInSpeaker {
- self.speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .normal)
- self.speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .highlighted)
- self.speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .selected)
- }
- else if output.portType == AVAudioSession.Port.headphones {
- self.speakerButton.setImage(UIImage.init(named: "HeadphoneActive"), for: .normal)
- self.speakerButton.setImage(UIImage.init(named: "HeadphoneActive"), for: .highlighted)
- self.speakerButton.setImage(UIImage.init(named: "HeadphoneActive"), for: .selected)
- }
- else if output.portType == AVAudioSession.Port.bluetoothA2DP || output.portType == AVAudioSession.Port.bluetoothHFP || output.portType == AVAudioSession.Port.bluetoothLE {
- self.speakerButton.setImage(UIImage.init(named: "BluetoothActive"), for: .normal)
- self.speakerButton.setImage(UIImage.init(named: "BluetoothActive"), for: .highlighted)
- self.speakerButton.setImage(UIImage.init(named: "BluetoothActive"), for: .selected)
- }
- else {
- self.speakerButton.setImage(UIImage.init(named: "SpeakerInactive"), for: .normal)
- self.speakerButton.setImage(UIImage.init(named: "SpeakerInactive"), for: .highlighted)
- self.speakerButton.setImage(UIImage.init(named: "SpeakerInactive"), for: .selected)
- }
-
- guard let info = n.userInfo,
- let value = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
- let reason = AVAudioSession.RouteChangeReason(rawValue: value) else { return }
-
- switch reason {
- case .newDeviceAvailable, .oldDeviceUnavailable:
- DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
- self.checkAndHandleAvailableBluetoothDevices()
- })
- break
- default: break
- }
- }
- }
- }
-
- let longPressRecognizer = UILongPressGestureRecognizer.init(target: self, action: #selector(handleLongPress))
- hideButton.addGestureRecognizer(longPressRecognizer)
-
- let panGR = UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:)))
- localVideoView.addGestureRecognizer(panGR)
-
- let showHideNavigation = UITapGestureRecognizer(target: self, action: #selector(showHideNavigation(gesture:)))
- remoteVideoView.addGestureRecognizer(showHideNavigation)
-
- let switchVideoViews = UITapGestureRecognizer(target: self, action: #selector(switchVideoViews(gesture:)))
- localVideoView.addGestureRecognizer(switchVideoViews)
-
- updateConstraintsAfterRotation(size: CGSize(width: 80.0, height: 107.0))
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
-
- VoIPHelper.shared()?.isCallActiveInBackground = false
- muteButton.isSelected = VoIPCallStateManager.shared.isCallMuted()
- if isTesting == false {
- if UserSettings.shared()?.disableProximityMonitoring == false {
- UIDevice.current.isProximityMonitoringEnabled = true
- }
- }
- UIApplication.shared.isIdleTimerDisabled = true
- setupView()
-
- if !isNavigationVisible() {
- moveLocalVideoViewToCorrectPosition(moveNavigation: true)
- }
-
- checkAndHandleAvailableBluetoothDevices()
- }
-
- override internal func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- showInitiatorVideoCallInfo()
-
- updateGradientBackground()
- }
-
- override internal func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
-
- UIDevice.current.isProximityMonitoringEnabled = false
- UIApplication.shared.isIdleTimerDisabled = false
- switch VoIPCallStateManager.shared.currentCallState() {
- case .ended, .remoteEnded, .rejected, .rejectedBusy, .rejectedTimeout, .rejectedDisabled, .rejectedOffHours, .rejectedUnknown, .microphoneDisabled:
- DispatchQueue.main.async {
- self.removeAllSubviewsFromVideoViews()
- }
- break
- default:
- break
- }
-
- hideInitiatorVideoCallInfo()
- hideRemoteVideoActivatedInfo()
- hideSpeakerInfo()
- }
-
- override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- didRotateDevice = true
- }
-
- override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
-
- if didRotateDevice {
- didRotateDevice = false
- updateGradientBackground()
- #if arch(arm64)
- if let rR = VoIPCallStateManager.shared.remoteVideoRenderer(),
- let remoteRenderer = rR as? RTCMTLVideoView {
- var remoteVideoSize = remoteVideoView.frame.size
- if isRemoteRendererInLocalView() {
- remoteVideoSize = localVideoView.frame.size
- }
- updateRemoteVideoContentMode(videoView: remoteRenderer, size: remoteVideoSize)
- }
- #endif
- }
- }
-
- override internal var supportedInterfaceOrientations: UIInterfaceOrientationMask {
- if #available(iOS 11.0, *) {
- return .all
- } else {
- return .portrait
- }
- }
-
- override internal var shouldAutorotate: Bool {
- if #available(iOS 11.0, *) {
- return true
- } else {
- return false
- }
- }
-
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .lightContent
- }
- }
- extension CallViewController {
- // MARK: Public functions
-
- func voIPCallStatusChanged(state: VoIPCallService.CallState, oldState: VoIPCallService.CallState) {
- if isTesting == true {
- return
- }
- var timerString = ""
- switch state {
- case .idle:
- timerString = BundleUtil.localizedString(forKey: "call_status_wait_ringing")
- break
- case .sendOffer:
- timerString = BundleUtil.localizedString(forKey: "call_status_wait_ringing")
- break
- case .receivedOffer:
- timerString = BundleUtil.localizedString(forKey: "call_status_wait_ringing")
- break
- case .outgoingRinging:
- timerString = BundleUtil.localizedString(forKey: "call_status_ringing")
- break
- case .incomingRinging:
- timerString = BundleUtil.localizedString(forKey: "call_status_incom_ringing")
- case .sendAnswer:
- timerString = BundleUtil.localizedString(forKey: "call_status_ringing")
- break
- case .receivedAnswer:
- timerString = BundleUtil.localizedString(forKey: "call_status_ringing")
- break
- case .initalizing:
- timerString = BundleUtil.localizedString(forKey: "call_status_initializing")
- break
- case .calling:
- timerString = BundleUtil.localizedString(forKey: "call_status_calling")
- break
- case .reconnecting:
- if oldState != .remoteEnded && oldState != .ended {
- timerString = BundleUtil.localizedString(forKey: "call_status_reconnecting")
- }
- break
- case .ended, .remoteEnded:
- timerString = BundleUtil.localizedString(forKey: "call_end")
- break
- case .rejected:
- timerString = BundleUtil.localizedString(forKey: "call_rejected")
- break
- case .rejectedBusy:
- timerString = BundleUtil.localizedString(forKey: "call_rejected_busy")
- break
- case .rejectedTimeout:
- timerString = BundleUtil.localizedString(forKey: "call_rejected_timeout")
- break
- case .rejectedOffHours:
- timerString = BundleUtil.localizedString(forKey: "call_rejected")
- break
- case .rejectedUnknown:
- timerString = BundleUtil.localizedString(forKey: "call_rejected")
- break
- case .rejectedDisabled:
- timerString = BundleUtil.localizedString(forKey: "call_rejected_disabled")
- break
- case .microphoneDisabled:
- timerString = BundleUtil.localizedString(forKey: "call_microphone_permission_title")
- break
- }
- DispatchQueue.main.async {
- self.timerLabel?.text = timerString
- self.muteButton?.isEnabled = state == .calling || state == .reconnecting
- }
-
- updateView()
- }
-
- func voIPCallDurationChanged(_ time: Int) {
- DispatchQueue.main.async {
- if self.timerLabel != nil {
- self.timerLabel.text = DateFormatter.timeFormatted(time)
- }
- }
- }
-
- func startDebugMode(connection: RTCPeerConnection) {
- let dict = ["connection": connection]
- statsTimer?.invalidate()
- statsTimer = nil
- DispatchQueue.main.async {
- var previousState: VoIPStatsState? = nil
- self.statsTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (timer) in
- if let connection = dict["connection"] {
- connection.statistics { (report) in
- let options = VoIPStatsOptions.init()
- options.selectedCandidatePair = true
- options.transport = true
- options.crypto = true
- options.inboundRtp = true
- options.outboundRtp = true
- options.tracks = true
- options.candidatePairsFlag = .OVERVIEW
- let stats = VoIPStats.init(report: report, options: options, transceivers: connection.transceivers, previousState: previousState)
- previousState = stats.buildVoIPStatsState()
- DispatchQueue.main.async {
- if self.debugLabel != nil {
- if self.debugLabel.isHidden == false {
- var statsString = stats.getShortRepresentation()
- if self.threemaVideoCallAvailable {
- statsString += "\n\n\(CallsignalingProtocol.printDebugQualityProfiles(remoteProfile: VoIPCallStateManager.shared.remoteVideoQualityProfile(), networkIsRelayed: VoIPCallStateManager.shared.networkIsRelayed()))"
- }
- self.debugLabel.text = statsString
- }
- }
- }
- }
- }
- })
- }
- }
-
- func resetStatsTimer() {
- statsTimer?.invalidate()
- statsTimer = nil
- }
-
- func enableThreemaVideoCall() {
- DispatchQueue.main.async {
- self.threemaVideoCallAvailable = true
- }
- }
-
- func disableThreemaVideoCall() {
- DispatchQueue.main.async {
- self.threemaVideoCallAvailable = false
- if VoIPCallStateManager.shared.localVideoRenderer() != nil && UserSettings.shared().enableVideoCall {
- self.showCameraDisabledInfo()
- }
- self.endLocalVideo()
- }
- }
- }
- extension CallViewController {
- // MARK: Private functions
-
- private func setupView() {
- contactLabel.text = contact?.displayName
-
- backgroundImage.contentMode = contact!.isProfilePictureSet() ? .scaleAspectFill : .scaleAspectFit
- backgroundImage.image = blurImage(image: AvatarMaker.shared().callBackground(for: contact), blurRadius: 4.0)
- backgroundImage.backgroundColor = Colors.black()
- verificationLevel.image = contact?.verificationLevelImage()
- if isTesting == false {
- debugLabel.isHidden = true
-
- updateView()
- timerLabel.isHidden = false
- }
-
- if UIDevice.current.userInterfaceIdiom == .pad {
- speakerButton.isSelected = true
- }
-
- debugLabel.text = ""
-
- acceptButton.setImage(UIImage.init(named: "AcceptCall", in: Colors.green()), for: .normal)
- acceptButton.setImage(UIImage.init(named: "AcceptCall", in: Colors.green()), for: .selected)
- acceptButton.setImage(UIImage.init(named: "AcceptCall", in: Colors.green()), for: .highlighted)
-
- rejectButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .normal)
- rejectButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .selected)
- rejectButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .highlighted)
-
- endButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .normal)
- endButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .selected)
- endButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .highlighted)
-
- cameraButton.setImage(UIImage.init(named: "VideoInactive"), for: .normal)
- cameraButton.setImage(UIImage.init(named: "VideoInactive"), for: .selected)
- cameraButton.setImage(UIImage.init(named: "VideoInactive"), for: .highlighted)
- cameraButton.layer.cornerRadius = cameraButton.frame.width / 2
- cameraButton.layer.masksToBounds = false
-
- contactLabel.layer.shadowColor = UIColor.black.cgColor
- contactLabel.layer.shadowOffset = CGSize(width: 0, height: 0)
- contactLabel.layer.shadowRadius = 1.0
- contactLabel.layer.shadowOpacity = 0.2
-
- timerLabel.layer.shadowColor = UIColor.black.cgColor
- timerLabel.layer.shadowOffset = CGSize(width: 0, height: 0)
- timerLabel.layer.shadowRadius = 1.0
- timerLabel.layer.shadowOpacity = 0.2
-
- cameraSwitchButton.setImage(UIImage.init(named: "SwitchCam"), for: .normal)
- cameraSwitchButton.setImage(UIImage.init(named: "SwitchCam"), for: .selected)
- cameraSwitchButton.setImage(UIImage.init(named: "SwitchCam"), for: .highlighted)
- cameraSwitchButton.layer.cornerRadius = cameraSwitchButton.frame.width / 2
- cameraSwitchButton.layer.masksToBounds = false
-
- muteButton.setImage(UIImage.init(named: "MuteInactive"), for: .normal)
- muteButton.setImage(UIImage.init(named: "MuteActive"), for: .selected)
- muteButton.layer.cornerRadius = muteButton.frame.width / 2
- muteButton.layer.masksToBounds = false
-
- speakerButton.layer.cornerRadius = speakerButton.frame.width / 2
- speakerButton.layer.masksToBounds = false
- hideButton.layer.cornerRadius = hideButton.frame.width / 2
- hideButton.layer.shadowColor = UIColor.black.cgColor
- hideButton.layer.shadowOffset = CGSize(width: 0, height: 0)
- hideButton.layer.shadowRadius = 1.0
- hideButton.layer.shadowOpacity = 0.2
- hideButton.layer.masksToBounds = false
-
- if isTesting == true {
- setupForIncomCallTest()
- }
- }
-
- private func updateView() {
- if isTesting == false {
- DispatchQueue.main.async {
- if VoIPCallStateManager.shared.currentCallState() == .microphoneDisabled {
- self.endButton?.isHidden = true
- self.acceptButton?.isHidden = false
- self.rejectButton?.isHidden = false
- self.muteButton?.isHidden = true
- self.phoneButtonsGradientView?.isHidden = true
- self.speakerButton?.isHidden = true
- self.cameraButton?.isHidden = true
- self.cameraSwitchButton?.isHidden = true
- self.localVideoView?.isHidden = true
- self.remoteVideoView?.isHidden = true
-
- self.endButton?.isEnabled = false
- self.acceptButton?.isEnabled = false
- self.rejectButton?.isEnabled = false
- self.muteButton?.isEnabled = false
- self.speakerButton?.isEnabled = false
- self.cameraButton?.isEnabled = false
- self.cameraSwitchButton?.isEnabled = false
-
- self.voIPCallStatusChanged(state: .microphoneDisabled, oldState: .microphoneDisabled)
- } else {
- self.endButton?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
- self.acceptButton?.isHidden = self.isCallInitiator || self.alreadyAccepted
- self.rejectButton?.isHidden = self.isCallInitiator || self.alreadyAccepted
- self.muteButton?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
- self.phoneButtonsGradientView?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
- self.phoneButtonsGradientView?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
- self.speakerButton?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
-
- self.endButton?.isEnabled = true
- self.acceptButton?.isEnabled = true
- self.muteButton?.isEnabled = true
- self.speakerButton?.isEnabled = true
-
- self.updateVideoViews()
- }
- }
- }
- }
-
- private func updateVideoViews() {
- if contact != nil, threemaVideoCallAvailable == true {
- let cameraImageName = self.isLocalVideoActive ? "VideoActive" : "VideoInactive"
- cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .normal)
- cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .selected)
- cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .highlighted)
- cameraButton?.accessibilityLabel = BundleUtil.localizedString(forKey: self.isLocalVideoActive ? "call_camera_deactivate_button" : "call_camera_activate_button")
- cameraButton?.alpha = 1.0
- cameraButton?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
- cameraSwitchButton?.isHidden = !(self.isLocalVideoActive && (AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .back) != nil))
- cameraButton?.isEnabled = true
- cameraSwitchButton?.isEnabled = true
-
- localVideoView?.isHidden = !(self.isLocalVideoActive && isReceivingRemoteVideo)
- remoteVideoView?.isHidden = !(self.isLocalVideoActive || isReceivingRemoteVideo)
- } else {
- if alreadyAccepted && UserSettings.shared().enableVideoCall {
- cameraButton?.setImage(UIImage.init(named: "VideoInactive")!.withTint(Colors.gray()), for: .normal)
- cameraButton?.setImage(UIImage.init(named: "VideoInactive")!.withTint(Colors.gray()), for: .selected)
- cameraButton?.setImage(UIImage.init(named: "VideoInactive")!.withTint(Colors.gray()), for: .highlighted)
- cameraButton?.accessibilityLabel = BundleUtil.localizedString(forKey: "call_camera_deactivate_button")
- cameraButton?.isHidden = isCallInitiator && !UserSettings.shared().enableVideoCall
- cameraButton?.alpha = 0.9
- } else {
- cameraButton?.alpha = 1.0
- cameraButton?.isHidden = true
- }
-
- localVideoView?.isHidden = true
- remoteVideoView?.isHidden = true
- cameraSwitchButton?.isHidden = true
- cameraButton?.isEnabled = true
- cameraSwitchButton?.isEnabled = false
- }
- }
-
- private func setupForIncomCallTest() {
- DispatchQueue.main.async {
- self.endButton.isHidden = true
- self.acceptButton.isHidden = false
- self.rejectButton.isHidden = false
- self.muteButton.isHidden = true
- self.speakerButton.isHidden = true
- self.timerLabel.isHidden = false
- self.cameraButton.isHidden = true
- self.cameraSwitchButton.isHidden = true
- self.debugLabel.isHidden = true
-
- self.timerLabel.text = BundleUtil.localizedString(forKey: "call_status_incom_ringing")
- }
- }
-
- private func setupForConnectedCallTest() {
- debugLabel.isHidden = true
- endButton.isHidden = false
- acceptButton.isHidden = true
- rejectButton.isHidden = true
- muteButton.isHidden = false
- speakerButton.isHidden = false
- timerLabel.isHidden = false
- cameraButton.isHidden = false
- cameraSwitchButton.isHidden = true
-
- self.endButton?.isEnabled = true
- self.muteButton?.isEnabled = true
- self.speakerButton?.isEnabled = true
- self.cameraButton?.isEnabled = true
- self.cameraSwitchButton.isEnabled = true
-
- DispatchQueue.main.async {
- self.timerLabel.text = "08:15"
- }
- speakerButton.setImage(UIImage.init(named: "SpeakerInactive"), for: .normal)
- speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .highlighted)
-
- cameraButton?.setImage(UIImage.init(named: "VideoInactive"), for: .normal)
- cameraButton?.setImage(UIImage.init(named: "VideoInactive"), for: .selected)
- cameraButton?.setImage(UIImage.init(named: "VideoInactive"), for: .highlighted)
- }
-
- private func setupForVideoCallTest() {
- debugLabel.isHidden = true
- endButton.isHidden = false
- acceptButton.isHidden = true
- rejectButton.isHidden = true
- muteButton.isHidden = false
- speakerButton.isHidden = false
- timerLabel.isHidden = false
- cameraButton.isHidden = false
- cameraButton.isSelected = true
- cameraSwitchButton.isHidden = false
-
- self.endButton?.isEnabled = true
- self.muteButton?.isEnabled = true
- self.speakerButton?.isEnabled = true
- self.cameraButton?.isEnabled = true
- self.cameraSwitchButton.isEnabled = true
-
- let cameraImageName = "VideoActive"
- self.cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .normal)
- self.cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .selected)
- self.cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .highlighted)
-
- DispatchQueue.main.async {
- self.timerLabel.text = "08:15"
- }
- speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .normal)
- speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .highlighted)
-
- self.localVideoView?.isHidden = false
- self.remoteVideoView?.isHidden = false
-
- var meImage = AvatarMaker.shared().unknownPersonImage()
- if let profilePicture = MyIdentityStore.shared()?.profilePicture {
- if let data = profilePicture["ProfilePicture"] {
- meImage = UIImage(data: data as! Data)
- }
- }
-
- let meImageView = UIImageView(image: meImage)
- meImageView.contentMode = .scaleAspectFill
- embedView(meImageView, into: localVideoView)
-
- let remoteImageView = UIImageView()
- remoteImageView.image = AvatarMaker.shared()?.avatar(for: contact, size: remoteVideoView.frame.size.width, masked: false)
- remoteImageView.contentMode = .scaleAspectFill
- embedView(remoteImageView, into: remoteVideoView)
- }
-
- private func startLocalVideo(useBackCamera: Bool = false, switchCamera: Bool = false) {
- DispatchQueue.main.async {
- self.backgroundImage.image = nil
- #if arch(arm64)
- // Using metal (arm64 only)
- let localRenderer = RTCMTLVideoView(frame: self.localVideoView?.frame ?? CGRect.zero)
- localRenderer.videoContentMode = .scaleAspectFill
- #else
- // Using OpenGLES for the rest
- let localRenderer = RTCEAGLVideoView(frame: self.localVideoView?.frame ?? CGRect.zero)
- #endif
- localRenderer.delegate = self
- VoIPCallStateManager.shared.startCaptureLocalVideo(renderer: localRenderer, useBackCamera: useBackCamera, switchCamera: switchCamera)
-
- if (!self.isReceivingRemoteVideo) {
- if let remoteVideoView = self.remoteVideoView {
- self.embedView(localRenderer, into: remoteVideoView)
- self.flipLocalRenderer()
- self.updateVideoViews()
- if switchCamera == false {
- self.activateSpeakerForVideo()
- }
- }
- } else {
- if let localVideoView = self.localVideoView {
- if self.isRemoteRendererInRemoteView() {
- self.embedView(localRenderer, into: localVideoView)
- } else {
- self.embedView(localRenderer, into: self.remoteVideoView)
- }
-
- self.flipLocalRenderer()
- self.updateVideoViews()
- if switchCamera == false {
- self.activateSpeakerForVideo()
- }
- }
- }
- }
- }
-
- private func endLocalVideo(switchCamera: Bool = false) {
- VoIPCallStateManager.shared.endCaptureLocalVideo(switchCamera: switchCamera)
-
- DispatchQueue.main.async {
- if (self.isReceivingRemoteVideo) {
- if let remoteRenderer = VoIPCallStateManager.shared.remoteVideoRenderer() {
- if self.localVideoView.subviews.first == remoteRenderer as? UIView {
- if !switchCamera {
- self.moveEmbedView(remoteRenderer as! UIView, from: self.localVideoView, into: self.remoteVideoView)
- } else {
- self.removeSubviewsFromRemoteView()
- }
- }
- else if self.remoteVideoView.subviews.first == remoteRenderer as? UIView {
- self.removeSubviewsFromLocalView()
- }
- }
- } else {
- self.backgroundImage.image = self.blurImage(image: AvatarMaker.shared().callBackground(for: self.contact), blurRadius: 4.0)
- self.removeAllSubviewsFromVideoViews()
- }
- self.flipLocalRenderer()
- self.updateVideoViews()
- }
- }
- private func startRemoteVideo() {
- DispatchQueue.main.async {
- self.backgroundImage.image = nil
- #if arch(arm64)
- // Using metal (arm64 only)
- let remoteRenderer = RTCMTLVideoView(frame: self.remoteVideoView?.frame ?? CGRect.zero)
- remoteRenderer.videoContentMode = .scaleAspectFill
- #else
- // Using OpenGLES for the rest
- let remoteRenderer = RTCEAGLVideoView(frame: self.remoteVideoView?.frame ?? CGRect.zero)
- #endif
- remoteRenderer.delegate = self
- VoIPCallStateManager.shared.renderRemoteVideo(to: remoteRenderer)
-
- if (!self.isLocalVideoActive || self.localVideoView.subviews.first == VoIPCallStateManager.shared.localVideoRenderer() as? UIView) {
- if let remoteVideoView = self.remoteVideoView {
- self.embedView(remoteRenderer, into: remoteVideoView)
- self.updateVideoViews()
- }
- } else {
- if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer() {
- self.moveEmbedView(localRenderer as! UIView, from: self.remoteVideoView, into: self.localVideoView)
- self.flipLocalRenderer()
- }
- if let remoteVideoView = self.remoteVideoView {
- self.embedView(remoteRenderer, into: remoteVideoView)
- self.updateVideoViews()
- }
- }
-
- if !self.isLocalVideoActive && self.isReceivingRemoteVideo && !self.viewWasHidden && self.isViewLoaded && self.view.window != nil {
- self.showRemoteVideoActivatedInfo()
- self.showSpeakerInfo()
- }
- }
- }
-
- private func endRemoteVideo() {
- VoIPCallStateManager.shared.endRemoteVideo()
- DispatchQueue.main.async {
- if (self.isLocalVideoActive) {
- if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer() {
- if self.localVideoView.subviews.first == localRenderer as? UIView {
- self.moveEmbedView(localRenderer as! UIView, from: self.localVideoView, into: self.remoteVideoView)
- }
- else if self.remoteVideoView.subviews.first == localRenderer as? UIView {
- self.removeSubviewsFromLocalView()
- }
- }
- } else {
- self.backgroundImage.image = self.blurImage(image: AvatarMaker.shared().callBackground(for: self.contact), blurRadius: 4.0)
- self.removeAllSubviewsFromVideoViews()
- }
- self.flipLocalRenderer()
- self.updateVideoViews()
- }
- }
-
- private func embedView(_ view: UIView, into containerView: UIView) {
- containerView.addSubview(view)
- view.translatesAutoresizingMaskIntoConstraints = false
- view.layer.masksToBounds = true
- containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
- containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
- containerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
- containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
-
- containerView.layoutIfNeeded()
- }
-
- private func moveEmbedView(_ view: UIView, from fromContainerView: UIView, into containerView: UIView) {
- self.removeAllSubviewsFromVideoViews()
- self.embedView(view, into: containerView)
- }
-
- private func switchCamera() {
- useBackCamera = !useBackCamera
- cameraButton.accessibilityLabel = BundleUtil.localizedString(forKey: useBackCamera ? "call_camera_switch_to_front_button" : "call_camera_switch_to_back_button")
- endLocalVideo(switchCamera: true)
- startLocalVideo(useBackCamera: useBackCamera, switchCamera: true)
- }
-
- private func isLocalRendererInLocalView() -> Bool {
- if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer() as? UIView {
- if self.localVideoView.subviews.first == localRenderer {
- return true
- }
- }
- return false
- }
-
- private func isLocalRendererInRemoteView() -> Bool {
- if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer() as? UIView {
- if self.remoteVideoView.subviews.first == localRenderer {
- return true
- }
- }
- return false
- }
-
- private func isRemoteRendererInRemoteView() -> Bool {
- if let remoteRenderer = VoIPCallStateManager.shared.remoteVideoRenderer() as? UIView {
- if self.remoteVideoView.subviews.first == remoteRenderer {
- return true
- }
- }
- return false
- }
-
- private func isRemoteRendererInLocalView() -> Bool {
- if let remoteRenderer = VoIPCallStateManager.shared.remoteVideoRenderer() as? UIView {
- if self.localVideoView.subviews.first == remoteRenderer {
- return true
- }
- }
- return false
- }
-
- private func removeSubviewsFromLocalView() {
- guard localVideoView != nil else {
- return
- }
- guard localVideoView.subviews.count > 0 else {
- return
- }
- self.localVideoView.subviews.forEach({ $0.removeFromSuperview() })
- }
-
- private func removeSubviewsFromRemoteView() {
- guard remoteVideoView != nil else {
- return
- }
- guard remoteVideoView.subviews.count > 0 else {
- return
- }
- self.remoteVideoView.subviews.forEach({ $0.removeFromSuperview() })
- }
-
- private func removeAllSubviewsFromVideoViews() {
- removeSubviewsFromLocalView()
- removeSubviewsFromRemoteView()
-
- // show navigation if needed
- if !isNavigationVisible() {
- moveLocalVideoViewToCorrectPosition(moveNavigation: true)
- }
- }
-
- private func flipLocalRenderer() {
- if useBackCamera == false {
- if isLocalRendererInLocalView() {
- if localVideoView != nil {
- localVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: -1, y: 1))
- }
- } else {
- if localVideoView != nil {
- localVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 1))
- }
- }
-
- if isLocalRendererInRemoteView() {
- if remoteVideoView != nil {
- remoteVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: -1, y: 1))
- }
- } else {
- if remoteVideoView != nil {
- remoteVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 1))
- }
- }
- } else {
- if localVideoView != nil {
- localVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 1))
- }
- if remoteVideoView != nil {
- remoteVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 1))
- }
- }
- }
-
- private func hasCameraAccess() -> Bool {
- let access = AVCaptureDevice.authorizationStatus(for: .video)
- if access == .authorized {
- return true
- }
- else if access == .denied || access == .restricted {
- self.showCameraAccessAlert()
- }
- else if access == .notDetermined {
- AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
- if granted {
- self.isLocalVideoActive = !self.isLocalVideoActive
- if self.isLocalVideoActive == true {
- self.startLocalVideo()
- } else {
- self.endLocalVideo()
- }
- } else {
- self.showCameraAccessAlert()
- }
- })
- }
- return false
- }
- private func showCameraAccessAlert() {
- UIAlertTemplate.showAlert(owner: self, title: BundleUtil.localizedString(forKey: "camera_disabled_title"), message: BundleUtil.localizedString(forKey: "camera_disabled_message"))
- }
-
- private func activateSpeakerForVideo() {
- let currentRoute = AVAudioSession.sharedInstance().currentRoute
-
- for output in currentRoute.outputs {
- if output.portType == AVAudioSession.Port.builtInReceiver {
- let action = VoIPCallUserAction.init(action: .speakerOn, contact: self.contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- VoIPCallStateManager.shared.processUserAction(action)
- }
- }
- }
-
- private func isNavigationVisible() -> Bool {
- guard let navigationConstraint = phoneButtonsStackViewBottomConstraint else {
- return true
- }
- return navigationConstraint.constant == 0.0
- }
-
- private func removeAllLocalVideoViewConstraints() {
- localVideoViewConstraintLeft.isActive = false
- localVideoViewConstraintRight.isActive = false
- localVideoViewConstraintBottom.isActive = false
- localVideoViewConstraintBottomNavigation.isActive = false
- localVideoViewConstraintTop.isActive = false
- localVideoViewConstraintTopNavigation.isActive = false
- localVideoViewConstraintTopNavigationLabel.isActive = false
- self.view.layoutIfNeeded()
- }
-
- private func moveLocalVideoViewToCorrectPosition(moveNavigation: Bool = false) {
- let localVideoViewCenterX = localVideoView.center.x
- let localVideoViewCenterY = localVideoView.center.y
- let screenMiddleX = self.view.frame.size.width / 2
- let screenMiddleY = self.view.frame.size.height / 2
-
- self.removeAllLocalVideoViewConstraints()
-
- guard let _ = phoneButtonsStackViewBottomConstraint,
- let _ = callInfoStackViewTopConstraint,
- let _ = phoneButtonsStackView,
- let _ = callInfoStackView,
- let phoneButtonsStackViewSuperView = phoneButtonsStackView.superview,
- let callInfoStackViewSuperView = callInfoStackView.superview else {
- return
- }
-
- if moveNavigation {
- if isNavigationVisible() {
- self.phoneButtonsStackViewBottomConstraint.constant = self.phoneButtonsStackView.frame.size.height + phoneButtonsStackViewSuperView.layoutMargins.bottom + 50.0
- self.callInfoStackViewTopConstraint.constant = -(self.callInfoStackView.frame.size.height + callInfoStackViewSuperView.layoutMargins.top + 50.0)
- } else {
- self.phoneButtonsStackViewBottomConstraint.constant = 0
- self.callInfoStackViewTopConstraint.constant = 16.0
- }
- }
-
- if localVideoViewCenterX < screenMiddleX && localVideoViewCenterY < screenMiddleY {
- self.addConstraintToTopLeftForLocalVideoView()
- }
- else if localVideoViewCenterX > screenMiddleX && localVideoViewCenterY < screenMiddleY {
- self.addConstraintToTopRightForLocalVideoView()
- }
- else if localVideoViewCenterX < screenMiddleX && localVideoViewCenterY > screenMiddleY {
- self.addConstraintToBottomLeftForLocalVideoView()
- }
- else if localVideoViewCenterX > screenMiddleX && localVideoViewCenterY > screenMiddleY {
- self.addConstraintToBottomRightForLocalVideoView()
- }
-
- UIView.animate(withDuration: 0.35, animations: {
- self.phoneButtonsGradientView.setNeedsLayout()
- self.phoneButtonsGradientView.layoutIfNeeded()
- self.callInfoGradientView.setNeedsLayout()
- self.callInfoGradientView.layoutIfNeeded()
- self.view.layoutIfNeeded()
- })
- }
-
- private func addConstraintToTopLeftForLocalVideoView() {
- localVideoViewConstraintLeft.isActive = true
-
- if isNavigationVisible() {
- localVideoViewConstraintTopNavigationLabel.isActive = true
- } else {
- localVideoViewConstraintTop.isActive = true
- }
- }
-
- private func addConstraintToTopRightForLocalVideoView() {
- localVideoViewConstraintRight.isActive = true
- if isNavigationVisible() {
- localVideoViewConstraintTopNavigation.isActive = true
- } else {
- localVideoViewConstraintTop.isActive = true
- }
- }
-
- private func addConstraintToBottomLeftForLocalVideoView() {
- localVideoViewConstraintLeft.isActive = true
- if isNavigationVisible() {
- localVideoViewConstraintBottomNavigation.isActive = true
- } else {
- localVideoViewConstraintBottom.isActive = true
- }
- }
-
- private func addConstraintToBottomRightForLocalVideoView() {
- localVideoViewConstraintRight.isActive = true
- if isNavigationVisible() {
- localVideoViewConstraintBottomNavigation.isActive = true
- } else {
- localVideoViewConstraintBottom.isActive = true
- }
- }
-
- private func updateConstraintsAfterRotation(size: CGSize?) {
- if isLocalRendererInLocalView() || isRemoteRendererInLocalView() {
- var maxSize: CGFloat = 100
- var newSize = size ?? CGSize(width: maxSize, height: 134.0)
-
- if UIDevice.current.userInterfaceIdiom == .pad {
- maxSize = 200
- newSize = size ?? CGSize(width: maxSize, height: 268.0)
- }
-
- if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
- if UIDevice.current.userInterfaceIdiom == .pad {
- maxSize = 270.0
- } else {
- maxSize = 150.0
- }
- }
-
- let ratio = newSize.width / maxSize
- localVideoViewConstraintHeight.constant = newSize.height / ratio
- localVideoViewConstraintWidth.constant = newSize.width / ratio
-
- self.view.layoutIfNeeded()
- }
- }
-
- private func checkAndHandleAvailableBluetoothDevices() {
- var bluetoothAvailable = false
- if let inputs = AVAudioSession.sharedInstance().availableInputs {
- for input in inputs {
- if input.portType == AVAudioSession.Port.bluetoothA2DP || input.portType == AVAudioSession.Port.bluetoothHFP || input.portType == AVAudioSession.Port.bluetoothLE {
- bluetoothAvailable = true
- }
- }
- }
-
- if bluetoothAvailable {
- if myVolumeView == nil {
- if #available(iOS 11.0, *) {
- myVolumeView = AVRoutePickerView.init(frame: CGRect.init(x: 0.0, y: 0.0, width: speakerButton.frame.size.width, height: speakerButton.frame.size.height))
- (myVolumeView as! AVRoutePickerView).activeTintColor = UIColor.clear
- (myVolumeView as! AVRoutePickerView).tintColor = UIColor.clear
- (myVolumeView as! AVRoutePickerView).isOpaque = true
- (myVolumeView as! AVRoutePickerView).alpha = 1.0
- (myVolumeView as! AVRoutePickerView).delegate = self
- if #available(iOS 13.0, *) {
- if isLocalVideoActive || isReceivingRemoteVideo {
- (myVolumeView as! AVRoutePickerView).prioritizesVideoDevices = true
- }
- }
- speakerButton.addSubview(myVolumeView!)
- } else {
- myVolumeView = MPVolumeView(frame: CGRect.init(x: 0.0, y: 0.0, width: speakerButton.frame.size.width, height: speakerButton.frame.size.height))
- (myVolumeView as! MPVolumeView).showsVolumeSlider = false
- (myVolumeView as! MPVolumeView).showsRouteButton = true
- (myVolumeView as! MPVolumeView).setRouteButtonImage(UIImage.init(named: "SpeakerInactive")!.resizedImage(newSize: CGSize(width: 45.0, height: 45.0)), for: .normal)
- (myVolumeView as! MPVolumeView).setRouteButtonImage(UIImage.init(named: "SpeakerActive")!.resizedImage(newSize: CGSize(width: 45.0, height: 45.0)), for: .selected)
- speakerButton.setImage(nil, for: .normal)
- speakerButton.addSubview(myVolumeView!)
- }
- }
- } else {
- speakerButton.subviews.forEach({
- if $0 == myVolumeView {
- $0.removeFromSuperview()
- }
- })
- myVolumeView = nil
- }
- }
-
- private func showInitiatorVideoCallInfo() {
- if let contact = contact, contact.isVideoCallAvailable() && isNavigationVisible() && isCallInitiator && UserSettings.shared().enableVideoCall && !UserSettings.shared().videoCallInfoShown && threemaVideoCallAvailable {
- self.presentInitiatorVideoCallMaterialShowcase()
- }
- }
-
- private func presentInitiatorVideoCallMaterialShowcase(completion handler: (()-> Void)? = nil) {
- if viewIfLoaded?.window != nil {
- if initiatorVideoCallShowcase == nil {
- initiatorVideoCallShowcase = MaterialShowcase()
- initiatorVideoCallShowcase!.setTargetView(button: cameraButton)
-
- initiatorVideoCallShowcase!.primaryText = BundleUtil.localizedString(forKey: "call_threema_video_in_chat_info_title")
- initiatorVideoCallShowcase!.secondaryText = BundleUtil.localizedString(forKey: "call_threema_initiator_video_info")
- initiatorVideoCallShowcase!.backgroundPromptColor = LicenseStore.requiresLicenseKey() ? Colors.workBlue() : Colors.green()
-
- initiatorVideoCallShowcase!.backgroundPromptColorAlpha = 0.7
- initiatorVideoCallShowcase!.backgroundRadius = -1
- initiatorVideoCallShowcase!.targetHolderColor = Colors.black()?.withAlphaComponent(0.2)
- initiatorVideoCallShowcase!.primaryTextSize = 24.0
- initiatorVideoCallShowcase!.secondaryTextSize = 18.0
- initiatorVideoCallShowcase!.primaryTextColor = Colors.white()
-
- initiatorVideoCallShowcase!.primaryTextAlignment = .right
- initiatorVideoCallShowcase!.secondaryTextAlignment = .right
- initiatorVideoCallShowcase!.onTapThrough = {
- self.startVideoAction(self.cameraButton)
- }
- initiatorVideoCallShowcase!.delegate = self
- }
-
- initiatorVideoCallShowcase!.show(hasSkipButton: false, completion: handler)
- }
- }
-
- private func hideInitiatorVideoCallInfo() {
- if let showcase = initiatorVideoCallShowcase {
- showcase.completeShowcase()
- }
- }
-
- private func showRemoteVideoActivatedInfo() {
- DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
- if !self.isLocalVideoActive {
- if let contact = self.contact, self.isNavigationVisible() && contact.isVideoCallAvailable() && UserSettings.shared().enableVideoCall && self.threemaVideoCallAvailable {
- self.presentRemoteVideoActivatedMaterialShowcase {
- DispatchQueue.main.asyncAfter(deadline: .now() + 6, execute: {
- self.hideRemoteVideoActivatedInfo()
- })
- }
- }
- }
- })
- }
-
- private func presentRemoteVideoActivatedMaterialShowcase(completion handler: (()-> Void)?) {
- if viewIfLoaded?.window != nil {
- if remoteVideoActivatedShowcase == nil {
- remoteVideoActivatedShowcase = MaterialShowcase()
- remoteVideoActivatedShowcase!.setTargetView(button: cameraButton)
- remoteVideoActivatedShowcase!.primaryText = BundleUtil.localizedString(forKey: "call_threema_remote_video_activated_info_title")
- remoteVideoActivatedShowcase!.secondaryText = BundleUtil.localizedString(forKey: "call_threema_remote_video_activated_info")
- remoteVideoActivatedShowcase!.backgroundPromptColor = Colors.darkGrey()
-
- remoteVideoActivatedShowcase!.backgroundPromptColorAlpha = 0.7
- remoteVideoActivatedShowcase!.backgroundRadius = -1
- remoteVideoActivatedShowcase!.targetHolderColor = Colors.black()?.withAlphaComponent(0.2)
- remoteVideoActivatedShowcase!.primaryTextSize = 24.0
- remoteVideoActivatedShowcase!.secondaryTextSize = 18.0
- remoteVideoActivatedShowcase!.primaryTextColor = Colors.white()
- remoteVideoActivatedShowcase!.primaryTextAlignment = .right
- remoteVideoActivatedShowcase!.secondaryTextAlignment = .right
- remoteVideoActivatedShowcase!.onTapThrough = {
- self.startVideoAction(self.cameraButton)
- }
-
- }
- remoteVideoActivatedShowcase!.show(hasSkipButton: false, completion: handler)
- }
- }
-
- private func hideRemoteVideoActivatedInfo() {
- if let showcase = remoteVideoActivatedShowcase {
- showcase.completeShowcase()
- }
- }
-
- private func showSpeakerInfo() {
- DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {
- var showSpeakerInfo = true
- let currentRoute = AVAudioSession.sharedInstance().currentRoute
- for output in currentRoute.outputs {
- if output.portType == AVAudioSession.Port.builtInSpeaker {
- showSpeakerInfo = false
- }
- else if output.portType == AVAudioSession.Port.headphones {
- showSpeakerInfo = false
- }
- else if output.portType == AVAudioSession.Port.bluetoothA2DP || output.portType == AVAudioSession.Port.bluetoothHFP || output.portType == AVAudioSession.Port.bluetoothLE {
- showSpeakerInfo = false
- }
- }
- if !self.isLocalVideoActive && showSpeakerInfo {
- if let contact = self.contact, self.isNavigationVisible() && contact.isVideoCallAvailable() && UserSettings.shared().enableVideoCall && !UserSettings.shared().videoCallSpeakerInfoShown && self.threemaVideoCallAvailable && self.isViewLoaded && self.view.window != nil {
- self.presentSpaekerMaterialShowcase {
- DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
- self.hideSpeakerInfo()
- })
- }
- }
- }
- })
- }
-
- private func presentSpaekerMaterialShowcase(completion handler: (()-> Void)?) {
- if viewIfLoaded?.window != nil {
- if speakerShowcase == nil {
- speakerShowcase = MaterialShowcase()
- speakerShowcase!.setTargetView(button: speakerButton)
- speakerShowcase!.primaryText = ""
- speakerShowcase!.secondaryText = BundleUtil.localizedString(forKey: "call_threema_speaker_info")
- speakerShowcase!.backgroundPromptColor = Colors.darkGrey()
-
- speakerShowcase!.backgroundPromptColorAlpha = 0.7
- speakerShowcase!.backgroundRadius = -1
- speakerShowcase!.targetHolderColor = Colors.black()?.withAlphaComponent(0.2)
- speakerShowcase!.secondaryTextSize = 18.0
- speakerShowcase!.primaryTextColor = Colors.white()
- speakerShowcase!.primaryTextAlignment = .left
- speakerShowcase!.secondaryTextAlignment = .left
- speakerShowcase!.onTapThrough = {
- UserSettings.shared().videoCallSpeakerInfoShown = true
- self.speakerShowcase!.completeShowcase()
- self.speakerAction(self.speakerButton, forEvent: UIEvent())
- }
- speakerShowcase!.skipButton = {
- UserSettings.shared().videoCallSpeakerInfoShown = true
- self.speakerShowcase!.completeShowcase()
- }
- }
-
- speakerShowcase!.layoutMargins = UIEdgeInsets(top: (speakerShowcase!.containerView.frame.size.height / 3) * 2, left: 0.0, bottom: 0.0, right: 0.0)
- speakerShowcase!.show(hasSkipButton: true, completion: handler)
- }
- }
-
- private func hideSpeakerInfo() {
- if let showcase = speakerShowcase {
- showcase.completeShowcase()
- }
- }
-
- private func showCameraDisabledInfo() {
- if isNavigationVisible() {
- presentCameraDisabledMaterialShowcase {
- DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
- self.hideCameraDisalbedInfo()
- })
- }
- }
- }
-
- private func presentCameraDisabledMaterialShowcase(completion handler: (()-> Void)?) {
- if viewIfLoaded?.window != nil {
- if cameraDisabledShowcase == nil {
- cameraDisabledShowcase = MaterialShowcase()
- cameraDisabledShowcase!.setTargetView(button: cameraButton)
- cameraDisabledShowcase!.primaryText = ""
- cameraDisabledShowcase!.secondaryText = BundleUtil.localizedString(forKey: "call_threema_camera_disabled_info")
- cameraDisabledShowcase!.backgroundPromptColor = Colors.darkGrey()
-
- cameraDisabledShowcase!.backgroundPromptColorAlpha = 0.7
- cameraDisabledShowcase!.backgroundRadius = -1
- cameraDisabledShowcase!.targetHolderColor = Colors.black()?.withAlphaComponent(0.2)
- cameraDisabledShowcase!.secondaryTextSize = 18.0
- cameraDisabledShowcase!.primaryTextColor = Colors.white()
- cameraDisabledShowcase!.primaryTextAlignment = .right
- cameraDisabledShowcase!.secondaryTextAlignment = .right
- }
-
- cameraDisabledShowcase!.show(hasSkipButton: false, completion: handler)
- }
- }
-
- private func hideCameraDisalbedInfo() {
- if let showcase = cameraDisabledShowcase {
- showcase.completeShowcase()
- }
- }
-
- private func blurImage(image: UIImage, blurRadius: CGFloat) -> UIImage {
- let context = CIContext(options: nil)
- let inputImage = CIImage(cgImage: image.cgImage!)
- let blurFilter = CIFilter(name: "CIGaussianBlur")
- blurFilter?.setValue(inputImage, forKey: kCIInputImageKey)
- blurFilter?.setValue(blurRadius, forKey: "inputRadius")
-
- var bounds = inputImage.extent
- if AvatarMaker.shared().isDefaultAvatar(for: contact) {
- bounds = CGRect(x: -10.0, y: -10.0, width: image.size.width + 20.0, height: image.size.height + 20.0)
- } else {
- bounds = CGRect(x: bounds.origin.x + 10.0, y: bounds.origin.y + 10.0, width: bounds.size.width - 20.0, height: bounds.size.height - 20.0)
- }
-
- let outputImage = blurFilter?.value(forKey: kCIOutputImageKey) as? CIImage
- let cgImage = context.createCGImage(outputImage ?? CIImage(), from: bounds)
- return UIImage(cgImage: cgImage!)
- }
-
- private func addGradientToView(view: UIView, startColor: UIColor, middleColor: UIColor, endColor: UIColor, locations: [NSNumber]) {
- let gradientLayerMask = CAGradientLayer()
- gradientLayerMask.colors = [startColor.cgColor, middleColor.cgColor, endColor.cgColor]
- gradientLayerMask.locations = locations
- gradientLayerMask.frame = view.bounds
- view.layer.insertSublayer(gradientLayerMask, at: 0)
- view.backgroundColor = .clear
- }
-
- private func updateGradientBackground() {
-
- self.callInfoGradientView.setNeedsLayout()
- self.callInfoGradientView.layoutIfNeeded()
- self.phoneButtonsGradientView.setNeedsLayout()
- self.phoneButtonsGradientView.layoutIfNeeded()
-
- self.callInfoGradientView.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
- self.phoneButtonsGradientView.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
-
- self.addGradientToView(view: self.callInfoGradientView, startColor: UIColor.black.withAlphaComponent(0.2), middleColor: UIColor.black.withAlphaComponent(0.1), endColor: UIColor.white.withAlphaComponent(0.0), locations: [0, 0.7, 1])
- self.addGradientToView(view: self.phoneButtonsGradientView, startColor: UIColor.white.withAlphaComponent(0.0), middleColor: UIColor.black.withAlphaComponent(0.1),endColor: UIColor.black.withAlphaComponent(0.2), locations: [0, 0.3, 1])
- }
-
- private func updateRemoteVideoContentMode(videoView: RTCVideoRenderer, size: CGSize) {
- #if arch(arm64)
- if let rR = VoIPCallStateManager.shared.remoteVideoRenderer(),
- let remoteRenderer = rR as? RTCMTLVideoView,
- remoteRenderer.isEqual(videoView) {
- if UIApplication.shared.statusBarOrientation.isPortrait,
- size.height > size.width {
- remoteRenderer.videoContentMode = .scaleAspectFill
- } else {
- if UIApplication.shared.statusBarOrientation.isLandscape,
- size.height < size.width {
- remoteRenderer.videoContentMode = .scaleAspectFill
- } else {
- remoteRenderer.videoContentMode = .scaleAspectFit
- }
- }
- }
- #endif
- }
-
- }
- extension CallViewController {
- // MARK: Actions
-
- @objc private func handleLongPress(gestureRecognizer: UIGestureRecognizer) {
- if gestureRecognizer.state == .began {
- // Toggle debug label
- debugLabel.isHidden = !debugLabel.isHidden
-
- if debugLabel.isHidden == true {
- debugLabel.text = ""
- }
- }
-
- updateViewConstraints()
- }
-
- @IBAction func hideView(sender: UIButton) {
- if isTesting == true {
- setupForConnectedCallTest()
- } else {
- VoIPHelper.shared()?.isCallActiveInBackground = true
- VoIPHelper.shared()?.contactName = contact?.displayName
-
- UIDevice.current.isProximityMonitoringEnabled = false
- dismiss(animated: true) {
- if AppDelegate.shared()?.isAppLocked == true {
- AppDelegate.shared()?.presentPasscodeView()
- }
- }
- }
- }
-
- @IBAction func acceptAction(_ sender: UIButton, forEvent event: UIEvent) {
- let action = VoIPCallUserAction.init(action: .accept, contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- VoIPCallStateManager.shared.processUserAction(action)
- }
-
- @IBAction func rejectAction(_ sender: UIButton, forEvent event: UIEvent) {
- if isTesting == true {
- dismiss(animated: true, completion: nil)
- } else {
- let action = VoIPCallUserAction.init(action: .reject, contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- VoIPCallStateManager.shared.processUserAction(action)
- }
- }
-
- @IBAction func endAction(_ sender: UIButton, forEvent event: UIEvent) {
- DDLogNotice("Threema call: HangupBug -> User pressed the end call button")
- let action = VoIPCallUserAction.init(action: .end, contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- VoIPCallStateManager.shared.processUserAction(action)
- }
-
- @IBAction func muteAction(_ sender: UIButton, forEvent event: UIEvent) {
- let action = VoIPCallUserAction.init(action: VoIPCallStateManager.shared.isCallMuted() ? .unmuteAudio : .muteAudio , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- muteButton.isSelected = action.action == .muteAudio
- VoIPCallStateManager.shared.processUserAction(action)
- }
-
- @IBAction func speakerAction(_ sender: UIButton, forEvent event: UIEvent) {
- self.checkAndHandleAvailableBluetoothDevices()
-
- let audioSession = AVAudioSession.sharedInstance()
- for output in audioSession.currentRoute.outputs {
- switch output.portType {
- case .builtInReceiver:
- let action = VoIPCallUserAction.init(action: .speakerOn , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- VoIPCallStateManager.shared.processUserAction(action)
- break
- case .builtInSpeaker:
- let action = VoIPCallUserAction.init(action: .speakerOff , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- VoIPCallStateManager.shared.processUserAction(action)
- break
- case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
- let action = VoIPCallUserAction.init(action: .speakerOn , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- VoIPCallStateManager.shared.processUserAction(action)
- break
- case .headphones:
- let action = VoIPCallUserAction.init(action: .speakerOn , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
- VoIPCallStateManager.shared.processUserAction(action)
- break
- default:
- break
- }
- }
- }
-
- @IBAction func startVideoAction(_ sender: UIButton) {
- if isTesting == true {
- setupForVideoCallTest()
- } else {
- if threemaVideoCallAvailable {
- if hasCameraAccess() {
- self.isLocalVideoActive = !self.isLocalVideoActive
- if self.isLocalVideoActive == true {
- startLocalVideo()
- } else {
- endLocalVideo()
- }
- }
- } else {
- self.showCameraDisabledInfo()
- }
- }
- }
-
- @IBAction func switchCameraAction(_ sender: UIButton, forEvent event: UIEvent) {
- switchCamera()
- }
-
- @objc func switchVideoViews(gesture: UITapGestureRecognizer) {
- DispatchQueue.main.async {
- if self.isLocalRendererInLocalView() && self.isRemoteRendererInRemoteView() {
- self.removeAllSubviewsFromVideoViews()
- self.embedView(VoIPCallStateManager.shared.remoteVideoRenderer()! as! UIView, into: self.localVideoView)
- self.embedView(VoIPCallStateManager.shared.localVideoRenderer() as! UIView, into: self.remoteVideoView)
- self.flipLocalRenderer()
- }
- else if self.isLocalRendererInRemoteView() && self.isRemoteRendererInLocalView() {
- self.removeAllSubviewsFromVideoViews()
- self.embedView(VoIPCallStateManager.shared.localVideoRenderer()! as! UIView, into: self.localVideoView)
- self.embedView(VoIPCallStateManager.shared.remoteVideoRenderer() as! UIView, into: self.remoteVideoView)
- self.flipLocalRenderer()
- }
- }
- }
-
- @objc func showHideNavigation(gesture: UITapGestureRecognizer) {
- moveLocalVideoViewToCorrectPosition(moveNavigation: true)
- }
-
- @objc func didPan(gesture: UIPanGestureRecognizer) {
- guard let dragView = gesture.view else {
- return
- }
-
- if (gesture.state == .began) {
- dragView.center = gesture.location(in: self.view)
- }
-
- let newCenter: CGPoint = gesture.location(in: self.view)
- let dX = newCenter.x - dragView.center.x
- let dY = newCenter.y - dragView.center.y
- dragView.center = CGPoint(x: dragView.center.x + dX, y: dragView.center.y + dY)
- localVideoView.center = dragView.center
- if gesture.state == .ended {
- moveLocalVideoViewToCorrectPosition()
- }
- }
- }
- @available(iOS 11.0, *)
- extension CallViewController: AVRoutePickerViewDelegate {
- func routePickerViewDidEndPresentingRoutes(_ routePickerView: AVRoutePickerView) {
- self.checkAndHandleAvailableBluetoothDevices()
- }
- }
- extension CallViewController: RTCVideoViewDelegate {
- func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
- updateRemoteVideoContentMode(videoView: videoView, size: size)
- if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer(),
- localRenderer.isEqual(videoView) {
- updateConstraintsAfterRotation(size: size)
- }
- }
- }
- extension CallViewController: MaterialShowcaseDelegate {
- func showCaseDidDismiss(showcase: MaterialShowcase, didTapTarget: Bool) {
- if showcase == initiatorVideoCallShowcase {
- UserSettings.shared().videoCallInfoShown = true
- }
- }
- }
|