element-ios/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackViewModel.swift

268 lines
9.9 KiB
Swift
Raw Normal View History

//
// 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.
//
import Combine
import SwiftUI
import MatrixSDK
typealias VoiceBroadcastPlaybackViewModelType = StateStoreViewModel<VoiceBroadcastPlaybackViewState, VoiceBroadcastPlaybackViewAction>
class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, VoiceBroadcastPlaybackViewModelProtocol {
2022-10-19 10:56:59 +00:00
// MARK: - Properties
// MARK: Private
2022-10-19 10:56:59 +00:00
private var voiceBroadcastAggregator: VoiceBroadcastAggregator
private let mediaServiceProvider: VoiceMessageMediaServiceProvider
private let cacheManager: VoiceMessageAttachmentCacheManager
private var audioPlayer: VoiceMessageAudioPlayer?
2022-10-19 21:09:48 +00:00
private var voiceBroadcastChunkQueue: [VoiceBroadcastChunk] = []
2022-10-20 09:33:17 +00:00
private var isLivePlayback = false
// MARK: Public
// MARK: - Setup
init(details: VoiceBroadcastPlaybackDetails,
mediaServiceProvider: VoiceMessageMediaServiceProvider,
2022-10-19 10:56:59 +00:00
cacheManager: VoiceMessageAttachmentCacheManager,
voiceBroadcastAggregator: VoiceBroadcastAggregator) {
self.mediaServiceProvider = mediaServiceProvider
self.cacheManager = cacheManager
self.voiceBroadcastAggregator = voiceBroadcastAggregator
let viewState = VoiceBroadcastPlaybackViewState(details: details,
broadcastState: .unknown,
playbackState: .stopped,
bindings: VoiceBroadcastPlaybackViewStateBindings())
super.init(initialViewState: viewState)
2022-10-19 10:56:59 +00:00
self.voiceBroadcastAggregator.delegate = self
}
private func release() {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] release")
if let audioPlayer = audioPlayer {
audioPlayer.deregisterDelegate(self)
self.audioPlayer = nil
}
}
// MARK: - Public
override func process(viewAction: VoiceBroadcastPlaybackViewAction) {
2022-10-18 14:36:02 +00:00
switch viewAction {
case .play:
play()
2022-10-20 09:33:17 +00:00
case .playLive:
playLive()
2022-10-18 14:36:02 +00:00
case .pause:
pause()
}
}
// MARK: - Private
2022-10-18 14:36:02 +00:00
/// Listen voice broadcast
private func play() {
2022-10-20 09:33:17 +00:00
isLivePlayback = false
2022-10-19 21:09:48 +00:00
if voiceBroadcastAggregator.isStarted == false {
// Start the streaming by fetching broadcast chunks
// The audio player will start the automatically playback on incoming chunks
MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: Start streaming")
state.playbackState = .buffering
voiceBroadcastAggregator.start()
}
else if let audioPlayer = audioPlayer {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: resume")
2022-10-19 21:09:48 +00:00
audioPlayer.play()
}
else {
let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks
MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: restart from the beginning: \(chunks.count) chunks")
// Reinject all the chunck we already have
voiceBroadcastChunkQueue.append(contentsOf: chunks)
processPendingVoiceBroadcastChunks()
2022-10-19 21:09:48 +00:00
}
}
2022-10-20 09:33:17 +00:00
private func playLive() {
guard isLivePlayback == false else {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Already playing live")
return
}
isLivePlayback = true
state.playbackState = .buffering
// TODO: TBC
}
2022-10-19 21:09:48 +00:00
/// Stop voice broadcast
private func pause() {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] pause")
2022-10-19 21:09:48 +00:00
if let audioPlayer = audioPlayer, audioPlayer.isPlaying {
audioPlayer.pause()
}
2022-10-19 21:09:48 +00:00
}
private func stopIfVoiceBroadcastOver() {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] stopIfVoiceBroadcastOver")
// TODO: 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()
}
2022-10-19 21:09:48 +00:00
private func stop() {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] stop")
// Objects will be released on audioPlayerDidStopPlaying
audioPlayer?.stop()
}
// MARK: - Voice broadcast chunks playback
private func processPendingVoiceBroadcastChunks() {
reorderPendingVoiceBroadcastChunks()
processNextVoiceBroadcastChunk()
}
private func reorderPendingVoiceBroadcastChunks() {
// Make sure we download and process check in the right order
voiceBroadcastChunkQueue = voiceBroadcastChunkQueue.sorted(by: {$0.sequence < $1.sequence})
}
private func processNextVoiceBroadcastChunk() {
2022-10-19 21:09:48 +00:00
MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: \(voiceBroadcastChunkQueue.count) chunks remaining")
guard voiceBroadcastChunkQueue.count > 0 else {
// We cached all chunks. Nothing more to do
return
}
// 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)
2022-10-19 21:09:48 +00:00
let chunk = voiceBroadcastChunkQueue.removeFirst()
// numberOfSamples is for the equalizer view we do not support yet
2022-10-19 21:09:48 +00:00
cacheManager.loadAttachment(chunk.attachment, numberOfSamples: 1) { [weak self] result in
guard let self = self else {
return
}
// 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):
2022-10-19 21:09:48 +00:00
guard result.eventIdentifier == chunk.attachment.eventId else {
return
}
2022-10-19 21:09:48 +00:00
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)
audioPlayer.play()
self.audioPlayer = audioPlayer
}
else {
// Append the chunk to the current playlist
self.audioPlayer?.addContentFromURL(result.url)
}
case .failure (let error):
2022-10-19 21:09:48 +00:00
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
}
}
2022-10-19 21:09:48 +00:00
self.processNextVoiceBroadcastChunk()
}
}
}
// MARK: - TODO: VoiceBroadcastAggregatorDelegate
2022-10-19 10:56:59 +00:00
extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate {
func voiceBroadcastAggregatorDidStartLoading(_ aggregator: VoiceBroadcastAggregator) {
}
func voiceBroadcastAggregatorDidEndLoading(_ aggregator: VoiceBroadcastAggregator) {
}
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didFailWithError: Error) {
2022-10-19 21:09:48 +00:00
MXLog.error("[VoiceBroadcastPlaybackViewModel] voiceBroadcastAggregator didFailWithError:", context: didFailWithError)
2022-10-19 10:56:59 +00:00
}
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk) {
2022-10-19 21:09:48 +00:00
voiceBroadcastChunkQueue.append(didReceiveChunk)
}
2022-10-19 10:56:59 +00:00
func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) {
processPendingVoiceBroadcastChunks()
2022-10-19 10:56:59 +00:00
}
}
// MARK: - TODO: VoiceMessageAudioPlayerDelegate
extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate {
func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) {
}
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
2022-10-20 09:33:17 +00:00
if isLivePlayback {
state.playbackState = .playingLive
}
else {
state.playbackState = .playing
}
}
func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
2022-10-19 13:01:43 +00:00
state.playbackState = .paused
}
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidStopPlaying")
2022-10-19 13:01:43 +00:00
state.playbackState = .stopped
release()
}
func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) {
2022-10-19 13:01:43 +00:00
state.playbackState = .error
}
func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidFinishPlaying: \(audioPlayer.playerItems.count)")
stopIfVoiceBroadcastOver()
}
}