From e560b49d14c363d0ace0a4abf574a0e606c77c78 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 11 Apr 2024 18:02:41 +0700 Subject: [PATCH] ios: sound prompts and vibration during calls (#4005) * ios: sound prompts and vibration * awaiting call receipt * update --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/SimpleXAPI.swift | 5 ++ .../Shared/Views/Call/ActiveCallView.swift | 38 +++++++++++ apps/ios/Shared/Views/Call/SoundPlayer.swift | 61 ++++++++++++++++++ apps/ios/sounds/connecting_call.mp3 | Bin 0 -> 7104 bytes apps/ios/sounds/in_call.mp3 | Bin 0 -> 63425 bytes 5 files changed, 104 insertions(+) create mode 100644 apps/ios/sounds/connecting_call.mp3 create mode 100644 apps/ios/sounds/in_call.mp3 diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 94a424d319..69b660ad3c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1469,6 +1469,8 @@ class ChatReceiver { private var receiveMessages = true private var _lastMsgTime = Date.now + var messagesChannel: ((ChatResponse) -> Void)? = nil + static let shared = ChatReceiver() var lastMsgTime: Date { get { _lastMsgTime } } @@ -1486,6 +1488,9 @@ class ChatReceiver { if let msg = await chatRecvMsg() { self._lastMsgTime = .now await processReceivedMsg(msg) + if let messagesChannel { + messagesChannel(msg) + } } _ = try? await Task.sleep(nanoseconds: 7_500_000) } diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index 9f246f63f3..cffdefaaa2 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -9,6 +9,7 @@ import SwiftUI import WebKit import SimpleXChat +import AVFoundation struct ActiveCallView: View { @EnvironmentObject var m: ChatModel @@ -21,6 +22,7 @@ struct ActiveCallView: View { @Binding var canConnectCall: Bool @State var prevColorScheme: ColorScheme = .dark @State var pipShown = false + @State var wasConnected = false var body: some View { ZStack(alignment: .topLeading) { @@ -69,6 +71,11 @@ struct ActiveCallView: View { Task { await m.callCommand.setClient(nil) } AppDelegate.keepScreenOn(false) client?.endCall() + CallSoundsPlayer.shared.stop() + try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) + if (wasConnected) { + CallSoundsPlayer.shared.vibrate(long: true) + } } .background(m.activeCallViewIsCollapsed ? .clear : .black) // Quite a big delay when opening/closing the view when a scheme changes (globally) this way. It's not needed when CallKit is used since status bar is green with white text on it @@ -103,6 +110,11 @@ struct ActiveCallView: View { call.callState = .invitationSent call.localCapabilities = capabilities } + if call.supportsVideo { + try? AVAudioSession.sharedInstance().setCategory(.playback, options: .defaultToSpeaker) + } + CallSoundsPlayer.shared.startConnectingCallSound() + activeCallWaitDeliveryReceipt() } case let .offer(offer, iceCandidates, capabilities): Task { @@ -126,6 +138,8 @@ struct ActiveCallView: View { } await MainActor.run { call.callState = .negotiated + CallSoundsPlayer.shared.stop() + try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) } } case let .ice(iceCandidates): @@ -144,6 +158,10 @@ struct ActiveCallView: View { : CallController.shared.reportIncomingCall(call: call, connectedAt: nil) call.callState = .connected call.connectedAt = .now + if !wasConnected { + CallSoundsPlayer.shared.vibrate(long: false) + wasConnected = true + } } if state.connectionState == "closed" { closeCallView(client) @@ -161,6 +179,10 @@ struct ActiveCallView: View { call.callState = .connected call.connectionInfo = connectionInfo call.connectedAt = .now + if !wasConnected { + CallSoundsPlayer.shared.vibrate(long: false) + wasConnected = true + } case .ended: closeCallView(client) call.callState = .ended @@ -187,6 +209,22 @@ struct ActiveCallView: View { } } + private func activeCallWaitDeliveryReceipt() { + ChatReceiver.shared.messagesChannel = { msg in + guard let call = ChatModel.shared.activeCall, call.callState == .invitationSent else { + ChatReceiver.shared.messagesChannel = nil + return + } + if case let .chatItemStatusUpdated(_, msg) = msg, + msg.chatInfo.id == call.contact.id, + case .sndCall = msg.chatItem.content, + case .sndRcvd = msg.chatItem.meta.itemStatus { + CallSoundsPlayer.shared.startInCallSound() + ChatReceiver.shared.messagesChannel = nil + } + } + } + private func closeCallView(_ client: WebRTCClient) { if m.activeCall != nil { m.showCallView = false diff --git a/apps/ios/Shared/Views/Call/SoundPlayer.swift b/apps/ios/Shared/Views/Call/SoundPlayer.swift index 17c13ab403..c7803a0cb8 100644 --- a/apps/ios/Shared/Views/Call/SoundPlayer.swift +++ b/apps/ios/Shared/Views/Call/SoundPlayer.swift @@ -8,6 +8,7 @@ import Foundation import AVFoundation +import UIKit class SoundPlayer { static let shared = SoundPlayer() @@ -43,3 +44,63 @@ class SoundPlayer { audioPlayer = nil } } + +class CallSoundsPlayer { + static let shared = CallSoundsPlayer() + private var audioPlayer: AVAudioPlayer? + private var playerTask: Task = Task {} + + private func start(_ soundName: String, delayMs: Double) { + audioPlayer?.stop() + playerTask.cancel() + logger.debug("start \(soundName)") + guard let path = Bundle.main.path(forResource: soundName, ofType: "mp3", inDirectory: "sounds") else { + logger.debug("start: file not found") + return + } + do { + let player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) + if player.prepareToPlay() { + audioPlayer = player + } + } catch { + logger.debug("start: AVAudioPlayer error \(error.localizedDescription)") + } + + playerTask = Task { + while let player = audioPlayer { + player.play() + do { + try await Task.sleep(nanoseconds: UInt64((player.duration * 1_000_000_000) + delayMs * 1_000_000)) + } catch { + break + } + } + } + } + + func startConnectingCallSound() { + start("connecting_call", delayMs: 0) + } + + func startInCallSound() { + // Taken from https://github.com/TelegramOrg/Telegram-Android + // https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE + start("in_call", delayMs: 1000) + } + + func stop() { + playerTask.cancel() + audioPlayer?.stop() + audioPlayer = nil + } + + func vibrate(long: Bool) { + // iOS just don't want to vibrate more than once after a short period of time, and all 'styles' feel the same + if long { + AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) + } else { + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + } + } +} diff --git a/apps/ios/sounds/connecting_call.mp3 b/apps/ios/sounds/connecting_call.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..fc425bab97663f579a8274758bb5a7a580441b70 GIT binary patch literal 7104 zcmeZtF=l1}0WmO%LHvKLZ~!RC6a$jk!m^QpVNwnQ!>a#O2S9rQVfJ7e&Z6MNW3YmQ z`HA`j{^$0Oo+z}<-`&_?AjZb(FeCFp@|C3jrOVg^oi9k7<2{ftg|)F^gC850gZxVo zcWKGljVoCl*hJ@7$VMkD0QqMP6VyKmk*^!?&&pz3c_jb;gCriq#^wSov1zIdKUf>% z|Nor1b9oZOwo@uA7NwYM+S}pu=a4{K(9GP~eL}^-ZgZDyIvgxgz{{cZ?Cy=!zrEgT z%hseB>podJW%`x_3MvdBd;W_ZV2WXM0Qzc%2>T2T!5J|UGb&tro7F@ZGQM;koun67 zF-5Z6(%|j9Z2iAarv&#$9)N=^4U zw^=pMDS(BG>xzSdp^4?BKrxU#@1z>5sv-}*VE(n`+U_4-i$ia6#j6J&W8th%51*Jh z%`$bmWkpnWkwBx=nVBnCrn5Rud^yqi((6dk|5N{;0VW^5m(zTBCBJ`ux8d*eHP*or zd9@enb@sj4_)f+vW6ScU^*^fxxv9Fz?cU)F`HWSfG8?A{KuMt{Xr2LH67drNKl438U?7#|05r(P`u*>gwYW8P#5Hj{}8dwtTL@xk~jbF>G@BA`uTNPt0m2y)-Nn%-!oXxw4+4T>m#m(!Qa%8zZ zzZa|0;$0`@E1wmrPCR9}&Cir!!)~4(i4vi#f41KQ*>hPkF{@gFrDB50x>py2LN`uI zzBYNLIx~w*pm)FK&gog7H_dD-Wzk8`HB4H2Guw26>&ru{Ivl2UsU1n}|I)=+KE0bK z#PzQ8q~@ik;;ndQ9?b07Wvw`sznHV=%+8rth1MCcHk}07b3!6AYqA7)$pmGUQ}e7U zR(tO5+)}YzCt(G<&Y4MN^Ol8gK3soC`_g%PK53f-t&S~B;>Nu}%q;!P3l^>3vh3^f ztsPpjt@|g)csYx=DPOv2-SPYQqD>Y@jz))ZGceQ{`<|WuX%WbtgW`;7%@W*Y6W9M= zeZD@qa^uP!Gqj8}_!{-D2n$R+omsHGFZx+{uHD+s#tkl?*jbJ(NIjv(qL6SgQE!Uq zQmgzUt^X?GkMF!&yZF;7jVo>v&G%V6CF`5F;`gZp4HVrkc@k!weH*L`K7nm{7u0GqS=BG|EU702)_@ Ok6Tj3MC^vp%N_vuAz$nO literal 0 HcmV?d00001 diff --git a/apps/ios/sounds/in_call.mp3 b/apps/ios/sounds/in_call.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..1049be4462aa3b39240ffc4e958179748250b2ab GIT binary patch literal 63425 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

v^nb|m1 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 literal 0 HcmV?d00001