Display live voice broadcast

This commit is contained in:
yostyle 2022-10-20 16:47:56 +02:00
parent 0690f9ac65
commit b1cde5f719
9 changed files with 156 additions and 32 deletions

View file

@ -406,7 +406,7 @@ final class BuildSettings: NSObject {
static let locationSharingEnabled = true
// MARK: - Voice Broadcast
static let voiceBroadcastChunkLength: Int = 600
static let voiceBroadcastChunkLength: Int = 120
static let voiceBroadcastMaxLength: UInt64 = 144000
// MARK: - MXKAppSettings

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "voice_broadcast_live.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.4589 2.79061C13.2328 2.49998 12.814 2.44762 12.5234 2.67367C12.233 2.89946 12.1805 3.31764 12.4057 3.60823L12.4062 3.60888L12.4068 3.60975L12.4159 3.62185C12.4248 3.63372 12.439 3.65309 12.4578 3.67965C12.4956 3.7328 12.5517 3.8145 12.6202 3.9221C12.7574 4.13766 12.9427 4.45508 13.1285 4.85325C13.502 5.65361 13.866 6.75551 13.866 7.9999C13.866 9.24429 13.502 10.3462 13.1285 11.1465C12.9427 11.5447 12.7574 11.8621 12.6202 12.0777C12.5517 12.1853 12.4956 12.267 12.4578 12.3201C12.439 12.3467 12.4248 12.3661 12.4159 12.3779L12.4068 12.3901L12.4062 12.3909L12.4056 12.3917C12.1805 12.6823 12.2331 13.1004 12.5234 13.3261C12.814 13.5522 13.2328 13.4998 13.4589 13.2092L12.962 12.8228C13.4589 13.2092 13.4589 13.2092 13.4589 13.2092L13.4602 13.2075L13.4621 13.205L13.4677 13.1978L13.4853 13.1743C13.4999 13.1547 13.5201 13.1272 13.5449 13.0921C13.5947 13.0221 13.6636 12.9216 13.7451 12.7935C13.9079 12.5377 14.1226 12.1694 14.3368 11.7104C14.7633 10.7965 15.1993 9.49837 15.1993 7.9999C15.1993 6.50143 14.7633 5.20333 14.3368 4.28941C14.1226 3.83043 13.9079 3.46214 13.7451 3.20627C13.6636 3.07816 13.5947 2.97772 13.5449 2.90765C13.5201 2.8726 13.4999 2.8451 13.4853 2.8255L13.4677 2.80202L13.4621 2.79479L13.4602 2.79232L13.4595 2.79137C13.4595 2.79137 13.4589 2.79061 12.9326 3.1999L13.4589 2.79061Z" fill="white"/>
<path d="M11.7261 5.19075C11.5001 4.90012 11.0812 4.84777 10.7906 5.07381C10.5007 5.29927 10.4479 5.71652 10.6719 6.00706L10.6742 6.01011C10.6772 6.01411 10.6828 6.02177 10.6907 6.03292C10.7066 6.05526 10.7315 6.09143 10.7625 6.14011C10.8246 6.23781 10.91 6.3838 10.9958 6.56768C11.1693 6.93947 11.3332 7.44137 11.3332 8.00005C11.3332 8.55872 11.1693 9.06062 10.9958 9.43241C10.91 9.61629 10.8246 9.76229 10.7625 9.85999C10.7315 9.90866 10.7066 9.94483 10.6907 9.96717C10.6828 9.97832 10.6772 9.98598 10.6742 9.98998L10.6719 9.99304C10.4479 10.2836 10.5007 10.7008 10.7906 10.9263C11.0812 11.1523 11.5001 11.1 11.7261 10.8093L11.1999 10.4C11.7261 10.8093 11.7261 10.8093 11.7261 10.8093L11.7273 10.8078L11.7288 10.8059L11.7326 10.8009L11.7436 10.7863C11.7523 10.7746 11.7639 10.7588 11.7778 10.7392C11.8057 10.6999 11.8433 10.645 11.8873 10.5758C11.9752 10.4378 12.0898 10.2409 12.204 9.99625C12.4305 9.5109 12.6666 8.8128 12.6666 8.00005C12.6666 7.18729 12.4305 6.48919 12.204 6.00384C12.0898 5.75915 11.9752 5.56229 11.8873 5.42427C11.8433 5.35509 11.8057 5.30019 11.7778 5.26092C11.7639 5.24127 11.7523 5.22549 11.7436 5.21376L11.7326 5.19918L11.7288 5.19419L11.7273 5.19228L11.7267 5.19147C11.7267 5.19147 11.7261 5.19075 11.1999 5.60005L11.7261 5.19075Z" fill="white"/>
<path d="M2.40733 13.2094C2.63337 13.5 3.05223 13.5524 3.34286 13.3263C3.63317 13.1005 3.68572 12.6824 3.46054 12.3918L3.46004 12.3911L3.45939 12.3903L3.45029 12.3781C3.44145 12.3663 3.42722 12.3469 3.40836 12.3203C3.37062 12.2672 3.31448 12.1855 3.246 12.0779C3.10883 11.8623 2.9235 11.5449 2.73768 11.1467C2.36418 10.3464 2.00023 9.24449 2.00023 8.0001C2.00023 6.75571 2.36418 5.65381 2.73768 4.85345C2.9235 4.45528 3.10883 4.13786 3.246 3.92231C3.31448 3.8147 3.37062 3.733 3.40836 3.67985C3.42722 3.65329 3.44145 3.63392 3.45029 3.62205L3.45939 3.60995L3.46004 3.60909L3.46064 3.60831C3.68571 3.31772 3.63313 2.89963 3.34286 2.67387C3.05223 2.44782 2.63337 2.50018 2.40733 2.79081L2.90417 3.17724C2.40732 2.79082 2.40733 2.79081 2.40733 2.79081L2.406 2.79252L2.40409 2.795L2.39856 2.80223L2.3809 2.8257C2.3663 2.84531 2.34615 2.8728 2.32126 2.90785C2.2715 2.97792 2.20265 3.07836 2.12112 3.20647C1.95829 3.46234 1.74363 3.83063 1.52944 4.28961C1.10294 5.20353 0.666896 6.50163 0.666896 8.0001C0.666896 9.49857 1.10294 10.7967 1.52944 11.7106C1.74363 12.1696 1.95829 12.5379 2.12112 12.7937C2.20265 12.9218 2.2715 13.0223 2.32126 13.0923C2.34615 13.1274 2.3663 13.1549 2.3809 13.1745L2.39856 13.198L2.40409 13.2052L2.406 13.2077L2.40674 13.2086C2.40674 13.2086 2.40733 13.2094 2.93356 12.8001L2.40733 13.2094Z" fill="white"/>
<path d="M4.14008 10.8092C4.36612 11.0999 4.78497 11.1522 5.0756 10.9262C5.36548 10.7007 5.41832 10.2835 5.19431 9.99294L5.19202 9.98989C5.18904 9.98589 5.18341 9.97823 5.17549 9.96708C5.15962 9.94474 5.13473 9.90857 5.10375 9.85989C5.04158 9.76219 4.95625 9.6162 4.87043 9.43232C4.69693 9.06053 4.53298 8.55863 4.53298 7.99995C4.53298 7.44128 4.69693 6.93938 4.87043 6.56759C4.95625 6.38371 5.04158 6.23771 5.10375 6.14001C5.13473 6.09134 5.15962 6.05517 5.17549 6.03283C5.18341 6.02168 5.18904 6.01402 5.19202 6.01002L5.19432 6.00696C5.41832 5.71642 5.36547 5.29917 5.0756 5.07372C4.78497 4.84767 4.36612 4.90003 4.14008 5.19066L4.66631 5.59996C4.14008 5.19066 4.14008 5.19066 4.14008 5.19066L4.13889 5.19218L4.13742 5.1941L4.1336 5.19909L4.12263 5.21366C4.11389 5.2254 4.10234 5.24118 4.08838 5.26083C4.0605 5.3001 4.02289 5.355 3.97887 5.42418C3.89104 5.56219 3.77638 5.75906 3.66219 6.00375C3.43569 6.4891 3.19964 7.1872 3.19964 7.99995C3.19964 8.81271 3.43569 9.51081 3.66219 9.99616C3.77638 10.2409 3.89104 10.4377 3.97887 10.5757C4.02289 10.6449 4.0605 10.6998 4.08838 10.7391C4.10234 10.7587 4.11389 10.7745 4.12263 10.7862L4.1336 10.8008L4.13742 10.8058L4.13889 10.8077L4.13952 10.8085C4.13952 10.8085 4.14008 10.8092 4.66631 10.4L4.14008 10.8092Z" fill="white"/>
<circle cx="8.00033" cy="8.00033" r="1.33333" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -335,6 +335,7 @@ internal class Asset: NSObject {
internal static let tabHome = ImageAsset(name: "tab_home")
internal static let tabPeople = ImageAsset(name: "tab_people")
internal static let tabRooms = ImageAsset(name: "tab_rooms")
internal static let voiceBroadcastLive = ImageAsset(name: "voice_broadcast_live")
internal static let voiceBroadcastPause = ImageAsset(name: "voice_broadcast_pause")
internal static let voiceBroadcastPlay = ImageAsset(name: "voice_broadcast_play")
internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo")

View file

@ -26,6 +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 voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator)
}
@ -56,6 +57,7 @@ public class VoiceBroadcastAggregator {
}
public private(set) var isStarted: Bool = false
public private(set) var voiceBroadcastState: VoiceBroadcastInfo.State
public var delegate: VoiceBroadcastAggregatorDelegate?
deinit {
@ -64,14 +66,15 @@ public class VoiceBroadcastAggregator {
}
}
public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String) throws {
public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String, voiceBroadcastState: VoiceBroadcastInfo.State) throws {
self.session = session
self.room = room
self.voiceBroadcastStartEventId = voiceBroadcastStartEventId
self.voiceBroadcastState = voiceBroadcastState
self.voiceBroadcastBuilder = VoiceBroadcastBuilder()
NotificationCenter.default.addObserver(self, selector: #selector(handleRoomDataFlush), name: NSNotification.Name.mxRoomDidFlushData, object: self.room)
try buildVoiceBroadcastStartContent()
}
@ -102,6 +105,20 @@ public class VoiceBroadcastAggregator {
MXLog.warning("[VoiceBroadcastAggregator] handleRoomDataFlush is not supported yet")
}
private func updateState() {
self.room.state { roomState in
guard let event = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last,
event.stateKey == self.voiceBroadcastSenderId,
let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: event.content),
(event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.eventId == self.voiceBroadcastStartEventId),
let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) else {
return
}
self.delegate?.voiceBroadcastAggregator(self, didReceiveState: state)
}
}
func start() {
if isStarted {
return
@ -121,26 +138,38 @@ public class VoiceBroadcastAggregator {
let eventTypes = [VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType, kMXEventTypeStringRoomMessage]
self.referenceEventsListener = self.room.listen(toEventsOfTypes: eventTypes) { [weak self] event, direction, state in
guard let self = self,
event.sender == self.voiceBroadcastSenderId,
let relatedEventId = event.relatesTo?.eventId,
relatedEventId == self.voiceBroadcastStartEventId,
event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else {
guard let self = self else {
return
}
if let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) {
self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
if event.eventType == .roomMessage {
guard event.sender == self.voiceBroadcastSenderId,
let relatedEventId = event.relatesTo?.eventId,
relatedEventId == self.voiceBroadcastStartEventId,
event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else {
return
}
if let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) {
self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
}
if !self.events.contains(where: { newEvent in
newEvent.eventId == event.eventId
}) {
self.events.append(event)
MXLog.debug("[VoiceBroadcastAggregator] Got a new chunk for broadcast \(relatedEventId). Total: \(self.events.count)")
self.voiceBroadcast = self.voiceBroadcastBuilder.build(mediaManager: self.session.mediaManager,
voiceBroadcastStartEventId: self.voiceBroadcastStartEventId,
voiceBroadcastInvoiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
events: self.events,
currentUserIdentifier: self.session.myUserId)
}
} else {
self.updateState()
}
self.events.append(event)
MXLog.debug("[VoiceBroadcastAggregator] Got a new chunk for broadcast \(relatedEventId). Total: \(self.events.count)")
self.voiceBroadcast = self.voiceBroadcastBuilder.build(mediaManager: self.session.mediaManager,
voiceBroadcastStartEventId: self.voiceBroadcastStartEventId,
voiceBroadcastInvoiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
events: self.events,
currentUserIdentifier: self.session.myUserId)
} as Any
@ -150,6 +179,8 @@ public class VoiceBroadcastAggregator {
}
self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
}
self.updateState()
self.voiceBroadcast = self.voiceBroadcastBuilder.build(mediaManager: self.session.mediaManager,
voiceBroadcastStartEventId: self.voiceBroadcastStartEventId,

View file

@ -22,6 +22,7 @@ struct VoiceBroadcastPlaybackCoordinatorParameters {
let session: MXSession
let room: MXRoom
let voiceBroadcastStartEvent: MXEvent
let voiceBroadcastState: VoiceBroadcastInfo.State
let senderDisplayName: String?
}
@ -45,7 +46,7 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable {
init(parameters: VoiceBroadcastPlaybackCoordinatorParameters) throws {
self.parameters = parameters
let voiceBroadcastAggregator = try VoiceBroadcastAggregator(session: parameters.session, room: parameters.room, voiceBroadcastStartEventId: parameters.voiceBroadcastStartEvent.eventId)
let voiceBroadcastAggregator = try VoiceBroadcastAggregator(session: parameters.session, room: parameters.room, voiceBroadcastStartEventId: parameters.voiceBroadcastStartEvent.eventId, voiceBroadcastState: parameters.voiceBroadcastState)
let details = VoiceBroadcastPlaybackDetails(senderDisplayName: parameters.senderDisplayName)
viewModel = VoiceBroadcastPlaybackViewModel(details: details,

View file

@ -35,9 +35,26 @@ class VoiceBroadcastPlaybackProvider {
return coordinator.toPresentable().view
}
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
var voiceBroadcastState = VoiceBroadcastInfo.State.stopped
room.state { roomState in
if let stateEvent = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last,
stateEvent.stateKey == event.stateKey,
let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: stateEvent.content),
(stateEvent.eventId == event.eventId || voiceBroadcastInfo.eventId == event.eventId),
let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) {
voiceBroadcastState = state
}
dispatchGroup.leave()
}
let parameters = VoiceBroadcastPlaybackCoordinatorParameters(session: session,
room: room,
voiceBroadcastStartEvent: event,
voiceBroadcastState: voiceBroadcastState,
senderDisplayName: senderDisplayName)
guard let coordinator = try? VoiceBroadcastPlaybackCoordinator(parameters: parameters) else {
return nil
@ -46,6 +63,7 @@ class VoiceBroadcastPlaybackProvider {
coordinatorsForEventIdentifiers[event.eventId] = coordinator
return coordinator.toPresentable().view
}
/// Retrieve the voiceBroadcast timeline coordinator for the given event or nil if it hasn't been created yet

View file

@ -23,6 +23,13 @@ struct VoiceBroadcastPlaybackView: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
private var backgroundColor: Color {
if viewModel.viewState.playbackState == .playingLive {
return theme.colors.alert
}
return theme.colors.quarterlyContent
}
// MARK: Public
@ObservedObject var viewModel: VoiceBroadcastPlaybackViewModel.Context
@ -30,25 +37,52 @@ struct VoiceBroadcastPlaybackView: View {
var body: some View {
let details = viewModel.viewState.details
VStack(alignment: .leading, spacing: 16.0) {
Text(details.senderDisplayName ?? "")
//Text(VectorL10n.voiceBroadcastInTimelineTitle)
.font(theme.fonts.bodySB)
.foregroundColor(theme.colors.primaryContent)
VStack(alignment: .center, spacing: 16.0) {
HStack {
Text(details.senderDisplayName ?? "")
//Text(VectorL10n.voiceBroadcastInTimelineTitle)
.font(theme.fonts.bodySB)
.foregroundColor(theme.colors.primaryContent)
if viewModel.viewState.broadcastState == .live {
Button { viewModel.send(viewAction: .playLive) } label:
{
HStack {
Image(uiImage: Asset.Images.voiceBroadcastLive.image)
.renderingMode(.original)
Text("Live")
.font(theme.fonts.bodySB)
.foregroundColor(Color.white)
}
}
.accessibilityIdentifier("liveButton")
.background(backgroundColor)
}
}
if viewModel.viewState.playbackState == .error {
VoiceBroadcastPlaybackErrorView()
} else {
HStack(alignment: .top, spacing: 16.0) {
if viewModel.viewState.playbackState == .playing {
ZStack {
if viewModel.viewState.playbackState == .playing ||
viewModel.viewState.playbackState == .playingLive {
Button { viewModel.send(viewAction: .pause) } label: {
Image("voice_broadcast_pause")
Image(uiImage: Asset.Images.voiceBroadcastPause.image)
.renderingMode(.original)
}
.accessibilityIdentifier("pauseButton")
} else {
Button { viewModel.send(viewAction: .play) } label: {
Image("voice_broadcast_play")
} else {
Button {
if viewModel.viewState.broadcastState == .live &&
viewModel.viewState.playbackState == .stopped {
viewModel.send(viewAction: .playLive)
} else {
viewModel.send(viewAction: .play)
}
} label: {
Image(uiImage: Asset.Images.voiceBroadcastPlay.image)
.renderingMode(.original)
}
.disabled(viewModel.viewState.playbackState == .buffering)

View file

@ -47,7 +47,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
self.voiceBroadcastAggregator = voiceBroadcastAggregator
let viewState = VoiceBroadcastPlaybackViewState(details: details,
broadcastState: .unknown,
broadcastState: VoiceBroadcastPlaybackViewModel.getBroadcastState(from: voiceBroadcastAggregator.voiceBroadcastState),
playbackState: .stopped,
bindings: VoiceBroadcastPlaybackViewStateBindings())
super.init(initialViewState: viewState)
@ -239,6 +239,22 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
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
@ -257,6 +273,10 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate {
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