mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-20 16:42:44 +02:00
VB: Moved the VM temporary under a MatrixSDK
to avoid to use it on the SwiftUI build
This commit is contained in:
+334
@@ -0,0 +1,334 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
// TODO: VoiceBroadcastPlaybackViewModel must be revisited in order to not depend on MatrixSDK
|
||||
// We need a VoiceBroadcastPlaybackServiceProtocol and VoiceBroadcastAggregatorProtocol
|
||||
import MatrixSDK
|
||||
|
||||
class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, VoiceBroadcastPlaybackViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
private var voiceBroadcastAggregator: VoiceBroadcastAggregator
|
||||
private let mediaServiceProvider: VoiceMessageMediaServiceProvider
|
||||
private let cacheManager: VoiceMessageAttachmentCacheManager
|
||||
private var audioPlayer: VoiceMessageAudioPlayer?
|
||||
|
||||
private var voiceBroadcastChunkQueue: [VoiceBroadcastChunk] = []
|
||||
|
||||
private var isLivePlayback = false
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(details: VoiceBroadcastPlaybackDetails,
|
||||
mediaServiceProvider: VoiceMessageMediaServiceProvider,
|
||||
cacheManager: VoiceMessageAttachmentCacheManager,
|
||||
voiceBroadcastAggregator: VoiceBroadcastAggregator) {
|
||||
self.mediaServiceProvider = mediaServiceProvider
|
||||
self.cacheManager = cacheManager
|
||||
self.voiceBroadcastAggregator = voiceBroadcastAggregator
|
||||
|
||||
let viewState = VoiceBroadcastPlaybackViewState(details: details,
|
||||
broadcastState: VoiceBroadcastPlaybackViewModel.getBroadcastState(from: voiceBroadcastAggregator.voiceBroadcastState),
|
||||
playbackState: .stopped,
|
||||
bindings: VoiceBroadcastPlaybackViewStateBindings())
|
||||
super.init(initialViewState: viewState)
|
||||
|
||||
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) {
|
||||
switch viewAction {
|
||||
case .play:
|
||||
play()
|
||||
case .playLive:
|
||||
playLive()
|
||||
case .pause:
|
||||
pause()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Listen voice broadcast
|
||||
private func play() {
|
||||
isLivePlayback = false
|
||||
|
||||
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] play: Start streaming")
|
||||
state.playbackState = .buffering
|
||||
voiceBroadcastAggregator.start()
|
||||
}
|
||||
else if let audioPlayer = audioPlayer {
|
||||
MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: resume")
|
||||
audioPlayer.play()
|
||||
}
|
||||
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
|
||||
voiceBroadcastChunkQueue.append(contentsOf: chunks)
|
||||
processPendingVoiceBroadcastChunks()
|
||||
}
|
||||
}
|
||||
|
||||
private func playLive() {
|
||||
guard isLivePlayback == false else {
|
||||
MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Already playing live")
|
||||
return
|
||||
}
|
||||
|
||||
isLivePlayback = true
|
||||
|
||||
// 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()
|
||||
}
|
||||
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
|
||||
voiceBroadcastChunkQueue.append(contentsOf: chunks)
|
||||
processPendingVoiceBroadcastChunksForLivePlayback()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop voice broadcast
|
||||
private func pause() {
|
||||
MXLog.debug("[VoiceBroadcastPlaybackViewModel] pause")
|
||||
|
||||
isLivePlayback = false
|
||||
|
||||
if let audioPlayer = audioPlayer, audioPlayer.isPlaying {
|
||||
audioPlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
private func stop() {
|
||||
MXLog.debug("[VoiceBroadcastPlaybackViewModel] stop")
|
||||
|
||||
isLivePlayback = false
|
||||
|
||||
// Objects will be released on audioPlayerDidStopPlaying
|
||||
audioPlayer?.stop()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Voice broadcast chunks playback
|
||||
|
||||
/// Start the playback from the beginning or push more chunks to it
|
||||
private func processPendingVoiceBroadcastChunks() {
|
||||
reorderPendingVoiceBroadcastChunks()
|
||||
processNextVoiceBroadcastChunk()
|
||||
}
|
||||
|
||||
/// Start the playback from the last known chunk
|
||||
private func processPendingVoiceBroadcastChunksForLivePlayback() {
|
||||
let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks))
|
||||
if let lastChunk = chunks.last {
|
||||
MXLog.debug("[VoiceBroadcastPlaybackViewModel] processPendingVoiceBroadcastChunksForLivePlayback. Use the last chunk: sequence: \(lastChunk.sequence) out of the \(voiceBroadcastChunkQueue.count) chunks")
|
||||
voiceBroadcastChunkQueue = [lastChunk]
|
||||
}
|
||||
processNextVoiceBroadcastChunk()
|
||||
}
|
||||
|
||||
private func reorderPendingVoiceBroadcastChunks() {
|
||||
// Make sure we download and process chunks in the right order
|
||||
voiceBroadcastChunkQueue = reorderVoiceBroadcastChunks(chunks: voiceBroadcastChunkQueue)
|
||||
}
|
||||
private func reorderVoiceBroadcastChunks(chunks: [VoiceBroadcastChunk]) -> [VoiceBroadcastChunk] {
|
||||
chunks.sorted(by: {$0.sequence < $1.sequence})
|
||||
}
|
||||
|
||||
private func processNextVoiceBroadcastChunk() {
|
||||
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)
|
||||
|
||||
let chunk = voiceBroadcastChunkQueue.removeFirst()
|
||||
|
||||
// numberOfSamples is for the equalizer view we do not support yet
|
||||
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):
|
||||
guard result.eventIdentifier == chunk.attachment.eventId else {
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
self.processNextVoiceBroadcastChunk()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate {
|
||||
func voiceBroadcastAggregatorDidStartLoading(_ aggregator: VoiceBroadcastAggregator) {
|
||||
}
|
||||
|
||||
func voiceBroadcastAggregatorDidEndLoading(_ aggregator: VoiceBroadcastAggregator) {
|
||||
}
|
||||
|
||||
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didFailWithError: Error) {
|
||||
MXLog.error("[VoiceBroadcastPlaybackViewModel] voiceBroadcastAggregator didFailWithError:", context: didFailWithError)
|
||||
}
|
||||
|
||||
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk) {
|
||||
voiceBroadcastChunkQueue.append(didReceiveChunk)
|
||||
}
|
||||
|
||||
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfo.State) {
|
||||
state.broadcastState = VoiceBroadcastPlaybackViewModel.getBroadcastState(from: didReceiveState)
|
||||
}
|
||||
|
||||
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
|
||||
// These are the first chunks we get. Start the playback on the latest one
|
||||
processPendingVoiceBroadcastChunksForLivePlayback()
|
||||
}
|
||||
else {
|
||||
processPendingVoiceBroadcastChunks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - VoiceMessageAudioPlayerDelegate
|
||||
extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate {
|
||||
func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
}
|
||||
|
||||
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
if isLivePlayback {
|
||||
state.playbackState = .playingLive
|
||||
}
|
||||
else {
|
||||
state.playbackState = .playing
|
||||
}
|
||||
}
|
||||
|
||||
func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
state.playbackState = .paused
|
||||
}
|
||||
|
||||
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidStopPlaying")
|
||||
state.playbackState = .stopped
|
||||
release()
|
||||
}
|
||||
|
||||
func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) {
|
||||
state.playbackState = .error
|
||||
}
|
||||
|
||||
func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidFinishPlaying: \(audioPlayer.playerItems.count)")
|
||||
stopIfVoiceBroadcastOver()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user