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

Commit 2146c91

Browse files
authored
Merge branch 'develop' into feat/reply-support-wysiwyg-composer
2 parents d1a98b6 + cf1b592 commit 2146c91

File tree

12 files changed

+518
-81
lines changed

12 files changed

+518
-81
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 { EventType, MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
18+
import { Relations } from "matrix-js-sdk/src/models/relations";
19+
20+
import { VoiceBroadcastInfoEventType } from "../voice-broadcast";
21+
22+
export const getReferenceRelationsForEvent = (
23+
event: MatrixEvent,
24+
messageType: EventType | typeof VoiceBroadcastInfoEventType,
25+
client: MatrixClient,
26+
): Relations | undefined => {
27+
const room = client.getRoom(event.getRoomId());
28+
return room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
29+
event.getId(),
30+
RelationType.Reference,
31+
messageType,
32+
);
33+
};

src/events/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ limitations under the License.
1616

1717
export { getForwardableEvent } from './forward/getForwardableEvent';
1818
export { getShareableLocationEvent } from './location/getShareableLocationEvent';
19+
export * from "./getReferenceRelationsForEvent";

src/voice-broadcast/components/VoiceBroadcastBody.tsx

+4-8
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import React from "react";
18-
import { MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
18+
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
1919

2020
import {
2121
VoiceBroadcastRecordingBody,
@@ -28,15 +28,11 @@ import {
2828
} from "..";
2929
import { IBodyProps } from "../../components/views/messages/IBodyProps";
3030
import { MatrixClientPeg } from "../../MatrixClientPeg";
31+
import { getReferenceRelationsForEvent } from "../../events";
3132

3233
export const VoiceBroadcastBody: React.FC<IBodyProps> = ({ mxEvent }) => {
3334
const client = MatrixClientPeg.get();
34-
const room = client.getRoom(mxEvent.getRoomId());
35-
const relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
36-
mxEvent.getId(),
37-
RelationType.Reference,
38-
VoiceBroadcastInfoEventType,
39-
);
35+
const relations = getReferenceRelationsForEvent(mxEvent, VoiceBroadcastInfoEventType, client);
4036
const relatedEvents = relations?.getRelations();
4137
const state = !relatedEvents?.find((event: MatrixEvent) => {
4238
return event.getContent()?.state === VoiceBroadcastInfoState.Stopped;
@@ -49,7 +45,7 @@ export const VoiceBroadcastBody: React.FC<IBodyProps> = ({ mxEvent }) => {
4945
/>;
5046
}
5147

52-
const playback = VoiceBroadcastPlaybacksStore.instance().getByInfoEvent(mxEvent);
48+
const playback = VoiceBroadcastPlaybacksStore.instance().getByInfoEvent(mxEvent, client);
5349
return <VoiceBroadcastPlaybackBody
5450
playback={playback}
5551
/>;

src/voice-broadcast/models/VoiceBroadcastPlayback.ts

+186-7
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,24 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
17+
import {
18+
EventType,
19+
MatrixClient,
20+
MatrixEvent,
21+
MatrixEventEvent,
22+
MsgType,
23+
RelationType,
24+
} from "matrix-js-sdk/src/matrix";
25+
import { Relations, RelationsEvent } from "matrix-js-sdk/src/models/relations";
1826
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
1927

28+
import { Playback, PlaybackState } from "../../audio/Playback";
29+
import { PlaybackManager } from "../../audio/PlaybackManager";
30+
import { getReferenceRelationsForEvent } from "../../events";
31+
import { UPDATE_EVENT } from "../../stores/AsyncStore";
32+
import { MediaEventHelper } from "../../utils/MediaEventHelper";
2033
import { IDestroyable } from "../../utils/IDestroyable";
34+
import { VoiceBroadcastChunkEventType } from "..";
2135

2236
export enum VoiceBroadcastPlaybackState {
2337
Paused,
@@ -26,51 +40,216 @@ export enum VoiceBroadcastPlaybackState {
2640
}
2741

2842
export enum VoiceBroadcastPlaybackEvent {
43+
LengthChanged = "length_changed",
2944
StateChanged = "state_changed",
3045
}
3146

3247
interface EventMap {
48+
[VoiceBroadcastPlaybackEvent.LengthChanged]: (length: number) => void;
3349
[VoiceBroadcastPlaybackEvent.StateChanged]: (state: VoiceBroadcastPlaybackState) => void;
3450
}
3551

3652
export class VoiceBroadcastPlayback
3753
extends TypedEventEmitter<VoiceBroadcastPlaybackEvent, EventMap>
3854
implements IDestroyable {
3955
private state = VoiceBroadcastPlaybackState.Stopped;
56+
private chunkEvents = new Map<string, MatrixEvent>();
57+
/** Holds the playback qeue with a 1-based index (sequence number) */
58+
private queue: Playback[] = [];
59+
private currentlyPlaying: Playback;
60+
private relations: Relations;
4061

4162
public constructor(
4263
public readonly infoEvent: MatrixEvent,
64+
private client: MatrixClient,
4365
) {
4466
super();
67+
this.setUpRelations();
4568
}
4669

47-
public start() {
70+
private addChunkEvent(event: MatrixEvent): boolean {
71+
const eventId = event.getId();
72+
73+
if (!eventId
74+
|| eventId.startsWith("~!") // don't add local events
75+
|| event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event
76+
|| this.chunkEvents.has(eventId)) {
77+
return false;
78+
}
79+
80+
this.chunkEvents.set(eventId, event);
81+
return true;
82+
}
83+
84+
private setUpRelations(): void {
85+
const relations = getReferenceRelationsForEvent(this.infoEvent, EventType.RoomMessage, this.client);
86+
87+
if (!relations) {
88+
// No related events, yet. Set up relation watcher.
89+
this.infoEvent.on(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
90+
return;
91+
}
92+
93+
this.relations = relations;
94+
relations.getRelations()?.forEach(e => this.addChunkEvent(e));
95+
relations.on(RelationsEvent.Add, this.onRelationsEventAdd);
96+
97+
if (this.chunkEvents.size > 0) {
98+
this.emitLengthChanged();
99+
}
100+
}
101+
102+
private onRelationsEventAdd = (event: MatrixEvent) => {
103+
if (this.addChunkEvent(event)) {
104+
this.emitLengthChanged();
105+
}
106+
};
107+
108+
private emitLengthChanged(): void {
109+
this.emit(VoiceBroadcastPlaybackEvent.LengthChanged, this.chunkEvents.size);
110+
}
111+
112+
private onRelationsCreated = (relationType: string) => {
113+
if (relationType !== RelationType.Reference) {
114+
return;
115+
}
116+
117+
this.infoEvent.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
118+
this.setUpRelations();
119+
};
120+
121+
private async loadChunks(): Promise<void> {
122+
const relations = getReferenceRelationsForEvent(this.infoEvent, EventType.RoomMessage, this.client);
123+
const chunkEvents = relations?.getRelations();
124+
125+
if (!chunkEvents) {
126+
return;
127+
}
128+
129+
for (const chunkEvent of chunkEvents) {
130+
await this.enqueueChunk(chunkEvent);
131+
}
132+
}
133+
134+
private async enqueueChunk(chunkEvent: MatrixEvent) {
135+
const sequenceNumber = parseInt(chunkEvent.getContent()?.[VoiceBroadcastChunkEventType]?.sequence, 10);
136+
if (isNaN(sequenceNumber)) return;
137+
138+
const helper = new MediaEventHelper(chunkEvent);
139+
const blob = await helper.sourceBlob.value;
140+
const buffer = await blob.arrayBuffer();
141+
const playback = PlaybackManager.instance.createPlaybackInstance(buffer);
142+
await playback.prepare();
143+
playback.clockInfo.populatePlaceholdersFrom(chunkEvent);
144+
this.queue[sequenceNumber] = playback;
145+
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, state));
146+
}
147+
148+
private onPlaybackStateChange(playback: Playback, newState: PlaybackState) {
149+
if (newState !== PlaybackState.Stopped) {
150+
return;
151+
}
152+
153+
const next = this.queue[this.queue.indexOf(playback) + 1];
154+
155+
if (next) {
156+
this.currentlyPlaying = next;
157+
next.play();
158+
return;
159+
}
160+
161+
this.setState(VoiceBroadcastPlaybackState.Stopped);
162+
}
163+
164+
public async start(): Promise<void> {
165+
if (this.queue.length === 0) {
166+
await this.loadChunks();
167+
}
168+
169+
if (this.queue.length === 0 || !this.queue[1]) {
170+
// set to stopped fi the queue is empty of the first chunk (sequence number: 1-based index) is missing
171+
this.setState(VoiceBroadcastPlaybackState.Stopped);
172+
return;
173+
}
174+
48175
this.setState(VoiceBroadcastPlaybackState.Playing);
176+
// index of the first schunk is the first sequence number
177+
const first = this.queue[1];
178+
this.currentlyPlaying = first;
179+
await first.play();
180+
}
181+
182+
public get length(): number {
183+
return this.chunkEvents.size;
49184
}
50185

51-
public stop() {
186+
public stop(): void {
52187
this.setState(VoiceBroadcastPlaybackState.Stopped);
188+
189+
if (this.currentlyPlaying) {
190+
this.currentlyPlaying.stop();
191+
}
53192
}
54193

55-
public toggle() {
194+
public pause(): void {
195+
if (!this.currentlyPlaying) return;
196+
197+
this.setState(VoiceBroadcastPlaybackState.Paused);
198+
this.currentlyPlaying.pause();
199+
}
200+
201+
public resume(): void {
202+
if (!this.currentlyPlaying) return;
203+
204+
this.setState(VoiceBroadcastPlaybackState.Playing);
205+
this.currentlyPlaying.play();
206+
}
207+
208+
/**
209+
* Toggles the playback:
210+
* stopped → playing
211+
* playing → paused
212+
* paused → playing
213+
*/
214+
public async toggle() {
56215
if (this.state === VoiceBroadcastPlaybackState.Stopped) {
57-
this.setState(VoiceBroadcastPlaybackState.Playing);
216+
await this.start();
58217
return;
59218
}
60219

61-
this.setState(VoiceBroadcastPlaybackState.Stopped);
220+
if (this.state === VoiceBroadcastPlaybackState.Paused) {
221+
this.resume();
222+
return;
223+
}
224+
225+
this.pause();
62226
}
63227

64228
public getState(): VoiceBroadcastPlaybackState {
65229
return this.state;
66230
}
67231

68232
private setState(state: VoiceBroadcastPlaybackState): void {
233+
if (this.state === state) {
234+
return;
235+
}
236+
69237
this.state = state;
70238
this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state);
71239
}
72240

73-
destroy(): void {
241+
private destroyQueue(): void {
242+
this.queue.forEach(p => p.destroy());
243+
this.queue = [];
244+
}
245+
246+
public destroy(): void {
247+
if (this.relations) {
248+
this.relations.off(RelationsEvent.Add, this.onRelationsEventAdd);
249+
}
250+
251+
this.infoEvent.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
74252
this.removeAllListeners();
253+
this.destroyQueue();
75254
}
76255
}

src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
17+
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
1818
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
1919

2020
import { VoiceBroadcastPlayback } from "..";
@@ -50,11 +50,11 @@ export class VoiceBroadcastPlaybacksStore extends TypedEventEmitter<VoiceBroadca
5050
return this.current;
5151
}
5252

53-
public getByInfoEvent(infoEvent: MatrixEvent): VoiceBroadcastPlayback {
53+
public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastPlayback {
5454
const infoEventId = infoEvent.getId();
5555

5656
if (!this.playbacks.has(infoEventId)) {
57-
this.playbacks.set(infoEventId, new VoiceBroadcastPlayback(infoEvent));
57+
this.playbacks.set(infoEventId, new VoiceBroadcastPlayback(infoEvent, client));
5858
}
5959

6060
return this.playbacks.get(infoEventId);

0 commit comments

Comments
 (0)