Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 66c20a0

Browse files
authored
Display voice broadcast total length (#9517)
1 parent 9b64484 commit 66c20a0

File tree

10 files changed

+443
-94
lines changed

10 files changed

+443
-94
lines changed

res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ limitations under the License.
1717
.mx_VoiceBroadcastBody {
1818
background-color: $quinary-content;
1919
border-radius: 8px;
20+
color: $secondary-content;
2021
display: inline-block;
22+
font-size: $font-12px;
2123
padding: $spacing-12;
2224
}
2325

@@ -37,3 +39,8 @@ limitations under the License.
3739
display: flex;
3840
justify-content: space-around;
3941
}
42+
43+
.mx_VoiceBroadcastBody_timerow {
44+
display: flex;
45+
justify-content: flex-end;
46+
}

src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback
2727
import { Icon as PlayIcon } from "../../../../res/img/element-icons/play.svg";
2828
import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg";
2929
import { _t } from "../../../languageHandler";
30+
import Clock from "../../../components/views/audio_messages/Clock";
3031

3132
interface VoiceBroadcastPlaybackBodyProps {
3233
playback: VoiceBroadcastPlayback;
@@ -36,6 +37,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
3637
playback,
3738
}) => {
3839
const {
40+
length,
3941
live,
4042
room,
4143
sender,
@@ -73,6 +75,8 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
7375
/>;
7476
}
7577

78+
const lengthSeconds = Math.round(length / 1000);
79+
7680
return (
7781
<div className="mx_VoiceBroadcastBody">
7882
<VoiceBroadcastHeader
@@ -84,6 +88,9 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
8488
<div className="mx_VoiceBroadcastBody_controls">
8589
{ control }
8690
</div>
91+
<div className="mx_VoiceBroadcastBody_timerow">
92+
<Clock seconds={lengthSeconds} />
93+
</div>
8794
</div>
8895
);
8996
};

src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,15 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => {
5050
},
5151
);
5252

53+
const [length, setLength] = useState(playback.getLength());
54+
useTypedEventEmitter(
55+
playback,
56+
VoiceBroadcastPlaybackEvent.LengthChanged,
57+
length => setLength(length),
58+
);
59+
5360
return {
61+
length,
5462
live: playbackInfoState !== VoiceBroadcastInfoState.Stopped,
5563
room: room,
5664
sender: playback.infoEvent.sender,

src/voice-broadcast/models/VoiceBroadcastPlayback.ts

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { IDestroyable } from "../../utils/IDestroyable";
3131
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
3232
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
3333
import { getReferenceRelationsForEvent } from "../../events";
34+
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
3435

3536
export enum VoiceBroadcastPlaybackState {
3637
Paused,
@@ -59,9 +60,9 @@ export class VoiceBroadcastPlayback
5960
implements IDestroyable {
6061
private state = VoiceBroadcastPlaybackState.Stopped;
6162
private infoState: VoiceBroadcastInfoState;
62-
private chunkEvents = new Map<string, MatrixEvent>();
63-
private queue: Playback[] = [];
64-
private currentlyPlaying: Playback;
63+
private chunkEvents = new VoiceBroadcastChunkEvents();
64+
private playbacks = new Map<string, Playback>();
65+
private currentlyPlaying: MatrixEvent;
6566
private lastInfoEvent: MatrixEvent;
6667
private chunkRelationHelper: RelationsHelper;
6768
private infoRelationHelper: RelationsHelper;
@@ -101,11 +102,12 @@ export class VoiceBroadcastPlayback
101102
if (!eventId
102103
|| eventId.startsWith("~!") // don't add local events
103104
|| event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event
104-
|| this.chunkEvents.has(eventId)) {
105+
) {
105106
return false;
106107
}
107108

108-
this.chunkEvents.set(eventId, event);
109+
this.chunkEvents.addEvent(event);
110+
this.emit(VoiceBroadcastPlaybackEvent.LengthChanged, this.chunkEvents.getLength());
109111

110112
if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) {
111113
await this.enqueueChunk(event);
@@ -143,6 +145,8 @@ export class VoiceBroadcastPlayback
143145
return;
144146
}
145147

148+
this.chunkEvents.addEvents(chunkEvents);
149+
146150
for (const chunkEvent of chunkEvents) {
147151
await this.enqueueChunk(chunkEvent);
148152
}
@@ -158,7 +162,7 @@ export class VoiceBroadcastPlayback
158162
const playback = PlaybackManager.instance.createPlaybackInstance(buffer);
159163
await playback.prepare();
160164
playback.clockInfo.populatePlaceholdersFrom(chunkEvent);
161-
this.queue[sequenceNumber - 1] = playback; // -1 because the sequence number starts at 1
165+
this.playbacks.set(chunkEvent.getId(), playback);
162166
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, state));
163167
}
164168

