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

Commit 631720b

Browse files
authored
Start playback for ongoing broadcast with the last chunk (#9434)
1 parent 1b74782 commit 631720b

File tree

2 files changed

+124
-85
lines changed

2 files changed

+124
-85
lines changed

src/voice-broadcast/models/VoiceBroadcastPlayback.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export class VoiceBroadcastPlayback
5656
private state = VoiceBroadcastPlaybackState.Stopped;
5757
private infoState: VoiceBroadcastInfoState;
5858
private chunkEvents = new Map<string, MatrixEvent>();
59-
/** Holds the playback queue with a 1-based index (sequence number) */
6059
private queue: Playback[] = [];
6160
private currentlyPlaying: Playback;
6261
private lastInfoEvent: MatrixEvent;
@@ -138,15 +137,15 @@ export class VoiceBroadcastPlayback
138137

139138
private async enqueueChunk(chunkEvent: MatrixEvent) {
140139
const sequenceNumber = parseInt(chunkEvent.getContent()?.[VoiceBroadcastChunkEventType]?.sequence, 10);
141-
if (isNaN(sequenceNumber)) return;
140+
if (isNaN(sequenceNumber) || sequenceNumber < 1) return;
142141

143142
const helper = new MediaEventHelper(chunkEvent);
144143
const blob = await helper.sourceBlob.value;
145144
const buffer = await blob.arrayBuffer();
146145
const playback = PlaybackManager.instance.createPlaybackInstance(buffer);
147146
await playback.prepare();
148147
playback.clockInfo.populatePlaceholdersFrom(chunkEvent);
149-
this.queue[sequenceNumber] = playback;
148+
this.queue[sequenceNumber - 1] = playback; // -1 because the sequence number starts at 1
150149
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, state));
151150
}
152151

@@ -171,17 +170,18 @@ export class VoiceBroadcastPlayback
171170
await this.loadChunks();
172171
}
173172

174-
if (this.queue.length === 0 || !this.queue[1]) {
175-
// set to stopped fi the queue is empty of the first chunk (sequence number: 1-based index) is missing
173+
const toPlayIndex = this.getInfoState() === VoiceBroadcastInfoState.Stopped
174+
? 0 // start at the beginning for an ended voice broadcast
175+
: this.queue.length - 1; // start at the current chunk for an ongoing voice broadcast
176+
177+
if (this.queue.length === 0 || !this.queue[toPlayIndex]) {
176178
this.setState(VoiceBroadcastPlaybackState.Stopped);
177179
return;
178180
}
179181

180182
this.setState(VoiceBroadcastPlaybackState.Playing);
181-
// index of the first chunk is the first sequence number
182-
const first = this.queue[1];
183-
this.currentlyPlaying = first;
184-
await first.play();
183+
this.currentlyPlaying = this.queue[toPlayIndex];
184+
await this.currentlyPlaying.play();
185185
}
186186

