Display voice broadcast total length (#9517)

This commit is contained in:
Michael Weimann 2022-10-31 18:35:02 +01:00 committed by GitHub
parent 9b644844da
commit 66c20a0798
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 443 additions and 94 deletions

View file

@ -27,6 +27,7 @@ import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback
import { Icon as PlayIcon } from "../../../../res/img/element-icons/play.svg";
import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg";
import { _t } from "../../../languageHandler";
import Clock from "../../../components/views/audio_messages/Clock";
interface VoiceBroadcastPlaybackBodyProps {
playback: VoiceBroadcastPlayback;
@ -36,6 +37,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
playback,
}) => {
const {
length,
live,
room,
sender,
@ -73,6 +75,8 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
/>;
}
const lengthSeconds = Math.round(length / 1000);
return (
<div className="mx_VoiceBroadcastBody">
<VoiceBroadcastHeader
@ -84,6 +88,9 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
<div className="mx_VoiceBroadcastBody_controls">
{ control }
</div>
<div className="mx_VoiceBroadcastBody_timerow">
<Clock seconds={lengthSeconds} />
</div>
</div>
);
};

View file

@ -50,7 +50,15 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => {
},
);
const [length, setLength] = useState(playback.getLength());
useTypedEventEmitter(
playback,
VoiceBroadcastPlaybackEvent.LengthChanged,
length => setLength(length),
);
return {
length,
live: playbackInfoState !== VoiceBroadcastInfoState.Stopped,
room: room,
sender: playback.infoEvent.sender,

View file

@ -31,6 +31,7 @@ import { IDestroyable } from "../../utils/IDestroyable";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
import { getReferenceRelationsForEvent } from "../../events";
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
export enum VoiceBroadcastPlaybackState {
Paused,
@ -59,9 +60,9 @@ export class VoiceBroadcastPlayback
implements IDestroyable {
private state = VoiceBroadcastPlaybackState.Stopped;
private infoState: VoiceBroadcastInfoState;
private chunkEvents = new Map<string, MatrixEvent>();
private queue: Playback[] = [];
private currentlyPlaying: Playback;
private chunkEvents = new VoiceBroadcastChunkEvents();
private playbacks = new Map<string, Playback>();
private currentlyPlaying: MatrixEvent;
private lastInfoEvent: MatrixEvent;
private chunkRelationHelper: RelationsHelper;
private infoRelationHelper: RelationsHelper;
@ -101,11 +102,12 @@ export class VoiceBroadcastPlayback
if (!eventId
|| eventId.startsWith("~!") // don't add local events
|| event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event
|| this.chunkEvents.has(eventId)) {
) {
return false;
}
this.chunkEvents.set(eventId, event);
this.chunkEvents.addEvent(event);
this.emit(VoiceBroadcastPlaybackEvent.LengthChanged, this.chunkEvents.getLength());
if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) {
await this.enqueueChunk(event);
@ -143,6 +145,8 @@ export class VoiceBroadcastPlayback
return;
}
this.chunkEvents.addEvents(chunkEvents);
for (const chunkEvent of chunkEvents) {
await this.enqueueChunk(chunkEvent);
}
@ -158,7 +162,7 @@ export class VoiceBroadcastPlayback
const playback = PlaybackManager.instance.createPlaybackInstance(buffer);
await playback.prepare();
playback.clockInfo.populatePlaceholdersFrom(chunkEvent);
this.queue[sequenceNumber - 1] = playback; // -1 because the sequence number starts at 1
this.playbacks.set(chunkEvent.getId(), playback);
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, state));
}
@ -167,16 +171,18 @@ export class VoiceBroadcastPlayback
return;
}
await this.playNext(playback);
await this.playNext();
}
private async playNext(current: Playback): Promise<void> {
const next = this.queue[this.queue.indexOf(current) + 1];
private async playNext(): Promise<void> {
if (!this.currentlyPlaying) return;
const next = this.chunkEvents.getNext(this.currentlyPlaying);
if (next) {
this.setState(VoiceBroadcastPlaybackState.Playing);
this.currentlyPlaying = next;
await next.play();
await this.playbacks.get(next.getId())?.play();
return;
}
@ -188,19 +194,25 @@ export class VoiceBroadcastPlayback
}
}
public getLength(): number {
return this.chunkEvents.getLength();
}
public async start(): Promise<void> {
if (this.queue.length === 0) {
if (this.playbacks.size === 0) {
await this.loadChunks();
}
const toPlayIndex = this.getInfoState() === VoiceBroadcastInfoState.Stopped
? 0 // start at the beginning for an ended voice broadcast
: this.queue.length - 1; // start at the current chunk for an ongoing voice broadcast
const chunkEvents = this.chunkEvents.getEvents();
if (this.queue[toPlayIndex]) {
const toPlay = this.getInfoState() === VoiceBroadcastInfoState.Stopped
? chunkEvents[0] // start at the beginning for an ended voice broadcast
: chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast
if (this.playbacks.has(toPlay?.getId())) {
this.setState(VoiceBroadcastPlaybackState.Playing);
this.currentlyPlaying = this.queue[toPlayIndex];
await this.currentlyPlaying.play();
this.currentlyPlaying = toPlay;
await this.playbacks.get(toPlay.getId()).play();
return;
}
@ -208,14 +220,14 @@ export class VoiceBroadcastPlayback
}
public get length(): number {
return this.chunkEvents.size;
return this.chunkEvents.getLength();
}
public stop(): void {
this.setState(VoiceBroadcastPlaybackState.Stopped);
if (this.currentlyPlaying) {
this.currentlyPlaying.stop();
this.playbacks.get(this.currentlyPlaying.getId()).stop();
}
}
@ -225,7 +237,7 @@ export class VoiceBroadcastPlayback
this.setState(VoiceBroadcastPlaybackState.Paused);
if (!this.currentlyPlaying) return;
this.currentlyPlaying.pause();
this.playbacks.get(this.currentlyPlaying.getId()).pause();
}
public resume(): void {
@ -236,7 +248,7 @@ export class VoiceBroadcastPlayback
}
this.setState(VoiceBroadcastPlaybackState.Playing);
this.currentlyPlaying.play();
this.playbacks.get(this.currentlyPlaying.getId()).play();
}
/**
@ -285,15 +297,13 @@ export class VoiceBroadcastPlayback
this.emit(VoiceBroadcastPlaybackEvent.InfoStateChanged, state);
}
private destroyQueue(): void {
this.queue.forEach(p => p.destroy());
this.queue = [];
}
public destroy(): void {
this.chunkRelationHelper.destroy();
this.infoRelationHelper.destroy();
this.removeAllListeners();
this.destroyQueue();
this.chunkEvents = new VoiceBroadcastChunkEvents();
this.playbacks.forEach(p => p.destroy());
this.playbacks = new Map<string, Playback>();
}
}

View file

@ -0,0 +1,99 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
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 { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { VoiceBroadcastChunkEventType } from "..";
/**
* Voice broadcast chunk collection.
* Orders chunks by sequence (if available) or timestamp.
*/
export class VoiceBroadcastChunkEvents {
private events: MatrixEvent[] = [];
public getEvents(): MatrixEvent[] {
return [...this.events];
}
public getNext(event: MatrixEvent): MatrixEvent | undefined {
return this.events[this.events.indexOf(event) + 1];
}
public addEvent(event: MatrixEvent): void {
if (this.addOrReplaceEvent(event)) {
this.sort();
}
}
public addEvents(events: MatrixEvent[]): void {
const atLeastOneNew = events.reduce((newSoFar: boolean, event: MatrixEvent): boolean => {
return this.addOrReplaceEvent(event) || newSoFar;
}, false);
if (atLeastOneNew) {
this.sort();
}
}
public includes(event: MatrixEvent): boolean {
return !!this.events.find(e => e.getId() === event.getId());
}
public getLength(): number {
return this.events.reduce((length: number, event: MatrixEvent) => {
return length + this.calculateChunkLength(event);
}, 0);
}
private calculateChunkLength(event: MatrixEvent): number {
return event.getContent()?.["org.matrix.msc1767.audio"]?.duration
|| event.getContent()?.info?.duration
|| 0;
}
private addOrReplaceEvent = (event: MatrixEvent): boolean => {
this.events = this.events.filter(e => e.getId() !== event.getId());
this.events.push(event);
return true;
};
/**
* Sort by sequence, if available for all events.
* Else fall back to timestamp.
*/
private sort(): void {
const compareFn = this.allHaveSequence() ? this.compareBySequence : this.compareByTimestamp;
this.events.sort(compareFn);
}
private compareBySequence = (a: MatrixEvent, b: MatrixEvent): number => {
const aSequence = a.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0;
const bSequence = b.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0;
return aSequence - bSequence;
};
private compareByTimestamp = (a: MatrixEvent, b: MatrixEvent): number => {
return a.getTs() - b.getTs();
};
private allHaveSequence(): boolean {
return !this.events.some((event: MatrixEvent) => {
const sequence = event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence;
return parseInt(sequence, 10) !== sequence;
});
}
}