From 36fa752ccf981a9e3036eef176a422440f6ffb06 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 2 Dec 2025 16:35:36 +0100 Subject: [PATCH] Add seeking support to the audio record player --- .../ViewModel/ConversationViewModel.swift | 15 ++++++ .../RecordingMediaPlayerFragment.swift | 50 +++++++++++++++---- .../RecordingMediaPlayerViewModel.swift | 8 +++ LinphoneApp.xcodeproj/project.pbxproj | 8 +-- .../xcshareddata/swiftpm/Package.resolved | 2 +- 5 files changed, 69 insertions(+), 14 deletions(-) diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index d944cadc2..b3ed8b785 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -3051,6 +3051,21 @@ class VoiceRecordPlayerManager { } } + func seekVoiceRecordPlayer(percent: Double) { + guard !isPlayerClosed(), + let player = voiceRecordPlayer, + player.duration > 0 else { return } + + let clamped = max(0, min(percent, 100)) + + let ratio = clamped / 100.0 + + let timeMs = Int(Double(player.duration) * ratio) + + print("Seek voice record to \(clamped)% (\(timeMs) ms)") + try? player.seek(timeMs: timeMs) + } + func getSpeakerSoundCard(core: Core) -> String? { var speakerCard: String? = nil var earpieceCard: String? = nil diff --git a/Linphone/UI/Main/Recordings/Fragments/RecordingMediaPlayerFragment.swift b/Linphone/UI/Main/Recordings/Fragments/RecordingMediaPlayerFragment.swift index 6d7d90eca..ef2ab34e7 100644 --- a/Linphone/UI/Main/Recordings/Fragments/RecordingMediaPlayerFragment.swift +++ b/Linphone/UI/Main/Recordings/Fragments/RecordingMediaPlayerFragment.swift @@ -27,9 +27,14 @@ struct RecordingMediaPlayerFragment: View { @State private var showShareSheet: Bool = false @State private var showPicker: Bool = false + @State private var isSeeking: Bool = false + + @State private var lastValue: Double = -1.0 @State private var value: Double = 40.0 + @State private var timer: Timer? + init(recording: RecordingModel) { _recordingMediaPlayerViewModel = StateObject(wrappedValue: RecordingMediaPlayerViewModel(recording: recording)) } @@ -133,19 +138,41 @@ struct RecordingMediaPlayerFragment: View { .frame(width: 50) let radius = geometry.size.height * 0.5 + let barWidth = geometry.size.width - 120 ZStack(alignment: .leading) { Rectangle() .foregroundColor(Color.orangeMain100) - .frame(width: (geometry.size.width - 120), height: 5) + .frame(width: barWidth, height: 5) .clipShape(RoundedRectangle(cornerRadius: radius)) - + Rectangle() .foregroundColor(Color.orangeMain500) - .frame(width: self.value * (geometry.size.width - 120) / 100, height: 5) - .animation(self.value > 0 ? .linear(duration: 0.1) : nil, value: self.value) + .frame(width: (self.value / 100) * barWidth, height: isSeeking ? 10 : 5) .clipShape(RoundedRectangle(cornerRadius: radius)) } - .clipShape(RoundedRectangle(cornerRadius: radius)) + .frame(width: barWidth, height: 20) + .contentShape(Rectangle()) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { gesture in + isSeeking = true + + let x = max(0, min(barWidth, gesture.location.x)) + let percent = x / barWidth * 100 + self.value = percent + } + .onEnded { gesture in + let x = max(0, min(barWidth, gesture.location.x)) + let percent = x / barWidth * 100 + self.value = percent + + isSeeking = false + recordingMediaPlayerViewModel.seekTo(percent: percent) + + lastValue = percent + } + ) + Text(recordingMediaPlayerViewModel.recording.formattedDuration) .default_text_style_white_600(styleSize: 18) @@ -153,6 +180,7 @@ struct RecordingMediaPlayerFragment: View { .lineLimit(1) .frame(width: 70) } + .padding(.bottom, 20) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.black) @@ -184,25 +212,29 @@ struct RecordingMediaPlayerFragment: View { private func playProgress() { timer?.invalidate() - var lastValue = -1.0 + lastValue = -1.0 value = recordingMediaPlayerViewModel.getPositionVoiceRecordPlayer() timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in let current = recordingMediaPlayerViewModel.getPositionVoiceRecordPlayer() - + + if isSeeking { + return + } + if !recordingMediaPlayerViewModel.isPlaying { self.value = current lastValue = current return } - + if current > lastValue { self.value = current lastValue = current return } - + recordingMediaPlayerViewModel.stopVoiceRecordPlayer() self.timer?.invalidate() self.value = 0 diff --git a/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift b/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift index a2b886b9a..c60de55be 100644 --- a/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift +++ b/Linphone/UI/Main/Recordings/ViewModel/RecordingMediaPlayerViewModel.swift @@ -101,4 +101,12 @@ class RecordingMediaPlayerViewModel: ObservableObject { } } } + + func seekTo(percent: Double) { + coreContext.doOnCoreQueue { _ in + if self.vrpManager != nil { + self.vrpManager!.seekVoiceRecordPlayer(percent: percent) + } + } + } } diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index 27006113f..210f2605a 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABC82ABC6FD700B41C10 /* CoreContext.swift */; }; D719ABCC2ABC769C00B41C10 /* AssistantView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABCB2ABC769C00B41C10 /* AssistantView.swift */; }; D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719ABCE2ABC779A00B41C10 /* AccountLoginViewModel.swift */; }; + D719EF892EDF4AFA00509AAB /* GeneratedGit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D719EF882EDF4AFA00509AAB /* GeneratedGit.swift */; }; D71A0E192B485ADF0002C6CD /* ViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71A0E182B485ADF0002C6CD /* ViewExtension.swift */; }; D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */; }; D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */; }; @@ -180,7 +181,6 @@ D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */; }; D7D1F5262EDD91B30034EEB0 /* RecordingMediaPlayerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1F5252EDD91B10034EEB0 /* RecordingMediaPlayerFragment.swift */; }; D7D1F5282EDD939E0034EEB0 /* RecordingMediaPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1F5272EDD939D0034EEB0 /* RecordingMediaPlayerViewModel.swift */; }; - D7D1F5452EDDBBA70034EEB0 /* GeneratedGit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1F5442EDDBBA70034EEB0 /* GeneratedGit.swift */; }; D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */; }; D7D24D142AC1B4E800C6F35B /* NotoSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */; }; D7D24D152AC1B4E800C6F35B /* NotoSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */; }; @@ -324,6 +324,7 @@ D719ABC82ABC6FD700B41C10 /* CoreContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreContext.swift; sourceTree = ""; }; D719ABCB2ABC769C00B41C10 /* AssistantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantView.swift; sourceTree = ""; }; D719ABCE2ABC779A00B41C10 /* AccountLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLoginViewModel.swift; sourceTree = ""; }; + D719EF882EDF4AFA00509AAB /* GeneratedGit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedGit.swift; sourceTree = ""; }; D71A0E182B485ADF0002C6CD /* ViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtension.swift; sourceTree = ""; }; D71C266F2E819A0D001A7F92 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = Localizable/sk.lproj/Localizable.strings; sourceTree = ""; }; D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListFragment.swift; sourceTree = ""; }; @@ -419,7 +420,6 @@ D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = ""; }; D7D1F5252EDD91B10034EEB0 /* RecordingMediaPlayerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingMediaPlayerFragment.swift; sourceTree = ""; }; D7D1F5272EDD939D0034EEB0 /* RecordingMediaPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingMediaPlayerViewModel.swift; sourceTree = ""; }; - D7D1F5442EDDBBA70034EEB0 /* GeneratedGit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedGit.swift; sourceTree = ""; }; D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = ""; }; D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = ""; }; D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Light.ttf"; sourceTree = ""; }; @@ -674,7 +674,7 @@ D719ABBD2ABC67BF00B41C10 /* Preview Content */, D7D24D0C2AC1B4C700C6F35B /* Fonts */, D7ADF6012AFE5C7C00212231 /* Ressources */, - D7D1F5442EDDBBA70034EEB0 /* GeneratedGit.swift */, + D719EF882EDF4AFA00509AAB /* GeneratedGit.swift */, ); path = Linphone; sourceTree = ""; @@ -1371,6 +1371,7 @@ D7C2DA1D2CA44DE400A2441B /* EventModel.swift in Sources */, D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */, D70A26F22B7F5D95006CC8FC /* ConversationFragment.swift in Sources */, + D719EF892EDF4AFA00509AAB /* GeneratedGit.swift in Sources */, D7D1F5282EDD939E0034EEB0 /* RecordingMediaPlayerViewModel.swift in Sources */, D7DC096F2CFA1D7600A6D47C /* AccountProfileFragment.swift in Sources */, D717A10E2CEB772300849D92 /* ShareSheetController.swift in Sources */, @@ -1419,7 +1420,6 @@ C628172E2C1C3A3600DBA646 /* AccountExtension.swift in Sources */, 66C491FF2B24D4AC00CEA16D /* FileUtils.swift in Sources */, C62817322C1C400A00DBA646 /* StringExtension.swift in Sources */, - D7D1F5452EDDBBA70034EEB0 /* GeneratedGit.swift in Sources */, D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */, D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */, D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */, diff --git a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 197fc9498..4ca9347f3 100644 --- a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -124,7 +124,7 @@ "location" : "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git", "state" : { "branch" : "alpha", - "revision" : "43ee1a062ef73808e27afe3c5341a27c1b82aae7" + "revision" : "4403eb00e8843352d9d4ca4e696c2824e53dd178" } }, {