Voice Broadcast playback (#9372)

* Implement actual voice broadcast playback

* Move PublicInterface type to test

* Implement pausing a voice broadcast playback

* Implement PR feedback

* Remove unnecessary early return
This commit is contained in:
Michael Weimann 2022-10-14 16:48:54 +02:00 committed by GitHub
parent 54008cff58
commit cb5667b4a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 505 additions and 63 deletions

View file

@ -15,22 +15,50 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { EventType, MatrixClient, MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
import { Relations } from "matrix-js-sdk/src/models/relations";
import { Playback, PlaybackState } from "../../../src/audio/Playback";
import { PlaybackManager } from "../../../src/audio/PlaybackManager";
import { getReferenceRelationsForEvent } from "../../../src/events";
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
import {
VoiceBroadcastChunkEventType,
VoiceBroadcastInfoEventType,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackEvent,
VoiceBroadcastPlaybackState,
} from "../../../src/voice-broadcast";
import { mkEvent } from "../../test-utils";
import { mkEvent, stubClient } from "../../test-utils";
import { createTestPlayback } from "../../test-utils/audio";
jest.mock("../../../src/events/getReferenceRelationsForEvent", () => ({
getReferenceRelationsForEvent: jest.fn(),
}));
jest.mock("../../../src/utils/MediaEventHelper", () => ({
MediaEventHelper: jest.fn(),
}));
describe("VoiceBroadcastPlayback", () => {
const userId = "@user:example.com";
const roomId = "!room:example.com";
let client: MatrixClient;
let infoEvent: MatrixEvent;
let playback: VoiceBroadcastPlayback;
let onStateChanged: (state: VoiceBroadcastPlaybackState) => void;
let chunk0Event: MatrixEvent;
let chunk1Event: MatrixEvent;
let chunk2Event: MatrixEvent;
const chunk0Data = new ArrayBuffer(1);
const chunk1Data = new ArrayBuffer(2);
const chunk2Data = new ArrayBuffer(3);
let chunk0Helper: MediaEventHelper;
let chunk1Helper: MediaEventHelper;
let chunk2Helper: MediaEventHelper;
let chunk0Playback: Playback;
let chunk1Playback: Playback;
let chunk2Playback: Playback;
const itShouldSetTheStateTo = (state: VoiceBroadcastPlaybackState) => {
it(`should set the state to ${state}`, () => {
@ -44,7 +72,36 @@ describe("VoiceBroadcastPlayback", () => {
});
};
const mkChunkEvent = (sequence: number) => {
return mkEvent({
event: true,
user: client.getUserId(),
room: roomId,
type: EventType.RoomMessage,
content: {
msgtype: MsgType.Audio,
[VoiceBroadcastChunkEventType]: {
sequence,
},
},
});
};
const mkChunkHelper = (data: ArrayBuffer): MediaEventHelper => {
return {
sourceBlob: {
cachedValue: null,
done: false,
value: {
// @ts-ignore
arrayBuffer: jest.fn().mockResolvedValue(data),
},
},
};
};
beforeAll(() => {
client = stubClient();
infoEvent = mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
@ -52,65 +109,159 @@ describe("VoiceBroadcastPlayback", () => {
room: roomId,
content: {},
});
// crap event to test 0 as first sequence number
chunk0Event = mkChunkEvent(0);
chunk1Event = mkChunkEvent(1);
chunk2Event = mkChunkEvent(2);
chunk0Helper = mkChunkHelper(chunk0Data);
chunk1Helper = mkChunkHelper(chunk1Data);
chunk2Helper = mkChunkHelper(chunk2Data);
chunk0Playback = createTestPlayback();
chunk1Playback = createTestPlayback();
chunk2Playback = createTestPlayback();
jest.spyOn(PlaybackManager.instance, "createPlaybackInstance").mockImplementation(
(buffer: ArrayBuffer, _waveForm?: number[]) => {
if (buffer === chunk0Data) return chunk0Playback;
if (buffer === chunk1Data) return chunk1Playback;
if (buffer === chunk2Data) return chunk2Playback;
},
);
mocked(MediaEventHelper).mockImplementation((event: MatrixEvent) => {
if (event === chunk0Event) return chunk0Helper;
if (event === chunk1Event) return chunk1Helper;
if (event === chunk2Event) return chunk2Helper;
});
});
beforeEach(() => {
onStateChanged = jest.fn();
playback = new VoiceBroadcastPlayback(infoEvent);
playback = new VoiceBroadcastPlayback(infoEvent, client);
jest.spyOn(playback, "removeAllListeners");
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
});
it("should expose the info event", () => {
expect(playback.infoEvent).toBe(infoEvent);
});
it("should be in state Stopped", () => {
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
});
describe("when calling start", () => {
describe("when there is only a 0 sequence event", () => {
beforeEach(() => {
playback.start();
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
jest.spyOn(relations, "getRelations").mockReturnValue([chunk0Event]);
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
describe("and calling toggle", () => {
beforeEach(() => {
playback.toggle();
describe("when calling start", () => {
beforeEach(async () => {
await playback.start();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Stopped);
});
});
describe("when calling stop", () => {
describe("when there are some chunks", () => {
beforeEach(() => {
playback.stop();
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
jest.spyOn(relations, "getRelations").mockReturnValue([chunk2Event, chunk1Event]);
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
it("should expose the info event", () => {
expect(playback.infoEvent).toBe(infoEvent);
});
describe("and calling toggle", () => {
beforeEach(() => {
playback.toggle();
it("should be in state Stopped", () => {
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
});
describe("when calling start", () => {
beforeEach(async () => {
await playback.start();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Stopped);
});
});
describe("when calling destroy", () => {
beforeEach(() => {
playback.destroy();
it("should play the chunks", () => {
// assert that the first chunk is being played
expect(chunk1Playback.play).toHaveBeenCalled();
expect(chunk2Playback.play).not.toHaveBeenCalled();
// simulate end of first chunk
chunk1Playback.emit(PlaybackState.Stopped);
// assert that the second chunk is being played
expect(chunk2Playback.play).toHaveBeenCalled();
// simulate end of second chunk
chunk2Playback.emit(PlaybackState.Stopped);
// assert that the entire playback is now in stopped state
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
});
describe("and calling pause", () => {
beforeEach(() => {
playback.pause();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
});
});
it("should call removeAllListeners", () => {
expect(playback.removeAllListeners).toHaveBeenCalled();
describe("when calling toggle for the first time", () => {
beforeEach(async () => {
await playback.toggle();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
describe("and calling toggle a second time", () => {
beforeEach(async () => {
await playback.toggle();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
describe("and calling toggle a third time", () => {
beforeEach(async () => {
await playback.toggle();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
});
});
});
describe("when calling stop", () => {
beforeEach(() => {
playback.stop();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
describe("and calling toggle", () => {
beforeEach(async () => {
mocked(onStateChanged).mockReset();
await playback.toggle();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
});
});
describe("when calling destroy", () => {
beforeEach(() => {
playback.destroy();
});
it("should call removeAllListeners", () => {
expect(playback.removeAllListeners).toHaveBeenCalled();
});
});
});
});