Display voice broadcast total length (#9517)
This commit is contained in:
parent
9b644844da
commit
66c20a0798
10 changed files with 443 additions and 94 deletions
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
99
src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts
Normal file
99
src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue