diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 275866995..5d13bf99f 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -599,6 +599,7 @@ static CGSize kThreadListBarButtonItemImageSize; [VoiceMessageMediaServiceProvider.sharedProvider pauseAllServices]; [VoiceBroadcastRecorderProvider.shared pauseRecording]; + [VoiceBroadcastPlaybackProvider.shared pausePlaying]; // Stop the loading indicator even if the session is still in progress [self stopLoadingUserIndicator]; diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 46e06cfed..231773f2b 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -72,6 +72,10 @@ class VoiceMessageAudioPlayer: NSObject { return audioPlayer.items() } + var currentUrl: URL? { + return (audioPlayer?.currentItem?.asset as? AVURLAsset)?.url + } + private(set) var isStopped = true deinit { diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift index e27f5258a..5bd472f6f 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift @@ -27,20 +27,25 @@ struct VoiceBroadcastBuilder { var voiceBroadcast = VoiceBroadcast() - voiceBroadcast.chunks = Set(events.compactMap { event in + let chunks = Set(events.compactMap { event in buildChunk(event: event, mediaManager: mediaManager, voiceBroadcastStartEventId: voiceBroadcastStartEventId) }) + voiceBroadcast.chunks = chunks + voiceBroadcast.duration = chunks.reduce(0) { $0 + $1.duration} + return voiceBroadcast } func buildChunk(event: MXEvent, mediaManager: MXMediaManager, voiceBroadcastStartEventId: String) -> VoiceBroadcastChunk? { guard let attachment = MXKAttachment(event: event, andMediaManager: mediaManager), let chunkInfo = event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] as? [String: UInt], - let sequence = chunkInfo[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence] else { + let sequence = chunkInfo[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence], + let audio = event.content[kMXMessageContentKeyExtensibleAudioMSC1767] as? [String: UInt], + let duration = audio[kMXMessageContentKeyExtensibleAudioDuration] else { return nil } - return VoiceBroadcastChunk(voiceBroadcastInfoEventId: voiceBroadcastStartEventId, sequence: sequence, attachment: attachment) + return VoiceBroadcastChunk(voiceBroadcastInfoEventId: voiceBroadcastStartEventId, sequence: sequence, attachment: attachment, duration: duration) } } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastChunk.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastChunk.swift index 1d974d791..8c18fc602 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastChunk.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastChunk.swift @@ -20,13 +20,16 @@ public class VoiceBroadcastChunk: NSObject { public private(set) var voiceBroadcastInfoEventId: String public private(set) var sequence: UInt public private(set) var attachment: MXKAttachment + public private(set) var duration: UInt public init(voiceBroadcastInfoEventId: String, sequence: UInt, - attachment: MXKAttachment) { + attachment: MXKAttachment, + duration: UInt) { self.voiceBroadcastInfoEventId = voiceBroadcastInfoEventId self.sequence = sequence self.attachment = attachment + self.duration = duration } public static func == (lhs: VoiceBroadcastChunk, rhs: VoiceBroadcastChunk) -> Bool { diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift index 138af9e32..4b36bea73 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift @@ -24,4 +24,5 @@ public enum VoiceBroadcastKind { public struct VoiceBroadcast { var chunks: Set = [] var kind: VoiceBroadcastKind = .player + var duration: UInt = 0 } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index 3a04f2312..a11cb3a2e 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -20,7 +20,16 @@ import Foundation class TimelinePollProvider: NSObject { static let shared = TimelinePollProvider() - var session: MXSession? + var session: MXSession? { + willSet { + guard let currentSession = self.session else { return } + + if currentSession != newValue { + // Clear all stored coordinators on new session + coordinatorsForEventIdentifiers.removeAll() + } + } + } var coordinatorsForEventIdentifiers = [String: TimelinePollCoordinator]() /// Create or retrieve the poll timeline coordinator for this event and return diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift index ee7b51e4e..d353e2f55 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift @@ -76,4 +76,8 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { } func endVoiceBroadcast() {} + + func pausePlaying() { + viewModel.context.send(viewAction: .pause) + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift index 7ca72c413..29b6252df 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift @@ -16,13 +16,22 @@ import Foundation -class VoiceBroadcastPlaybackProvider { - static let shared = VoiceBroadcastPlaybackProvider() +@objc class VoiceBroadcastPlaybackProvider: NSObject { + @objc static let shared = VoiceBroadcastPlaybackProvider() - var session: MXSession? + var session: MXSession? { + willSet { + guard let currentSession = self.session else { return } + + if currentSession != newValue { + // Clear all stored coordinators on new session + coordinatorsForEventIdentifiers.removeAll() + } + } + } var coordinatorsForEventIdentifiers = [String: VoiceBroadcastPlaybackCoordinator]() - private init() { } + private override init() { } /// Create or retrieve the voiceBroadcast timeline coordinator for this event and return /// a view to be displayed in the timeline @@ -54,4 +63,11 @@ class VoiceBroadcastPlaybackProvider { func voiceBroadcastPlaybackCoordinatorForEventIdentifier(_ eventIdentifier: String) -> VoiceBroadcastPlaybackCoordinator? { coordinatorsForEventIdentifiers[eventIdentifier] } + + /// Pause current voice broadcast playback. + @objc public func pausePlaying() { + coordinatorsForEventIdentifiers.forEach { _, coordinator in + coordinator.pausePlaying() + } + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index c27da240e..ff237a320 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -26,14 +26,20 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // MARK: - Properties // MARK: Private - private var voiceBroadcastAggregator: VoiceBroadcastAggregator private let mediaServiceProvider: VoiceMessageMediaServiceProvider private let cacheManager: VoiceMessageAttachmentCacheManager - private var audioPlayer: VoiceMessageAudioPlayer? + private var voiceBroadcastAggregator: VoiceBroadcastAggregator private var voiceBroadcastChunkQueue: [VoiceBroadcastChunk] = [] + private var voiceBroadcastAttachmentCacheManagerLoadResults: [VoiceMessageAttachmentCacheManagerLoadResult] = [] + + private var audioPlayer: VoiceMessageAudioPlayer? + private var displayLink: CADisplayLink! private var isLivePlayback = false + private var acceptProgressUpdates = true + + private var isActuallyPaused: Bool = false // MARK: Public @@ -50,9 +56,14 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic let viewState = VoiceBroadcastPlaybackViewState(details: details, broadcastState: VoiceBroadcastPlaybackViewModel.getBroadcastState(from: voiceBroadcastAggregator.voiceBroadcastState), playbackState: .stopped, - bindings: VoiceBroadcastPlaybackViewStateBindings()) + playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration)), + bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0)) super.init(initialViewState: viewState) + displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) + displayLink.isPaused = true + displayLink.add(to: .current, forMode: .common) + self.voiceBroadcastAggregator.delegate = self } @@ -74,6 +85,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic playLive() case .pause: pause() + case .sliderChange(let didChange): + didSliderChanged(didChange) } } @@ -83,6 +96,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic /// Listen voice broadcast private func play() { isLivePlayback = false + displayLink.isPaused = false + isActuallyPaused = false if voiceBroadcastAggregator.isStarted == false { // Start the streaming by fetching broadcast chunks @@ -90,16 +105,16 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: Start streaming") state.playbackState = .buffering voiceBroadcastAggregator.start() - } - else if let audioPlayer = audioPlayer { + + updateDuration() + } else if let audioPlayer = audioPlayer { MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: resume") audioPlayer.play() - } - else { + } else { let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: restart from the beginning: \(chunks.count) chunks") - // Reinject all the chuncks we already have and play them + // Reinject all the chunks we already have and play them voiceBroadcastChunkQueue.append(contentsOf: chunks) processPendingVoiceBroadcastChunks() } @@ -112,6 +127,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic } isLivePlayback = true + displayLink.isPaused = false + isActuallyPaused = false // Flush the current audio player playlist audioPlayer?.removeAllPlayerItems() @@ -122,22 +139,25 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Start streaming") state.playbackState = .buffering voiceBroadcastAggregator.start() - } - else { + + 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 chuncks we already have and play the last one + // Reinject all the chunks we already have and play the last one voiceBroadcastChunkQueue.append(contentsOf: chunks) processPendingVoiceBroadcastChunksForLivePlayback() } } - /// Stop voice broadcast + /// Pause voice broadcast private func pause() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] pause") isLivePlayback = false + displayLink.isPaused = true + isActuallyPaused = true if let audioPlayer = audioPlayer, audioPlayer.isPlaying { audioPlayer.pause() @@ -147,15 +167,22 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private func stopIfVoiceBroadcastOver() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] stopIfVoiceBroadcastOver") - // TODO: Check if the broadcast is over before stopping everything + // Check if the broadcast is over before stopping everything // If not, the player should not stopped. The view state must be move to buffering - stop() + // 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 { + stop() + } else { + state.playbackState = .buffering + } } private func stop() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] stop") isLivePlayback = false + displayLink.isPaused = true // Objects will be released on audioPlayerDidStopPlaying audioPlayer?.stop() @@ -165,9 +192,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // MARK: - Voice broadcast chunks playback /// Start the playback from the beginning or push more chunks to it - private func processPendingVoiceBroadcastChunks() { + private func processPendingVoiceBroadcastChunks(_ time: TimeInterval? = nil) { reorderPendingVoiceBroadcastChunks() - processNextVoiceBroadcastChunk() + processNextVoiceBroadcastChunk(time) } /// Start the playback from the last known chunk @@ -188,7 +215,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic chunks.sorted(by: {$0.sequence < $1.sequence}) } - private func processNextVoiceBroadcastChunk() { + private func processNextVoiceBroadcastChunk(_ time: TimeInterval? = nil) { MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: \(voiceBroadcastChunkQueue.count) chunks remaining") guard voiceBroadcastChunkQueue.count > 0 else { @@ -196,6 +223,10 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return } + if (isActuallyPaused == false && state.playbackState == .paused) || state.playbackState == .stopped { + state.playbackState = .buffering + } + // TODO: Control the download rate to avoid to download all chunk in mass // We could synchronise it with the number of chunks in the player playlist (audioPlayer.playerItems) @@ -210,45 +241,113 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // TODO: Make sure there has no new incoming chunk that should be before this attachment // Be careful that this new chunk is not older than the chunk being played by the audio player. Else // we will get an unexecpted rewind. - + switch result { - case .success(let result): - guard result.eventIdentifier == chunk.attachment.eventId else { - return - } + case .success(let result): + guard result.eventIdentifier == chunk.attachment.eventId else { + return + } + + self.voiceBroadcastAttachmentCacheManagerLoadResults.append(result) + + if let audioPlayer = self.audioPlayer { + // Append the chunk to the current playlist + audioPlayer.addContentFromURL(result.url) - if let audioPlayer = self.audioPlayer { - // Append the chunk to the current playlist - audioPlayer.addContentFromURL(result.url) - - // Resume the player. Needed after a pause - if audioPlayer.isPlaying == false { - MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Resume the player") - audioPlayer.play() + // 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) } } - else { - // 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) - audioPlayer.play() - self.audioPlayer = audioPlayer - } - - case .failure (let error): - MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error) - if self.voiceBroadcastChunkQueue.count == 0 { - // No more chunk to try. Go to error - self.state.playbackState = .error + } else { + // 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 = time { + audioPlayer.seekToTime(time) } + self.audioPlayer = audioPlayer + } + + case .failure (let error): + MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error) + if self.voiceBroadcastChunkQueue.count == 0 { + // No more chunk to try. Go to error + self.state.playbackState = .error + } } self.processNextVoiceBroadcastChunk() } } + private func updateDuration() { + let duration = voiceBroadcastAggregator.voiceBroadcast.duration + let time = TimeInterval(duration / 1000) + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .abbreviated + + state.playingState.duration = Float(duration) + state.playingState.durationLabel = formatter.string(from: time) + } + + private func didSliderChanged(_ didChange: Bool) { + acceptProgressUpdates = !didChange + if didChange { + audioPlayer?.pause() + displayLink.isPaused = true + } else { + // Flush the current audio player playlist + audioPlayer?.removeAllPlayerItems() + + let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks)) + + // Reinject the chunks we need and play them + let remainingTime = state.playingState.duration - state.bindings.progress + var chunksDuration: UInt = 0 + for chunk in chunks.reversed() { + chunksDuration += chunk.duration + voiceBroadcastChunkQueue.append(chunk) + if Float(chunksDuration) >= remainingTime { + break + } + } + + MXLog.debug("[VoiceBroadcastPlaybackViewModel] didSliderChanged: restart to time: \(state.bindings.progress) milliseconds") + let time = state.bindings.progress - state.playingState.duration + Float(chunksDuration) + processPendingVoiceBroadcastChunks(TimeInterval(time / 1000)) + } + } + + @objc private func handleDisplayLinkTick() { + updateUI() + } + + private func updateUI() { + guard let playingEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in + result.url == audioPlayer?.currentUrl + })?.eventIdentifier, + let playingSequence = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in + chunk.attachment.eventId == playingEventId + })?.sequence else { + return + } + + let progress = Double(voiceBroadcastAggregator.voiceBroadcast.chunks.filter { chunk in + chunk.sequence < playingSequence + }.reduce(0) { $0 + $1.duration}) + (audioPlayer?.currentTime.rounded() ?? 0) * 1000 + + state.bindings.progress = Float(progress) + } + private static func getBroadcastState(from state: VoiceBroadcastInfo.State) -> VoiceBroadcastState { var broadcastState: VoiceBroadcastState switch state { @@ -288,11 +387,10 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) { if isLivePlayback && state.playbackState == .buffering { - // We started directly with a live playback but there was no known chuncks at that time + // 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 processPendingVoiceBroadcastChunksForLivePlayback() - } - else { + } else { processPendingVoiceBroadcastChunks() } } @@ -307,8 +405,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { if isLivePlayback { state.playbackState = .playingLive - } - else { + } else { state.playbackState = .playing } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index a16c83471..fb2da1ddf 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -117,6 +117,16 @@ struct VoiceBroadcastPlaybackView: View { } .activityIndicator(show: viewModel.viewState.playbackState == .buffering) } + + Slider(value: $viewModel.progress, in: 0...viewModel.viewState.playingState.duration) { + Text("Slider") + } minimumValueLabel: { + Text("") + } maximumValueLabel: { + Text(viewModel.viewState.playingState.durationLabel ?? "").font(.body) + } onEditingChanged: { didChange in + viewModel.send(viewAction: .sliderChange(didChange: didChange)) + } } .padding([.horizontal, .top], 2.0) .padding([.bottom]) diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift index 3fed0075f..c9133f68e 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift @@ -21,6 +21,7 @@ enum VoiceBroadcastPlaybackViewAction { case play case playLive case pause + case sliderChange(didChange: Bool) } enum VoiceBroadcastPlaybackState { @@ -44,13 +45,20 @@ enum VoiceBroadcastState { case paused } +struct VoiceBroadcastPlayingState { + var duration: Float + var durationLabel: String? +} + struct VoiceBroadcastPlaybackViewState: BindableState { var details: VoiceBroadcastPlaybackDetails var broadcastState: VoiceBroadcastState var playbackState: VoiceBroadcastPlaybackState + var playingState: VoiceBroadcastPlayingState var bindings: VoiceBroadcastPlaybackViewStateBindings } struct VoiceBroadcastPlaybackViewStateBindings { + var progress: Float } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift index f4fabadb1..4159d9aa7 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, bindings: VoiceBroadcastPlaybackViewStateBindings())) + let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .live, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0))) return ( [false, viewModel], diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift index c7bc2b1a0..3db5cad54 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift @@ -23,7 +23,16 @@ import Foundation // MARK: - Properties // MARK: Public - var session: MXSession? + var session: MXSession? { + willSet { + guard let currentSession = self.session else { return } + + if currentSession != newValue { + // Clear all stored coordinators on new session + coordinatorsForEventIdentifiers.removeAll() + } + } + } var coordinatorsForEventIdentifiers = [String: VoiceBroadcastRecorderCoordinator]() // MARK: Private diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift index 0ad1fa682..f2e28e5da 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift @@ -49,23 +49,31 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // MARK: - VoiceBroadcastRecorderServiceProtocol func startRecordingVoiceBroadcast() { - let inputNode = audioEngine.inputNode + do { + try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default) + try AVAudioSession.sharedInstance().setActive(true) - let inputFormat = inputNode.inputFormat(forBus: audioNodeBus) - MXLog.debug("[VoiceBroadcastRecorderService] Start recording voice broadcast for bus name : \(String(describing: inputNode.name(forInputBus: audioNodeBus)))") + let inputNode = audioEngine.inputNode - inputNode.installTap(onBus: audioNodeBus, - bufferSize: 512, - format: inputFormat) { (buffer, time) -> Void in - DispatchQueue.main.async { - self.writeBuffer(buffer) + let inputFormat = inputNode.inputFormat(forBus: audioNodeBus) + MXLog.debug("[VoiceBroadcastRecorderService] Start recording voice broadcast for bus name : \(String(describing: inputNode.name(forInputBus: audioNodeBus)))") + + inputNode.installTap(onBus: audioNodeBus, + bufferSize: 512, + format: inputFormat) { (buffer, time) -> Void in + DispatchQueue.main.async { + self.writeBuffer(buffer) + } } - } - try? audioEngine.start() - - // Disable the sleep mode during the recording until we are able to handle it - UIApplication.shared.isIdleTimerDisabled = true + try audioEngine.start() + + // Disable the sleep mode during the recording until we are able to handle it + UIApplication.shared.isIdleTimerDisabled = true + } catch { + MXLog.debug("[VoiceBroadcastRecorderService] startRecordingVoiceBroadcast error", context: error) + stopRecordingVoiceBroadcast() + } } func stopRecordingVoiceBroadcast() { @@ -141,6 +149,12 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { private func tearDownVoiceBroadcastService() { resetValues() session.tearDownVoiceBroadcastService() + + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + MXLog.error("[VoiceBroadcastRecorderService] tearDownVoiceBroadcastService error", context: error) + } } /// Write audio buffer to chunk file.