Refactor audio session management and add incoming message notification sound

This commit is contained in:
Benoit Martins 2026-02-25 15:05:47 +01:00
parent db9c9f1834
commit 9cc8923e3f
10 changed files with 185 additions and 24 deletions

View file

@ -2,6 +2,6 @@ import Foundation
public enum AppGitInfo {
public static let branch = "master"
public static let commit = "ef09f6c41"
public static let commit = "db9c9f183"
public static let tag = "6.1.0-alpha"
}

Binary file not shown.

View file

@ -95,9 +95,9 @@ class CallViewModel: ObservableObject {
init() {
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
} catch _ {
try configureAudio(.call)
} catch {
print("Audio session error: \(error)")
}
NotificationCenter.default.addObserver(forName: Notification.Name("CallViewModelReset"), object: nil, queue: nil) { notification in
self.resetCallView()

View file

@ -39,9 +39,9 @@ class MeetingWaitingRoomViewModel: ObservableObject {
init() {
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
} catch _ {
try configureAudio(.call)
} catch {
print("Audio session error: \(error)")
}
if !telecomManager.callStarted {
self.resetMeetingRoomView()
@ -51,9 +51,9 @@ class MeetingWaitingRoomViewModel: ObservableObject {
func resetMeetingRoomView() {
if self.telecomManager.meetingWaitingRoomSelected != nil {
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
} catch _ {
try configureAudio(.call)
} catch {
print("Audio session error: \(error)")
}
coreContext.doOnCoreQueue { core in
@ -212,9 +212,9 @@ class MeetingWaitingRoomViewModel: ObservableObject {
func enableAVAudioSession() {
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch _ {
try configureAudio(.call)
} catch {
print("Audio session error: \(error)")
}
}

View file

@ -1958,7 +1958,11 @@ struct VoiceRecorderPlayer: View {
.padding(.horizontal, 4)
.padding(.vertical, 5)
.onAppear {
self.audioRecorder.startRecording()
conversationViewModel.isRecording = isRecording
audioRecorder.startRecording()
}
.onChange(of: isRecording) { newValue in
conversationViewModel.isRecording = newValue
}
.onDisappear {
self.audioRecorder.stopVoiceRecorder()

View file

@ -99,7 +99,7 @@ class ConversationViewModel: ObservableObject {
var vrpManager: VoiceRecordPlayerManager?
@Published var isPlaying = false
@Published var progress: Double = 0.0
@Published var isRecording = false
@Published var attachments: [Attachment] = []
@Published var attachmentTransferInProgress: Attachment?
@ -1511,6 +1511,10 @@ class ConversationViewModel: ObservableObject {
if !eventLogMessage.message.isOutgoing {
self.displayedConversationUnreadMessagesCount = unreadMessagesCount
if !self.isPlaying && !self.isRecording {
SoundPlayer.shared.playIncomingMessage()
}
}
}
}
@ -2613,11 +2617,14 @@ class ConversationViewModel: ObservableObject {
func startVoiceRecordPlayer(voiceRecordPath: URL) {
coreContext.doOnCoreQueue { core in
if self.vrpManager == nil || self.vrpManager!.voiceRecordPath != voiceRecordPath {
self.vrpManager = VoiceRecordPlayerManager(core: core, voiceRecordPath: voiceRecordPath)
self.vrpManager = VoiceRecordPlayerManager(core: core, voiceRecordPath: voiceRecordPath, isPlaying: self.isPlaying)
}
if self.vrpManager != nil {
self.vrpManager!.startVoiceRecordPlayer()
DispatchQueue.main.async {
self.isPlaying = true
}
}
}
}
@ -2642,6 +2649,9 @@ class ConversationViewModel: ObservableObject {
coreContext.doOnCoreQueue { _ in
if self.vrpManager != nil {
self.vrpManager!.pauseVoiceRecordPlayer()
DispatchQueue.main.async {
self.isPlaying = false
}
}
}
}
@ -2650,6 +2660,9 @@ class ConversationViewModel: ObservableObject {
coreContext.doOnCoreQueue { _ in
if self.vrpManager != nil {
self.vrpManager!.stopVoiceRecordPlayer()
DispatchQueue.main.async {
self.isPlaying = false
}
}
}
}
@ -3384,9 +3397,12 @@ class VoiceRecordPlayerManager {
//private var voiceRecordPlayerPosition: Double = 0
//private var voiceRecordingDuration: TimeInterval = 0
init(core: Core, voiceRecordPath: URL) {
@State var isPlaying: Bool
init(core: Core, voiceRecordPath: URL, isPlaying: Bool) {
self.core = core
self.voiceRecordPath = voiceRecordPath
self.isPlaying = isPlaying
}
private func initVoiceRecordPlayer() {
@ -3412,7 +3428,11 @@ class VoiceRecordPlayerManager {
if voiceRecordAudioFocusRequest == nil {
voiceRecordAudioFocusRequest = AVAudioSession.sharedInstance()
if let request = voiceRecordAudioFocusRequest {
try? request.setActive(true)
do {
try configureAudio(.voiceMessage)
} catch {
print("Audio session error: \(error)")
}
}
}
@ -3428,6 +3448,7 @@ class VoiceRecordPlayerManager {
}
do {
self.isPlaying = true
try voiceRecordPlayer!.start()
print("Playing voice record")
} catch {
@ -3445,8 +3466,9 @@ class VoiceRecordPlayerManager {
func pauseVoiceRecordPlayer() {
if !isPlayerClosed() {
print("Pausing voice record")
self.isPlaying = false
try? voiceRecordPlayer?.pause()
print("Pausing voice record")
}
}
@ -3456,10 +3478,11 @@ class VoiceRecordPlayerManager {
func stopVoiceRecordPlayer() {
if !isPlayerClosed() {
print("Stopping voice record")
self.isPlaying = false
try? voiceRecordPlayer?.pause()
try? voiceRecordPlayer?.seek(timeMs: 0)
voiceRecordPlayer?.close()
print("Stopping voice record")
}
if let request = voiceRecordAudioFocusRequest {
@ -3523,8 +3546,8 @@ class AudioRecorder: NSObject, ObservableObject {
if recordingSession != nil {
do {
try recordingSession!.setCategory(.playAndRecord, mode: .default)
try recordingSession!.setActive(true)
try configureAudio(.recording)
recordingSession!.requestRecordPermission { allowed in
if allowed {
self.initVoiceRecorder()
@ -3533,7 +3556,7 @@ class AudioRecorder: NSObject, ObservableObject {
}
}
} catch {
print("Failed to setup recording session.")
print("Audio session error: \(error)")
}
}
}

View file

@ -41,7 +41,7 @@ class RecordingMediaPlayerViewModel: ObservableObject {
func startVoiceRecordPlayer(voiceRecordPath: URL) {
coreContext.doOnCoreQueue { core in
if self.vrpManager == nil || self.vrpManager!.voiceRecordPath != voiceRecordPath {
self.vrpManager = VoiceRecordPlayerManager(core: core, voiceRecordPath: voiceRecordPath)
self.vrpManager = VoiceRecordPlayerManager(core: core, voiceRecordPath: voiceRecordPath, isPlaying: self.isPlaying)
}
if self.vrpManager != nil {

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2010-2025 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import AVFoundation
enum AudioMode {
case notification
case voiceMessage
case recording
case call
}
func configureAudio(_ mode: AudioMode) throws {
let session = AVAudioSession.sharedInstance()
switch mode {
case .notification:
try session.setCategory(
.ambient,
mode: .default,
options: [.mixWithOthers]
)
case .voiceMessage:
try session.setCategory(
.playback,
mode: .spokenAudio,
options: [.allowBluetoothHFP, .mixWithOthers]
)
case .recording:
try session.setCategory(
.playAndRecord,
mode: .default,
options: [.allowBluetoothHFP, .defaultToSpeaker, .mixWithOthers]
)
case .call:
try session.setCategory(
.playAndRecord,
mode: .voiceChat,
options: [.allowBluetoothHFP]
)
}
try session.setActive(true)
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2010-2025 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import AVFoundation
final class SoundPlayer {
static let shared = SoundPlayer()
private var player: AVAudioPlayer?
func playIncomingMessage() {
guard let url = Bundle.main.url(
forResource: "incoming_chat",
withExtension: "wav"
) else {
print("Sound not found")
return
}
do {
try configureAudio(.notification)
} catch {
print("Audio session error: \(error)")
}
do {
player = try AVAudioPlayer(contentsOf: url)
player?.prepareToPlay()
player?.play()
} catch {
print("Audio error:", error)
}
}
}

View file

@ -173,6 +173,9 @@
D7B5678E2B28888F00DE63EB /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5678D2B28888F00DE63EB /* CallView.swift */; };
D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */; };
D7B99E9B2B29F7C300BE7BF2 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */; };
D7BC10D42F4EF18600F09BDA /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BC10D32F4EF18100F09BDA /* SoundPlayer.swift */; };
D7BC10D72F4EF25400F09BDA /* incoming_chat.wav in Resources */ = {isa = PBXBuildFile; fileRef = D7BC10D62F4EF25400F09BDA /* incoming_chat.wav */; };
D7BC10D92F4EF65900F09BDA /* AudioMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BC10D82F4EF64E00F09BDA /* AudioMode.swift */; };
D7C2DA1D2CA44DE400A2441B /* EventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C2DA1C2CA44DE400A2441B /* EventModel.swift */; };
D7C365082AEFAB7F00FE6142 /* ContactListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */; };
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365092AF001C300FE6142 /* EditContactFragment.swift */; };
@ -439,6 +442,9 @@
D7B5678D2B28888F00DE63EB /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = "<group>"; };
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewModel.swift; sourceTree = "<group>"; };
D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
D7BC10D32F4EF18100F09BDA /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = "<group>"; };
D7BC10D62F4EF25400F09BDA /* incoming_chat.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = incoming_chat.wav; sourceTree = "<group>"; };
D7BC10D82F4EF64E00F09BDA /* AudioMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMode.swift; sourceTree = "<group>"; };
D7C2DA1C2CA44DE400A2441B /* EventModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = "<group>"; };
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = "<group>"; };
D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = "<group>"; };
@ -663,6 +669,8 @@
D717071C2AC591EF0037746F /* Utils */ = {
isa = PBXGroup;
children = (
D7BC10D82F4EF64E00F09BDA /* AudioMode.swift */,
D7BC10D32F4EF18100F09BDA /* SoundPlayer.swift */,
C642277A2E8E4AC50094FEDC /* ThemeManager.swift */,
D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */,
D7DF8BE82E2104E5003A3BC7 /* EmojiPickerView.swift */,
@ -1038,6 +1046,7 @@
D7ADF6012AFE5C7C00212231 /* Ressources */ = {
isa = PBXGroup;
children = (
D7BC10D52F4EF21D00F09BDA /* Sounds */,
D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */,
D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */,
D732A90A2B0376F500DB42BA /* linphonerc-default */,
@ -1068,6 +1077,14 @@
path = ViewModel;
sourceTree = "<group>";
};
D7BC10D52F4EF21D00F09BDA /* Sounds */ = {
isa = PBXGroup;
children = (
D7BC10D62F4EF25400F09BDA /* incoming_chat.wav */,
);
path = Sounds;
sourceTree = "<group>";
};
D7CEE0332B7A20A400FD79B7 /* Conversations */ = {
isa = PBXGroup;
children = (
@ -1362,6 +1379,7 @@
buildActionMask = 2147483647;
files = (
D7D24D142AC1B4E800C6F35B /* NotoSans-Regular.ttf in Resources */,
D7BC10D72F4EF25400F09BDA /* incoming_chat.wav in Resources */,
D7D24D182AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf in Resources */,
D7D24D152AC1B4E800C6F35B /* NotoSans-Light.ttf in Resources */,
D7FC8E4A2EBA12F90080C09D /* Launch Screen.storyboard in Resources */,
@ -1453,6 +1471,7 @@
files = (
D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */,
D795F57E2EC5F9500022C17D /* RecordingsListFragment.swift in Sources */,
D7BC10D92F4EF65900F09BDA /* AudioMode.swift in Sources */,
D7ADF6002AFE356400212231 /* Avatar.swift in Sources */,
D7CEE03B2B7A234200FD79B7 /* ConversationsFragment.swift in Sources */,
D71707202AC5989C0037746F /* TextExtension.swift in Sources */,
@ -1479,6 +1498,7 @@
D719EF892EDF4AFA00509AAB /* GeneratedGit.swift in Sources */,
D7D1F5282EDD939E0034EEB0 /* RecordingMediaPlayerViewModel.swift in Sources */,
D7DC096F2CFA1D7600A6D47C /* AccountProfileFragment.swift in Sources */,
D7BC10D42F4EF18600F09BDA /* SoundPlayer.swift in Sources */,
D717A10E2CEB772300849D92 /* ShareSheetController.swift in Sources */,
66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */,
D738ACEE2E857BF10039F7D1 /* KeyboardResponder.swift in Sources */,