VoIPCallService.swift 100 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158
  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 CocoaLumberjackSwift
  22. protocol VoIPCallServiceDelegate: class {
  23. func callServiceFinishedProcess()
  24. }
  25. class VoIPCallService: NSObject {
  26. private let kIncomingCallTimeout = 60.0
  27. private let kCallFailedTimeout = 15.0
  28. @objc public enum CallState: Int, RawRepresentable, Equatable {
  29. case idle
  30. case sendOffer
  31. case receivedOffer
  32. case outgoingRinging
  33. case incomingRinging
  34. case sendAnswer
  35. case receivedAnswer
  36. case initalizing
  37. case calling
  38. case reconnecting
  39. case ended
  40. case remoteEnded
  41. case rejected
  42. case rejectedBusy
  43. case rejectedTimeout
  44. case rejectedDisabled
  45. case rejectedOffHours
  46. case rejectedUnknown
  47. case microphoneDisabled
  48. }
  49. weak var delegate: VoIPCallServiceDelegate?
  50. private var peerConnectionClient: VoIPCallPeerConnectionClient?
  51. private var callKitManager: VoIPCallKitManager?
  52. private var threemaVideoCallAvailable: Bool = false
  53. private var callViewController: CallViewController?
  54. private var state: CallState = .idle {
  55. didSet {
  56. self.invalidateTimers(state: state)
  57. callViewController?.voIPCallStatusChanged(state: state, oldState: oldValue)
  58. self.handleLocalNotification()
  59. switch state {
  60. case .idle:
  61. localAddedIceCandidates.removeAll()
  62. localRelatedAddresses.removeAll()
  63. receivedIcecandidatesMessages.removeAll()
  64. case .initalizing:
  65. handleLocalIceCandidates([])
  66. default:
  67. // do nothing
  68. break
  69. }
  70. self.addCallMessageToConversation(oldCallState: oldValue)
  71. handleTones(state: state, oldState: oldValue)
  72. }
  73. }
  74. private var audioPlayer: AVAudioPlayer?
  75. private var contact: Contact?
  76. private var callId: VoIPCallId?
  77. private var alreadyAccepted: Bool = false {
  78. didSet {
  79. callViewController?.alreadyAccepted = alreadyAccepted
  80. }
  81. }
  82. private var callInitiator: Bool = false {
  83. didSet {
  84. callViewController?.isCallInitiator = callInitiator
  85. }
  86. }
  87. private var audioMuted: Bool = false
  88. private var speakerActive: Bool = false
  89. private var videoActive: Bool = false
  90. private var isReceivingVideo: Bool = false {
  91. didSet {
  92. if callViewController != nil {
  93. callViewController?.isReceivingRemoteVideo = self.isReceivingVideo
  94. }
  95. }
  96. }
  97. private var initCallTimeoutTimer: Timer?
  98. private var incomingCallTimeoutTimer: Timer?
  99. private var callDurationTimer: Timer?
  100. private var callDurationTime: Int = 0
  101. private var callFailedTimer: Timer?
  102. private var incomingOffer: VoIPCallOfferMessage?
  103. private var iceCandidatesLockQueue = DispatchQueue(label: "VoIPCallIceCandidatesLockQueue")
  104. private var iceCandidatesTimer: Timer?
  105. private var localAddedIceCandidates = [RTCIceCandidate]()
  106. private var localRelatedAddresses: Set<String> = []
  107. private var receivedIceCandidatesLockQueue = DispatchQueue(label: "VoIPCallReceivedIceCandidatesLockQueue")
  108. private var receivedIcecandidatesMessages = [VoIPCallIceCandidatesMessage]()
  109. private var receivedUnknowCallIcecandidatesMessages = [String: [VoIPCallIceCandidatesMessage]]()
  110. private var localRenderer: RTCVideoRenderer?
  111. private var remoteRenderer: RTCVideoRenderer?
  112. private var reconnectingTimer: Timer?
  113. private var iceWasConnected: Bool = false
  114. private var isModal : Bool {
  115. // Check whether our callViewController is currently in the state presented modally
  116. let a = self.callViewController?.presentingViewController?.presentedViewController == self.callViewController
  117. // Check whether our callViewController has a navigationController
  118. let b1 = self.callViewController?.navigationController != nil
  119. // Check whether our callViewController is in the state presented modally as part of a navigation controller
  120. let b2 = self.callViewController?.navigationController?.presentingViewController?.presentedViewController == self.callViewController?.navigationController
  121. let b = b1 && b2
  122. // Check whether our callViewController has a tabbarcontroller which has a tabbarcontroller. Nesting two
  123. // tabBarControllers is only possible in the state presented modally
  124. let c = self.callViewController?.tabBarController?.presentingViewController is UITabBarController
  125. return a || b || c
  126. }
  127. required override init() {
  128. super.init()
  129. NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: nil) { (n) in
  130. if self.state != .idle {
  131. var isBluetoothAvailable = false
  132. if let inputs = AVAudioSession.sharedInstance().availableInputs {
  133. for input in inputs {
  134. if input.portType == AVAudioSession.Port.bluetoothA2DP || input.portType == AVAudioSession.Port.bluetoothHFP || input.portType == AVAudioSession.Port.bluetoothLE {
  135. isBluetoothAvailable = true
  136. }
  137. }
  138. }
  139. guard let info = n.userInfo,
  140. let value = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
  141. let reason = AVAudioSession.RouteChangeReason(rawValue: value) else { return }
  142. switch reason {
  143. case .categoryChange:
  144. let currentRoute = AVAudioSession.sharedInstance().currentRoute
  145. for output in currentRoute.outputs {
  146. switch output.portType {
  147. case .builtInReceiver:
  148. if isBluetoothAvailable {
  149. self.speakerActive = false
  150. try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
  151. }
  152. if self.speakerActive {
  153. try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
  154. }
  155. break
  156. case .builtInSpeaker:
  157. if isBluetoothAvailable {
  158. self.speakerActive = true
  159. try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
  160. }
  161. if !self.speakerActive {
  162. try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
  163. }
  164. break
  165. case .headphones:
  166. try? AVAudioSession.sharedInstance().overrideOutputAudioPort(self.speakerActive ? .speaker : .none)
  167. break
  168. case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
  169. break
  170. default:break
  171. }
  172. }
  173. break
  174. default: break
  175. }
  176. }
  177. }
  178. }
  179. }
  180. extension VoIPCallService {
  181. // MARK: class functions
  182. }
  183. extension VoIPCallService {
  184. // MARK: public functions
  185. /**
  186. Return the string of the current state for the ValidationLogger
  187. - Returns: String of the current state
  188. */
  189. func callStateString() -> String {
  190. switch state {
  191. case .idle: return "idle"
  192. case .sendOffer: return "sendOffer"
  193. case .receivedOffer: return "receivedOffer"
  194. case .outgoingRinging: return "outgoingRinging"
  195. case .incomingRinging: return "incomingRinging"
  196. case .sendAnswer: return "sendAnswer"
  197. case .receivedAnswer: return "receivedAnswer"
  198. case .initalizing: return "initalizing"
  199. case .calling: return "calling"
  200. case .reconnecting: return "reconnecting"
  201. case .ended: return "ended"
  202. case .remoteEnded: return "remoteEnded"
  203. case .rejected: return "rejected"
  204. case .rejectedBusy: return "rejectedBusy"
  205. case .rejectedTimeout: return "rejectedTimeout"
  206. case .rejectedDisabled: return "rejectedDisabled"
  207. case .rejectedOffHours: return "rejectedOffHours"
  208. case .rejectedUnknown: return "rejectedUnknown"
  209. case .microphoneDisabled: return "microphoneDisabled"
  210. }
  211. }
  212. /**
  213. Get the localized string for the current state
  214. - Returns: Current localized call state string
  215. */
  216. func callStateLocalizedString() -> String {
  217. switch state {
  218. case .idle: return BundleUtil.localizedString(forKey: "call_status_idle")
  219. case .sendOffer: return BundleUtil.localizedString(forKey: "call_status_wait_ringing")
  220. case .receivedOffer: return BundleUtil.localizedString(forKey: "call_status_wait_ringing")
  221. case .outgoingRinging: return BundleUtil.localizedString(forKey: "call_status_ringing")
  222. case .incomingRinging: return BundleUtil.localizedString(forKey: "call_status_incom_ringing")
  223. case .sendAnswer: return BundleUtil.localizedString(forKey: "call_status_ringing")
  224. case .receivedAnswer: return BundleUtil.localizedString(forKey: "call_status_ringing")
  225. case .initalizing: return BundleUtil.localizedString(forKey: "call_status_initializing")
  226. case .calling: return BundleUtil.localizedString(forKey: "call_status_calling")
  227. case .reconnecting: return BundleUtil.localizedString(forKey: "call_status_reconnecting")
  228. case .ended: return BundleUtil.localizedString(forKey: "call_end")
  229. case .remoteEnded: return BundleUtil.localizedString(forKey: "call_end")
  230. case .rejected: return BundleUtil.localizedString(forKey: "call_rejected")
  231. case .rejectedBusy: return BundleUtil.localizedString(forKey: "call_rejected_busy")
  232. case .rejectedTimeout: return BundleUtil.localizedString(forKey: "call_rejected_timeout")
  233. case .rejectedDisabled: return BundleUtil.localizedString(forKey: "call_rejected_disabled")
  234. case .rejectedOffHours: return BundleUtil.localizedString(forKey: "call_rejected")
  235. case .rejectedUnknown: return BundleUtil.localizedString(forKey: "call_rejected")
  236. case .microphoneDisabled: return BundleUtil.localizedString(forKey: "call_microphone_permission_title")
  237. }
  238. }
  239. /**
  240. Start process to handle the message
  241. - parameter element: Message
  242. */
  243. func startProcess(element: Any) {
  244. if let action = element as? VoIPCallUserAction {
  245. switch action.action {
  246. case .call:
  247. startCallAsInitiator(action: action, completion: {
  248. self.delegate?.callServiceFinishedProcess()
  249. action.completion?()
  250. })
  251. break
  252. case .callWithVideo:
  253. startCallAsInitiator(action: action, completion: {
  254. self.delegate?.callServiceFinishedProcess()
  255. action.completion?()
  256. })
  257. break
  258. case .accept:
  259. self.alreadyAccepted = true
  260. self.acceptIncomingCall(action: action) {
  261. self.delegate?.callServiceFinishedProcess()
  262. action.completion?()
  263. }
  264. break
  265. case .acceptCallKit:
  266. self.alreadyAccepted = true
  267. self.acceptIncomingCall(action: action) {
  268. self.delegate?.callServiceFinishedProcess()
  269. action.completion?()
  270. }
  271. break
  272. case .reject:
  273. rejectCall(action: action)
  274. action.completion?()
  275. self.delegate?.callServiceFinishedProcess()
  276. break
  277. case .rejectDisabled:
  278. rejectCall(action: action)
  279. action.completion?()
  280. self.delegate?.callServiceFinishedProcess()
  281. break
  282. case .rejectTimeout:
  283. rejectCall(action: action)
  284. action.completion?()
  285. self.delegate?.callServiceFinishedProcess()
  286. break
  287. case .rejectBusy:
  288. rejectCall(action: action)
  289. action.completion?()
  290. self.delegate?.callServiceFinishedProcess()
  291. break
  292. case .rejectOffHours:
  293. rejectCall(action: action)
  294. action.completion?()
  295. self.delegate?.callServiceFinishedProcess()
  296. break
  297. case .rejectUnknown:
  298. rejectCall(action: action)
  299. action.completion?()
  300. self.delegate?.callServiceFinishedProcess()
  301. break
  302. case .end:
  303. DDLogNotice("Threema call: HangupBug -> Send hangup for end action")
  304. if state == .sendOffer || state == .outgoingRinging || state == .sendAnswer || state == .receivedAnswer || state == .initalizing || state == .calling || state == .reconnecting {
  305. RTCAudioSession.sharedInstance().isAudioEnabled = false
  306. let hangupMessage = VoIPCallHangupMessage(contact: action.contact, callId: action.callId!, completion: nil)
  307. VoIPCallSender.sendVoIPCallHangup(hangupMessage: hangupMessage, wait: false)
  308. state = .ended
  309. callKitManager?.endCall()
  310. dismissCallView()
  311. disconnectPeerConnection()
  312. }
  313. self.delegate?.callServiceFinishedProcess()
  314. action.completion?()
  315. break
  316. case .speakerOn:
  317. speakerActive = true
  318. peerConnectionClient?.speakerOn()
  319. self.delegate?.callServiceFinishedProcess()
  320. action.completion?()
  321. break
  322. case .speakerOff:
  323. speakerActive = false
  324. peerConnectionClient?.speakerOff()
  325. self.delegate?.callServiceFinishedProcess()
  326. action.completion?()
  327. break
  328. case .muteAudio:
  329. peerConnectionClient?.muteAudio(completion: {
  330. self.delegate?.callServiceFinishedProcess()
  331. action.completion?()
  332. })
  333. break
  334. case .unmuteAudio:
  335. peerConnectionClient?.unmuteAudio(completion: {
  336. self.delegate?.callServiceFinishedProcess()
  337. action.completion?()
  338. })
  339. break
  340. case .showCallScreen:
  341. if contact != nil {
  342. presentCallView(contact: contact!, alreadyAccepted:alreadyAccepted , isCallInitiator: callInitiator, isThreemaVideoCallAvailable: threemaVideoCallAvailable, videoActive: videoActive, receivingVideo: isReceivingVideo, viewWasHidden: true)
  343. }
  344. self.delegate?.callServiceFinishedProcess()
  345. action.completion?()
  346. break
  347. case .hideCallScreen:
  348. dismissCallView()
  349. self.delegate?.callServiceFinishedProcess()
  350. action.completion?()
  351. break
  352. }
  353. }
  354. else if let offer = element as? VoIPCallOfferMessage {
  355. handleOfferMessage(offer: offer, completion: {
  356. offer.completion?()
  357. self.delegate?.callServiceFinishedProcess()
  358. })
  359. }
  360. else if let answer = element as? VoIPCallAnswerMessage {
  361. handleAnswerMessage(answer: answer, completion: {
  362. answer.completion?()
  363. self.delegate?.callServiceFinishedProcess()
  364. })
  365. }
  366. else if let ringing = element as? VoIPCallRingingMessage {
  367. handleRingingMessage(ringing: ringing, completion: {
  368. ringing.completion?()
  369. self.delegate?.callServiceFinishedProcess()
  370. })
  371. }
  372. else if let hangup = element as? VoIPCallHangupMessage {
  373. handleHangupMessage(hangup: hangup, completion: {
  374. hangup.completion?()
  375. self.delegate?.callServiceFinishedProcess()
  376. })
  377. }
  378. else if let ice = element as? VoIPCallIceCandidatesMessage {
  379. handleIceCandidatesMessage(ice: ice) {
  380. ice.completion?()
  381. self.delegate?.callServiceFinishedProcess()
  382. }
  383. }
  384. else {
  385. self.delegate?.callServiceFinishedProcess()
  386. }
  387. }
  388. /**
  389. Get the current call state
  390. - Returns: CallState
  391. */
  392. func currentState() -> CallState {
  393. return state
  394. }
  395. /**
  396. Get the current call contact
  397. - Returns: Contact or nil
  398. */
  399. func currentContact() -> Contact? {
  400. return contact
  401. }
  402. /**
  403. Get the current callId
  404. - Returns: VoIPCallId or nil
  405. */
  406. func currentCallId() -> VoIPCallId? {
  407. return callId
  408. }
  409. /**
  410. Is initiator of the current call
  411. - Returns: true or false
  412. */
  413. func isCallInitiator() -> Bool {
  414. return callInitiator
  415. }
  416. /**
  417. Is the current call muted
  418. - Returns: true or false
  419. */
  420. func isCallMuted() -> Bool {
  421. return audioMuted
  422. }
  423. /**
  424. Is the speaker for the current call active
  425. - Returns: true or false
  426. */
  427. func isSpeakerActive() -> Bool {
  428. return speakerActive
  429. }
  430. /**
  431. Is the current call already accepted
  432. - Returns: true or false
  433. */
  434. func isCallAlreadyAccepted() -> Bool {
  435. return alreadyAccepted
  436. }
  437. /**
  438. Present the CallViewController
  439. */
  440. func presentCallViewController() {
  441. if contact != nil {
  442. presentCallView(contact: contact!, alreadyAccepted: alreadyAccepted, isCallInitiator: callInitiator, isThreemaVideoCallAvailable: threemaVideoCallAvailable, videoActive: videoActive, receivingVideo: isReceivingVideo, viewWasHidden: alreadyAccepted)
  443. }
  444. }
  445. /**
  446. Dismiss the CallViewController
  447. */
  448. func dismissCallViewController() {
  449. dismissCallView()
  450. }
  451. /**
  452. Set the RTC audio session from CallKit
  453. - parameter callKitAudioSession: AVAudioSession from callkit
  454. */
  455. func setRTCAudioSession(_ callKitAudioSession: AVAudioSession) {
  456. handleTones(state: .calling, oldState: .calling)
  457. RTCAudioSession.sharedInstance().audioSessionDidActivate(callKitAudioSession)
  458. }
  459. /**
  460. Configure the audio session and set RTC audio active
  461. */
  462. func activateRTCAudio() {
  463. peerConnectionClient?.activateRTCAudio(speakerActive: speakerActive)
  464. }
  465. /**
  466. Start capture local video
  467. */
  468. func startCaptureLocalVideo(renderer: RTCVideoRenderer, useBackCamera: Bool, switchCamera: Bool = false) {
  469. localRenderer = renderer
  470. videoActive = true
  471. peerConnectionClient?.startCaptureLocalVideo(renderer: renderer, useBackCamera: useBackCamera, switchCamera: switchCamera)
  472. }
  473. /**
  474. End capture local video
  475. */
  476. func endCaptureLocalVideo(switchCamera: Bool = false) {
  477. if !switchCamera {
  478. videoActive = false
  479. }
  480. if let renderer = localRenderer {
  481. peerConnectionClient?.endCaptureLocalVideo(renderer: renderer, switchCamera: switchCamera)
  482. localRenderer = nil
  483. }
  484. }
  485. /**
  486. Get local video renderer
  487. */
  488. func localVideoRenderer() -> RTCVideoRenderer? {
  489. return localRenderer
  490. }
  491. /**
  492. Start render remote video
  493. */
  494. func renderRemoteVideo(to renderer: RTCVideoRenderer) {
  495. remoteRenderer = renderer
  496. peerConnectionClient?.renderRemoteVideo(to: renderer)
  497. }
  498. /**
  499. End remote video
  500. */
  501. func endRemoteVideo() {
  502. if let renderer = remoteRenderer {
  503. peerConnectionClient?.endRemoteVideo(renderer: renderer)
  504. remoteRenderer = nil
  505. }
  506. }
  507. /**
  508. Get remote video renderer
  509. */
  510. func remoteVideoRenderer() -> RTCVideoRenderer? {
  511. return remoteRenderer
  512. }
  513. /**
  514. Get peer video quality profile
  515. */
  516. func remoteVideoQualityProfile() -> CallsignalingProtocol.ThreemaVideoCallQualityProfile? {
  517. return peerConnectionClient?.remoteVideoQualityProfile
  518. }
  519. /**
  520. Get peer is using turn server
  521. */
  522. func networkIsRelayed() -> Bool {
  523. return peerConnectionClient?.networkIsRelayed ?? false
  524. }
  525. }
  526. extension VoIPCallService {
  527. // MARK: private functions
  528. /**
  529. When the current call state is idle and the permission is granted to the microphone, it will create the peer client and add the offer.
  530. If the state is wrong, it will reject the call with the reason unknown.
  531. If the permission to the microphone is not granted, it will reject the call with the reason unknown.
  532. If Threema Calls are disabled, it will reject the call with the reason disabled.
  533. - parameter offer: VoIPCallOfferMessage
  534. - parameter completion: Completion block
  535. */
  536. private func handleOfferMessage(offer: VoIPCallOfferMessage, completion: @escaping (() -> Void)) {
  537. DDLogNotice("Threema call: handle incomming offer from \(offer.contact?.identity ?? "?") with callId \(offer.callId.callId)")
  538. if UserSettings.shared().enableThreemaCall == true && is64Bit == 1 {
  539. var appRunsInBackground = false
  540. DispatchQueue.main.sync {
  541. appRunsInBackground = AppDelegate.shared().isAppInBackground()
  542. }
  543. if state == .idle {
  544. if PendingMessagesManager.canMasterDndSendPush() == false {
  545. DDLogNotice("Threema call: handleOfferMessage -> Master DND active -> reject call from \(String(describing: offer.contact?.identity))");
  546. self.contact = offer.contact
  547. let action = VoIPCallUserAction.init(action: .rejectOffHours, contact: offer.contact!, callId: offer.callId, completion: offer.completion)
  548. self.rejectCall(action: action, closeCallView: true)
  549. completion()
  550. return
  551. }
  552. AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
  553. if granted == true {
  554. DDLogNotice("Threema call: handleOfferMessage -> Add offer -> set contact to service \(String(describing: offer.contact?.identity))");
  555. self.contact = offer.contact
  556. self.alreadyAccepted = false
  557. self.state = .receivedOffer
  558. self.incomingOffer = offer
  559. self.callId = offer.callId
  560. self.videoActive = false
  561. self.isReceivingVideo = false
  562. self.localRenderer = nil
  563. self.remoteRenderer = nil
  564. self.threemaVideoCallAvailable = offer.isVideoAvailable
  565. self.startIncomingCallTimeoutTimer()
  566. if UserSettings.shared()?.enableCallKit == true && Locale.current.regionCode != "CN" {
  567. if self.callKitManager == nil {
  568. self.callKitManager = VoIPCallKitManager.init()
  569. }
  570. } else {
  571. self.callKitManager = nil
  572. }
  573. // send ringing message
  574. let ringingMessage = VoIPCallRingingMessage(contact: offer.contact!, callId: offer.callId, completion: nil)
  575. VoIPCallSender.sendVoIPCallRinging(ringingMessage: ringingMessage)
  576. self.callKitManager?.reportIncomingCall(uuid: UUID.init(), contact: offer.contact!)
  577. if self.callKitManager == nil {
  578. self.presentCallView(contact: offer.contact!, alreadyAccepted: false, isCallInitiator: false, isThreemaVideoCallAvailable: self.threemaVideoCallAvailable, videoActive: false, receivingVideo: false, viewWasHidden: false)
  579. }
  580. self.state = .incomingRinging
  581. // Prefetch ICE/TURN servers so they're likely to be already available when the user accepts the call
  582. VoIPIceServerSource.prefetchIceServers()
  583. completion()
  584. } else {
  585. DDLogNotice("Threema call: handleOfferMessage -> Audio is not granted -> reject call from \(String(describing: offer.contact?.identity))");
  586. self.contact = offer.contact
  587. self.state = .microphoneDisabled
  588. // reject call because there is no permission for the microphone
  589. self.state = .rejectedDisabled
  590. let action = VoIPCallUserAction.init(action: .rejectUnknown, contact: offer.contact!, callId: offer.callId, completion: offer.completion)
  591. self.rejectCall(action: action, closeCallView: false)
  592. if appRunsInBackground == true {
  593. // show notification that incoming call can't process because mic is not granted
  594. self.disconnectPeerConnection()
  595. completion()
  596. } else {
  597. self.presentCallView(contact: offer.contact!, alreadyAccepted: false, isCallInitiator: false, isThreemaVideoCallAvailable: self.threemaVideoCallAvailable, videoActive: false, receivingVideo: false, viewWasHidden: false, completion: {
  598. // no access to microphone, stopp call
  599. let alertTitle = BundleUtil.localizedString(forKey: "call_microphone_permission_title")
  600. let alertMessage = BundleUtil.localizedString(forKey: "call_microphone_permission_text")
  601. let alert = UIAlertController.init(title:alertTitle , message: alertMessage, preferredStyle: .alert)
  602. alert.addAction(UIAlertAction.init(title: BundleUtil.localizedString(forKey: "settings"), style: .default, handler: { (action) in
  603. self.dismissCallView()
  604. self.disconnectPeerConnection()
  605. UIApplication.shared.open(NSURL.init(string: UIApplication.openSettingsURLString)! as URL, options: [:], completionHandler: nil)
  606. }))
  607. alert.addAction(UIAlertAction.init(title: BundleUtil.localizedString(forKey: "ok"), style: .default, handler: { (action) in
  608. self.dismissCallView()
  609. self.disconnectPeerConnection()
  610. }))
  611. let rootVC = self.callViewController != nil ? self.callViewController! : UIApplication.shared.keyWindow?.rootViewController!
  612. DispatchQueue.main.async {
  613. rootVC?.present(alert, animated: true, completion: nil)
  614. }
  615. completion()
  616. })
  617. }
  618. }
  619. }
  620. } else {
  621. DDLogNotice("Threema call: handleOfferMessage -> State is not idle");
  622. if contact == offer.contact && state == .incomingRinging {
  623. DDLogNotice("Threema call: handleOfferMessage -> same contact as the current call");
  624. if PendingMessagesManager.canMasterDndSendPush() == false && appRunsInBackground == true{
  625. DDLogNotice("Threema call: handleOfferMessage -> Master DND active -> reject call from \(String(describing: offer.contact?.identity))");
  626. let action = VoIPCallUserAction.init(action: .rejectOffHours, contact: offer.contact!, callId: offer.callId, completion: offer.completion)
  627. self.rejectCall(action: action, closeCallView: true)
  628. completion()
  629. } else {
  630. DDLogNotice("Threema call: handleOfferMessage -> Master DND inactive -> set offer \(String(describing: offer.contact?.identity))");
  631. disconnectPeerConnection()
  632. handleOfferMessage(offer: offer, completion: completion)
  633. }
  634. } else {
  635. DDLogNotice("Threema call: handleOfferMessage -> reject call, it's the wrong state");
  636. // reject call because it's the wrong state
  637. let reason: VoIPCallUserAction.Action = contact == offer.contact ? .rejectUnknown : .rejectBusy
  638. let action = VoIPCallUserAction.init(action: reason, contact: offer.contact!, callId: offer.callId, completion: offer.completion)
  639. rejectCall(action: action)
  640. completion()
  641. }
  642. }
  643. } else {
  644. DDLogNotice("Threema call: handleOfferMessage -> reject all, threema call is disabled");
  645. // reject call because Threema Calls are disabled or unavailable
  646. let action = VoIPCallUserAction.init(action: .rejectDisabled, contact: offer.contact!, callId: offer.callId, completion: offer.completion)
  647. rejectCall(action: action)
  648. completion()
  649. }
  650. }
  651. private func startIncomingCallTimeoutTimer() {
  652. DispatchQueue.main.async {
  653. if let offer = self.incomingOffer {
  654. self.invalidateIncomingCallTimeout()
  655. self.incomingCallTimeoutTimer = Timer.scheduledTimer(withTimeInterval: self.kIncomingCallTimeout, repeats: false, block: { (timeout) in
  656. BackgroundTaskManager.shared.newBackgroundTask(key: kAppVoIPBackgroundTask, timeout: Int(kAppVoIPBackgroundTaskTime)) {
  657. ServerConnector.shared()?.connectWait()
  658. let action = VoIPCallUserAction.init(action: .rejectTimeout, contact: offer.contact!, callId: offer.callId, completion: offer.completion)
  659. self.state = .rejectedTimeout
  660. self.callKitManager?.timeoutCall()
  661. self.rejectCall(action: action)
  662. self.invalidateIncomingCallTimeout()
  663. }
  664. })
  665. }
  666. }
  667. }
  668. /**
  669. Handle the answer message if the contact in the answer message is the same as in the call service and call state is ringing.
  670. Call will cancel if it's rejected and CallViewController will close.
  671. - parameter answer: VoIPCallAnswerMessage
  672. - parameter completion: Completion block
  673. */
  674. private func handleAnswerMessage(answer: VoIPCallAnswerMessage, completion: @escaping (() -> Void)) {
  675. DDLogNotice("Threema call: handle incomming answer from \(answer.contact?.identity ?? "?") with callId \(answer.callId.callId)")
  676. if contact != nil {
  677. if callInitiator == true {
  678. if let callId = callId, (state == .sendOffer || state == .outgoingRinging) && contact!.identity == answer.contact?.identity && callId.isSame(answer.callId) {
  679. state = .receivedAnswer
  680. if answer.action == VoIPCallAnswerMessage.MessageAction.reject {
  681. // call is rejected
  682. switch answer.rejectReason {
  683. case .busy?:
  684. state = .rejectedBusy
  685. break
  686. case .timeout?:
  687. state = .rejectedTimeout
  688. break
  689. case .reject?:
  690. state = .rejected
  691. break
  692. case .disabled?:
  693. state = .rejectedDisabled
  694. break
  695. case .offHours?:
  696. state = .rejectedOffHours
  697. break
  698. case .none:
  699. state = .rejected
  700. case .some(.unknown):
  701. state = .rejectedUnknown
  702. }
  703. callKitManager?.rejectCall()
  704. self.dismissCallView(rejected: true, completion: {
  705. self.disconnectPeerConnection()
  706. completion()
  707. })
  708. } else {
  709. // handle answer
  710. state = .receivedAnswer
  711. if answer.isVideoAvailable && UserSettings.shared().enableVideoCall {
  712. self.threemaVideoCallAvailable = true
  713. callViewController?.enableThreemaVideoCall()
  714. } else {
  715. self.threemaVideoCallAvailable = false
  716. callViewController?.disableThreemaVideoCall()
  717. }
  718. if let remoteSdp = answer.answer {
  719. peerConnectionClient?.set(remoteSdp: remoteSdp, completion: { (error) in
  720. if error == nil {
  721. switch self.state {
  722. case .idle, .sendOffer, .receivedOffer, .outgoingRinging, .incomingRinging, .sendAnswer, .receivedAnswer:
  723. self.state = .initalizing
  724. default:
  725. break
  726. }
  727. } else {
  728. // can't set remote sdp --> end call
  729. DDLogNotice("Threema call: HangupBug -> Can't set remote sdp -> hangup")
  730. let hangupMessage = VoIPCallHangupMessage(contact: self.contact!, callId: self.callId!, completion: nil)
  731. VoIPCallSender.sendVoIPCallHangup(hangupMessage: hangupMessage, wait: false)
  732. self.state = .rejectedUnknown
  733. self.dismissCallView()
  734. self.disconnectPeerConnection()
  735. }
  736. completion()
  737. })
  738. } else {
  739. // remote sdp is empty --> end call
  740. DDLogNotice("Threema call: HangupBug -> Remote sdp is empty -> hangup")
  741. let hangupMessage = VoIPCallHangupMessage(contact: self.contact!, callId: self.callId!, completion: nil)
  742. VoIPCallSender.sendVoIPCallHangup(hangupMessage: hangupMessage, wait: false)
  743. self.state = .rejectedUnknown
  744. self.dismissCallView()
  745. self.disconnectPeerConnection()
  746. completion()
  747. }
  748. }
  749. } else {
  750. if contact!.identity == answer.contact?.identity {
  751. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)): Can't handle answer message, because \(callStateString()) is the wrong state or answer callId \(answer.callId.callId) is different to \(callId?.callId ?? 0)")
  752. } else {
  753. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)): Contact in manager is different to answer message \(String(describing: answer.contact?.identity))")
  754. }
  755. completion()
  756. }
  757. } else {
  758. // We are not the initiator so we can ignore this message
  759. ValidationLogger.shared().logString("Threema call: Not initiator, ignore this message -> answer message from contact \(String(describing: answer.contact?.identity))")
  760. completion()
  761. }
  762. } else {
  763. ValidationLogger.shared().logString("Threema call: No contact set in manager -> answer message from contact \(String(describing: answer.contact?.identity))")
  764. completion()
  765. }
  766. }
  767. /**
  768. Handle the ringing message if the contact in the answer message is the same as in the call service and call state is sendOffer.
  769. CallViewController will play the ringing tone
  770. - parameter ringing: VoIPCallRingingMessage
  771. - parameter completion: Completion block
  772. */
  773. private func handleRingingMessage(ringing: VoIPCallRingingMessage, completion: @escaping (() -> Void)) {
  774. DDLogNotice("Threema call: handle incoming ringing from \(ringing.contact.identity ?? "?") with callId \(ringing.callId.callId)")
  775. if contact != nil {
  776. if let callId = callId, contact!.identity == ringing.contact.identity && callId.isSame(ringing.callId) {
  777. switch state {
  778. case .sendOffer:
  779. state = .outgoingRinging
  780. break
  781. default:
  782. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)): Can't handle ringing message, because \(callStateString()) is the wrong state")
  783. }
  784. } else {
  785. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)) (\(callId?.callId ?? 0): Contact in manager is different to ringing message \(String(describing: ringing.contact.identity)) (\(ringing.callId.callId)")
  786. }
  787. } else {
  788. ValidationLogger.shared().logString("Threema call: No contact set in manager -> ringing message from contact \(String(describing: ringing.contact.identity))")
  789. }
  790. completion()
  791. }
  792. /**
  793. Handle add or remove received remote ice candidates (IpV6 candidates will be removed)
  794. - parameter ice: VoIPCallIceCandidatesMessage
  795. - parameter completion: Completion block
  796. */
  797. private func handleIceCandidatesMessage(ice: VoIPCallIceCandidatesMessage, completion: @escaping (() -> Void)) {
  798. DDLogNotice("Threema call: handle incoming ice candidates from \(ice.contact?.identity ?? "?") with callId \(ice.callId.callId)")
  799. if contact != nil {
  800. if let callId = callId, contact!.identity == ice.contact?.identity && callId.isSame(ice.callId) {
  801. switch state {
  802. case .sendOffer, .outgoingRinging, .sendAnswer, .receivedAnswer, .initalizing, .calling, .reconnecting:
  803. if ice.removed == false {
  804. for candidate in ice.candidates {
  805. if shouldAddLocalCandidate(candidate) == true {
  806. peerConnectionClient?.set(addRemoteCandidate: candidate)
  807. }
  808. }
  809. completion()
  810. } else {
  811. // ICE candidate messages are currently allowed to have a "removed" flag. However, this is non-standard.
  812. // When receiving an VoIP ICE Candidate (0x62) message with removed set to true, discard the message
  813. completion()
  814. }
  815. break
  816. case .receivedOffer, .incomingRinging:
  817. // add to local array
  818. receivedIceCandidatesLockQueue.sync {
  819. receivedIcecandidatesMessages.append(ice)
  820. completion()
  821. }
  822. break
  823. default:
  824. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)): Can't handle ice candidates message, because \(callStateString()) is the wrong state")
  825. completion()
  826. }
  827. } else {
  828. addUnknownCallIcecandidatesMessages(message: ice)
  829. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)): Contact in manager is different to ice candidates message \(String(describing: ice.contact?.identity))")
  830. completion()
  831. }
  832. } else {
  833. addUnknownCallIcecandidatesMessages(message: ice)
  834. ValidationLogger.shared().logString("Threema call: No contact set in manager -> ice candidates message from contact \(String(describing: ice.contact?.identity))")
  835. completion()
  836. }
  837. }
  838. /**
  839. Handle the hangup message if the contact in the answer message is the same as in the call service and call state is receivedOffer, ringing, sendAnswer, initializing, calling or reconnecting.
  840. It will dismiss the CallViewController after the call was ended.
  841. - parameter hangup: VoIPCallHangupMessage
  842. - parameter completion: Completion block
  843. */
  844. private func handleHangupMessage(hangup: VoIPCallHangupMessage, completion: @escaping (() -> Void)) {
  845. DDLogNotice("Threema call: handle incoming hangup from \(hangup.contact.identity ?? "?") with callId \(hangup.callId.callId)")
  846. if contact != nil {
  847. if let callId = callId, contact!.identity == hangup.contact.identity && callId.isSame(hangup.callId) {
  848. switch state {
  849. case .receivedOffer, .outgoingRinging, .incomingRinging, .sendAnswer, .initalizing, .calling, .reconnecting:
  850. RTCAudioSession.sharedInstance().isAudioEnabled = false
  851. state = .remoteEnded
  852. callKitManager?.endCall()
  853. dismissCallView()
  854. disconnectPeerConnection()
  855. break
  856. default:
  857. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)): Can't handle hangup message, because \(callStateString()) is the wrong state")
  858. }
  859. } else {
  860. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)) (\(callId?.callId ?? 0): Contact in manager is different to hangup message \(String(describing: hangup.contact.identity)) (\(hangup.callId.callId)")
  861. }
  862. } else {
  863. ValidationLogger.shared().logString("Threema call: No contact set in manager -> hangup message contact \(String(describing: hangup.contact.identity))")
  864. }
  865. completion()
  866. }
  867. /**
  868. Handle a new outgoing call if Threema calls are enabled and permission for microphone is granted.
  869. It will present the CallViewController.
  870. - parameter action: VoIPCallUserAction
  871. - parameter completion: Completion block
  872. */
  873. private func startCallAsInitiator(action: VoIPCallUserAction, completion: @escaping (() -> Void)) {
  874. if UserSettings.shared().enableThreemaCall == true && is64Bit == 1 {
  875. RTCAudioSession.sharedInstance().useManualAudio = true
  876. if state == .idle {
  877. AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
  878. if granted == true {
  879. self.callInitiator = true
  880. self.contact = action.contact
  881. self.createPeerConnectionForInitiator(action: action, completion: completion)
  882. } else {
  883. // no access to microphone, stop call
  884. let alertTitle = BundleUtil.localizedString(forKey: "call_microphone_permission_title")
  885. let alertMessage = BundleUtil.localizedString(forKey: "call_microphone_permission_text")
  886. let alert = UIAlertController.init(title:alertTitle , message: alertMessage, preferredStyle: .alert)
  887. alert.addAction(UIAlertAction.init(title: BundleUtil.localizedString(forKey: "settings"), style: .default, handler: { (action) in
  888. UIApplication.shared.open(NSURL.init(string: UIApplication.openSettingsURLString)! as URL, options: [:], completionHandler: nil)
  889. }))
  890. alert.addAction(UIAlertAction(title: BundleUtil.localizedString(forKey: "ok"), style: .default, handler: nil))
  891. DispatchQueue.main.async {
  892. let rootVC = UIApplication.shared.keyWindow?.rootViewController!
  893. rootVC?.present(alert, animated: true, completion: nil)
  894. }
  895. completion()
  896. }
  897. }
  898. } else {
  899. // do nothing because it's the wrong state
  900. ValidationLogger.shared().logString("Threema call with \(String(describing: contact!.identity)): Can't handle call, because \(callStateString()) is the wrong state")
  901. completion()
  902. }
  903. } else {
  904. // do nothing because Threema calls are disabled or unavailable
  905. completion()
  906. }
  907. }
  908. /**
  909. Accept a incoming call if state is ringing. Will send a answer message to initiator and update CallViewController.
  910. It will present the CallViewController.
  911. - parameter action: VoIPCallUserAction
  912. - parameter completion: Completion block
  913. */
  914. private func acceptIncomingCall(action: VoIPCallUserAction, completion: @escaping (() -> Void)) {
  915. createPeerConnectionForIncomingCall {
  916. RTCAudioSession.sharedInstance().useManualAudio = true
  917. if self.state == .incomingRinging {
  918. self.state = .sendAnswer
  919. if #available(iOS 14.0, *) {
  920. self.presentCallViewController()
  921. }
  922. self.peerConnectionClient?.answer(completion: { (sdp) in
  923. if self.threemaVideoCallAvailable && UserSettings.shared().enableVideoCall {
  924. self.threemaVideoCallAvailable = true
  925. self.callViewController?.enableThreemaVideoCall()
  926. } else {
  927. self.threemaVideoCallAvailable = false
  928. self.callViewController?.disableThreemaVideoCall()
  929. }
  930. let answerMessage = VoIPCallAnswerMessage.init(action: .call, contact: action.contact, answer: sdp, rejectReason: nil, features: nil, isVideoAvailable: self.threemaVideoCallAvailable, callId: self.callId!, completion: nil)
  931. VoIPCallSender.sendVoIPCall(answer: answerMessage)
  932. if action.action != .acceptCallKit {
  933. self.callKitManager?.callAccepted()
  934. }
  935. self.receivedIceCandidatesLockQueue.sync {
  936. if let receivedCandidatesBeforeCall = self.receivedUnknowCallIcecandidatesMessages[action.contact.identity] {
  937. for ice in receivedCandidatesBeforeCall {
  938. if ice.callId.callId == self.callId?.callId {
  939. self.receivedIcecandidatesMessages.append(ice)
  940. }
  941. }
  942. self.receivedUnknowCallIcecandidatesMessages.removeAll()
  943. }
  944. for message in self.receivedIcecandidatesMessages {
  945. if message.removed == false {
  946. for candidate in message.candidates {
  947. if self.shouldAddLocalCandidate(candidate) == true {
  948. self.peerConnectionClient?.set(addRemoteCandidate: candidate)
  949. }
  950. }
  951. }
  952. }
  953. self.receivedIcecandidatesMessages.removeAll()
  954. }
  955. completion()
  956. return
  957. })
  958. } else {
  959. // dismiss call view because it's the wrong state
  960. let identity = action.contact.identity ?? "?"
  961. ValidationLogger.shared().logString("Threema call with \(identity): Can't handle accept call, because \(self.callStateString()) is the wrong state")
  962. self.callKitManager?.answerFailed()
  963. self.dismissCallView()
  964. self.disconnectPeerConnection()
  965. completion()
  966. return
  967. }
  968. }
  969. }
  970. /**
  971. Creates the peer connection for the initiator and set the offer.
  972. After this, it will present the CallViewController.
  973. - parameter action: VoIPCallUserAction
  974. - parameter completion: Completion block
  975. */
  976. private func createPeerConnectionForInitiator(action: VoIPCallUserAction, completion: @escaping (() -> Void)) {
  977. FeatureMask.check(Int(FEATURE_MASK_VOIP_VIDEO), forContacts: [contact!]) { (unsupportedContacts) in
  978. self.threemaVideoCallAvailable = false
  979. if unsupportedContacts!.count == 0 && UserSettings.shared().enableVideoCall {
  980. self.threemaVideoCallAvailable = true
  981. }
  982. self.peerConnectionClient?.peerConnection.close()
  983. self.peerConnectionClient = nil
  984. let forceTurn: Bool = Int(truncating: self.contact!.verificationLevel) == kVerificationLevelUnverified || UserSettings.shared()?.alwaysRelayCalls == true
  985. let peerConnectionParameters = VoIPCallPeerConnectionClient.PeerConnectionParameters(isVideoCallAvailable: self.threemaVideoCallAvailable, videoCodecHwAcceleration: self.threemaVideoCallAvailable, forceTurn: forceTurn, gatherContinually: true, allowIpv6: UserSettings.shared().enableIPv6, isDataChannelAvailable: false)
  986. VoIPCallPeerConnectionClient.instantiate(contact: self.contact!, peerConnectionParameters: peerConnectionParameters) { (result) in
  987. do {
  988. self.peerConnectionClient = try result.get()
  989. } catch let error {
  990. self.callCantCreateOffer(error: error)
  991. return
  992. }
  993. self.peerConnectionClient?.delegate = self
  994. if UserSettings.shared()?.enableCallKit == true && Locale.current.regionCode != "CN" {
  995. if self.callKitManager == nil {
  996. self.callKitManager = VoIPCallKitManager.init()
  997. }
  998. } else {
  999. self.callKitManager = nil
  1000. }
  1001. self.peerConnectionClient?.offer(completion: { (sdp, sdpError) in
  1002. if let error = sdpError {
  1003. self.callCantCreateOffer(error: error)
  1004. return
  1005. }
  1006. guard let sdp = sdp else {
  1007. self.callCantCreateOffer(error: nil)
  1008. return
  1009. }
  1010. self.callId = VoIPCallId.generate()
  1011. let offerMessage = VoIPCallOfferMessage.init(offer: sdp, contact: self.contact!, features: nil, isVideoAvailable: self.threemaVideoCallAvailable, callId: self.callId!, completion: nil)
  1012. VoIPCallSender.sendVoIPCall(offer: offerMessage)
  1013. self.state = .sendOffer
  1014. DispatchQueue.main.async {
  1015. self.initCallTimeoutTimer = Timer.scheduledTimer(withTimeInterval: self.kIncomingCallTimeout, repeats: false, block: { (timeout) in
  1016. BackgroundTaskManager.shared.newBackgroundTask(key: kAppVoIPBackgroundTask, timeout: Int(kAppPushBackgroundTaskTime)) {
  1017. ServerConnector.shared()?.connectWait()
  1018. RTCAudioSession.sharedInstance().isAudioEnabled = false
  1019. DDLogNotice("Threema call: HangupBug -> call ringing timeout -> hangup")
  1020. let hangupMessage = VoIPCallHangupMessage(contact: self.contact!, callId: self.callId!, completion: nil)
  1021. VoIPCallSender.sendVoIPCallHangup(hangupMessage: hangupMessage, wait: false)
  1022. self.state = .ended
  1023. self.disconnectPeerConnection()
  1024. DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
  1025. self.dismissCallView(rejected: false, completion: {
  1026. self.callKitManager?.endCall()
  1027. self.invalidateInitCallTimeout()
  1028. let rootVC = UIApplication.shared.keyWindow?.rootViewController!
  1029. UIAlertTemplate.showAlert(owner: rootVC!, title: BundleUtil.localizedString(forKey: "call_voip_not_supported_title"), message: BundleUtil.localizedString(forKey: "call_contact_not_reachable"))
  1030. })
  1031. })
  1032. }
  1033. })
  1034. }
  1035. self.alreadyAccepted = true
  1036. self.presentCallView(contact: self.contact!, alreadyAccepted: true, isCallInitiator: true, isThreemaVideoCallAvailable: self.threemaVideoCallAvailable, videoActive: action.action == .callWithVideo, receivingVideo: false, viewWasHidden: false)
  1037. self.callKitManager?.startCall(contact: self.contact!)
  1038. completion()
  1039. })
  1040. }
  1041. }
  1042. }
  1043. /**
  1044. Creates the peer connection for the incoming call and set the offer if contact is set in the offer.
  1045. After this, it will present the CallViewController.
  1046. - parameter action: VoIPCallUserAction
  1047. - parameter completion: Completion block
  1048. */
  1049. private func createPeerConnectionForIncomingCall(completion: @escaping (() -> Void)) {
  1050. peerConnectionClient?.peerConnection.close()
  1051. peerConnectionClient = nil
  1052. guard let offer = self.incomingOffer, let contact = offer.contact else {
  1053. self.state = .idle
  1054. completion()
  1055. return
  1056. }
  1057. FeatureMask.check(Int(FEATURE_MASK_VOIP_VIDEO), forContacts: [contact]) { (unsupportedContacts) in
  1058. if self.incomingOffer?.isVideoAvailable ?? false && UserSettings.shared().enableVideoCall {
  1059. self.threemaVideoCallAvailable = true
  1060. self.callViewController?.enableThreemaVideoCall()
  1061. } else {
  1062. self.threemaVideoCallAvailable = false
  1063. self.callViewController?.disableThreemaVideoCall()
  1064. }
  1065. let forceTurn = Int(truncating: contact.verificationLevel) == kVerificationLevelUnverified || UserSettings.shared().alwaysRelayCalls
  1066. let peerConnectionParameters = VoIPCallPeerConnectionClient.PeerConnectionParameters(isVideoCallAvailable: self.threemaVideoCallAvailable, videoCodecHwAcceleration: self.threemaVideoCallAvailable, forceTurn: forceTurn, gatherContinually: true, allowIpv6: UserSettings.shared().enableIPv6, isDataChannelAvailable: false)
  1067. VoIPCallPeerConnectionClient.instantiate(contact: contact, peerConnectionParameters: peerConnectionParameters) { (result) in
  1068. do {
  1069. self.peerConnectionClient = try result.get()
  1070. } catch let error {
  1071. print("Can't instantiate client: \(error)")
  1072. }
  1073. self.peerConnectionClient?.delegate = self
  1074. self.peerConnectionClient?.set(remoteSdp: offer.offer!, completion: { (error) in
  1075. if error == nil {
  1076. completion()
  1077. } else {
  1078. // reject because we can't add offer
  1079. print("We can't add the offer \(String(describing: error))")
  1080. let action = VoIPCallUserAction.init(action: .reject, contact: contact, callId: offer.callId, completion: offer.completion)
  1081. self.rejectCall(action: action)
  1082. }
  1083. })
  1084. }
  1085. }
  1086. }
  1087. /**
  1088. Removes the peer connection, reset the call state and reset all other values
  1089. */
  1090. private func disconnectPeerConnection() {
  1091. // remove peerConnection
  1092. func reset() {
  1093. peerConnectionClient?.peerConnection.close()
  1094. peerConnectionClient = nil
  1095. contact = nil
  1096. callId = nil
  1097. threemaVideoCallAvailable = false
  1098. alreadyAccepted = false
  1099. callInitiator = false
  1100. audioMuted = false
  1101. speakerActive = false
  1102. videoActive = false
  1103. isReceivingVideo = false
  1104. state = .idle
  1105. incomingOffer = nil
  1106. localRenderer = nil
  1107. remoteRenderer = nil
  1108. audioPlayer?.pause()
  1109. do {
  1110. RTCAudioSession.sharedInstance().lockForConfiguration()
  1111. try RTCAudioSession.sharedInstance().setActive(false)
  1112. RTCAudioSession.sharedInstance().unlockForConfiguration()
  1113. } catch {
  1114. DDLogError("Could not set shared session to not active. Error: \(error)")
  1115. }
  1116. DispatchQueue.main.async {
  1117. VoIPHelper.shared()?.isCallActiveInBackground = false
  1118. VoIPHelper.shared()?.contactName = nil
  1119. NotificationCenter.default.post(name: NSNotification.Name(kNotificationCallInBackgroundTimeChanged), object: nil)
  1120. }
  1121. }
  1122. if peerConnectionClient != nil {
  1123. peerConnectionClient!.stopVideoCall()
  1124. peerConnectionClient?.logDebugEndStats {
  1125. reset()
  1126. }
  1127. } else {
  1128. reset()
  1129. }
  1130. }
  1131. /**
  1132. Present the CallViewController in the main thread.
  1133. - parameter contact: Contact of the call
  1134. - parameter alreadyAccepted: Set to true if the call was alreay accepted
  1135. - parameter isCallInitiator: If user is the call initiator
  1136. */
  1137. private func presentCallView(contact: Contact, alreadyAccepted: Bool, isCallInitiator: Bool, isThreemaVideoCallAvailable: Bool, videoActive: Bool, receivingVideo: Bool, viewWasHidden: Bool, completion: (() -> Void)? = nil) {
  1138. DispatchQueue.main.async {
  1139. var viewWasHidden = viewWasHidden
  1140. if self.callViewController == nil {
  1141. let callStoryboard = UIStoryboard.init(name: "CallStoryboard", bundle: nil)
  1142. let callVC = callStoryboard.instantiateInitialViewController() as! CallViewController
  1143. self.callViewController = callVC
  1144. viewWasHidden = false
  1145. }
  1146. let rootVC = UIApplication.shared.keyWindow?.rootViewController
  1147. var presentingVC = (rootVC?.presentedViewController ?? rootVC)
  1148. if let navController = presentingVC as? UINavigationController {
  1149. presentingVC = navController.viewControllers.last
  1150. }
  1151. if !(presentingVC?.isKind(of: CallViewController.self))! {
  1152. if let presentedVC = presentingVC?.presentedViewController {
  1153. if presentedVC.isKind(of: CallViewController.self) {
  1154. return
  1155. }
  1156. }
  1157. if UIApplication.shared.applicationState == .active
  1158. && !self.callViewController!.isBeingPresented
  1159. && !self.isModal {
  1160. self.callViewController!.viewWasHidden = viewWasHidden
  1161. self.callViewController!.voIPCallStatusChanged(state: self.state, oldState: self.state)
  1162. self.callViewController!.contact = contact
  1163. self.callViewController!.alreadyAccepted = alreadyAccepted
  1164. self.callViewController!.isCallInitiator = isCallInitiator
  1165. self.callViewController!.threemaVideoCallAvailable = isThreemaVideoCallAvailable
  1166. self.callViewController!.isLocalVideoActive = videoActive
  1167. self.callViewController!.isReceivingRemoteVideo = receivingVideo
  1168. if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") {
  1169. self.callViewController!.isTesting = true
  1170. }
  1171. self.callViewController!.modalPresentationStyle = .overFullScreen
  1172. presentingVC?.present(self.callViewController!, animated: false, completion: {
  1173. if completion != nil {
  1174. completion!()
  1175. }
  1176. })
  1177. }
  1178. }
  1179. }
  1180. }
  1181. /**
  1182. Dismiss the CallViewController in the main thread.
  1183. */
  1184. private func dismissCallView(rejected: Bool? = false, completion: (() -> Void)? = nil) {
  1185. DispatchQueue.main.async {
  1186. if let callVC = self.callViewController {
  1187. self.callViewController?.resetStatsTimer()
  1188. if rejected == true {
  1189. if let callViewController = self.callViewController {
  1190. callViewController.endButton.isEnabled = false
  1191. callViewController.speakerButton.isEnabled = false
  1192. callViewController.muteButton.isEnabled = false
  1193. }
  1194. Timer.scheduledTimer(withTimeInterval: 4, repeats: false, block: { (timer) in
  1195. callVC.dismiss(animated: true, completion: {
  1196. switch self.state {
  1197. case .sendOffer, .receivedOffer, .outgoingRinging, .incomingRinging, .sendAnswer, .receivedAnswer, .initalizing, .calling, .reconnecting: break
  1198. case .idle, .ended, .remoteEnded, .rejected, .rejectedBusy, .rejectedTimeout, .rejectedDisabled, .rejectedOffHours, .rejectedUnknown, .microphoneDisabled:
  1199. self.callViewController = nil
  1200. }
  1201. if AppDelegate.shared()?.isAppLocked == true {
  1202. AppDelegate .shared()?.presentPasscodeView()
  1203. }
  1204. completion?()
  1205. })
  1206. })
  1207. } else {
  1208. callVC.dismiss(animated: true, completion: {
  1209. switch self.state {
  1210. case .sendOffer, .receivedOffer, .outgoingRinging, .incomingRinging, .sendAnswer, .receivedAnswer, .initalizing, .calling, .reconnecting: break
  1211. case .idle, .ended, .remoteEnded, .rejected, .rejectedBusy, .rejectedTimeout, .rejectedDisabled, .rejectedOffHours, .rejectedUnknown, .microphoneDisabled:
  1212. self.callViewController = nil
  1213. }
  1214. if AppDelegate.shared()?.isAppLocked == true {
  1215. AppDelegate .shared()?.presentPasscodeView()
  1216. }
  1217. completion?()
  1218. })
  1219. }
  1220. }
  1221. }
  1222. }
  1223. /**
  1224. Reject the call with the reason given in the action.
  1225. Will end call and dismiss the CallViewController.
  1226. - parameter action: VoIPCallUserAction with the given reject reason
  1227. - parameter closeCallView: Default is true. If set false, it will not disconnect the peer connection and will not close the call view
  1228. */
  1229. private func rejectCall(action: VoIPCallUserAction, closeCallView: Bool? = true) {
  1230. var reason: VoIPCallAnswerMessage.MessageRejectReason = .reject
  1231. switch action.action {
  1232. case .rejectDisabled:
  1233. reason = .disabled
  1234. if action.contact == contact {
  1235. state = .rejectedDisabled
  1236. }
  1237. break
  1238. case .rejectTimeout:
  1239. reason = .timeout
  1240. if action.contact == contact {
  1241. state = .rejectedTimeout
  1242. }
  1243. break
  1244. case .rejectBusy:
  1245. reason = .busy
  1246. if action.contact == contact {
  1247. state = .rejectedBusy
  1248. }
  1249. break
  1250. case .rejectOffHours:
  1251. reason = .offHours
  1252. if action.contact == contact {
  1253. state = .rejectedOffHours
  1254. }
  1255. break
  1256. case .rejectUnknown:
  1257. reason = .unknown
  1258. if action.contact == contact {
  1259. state = .rejectedUnknown
  1260. }
  1261. break
  1262. default:
  1263. if action.contact == contact {
  1264. state = .rejected
  1265. }
  1266. break
  1267. }
  1268. let answer = VoIPCallAnswerMessage.init(action: .reject, contact: action.contact, answer: nil, rejectReason: reason, features: nil, isVideoAvailable: UserSettings.shared().enableVideoCall, callId: action.callId!, completion: nil)
  1269. VoIPCallSender.sendVoIPCall(answer: answer)
  1270. if contact == action.contact {
  1271. callKitManager?.rejectCall()
  1272. if closeCallView == true {
  1273. // remove peerConnection
  1274. self.dismissCallView()
  1275. self.disconnectPeerConnection()
  1276. }
  1277. } else {
  1278. addRejectedMessageToConversation(contact: action.contact, reason: kSystemMessageCallMissed)
  1279. }
  1280. }
  1281. /**
  1282. It will check the current call state and play the correct tone if it's needed
  1283. */
  1284. private func handleTones(state: VoIPCallService.CallState, oldState: VoIPCallService.CallState) {
  1285. switch state {
  1286. case .outgoingRinging, .incomingRinging:
  1287. if callInitiator == true {
  1288. let soundFilePath = BundleUtil.path(forResource: "ringing-tone-ch-fade", ofType: "mp3")
  1289. let soundUrl = URL.init(fileURLWithPath: soundFilePath!)
  1290. setupAudioSession()
  1291. playSound(soundUrl: soundUrl, loops: -1)
  1292. } else {
  1293. if UserSettings.shared().enableCallKit == false {
  1294. var voIPSound = UserSettings.shared().voIPSound
  1295. if voIPSound == "default" {
  1296. voIPSound = "threema_best"
  1297. }
  1298. let soundFilePath = BundleUtil.path(forResource: voIPSound, ofType: "caf")
  1299. let soundUrl = URL.init(fileURLWithPath: soundFilePath!)
  1300. setupAudioSession(true)
  1301. playSound(soundUrl: soundUrl, loops: -1)
  1302. } else {
  1303. audioPlayer?.stop()
  1304. }
  1305. }
  1306. break
  1307. case .rejected, .rejectedBusy, .rejectedTimeout, .rejectedOffHours, .rejectedUnknown, .rejectedDisabled:
  1308. if PendingMessagesManager.canMasterDndSendPush() == false || self.isCallInitiator() == false {
  1309. // do not play sound if dnd mode is active and user is not the call initiator
  1310. audioPlayer?.stop()
  1311. }
  1312. else {
  1313. let soundFilePath = BundleUtil.path(forResource: "busy-4x", ofType: "mp3")
  1314. let soundUrl = URL.init(fileURLWithPath: soundFilePath!)
  1315. setupAudioSession()
  1316. playSound(soundUrl: soundUrl, loops: 0)
  1317. }
  1318. break
  1319. case .ended, .remoteEnded:
  1320. if oldState != .incomingRinging {
  1321. let soundFilePath = BundleUtil.path(forResource: "threema_hangup", ofType: "mp3")
  1322. let soundUrl = URL.init(fileURLWithPath: soundFilePath!)
  1323. setupAudioSession()
  1324. playSound(soundUrl: soundUrl, loops: 0)
  1325. } else {
  1326. audioPlayer?.stop()
  1327. }
  1328. break
  1329. case .calling:
  1330. if oldState != .reconnecting {
  1331. let soundFilePath = BundleUtil.path(forResource: "threema_pickup", ofType: "mp3")
  1332. let soundUrl = URL.init(fileURLWithPath: soundFilePath!)
  1333. setupAudioSession()
  1334. playSound(soundUrl: soundUrl, loops: 0)
  1335. } else {
  1336. audioPlayer?.stop()
  1337. }
  1338. break
  1339. case .reconnecting:
  1340. let soundFilePath = BundleUtil.path(forResource: "threema_problem", ofType: "mp3")
  1341. let soundUrl = URL.init(fileURLWithPath: soundFilePath!)
  1342. setupAudioSession()
  1343. playSound(soundUrl: soundUrl, loops: -1)
  1344. break
  1345. case .idle:
  1346. break
  1347. case .sendOffer, .receivedOffer, .sendAnswer, .receivedAnswer, .initalizing:
  1348. // do nothing
  1349. break
  1350. case .microphoneDisabled:
  1351. // do nothing
  1352. break
  1353. }
  1354. }
  1355. private func setupAudioSession(_ soloAmbient: Bool = false) {
  1356. let audioSession = AVAudioSession.sharedInstance()
  1357. if soloAmbient == true {
  1358. do {
  1359. try audioSession.setCategory(.soloAmbient, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP])
  1360. try audioSession.overrideOutputAudioPort(speakerActive ? .speaker : .none)
  1361. try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
  1362. } catch let error {
  1363. print(error.localizedDescription)
  1364. }
  1365. } else {
  1366. do {
  1367. try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.duckOthers, .allowBluetooth, .allowBluetoothA2DP])
  1368. try audioSession.overrideOutputAudioPort(speakerActive ? .speaker : .none)
  1369. try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
  1370. } catch let error {
  1371. print(error.localizedDescription)
  1372. }
  1373. }
  1374. }
  1375. /**
  1376. It will play the given sound
  1377. - parameter soundUrl: URL of the sound file
  1378. - parameter loop: -1 for endless
  1379. - parameter playOnSpeaker: True or false if should play the tone over the speaker
  1380. */
  1381. private func playSound(soundUrl: URL, loops: Int) {
  1382. audioPlayer?.stop()
  1383. do {
  1384. let player = try AVAudioPlayer(contentsOf: soundUrl, fileTypeHint: AVFileType.mp3.rawValue)
  1385. player.numberOfLoops = loops
  1386. audioPlayer = player
  1387. player.play()
  1388. } catch let error {
  1389. print(error.localizedDescription)
  1390. }
  1391. }
  1392. /**
  1393. Invalidate the timers per call state
  1394. - parameter state: new set state of the call state
  1395. */
  1396. private func invalidateTimers(state: CallState) {
  1397. switch state {
  1398. case .idle:
  1399. invalidateIncomingCallTimeout()
  1400. invalidateInitCallTimeout()
  1401. invalidateCallDuration()
  1402. invalidateCallFailedTimer()
  1403. case .sendOffer:
  1404. invalidateCallFailedTimer()
  1405. case .receivedOffer:
  1406. invalidateInitCallTimeout()
  1407. invalidateCallFailedTimer()
  1408. case .outgoingRinging, .incomingRinging:
  1409. invalidateInitCallTimeout()
  1410. invalidateCallFailedTimer()
  1411. case .sendAnswer:
  1412. invalidateInitCallTimeout()
  1413. invalidateIncomingCallTimeout()
  1414. invalidateCallFailedTimer()
  1415. case .receivedAnswer:
  1416. invalidateInitCallTimeout()
  1417. invalidateCallFailedTimer()
  1418. case .initalizing:
  1419. invalidateInitCallTimeout()
  1420. invalidateIncomingCallTimeout()
  1421. case .calling:
  1422. invalidateInitCallTimeout()
  1423. invalidateIncomingCallTimeout()
  1424. invalidateCallFailedTimer()
  1425. case .reconnecting:
  1426. invalidateInitCallTimeout()
  1427. invalidateIncomingCallTimeout()
  1428. case .ended, .remoteEnded:
  1429. invalidateInitCallTimeout()
  1430. invalidateIncomingCallTimeout()
  1431. invalidateCallFailedTimer()
  1432. case .rejected, .rejectedBusy, .rejectedTimeout, .rejectedDisabled, .rejectedOffHours, .rejectedUnknown:
  1433. invalidateInitCallTimeout()
  1434. invalidateCallDuration()
  1435. invalidateIncomingCallTimeout()
  1436. invalidateCallFailedTimer()
  1437. case .microphoneDisabled:
  1438. invalidateInitCallTimeout()
  1439. invalidateCallDuration()
  1440. invalidateIncomingCallTimeout()
  1441. invalidateCallFailedTimer()
  1442. @unknown default:
  1443. break
  1444. }
  1445. }
  1446. /**
  1447. Invalidate the incoming call timer
  1448. */
  1449. private func invalidateIncomingCallTimeout() {
  1450. incomingCallTimeoutTimer?.invalidate()
  1451. incomingCallTimeoutTimer = nil
  1452. }
  1453. /**
  1454. Invalidate the init call timer
  1455. */
  1456. private func invalidateInitCallTimeout() {
  1457. initCallTimeoutTimer?.invalidate()
  1458. initCallTimeoutTimer = nil
  1459. }
  1460. /**
  1461. Invalidate the call duration timer and set the callDurationTime to 0
  1462. */
  1463. private func invalidateCallDuration() {
  1464. callDurationTimer?.invalidate()
  1465. callDurationTimer = nil
  1466. callDurationTime = 0
  1467. }
  1468. /**
  1469. Invalidate the call duration timer and set the callDurationTime to 0
  1470. */
  1471. private func invalidateCallFailedTimer() {
  1472. callFailedTimer?.invalidate()
  1473. callFailedTimer = nil
  1474. }
  1475. /**
  1476. Add icecandidate to local array if it's in the correct state. Start a timer to send candidates as packets all 0.05 seconds
  1477. - parameter candidate: RTCIceCandidate
  1478. */
  1479. private func handleLocalIceCandidates(_ candidates: [RTCIceCandidate]) {
  1480. func addCandidateToLocalArray(_ addedCadidates: [RTCIceCandidate]) {
  1481. iceCandidatesLockQueue.sync {
  1482. for (_, candidate) in addedCadidates.enumerated() {
  1483. if shouldAddLocalCandidate(candidate) == true {
  1484. localAddedIceCandidates.append(candidate)
  1485. }
  1486. }
  1487. }
  1488. }
  1489. switch state {
  1490. case .sendOffer, .outgoingRinging, .receivedAnswer, .initalizing, .calling, .reconnecting:
  1491. addCandidateToLocalArray(candidates)
  1492. let seperatedCandidates = self.localAddedIceCandidates.take(localAddedIceCandidates.count)
  1493. if (seperatedCandidates.count > 0) {
  1494. let message = VoIPCallIceCandidatesMessage.init(removed: false, candidates: seperatedCandidates, contact: self.contact, callId: self.callId!, completion: nil)
  1495. VoIPCallSender.sendVoIPCall(iceCandidates: message)
  1496. }
  1497. self.localAddedIceCandidates.removeAll()
  1498. break
  1499. case .idle, .receivedOffer, .incomingRinging, .sendAnswer:
  1500. addCandidateToLocalArray(candidates)
  1501. break
  1502. case .ended, .remoteEnded, .rejected, .rejectedBusy, .rejectedTimeout, .rejectedOffHours, .rejectedUnknown, .rejectedDisabled, .microphoneDisabled:
  1503. // do nothing
  1504. break
  1505. }
  1506. }
  1507. /**
  1508. Check if should add a ice candidate
  1509. - parameter candidate: RTCIceCandidate
  1510. - Returns: true or false
  1511. */
  1512. private func shouldAddLocalCandidate(_ candidate: RTCIceCandidate) -> Bool {
  1513. let parts = candidate.sdp.components(separatedBy: CharacterSet.init(charactersIn: " "))
  1514. // Invalid candidate but who knows what they're doing, so we'll just eat it...
  1515. if parts.count < 8 {
  1516. return true
  1517. }
  1518. // Discard loopback
  1519. let ip = parts[4]
  1520. if ip == "172.0.0.1" || ip == "::1" {
  1521. debugPrint("Call: Discarding loopback candidate: \(candidate.sdp)")
  1522. return false
  1523. }
  1524. // Discard IPv6 if disabled
  1525. if UserSettings.shared()?.enableIPv6 == false && ip.contains(":") {
  1526. debugPrint("Call: Discarding local IPv6 candidate: \(candidate.sdp)")
  1527. return false
  1528. }
  1529. // Always add if not relay
  1530. let type = parts[7]
  1531. if type != "relay" || parts.count < 10 {
  1532. return true
  1533. }
  1534. // Always add if related address is any
  1535. let relatedAddress = parts[9]
  1536. if relatedAddress == "0.0.0.0" {
  1537. return true
  1538. }
  1539. // Discard relay candidates with the same related address
  1540. // Important: This only works as long as we don't do ICE restarts and don't add further relay transport types!
  1541. if localRelatedAddresses.contains(relatedAddress) {
  1542. debugPrint("Call: Discarding local relay candidate (duplicate related address: \(relatedAddress)): \(candidate.sdp)")
  1543. return false
  1544. } else {
  1545. localRelatedAddresses.insert(relatedAddress)
  1546. }
  1547. // Add it!
  1548. return true
  1549. }
  1550. /**
  1551. Check if an IP address is IPv6
  1552. - parameter ipToValidate: String of the ip
  1553. - Returns: true or false
  1554. */
  1555. private func isIPv6Address(_ ipToValidate: String) -> Bool {
  1556. var sin6 = sockaddr_in6()
  1557. if ipToValidate.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 {
  1558. return true
  1559. }
  1560. return false
  1561. }
  1562. /**
  1563. Handle notification if needed
  1564. */
  1565. private func handleLocalNotification() {
  1566. func addIncomCall() {
  1567. if self.callKitManager == nil {
  1568. // callkit is disabled --> show local notification
  1569. DispatchQueue.main.async {
  1570. if UIApplication.shared.applicationState != .active {
  1571. let notification = UNMutableNotificationContent.init()
  1572. notification.categoryIdentifier = "INCOMCALL"
  1573. if let pushSetting = PushSetting.find(forIdentity: self.contact!.identity) {
  1574. if pushSetting.canSendPush() && pushSetting.silent == false {
  1575. var soundName = "threema_best.caf"
  1576. if UserSettings.shared().voIPSound != "default" {
  1577. soundName = "\(UserSettings.shared().voIPSound!).caf"
  1578. }
  1579. notification.sound = UNNotificationSound.init(named: UNNotificationSoundName.init(soundName))
  1580. }
  1581. } else {
  1582. var soundName = "threema_best.caf"
  1583. if UserSettings.shared().voIPSound != "default" {
  1584. soundName = "\(UserSettings.shared().voIPSound!).caf"
  1585. }
  1586. notification.sound = UNNotificationSound.init(named: UNNotificationSoundName.init(soundName))
  1587. }
  1588. notification.userInfo = ["threema": ["cmd": "newcall", "from": self.contact!.displayName ?? "Unknown", "callId": self.callId?.callId ?? 0]]
  1589. if !UserSettings.shared().pushShowNickname {
  1590. notification.title = self.contact!.displayName
  1591. } else {
  1592. if self.contact!.publicNickname != nil && self.contact!.publicNickname.count > 0 {
  1593. notification.title = self.contact!.publicNickname
  1594. } else {
  1595. notification.title = self.contact!.identity
  1596. }
  1597. }
  1598. notification.body = BundleUtil.localizedString(forKey: "call_incoming_ended")
  1599. notification.threadIdentifier = "INCOMCALL-\(self.contact?.identity ?? "")"
  1600. let notificationRequest = UNNotificationRequest.init(identifier: self.contact!.identity, content: notification, trigger: nil)
  1601. UNUserNotificationCenter.current().add(notificationRequest, withCompletionHandler: { (error) in
  1602. })
  1603. }
  1604. }
  1605. }
  1606. }
  1607. func removeIncomCall() {
  1608. if self.callKitManager == nil {
  1609. // callkit is disabled --> delete local notification
  1610. if let identity = self.contact?.identity {
  1611. DispatchQueue.main.async {
  1612. UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identity])
  1613. }
  1614. }
  1615. }
  1616. }
  1617. func addMissedCall() {
  1618. if let contact = self.contact {
  1619. DispatchQueue.main.async {
  1620. if AppDelegate.shared().isAppInBackground() == true {
  1621. if self.isCallInitiator() == false && self.callDurationTime == 0 {
  1622. var canSendPush = true
  1623. var foundPushSetting: PushSetting?
  1624. if let pushSetting = PushSetting.find(forIdentity: contact.identity) {
  1625. foundPushSetting = pushSetting
  1626. canSendPush = pushSetting.canSendPush()
  1627. }
  1628. if canSendPush == true {
  1629. // callkit is disabled --> show missed notification
  1630. let notification = UNMutableNotificationContent.init()
  1631. notification.categoryIdentifier = "CALL"
  1632. if UserSettings.shared().pushSound != "none" {
  1633. if foundPushSetting != nil {
  1634. if foundPushSetting?.silent == false {
  1635. notification.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: UserSettings.shared().pushSound! + ".caf"))
  1636. }
  1637. } else {
  1638. notification.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: UserSettings.shared().pushSound! + ".caf"))
  1639. }
  1640. }
  1641. notification.userInfo = ["threema": ["cmd": "missedcall", "from": contact.displayName]]
  1642. if !UserSettings.shared().pushShowNickname {
  1643. notification.title = contact.displayName
  1644. } else {
  1645. if contact.publicNickname != nil && contact.publicNickname.count > 0 {
  1646. notification.title = contact.publicNickname
  1647. } else {
  1648. notification.title = contact.identity
  1649. }
  1650. }
  1651. notification.body = BundleUtil.localizedString(forKey: "call_missed")
  1652. // Group notification together with others from the same contact
  1653. notification.threadIdentifier = "SINGLE-\(self.contact?.identity ?? "")"
  1654. let notificationRequest = UNNotificationRequest.init(identifier: contact.identity, content: notification, trigger: nil)
  1655. UNUserNotificationCenter.current().add(notificationRequest, withCompletionHandler: { (error) in
  1656. })
  1657. }
  1658. }
  1659. }
  1660. }
  1661. }
  1662. }
  1663. switch state {
  1664. case .idle:
  1665. break
  1666. case .sendOffer:
  1667. break
  1668. case .receivedOffer:
  1669. break
  1670. case .outgoingRinging:
  1671. break
  1672. case .incomingRinging:
  1673. addIncomCall()
  1674. break
  1675. case .sendAnswer:
  1676. removeIncomCall()
  1677. break
  1678. case .receivedAnswer:
  1679. break
  1680. case .initalizing:
  1681. removeIncomCall()
  1682. break
  1683. case .calling:
  1684. break
  1685. case .reconnecting:
  1686. break
  1687. case .ended:
  1688. removeIncomCall()
  1689. break
  1690. case .remoteEnded:
  1691. removeIncomCall()
  1692. addMissedCall()
  1693. break
  1694. case .rejected, .rejectedOffHours, .rejectedUnknown, .rejectedDisabled:
  1695. removeIncomCall()
  1696. break
  1697. case .rejectedBusy, .rejectedTimeout:
  1698. removeIncomCall()
  1699. addMissedCall()
  1700. break
  1701. case .microphoneDisabled:
  1702. removeIncomCall()
  1703. break
  1704. }
  1705. }
  1706. /**
  1707. Add call message to conversation
  1708. */
  1709. private func addCallMessageToConversation(oldCallState: CallState) {
  1710. switch state {
  1711. case .idle:
  1712. break
  1713. case .sendOffer:
  1714. break
  1715. case .receivedOffer:
  1716. break
  1717. case .outgoingRinging:
  1718. break
  1719. case .incomingRinging:
  1720. break
  1721. case .sendAnswer:
  1722. break
  1723. case .receivedAnswer:
  1724. break
  1725. case .initalizing:
  1726. break
  1727. case .calling:
  1728. break
  1729. case .reconnecting:
  1730. break
  1731. case .ended, .remoteEnded:
  1732. // add call message
  1733. if (UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT")) {
  1734. return
  1735. }
  1736. // if remoteEnded is incoming at the same time like user tap on end call button
  1737. if oldCallState == .ended || oldCallState == .remoteEnded {
  1738. return
  1739. }
  1740. let entityManager = EntityManager()
  1741. let conversation = entityManager.conversation(for: contact!, createIfNotExisting: true)
  1742. entityManager.performSyncBlockAndSafe({
  1743. let systemMessage = entityManager.entityCreator.systemMessage(for: conversation)
  1744. systemMessage?.type = NSNumber(value: kSystemMessageCallEnded)
  1745. var callInfo = ["DateString": DateFormatter.shortStyleTimeNoDate(Date()), "CallInitiator": NSNumber(booleanLiteral: self.isCallInitiator())] as [String : Any]
  1746. if self.callDurationTime > 0 {
  1747. callInfo["CallTime"] = DateFormatter.timeFormatted(self.callDurationTime)
  1748. }
  1749. do {
  1750. let callInfoData = try JSONSerialization.data(withJSONObject: callInfo, options: .prettyPrinted)
  1751. systemMessage?.arg = callInfoData
  1752. systemMessage?.isOwn = NSNumber(booleanLiteral: self.isCallInitiator())
  1753. systemMessage?.conversation = conversation
  1754. conversation?.lastMessage = systemMessage
  1755. if self.state == .remoteEnded && self.callDurationTime == 0 {
  1756. conversation?.unreadMessageCount = NSNumber(integerLiteral: (conversation?.unreadMessageCount.intValue)!+1)
  1757. }
  1758. }
  1759. catch let error {
  1760. print(error)
  1761. }
  1762. })
  1763. if state == .remoteEnded && self.callDurationTime == 0 {
  1764. DispatchQueue.main.async {
  1765. NotificationManager.sharedInstance()?.updateUnreadMessagesCount(false)
  1766. }
  1767. }
  1768. break
  1769. case .rejected:
  1770. // add call message
  1771. if contact != nil {
  1772. addRejectedMessageToConversation(contact: contact!, reason: kSystemMessageCallRejected)
  1773. }
  1774. break
  1775. case .rejectedTimeout:
  1776. // add call message
  1777. let reason = self.isCallInitiator() ? kSystemMessageCallRejectedTimeout : kSystemMessageCallMissed
  1778. if contact != nil {
  1779. addRejectedMessageToConversation(contact: contact!, reason: reason)
  1780. }
  1781. break
  1782. case .rejectedBusy:
  1783. // add call message
  1784. let reason = self.isCallInitiator() ? kSystemMessageCallRejectedBusy : kSystemMessageCallMissed
  1785. if contact != nil {
  1786. addRejectedMessageToConversation(contact: contact!, reason: reason)
  1787. }
  1788. break
  1789. case .rejectedOffHours:
  1790. // add call message
  1791. let reason = self.isCallInitiator() ? kSystemMessageCallRejectedOffHours : kSystemMessageCallMissed
  1792. if contact != nil {
  1793. addRejectedMessageToConversation(contact: contact!, reason: reason)
  1794. }
  1795. break
  1796. case .rejectedUnknown:
  1797. // add call message
  1798. let reason = self.isCallInitiator() ? kSystemMessageCallRejectedUnknown : kSystemMessageCallMissed
  1799. if contact != nil {
  1800. addRejectedMessageToConversation(contact: contact!, reason: reason)
  1801. }
  1802. break
  1803. case .rejectedDisabled:
  1804. // add call message
  1805. if callInitiator == true {
  1806. if contact != nil {
  1807. addRejectedMessageToConversation(contact: contact!, reason: kSystemMessageCallRejectedDisabled)
  1808. }
  1809. }
  1810. break
  1811. case .microphoneDisabled:
  1812. break
  1813. }
  1814. }
  1815. private func addRejectedMessageToConversation(contact: Contact, reason: Int) {
  1816. let entityManager = EntityManager()
  1817. let conversation = entityManager.conversation(for: contact, createIfNotExisting: true)
  1818. entityManager.performSyncBlockAndSafe({
  1819. let systemMessage = entityManager.entityCreator.systemMessage(for: conversation)
  1820. systemMessage?.type = NSNumber(value: reason)
  1821. let callInfo = ["DateString": DateFormatter.shortStyleTimeNoDate(Date()), "CallInitiator": NSNumber(booleanLiteral: self.isCallInitiator())] as [String : Any]
  1822. do {
  1823. let callInfoData = try JSONSerialization.data(withJSONObject: callInfo, options: .prettyPrinted)
  1824. systemMessage?.arg = callInfoData
  1825. systemMessage?.isOwn = NSNumber(booleanLiteral: self.isCallInitiator())
  1826. systemMessage?.conversation = conversation
  1827. conversation?.lastMessage = systemMessage
  1828. if reason == kSystemMessageCallMissed || reason == kSystemMessageCallRejectedBusy || reason == kSystemMessageCallRejectedTimeout || reason == kSystemMessageCallRejectedDisabled {
  1829. conversation?.unreadMessageCount = NSNumber(integerLiteral: (conversation?.unreadMessageCount.intValue)!+1)
  1830. } else {
  1831. systemMessage?.read = true
  1832. systemMessage?.readDate = Date()
  1833. }
  1834. }
  1835. catch let error {
  1836. print(error)
  1837. }
  1838. })
  1839. if reason == kSystemMessageCallMissed || reason == kSystemMessageCallRejectedBusy || reason == kSystemMessageCallRejectedTimeout || reason == kSystemMessageCallRejectedDisabled {
  1840. DispatchQueue.main.async {
  1841. NotificationManager.sharedInstance()?.updateUnreadMessagesCount(false)
  1842. }
  1843. }
  1844. }
  1845. private func addUnknownCallIcecandidatesMessages(message: VoIPCallIceCandidatesMessage) {
  1846. receivedIceCandidatesLockQueue.sync {
  1847. guard let contact = message.contact else {
  1848. return
  1849. }
  1850. if var contactCandidates = receivedUnknowCallIcecandidatesMessages[contact.identity] {
  1851. contactCandidates.append(message)
  1852. } else {
  1853. receivedUnknowCallIcecandidatesMessages[contact.identity] = [message]
  1854. }
  1855. }
  1856. }
  1857. private func sdpContainsVideo(sdp: RTCSessionDescription?) -> Bool {
  1858. guard sdp != nil else {
  1859. return false
  1860. }
  1861. return sdp!.sdp.contains("m=video")
  1862. }
  1863. private func callFailed() {
  1864. DDLogNotice("Threema call: peerconnection new state failed -> close connection")
  1865. var message = BundleUtil.localizedString(forKey: "call_status_failed_connected_message")
  1866. if !self.iceWasConnected {
  1867. // show error as notification
  1868. if self.contact != nil {
  1869. let hangupMessage = VoIPCallHangupMessage(contact: self.contact!, callId: self.callId!, completion: nil)
  1870. VoIPCallSender.sendVoIPCallHangup(hangupMessage: hangupMessage, wait: false)
  1871. }
  1872. message = BundleUtil.localizedString(forKey: "call_status_failed_initializing_message")
  1873. }
  1874. NotificationBannerHelper.newErrorToast(title: BundleUtil.localizedString(forKey: "call_status_failed_title"), body: message!)
  1875. invalidateCallFailedTimer()
  1876. handleTones(state: .ended, oldState: .reconnecting)
  1877. callKitManager?.endCall()
  1878. dismissCallView()
  1879. disconnectPeerConnection()
  1880. }
  1881. private func callCantCreateOffer(error: Error?) {
  1882. DDLogNotice("Threema call: Can't create offer -> \(error?.localizedDescription ?? "error is missing")")
  1883. let message = BundleUtil.localizedString(forKey: "call_status_failed_sdp_patch_message")
  1884. NotificationBannerHelper.newErrorToast(title: BundleUtil.localizedString(forKey: "call_status_failed_title"), body: message!)
  1885. invalidateCallFailedTimer()
  1886. handleTones(state: .ended, oldState: .reconnecting)
  1887. callKitManager?.endCall()
  1888. dismissCallView()
  1889. disconnectPeerConnection()
  1890. }
  1891. }
  1892. extension VoIPCallService: VoIPCallPeerConnectionClientDelegate {
  1893. func peerConnectionClient(_ client: VoIPCallPeerConnectionClient, removedCandidates: [RTCIceCandidate]) {
  1894. // ICE candidate messages are currently allowed to have a "removed" flag. However, this is non-standard.
  1895. // Ignore generated ICE candidates with removed set to true coming from libwebrtc
  1896. }
  1897. func peerConnectionClient(_ client: VoIPCallPeerConnectionClient, addedCandidate: RTCIceCandidate) {
  1898. if contact != nil {
  1899. handleLocalIceCandidates([addedCandidate])
  1900. }
  1901. }
  1902. func peerConnectionClient(_ client: VoIPCallPeerConnectionClient, changeState: CallState) {
  1903. state = changeState
  1904. }
  1905. func peerConnectionClient(_ client: VoIPCallPeerConnectionClient, audioMuted: Bool) {
  1906. self.audioMuted = audioMuted
  1907. }
  1908. func peerConnectionClient(_ client: VoIPCallPeerConnectionClient, speakerActive: Bool) {
  1909. self.speakerActive = speakerActive
  1910. let audioSession = AVAudioSession.sharedInstance()
  1911. do {
  1912. try audioSession.setCategory(.playAndRecord, mode: speakerActive ? .videoChat : .voiceChat, options: [.duckOthers, .allowBluetooth, .allowBluetoothA2DP])
  1913. try audioSession.overrideOutputAudioPort(speakerActive ? .speaker : .none)
  1914. try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
  1915. } catch let error {
  1916. print(error.localizedDescription)
  1917. }
  1918. }
  1919. func peerConnectionClient(_ client: VoIPCallPeerConnectionClient, receivingVideo: Bool) {
  1920. if self.isReceivingVideo != receivingVideo {
  1921. self.isReceivingVideo = receivingVideo
  1922. }
  1923. }
  1924. func peerConnectionClient(_ client: VoIPCallPeerConnectionClient, didChangeConnectionState state: RTCIceConnectionState) {
  1925. DDLogNotice("Threema call: peerConnectionClient state changed: new state \(state.rawValue))")
  1926. switch state {
  1927. case .new:
  1928. break
  1929. case .checking:
  1930. self.state = .initalizing
  1931. case .connected:
  1932. invalidateCallFailedTimer()
  1933. iceWasConnected = true
  1934. if self.state != .reconnecting {
  1935. self.state = .calling
  1936. ValidationLogger.shared().logString("Threema call status is calling: \(self.callStateString())")
  1937. DispatchQueue.main.async {
  1938. self.callDurationTime = 0
  1939. self.callDurationTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (timer) in
  1940. self.callDurationTime = self.callDurationTime + 1
  1941. if self.state == .calling {
  1942. self.callViewController?.voIPCallDurationChanged(self.callDurationTime)
  1943. } else {
  1944. self.callViewController?.voIPCallStatusChanged(state: self.state, oldState: self.state)
  1945. ValidationLogger.shared().logString("Threema call status is connected, but shows something different: \(self.callStateString())")
  1946. }
  1947. if VoIPHelper.shared()?.isCallActiveInBackground == true {
  1948. NotificationCenter.default.post(name: NSNotification.Name(kNotificationCallInBackgroundTimeChanged), object: self.callDurationTime)
  1949. }
  1950. })
  1951. }
  1952. self.callViewController?.startDebugMode(connection: client.peerConnection)
  1953. callKitManager?.callConnected()
  1954. } else {
  1955. ValidationLogger.shared().logString("Threema call status is reconnecting: \(self.callStateString())")
  1956. self.state = .calling
  1957. ValidationLogger.shared().logString("Threema call status is calling: \(self.callStateString())")
  1958. }
  1959. self.activateRTCAudio()
  1960. case .completed:
  1961. break
  1962. case .failed:
  1963. if self.state == .reconnecting {
  1964. callFailed()
  1965. } else {
  1966. if self.iceWasConnected {
  1967. self.state = .reconnecting
  1968. DDLogNotice("Threema call: peerconnection failed, set state to reconnecting -> start callFailedTimer")
  1969. } else {
  1970. self.state = .initalizing
  1971. DDLogNotice("Threema call: peerconnection failed, set state to initalizing -> start callFailedTimer")
  1972. }
  1973. // start timer and wait if state change back to connected
  1974. DispatchQueue.main.async {
  1975. self.invalidateCallFailedTimer()
  1976. self.callFailedTimer = Timer.scheduledTimer(withTimeInterval: self.kCallFailedTimeout, repeats: false, block: { (timeout) in
  1977. self.callFailed()
  1978. })
  1979. }
  1980. }
  1981. case .disconnected:
  1982. if self.state == .calling || self.state == .initalizing {
  1983. self.state = .reconnecting
  1984. }
  1985. case .closed:
  1986. break
  1987. case .count:
  1988. break
  1989. @unknown default:
  1990. break
  1991. }
  1992. }
  1993. func peerConnectionClient(_ client: VoIPCallPeerConnectionClient, didReceiveData: Data) {
  1994. let threemaVideoCallSignalingMessage = CallsignalingProtocol.decodeThreemaVideoCallSignalingMessage(didReceiveData)
  1995. if let videoQualityProfile = threemaVideoCallSignalingMessage.videoQualityProfile {
  1996. peerConnectionClient?.remoteVideoQualityProfile = videoQualityProfile
  1997. }
  1998. if let captureState = threemaVideoCallSignalingMessage.captureStateChange {
  1999. switch captureState.device {
  2000. case .camera:
  2001. switch captureState.state {
  2002. case .off:
  2003. peerConnectionClient?.isRemoteVideoActivated = false
  2004. case .on:
  2005. peerConnectionClient?.isRemoteVideoActivated = true
  2006. default: break
  2007. }
  2008. default: break
  2009. }
  2010. }
  2011. debugPrint(threemaVideoCallSignalingMessage)
  2012. }
  2013. }
  2014. extension Array {
  2015. func take(_ elementsCount: Int) -> [Element] {
  2016. let min = Swift.min(elementsCount, count)
  2017. return Array(self[0..<min])
  2018. }
  2019. }