Use server side relations for voice broadcasts (#9534)

This commit is contained in:
Michael Weimann 2022-11-07 15:19:49 +01:00 committed by GitHub
parent 3747464b41
commit 36a574a14f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 396 additions and 192 deletions

View file

@ -15,24 +15,21 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import { EventType, MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { Relations } from "matrix-js-sdk/src/models/relations";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { Playback, PlaybackState } from "../../../src/audio/Playback";
import { PlaybackManager } from "../../../src/audio/PlaybackManager";
import { getReferenceRelationsForEvent } from "../../../src/events";
import { RelationsHelperEvent } from "../../../src/events/RelationsHelper";
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
import {
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackEvent,
VoiceBroadcastPlaybackState,
} from "../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../test-utils";
import { flushPromises, stubClient } from "../../test-utils";
import { createTestPlayback } from "../../test-utils/audio";
import { mkVoiceBroadcastChunkEvent } from "../utils/test-utils";
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
jest.mock("../../../src/events/getReferenceRelationsForEvent", () => ({
getReferenceRelationsForEvent: jest.fn(),
@ -44,6 +41,7 @@ jest.mock("../../../src/utils/MediaEventHelper", () => ({
describe("VoiceBroadcastPlayback", () => {
const userId = "@user:example.com";
let deviceId: string;
const roomId = "!room:example.com";
let client: MatrixClient;
let infoEvent: MatrixEvent;
@ -98,7 +96,7 @@ describe("VoiceBroadcastPlayback", () => {
const mkChunkHelper = (data: ArrayBuffer): MediaEventHelper => {
return {
sourceBlob: {
cachedValue: null,
cachedValue: new Blob(),
done: false,
value: {
// @ts-ignore
@ -109,32 +107,31 @@ describe("VoiceBroadcastPlayback", () => {
};
const mkInfoEvent = (state: VoiceBroadcastInfoState) => {
return mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
user: userId,
room: roomId,
content: {
state,
},
});
return mkVoiceBroadcastInfoStateEvent(
roomId,
state,
userId,
deviceId,
);
};
const mkPlayback = () => {
const mkPlayback = async () => {
const playback = new VoiceBroadcastPlayback(infoEvent, client);
jest.spyOn(playback, "removeAllListeners");
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
await flushPromises();
return playback;
};
const setUpChunkEvents = (chunkEvents: MatrixEvent[]) => {
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
jest.spyOn(relations, "getRelations").mockReturnValue(chunkEvents);
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
mocked(client.relations).mockResolvedValueOnce({
events: chunkEvents,
});
};
beforeAll(() => {
client = stubClient();
deviceId = client.getDeviceId() || "";
chunk1Event = mkVoiceBroadcastChunkEvent(userId, roomId, chunk1Length, 1);
chunk2Event = mkVoiceBroadcastChunkEvent(userId, roomId, chunk2Length, 2);
@ -153,6 +150,8 @@ describe("VoiceBroadcastPlayback", () => {
if (buffer === chunk1Data) return chunk1Playback;
if (buffer === chunk2Data) return chunk2Playback;
if (buffer === chunk3Data) return chunk3Playback;
throw new Error("unexpected buffer");
},
);
@ -168,11 +167,17 @@ describe("VoiceBroadcastPlayback", () => {
onStateChanged = jest.fn();
});
afterEach(() => {
playback.destroy();
});
describe(`when there is a ${VoiceBroadcastInfoState.Resumed} broadcast without chunks yet`, () => {
beforeEach(() => {
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
playback = mkPlayback();
beforeEach(async () => {
// info relation
mocked(client.relations).mockResolvedValueOnce({ events: [] });
setUpChunkEvents([]);
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
playback = await mkPlayback();
});
describe("and calling start", () => {
@ -227,10 +232,12 @@ describe("VoiceBroadcastPlayback", () => {
});
describe(`when there is a ${VoiceBroadcastInfoState.Resumed} voice broadcast with some chunks`, () => {
beforeEach(() => {
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
playback = mkPlayback();
beforeEach(async () => {
// info relation
mocked(client.relations).mockResolvedValueOnce({ events: [] });
setUpChunkEvents([chunk2Event, chunk1Event]);
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed);
playback = await mkPlayback();
});
describe("and calling start", () => {
@ -267,158 +274,153 @@ describe("VoiceBroadcastPlayback", () => {
});
describe("when there is a stopped voice broadcast", () => {
beforeEach(() => {
beforeEach(async () => {
setUpChunkEvents([chunk2Event, chunk1Event]);
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
playback = mkPlayback();
playback = await mkPlayback();
});
describe("and there are some chunks", () => {
beforeEach(() => {
setUpChunkEvents([chunk2Event, chunk1Event]);
it("should expose the info event", () => {
expect(playback.infoEvent).toBe(infoEvent);
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
describe("and calling start", () => {
startPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
it("should play the chunks beginning with the first one", () => {
// assert that the first chunk is being played
expect(chunk1Playback.play).toHaveBeenCalled();
expect(chunk2Playback.play).not.toHaveBeenCalled();
});
it("should expose the info event", () => {
expect(playback.infoEvent).toBe(infoEvent);
describe("and the chunk playback progresses", () => {
beforeEach(() => {
chunk1Playback.clockInfo.liveData.update([11]);
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(11);
});
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
describe("and skipping to the middle of the second chunk", () => {
const middleOfSecondChunk = (chunk1Length + (chunk2Length / 2)) / 1000;
describe("and calling start", () => {
startPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
it("should play the chunks beginning with the first one", () => {
// assert that the first chunk is being played
expect(chunk1Playback.play).toHaveBeenCalled();
expect(chunk2Playback.play).not.toHaveBeenCalled();
beforeEach(async () => {
await playback.skipTo(middleOfSecondChunk);
});
describe("and the chunk playback progresses", () => {
beforeEach(() => {
chunk1Playback.clockInfo.liveData.update([11]);
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(11);
});
it("should play the second chunk", () => {
expect(chunk1Playback.stop).toHaveBeenCalled();
expect(chunk2Playback.play).toHaveBeenCalled();
});
describe("and skipping to the middle of the second chunk", () => {
const middleOfSecondChunk = (chunk1Length + (chunk2Length / 2)) / 1000;
it("should update the time", () => {
expect(playback.timeSeconds).toBe(middleOfSecondChunk);
});
describe("and skipping to the start", () => {
beforeEach(async () => {
await playback.skipTo(middleOfSecondChunk);
await playback.skipTo(0);
});
it("should play the second chunk", () => {
expect(chunk1Playback.stop).toHaveBeenCalled();
expect(chunk2Playback.play).toHaveBeenCalled();
expect(chunk1Playback.play).toHaveBeenCalled();
expect(chunk2Playback.stop).toHaveBeenCalled();
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(middleOfSecondChunk);
});
describe("and skipping to the start", () => {
beforeEach(async () => {
await playback.skipTo(0);
});
it("should play the second chunk", () => {
expect(chunk1Playback.play).toHaveBeenCalled();
expect(chunk2Playback.stop).toHaveBeenCalled();
});
it("should update the time", () => {
expect(playback.timeSeconds).toBe(0);
});
});
});
describe("and the first chunk ends", () => {
beforeEach(() => {
chunk1Playback.emit(PlaybackState.Stopped);
});
it("should play until the end", () => {
// 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", () => {
pausePlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
});
describe("and calling stop", () => {
stopPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
});
describe("and calling destroy", () => {
beforeEach(() => {
playback.destroy();
});
it("should call removeAllListeners", () => {
expect(playback.removeAllListeners).toHaveBeenCalled();
});
it("should call destroy on the playbacks", () => {
expect(chunk1Playback.destroy).toHaveBeenCalled();
expect(chunk2Playback.destroy).toHaveBeenCalled();
expect(playback.timeSeconds).toBe(0);
});
});
});
describe("and calling toggle for the first time", () => {
beforeEach(async () => {
await playback.toggle();
describe("and the first chunk ends", () => {
beforeEach(() => {
chunk1Playback.emit(PlaybackState.Stopped);
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
it("should play until the end", () => {
// assert that the second chunk is being played
expect(chunk2Playback.play).toHaveBeenCalled();
describe("and calling toggle a second time", () => {
beforeEach(async () => {
await playback.toggle();
});
// simulate end of second chunk
chunk2Playback.emit(PlaybackState.Stopped);
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
describe("and calling toggle a third time", () => {
beforeEach(async () => {
await playback.toggle();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
});
// assert that the entire playback is now in stopped state
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
});
});
describe("and calling pause", () => {
pausePlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
});
describe("and calling stop", () => {
stopPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
});
describe("and calling toggle", () => {
describe("and calling destroy", () => {
beforeEach(() => {
playback.destroy();
});
it("should call removeAllListeners", () => {
expect(playback.removeAllListeners).toHaveBeenCalled();
});
it("should call destroy on the playbacks", () => {
expect(chunk1Playback.destroy).toHaveBeenCalled();
expect(chunk2Playback.destroy).toHaveBeenCalled();
});
});
});
describe("and 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 () => {
mocked(onStateChanged).mockReset();
await playback.toggle();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
});
});
});
describe("and calling stop", () => {
stopPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
describe("and calling toggle", () => {
beforeEach(async () => {
mocked(onStateChanged).mockReset();
await playback.toggle();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
});
});
});
});