187187
public get length(): number {

test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts

Lines changed: 115 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
2525
import {
2626
VoiceBroadcastChunkEventType,
2727
VoiceBroadcastInfoEventType,
28+
VoiceBroadcastInfoState,
2829
VoiceBroadcastPlayback,
2930
VoiceBroadcastPlaybackEvent,
3031
VoiceBroadcastPlaybackState,
@@ -100,15 +101,33 @@ describe("VoiceBroadcastPlayback", () => {
100101
};
101102
};
102103

103-
beforeAll(() => {
104-
client = stubClient();
105-
infoEvent = mkEvent({
104+
const mkInfoEvent = (state: VoiceBroadcastInfoState) => {
105+
return mkEvent({
106106
event: true,
107107
type: VoiceBroadcastInfoEventType,
108108
user: userId,
109109
room: roomId,
110-
content: {},
110+
content: {
111+
state,
112+
},
111113
});
114+
};
115+
116+
const mkPlayback = () => {
117+
const playback = new VoiceBroadcastPlayback(infoEvent, client);
118+
jest.spyOn(playback, "removeAllListeners");
119+
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
120+
return playback;
121+
};
122+
123+
const setUpChunkEvents = (chunkEvents: MatrixEvent[]) => {
124+
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
125+
jest.spyOn(relations, "getRelations").mockReturnValue(chunkEvents);
126+
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
127+
};
128+
129+
beforeAll(() => {
130+
client = stubClient();
112131

113132
// crap event to test 0 as first sequence number
114133
chunk0Event = mkChunkEvent(0);
@@ -139,128 +158,148 @@ describe("VoiceBroadcastPlayback", () => {
139158
});
140159

141160
beforeEach(() => {
161+
jest.clearAllMocks();
142162
onStateChanged = jest.fn();
143-
144-
playback = new VoiceBroadcastPlayback(infoEvent, client);
145-
jest.spyOn(playback, "removeAllListeners");
146-
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
147163
});
148164

149-
describe("when there is only a 0 sequence event", () => {
165+
describe("when there is a running voice broadcast with some chunks", () => {
150166
beforeEach(() => {
151-
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
152-
jest.spyOn(relations, "getRelations").mockReturnValue([chunk0Event]);
153-
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
167+
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Running);
168+
playback = mkPlayback();
169+
setUpChunkEvents([chunk2Event, chunk0Event, chunk1Event]);
154170
});
155171

156-
describe("when calling start", () => {
172+
describe("and calling start", () => {
157173
beforeEach(async () => {
158174
await playback.start();
159175
});
160176

161-
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
177+
it("should play the last chunk", () => {
178+
// assert that the first chunk is being played
179+
expect(chunk2Playback.play).toHaveBeenCalled();
180+
expect(chunk1Playback.play).not.toHaveBeenCalled();
181+
});
162182
});
163183
});
164184

165-
describe("when there are some chunks", () => {
185+
describe("when there is a stopped voice broadcast", () => {
166186
beforeEach(() => {
167-
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
168-
jest.spyOn(relations, "getRelations").mockReturnValue([chunk2Event, chunk1Event]);
169-
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
187+
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
188+
playback = mkPlayback();
170189
});
171190

172-
it("should expose the info event", () => {
173-
expect(playback.infoEvent).toBe(infoEvent);
174-
});
191+
describe("and there is only a 0 sequence event", () => {
192+
beforeEach(() => {
193+
setUpChunkEvents([chunk0Event]);
194+
});
175195

176-
it("should be in state Stopped", () => {
177-
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
196+
describe("and calling start", () => {
197+
beforeEach(async () => {
198+
await playback.start();
199+
});
200+
201+
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
202+
});
178203
});
179204

180-
describe("when calling start", () => {
181-
beforeEach(async () => {
182-
await playback.start();
205+
describe("and there are some chunks", () => {
206+
beforeEach(() => {
207+
setUpChunkEvents([chunk2Event, chunk0Event, chunk1Event]);
183208
});
184209

185-
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
210+
it("should expose the info event", () => {
211+
expect(playback.infoEvent).toBe(infoEvent);
212+
});
186213

187-
it("should play the chunks", () => {
188-
// assert that the first chunk is being played
189-
expect(chunk1Playback.play).toHaveBeenCalled();
190-
expect(chunk2Playback.play).not.toHaveBeenCalled();
214+
it("should be in state Stopped", () => {
215+
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
216+
});
191217

192-
// simulate end of first chunk
193-
chunk1Playback.emit(PlaybackState.Stopped);
218+
describe("and calling start", () => {
219+
beforeEach(async () => {
220+
await playback.start();
221+
});
194222

195-
// assert that the second chunk is being played
196-
expect(chunk2Playback.play).toHaveBeenCalled();
223+
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
197224

198-
// simulate end of second chunk
199-
chunk2Playback.emit(PlaybackState.Stopped);
225+
it("should play the chunks beginning with the first one", () => {
226+
// assert that the first chunk is being played
227+
expect(chunk1Playback.play).toHaveBeenCalled();
228+
expect(chunk2Playback.play).not.toHaveBeenCalled();
200229

201-
// assert that the entire playback is now in stopped state
202-
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
203-
});
230+
// simulate end of first chunk
231+
chunk1Playback.emit(PlaybackState.Stopped);
204232

205-
describe("and calling pause", () => {
206-
beforeEach(() => {
207-
playback.pause();
233+
// assert that the second chunk is being played
234+
expect(chunk2Playback.play).toHaveBeenCalled();
235+
236+
// simulate end of second chunk
237+
chunk2Playback.emit(PlaybackState.Stopped);
238+
239+
// assert that the entire playback is now in stopped state
240+
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
208241
});
209242

210-
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
211-
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
212-
});
213-
});
243+
describe("and calling pause", () => {
244+
beforeEach(() => {
245+
playback.pause();
246+
});
214247

215-
describe("when calling toggle for the first time", () => {
216-
beforeEach(async () => {
217-
await playback.toggle();
248+
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
249+
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
250+
});
218251
});
219252

220-
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
221-
222-
describe("and calling toggle a second time", () => {
253+
describe("and calling toggle for the first time", () => {
223254
beforeEach(async () => {
224255
await playback.toggle();
225256
});
226257

227-
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
258+
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
228259

229-
describe("and calling toggle a third time", () => {
260+
describe("and calling toggle a second time", () => {
230261
beforeEach(async () => {
231262
await playback.toggle();
232263
});
233264

234-
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
265+
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
266+
267+
describe("and calling toggle a third time", () => {
268+
beforeEach(async () => {
269+
await playback.toggle();
270+
});
271+
272+
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
273+
});
235274
});
236275
});
237-
});
238276

239-
describe("when calling stop", () => {
240-
beforeEach(() => {
241-
playback.stop();
242-
});
277+
describe("and calling stop", () => {
278+
beforeEach(() => {
279+
playback.stop();
280+
});
243281

244-
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
282+
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
245283

246-
describe("and calling toggle", () => {
247-
beforeEach(async () => {
248-
mocked(onStateChanged).mockReset();
249-
await playback.toggle();
250-
});
284+
describe("and calling toggle", () => {
285+
beforeEach(async () => {
286+
mocked(onStateChanged).mockReset();
287+
await playback.toggle();
288+
});
251289

252-
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
253-
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
290+
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
291+
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
292+
});
254293
});
255-
});
256294

257-
describe("when calling destroy", () => {
258-
beforeEach(() => {
259-
playback.destroy();
260-
});
295+
describe("and calling destroy", () => {
296+
beforeEach(() => {
297+
playback.destroy();
298+
});
261299

262-
it("should call removeAllListeners", () => {
263-
expect(playback.removeAllListeners).toHaveBeenCalled();
300+
it("should call removeAllListeners", () => {
301+
expect(playback.removeAllListeners).toHaveBeenCalled();
302+
});
264303
});
265304
});
266305
});

0 commit comments

Comments
 (0)