mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
ios: sound prompts and vibration during calls (#4005)
* ios: sound prompts and vibration * awaiting call receipt * update --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
parent
d8b52ee0d3
commit
e560b49d14
5 changed files with 104 additions and 0 deletions
|
@ -1469,6 +1469,8 @@ class ChatReceiver {
|
||||||
private var receiveMessages = true
|
private var receiveMessages = true
|
||||||
private var _lastMsgTime = Date.now
|
private var _lastMsgTime = Date.now
|
||||||
|
|
||||||
|
var messagesChannel: ((ChatResponse) -> Void)? = nil
|
||||||
|
|
||||||
static let shared = ChatReceiver()
|
static let shared = ChatReceiver()
|
||||||
|
|
||||||
var lastMsgTime: Date { get { _lastMsgTime } }
|
var lastMsgTime: Date { get { _lastMsgTime } }
|
||||||
|
@ -1486,6 +1488,9 @@ class ChatReceiver {
|
||||||
if let msg = await chatRecvMsg() {
|
if let msg = await chatRecvMsg() {
|
||||||
self._lastMsgTime = .now
|
self._lastMsgTime = .now
|
||||||
await processReceivedMsg(msg)
|
await processReceivedMsg(msg)
|
||||||
|
if let messagesChannel {
|
||||||
|
messagesChannel(msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = try? await Task.sleep(nanoseconds: 7_500_000)
|
_ = try? await Task.sleep(nanoseconds: 7_500_000)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WebKit
|
import WebKit
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
struct ActiveCallView: View {
|
struct ActiveCallView: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@ -21,6 +22,7 @@ struct ActiveCallView: View {
|
||||||
@Binding var canConnectCall: Bool
|
@Binding var canConnectCall: Bool
|
||||||
@State var prevColorScheme: ColorScheme = .dark
|
@State var prevColorScheme: ColorScheme = .dark
|
||||||
@State var pipShown = false
|
@State var pipShown = false
|
||||||
|
@State var wasConnected = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
|
@ -69,6 +71,11 @@ struct ActiveCallView: View {
|
||||||
Task { await m.callCommand.setClient(nil) }
|
Task { await m.callCommand.setClient(nil) }
|
||||||
AppDelegate.keepScreenOn(false)
|
AppDelegate.keepScreenOn(false)
|
||||||
client?.endCall()
|
client?.endCall()
|
||||||
|
CallSoundsPlayer.shared.stop()
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
|
||||||
|
if (wasConnected) {
|
||||||
|
CallSoundsPlayer.shared.vibrate(long: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.background(m.activeCallViewIsCollapsed ? .clear : .black)
|
.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
|
// 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.callState = .invitationSent
|
||||||
call.localCapabilities = capabilities
|
call.localCapabilities = capabilities
|
||||||
}
|
}
|
||||||
|
if call.supportsVideo {
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .defaultToSpeaker)
|
||||||
|
}
|
||||||
|
CallSoundsPlayer.shared.startConnectingCallSound()
|
||||||
|
activeCallWaitDeliveryReceipt()
|
||||||
}
|
}
|
||||||
case let .offer(offer, iceCandidates, capabilities):
|
case let .offer(offer, iceCandidates, capabilities):
|
||||||
Task {
|
Task {
|
||||||
|
@ -126,6 +138,8 @@ struct ActiveCallView: View {
|
||||||
}
|
}
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
call.callState = .negotiated
|
call.callState = .negotiated
|
||||||
|
CallSoundsPlayer.shared.stop()
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .ice(iceCandidates):
|
case let .ice(iceCandidates):
|
||||||
|
@ -144,6 +158,10 @@ struct ActiveCallView: View {
|
||||||
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
|
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
|
||||||
call.callState = .connected
|
call.callState = .connected
|
||||||
call.connectedAt = .now
|
call.connectedAt = .now
|
||||||
|
if !wasConnected {
|
||||||
|
CallSoundsPlayer.shared.vibrate(long: false)
|
||||||
|
wasConnected = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if state.connectionState == "closed" {
|
if state.connectionState == "closed" {
|
||||||
closeCallView(client)
|
closeCallView(client)
|
||||||
|
@ -161,6 +179,10 @@ struct ActiveCallView: View {
|
||||||
call.callState = .connected
|
call.callState = .connected
|
||||||
call.connectionInfo = connectionInfo
|
call.connectionInfo = connectionInfo
|
||||||
call.connectedAt = .now
|
call.connectedAt = .now
|
||||||
|
if !wasConnected {
|
||||||
|
CallSoundsPlayer.shared.vibrate(long: false)
|
||||||
|
wasConnected = true
|
||||||
|
}
|
||||||
case .ended:
|
case .ended:
|
||||||
closeCallView(client)
|
closeCallView(client)
|
||||||
call.callState = .ended
|
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) {
|
private func closeCallView(_ client: WebRTCClient) {
|
||||||
if m.activeCall != nil {
|
if m.activeCall != nil {
|
||||||
m.showCallView = false
|
m.showCallView = false
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
class SoundPlayer {
|
class SoundPlayer {
|
||||||
static let shared = SoundPlayer()
|
static let shared = SoundPlayer()
|
||||||
|
@ -43,3 +44,63 @@ class SoundPlayer {
|
||||||
audioPlayer = nil
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
BIN
apps/ios/sounds/connecting_call.mp3
Normal file
BIN
apps/ios/sounds/connecting_call.mp3
Normal file
Binary file not shown.
BIN
apps/ios/sounds/in_call.mp3
Normal file
BIN
apps/ios/sounds/in_call.mp3
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue