diff --git a/Linphone/GeneratedGit.swift b/Linphone/GeneratedGit.swift
index 318d5e872..d6bcab926 100644
--- a/Linphone/GeneratedGit.swift
+++ b/Linphone/GeneratedGit.swift
@@ -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"
}
diff --git a/Linphone/Ressources/Sounds/incoming_chat.wav b/Linphone/Ressources/Sounds/incoming_chat.wav
new file mode 100644
index 000000000..99a2e7dfc
Binary files /dev/null and b/Linphone/Ressources/Sounds/incoming_chat.wav differ
diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift
index da4300cd9..21cb870bd 100644
--- a/Linphone/UI/Call/ViewModel/CallViewModel.swift
+++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift
@@ -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()
diff --git a/Linphone/UI/Call/ViewModel/MeetingWaitingRoomViewModel.swift b/Linphone/UI/Call/ViewModel/MeetingWaitingRoomViewModel.swift
index e66ee806d..858cc91f1 100644
--- a/Linphone/UI/Call/ViewModel/MeetingWaitingRoomViewModel.swift
+++ b/Linphone/UI/Call/ViewModel/MeetingWaitingRoomViewModel.swift
@@ -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)")
}
}
diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift
index f93219291..27ba3099c 100644
--- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift
+++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift
@@ -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()
diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift
index 2bee336e6..ba58dd36f 100644
--- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift
+++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift
@@ -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)")
}
}
}
diff --git a/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift b/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift
index c60de55be..6b42e7022 100644
--- a/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift
+++ b/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift
@@ -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 {
diff --git a/Linphone/Utils/AudioMode.swift b/Linphone/Utils/AudioMode.swift
new file mode 100644
index 000000000..279799ed3
--- /dev/null
+++ b/Linphone/Utils/AudioMode.swift
@@ -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 .
+ */
+
+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)
+}
diff --git a/Linphone/Utils/SoundPlayer.swift b/Linphone/Utils/SoundPlayer.swift
new file mode 100644
index 000000000..5747dd5ab
--- /dev/null
+++ b/Linphone/Utils/SoundPlayer.swift
@@ -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 .
+ */
+
+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)
+ }
+ }
+}
diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj
index 5ef2593a0..9f6ef6b58 100644
--- a/LinphoneApp.xcodeproj/project.pbxproj
+++ b/LinphoneApp.xcodeproj/project.pbxproj
@@ -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 = ""; };
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewModel.swift; sourceTree = ""; };
D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; };
+ D7BC10D32F4EF18100F09BDA /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; };
+ D7BC10D62F4EF25400F09BDA /* incoming_chat.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = incoming_chat.wav; sourceTree = ""; };
+ D7BC10D82F4EF64E00F09BDA /* AudioMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMode.swift; sourceTree = ""; };
D7C2DA1C2CA44DE400A2441B /* EventModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = ""; };
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = ""; };
D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = ""; };
@@ -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 = "";
};
+ D7BC10D52F4EF21D00F09BDA /* Sounds */ = {
+ isa = PBXGroup;
+ children = (
+ D7BC10D62F4EF25400F09BDA /* incoming_chat.wav */,
+ );
+ path = Sounds;
+ sourceTree = "";
+ };
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 */,