@@ -167,16 +171,18 @@ export class VoiceBroadcastPlayback
167171
return;
168172
}
169173

170-
await this.playNext(playback);
174+
await this.playNext();
171175
}
172176

173-
private async playNext(current: Playback): Promise<void> {
174-
const next = this.queue[this.queue.indexOf(current) + 1];
177+
private async playNext(): Promise<void> {
178+
if (!this.currentlyPlaying) return;
179+
180+
const next = this.chunkEvents.getNext(this.currentlyPlaying);
175181

176182
if (next) {
177183
this.setState(VoiceBroadcastPlaybackState.Playing);
178184
this.currentlyPlaying = next;
179-
await next.play();
185+
await this.playbacks.get(next.getId())?.play();
180186
return;
181187
}
182188

@@ -188,34 +194,40 @@ export class VoiceBroadcastPlayback
188194
}
189195
}
190196

197+
public getLength(): number {
198+
return this.chunkEvents.getLength();
199+
}
200+
191201
public async start(): Promise<void> {
192-
if (this.queue.length === 0) {
202+
if (this.playbacks.size === 0) {
193203
await this.loadChunks();
194204
}
195205

196-
const toPlayIndex = this.getInfoState() === VoiceBroadcastInfoState.Stopped
197-
? 0 // start at the beginning for an ended voice broadcast
198-
: this.queue.length - 1; // start at the current chunk for an ongoing voice broadcast
206+
const chunkEvents = this.chunkEvents.getEvents();
199207

200-
if (this.queue[toPlayIndex]) {
208+
const toPlay = this.getInfoState() === VoiceBroadcastInfoState.Stopped
209+
? chunkEvents[0] // start at the beginning for an ended voice broadcast
210+
: chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast
211+
212+
if (this.playbacks.has(toPlay?.getId())) {
201213
this.setState(VoiceBroadcastPlaybackState.Playing);
202-
this.currentlyPlaying = this.queue[toPlayIndex];
203-
await this.currentlyPlaying.play();
214+
this.currentlyPlaying = toPlay;
215+
await this.playbacks.get(toPlay.getId()).play();
204216
return;
205217
}
206218

207219
this.setState(VoiceBroadcastPlaybackState.Buffering);
208220
}
209221

210222
public get length(): number {
211-
return this.chunkEvents.size;
223+
return this.chunkEvents.getLength();
212224
}
213225

214226
public stop(): void {
215227
this.setState(VoiceBroadcastPlaybackState.Stopped);
216228

217229
if (this.currentlyPlaying) {
218-
this.currentlyPlaying.stop();
230+
this.playbacks.get(this.currentlyPlaying.getId()).stop();
219231
}
220232
}
221233

@@ -225,7 +237,7 @@ export class VoiceBroadcastPlayback
225237

226238
this.setState(VoiceBroadcastPlaybackState.Paused);
227239
if (!this.currentlyPlaying) return;
228-
this.currentlyPlaying.pause();
240+
this.playbacks.get(this.currentlyPlaying.getId()).pause();
229241
}
230242

231243
public resume(): void {
@@ -236,7 +248,7 @@ export class VoiceBroadcastPlayback
236248
}
237249

238250
this.setState(VoiceBroadcastPlaybackState.Playing);
239-
this.currentlyPlaying.play();
251+
this.playbacks.get(this.currentlyPlaying.getId()).play();
240252
}
241253

