CallViewController.swift 70 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2019-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. import Foundation
  21. import WebRTC
  22. import ThreemaFramework
  23. class CallViewController: UIViewController {
  24. @IBOutlet private weak var backgroundImage: UIImageView!
  25. @IBOutlet private weak var contentView: UIView!
  26. @IBOutlet private weak var contactLabel: UILabel!
  27. @IBOutlet private weak var verificationLevel: UIImageView!
  28. @IBOutlet private weak var debugLabel: UILabel!
  29. @IBOutlet private weak var acceptButton: UIButton!
  30. @IBOutlet private weak var rejectButton: UIButton!
  31. @IBOutlet private weak var hideButton: UIButton!
  32. @IBOutlet private weak var timerLabel: UILabel!
  33. @IBOutlet private weak var localVideoView: UIView!
  34. @IBOutlet private weak var remoteVideoView: UIView!
  35. @IBOutlet weak var muteButton: UIButton!
  36. @IBOutlet weak var speakerButton: UIButton!
  37. @IBOutlet weak var endButton: UIButton!
  38. @IBOutlet weak var cameraButton: UIButton!
  39. @IBOutlet weak var cameraSwitchButton: UIButton!
  40. @IBOutlet weak var callInfoStackView: UIStackView!
  41. @IBOutlet weak var callInfoStackViewTopConstraint: NSLayoutConstraint!
  42. @IBOutlet weak var phoneButtonsStackView: UIStackView!
  43. @IBOutlet weak var phoneButtonsStackViewBottomConstraint: NSLayoutConstraint!
  44. @IBOutlet weak var localVideoViewConstraintHeight: NSLayoutConstraint!
  45. @IBOutlet weak var localVideoViewConstraintWidth: NSLayoutConstraint!
  46. @IBOutlet weak var localVideoViewConstraintLeft: NSLayoutConstraint!
  47. @IBOutlet weak var localVideoViewConstraintRight: NSLayoutConstraint!
  48. @IBOutlet weak var localVideoViewConstraintBottom: NSLayoutConstraint!
  49. @IBOutlet weak var localVideoViewConstraintBottomNavigation: NSLayoutConstraint!
  50. @IBOutlet weak var localVideoViewConstraintTop: NSLayoutConstraint!
  51. @IBOutlet weak var localVideoViewConstraintTopNavigation: NSLayoutConstraint!
  52. @IBOutlet weak var localVideoViewConstraintTopNavigationLabel: NSLayoutConstraint!
  53. @IBOutlet weak var phoneButtonsGradientView: UIView!
  54. @IBOutlet weak var callInfoGradientView: UIView!
  55. var contact: Contact?
  56. var alreadyAccepted: Bool = false
  57. var isCallInitiator: Bool = false
  58. var isTesting: Bool = false
  59. var viewWasHidden: Bool = false
  60. var threemaVideoCallAvailable: Bool = false
  61. var isLocalVideoActive: Bool = false
  62. var isReceivingRemoteVideo: Bool = false {
  63. didSet {
  64. if self.isReceivingRemoteVideo {
  65. self.startRemoteVideo()
  66. } else {
  67. self.endRemoteVideo()
  68. }
  69. }
  70. }
  71. private var statsTimer: Timer?
  72. private var useBackCamera: Bool = false
  73. private var myVolumeView: UIView?
  74. private var initiatorVideoCallShowcase: MaterialShowcase?
  75. private var remoteVideoActivatedShowcase: MaterialShowcase?
  76. private var speakerShowcase: MaterialShowcase?
  77. private var cameraDisabledShowcase: MaterialShowcase?
  78. private var didRotateDevice: Bool = false
  79. required init?(coder aDecoder: NSCoder) {
  80. super.init(coder: aDecoder)
  81. }
  82. deinit {
  83. NotificationCenter.default.removeObserver(self)
  84. }
  85. override func viewDidLoad() {
  86. super.viewDidLoad()
  87. modalPresentationCapturesStatusBarAppearance = true
  88. muteButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_mute")
  89. speakerButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_speaker")
  90. endButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_end")
  91. acceptButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_accept")
  92. rejectButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_reject")
  93. hideButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_hide_call")
  94. cameraButton.accessibilityLabel = BundleUtil.localizedString(forKey: self.isLocalVideoActive ? "call_camera_deactivate_button" : "call_camera_activate_button")
  95. cameraSwitchButton.accessibilityLabel = BundleUtil.localizedString(forKey: "call_camera_switch_to_back_button")
  96. NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: nil) { (n) in
  97. DispatchQueue.main.async {
  98. let currentRoute = AVAudioSession.sharedInstance().currentRoute
  99. for output in currentRoute.outputs {
  100. if self.isBeingDismissed {
  101. UIDevice.current.isProximityMonitoringEnabled = false
  102. } else {
  103. if output.portType == AVAudioSession.Port.builtInReceiver {
  104. if UserSettings.shared()?.disableProximityMonitoring == false && !UIDevice.current.isProximityMonitoringEnabled && VoIPCallStateManager.shared.currentCallState() != .idle {
  105. UIDevice.current.isProximityMonitoringEnabled = true
  106. }
  107. } else {
  108. if UIDevice.current.isProximityMonitoringEnabled {
  109. UIDevice.current.isProximityMonitoringEnabled = false
  110. }
  111. }
  112. }
  113. if output.portType == AVAudioSession.Port.builtInSpeaker {
  114. self.speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .normal)
  115. self.speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .highlighted)
  116. self.speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .selected)
  117. }
  118. else if output.portType == AVAudioSession.Port.headphones {
  119. self.speakerButton.setImage(UIImage.init(named: "HeadphoneActive"), for: .normal)
  120. self.speakerButton.setImage(UIImage.init(named: "HeadphoneActive"), for: .highlighted)
  121. self.speakerButton.setImage(UIImage.init(named: "HeadphoneActive"), for: .selected)
  122. }
  123. else if output.portType == AVAudioSession.Port.bluetoothA2DP || output.portType == AVAudioSession.Port.bluetoothHFP || output.portType == AVAudioSession.Port.bluetoothLE {
  124. self.speakerButton.setImage(UIImage.init(named: "BluetoothActive"), for: .normal)
  125. self.speakerButton.setImage(UIImage.init(named: "BluetoothActive"), for: .highlighted)
  126. self.speakerButton.setImage(UIImage.init(named: "BluetoothActive"), for: .selected)
  127. }
  128. else {
  129. self.speakerButton.setImage(UIImage.init(named: "SpeakerInactive"), for: .normal)
  130. self.speakerButton.setImage(UIImage.init(named: "SpeakerInactive"), for: .highlighted)
  131. self.speakerButton.setImage(UIImage.init(named: "SpeakerInactive"), for: .selected)
  132. }
  133. guard let info = n.userInfo,
  134. let value = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
  135. let reason = AVAudioSession.RouteChangeReason(rawValue: value) else { return }
  136. switch reason {
  137. case .newDeviceAvailable, .oldDeviceUnavailable:
  138. DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
  139. self.checkAndHandleAvailableBluetoothDevices()
  140. })
  141. break
  142. default: break
  143. }
  144. }
  145. }
  146. }
  147. let longPressRecognizer = UILongPressGestureRecognizer.init(target: self, action: #selector(handleLongPress))
  148. hideButton.addGestureRecognizer(longPressRecognizer)
  149. let panGR = UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:)))
  150. localVideoView.addGestureRecognizer(panGR)
  151. let showHideNavigation = UITapGestureRecognizer(target: self, action: #selector(showHideNavigation(gesture:)))
  152. remoteVideoView.addGestureRecognizer(showHideNavigation)
  153. let switchVideoViews = UITapGestureRecognizer(target: self, action: #selector(switchVideoViews(gesture:)))
  154. localVideoView.addGestureRecognizer(switchVideoViews)
  155. updateConstraintsAfterRotation(size: CGSize(width: 80.0, height: 107.0))
  156. }
  157. override func viewWillAppear(_ animated: Bool) {
  158. super.viewWillAppear(animated)
  159. VoIPHelper.shared()?.isCallActiveInBackground = false
  160. muteButton.isSelected = VoIPCallStateManager.shared.isCallMuted()
  161. if isTesting == false {
  162. if UserSettings.shared()?.disableProximityMonitoring == false {
  163. UIDevice.current.isProximityMonitoringEnabled = true
  164. }
  165. }
  166. UIApplication.shared.isIdleTimerDisabled = true
  167. setupView()
  168. if !isNavigationVisible() {
  169. moveLocalVideoViewToCorrectPosition(moveNavigation: true)
  170. }
  171. checkAndHandleAvailableBluetoothDevices()
  172. }
  173. override internal func viewDidAppear(_ animated: Bool) {
  174. super.viewDidAppear(animated)
  175. showInitiatorVideoCallInfo()
  176. updateGradientBackground()
  177. }
  178. override internal func viewWillDisappear(_ animated: Bool) {
  179. super.viewWillDisappear(animated)
  180. UIDevice.current.isProximityMonitoringEnabled = false
  181. UIApplication.shared.isIdleTimerDisabled = false
  182. switch VoIPCallStateManager.shared.currentCallState() {
  183. case .ended, .remoteEnded, .rejected, .rejectedBusy, .rejectedTimeout, .rejectedDisabled, .rejectedOffHours, .rejectedUnknown, .microphoneDisabled:
  184. DispatchQueue.main.async {
  185. self.removeAllSubviewsFromVideoViews()
  186. }
  187. break
  188. default:
  189. break
  190. }
  191. hideInitiatorVideoCallInfo()
  192. hideRemoteVideoActivatedInfo()
  193. hideSpeakerInfo()
  194. }
  195. override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  196. didRotateDevice = true
  197. }
  198. override func viewDidLayoutSubviews() {
  199. super.viewDidLayoutSubviews()
  200. if didRotateDevice {
  201. didRotateDevice = false
  202. updateGradientBackground()
  203. #if arch(arm64)
  204. if let rR = VoIPCallStateManager.shared.remoteVideoRenderer(),
  205. let remoteRenderer = rR as? RTCMTLVideoView {
  206. var remoteVideoSize = remoteVideoView.frame.size
  207. if isRemoteRendererInLocalView() {
  208. remoteVideoSize = localVideoView.frame.size
  209. }
  210. updateRemoteVideoContentMode(videoView: remoteRenderer, size: remoteVideoSize)
  211. }
  212. #endif
  213. }
  214. }
  215. override internal var supportedInterfaceOrientations: UIInterfaceOrientationMask {
  216. if #available(iOS 11.0, *) {
  217. return .all
  218. } else {
  219. return .portrait
  220. }
  221. }
  222. override internal var shouldAutorotate: Bool {
  223. if #available(iOS 11.0, *) {
  224. return true
  225. } else {
  226. return false
  227. }
  228. }
  229. override var preferredStatusBarStyle: UIStatusBarStyle {
  230. return .lightContent
  231. }
  232. }
  233. extension CallViewController {
  234. // MARK: Public functions
  235. func voIPCallStatusChanged(state: VoIPCallService.CallState, oldState: VoIPCallService.CallState) {
  236. if isTesting == true {
  237. return
  238. }
  239. var timerString = ""
  240. switch state {
  241. case .idle:
  242. timerString = BundleUtil.localizedString(forKey: "call_status_wait_ringing")
  243. break
  244. case .sendOffer:
  245. timerString = BundleUtil.localizedString(forKey: "call_status_wait_ringing")
  246. break
  247. case .receivedOffer:
  248. timerString = BundleUtil.localizedString(forKey: "call_status_wait_ringing")
  249. break
  250. case .outgoingRinging:
  251. timerString = BundleUtil.localizedString(forKey: "call_status_ringing")
  252. break
  253. case .incomingRinging:
  254. timerString = BundleUtil.localizedString(forKey: "call_status_incom_ringing")
  255. case .sendAnswer:
  256. timerString = BundleUtil.localizedString(forKey: "call_status_ringing")
  257. break
  258. case .receivedAnswer:
  259. timerString = BundleUtil.localizedString(forKey: "call_status_ringing")
  260. break
  261. case .initalizing:
  262. timerString = BundleUtil.localizedString(forKey: "call_status_initializing")
  263. break
  264. case .calling:
  265. timerString = BundleUtil.localizedString(forKey: "call_status_calling")
  266. break
  267. case .reconnecting:
  268. if oldState != .remoteEnded && oldState != .ended {
  269. timerString = BundleUtil.localizedString(forKey: "call_status_reconnecting")
  270. }
  271. break
  272. case .ended, .remoteEnded:
  273. timerString = BundleUtil.localizedString(forKey: "call_end")
  274. break
  275. case .rejected:
  276. timerString = BundleUtil.localizedString(forKey: "call_rejected")
  277. break
  278. case .rejectedBusy:
  279. timerString = BundleUtil.localizedString(forKey: "call_rejected_busy")
  280. break
  281. case .rejectedTimeout:
  282. timerString = BundleUtil.localizedString(forKey: "call_rejected_timeout")
  283. break
  284. case .rejectedOffHours:
  285. timerString = BundleUtil.localizedString(forKey: "call_rejected")
  286. break
  287. case .rejectedUnknown:
  288. timerString = BundleUtil.localizedString(forKey: "call_rejected")
  289. break
  290. case .rejectedDisabled:
  291. timerString = BundleUtil.localizedString(forKey: "call_rejected_disabled")
  292. break
  293. case .microphoneDisabled:
  294. timerString = BundleUtil.localizedString(forKey: "call_microphone_permission_title")
  295. break
  296. }
  297. DispatchQueue.main.async {
  298. self.timerLabel?.text = timerString
  299. self.muteButton?.isEnabled = state == .calling || state == .reconnecting
  300. }
  301. updateView()
  302. }
  303. func voIPCallDurationChanged(_ time: Int) {
  304. DispatchQueue.main.async {
  305. if self.timerLabel != nil {
  306. self.timerLabel.text = DateFormatter.timeFormatted(time)
  307. }
  308. }
  309. }
  310. func startDebugMode(connection: RTCPeerConnection) {
  311. let dict = ["connection": connection]
  312. statsTimer?.invalidate()
  313. statsTimer = nil
  314. DispatchQueue.main.async {
  315. var previousState: VoIPStatsState? = nil
  316. self.statsTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (timer) in
  317. if let connection = dict["connection"] {
  318. connection.statistics { (report) in
  319. let options = VoIPStatsOptions.init()
  320. options.selectedCandidatePair = true
  321. options.transport = true
  322. options.crypto = true
  323. options.inboundRtp = true
  324. options.outboundRtp = true
  325. options.tracks = true
  326. options.candidatePairsFlag = .OVERVIEW
  327. let stats = VoIPStats.init(report: report, options: options, transceivers: connection.transceivers, previousState: previousState)
  328. previousState = stats.buildVoIPStatsState()
  329. DispatchQueue.main.async {
  330. if self.debugLabel != nil {
  331. if self.debugLabel.isHidden == false {
  332. var statsString = stats.getShortRepresentation()
  333. if self.threemaVideoCallAvailable {
  334. statsString += "\n\n\(CallsignalingProtocol.printDebugQualityProfiles(remoteProfile: VoIPCallStateManager.shared.remoteVideoQualityProfile(), networkIsRelayed: VoIPCallStateManager.shared.networkIsRelayed()))"
  335. }
  336. self.debugLabel.text = statsString
  337. }
  338. }
  339. }
  340. }
  341. }
  342. })
  343. }
  344. }
  345. func resetStatsTimer() {
  346. statsTimer?.invalidate()
  347. statsTimer = nil
  348. }
  349. func enableThreemaVideoCall() {
  350. DispatchQueue.main.async {
  351. self.threemaVideoCallAvailable = true
  352. }
  353. }
  354. func disableThreemaVideoCall() {
  355. DispatchQueue.main.async {
  356. self.threemaVideoCallAvailable = false
  357. if VoIPCallStateManager.shared.localVideoRenderer() != nil && UserSettings.shared().enableVideoCall {
  358. self.showCameraDisabledInfo()
  359. }
  360. self.endLocalVideo()
  361. }
  362. }
  363. }
  364. extension CallViewController {
  365. // MARK: Private functions
  366. private func setupView() {
  367. contactLabel.text = contact?.displayName
  368. backgroundImage.contentMode = contact!.isProfilePictureSet() ? .scaleAspectFill : .scaleAspectFit
  369. backgroundImage.image = blurImage(image: AvatarMaker.shared().callBackground(for: contact), blurRadius: 4.0)
  370. backgroundImage.backgroundColor = Colors.black()
  371. verificationLevel.image = contact?.verificationLevelImage()
  372. if isTesting == false {
  373. debugLabel.isHidden = true
  374. updateView()
  375. timerLabel.isHidden = false
  376. }
  377. if UIDevice.current.userInterfaceIdiom == .pad {
  378. speakerButton.isSelected = true
  379. }
  380. debugLabel.text = ""
  381. acceptButton.setImage(UIImage.init(named: "AcceptCall", in: Colors.green()), for: .normal)
  382. acceptButton.setImage(UIImage.init(named: "AcceptCall", in: Colors.green()), for: .selected)
  383. acceptButton.setImage(UIImage.init(named: "AcceptCall", in: Colors.green()), for: .highlighted)
  384. rejectButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .normal)
  385. rejectButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .selected)
  386. rejectButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .highlighted)
  387. endButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .normal)
  388. endButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .selected)
  389. endButton.setImage(UIImage.init(named: "RejectCall", in: Colors.red()), for: .highlighted)
  390. cameraButton.setImage(UIImage.init(named: "VideoInactive"), for: .normal)
  391. cameraButton.setImage(UIImage.init(named: "VideoInactive"), for: .selected)
  392. cameraButton.setImage(UIImage.init(named: "VideoInactive"), for: .highlighted)
  393. cameraButton.layer.cornerRadius = cameraButton.frame.width / 2
  394. cameraButton.layer.masksToBounds = false
  395. contactLabel.layer.shadowColor = UIColor.black.cgColor
  396. contactLabel.layer.shadowOffset = CGSize(width: 0, height: 0)
  397. contactLabel.layer.shadowRadius = 1.0
  398. contactLabel.layer.shadowOpacity = 0.2
  399. timerLabel.layer.shadowColor = UIColor.black.cgColor
  400. timerLabel.layer.shadowOffset = CGSize(width: 0, height: 0)
  401. timerLabel.layer.shadowRadius = 1.0
  402. timerLabel.layer.shadowOpacity = 0.2
  403. cameraSwitchButton.setImage(UIImage.init(named: "SwitchCam"), for: .normal)
  404. cameraSwitchButton.setImage(UIImage.init(named: "SwitchCam"), for: .selected)
  405. cameraSwitchButton.setImage(UIImage.init(named: "SwitchCam"), for: .highlighted)
  406. cameraSwitchButton.layer.cornerRadius = cameraSwitchButton.frame.width / 2
  407. cameraSwitchButton.layer.masksToBounds = false
  408. muteButton.setImage(UIImage.init(named: "MuteInactive"), for: .normal)
  409. muteButton.setImage(UIImage.init(named: "MuteActive"), for: .selected)
  410. muteButton.layer.cornerRadius = muteButton.frame.width / 2
  411. muteButton.layer.masksToBounds = false
  412. speakerButton.layer.cornerRadius = speakerButton.frame.width / 2
  413. speakerButton.layer.masksToBounds = false
  414. hideButton.layer.cornerRadius = hideButton.frame.width / 2
  415. hideButton.layer.shadowColor = UIColor.black.cgColor
  416. hideButton.layer.shadowOffset = CGSize(width: 0, height: 0)
  417. hideButton.layer.shadowRadius = 1.0
  418. hideButton.layer.shadowOpacity = 0.2
  419. hideButton.layer.masksToBounds = false
  420. if isTesting == true {
  421. setupForIncomCallTest()
  422. }
  423. }
  424. private func updateView() {
  425. if isTesting == false {
  426. DispatchQueue.main.async {
  427. if VoIPCallStateManager.shared.currentCallState() == .microphoneDisabled {
  428. self.endButton?.isHidden = true
  429. self.acceptButton?.isHidden = false
  430. self.rejectButton?.isHidden = false
  431. self.muteButton?.isHidden = true
  432. self.phoneButtonsGradientView?.isHidden = true
  433. self.speakerButton?.isHidden = true
  434. self.cameraButton?.isHidden = true
  435. self.cameraSwitchButton?.isHidden = true
  436. self.localVideoView?.isHidden = true
  437. self.remoteVideoView?.isHidden = true
  438. self.endButton?.isEnabled = false
  439. self.acceptButton?.isEnabled = false
  440. self.rejectButton?.isEnabled = false
  441. self.muteButton?.isEnabled = false
  442. self.speakerButton?.isEnabled = false
  443. self.cameraButton?.isEnabled = false
  444. self.cameraSwitchButton?.isEnabled = false
  445. self.voIPCallStatusChanged(state: .microphoneDisabled, oldState: .microphoneDisabled)
  446. } else {
  447. self.endButton?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
  448. self.acceptButton?.isHidden = self.isCallInitiator || self.alreadyAccepted
  449. self.rejectButton?.isHidden = self.isCallInitiator || self.alreadyAccepted
  450. self.muteButton?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
  451. self.phoneButtonsGradientView?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
  452. self.phoneButtonsGradientView?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
  453. self.speakerButton?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
  454. self.endButton?.isEnabled = true
  455. self.acceptButton?.isEnabled = true
  456. self.muteButton?.isEnabled = true
  457. self.speakerButton?.isEnabled = true
  458. self.updateVideoViews()
  459. }
  460. }
  461. }
  462. }
  463. private func updateVideoViews() {
  464. if contact != nil, threemaVideoCallAvailable == true {
  465. let cameraImageName = self.isLocalVideoActive ? "VideoActive" : "VideoInactive"
  466. cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .normal)
  467. cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .selected)
  468. cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .highlighted)
  469. cameraButton?.accessibilityLabel = BundleUtil.localizedString(forKey: self.isLocalVideoActive ? "call_camera_deactivate_button" : "call_camera_activate_button")
  470. cameraButton?.alpha = 1.0
  471. cameraButton?.isHidden = !self.isCallInitiator && !self.alreadyAccepted
  472. cameraSwitchButton?.isHidden = !(self.isLocalVideoActive && (AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .back) != nil))
  473. cameraButton?.isEnabled = true
  474. cameraSwitchButton?.isEnabled = true
  475. localVideoView?.isHidden = !(self.isLocalVideoActive && isReceivingRemoteVideo)
  476. remoteVideoView?.isHidden = !(self.isLocalVideoActive || isReceivingRemoteVideo)
  477. } else {
  478. if alreadyAccepted && UserSettings.shared().enableVideoCall {
  479. cameraButton?.setImage(UIImage.init(named: "VideoInactive")!.withTint(Colors.gray()), for: .normal)
  480. cameraButton?.setImage(UIImage.init(named: "VideoInactive")!.withTint(Colors.gray()), for: .selected)
  481. cameraButton?.setImage(UIImage.init(named: "VideoInactive")!.withTint(Colors.gray()), for: .highlighted)
  482. cameraButton?.accessibilityLabel = BundleUtil.localizedString(forKey: "call_camera_deactivate_button")
  483. cameraButton?.isHidden = isCallInitiator && !UserSettings.shared().enableVideoCall
  484. cameraButton?.alpha = 0.9
  485. } else {
  486. cameraButton?.alpha = 1.0
  487. cameraButton?.isHidden = true
  488. }
  489. localVideoView?.isHidden = true
  490. remoteVideoView?.isHidden = true
  491. cameraSwitchButton?.isHidden = true
  492. cameraButton?.isEnabled = true
  493. cameraSwitchButton?.isEnabled = false
  494. }
  495. }
  496. private func setupForIncomCallTest() {
  497. DispatchQueue.main.async {
  498. self.endButton.isHidden = true
  499. self.acceptButton.isHidden = false
  500. self.rejectButton.isHidden = false
  501. self.muteButton.isHidden = true
  502. self.speakerButton.isHidden = true
  503. self.timerLabel.isHidden = false
  504. self.cameraButton.isHidden = true
  505. self.cameraSwitchButton.isHidden = true
  506. self.debugLabel.isHidden = true
  507. self.timerLabel.text = BundleUtil.localizedString(forKey: "call_status_incom_ringing")
  508. }
  509. }
  510. private func setupForConnectedCallTest() {
  511. debugLabel.isHidden = true
  512. endButton.isHidden = false
  513. acceptButton.isHidden = true
  514. rejectButton.isHidden = true
  515. muteButton.isHidden = false
  516. speakerButton.isHidden = false
  517. timerLabel.isHidden = false
  518. cameraButton.isHidden = false
  519. cameraSwitchButton.isHidden = true
  520. self.endButton?.isEnabled = true
  521. self.muteButton?.isEnabled = true
  522. self.speakerButton?.isEnabled = true
  523. self.cameraButton?.isEnabled = true
  524. self.cameraSwitchButton.isEnabled = true
  525. DispatchQueue.main.async {
  526. self.timerLabel.text = "08:15"
  527. }
  528. speakerButton.setImage(UIImage.init(named: "SpeakerInactive"), for: .normal)
  529. speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .highlighted)
  530. cameraButton?.setImage(UIImage.init(named: "VideoInactive"), for: .normal)
  531. cameraButton?.setImage(UIImage.init(named: "VideoInactive"), for: .selected)
  532. cameraButton?.setImage(UIImage.init(named: "VideoInactive"), for: .highlighted)
  533. }
  534. private func setupForVideoCallTest() {
  535. debugLabel.isHidden = true
  536. endButton.isHidden = false
  537. acceptButton.isHidden = true
  538. rejectButton.isHidden = true
  539. muteButton.isHidden = false
  540. speakerButton.isHidden = false
  541. timerLabel.isHidden = false
  542. cameraButton.isHidden = false
  543. cameraButton.isSelected = true
  544. cameraSwitchButton.isHidden = false
  545. self.endButton?.isEnabled = true
  546. self.muteButton?.isEnabled = true
  547. self.speakerButton?.isEnabled = true
  548. self.cameraButton?.isEnabled = true
  549. self.cameraSwitchButton.isEnabled = true
  550. let cameraImageName = "VideoActive"
  551. self.cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .normal)
  552. self.cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .selected)
  553. self.cameraButton?.setImage(UIImage.init(named: cameraImageName), for: .highlighted)
  554. DispatchQueue.main.async {
  555. self.timerLabel.text = "08:15"
  556. }
  557. speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .normal)
  558. speakerButton.setImage(UIImage.init(named: "SpeakerActive"), for: .highlighted)
  559. self.localVideoView?.isHidden = false
  560. self.remoteVideoView?.isHidden = false
  561. var meImage = AvatarMaker.shared().unknownPersonImage()
  562. if let profilePicture = MyIdentityStore.shared()?.profilePicture {
  563. if let data = profilePicture["ProfilePicture"] {
  564. meImage = UIImage(data: data as! Data)
  565. }
  566. }
  567. let meImageView = UIImageView(image: meImage)
  568. meImageView.contentMode = .scaleAspectFill
  569. embedView(meImageView, into: localVideoView)
  570. let remoteImageView = UIImageView()
  571. remoteImageView.image = AvatarMaker.shared()?.avatar(for: contact, size: remoteVideoView.frame.size.width, masked: false)
  572. remoteImageView.contentMode = .scaleAspectFill
  573. embedView(remoteImageView, into: remoteVideoView)
  574. }
  575. private func startLocalVideo(useBackCamera: Bool = false, switchCamera: Bool = false) {
  576. DispatchQueue.main.async {
  577. self.backgroundImage.image = nil
  578. #if arch(arm64)
  579. // Using metal (arm64 only)
  580. let localRenderer = RTCMTLVideoView(frame: self.localVideoView?.frame ?? CGRect.zero)
  581. localRenderer.videoContentMode = .scaleAspectFill
  582. #else
  583. // Using OpenGLES for the rest
  584. let localRenderer = RTCEAGLVideoView(frame: self.localVideoView?.frame ?? CGRect.zero)
  585. #endif
  586. localRenderer.delegate = self
  587. VoIPCallStateManager.shared.startCaptureLocalVideo(renderer: localRenderer, useBackCamera: useBackCamera, switchCamera: switchCamera)
  588. if (!self.isReceivingRemoteVideo) {
  589. if let remoteVideoView = self.remoteVideoView {
  590. self.embedView(localRenderer, into: remoteVideoView)
  591. self.flipLocalRenderer()
  592. self.updateVideoViews()
  593. if switchCamera == false {
  594. self.activateSpeakerForVideo()
  595. }
  596. }
  597. } else {
  598. if let localVideoView = self.localVideoView {
  599. if self.isRemoteRendererInRemoteView() {
  600. self.embedView(localRenderer, into: localVideoView)
  601. } else {
  602. self.embedView(localRenderer, into: self.remoteVideoView)
  603. }
  604. self.flipLocalRenderer()
  605. self.updateVideoViews()
  606. if switchCamera == false {
  607. self.activateSpeakerForVideo()
  608. }
  609. }
  610. }
  611. }
  612. }
  613. private func endLocalVideo(switchCamera: Bool = false) {
  614. VoIPCallStateManager.shared.endCaptureLocalVideo(switchCamera: switchCamera)
  615. DispatchQueue.main.async {
  616. if (self.isReceivingRemoteVideo) {
  617. if let remoteRenderer = VoIPCallStateManager.shared.remoteVideoRenderer() {
  618. if self.localVideoView.subviews.first == remoteRenderer as? UIView {
  619. if !switchCamera {
  620. self.moveEmbedView(remoteRenderer as! UIView, from: self.localVideoView, into: self.remoteVideoView)
  621. } else {
  622. self.removeSubviewsFromRemoteView()
  623. }
  624. }
  625. else if self.remoteVideoView.subviews.first == remoteRenderer as? UIView {
  626. self.removeSubviewsFromLocalView()
  627. }
  628. }
  629. } else {
  630. self.backgroundImage.image = self.blurImage(image: AvatarMaker.shared().callBackground(for: self.contact), blurRadius: 4.0)
  631. self.removeAllSubviewsFromVideoViews()
  632. }
  633. self.flipLocalRenderer()
  634. self.updateVideoViews()
  635. }
  636. }
  637. private func startRemoteVideo() {
  638. DispatchQueue.main.async {
  639. self.backgroundImage.image = nil
  640. #if arch(arm64)
  641. // Using metal (arm64 only)
  642. let remoteRenderer = RTCMTLVideoView(frame: self.remoteVideoView?.frame ?? CGRect.zero)
  643. remoteRenderer.videoContentMode = .scaleAspectFill
  644. #else
  645. // Using OpenGLES for the rest
  646. let remoteRenderer = RTCEAGLVideoView(frame: self.remoteVideoView?.frame ?? CGRect.zero)
  647. #endif
  648. remoteRenderer.delegate = self
  649. VoIPCallStateManager.shared.renderRemoteVideo(to: remoteRenderer)
  650. if (!self.isLocalVideoActive || self.localVideoView.subviews.first == VoIPCallStateManager.shared.localVideoRenderer() as? UIView) {
  651. if let remoteVideoView = self.remoteVideoView {
  652. self.embedView(remoteRenderer, into: remoteVideoView)
  653. self.updateVideoViews()
  654. }
  655. } else {
  656. if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer() {
  657. self.moveEmbedView(localRenderer as! UIView, from: self.remoteVideoView, into: self.localVideoView)
  658. self.flipLocalRenderer()
  659. }
  660. if let remoteVideoView = self.remoteVideoView {
  661. self.embedView(remoteRenderer, into: remoteVideoView)
  662. self.updateVideoViews()
  663. }
  664. }
  665. if !self.isLocalVideoActive && self.isReceivingRemoteVideo && !self.viewWasHidden && self.isViewLoaded && self.view.window != nil {
  666. self.showRemoteVideoActivatedInfo()
  667. self.showSpeakerInfo()
  668. }
  669. }
  670. }
  671. private func endRemoteVideo() {
  672. VoIPCallStateManager.shared.endRemoteVideo()
  673. DispatchQueue.main.async {
  674. if (self.isLocalVideoActive) {
  675. if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer() {
  676. if self.localVideoView.subviews.first == localRenderer as? UIView {
  677. self.moveEmbedView(localRenderer as! UIView, from: self.localVideoView, into: self.remoteVideoView)
  678. }
  679. else if self.remoteVideoView.subviews.first == localRenderer as? UIView {
  680. self.removeSubviewsFromLocalView()
  681. }
  682. }
  683. } else {
  684. self.backgroundImage.image = self.blurImage(image: AvatarMaker.shared().callBackground(for: self.contact), blurRadius: 4.0)
  685. self.removeAllSubviewsFromVideoViews()
  686. }
  687. self.flipLocalRenderer()
  688. self.updateVideoViews()
  689. }
  690. }
  691. private func embedView(_ view: UIView, into containerView: UIView) {
  692. containerView.addSubview(view)
  693. view.translatesAutoresizingMaskIntoConstraints = false
  694. view.layer.masksToBounds = true
  695. containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
  696. containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
  697. containerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
  698. containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
  699. containerView.layoutIfNeeded()
  700. }
  701. private func moveEmbedView(_ view: UIView, from fromContainerView: UIView, into containerView: UIView) {
  702. self.removeAllSubviewsFromVideoViews()
  703. self.embedView(view, into: containerView)
  704. }
  705. private func switchCamera() {
  706. useBackCamera = !useBackCamera
  707. cameraButton.accessibilityLabel = BundleUtil.localizedString(forKey: useBackCamera ? "call_camera_switch_to_front_button" : "call_camera_switch_to_back_button")
  708. endLocalVideo(switchCamera: true)
  709. startLocalVideo(useBackCamera: useBackCamera, switchCamera: true)
  710. }
  711. private func isLocalRendererInLocalView() -> Bool {
  712. if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer() as? UIView {
  713. if self.localVideoView.subviews.first == localRenderer {
  714. return true
  715. }
  716. }
  717. return false
  718. }
  719. private func isLocalRendererInRemoteView() -> Bool {
  720. if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer() as? UIView {
  721. if self.remoteVideoView.subviews.first == localRenderer {
  722. return true
  723. }
  724. }
  725. return false
  726. }
  727. private func isRemoteRendererInRemoteView() -> Bool {
  728. if let remoteRenderer = VoIPCallStateManager.shared.remoteVideoRenderer() as? UIView {
  729. if self.remoteVideoView.subviews.first == remoteRenderer {
  730. return true
  731. }
  732. }
  733. return false
  734. }
  735. private func isRemoteRendererInLocalView() -> Bool {
  736. if let remoteRenderer = VoIPCallStateManager.shared.remoteVideoRenderer() as? UIView {
  737. if self.localVideoView.subviews.first == remoteRenderer {
  738. return true
  739. }
  740. }
  741. return false
  742. }
  743. private func removeSubviewsFromLocalView() {
  744. guard localVideoView != nil else {
  745. return
  746. }
  747. guard localVideoView.subviews.count > 0 else {
  748. return
  749. }
  750. self.localVideoView.subviews.forEach({ $0.removeFromSuperview() })
  751. }
  752. private func removeSubviewsFromRemoteView() {
  753. guard remoteVideoView != nil else {
  754. return
  755. }
  756. guard remoteVideoView.subviews.count > 0 else {
  757. return
  758. }
  759. self.remoteVideoView.subviews.forEach({ $0.removeFromSuperview() })
  760. }
  761. private func removeAllSubviewsFromVideoViews() {
  762. removeSubviewsFromLocalView()
  763. removeSubviewsFromRemoteView()
  764. // show navigation if needed
  765. if !isNavigationVisible() {
  766. moveLocalVideoViewToCorrectPosition(moveNavigation: true)
  767. }
  768. }
  769. private func flipLocalRenderer() {
  770. if useBackCamera == false {
  771. if isLocalRendererInLocalView() {
  772. if localVideoView != nil {
  773. localVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: -1, y: 1))
  774. }
  775. } else {
  776. if localVideoView != nil {
  777. localVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 1))
  778. }
  779. }
  780. if isLocalRendererInRemoteView() {
  781. if remoteVideoView != nil {
  782. remoteVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: -1, y: 1))
  783. }
  784. } else {
  785. if remoteVideoView != nil {
  786. remoteVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 1))
  787. }
  788. }
  789. } else {
  790. if localVideoView != nil {
  791. localVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 1))
  792. }
  793. if remoteVideoView != nil {
  794. remoteVideoView.layer.setAffineTransform(CGAffineTransform(scaleX: 1, y: 1))
  795. }
  796. }
  797. }
  798. private func hasCameraAccess() -> Bool {
  799. let access = AVCaptureDevice.authorizationStatus(for: .video)
  800. if access == .authorized {
  801. return true
  802. }
  803. else if access == .denied || access == .restricted {
  804. self.showCameraAccessAlert()
  805. }
  806. else if access == .notDetermined {
  807. AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
  808. if granted {
  809. self.isLocalVideoActive = !self.isLocalVideoActive
  810. if self.isLocalVideoActive == true {
  811. self.startLocalVideo()
  812. } else {
  813. self.endLocalVideo()
  814. }
  815. } else {
  816. self.showCameraAccessAlert()
  817. }
  818. })
  819. }
  820. return false
  821. }
  822. private func showCameraAccessAlert() {
  823. UIAlertTemplate.showAlert(owner: self, title: BundleUtil.localizedString(forKey: "camera_disabled_title"), message: BundleUtil.localizedString(forKey: "camera_disabled_message"))
  824. }
  825. private func activateSpeakerForVideo() {
  826. let currentRoute = AVAudioSession.sharedInstance().currentRoute
  827. for output in currentRoute.outputs {
  828. if output.portType == AVAudioSession.Port.builtInReceiver {
  829. let action = VoIPCallUserAction.init(action: .speakerOn, contact: self.contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  830. VoIPCallStateManager.shared.processUserAction(action)
  831. }
  832. }
  833. }
  834. private func isNavigationVisible() -> Bool {
  835. guard let navigationConstraint = phoneButtonsStackViewBottomConstraint else {
  836. return true
  837. }
  838. return navigationConstraint.constant == 0.0
  839. }
  840. private func removeAllLocalVideoViewConstraints() {
  841. localVideoViewConstraintLeft.isActive = false
  842. localVideoViewConstraintRight.isActive = false
  843. localVideoViewConstraintBottom.isActive = false
  844. localVideoViewConstraintBottomNavigation.isActive = false
  845. localVideoViewConstraintTop.isActive = false
  846. localVideoViewConstraintTopNavigation.isActive = false
  847. localVideoViewConstraintTopNavigationLabel.isActive = false
  848. self.view.layoutIfNeeded()
  849. }
  850. private func moveLocalVideoViewToCorrectPosition(moveNavigation: Bool = false) {
  851. let localVideoViewCenterX = localVideoView.center.x
  852. let localVideoViewCenterY = localVideoView.center.y
  853. let screenMiddleX = self.view.frame.size.width / 2
  854. let screenMiddleY = self.view.frame.size.height / 2
  855. self.removeAllLocalVideoViewConstraints()
  856. guard let _ = phoneButtonsStackViewBottomConstraint,
  857. let _ = callInfoStackViewTopConstraint,
  858. let _ = phoneButtonsStackView,
  859. let _ = callInfoStackView,
  860. let phoneButtonsStackViewSuperView = phoneButtonsStackView.superview,
  861. let callInfoStackViewSuperView = callInfoStackView.superview else {
  862. return
  863. }
  864. if moveNavigation {
  865. if isNavigationVisible() {
  866. self.phoneButtonsStackViewBottomConstraint.constant = self.phoneButtonsStackView.frame.size.height + phoneButtonsStackViewSuperView.layoutMargins.bottom + 50.0
  867. self.callInfoStackViewTopConstraint.constant = -(self.callInfoStackView.frame.size.height + callInfoStackViewSuperView.layoutMargins.top + 50.0)
  868. } else {
  869. self.phoneButtonsStackViewBottomConstraint.constant = 0
  870. self.callInfoStackViewTopConstraint.constant = 16.0
  871. }
  872. }
  873. if localVideoViewCenterX < screenMiddleX && localVideoViewCenterY < screenMiddleY {
  874. self.addConstraintToTopLeftForLocalVideoView()
  875. }
  876. else if localVideoViewCenterX > screenMiddleX && localVideoViewCenterY < screenMiddleY {
  877. self.addConstraintToTopRightForLocalVideoView()
  878. }
  879. else if localVideoViewCenterX < screenMiddleX && localVideoViewCenterY > screenMiddleY {
  880. self.addConstraintToBottomLeftForLocalVideoView()
  881. }
  882. else if localVideoViewCenterX > screenMiddleX && localVideoViewCenterY > screenMiddleY {
  883. self.addConstraintToBottomRightForLocalVideoView()
  884. }
  885. UIView.animate(withDuration: 0.35, animations: {
  886. self.phoneButtonsGradientView.setNeedsLayout()
  887. self.phoneButtonsGradientView.layoutIfNeeded()
  888. self.callInfoGradientView.setNeedsLayout()
  889. self.callInfoGradientView.layoutIfNeeded()
  890. self.view.layoutIfNeeded()
  891. })
  892. }
  893. private func addConstraintToTopLeftForLocalVideoView() {
  894. localVideoViewConstraintLeft.isActive = true
  895. if isNavigationVisible() {
  896. localVideoViewConstraintTopNavigationLabel.isActive = true
  897. } else {
  898. localVideoViewConstraintTop.isActive = true
  899. }
  900. }
  901. private func addConstraintToTopRightForLocalVideoView() {
  902. localVideoViewConstraintRight.isActive = true
  903. if isNavigationVisible() {
  904. localVideoViewConstraintTopNavigation.isActive = true
  905. } else {
  906. localVideoViewConstraintTop.isActive = true
  907. }
  908. }
  909. private func addConstraintToBottomLeftForLocalVideoView() {
  910. localVideoViewConstraintLeft.isActive = true
  911. if isNavigationVisible() {
  912. localVideoViewConstraintBottomNavigation.isActive = true
  913. } else {
  914. localVideoViewConstraintBottom.isActive = true
  915. }
  916. }
  917. private func addConstraintToBottomRightForLocalVideoView() {
  918. localVideoViewConstraintRight.isActive = true
  919. if isNavigationVisible() {
  920. localVideoViewConstraintBottomNavigation.isActive = true
  921. } else {
  922. localVideoViewConstraintBottom.isActive = true
  923. }
  924. }
  925. private func updateConstraintsAfterRotation(size: CGSize?) {
  926. if isLocalRendererInLocalView() || isRemoteRendererInLocalView() {
  927. var maxSize: CGFloat = 100
  928. var newSize = size ?? CGSize(width: maxSize, height: 134.0)
  929. if UIDevice.current.userInterfaceIdiom == .pad {
  930. maxSize = 200
  931. newSize = size ?? CGSize(width: maxSize, height: 268.0)
  932. }
  933. if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
  934. if UIDevice.current.userInterfaceIdiom == .pad {
  935. maxSize = 270.0
  936. } else {
  937. maxSize = 150.0
  938. }
  939. }
  940. let ratio = newSize.width / maxSize
  941. localVideoViewConstraintHeight.constant = newSize.height / ratio
  942. localVideoViewConstraintWidth.constant = newSize.width / ratio
  943. self.view.layoutIfNeeded()
  944. }
  945. }
  946. private func checkAndHandleAvailableBluetoothDevices() {
  947. var bluetoothAvailable = false
  948. if let inputs = AVAudioSession.sharedInstance().availableInputs {
  949. for input in inputs {
  950. if input.portType == AVAudioSession.Port.bluetoothA2DP || input.portType == AVAudioSession.Port.bluetoothHFP || input.portType == AVAudioSession.Port.bluetoothLE {
  951. bluetoothAvailable = true
  952. }
  953. }
  954. }
  955. if bluetoothAvailable {
  956. if myVolumeView == nil {
  957. if #available(iOS 11.0, *) {
  958. myVolumeView = AVRoutePickerView.init(frame: CGRect.init(x: 0.0, y: 0.0, width: speakerButton.frame.size.width, height: speakerButton.frame.size.height))
  959. (myVolumeView as! AVRoutePickerView).activeTintColor = UIColor.clear
  960. (myVolumeView as! AVRoutePickerView).tintColor = UIColor.clear
  961. (myVolumeView as! AVRoutePickerView).isOpaque = true
  962. (myVolumeView as! AVRoutePickerView).alpha = 1.0
  963. (myVolumeView as! AVRoutePickerView).delegate = self
  964. if #available(iOS 13.0, *) {
  965. if isLocalVideoActive || isReceivingRemoteVideo {
  966. (myVolumeView as! AVRoutePickerView).prioritizesVideoDevices = true
  967. }
  968. }
  969. speakerButton.addSubview(myVolumeView!)
  970. } else {
  971. myVolumeView = MPVolumeView(frame: CGRect.init(x: 0.0, y: 0.0, width: speakerButton.frame.size.width, height: speakerButton.frame.size.height))
  972. (myVolumeView as! MPVolumeView).showsVolumeSlider = false
  973. (myVolumeView as! MPVolumeView).showsRouteButton = true
  974. (myVolumeView as! MPVolumeView).setRouteButtonImage(UIImage.init(named: "SpeakerInactive")!.resizedImage(newSize: CGSize(width: 45.0, height: 45.0)), for: .normal)
  975. (myVolumeView as! MPVolumeView).setRouteButtonImage(UIImage.init(named: "SpeakerActive")!.resizedImage(newSize: CGSize(width: 45.0, height: 45.0)), for: .selected)
  976. speakerButton.setImage(nil, for: .normal)
  977. speakerButton.addSubview(myVolumeView!)
  978. }
  979. }
  980. } else {
  981. speakerButton.subviews.forEach({
  982. if $0 == myVolumeView {
  983. $0.removeFromSuperview()
  984. }
  985. })
  986. myVolumeView = nil
  987. }
  988. }
  989. private func showInitiatorVideoCallInfo() {
  990. if let contact = contact, contact.isVideoCallAvailable() && isNavigationVisible() && isCallInitiator && UserSettings.shared().enableVideoCall && !UserSettings.shared().videoCallInfoShown && threemaVideoCallAvailable {
  991. self.presentInitiatorVideoCallMaterialShowcase()
  992. }
  993. }
  994. private func presentInitiatorVideoCallMaterialShowcase(completion handler: (()-> Void)? = nil) {
  995. if viewIfLoaded?.window != nil {
  996. if initiatorVideoCallShowcase == nil {
  997. initiatorVideoCallShowcase = MaterialShowcase()
  998. initiatorVideoCallShowcase!.setTargetView(button: cameraButton)
  999. initiatorVideoCallShowcase!.primaryText = BundleUtil.localizedString(forKey: "call_threema_video_in_chat_info_title")
  1000. initiatorVideoCallShowcase!.secondaryText = BundleUtil.localizedString(forKey: "call_threema_initiator_video_info")
  1001. initiatorVideoCallShowcase!.backgroundPromptColor = LicenseStore.requiresLicenseKey() ? Colors.workBlue() : Colors.green()
  1002. initiatorVideoCallShowcase!.backgroundPromptColorAlpha = 0.7
  1003. initiatorVideoCallShowcase!.backgroundRadius = -1
  1004. initiatorVideoCallShowcase!.targetHolderColor = Colors.black()?.withAlphaComponent(0.2)
  1005. initiatorVideoCallShowcase!.primaryTextSize = 24.0
  1006. initiatorVideoCallShowcase!.secondaryTextSize = 18.0
  1007. initiatorVideoCallShowcase!.primaryTextColor = Colors.white()
  1008. initiatorVideoCallShowcase!.primaryTextAlignment = .right
  1009. initiatorVideoCallShowcase!.secondaryTextAlignment = .right
  1010. initiatorVideoCallShowcase!.onTapThrough = {
  1011. self.startVideoAction(self.cameraButton)
  1012. }
  1013. initiatorVideoCallShowcase!.delegate = self
  1014. }
  1015. initiatorVideoCallShowcase!.show(hasSkipButton: false, completion: handler)
  1016. }
  1017. }
  1018. private func hideInitiatorVideoCallInfo() {
  1019. if let showcase = initiatorVideoCallShowcase {
  1020. showcase.completeShowcase()
  1021. }
  1022. }
  1023. private func showRemoteVideoActivatedInfo() {
  1024. DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
  1025. if !self.isLocalVideoActive {
  1026. if let contact = self.contact, self.isNavigationVisible() && contact.isVideoCallAvailable() && UserSettings.shared().enableVideoCall && self.threemaVideoCallAvailable {
  1027. self.presentRemoteVideoActivatedMaterialShowcase {
  1028. DispatchQueue.main.asyncAfter(deadline: .now() + 6, execute: {
  1029. self.hideRemoteVideoActivatedInfo()
  1030. })
  1031. }
  1032. }
  1033. }
  1034. })
  1035. }
  1036. private func presentRemoteVideoActivatedMaterialShowcase(completion handler: (()-> Void)?) {
  1037. if viewIfLoaded?.window != nil {
  1038. if remoteVideoActivatedShowcase == nil {
  1039. remoteVideoActivatedShowcase = MaterialShowcase()
  1040. remoteVideoActivatedShowcase!.setTargetView(button: cameraButton)
  1041. remoteVideoActivatedShowcase!.primaryText = BundleUtil.localizedString(forKey: "call_threema_remote_video_activated_info_title")
  1042. remoteVideoActivatedShowcase!.secondaryText = BundleUtil.localizedString(forKey: "call_threema_remote_video_activated_info")
  1043. remoteVideoActivatedShowcase!.backgroundPromptColor = Colors.darkGrey()
  1044. remoteVideoActivatedShowcase!.backgroundPromptColorAlpha = 0.7
  1045. remoteVideoActivatedShowcase!.backgroundRadius = -1
  1046. remoteVideoActivatedShowcase!.targetHolderColor = Colors.black()?.withAlphaComponent(0.2)
  1047. remoteVideoActivatedShowcase!.primaryTextSize = 24.0
  1048. remoteVideoActivatedShowcase!.secondaryTextSize = 18.0
  1049. remoteVideoActivatedShowcase!.primaryTextColor = Colors.white()
  1050. remoteVideoActivatedShowcase!.primaryTextAlignment = .right
  1051. remoteVideoActivatedShowcase!.secondaryTextAlignment = .right
  1052. remoteVideoActivatedShowcase!.onTapThrough = {
  1053. self.startVideoAction(self.cameraButton)
  1054. }
  1055. }
  1056. remoteVideoActivatedShowcase!.show(hasSkipButton: false, completion: handler)
  1057. }
  1058. }
  1059. private func hideRemoteVideoActivatedInfo() {
  1060. if let showcase = remoteVideoActivatedShowcase {
  1061. showcase.completeShowcase()
  1062. }
  1063. }
  1064. private func showSpeakerInfo() {
  1065. DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {
  1066. var showSpeakerInfo = true
  1067. let currentRoute = AVAudioSession.sharedInstance().currentRoute
  1068. for output in currentRoute.outputs {
  1069. if output.portType == AVAudioSession.Port.builtInSpeaker {
  1070. showSpeakerInfo = false
  1071. }
  1072. else if output.portType == AVAudioSession.Port.headphones {
  1073. showSpeakerInfo = false
  1074. }
  1075. else if output.portType == AVAudioSession.Port.bluetoothA2DP || output.portType == AVAudioSession.Port.bluetoothHFP || output.portType == AVAudioSession.Port.bluetoothLE {
  1076. showSpeakerInfo = false
  1077. }
  1078. }
  1079. if !self.isLocalVideoActive && showSpeakerInfo {
  1080. if let contact = self.contact, self.isNavigationVisible() && contact.isVideoCallAvailable() && UserSettings.shared().enableVideoCall && !UserSettings.shared().videoCallSpeakerInfoShown && self.threemaVideoCallAvailable && self.isViewLoaded && self.view.window != nil {
  1081. self.presentSpaekerMaterialShowcase {
  1082. DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
  1083. self.hideSpeakerInfo()
  1084. })
  1085. }
  1086. }
  1087. }
  1088. })
  1089. }
  1090. private func presentSpaekerMaterialShowcase(completion handler: (()-> Void)?) {
  1091. if viewIfLoaded?.window != nil {
  1092. if speakerShowcase == nil {
  1093. speakerShowcase = MaterialShowcase()
  1094. speakerShowcase!.setTargetView(button: speakerButton)
  1095. speakerShowcase!.primaryText = ""
  1096. speakerShowcase!.secondaryText = BundleUtil.localizedString(forKey: "call_threema_speaker_info")
  1097. speakerShowcase!.backgroundPromptColor = Colors.darkGrey()
  1098. speakerShowcase!.backgroundPromptColorAlpha = 0.7
  1099. speakerShowcase!.backgroundRadius = -1
  1100. speakerShowcase!.targetHolderColor = Colors.black()?.withAlphaComponent(0.2)
  1101. speakerShowcase!.secondaryTextSize = 18.0
  1102. speakerShowcase!.primaryTextColor = Colors.white()
  1103. speakerShowcase!.primaryTextAlignment = .left
  1104. speakerShowcase!.secondaryTextAlignment = .left
  1105. speakerShowcase!.onTapThrough = {
  1106. UserSettings.shared().videoCallSpeakerInfoShown = true
  1107. self.speakerShowcase!.completeShowcase()
  1108. self.speakerAction(self.speakerButton, forEvent: UIEvent())
  1109. }
  1110. speakerShowcase!.skipButton = {
  1111. UserSettings.shared().videoCallSpeakerInfoShown = true
  1112. self.speakerShowcase!.completeShowcase()
  1113. }
  1114. }
  1115. speakerShowcase!.layoutMargins = UIEdgeInsets(top: (speakerShowcase!.containerView.frame.size.height / 3) * 2, left: 0.0, bottom: 0.0, right: 0.0)
  1116. speakerShowcase!.show(hasSkipButton: true, completion: handler)
  1117. }
  1118. }
  1119. private func hideSpeakerInfo() {
  1120. if let showcase = speakerShowcase {
  1121. showcase.completeShowcase()
  1122. }
  1123. }
  1124. private func showCameraDisabledInfo() {
  1125. if isNavigationVisible() {
  1126. presentCameraDisabledMaterialShowcase {
  1127. DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
  1128. self.hideCameraDisalbedInfo()
  1129. })
  1130. }
  1131. }
  1132. }
  1133. private func presentCameraDisabledMaterialShowcase(completion handler: (()-> Void)?) {
  1134. if viewIfLoaded?.window != nil {
  1135. if cameraDisabledShowcase == nil {
  1136. cameraDisabledShowcase = MaterialShowcase()
  1137. cameraDisabledShowcase!.setTargetView(button: cameraButton)
  1138. cameraDisabledShowcase!.primaryText = ""
  1139. cameraDisabledShowcase!.secondaryText = BundleUtil.localizedString(forKey: "call_threema_camera_disabled_info")
  1140. cameraDisabledShowcase!.backgroundPromptColor = Colors.darkGrey()
  1141. cameraDisabledShowcase!.backgroundPromptColorAlpha = 0.7
  1142. cameraDisabledShowcase!.backgroundRadius = -1
  1143. cameraDisabledShowcase!.targetHolderColor = Colors.black()?.withAlphaComponent(0.2)
  1144. cameraDisabledShowcase!.secondaryTextSize = 18.0
  1145. cameraDisabledShowcase!.primaryTextColor = Colors.white()
  1146. cameraDisabledShowcase!.primaryTextAlignment = .right
  1147. cameraDisabledShowcase!.secondaryTextAlignment = .right
  1148. }
  1149. cameraDisabledShowcase!.show(hasSkipButton: false, completion: handler)
  1150. }
  1151. }
  1152. private func hideCameraDisalbedInfo() {
  1153. if let showcase = cameraDisabledShowcase {
  1154. showcase.completeShowcase()
  1155. }
  1156. }
  1157. private func blurImage(image: UIImage, blurRadius: CGFloat) -> UIImage {
  1158. let context = CIContext(options: nil)
  1159. let inputImage = CIImage(cgImage: image.cgImage!)
  1160. let blurFilter = CIFilter(name: "CIGaussianBlur")
  1161. blurFilter?.setValue(inputImage, forKey: kCIInputImageKey)
  1162. blurFilter?.setValue(blurRadius, forKey: "inputRadius")
  1163. var bounds = inputImage.extent
  1164. if AvatarMaker.shared().isDefaultAvatar(for: contact) {
  1165. bounds = CGRect(x: -10.0, y: -10.0, width: image.size.width + 20.0, height: image.size.height + 20.0)
  1166. } else {
  1167. 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)
  1168. }
  1169. let outputImage = blurFilter?.value(forKey: kCIOutputImageKey) as? CIImage
  1170. let cgImage = context.createCGImage(outputImage ?? CIImage(), from: bounds)
  1171. return UIImage(cgImage: cgImage!)
  1172. }
  1173. private func addGradientToView(view: UIView, startColor: UIColor, middleColor: UIColor, endColor: UIColor, locations: [NSNumber]) {
  1174. let gradientLayerMask = CAGradientLayer()
  1175. gradientLayerMask.colors = [startColor.cgColor, middleColor.cgColor, endColor.cgColor]
  1176. gradientLayerMask.locations = locations
  1177. gradientLayerMask.frame = view.bounds
  1178. view.layer.insertSublayer(gradientLayerMask, at: 0)
  1179. view.backgroundColor = .clear
  1180. }
  1181. private func updateGradientBackground() {
  1182. self.callInfoGradientView.setNeedsLayout()
  1183. self.callInfoGradientView.layoutIfNeeded()
  1184. self.phoneButtonsGradientView.setNeedsLayout()
  1185. self.phoneButtonsGradientView.layoutIfNeeded()
  1186. self.callInfoGradientView.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
  1187. self.phoneButtonsGradientView.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
  1188. 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])
  1189. 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])
  1190. }
  1191. private func updateRemoteVideoContentMode(videoView: RTCVideoRenderer, size: CGSize) {
  1192. #if arch(arm64)
  1193. if let rR = VoIPCallStateManager.shared.remoteVideoRenderer(),
  1194. let remoteRenderer = rR as? RTCMTLVideoView,
  1195. remoteRenderer.isEqual(videoView) {
  1196. if UIApplication.shared.statusBarOrientation.isPortrait,
  1197. size.height > size.width {
  1198. remoteRenderer.videoContentMode = .scaleAspectFill
  1199. } else {
  1200. if UIApplication.shared.statusBarOrientation.isLandscape,
  1201. size.height < size.width {
  1202. remoteRenderer.videoContentMode = .scaleAspectFill
  1203. } else {
  1204. remoteRenderer.videoContentMode = .scaleAspectFit
  1205. }
  1206. }
  1207. }
  1208. #endif
  1209. }
  1210. }
  1211. extension CallViewController {
  1212. // MARK: Actions
  1213. @objc private func handleLongPress(gestureRecognizer: UIGestureRecognizer) {
  1214. if gestureRecognizer.state == .began {
  1215. // Toggle debug label
  1216. debugLabel.isHidden = !debugLabel.isHidden
  1217. if debugLabel.isHidden == true {
  1218. debugLabel.text = ""
  1219. }
  1220. }
  1221. updateViewConstraints()
  1222. }
  1223. @IBAction func hideView(sender: UIButton) {
  1224. if isTesting == true {
  1225. setupForConnectedCallTest()
  1226. } else {
  1227. VoIPHelper.shared()?.isCallActiveInBackground = true
  1228. VoIPHelper.shared()?.contactName = contact?.displayName
  1229. UIDevice.current.isProximityMonitoringEnabled = false
  1230. dismiss(animated: true) {
  1231. if AppDelegate.shared()?.isAppLocked == true {
  1232. AppDelegate.shared()?.presentPasscodeView()
  1233. }
  1234. }
  1235. }
  1236. }
  1237. @IBAction func acceptAction(_ sender: UIButton, forEvent event: UIEvent) {
  1238. let action = VoIPCallUserAction.init(action: .accept, contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  1239. VoIPCallStateManager.shared.processUserAction(action)
  1240. }
  1241. @IBAction func rejectAction(_ sender: UIButton, forEvent event: UIEvent) {
  1242. if isTesting == true {
  1243. dismiss(animated: true, completion: nil)
  1244. } else {
  1245. let action = VoIPCallUserAction.init(action: .reject, contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  1246. VoIPCallStateManager.shared.processUserAction(action)
  1247. }
  1248. }
  1249. @IBAction func endAction(_ sender: UIButton, forEvent event: UIEvent) {
  1250. DDLogNotice("Threema call: HangupBug -> User pressed the end call button")
  1251. let action = VoIPCallUserAction.init(action: .end, contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  1252. VoIPCallStateManager.shared.processUserAction(action)
  1253. }
  1254. @IBAction func muteAction(_ sender: UIButton, forEvent event: UIEvent) {
  1255. let action = VoIPCallUserAction.init(action: VoIPCallStateManager.shared.isCallMuted() ? .unmuteAudio : .muteAudio , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  1256. muteButton.isSelected = action.action == .muteAudio
  1257. VoIPCallStateManager.shared.processUserAction(action)
  1258. }
  1259. @IBAction func speakerAction(_ sender: UIButton, forEvent event: UIEvent) {
  1260. self.checkAndHandleAvailableBluetoothDevices()
  1261. let audioSession = AVAudioSession.sharedInstance()
  1262. for output in audioSession.currentRoute.outputs {
  1263. switch output.portType {
  1264. case .builtInReceiver:
  1265. let action = VoIPCallUserAction.init(action: .speakerOn , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  1266. VoIPCallStateManager.shared.processUserAction(action)
  1267. break
  1268. case .builtInSpeaker:
  1269. let action = VoIPCallUserAction.init(action: .speakerOff , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  1270. VoIPCallStateManager.shared.processUserAction(action)
  1271. break
  1272. case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
  1273. let action = VoIPCallUserAction.init(action: .speakerOn , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  1274. VoIPCallStateManager.shared.processUserAction(action)
  1275. break
  1276. case .headphones:
  1277. let action = VoIPCallUserAction.init(action: .speakerOn , contact: contact!, callId: VoIPCallStateManager.shared.currentCallId(), completion: nil)
  1278. VoIPCallStateManager.shared.processUserAction(action)
  1279. break
  1280. default:
  1281. break
  1282. }
  1283. }
  1284. }
  1285. @IBAction func startVideoAction(_ sender: UIButton) {
  1286. if isTesting == true {
  1287. setupForVideoCallTest()
  1288. } else {
  1289. if threemaVideoCallAvailable {
  1290. if hasCameraAccess() {
  1291. self.isLocalVideoActive = !self.isLocalVideoActive
  1292. if self.isLocalVideoActive == true {
  1293. startLocalVideo()
  1294. } else {
  1295. endLocalVideo()
  1296. }
  1297. }
  1298. } else {
  1299. self.showCameraDisabledInfo()
  1300. }
  1301. }
  1302. }
  1303. @IBAction func switchCameraAction(_ sender: UIButton, forEvent event: UIEvent) {
  1304. switchCamera()
  1305. }
  1306. @objc func switchVideoViews(gesture: UITapGestureRecognizer) {
  1307. DispatchQueue.main.async {
  1308. if self.isLocalRendererInLocalView() && self.isRemoteRendererInRemoteView() {
  1309. self.removeAllSubviewsFromVideoViews()
  1310. self.embedView(VoIPCallStateManager.shared.remoteVideoRenderer()! as! UIView, into: self.localVideoView)
  1311. self.embedView(VoIPCallStateManager.shared.localVideoRenderer() as! UIView, into: self.remoteVideoView)
  1312. self.flipLocalRenderer()
  1313. }
  1314. else if self.isLocalRendererInRemoteView() && self.isRemoteRendererInLocalView() {
  1315. self.removeAllSubviewsFromVideoViews()
  1316. self.embedView(VoIPCallStateManager.shared.localVideoRenderer()! as! UIView, into: self.localVideoView)
  1317. self.embedView(VoIPCallStateManager.shared.remoteVideoRenderer() as! UIView, into: self.remoteVideoView)
  1318. self.flipLocalRenderer()
  1319. }
  1320. }
  1321. }
  1322. @objc func showHideNavigation(gesture: UITapGestureRecognizer) {
  1323. moveLocalVideoViewToCorrectPosition(moveNavigation: true)
  1324. }
  1325. @objc func didPan(gesture: UIPanGestureRecognizer) {
  1326. guard let dragView = gesture.view else {
  1327. return
  1328. }
  1329. if (gesture.state == .began) {
  1330. dragView.center = gesture.location(in: self.view)
  1331. }
  1332. let newCenter: CGPoint = gesture.location(in: self.view)
  1333. let dX = newCenter.x - dragView.center.x
  1334. let dY = newCenter.y - dragView.center.y
  1335. dragView.center = CGPoint(x: dragView.center.x + dX, y: dragView.center.y + dY)
  1336. localVideoView.center = dragView.center
  1337. if gesture.state == .ended {
  1338. moveLocalVideoViewToCorrectPosition()
  1339. }
  1340. }
  1341. }
  1342. @available(iOS 11.0, *)
  1343. extension CallViewController: AVRoutePickerViewDelegate {
  1344. func routePickerViewDidEndPresentingRoutes(_ routePickerView: AVRoutePickerView) {
  1345. self.checkAndHandleAvailableBluetoothDevices()
  1346. }
  1347. }
  1348. extension CallViewController: RTCVideoViewDelegate {
  1349. func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
  1350. updateRemoteVideoContentMode(videoView: videoView, size: size)
  1351. if let localRenderer = VoIPCallStateManager.shared.localVideoRenderer(),
  1352. localRenderer.isEqual(videoView) {
  1353. updateConstraintsAfterRotation(size: size)
  1354. }
  1355. }
  1356. }
  1357. extension CallViewController: MaterialShowcaseDelegate {
  1358. func showCaseDidDismiss(showcase: MaterialShowcase, didTapTarget: Bool) {
  1359. if showcase == initiatorVideoCallShowcase {
  1360. UserSettings.shared().videoCallInfoShown = true
  1361. }
  1362. }
  1363. }