diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift index 8987cb1de..f673bebee 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift @@ -27,7 +27,7 @@ class VoiceBroadcastPlaybackPlainCell: SizableBaseRoomCell, RoomCellReactionsDis let bubbleData = cellData as? RoomBubbleCellData, let event = bubbleData.events.last, let voiceBroadcastContent = VoiceBroadcastInfo(fromJSON: event.content), - voiceBroadcastContent.state == VoiceBroadcastInfo.State.started.rawValue, + voiceBroadcastContent.state == VoiceBroadcastInfoState.started.rawValue, let controller = VoiceBroadcastPlaybackProvider.shared.buildVoiceBroadcastPlaybackVCForEvent(event, senderDisplayName: bubbleData.senderDisplayName, voiceBroadcastState: bubbleData.voiceBroadcastState) diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift index a65254be5..43047cfba 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift @@ -28,7 +28,7 @@ class VoiceBroadcastRecorderPlainCell: SizableBaseRoomCell, RoomCellReactionsDis let bubbleData = cellData as? RoomBubbleCellData, let event = bubbleData.events.last, let voiceBroadcastContent = VoiceBroadcastInfo(fromJSON: event.content), - voiceBroadcastContent.state == VoiceBroadcastInfo.State.started.rawValue, + voiceBroadcastContent.state == VoiceBroadcastInfoState.started.rawValue, let view = VoiceBroadcastRecorderProvider.shared.buildVoiceBroadcastRecorderViewForEvent(event, senderDisplayName: bubbleData.senderDisplayName) else { return } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift index fb90d834d..3dd215f34 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift @@ -26,7 +26,7 @@ public protocol VoiceBroadcastAggregatorDelegate: AnyObject { func voiceBroadcastAggregatorDidEndLoading(_ aggregator: VoiceBroadcastAggregator) func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didFailWithError: Error) func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk) - func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfo.State) + func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState) func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) } @@ -57,7 +57,7 @@ public class VoiceBroadcastAggregator { } public private(set) var isStarted: Bool = false - public private(set) var voiceBroadcastState: VoiceBroadcastInfo.State + public private(set) var voiceBroadcastState: VoiceBroadcastInfoState public var delegate: VoiceBroadcastAggregatorDelegate? deinit { @@ -66,7 +66,7 @@ public class VoiceBroadcastAggregator { } } - public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String, voiceBroadcastState: VoiceBroadcastInfo.State) throws { + public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String, voiceBroadcastState: VoiceBroadcastInfoState) throws { self.session = session self.room = room self.voiceBroadcastStartEventId = voiceBroadcastStartEventId @@ -111,7 +111,7 @@ public class VoiceBroadcastAggregator { event.stateKey == self.voiceBroadcastSenderId, let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: event.content), (event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.voiceBroadcastId == self.voiceBroadcastStartEventId), - let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) else { + let state = VoiceBroadcastInfoState(rawValue: voiceBroadcastInfo.state) else { return } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift index b2bc1afe4..5e6218f29 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift @@ -19,36 +19,29 @@ import Foundation extension VoiceBroadcastInfo { // MARK: - Constants - public enum State: String { - case started - case paused - case resumed - case stopped - } - // MARK: - Public @objc static func isStarted(for name: String) -> Bool { - return name == State.started.rawValue + return name == VoiceBroadcastInfoState.started.rawValue } @objc static func isStopped(for name: String) -> Bool { - return name == State.stopped.rawValue + return name == VoiceBroadcastInfoState.stopped.rawValue } @objc static func startedValue() -> String { - return State.started.rawValue + return VoiceBroadcastInfoState.started.rawValue } @objc static func pausedValue() -> String { - return State.paused.rawValue + return VoiceBroadcastInfoState.paused.rawValue } @objc static func resumedValue() -> String { - return State.resumed.rawValue + return VoiceBroadcastInfoState.resumed.rawValue } @objc static func stoppedValue() -> String { - return State.stopped.rawValue + return VoiceBroadcastInfoState.stopped.rawValue } } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift new file mode 100644 index 000000000..e808ddeb3 --- /dev/null +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift @@ -0,0 +1,22 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +public enum VoiceBroadcastInfoState: String { + case started + case paused + case resumed + case stopped +} diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift index e6d6171a8..6a3072ec3 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift @@ -25,13 +25,13 @@ public class VoiceBroadcastService: NSObject { public let room: MXRoom public private(set) var voiceBroadcastId: String? - public private(set) var state: VoiceBroadcastInfo.State + public private(set) var state: VoiceBroadcastInfoState // Mechanism to process one call of sendVoiceBroadcastInfo() at a time private let asyncTaskQueue: MXAsyncTaskQueue // MARK: - Setup - public init(room: MXRoom, state: VoiceBroadcastInfo.State) { + public init(room: MXRoom, state: VoiceBroadcastInfoState) { self.room = room self.state = state self.asyncTaskQueue = MXAsyncTaskQueue(label: "VoiceBroadcastServiceQueueEventSerialQueue-" + MXTools.generateSecret()) @@ -47,7 +47,7 @@ public class VoiceBroadcastService: NSObject { /// - Parameters: /// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success. func startVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { - sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.started) { [weak self] response in + sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.started) { [weak self] response in guard let self = self else { return } switch response { @@ -64,21 +64,21 @@ public class VoiceBroadcastService: NSObject { /// - Parameters: /// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success. func pauseVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { - sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.paused, completion: completion) + sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.paused, completion: completion) } /// resume a voice broadcast. /// - Parameters: /// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success. func resumeVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { - sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.resumed, completion: completion) + sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.resumed, completion: completion) } /// stop a voice broadcast info. /// - Parameters: /// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success. func stopVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { - sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.stopped, completion: completion) + sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.stopped, completion: completion) } func getState() -> String { @@ -121,7 +121,7 @@ public class VoiceBroadcastService: NSObject { // MARK: - Private - private func allowedStates(from state: VoiceBroadcastInfo.State) -> [VoiceBroadcastInfo.State] { + private func allowedStates(from state: VoiceBroadcastInfoState) -> [VoiceBroadcastInfoState] { switch state { case .started: return [.paused, .stopped] @@ -134,7 +134,7 @@ public class VoiceBroadcastService: NSObject { } } - private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State, completion: @escaping (MXResponse) -> Void) { + private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState, completion: @escaping (MXResponse) -> Void) { guard let userId = self.room.mxSession.myUserId else { completion(.failure(VoiceBroadcastServiceError.missingUserId)) return @@ -156,7 +156,7 @@ public class VoiceBroadcastService: NSObject { voiceBroadcastInfo.state = state.rawValue - if state != VoiceBroadcastInfo.State.started { + if state != VoiceBroadcastInfoState.started { guard let voiceBroadcastId = self.voiceBroadcastId else { completion(.failure(VoiceBroadcastServiceError.notStarted)) taskCompleted() diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift index e39c838b7..83051e1d5 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift @@ -70,9 +70,9 @@ class VoiceBroadcastServiceProvider { } } - private func createVoiceBroadcastService(for room: MXRoom, state: VoiceBroadcastInfo.State) { + private func createVoiceBroadcastService(for room: MXRoom, state: VoiceBroadcastInfoState) { - let voiceBroadcastService = VoiceBroadcastService(room: room, state: VoiceBroadcastInfo.State.stopped) + let voiceBroadcastService = VoiceBroadcastService(room: room, state: VoiceBroadcastInfoState.stopped) self.currentVoiceBroadcastService = voiceBroadcastService @@ -95,22 +95,22 @@ class VoiceBroadcastServiceProvider { private func setupVoiceBroadcastService(for room: MXRoom, completion: @escaping (VoiceBroadcastService?) -> Void) { self.getLastVoiceBroadcastInfo(for: room) { event in guard let voiceBroadcastInfoEvent = event else { - self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfo.State.stopped) + self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfoState.stopped) completion(self.currentVoiceBroadcastService) return } guard let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: voiceBroadcastInfoEvent.content) else { - self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfo.State.stopped) + self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfoState.stopped) completion(self.currentVoiceBroadcastService) return } - if voiceBroadcastInfo.state == VoiceBroadcastInfo.State.stopped.rawValue { - self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfo.State.stopped) + if voiceBroadcastInfo.state == VoiceBroadcastInfoState.stopped.rawValue { + self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfoState.stopped) completion(self.currentVoiceBroadcastService) } else if voiceBroadcastInfoEvent.stateKey == room.mxSession.myUserId { - self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) ?? VoiceBroadcastInfo.State.stopped) + self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfoState(rawValue: voiceBroadcastInfo.state) ?? VoiceBroadcastInfoState.stopped) completion(self.currentVoiceBroadcastService) } else { completion(nil) diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift index d353e2f55..652d6f7b2 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift @@ -22,7 +22,7 @@ struct VoiceBroadcastPlaybackCoordinatorParameters { let session: MXSession let room: MXRoom let voiceBroadcastStartEvent: MXEvent - let voiceBroadcastState: VoiceBroadcastInfo.State + let voiceBroadcastState: VoiceBroadcastInfoState let senderDisplayName: String? } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift index 29b6252df..eaae1b11f 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift @@ -47,7 +47,7 @@ import Foundation let parameters = VoiceBroadcastPlaybackCoordinatorParameters(session: session, room: room, voiceBroadcastStartEvent: event, - voiceBroadcastState: VoiceBroadcastInfo.State(rawValue: voiceBroadcastState) ?? VoiceBroadcastInfo.State.stopped, + voiceBroadcastState: VoiceBroadcastInfoState(rawValue: voiceBroadcastState) ?? VoiceBroadcastInfoState.stopped, senderDisplayName: senderDisplayName) guard let coordinator = try? VoiceBroadcastPlaybackCoordinator(parameters: parameters) else { return nil diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index ff237a320..fa8fdd93d 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -36,11 +36,23 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private var audioPlayer: VoiceMessageAudioPlayer? private var displayLink: CADisplayLink! - private var isLivePlayback = false - private var acceptProgressUpdates = true - + private var isPlaybackInitialized: Bool = false + private var acceptProgressUpdates: Bool = true private var isActuallyPaused: Bool = false + private var isPlayingLastChunk: Bool { + let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks)) + guard let chunkDuration = chunks.last?.duration else { + return false + } + + return state.bindings.progress + 1000 >= state.playingState.duration - Float(chunkDuration) + } + + private var isLivePlayback: Bool { + return (!isPlaybackInitialized || isPlayingLastChunk) && (state.broadcastState == .started || state.broadcastState == .resumed) + } + // MARK: Public // MARK: - Setup @@ -54,9 +66,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic self.voiceBroadcastAggregator = voiceBroadcastAggregator let viewState = VoiceBroadcastPlaybackViewState(details: details, - broadcastState: VoiceBroadcastPlaybackViewModel.getBroadcastState(from: voiceBroadcastAggregator.voiceBroadcastState), + broadcastState: voiceBroadcastAggregator.voiceBroadcastState, playbackState: .stopped, - playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration)), + playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration), isLive: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0)) super.init(initialViewState: viewState) @@ -81,8 +93,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic switch viewAction { case .play: play() - case .playLive: - playLive() case .pause: pause() case .sliderChange(let didChange): @@ -95,7 +105,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic /// Listen voice broadcast private func play() { - isLivePlayback = false displayLink.isPaused = false isActuallyPaused = false @@ -105,8 +114,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: Start streaming") state.playbackState = .buffering voiceBroadcastAggregator.start() - - updateDuration() } else if let audioPlayer = audioPlayer { MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: resume") audioPlayer.play() @@ -120,42 +127,10 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic } } - private func playLive() { - guard isLivePlayback == false else { - MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Already playing live") - return - } - - isLivePlayback = true - displayLink.isPaused = false - isActuallyPaused = false - - // Flush the current audio player playlist - audioPlayer?.removeAllPlayerItems() - - if voiceBroadcastAggregator.isStarted == false { - // Start the streaming by fetching broadcast chunks - // The audio player will automatically start the playback on incoming chunks - MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Start streaming") - state.playbackState = .buffering - voiceBroadcastAggregator.start() - - state.playingState.duration = Float(voiceBroadcastAggregator.voiceBroadcast.duration) - } else { - let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks - MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: restart from the last chunk: \(chunks.count) chunks") - - // Reinject all the chunks we already have and play the last one - voiceBroadcastChunkQueue.append(contentsOf: chunks) - processPendingVoiceBroadcastChunksForLivePlayback() - } - } - /// Pause voice broadcast private func pause() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] pause") - isLivePlayback = false displayLink.isPaused = true isActuallyPaused = true @@ -169,9 +144,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // Check if the broadcast is over before stopping everything // If not, the player should not stopped. The view state must be move to buffering - // TODO: Define with more accuracy the threshold to detect the end of the playback - let remainingTime = state.playingState.duration - state.bindings.progress - if remainingTime < 500 { + if state.broadcastState == .stopped, isPlayingLastChunk { stop() } else { state.playbackState = .buffering @@ -181,7 +154,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private func stop() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] stop") - isLivePlayback = false displayLink.isPaused = true // Objects will be released on audioPlayerDidStopPlaying @@ -254,13 +226,19 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // Append the chunk to the current playlist audioPlayer.addContentFromURL(result.url) + if let time = time { + audioPlayer.seekToTime(time) + } + // Resume the player. Needed after a buffering - if audioPlayer.isPlaying == false && self.state.playbackState == .buffering { - MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Resume the player") - self.displayLink.isPaused = false - audioPlayer.play() - if let time = time { - audioPlayer.seekToTime(time) + 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 { @@ -347,22 +325,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic state.bindings.progress = Float(progress) } - - private static func getBroadcastState(from state: VoiceBroadcastInfo.State) -> VoiceBroadcastState { - var broadcastState: VoiceBroadcastState - switch state { - case .started: - broadcastState = VoiceBroadcastState.live - case .paused: - broadcastState = VoiceBroadcastState.paused - case .resumed: - broadcastState = VoiceBroadcastState.live - case .stopped: - broadcastState = VoiceBroadcastState.stopped - } - - return broadcastState - } } // MARK: VoiceBroadcastAggregatorDelegate @@ -381,14 +343,20 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { voiceBroadcastChunkQueue.append(didReceiveChunk) } - func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfo.State) { - state.broadcastState = VoiceBroadcastPlaybackViewModel.getBroadcastState(from: didReceiveState) + func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState) { + state.broadcastState = didReceiveState + + // Handle the live icon appearance + state.playingState.isLive = isLivePlayback } func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) { - if isLivePlayback && state.playbackState == .buffering { - // We started directly with a live playback but there was no known chunks at that time - // These are the first chunks we get. Start the playback on the latest one + + updateDuration() + + // Handle specifically the case where we were waiting data to start playing a live playback + if isLivePlayback, state.playbackState == .buffering { + // Start the playback on the latest one processPendingVoiceBroadcastChunksForLivePlayback() } else { processPendingVoiceBroadcastChunks() @@ -403,20 +371,20 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { } func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { - if isLivePlayback { - state.playbackState = .playingLive - } else { - state.playbackState = .playing - } + state.playbackState = .playing + state.playingState.isLive = isLivePlayback + isPlaybackInitialized = true } func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { state.playbackState = .paused + state.playingState.isLive = false } func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidStopPlaying") state.playbackState = .stopped + state.playingState.isLive = false release() } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index fb2da1ddf..c06d74976 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -32,7 +32,7 @@ struct VoiceBroadcastPlaybackView: View { @Environment(\.theme) private var theme: ThemeSwiftUI private var backgroundColor: Color { - if viewModel.viewState.playbackState == .playingLive { + if viewModel.viewState.playingState.isLive { return theme.colors.alert } return theme.colors.quarterlyContent @@ -70,20 +70,17 @@ struct VoiceBroadcastPlaybackView: View { } }.frame(maxWidth: .infinity, alignment: .leading) - if viewModel.viewState.broadcastState == .live { - Button { viewModel.send(viewAction: .playLive) } label: - { - Label { - Text(VectorL10n.voiceBroadcastLive) - .font(theme.fonts.caption1SB) - .foregroundColor(Color.white) - } icon: { - Image(uiImage: Asset.Images.voiceBroadcastLive.image) - } + if viewModel.viewState.broadcastState != .stopped { + Label { + Text(VectorL10n.voiceBroadcastLive) + .font(theme.fonts.caption1SB) + .foregroundColor(Color.white) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastLive.image) } .padding(.horizontal, 5) .background(RoundedRectangle(cornerRadius: 4, style: .continuous).fill(backgroundColor)) - .accessibilityIdentifier("liveButton") + .accessibilityIdentifier("liveLabel") } } .frame(maxWidth: .infinity, alignment: .leading) @@ -92,22 +89,14 @@ struct VoiceBroadcastPlaybackView: View { VoiceBroadcastPlaybackErrorView() } else { ZStack { - if viewModel.viewState.playbackState == .playing || - viewModel.viewState.playbackState == .playingLive { + if viewModel.viewState.playbackState == .playing { Button { viewModel.send(viewAction: .pause) } label: { Image(uiImage: Asset.Images.voiceBroadcastPause.image) .renderingMode(.original) } .accessibilityIdentifier("pauseButton") } else { - Button { - if viewModel.viewState.broadcastState == .live && - viewModel.viewState.playbackState == .stopped { - viewModel.send(viewAction: .playLive) - } else { - viewModel.send(viewAction: .play) - } - } label: { + Button { viewModel.send(viewAction: .play) } label: { Image(uiImage: Asset.Images.voiceBroadcastPlay.image) .renderingMode(.original) } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift index c9133f68e..18d80d3af 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift @@ -19,7 +19,6 @@ import SwiftUI enum VoiceBroadcastPlaybackViewAction { case play - case playLive case pause case sliderChange(didChange: Bool) } @@ -28,7 +27,6 @@ enum VoiceBroadcastPlaybackState { case stopped case buffering case playing - case playingLive case paused case error } @@ -38,21 +36,15 @@ struct VoiceBroadcastPlaybackDetails { let avatarData: AvatarInputProtocol } -enum VoiceBroadcastState { - case unknown - case stopped - case live - case paused -} - struct VoiceBroadcastPlayingState { var duration: Float var durationLabel: String? + var isLive: Bool } struct VoiceBroadcastPlaybackViewState: BindableState { var details: VoiceBroadcastPlaybackDetails - var broadcastState: VoiceBroadcastState + var broadcastState: VoiceBroadcastInfoState var playbackState: VoiceBroadcastPlaybackState var playingState: VoiceBroadcastPlayingState var bindings: VoiceBroadcastPlaybackViewStateBindings diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift index 4159d9aa7..d88f7dfa8 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift @@ -43,7 +43,7 @@ enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) - let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .live, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0))) + let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0))) return ( [false, viewModel], diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 99c555cc8..dbcf4854a 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -63,6 +63,7 @@ targets: - path: ../Riot/Modules/Analytics/AnalyticsScreen.swift - path: ../Riot/Modules/LocationSharing/LocationAuthorizationStatus.swift - path: ../Riot/Modules/QRCode/QRCodeGenerator.swift + - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift - path: ../Riot/Assets/en.lproj/Untranslated.strings buildPhase: resources - path: ../Riot/Assets/Images.xcassets diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index e2db2be61..533efab5f 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -72,6 +72,7 @@ targets: - path: ../Riot/Modules/Analytics/AnalyticsScreen.swift - path: ../Riot/Modules/LocationSharing/LocationAuthorizationStatus.swift - path: ../Riot/Modules/QRCode/QRCodeGenerator.swift + - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift - path: ../Riot/Assets/en.lproj/Untranslated.strings buildPhase: resources - path: ../Riot/Assets/Images.xcassets diff --git a/changelog.d/6721.wip b/changelog.d/6721.wip new file mode 100644 index 000000000..f9d51ccbd --- /dev/null +++ b/changelog.d/6721.wip @@ -0,0 +1 @@ + Labs: VoiceBroadcast - Add the Voice Broadcast option in the room functionalities \ No newline at end of file diff --git a/changelog.d/7094.bugfix b/changelog.d/7094.bugfix new file mode 100644 index 000000000..888b6ea37 --- /dev/null +++ b/changelog.d/7094.bugfix @@ -0,0 +1 @@ +Support voice broadcast live playback \ No newline at end of file