Support voice broadcast live playback (#7094)

This commit is contained in:
Yoan Pintas 2022-11-25 20:59:34 +01:00 committed by GitHub
parent 11e39db7ab
commit 17b45aa79b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 116 additions and 148 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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<String?>) -> 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<String?>) -> 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<String?>) -> 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<String?>) -> 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<String?>) -> Void) {
private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState, completion: @escaping (MXResponse<String?>) -> 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()

View file

@ -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)

View file

@ -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?
}

View file

@ -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

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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

View file

@ -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],

View file

@ -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

View file

@ -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

1
changelog.d/6721.wip Normal file
View file

@ -0,0 +1 @@
Labs: VoiceBroadcast - Add the Voice Broadcast option in the room functionalities

1
changelog.d/7094.bugfix Normal file
View file

@ -0,0 +1 @@
Support voice broadcast live playback