242254
/**
@@ -285,15 +297,13 @@ export class VoiceBroadcastPlayback
285297
this.emit(VoiceBroadcastPlaybackEvent.InfoStateChanged, state);
286298
}
287299

288-
private destroyQueue(): void {
289-
this.queue.forEach(p => p.destroy());
290-
this.queue = [];
291-
}
292-
293300
public destroy(): void {
294301
this.chunkRelationHelper.destroy();
295302
this.infoRelationHelper.destroy();
296303
this.removeAllListeners();
297-
this.destroyQueue();
304+
305+
this.chunkEvents = new VoiceBroadcastChunkEvents();
306+
this.playbacks.forEach(p => p.destroy());
307+
this.playbacks = new Map<string, Playback>();
298308
}
299309
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
18+
19+
import { VoiceBroadcastChunkEventType } from "..";
20+
21+
/**
22+
* Voice broadcast chunk collection.
23+
* Orders chunks by sequence (if available) or timestamp.
24+
*/
25+
export class VoiceBroadcastChunkEvents {
26+
private events: MatrixEvent[] = [];
27+
28+
public getEvents(): MatrixEvent[] {
29+
return [...this.events];
30+
}
31+
32+
public getNext(event: MatrixEvent): MatrixEvent | undefined {
33+
return this.events[this.events.indexOf(event) + 1];
34+
}
35+
36+
public addEvent(event: MatrixEvent): void {
37+
if (this.addOrReplaceEvent(event)) {
38+
this.sort();
39+
}
40+
}
41+
42+
public addEvents(events: MatrixEvent[]): void {
43+
const atLeastOneNew = events.reduce((newSoFar: boolean, event: MatrixEvent): boolean => {
44+
return this.addOrReplaceEvent(event) || newSoFar;
45+
}, false);
46+
47+
if (atLeastOneNew) {
48+
this.sort();
49+
}
50+
}
51+
52+
public includes(event: MatrixEvent): boolean {
53+
return !!this.events.find(e => e.getId() === event.getId());
54+
}
55+
56+
public getLength(): number {
57+
return this.events.reduce((length: number, event: MatrixEvent) => {
58+
return length + this.calculateChunkLength(event);
59+
}, 0);
60+
}
61+
62+
private calculateChunkLength(event: MatrixEvent): number {
63+
return event.getContent()?.["org.matrix.msc1767.audio"]?.duration
64+
|| event.getContent()?.info?.duration
65+
|| 0;
66+
}
67+
68+
private addOrReplaceEvent = (event: MatrixEvent): boolean => {
69+
this.events = this.events.filter(e => e.getId() !== event.getId());
70+
this.events.push(event);
71+
return true;
72+
};
73+
74+
/**
75+
* Sort by sequence, if available for all events.
76+
* Else fall back to timestamp.
77+
*/
78+
private sort(): void {
79+
const compareFn = this.allHaveSequence() ? this.compareBySequence : this.compareByTimestamp;
80+
this.events.sort(compareFn);
81+
}
82+
83+
private compareBySequence = (a: MatrixEvent, b: MatrixEvent): number => {
84+
const aSequence = a.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0;
85+
const bSequence = b.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0;
86+
return aSequence - bSequence;
87+
};
88+
89+
private compareByTimestamp = (a: MatrixEvent, b: MatrixEvent): number => {
90+
return a.getTs() - b.getTs();
91+
};
92+
93+
private allHaveSequence(): boolean {
94+
return !this.events.some((event: MatrixEvent) => {
95+
const sequence = event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence;
96+
return parseInt(sequence, 10) !== sequence;
97+
});
98+
}
99+
}

0 commit comments

Comments
 (0)