diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/Contents.json new file mode 100644 index 000000000..f00919cff --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "voice_broadcast_spinner.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/voice_broadcast_spinner.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/voice_broadcast_spinner.svg new file mode 100644 index 000000000..83abf0805 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/voice_broadcast_spinner.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index e9295cc9e..776b5e5bd 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2203,6 +2203,7 @@ Tap the + to start adding people."; "voice_broadcast_live" = "Live"; "voice_broadcast_tile" = "Voice broadcast"; "voice_broadcast_time_left" = "%@ left"; +"voice_broadcast_buffering" = "Buffering..."; // Mark: - Version check diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index dcf78a2e1..11839848c 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -344,6 +344,7 @@ internal class Asset: NSObject { internal static let voiceBroadcastPlay = ImageAsset(name: "voice_broadcast_play") internal static let voiceBroadcastRecord = ImageAsset(name: "voice_broadcast_record") internal static let voiceBroadcastRecordPause = ImageAsset(name: "voice_broadcast_record_pause") + internal static let voiceBroadcastSpinner = ImageAsset(name: "voice_broadcast_spinner") internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop") internal static let voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live") internal static let voiceBroadcastTileMic = ImageAsset(name: "voice_broadcast_tile_mic") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index f773146b2..5f29c832a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9139,6 +9139,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastBlockedBySomeoneElseMessage: String { return VectorL10n.tr("Vector", "voice_broadcast_blocked_by_someone_else_message") } + /// Buffering... + public static var voiceBroadcastBuffering: String { + return VectorL10n.tr("Vector", "voice_broadcast_buffering") + } /// Live public static var voiceBroadcastLive: String { return VectorL10n.tr("Vector", "voice_broadcast_live") diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index 0d17cc35f..a07ff8fed 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -137,6 +137,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic if let audioPlayer = audioPlayer, audioPlayer.isPlaying { audioPlayer.pause() + } else { + state.playbackState = .paused + state.playingState.isLive = false } } @@ -196,10 +199,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return } - if (isActuallyPaused == false && state.playbackState == .paused) { - state.playbackState = .buffering - } - guard !isProcessingVoiceBroadcastChunk else { // Chunks caching is already in progress return @@ -233,41 +232,41 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic self.voiceBroadcastAttachmentCacheManagerLoadResults.append(result) - if let audioPlayer = self.audioPlayer { - // Append the chunk to the current playlist - audioPlayer.addContentFromURL(result.url) - - if let time = self.seekToChunkTime { - audioPlayer.seekToTime(time) - self.seekToChunkTime = nil - } - - // Resume the player. Needed after a buffering - if self.state.playbackState == .buffering { - if audioPlayer.isPlaying == false { - MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Resume the player") - self.displayLink.isPaused = false - audioPlayer.play() - } else { - self.state.playbackState = .playing - self.state.playingState.isLive = self.isLivePlayback - } - } - } else { + // Instanciate audioPlayer if needed. + if self.audioPlayer == nil { // Init and start the player on the first chunk let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) audioPlayer.registerDelegate(self) audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName) - self.displayLink.isPaused = false - audioPlayer.play() - if let time = self.seekToChunkTime { - audioPlayer.seekToTime(time) - self.seekToChunkTime = nil - } self.audioPlayer = audioPlayer + } else { + // Append the chunk to the current playlist + self.audioPlayer?.addContentFromURL(result.url) } + guard let audioPlayer = self.audioPlayer else { + MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: audioPlayer is nil !") + return + } + + // Start or Resume the player. Needed after a buffering + if self.state.playbackState == .buffering { + if audioPlayer.isPlaying == false { + MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Start or Resume the player") + self.displayLink.isPaused = false + audioPlayer.play() + } else { + self.state.playbackState = .playing + self.state.playingState.isLive = self.isLivePlayback + } + } + + if let time = self.seekToChunkTime { + audioPlayer.seekToTime(time) + self.seekToChunkTime = nil + } + case .failure (let error): MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error) if self.voiceBroadcastChunkQueue.count == 0 { @@ -317,6 +316,10 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic MXLog.debug("[VoiceBroadcastPlaybackViewModel] didSliderChanged: restart to time: \(state.bindings.progress) milliseconds") let time = state.bindings.progress - state.playingState.duration + Float(chunksDuration) seekToChunkTime = TimeInterval(time / 1000) + // Check the condition to resume the playback when data will be ready (after the chunk process). + if state.playbackState != .stopped, isActuallyPaused == false { + state.playbackState = .buffering + } processPendingVoiceBroadcastChunks() } } @@ -380,7 +383,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { updateDuration() - if state.playbackState != .stopped { + if state.playbackState != .stopped, !isActuallyPaused { handleVoiceBroadcastChunksProcessing() } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index c06d74976..c518f7e59 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -30,6 +30,7 @@ struct VoiceBroadcastPlaybackView: View { // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI + @State private var bufferingSpinnerRotationValue = 0.0 private var backgroundColor: Color { if viewModel.viewState.playingState.isLive { @@ -61,12 +62,33 @@ struct VoiceBroadcastPlaybackView: View { } icon: { Image(uiImage: Asset.Images.voiceBroadcastTileMic.image) } - Label { - Text(VectorL10n.voiceBroadcastTile) - .foregroundColor(theme.colors.secondaryContent) - .font(theme.fonts.caption1) - } icon: { - Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) + if viewModel.viewState.playbackState != .buffering { + Label { + Text(VectorL10n.voiceBroadcastTile) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) + } + } else { + Label { + Text(VectorL10n.voiceBroadcastBuffering) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastSpinner.image) + .frame(width: 16.0, height: 16.0) + .rotationEffect(Angle.degrees(bufferingSpinnerRotationValue)) + .onAppear { + let baseAnimation = Animation.linear(duration: 1.0).repeatForever(autoreverses: false) + withAnimation(baseAnimation) { + bufferingSpinnerRotationValue = 360.0 + } + } + .onDisappear { + bufferingSpinnerRotationValue = 0.0 + } + } } }.frame(maxWidth: .infinity, alignment: .leading) @@ -89,13 +111,13 @@ struct VoiceBroadcastPlaybackView: View { VoiceBroadcastPlaybackErrorView() } else { ZStack { - if viewModel.viewState.playbackState == .playing { + if viewModel.viewState.playbackState == .playing || viewModel.viewState.playbackState == .buffering { Button { viewModel.send(viewAction: .pause) } label: { Image(uiImage: Asset.Images.voiceBroadcastPause.image) .renderingMode(.original) } .accessibilityIdentifier("pauseButton") - } else { + } else { Button { viewModel.send(viewAction: .play) } label: { Image(uiImage: Asset.Images.voiceBroadcastPlay.image) .renderingMode(.original) @@ -104,7 +126,6 @@ struct VoiceBroadcastPlaybackView: View { .accessibilityIdentifier("playButton") } } - .activityIndicator(show: viewModel.viewState.playbackState == .buffering) } Slider(value: $viewModel.progress, in: 0...viewModel.viewState.playingState.duration) { diff --git a/changelog.d/pr-7125.change b/changelog.d/pr-7125.change new file mode 100644 index 000000000..7f9b224af --- /dev/null +++ b/changelog.d/pr-7125.change @@ -0,0 +1 @@ +Labs: VoiceBroadcast: Be able to pause the playback when it is buffering