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 */,