mirror of https://github.com/vector-im/riot-web
Stabilise seekign in broadcast (#9968)
parent
ed06ed0185
commit
c1c50ec182
|
@ -25,6 +25,7 @@ import {
|
||||||
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
|
||||||
import { SimpleObservable } from "matrix-widget-api";
|
import { SimpleObservable } from "matrix-widget-api";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { Playback, PlaybackInterface, PlaybackState } from "../../audio/Playback";
|
import { Playback, PlaybackInterface, PlaybackState } from "../../audio/Playback";
|
||||||
import { PlaybackManager } from "../../audio/PlaybackManager";
|
import { PlaybackManager } from "../../audio/PlaybackManager";
|
||||||
|
@ -96,6 +97,9 @@ export class VoiceBroadcastPlayback
|
||||||
private chunkRelationHelper!: RelationsHelper;
|
private chunkRelationHelper!: RelationsHelper;
|
||||||
private infoRelationHelper!: RelationsHelper;
|
private infoRelationHelper!: RelationsHelper;
|
||||||
|
|
||||||
|
private skipToNext?: number;
|
||||||
|
private skipToDeferred?: IDeferred<void>;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly infoEvent: MatrixEvent,
|
public readonly infoEvent: MatrixEvent,
|
||||||
private client: MatrixClient,
|
private client: MatrixClient,
|
||||||
|
@ -370,6 +374,28 @@ export class VoiceBroadcastPlayback
|
||||||
}
|
}
|
||||||
|
|
||||||
public async skipTo(timeSeconds: number): Promise<void> {
|
public async skipTo(timeSeconds: number): Promise<void> {
|
||||||
|
this.skipToNext = timeSeconds;
|
||||||
|
|
||||||
|
if (this.skipToDeferred) {
|
||||||
|
// Skip to position is already in progress. Return the promise for that.
|
||||||
|
return this.skipToDeferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.skipToDeferred = defer();
|
||||||
|
|
||||||
|
while (this.skipToNext !== undefined) {
|
||||||
|
// Skip to position until skipToNext is undefined.
|
||||||
|
// skipToNext can be set if skipTo is called while already skipping.
|
||||||
|
const skipToNext = this.skipToNext;
|
||||||
|
this.skipToNext = undefined;
|
||||||
|
await this.doSkipTo(skipToNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.skipToDeferred.resolve();
|
||||||
|
this.skipToDeferred = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doSkipTo(timeSeconds: number): Promise<void> {
|
||||||
const time = timeSeconds * 1000;
|
const time = timeSeconds * 1000;
|
||||||
const event = this.chunkEvents.findByTime(time);
|
const event = this.chunkEvents.findByTime(time);
|
||||||
|
|
||||||
|
@ -379,6 +405,7 @@ export class VoiceBroadcastPlayback
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPlayback = this.getCurrentPlayback();
|
const currentPlayback = this.getCurrentPlayback();
|
||||||
|
const currentPlaybackEvent = this.currentlyPlaying;
|
||||||
const skipToPlayback = await this.getOrLoadPlaybackForEvent(event);
|
const skipToPlayback = await this.getOrLoadPlaybackForEvent(event);
|
||||||
|
|
||||||
if (!skipToPlayback) {
|
if (!skipToPlayback) {
|
||||||
|
@ -388,10 +415,12 @@ export class VoiceBroadcastPlayback
|
||||||
|
|
||||||
this.currentlyPlaying = event;
|
this.currentlyPlaying = event;
|
||||||
|
|
||||||
if (currentPlayback && currentPlayback !== skipToPlayback) {
|
if (currentPlayback && currentPlaybackEvent && currentPlayback !== skipToPlayback) {
|
||||||
|
// only stop and unload the playback here without triggering other effects, e.g. play next
|
||||||
currentPlayback.off(UPDATE_EVENT, this.onPlaybackStateChange);
|
currentPlayback.off(UPDATE_EVENT, this.onPlaybackStateChange);
|
||||||
await currentPlayback.stop();
|
await currentPlayback.stop();
|
||||||
currentPlayback.on(UPDATE_EVENT, this.onPlaybackStateChange);
|
currentPlayback.on(UPDATE_EVENT, this.onPlaybackStateChange);
|
||||||
|
this.unloadPlayback(currentPlaybackEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetInChunk = time - this.chunkEvents.getLengthTo(event);
|
const offsetInChunk = time - this.chunkEvents.getLengthTo(event);
|
||||||
|
|
|
@ -31,7 +31,7 @@ import {
|
||||||
VoiceBroadcastPlaybackState,
|
VoiceBroadcastPlaybackState,
|
||||||
VoiceBroadcastRecording,
|
VoiceBroadcastRecording,
|
||||||
} from "../../../src/voice-broadcast";
|
} from "../../../src/voice-broadcast";
|
||||||
import { flushPromises, stubClient } from "../../test-utils";
|
import { filterConsole, flushPromises, stubClient } from "../../test-utils";
|
||||||
import { createTestPlayback } from "../../test-utils/audio";
|
import { createTestPlayback } from "../../test-utils/audio";
|
||||||
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||||
|
|
||||||
|
@ -64,6 +64,8 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
let chunk1Playback: Playback;
|
let chunk1Playback: Playback;
|
||||||
let chunk2Playback: Playback;
|
let chunk2Playback: Playback;
|
||||||
let chunk3Playback: Playback;
|
let chunk3Playback: Playback;
|
||||||
|
let middleOfSecondChunk!: number;
|
||||||
|
let middleOfThirdChunk!: number;
|
||||||
|
|
||||||
const queryConfirmListeningDialog = () => {
|
const queryConfirmListeningDialog = () => {
|
||||||
return screen.queryByText(
|
return screen.queryByText(
|
||||||
|
@ -164,6 +166,9 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
chunk2Playback = createTestPlayback();
|
chunk2Playback = createTestPlayback();
|
||||||
chunk3Playback = createTestPlayback();
|
chunk3Playback = createTestPlayback();
|
||||||
|
|
||||||
|
middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000;
|
||||||
|
middleOfThirdChunk = (chunk1Length + chunk2Length + chunk3Length / 2) / 1000;
|
||||||
|
|
||||||
jest.spyOn(PlaybackManager.instance, "createPlaybackInstance").mockImplementation(
|
jest.spyOn(PlaybackManager.instance, "createPlaybackInstance").mockImplementation(
|
||||||
(buffer: ArrayBuffer, _waveForm?: number[]) => {
|
(buffer: ArrayBuffer, _waveForm?: number[]) => {
|
||||||
if (buffer === chunk1Data) return chunk1Playback;
|
if (buffer === chunk1Data) return chunk1Playback;
|
||||||
|
@ -181,10 +186,11 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
filterConsole("Starting load of AsyncWrapper for modal");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
deviceId = client.getDeviceId() || "";
|
deviceId = client.getDeviceId() || "";
|
||||||
jest.clearAllMocks();
|
|
||||||
room = new Room(roomId, client, client.getSafeUserId());
|
room = new Room(roomId, client, client.getSafeUserId());
|
||||||
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
|
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
|
||||||
if (roomId === room.roomId) return room;
|
if (roomId === room.roomId) return room;
|
||||||
|
@ -425,8 +431,8 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
|
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
|
||||||
createChunkEvents();
|
createChunkEvents();
|
||||||
setUpChunkEvents([chunk2Event, chunk1Event]);
|
setUpChunkEvents([chunk2Event, chunk1Event, chunk3Event]);
|
||||||
room.addLiveEvents([infoEvent, chunk1Event, chunk2Event]);
|
room.addLiveEvents([infoEvent, chunk1Event, chunk2Event, chunk3Event]);
|
||||||
playback = await mkPlayback();
|
playback = await mkPlayback();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -472,6 +478,7 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
|
|
||||||
it("should play the second chunk", () => {
|
it("should play the second chunk", () => {
|
||||||
expect(chunk1Playback.stop).toHaveBeenCalled();
|
expect(chunk1Playback.stop).toHaveBeenCalled();
|
||||||
|
expect(chunk1Playback.destroy).toHaveBeenCalled();
|
||||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -484,9 +491,10 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
await playback.skipTo(0);
|
await playback.skipTo(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should play the second chunk", () => {
|
it("should play the first chunk", () => {
|
||||||
expect(chunk1Playback.play).toHaveBeenCalled();
|
|
||||||
expect(chunk2Playback.stop).toHaveBeenCalled();
|
expect(chunk2Playback.stop).toHaveBeenCalled();
|
||||||
|
expect(chunk2Playback.destroy).toHaveBeenCalled();
|
||||||
|
expect(chunk1Playback.play).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update the time", () => {
|
it("should update the time", () => {
|
||||||
|
@ -495,6 +503,28 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("and skipping multiple times", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
return Promise.all([
|
||||||
|
playback.skipTo(middleOfSecondChunk),
|
||||||
|
playback.skipTo(middleOfThirdChunk),
|
||||||
|
playback.skipTo(0),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only skip to the first and last position", () => {
|
||||||
|
expect(chunk1Playback.stop).toHaveBeenCalled();
|
||||||
|
expect(chunk1Playback.destroy).toHaveBeenCalled();
|
||||||
|
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(chunk3Playback.play).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(chunk2Playback.stop).toHaveBeenCalled();
|
||||||
|
expect(chunk2Playback.destroy).toHaveBeenCalled();
|
||||||
|
expect(chunk1Playback.play).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("and the first chunk ends", () => {
|
describe("and the first chunk ends", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
chunk1Playback.emit(PlaybackState.Stopped);
|
chunk1Playback.emit(PlaybackState.Stopped);
|
||||||
|
@ -507,8 +537,9 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
// assert that the second chunk is being played
|
// assert that the second chunk is being played
|
||||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||||
|
|
||||||
// simulate end of second chunk
|
// simulate end of second and third chunk
|
||||||
chunk2Playback.emit(PlaybackState.Stopped);
|
chunk2Playback.emit(PlaybackState.Stopped);
|
||||||
|
chunk3Playback.emit(PlaybackState.Stopped);
|
||||||
|
|
||||||
// assert that the entire playback is now in stopped state
|
// assert that the entire playback is now in stopped state
|
||||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
|
Loading…
Reference in New Issue