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

Commit c1c50ec

Browse files
authored
Stabilise seekign in broadcast (#9968)
1 parent ed06ed0 commit c1c50ec

File tree

2 files changed

+68
-8
lines changed

2 files changed

+68
-8
lines changed

src/voice-broadcast/models/VoiceBroadcastPlayback.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
2626
import { SimpleObservable } from "matrix-widget-api";
2727
import { logger } from "matrix-js-sdk/src/logger";
28+
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
2829

2930
import { Playback, PlaybackInterface, PlaybackState } from "../../audio/Playback";
3031
import { PlaybackManager } from "../../audio/PlaybackManager";
@@ -96,6 +97,9 @@ export class VoiceBroadcastPlayback
9697
private chunkRelationHelper!: RelationsHelper;
9798
private infoRelationHelper!: RelationsHelper;
9899

100+
private skipToNext?: number;
101+
private skipToDeferred?: IDeferred<void>;
102+
99103
public constructor(
100104
public readonly infoEvent: MatrixEvent,
101105
private client: MatrixClient,
@@ -370,6 +374,28 @@ export class VoiceBroadcastPlayback
370374
}
371375

372376
public async skipTo(timeSeconds: number): Promise<void> {
377+
this.skipToNext = timeSeconds;
378+
379+
if (this.skipToDeferred) {
380+
// Skip to position is already in progress. Return the promise for that.
381+
return this.skipToDeferred.promise;
382+
}
383+
384+
this.skipToDeferred = defer();
385+
386+
while (this.skipToNext !== undefined) {
387+
// Skip to position until skipToNext is undefined.
388+
// skipToNext can be set if skipTo is called while already skipping.
389+
const skipToNext = this.skipToNext;
390+
this.skipToNext = undefined;
391+
await this.doSkipTo(skipToNext);
392+
}
393+
394+
this.skipToDeferred.resolve();
395+
this.skipToDeferred = undefined;
396+
}
397+
398+
private async doSkipTo(timeSeconds: number): Promise<void> {
373399
const time = timeSeconds * 1000;
374400
const event = this.chunkEvents.findByTime(time);
375401

@@ -379,6 +405,7 @@ export class VoiceBroadcastPlayback
379405
}
380406

381407
const currentPlayback = this.getCurrentPlayback();
408+
const currentPlaybackEvent = this.currentlyPlaying;
382409
const skipToPlayback = await this.getOrLoadPlaybackForEvent(event);
383410

384411
if (!skipToPlayback) {
@@ -388,10 +415,12 @@ export class VoiceBroadcastPlayback
388415

389416
this.currentlyPlaying = event;
390417

391-
if (currentPlayback && currentPlayback !== skipToPlayback) {
418+
if (currentPlayback && currentPlaybackEvent && currentPlayback !== skipToPlayback) {
419+
// only stop and unload the playback here without triggering other effects, e.g. play next
392420
currentPlayback.off(UPDATE_EVENT, this.onPlaybackStateChange);
393421
await currentPlayback.stop();
394422
currentPlayback.on(UPDATE_EVENT, this.onPlaybackStateChange);
423+
this.unloadPlayback(currentPlaybackEvent);
395424
}
396425

397426
const offsetInChunk = time - this.chunkEvents.getLengthTo(event);

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

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
VoiceBroadcastPlaybackState,
3232
VoiceBroadcastRecording,
3333
} from "../../../src/voice-broadcast";
34-
import { flushPromises, stubClient } from "../../test-utils";
34+
import { filterConsole, flushPromises, stubClient } from "../../test-utils";
3535
import { createTestPlayback } from "../../test-utils/audio";
3636
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
3737

@@ -64,6 +64,8 @@ describe("VoiceBroadcastPlayback", () => {
6464
let chunk1Playback: Playback;
6565
let chunk2Playback: Playback;
6666
let chunk3Playback: Playback;
67+
let middleOfSecondChunk!: number;
68+
let middleOfThirdChunk!: number;
6769

6870
const queryConfirmListeningDialog = () => {
6971
return screen.queryByText(
@@ -164,6 +166,9 @@ describe("VoiceBroadcastPlayback", () => {
164166
chunk2Playback = createTestPlayback();
165167
chunk3Playback = createTestPlayback();
166168

169+
middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000;
170+
middleOfThirdChunk = (chunk1Length + chunk2Length + chunk3Length / 2) / 1000;
171+
167172
jest.spyOn(PlaybackManager.instance, "createPlaybackInstance").mockImplementation(
168173
(buffer: ArrayBuffer, _waveForm?: number[]) => {
169174
if (buffer === chunk1Data) return chunk1Playback;
@@ -181,10 +186,11 @@ describe("VoiceBroadcastPlayback", () => {
181186
});
182187
};
183188

189+
filterConsole("Starting load of AsyncWrapper for modal");
190+
184191
beforeEach(() => {
185192
client = stubClient();
186193
deviceId = client.getDeviceId() || "";
187-
jest.clearAllMocks();
188194
room = new Room(roomId, client, client.getSafeUserId());
189195
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
190196
if (roomId === room.roomId) return room;
@@ -425,8 +431,8 @@ describe("VoiceBroadcastPlayback", () => {
425431
beforeEach(async () => {
426432
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
427433
createChunkEvents();
428-
setUpChunkEvents([chunk2Event, chunk1Event]);
429-
room.addLiveEvents([infoEvent, chunk1Event, chunk2Event]);
434+
setUpChunkEvents([chunk2Event, chunk1Event, chunk3Event]);
435+
room.addLiveEvents([infoEvent, chunk1Event, chunk2Event, chunk3Event]);
430436
playback = await mkPlayback();
431437
});
432438

@@ -472,6 +478,7 @@ describe("VoiceBroadcastPlayback", () => {
472478

473479
it("should play the second chunk", () => {
474480
expect(chunk1Playback.stop).toHaveBeenCalled();
481+
expect(chunk1Playback.destroy).toHaveBeenCalled();
475482
expect(chunk2Playback.play).toHaveBeenCalled();
476483
});
477484

@@ -484,9 +491,10 @@ describe("VoiceBroadcastPlayback", () => {
484491
await playback.skipTo(0);
485492
});
486493

487-
it("should play the second chunk", () => {
488-
expect(chunk1Playback.play).toHaveBeenCalled();
494+
it("should play the first chunk", () => {
489495
expect(chunk2Playback.stop).toHaveBeenCalled();
496+
expect(chunk2Playback.destroy).toHaveBeenCalled();
497+
expect(chunk1Playback.play).toHaveBeenCalled();
490498
});
491499

492500
it("should update the time", () => {
@@ -495,6 +503,28 @@ describe("VoiceBroadcastPlayback", () => {
495503
});
496504
});
497505

506+
describe("and skipping multiple times", () => {
507+
beforeEach(async () => {
508+
return Promise.all([
509+
playback.skipTo(middleOfSecondChunk),
510+
playback.skipTo(middleOfThirdChunk),
511+
playback.skipTo(0),
512+
]);
513+
});
514+
515+
it("should only skip to the first and last position", () => {
516+
expect(chunk1Playback.stop).toHaveBeenCalled();
517+
expect(chunk1Playback.destroy).toHaveBeenCalled();
518+
expect(chunk2Playback.play).toHaveBeenCalled();
519+
520+
expect(chunk3Playback.play).not.toHaveBeenCalled();
521+
522+
expect(chunk2Playback.stop).toHaveBeenCalled();
523+
expect(chunk2Playback.destroy).toHaveBeenCalled();
524+
expect(chunk1Playback.play).toHaveBeenCalled();
525+
});
526+
});
527+
498528
describe("and the first chunk ends", () => {
499529
beforeEach(() => {
500530
chunk1Playback.emit(PlaybackState.Stopped);
@@ -507,8 +537,9 @@ describe("VoiceBroadcastPlayback", () => {
507537
// assert that the second chunk is being played
508538
expect(chunk2Playback.play).toHaveBeenCalled();
509539

510-
// simulate end of second chunk
540+
// simulate end of second and third chunk
511541
chunk2Playback.emit(PlaybackState.Stopped);
542+
chunk3Playback.emit(PlaybackState.Stopped);
512543

513544
// assert that the entire playback is now in stopped state
514545
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);

0 commit comments

Comments
 (0)