mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-24 10:32:46 +02:00
Merge branch 'develop' into phlpro/voicebroadcast-display-playback-duration
This commit is contained in:
+1
-1
@@ -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?
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+18
-18
@@ -40,6 +40,19 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
|
||||
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
|
||||
@@ -127,7 +140,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
|
||||
if state.broadcastState == .stopped {
|
||||
if state.broadcastState == .stopped, isPlayingLastChunk {
|
||||
stop()
|
||||
} else {
|
||||
state.playbackState = .buffering
|
||||
@@ -221,7 +234,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
|
||||
audioPlayer.play()
|
||||
} else {
|
||||
self.state.playbackState = .playing
|
||||
self.state.playingState.isLive = self.isLivePlayback()
|
||||
self.state.playingState.isLive = self.isLivePlayback
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -308,19 +321,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
|
||||
|
||||
state.bindings.progress = Float(progress)
|
||||
}
|
||||
|
||||
private func 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 func isLivePlayback() -> Bool {
|
||||
return (!isPlaybackInitialized || isPlayingLastChunk()) && (state.broadcastState == .started || state.broadcastState == .resumed)
|
||||
}
|
||||
|
||||
private func handleWaitingLiveData() {
|
||||
// Handle specifically the case where we were waiting data to start playing a live playback
|
||||
@@ -352,11 +352,11 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate {
|
||||
voiceBroadcastChunkQueue.append(didReceiveChunk)
|
||||
}
|
||||
|
||||
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfo.State) {
|
||||
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState) {
|
||||
state.broadcastState = didReceiveState
|
||||
|
||||
// Handle the live icon appearance
|
||||
state.playingState.isLive = isLivePlayback()
|
||||
state.playingState.isLive = isLivePlayback
|
||||
}
|
||||
|
||||
func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) {
|
||||
@@ -377,7 +377,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate {
|
||||
|
||||
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
|
||||
state.playbackState = .playing
|
||||
state.playingState.isLive = isLivePlayback()
|
||||
state.playingState.isLive = isLivePlayback
|
||||
isPlaybackInitialized = true
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ struct VoiceBroadcastPlayingState {
|
||||
|
||||
struct VoiceBroadcastPlaybackViewState: BindableState {
|
||||
var details: VoiceBroadcastPlaybackDetails
|
||||
var broadcastState: VoiceBroadcastInfo.State
|
||||
var broadcastState: VoiceBroadcastInfoState
|
||||
var playbackState: VoiceBroadcastPlaybackState
|
||||
var playingState: VoiceBroadcastPlayingState
|
||||
var bindings: VoiceBroadcastPlaybackViewStateBindings
|
||||
|
||||
+39
-1
@@ -34,7 +34,13 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
|
||||
private var chunkFile: AVAudioFile! = nil
|
||||
private var chunkFrames: AVAudioFrameCount = 0
|
||||
private var chunkFileNumber: Int = 0
|
||||
|
||||
|
||||
private var currentElapsedTime: UInt = 0 // Time in seconds.
|
||||
private var currentRemainingTime: UInt { // Time in seconds.
|
||||
BuildSettings.voiceBroadcastMaxLength - currentElapsedTime
|
||||
}
|
||||
private var elapsedTimeTimer: Timer?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate?
|
||||
@@ -67,12 +73,14 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
|
||||
}
|
||||
|
||||
try audioEngine.start()
|
||||
startTimer()
|
||||
|
||||
// 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()
|
||||
invalidateTimer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +89,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
|
||||
audioEngine.stop()
|
||||
audioEngine.inputNode.removeTap(onBus: audioNodeBus)
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
invalidateTimer()
|
||||
|
||||
voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in
|
||||
MXLog.debug("[VoiceBroadcastRecorderService] Stopped")
|
||||
@@ -110,6 +119,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
|
||||
func pauseRecordingVoiceBroadcast() {
|
||||
audioEngine.pause()
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
invalidateTimer()
|
||||
|
||||
voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
@@ -126,6 +136,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
|
||||
|
||||
func resumeRecordingVoiceBroadcast() {
|
||||
try? audioEngine.start()
|
||||
startTimer()
|
||||
|
||||
voiceBroadcastService?.resumeVoiceBroadcast(success: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
@@ -143,12 +154,14 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
|
||||
private func resetValues() {
|
||||
chunkFrames = 0
|
||||
chunkFileNumber = 0
|
||||
currentElapsedTime = 0
|
||||
}
|
||||
|
||||
/// Release the service
|
||||
private func tearDownVoiceBroadcastService() {
|
||||
resetValues()
|
||||
session.tearDownVoiceBroadcastService()
|
||||
invalidateTimer()
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setActive(false)
|
||||
@@ -157,6 +170,31 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
/// Start ElapsedTimeTimer.
|
||||
private func startTimer() {
|
||||
elapsedTimeTimer = Timer.scheduledTimer(timeInterval: 1.0,
|
||||
target: self,
|
||||
selector: #selector(updateCurrentElapsedTimeValue),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
}
|
||||
|
||||
/// Invalidate ElapsedTimeTimer.
|
||||
private func invalidateTimer() {
|
||||
elapsedTimeTimer?.invalidate()
|
||||
elapsedTimeTimer = nil
|
||||
}
|
||||
|
||||
/// Update currentElapsedTime value.
|
||||
@objc private func updateCurrentElapsedTimeValue() {
|
||||
guard currentRemainingTime > 0 else {
|
||||
stopRecordingVoiceBroadcast()
|
||||
return
|
||||
}
|
||||
currentElapsedTime += 1
|
||||
serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateRemainingTime: self.currentRemainingTime)
|
||||
}
|
||||
|
||||
/// Write audio buffer to chunk file.
|
||||
private func writeBuffer(_ buffer: AVAudioPCMBuffer) {
|
||||
let sampleRate = buffer.format.sampleRate
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ import Foundation
|
||||
|
||||
protocol VoiceBroadcastRecorderServiceDelegate: AnyObject {
|
||||
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState)
|
||||
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateRemainingTime remainingTime: UInt)
|
||||
}
|
||||
|
||||
protocol VoiceBroadcastRecorderServiceProtocol {
|
||||
|
||||
@@ -53,6 +53,14 @@ struct VoiceBroadcastRecorderView: View {
|
||||
} icon: {
|
||||
Image(uiImage: Asset.Images.voiceBroadcastTileLive.image)
|
||||
}
|
||||
|
||||
Label {
|
||||
Text(viewModel.viewState.currentRecordingState.remainingTimeLabel)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.font(theme.fonts.caption1)
|
||||
} icon: {
|
||||
Image(uiImage: Asset.Images.voiceBroadcastTimeLeft.image)
|
||||
}
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Label {
|
||||
|
||||
@@ -35,9 +35,15 @@ struct VoiceBroadcastRecorderDetails {
|
||||
let avatarData: AvatarInputProtocol
|
||||
}
|
||||
|
||||
struct VoiceBroadcastRecordingState {
|
||||
var remainingTime: UInt
|
||||
var remainingTimeLabel: String
|
||||
}
|
||||
|
||||
struct VoiceBroadcastRecorderViewState: BindableState {
|
||||
var details: VoiceBroadcastRecorderDetails
|
||||
var recordingState: VoiceBroadcastRecorderState
|
||||
var currentRecordingState: VoiceBroadcastRecordingState
|
||||
var bindings: VoiceBroadcastRecorderViewStateBindings
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -32,7 +32,8 @@ enum MockVoiceBroadcastRecorderScreenState: MockScreenState, CaseIterable {
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
let details = VoiceBroadcastRecorderDetails(senderDisplayName: "", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room"))
|
||||
let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, bindings: VoiceBroadcastRecorderViewStateBindings()))
|
||||
let recordingState = VoiceBroadcastRecordingState(remainingTime: BuildSettings.voiceBroadcastMaxLength, remainingTimeLabel: "1h 20m 47s left")
|
||||
let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, currentRecordingState: recordingState, bindings: VoiceBroadcastRecorderViewStateBindings()))
|
||||
|
||||
return (
|
||||
[false, viewModel],
|
||||
|
||||
@@ -34,8 +34,10 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic
|
||||
init(details: VoiceBroadcastRecorderDetails,
|
||||
recorderService: VoiceBroadcastRecorderServiceProtocol) {
|
||||
self.voiceBroadcastRecorderService = recorderService
|
||||
let currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: BuildSettings.voiceBroadcastMaxLength)
|
||||
super.init(initialViewState: VoiceBroadcastRecorderViewState(details: details,
|
||||
recordingState: .stopped,
|
||||
currentRecordingState: currentRecordingState,
|
||||
bindings: VoiceBroadcastRecorderViewStateBindings()))
|
||||
|
||||
self.voiceBroadcastRecorderService.serviceDelegate = self
|
||||
@@ -77,10 +79,27 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic
|
||||
self.state.recordingState = .resumed
|
||||
voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast()
|
||||
}
|
||||
|
||||
private func updateRemainingTime(_ remainingTime: UInt) {
|
||||
state.currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: remainingTime)
|
||||
}
|
||||
|
||||
private static func currentRecordingState(from remainingTime: UInt) -> VoiceBroadcastRecordingState {
|
||||
let time = TimeInterval(Double(remainingTime))
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.unitsStyle = .abbreviated
|
||||
|
||||
return VoiceBroadcastRecordingState(remainingTime: remainingTime,
|
||||
remainingTimeLabel: VectorL10n.voiceBroadcastTimeLeft(formatter.string(from: time) ?? "0s"))
|
||||
}
|
||||
}
|
||||
|
||||
extension VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderServiceDelegate {
|
||||
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState) {
|
||||
self.state.recordingState = state
|
||||
}
|
||||
|
||||
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateRemainingTime remainingTime: UInt) {
|
||||
self.updateRemainingTime(remainingTime)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user