Use server side relations for voice broadcasts (#9534)
This commit is contained in:
parent
3747464b41
commit
36a574a14f
11 changed files with 396 additions and 192 deletions
|
@ -38,6 +38,8 @@ export class RelationsHelper
|
|||
extends TypedEventEmitter<RelationsHelperEvent, EventMap>
|
||||
implements IDestroyable {
|
||||
private relations?: Relations;
|
||||
private eventId: string;
|
||||
private roomId: string;
|
||||
|
||||
public constructor(
|
||||
private event: MatrixEvent,
|
||||
|
@ -46,6 +48,21 @@ export class RelationsHelper
|
|||
private client: MatrixClient,
|
||||
) {
|
||||
super();
|
||||
|
||||
const eventId = event.getId();
|
||||
|
||||
if (!eventId) {
|
||||
throw new Error("unable to create RelationsHelper: missing event ID");
|
||||
}
|
||||
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
if (!roomId) {
|
||||
throw new Error("unable to create RelationsHelper: missing room ID");
|
||||
}
|
||||
|
||||
this.eventId = eventId;
|
||||
this.roomId = roomId;
|
||||
this.setUpRelations();
|
||||
}
|
||||
|
||||
|
@ -73,7 +90,7 @@ export class RelationsHelper
|
|||
private setRelations(): void {
|
||||
const room = this.client.getRoom(this.event.getRoomId());
|
||||
this.relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
|
||||
this.event.getId(),
|
||||
this.eventId,
|
||||
this.relationType,
|
||||
this.relationEventType,
|
||||
);
|
||||
|
@ -87,6 +104,32 @@ export class RelationsHelper
|
|||
this.relations?.getRelations()?.forEach(e => this.emit(RelationsHelperEvent.Add, e));
|
||||
}
|
||||
|
||||
public getCurrent(): MatrixEvent[] {
|
||||
return this.relations?.getRelations() || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all related events from the server and emits them.
|
||||
*/
|
||||
public async emitFetchCurrent(): Promise<void> {
|
||||
let nextBatch: string | undefined = undefined;
|
||||
|
||||
do {
|
||||
const response = await this.client.relations(
|
||||
this.roomId,
|
||||
this.eventId,
|
||||
this.relationType,
|
||||
this.relationEventType,
|
||||
{
|
||||
from: nextBatch,
|
||||
limit: 50,
|
||||
},
|
||||
);
|
||||
nextBatch = response?.nextBatch;
|
||||
response?.events.forEach(e => this.emit(RelationsHelperEvent.Add, e));
|
||||
} while (nextBatch);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.removeAllListeners();
|
||||
this.event.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);
|
||||
|
|
|
@ -32,7 +32,6 @@ import { MediaEventHelper } from "../../utils/MediaEventHelper";
|
|||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
|
||||
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
|
||||
import { getReferenceRelationsForEvent } from "../../events";
|
||||
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
|
||||
|
||||
export enum VoiceBroadcastPlaybackState {
|
||||
|
@ -89,15 +88,27 @@ export class VoiceBroadcastPlayback
|
|||
this.setUpRelationsHelper();
|
||||
}
|
||||
|
||||
private setUpRelationsHelper(): void {
|
||||
private async setUpRelationsHelper(): Promise<void> {
|
||||
this.infoRelationHelper = new RelationsHelper(
|
||||
this.infoEvent,
|
||||
RelationType.Reference,
|
||||
VoiceBroadcastInfoEventType,
|
||||
this.client,
|
||||
);
|
||||
this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent);
|
||||
this.infoRelationHelper.emitCurrent();
|
||||
this.infoRelationHelper.getCurrent().forEach(this.addInfoEvent);
|
||||
|
||||
if (this.infoState !== VoiceBroadcastInfoState.Stopped) {
|
||||
// Only required if not stopped. Stopped is the final state.
|
||||
this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent);
|
||||
|
||||
try {
|
||||
await this.infoRelationHelper.emitFetchCurrent();
|
||||
} catch (err) {
|
||||
logger.warn("error fetching server side relation for voice broadcast info", err);
|
||||
// fall back to local events
|
||||
this.infoRelationHelper.emitCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
this.chunkRelationHelper = new RelationsHelper(
|
||||
this.infoEvent,
|
||||
|
@ -106,7 +117,15 @@ export class VoiceBroadcastPlayback
|
|||
this.client,
|
||||
);
|
||||
this.chunkRelationHelper.on(RelationsHelperEvent.Add, this.addChunkEvent);
|
||||
this.chunkRelationHelper.emitCurrent();
|
||||
|
||||
try {
|
||||
// TODO Michael W: only fetch events if needed, blocked by PSF-1708
|
||||
await this.chunkRelationHelper.emitFetchCurrent();
|
||||
} catch (err) {
|
||||
logger.warn("error fetching server side relation for voice broadcast chunks", err);
|
||||
// fall back to local events
|
||||
this.chunkRelationHelper.emitCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
private addChunkEvent = async (event: MatrixEvent): Promise<boolean> => {
|
||||
|
@ -150,23 +169,18 @@ export class VoiceBroadcastPlayback
|
|||
this.setInfoState(state);
|
||||
};
|
||||
|
||||
private async loadChunks(): Promise<void> {
|
||||
const relations = getReferenceRelationsForEvent(this.infoEvent, EventType.RoomMessage, this.client);
|
||||
const chunkEvents = relations?.getRelations();
|
||||
private async enqueueChunks(): Promise<void> {
|
||||
const promises = this.chunkEvents.getEvents().reduce((promises, event: MatrixEvent) => {
|
||||
if (!this.playbacks.has(event.getId() || "")) {
|
||||
promises.push(this.enqueueChunk(event));
|
||||
}
|
||||
return promises;
|
||||
}, [] as Promise<void>[]);
|
||||
|
||||
if (!chunkEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chunkEvents.addEvents(chunkEvents);
|
||||
this.setDuration(this.chunkEvents.getLength());
|
||||
|
||||
for (const chunkEvent of chunkEvents) {
|
||||
await this.enqueueChunk(chunkEvent);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async enqueueChunk(chunkEvent: MatrixEvent) {
|
||||
private async enqueueChunk(chunkEvent: MatrixEvent): Promise<void> {
|
||||
const eventId = chunkEvent.getId();
|
||||
|
||||
if (!eventId) {
|
||||
|
@ -317,10 +331,7 @@ export class VoiceBroadcastPlayback
|
|||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
if (this.playbacks.size === 0) {
|
||||
await this.loadChunks();
|
||||
}
|
||||
|
||||
await this.enqueueChunks();
|
||||
const chunkEvents = this.chunkEvents.getEvents();
|
||||
|
||||
const toPlay = this.getInfoState() === VoiceBroadcastInfoState.Stopped
|
||||
|
|
19
test/@types/common.ts
Normal file
19
test/@types/common.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export type PublicInterface<T> = {
|
||||
[P in keyof T]: T[P];
|
||||
};
|
|
@ -28,13 +28,15 @@ import { Relations } from "matrix-js-sdk/src/models/relations";
|
|||
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||
|
||||
import { RelationsHelper, RelationsHelperEvent } from "../../src/events/RelationsHelper";
|
||||
import { mkEvent, mkStubRoom, stubClient } from "../test-utils";
|
||||
import { mkEvent, mkRelationsContainer, mkStubRoom, stubClient } from "../test-utils";
|
||||
|
||||
describe("RelationsHelper", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let userId: string;
|
||||
let event: MatrixEvent;
|
||||
let relatedEvent1: MatrixEvent;
|
||||
let relatedEvent2: MatrixEvent;
|
||||
let relatedEvent3: MatrixEvent;
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
let relationsHelper: RelationsHelper;
|
||||
|
@ -46,47 +48,81 @@ describe("RelationsHelper", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
userId = client.getUserId() || "";
|
||||
mocked(client.relations).mockClear();
|
||||
room = mkStubRoom(roomId, "test room", client);
|
||||
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
|
||||
mocked(client.getRoom).mockImplementation((getRoomId?: string) => {
|
||||
if (getRoomId === roomId) {
|
||||
return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
user: userId,
|
||||
content: {},
|
||||
});
|
||||
relatedEvent1 = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
content: {},
|
||||
user: userId,
|
||||
content: { relatedEvent: 1 },
|
||||
});
|
||||
relatedEvent2 = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: roomId,
|
||||
user: client.getUserId(),
|
||||
content: {},
|
||||
user: userId,
|
||||
content: { relatedEvent: 2 },
|
||||
});
|
||||
relatedEvent3 = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
room: roomId,
|
||||
user: userId,
|
||||
content: { relatedEvent: 3 },
|
||||
});
|
||||
onAdd = jest.fn();
|
||||
relationsContainer = mkRelationsContainer();
|
||||
// TODO Michael W: create test utils, remove casts
|
||||
relationsContainer = {
|
||||
getChildEventsForEvent: jest.fn(),
|
||||
} as unknown as RelationsContainer;
|
||||
relations = {
|
||||
getRelations: jest.fn(),
|
||||
on: jest.fn().mockImplementation((type, l) => relationsOnAdd = l),
|
||||
off: jest.fn(),
|
||||
} as unknown as Relations;
|
||||
timelineSet = {
|
||||
relations: relationsContainer,
|
||||
} as unknown as EventTimelineSet;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
relationsHelper?.destroy();
|
||||
});
|
||||
|
||||
describe("when there is an event without ID", () => {
|
||||
it("should raise an error", () => {
|
||||
jest.spyOn(event, "getId").mockReturnValue(undefined);
|
||||
|
||||
expect(() => {
|
||||
new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
||||
}).toThrowError("unable to create RelationsHelper: missing event ID");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is an event without room ID", () => {
|
||||
it("should raise an error", () => {
|
||||
jest.spyOn(event, "getRoomId").mockReturnValue(undefined);
|
||||
|
||||
expect(() => {
|
||||
new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
||||
}).toThrowError("unable to create RelationsHelper: missing room ID");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is an event without relations", () => {
|
||||
beforeEach(() => {
|
||||
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
||||
|
@ -118,6 +154,34 @@ describe("RelationsHelper", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("when there is an event with two pages server side relations", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.relations)
|
||||
.mockResolvedValueOnce({
|
||||
events: [relatedEvent1, relatedEvent2],
|
||||
nextBatch: "next",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
events: [relatedEvent3],
|
||||
nextBatch: null,
|
||||
});
|
||||
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
|
||||
relationsHelper.on(RelationsHelperEvent.Add, onAdd);
|
||||
});
|
||||
|
||||
describe("emitFetchCurrent", () => {
|
||||
beforeEach(async () => {
|
||||
await relationsHelper.emitFetchCurrent();
|
||||
});
|
||||
|
||||
it("should emit the server side events", () => {
|
||||
expect(onAdd).toHaveBeenCalledWith(relatedEvent1);
|
||||
expect(onAdd).toHaveBeenCalledWith(relatedEvent2);
|
||||
expect(onAdd).toHaveBeenCalledWith(relatedEvent3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is an event with relations", () => {
|
||||
beforeEach(() => {
|
||||
mocked(room.getUnfilteredTimelineSet).mockReturnValue(timelineSet);
|
||||
|
|
|
@ -20,10 +20,7 @@ import { SimpleObservable } from "matrix-widget-api";
|
|||
import { Playback, PlaybackState } from "../../src/audio/Playback";
|
||||
import { PlaybackClock } from "../../src/audio/PlaybackClock";
|
||||
import { UPDATE_EVENT } from "../../src/stores/AsyncStore";
|
||||
|
||||
type PublicInterface<T> = {
|
||||
[P in keyof T]: T[P];
|
||||
};
|
||||
import { PublicInterface } from "../@types/common";
|
||||
|
||||
export const createTestPlayback = (): Playback => {
|
||||
const eventEmitter = new EventEmitter();
|
||||
|
|
|
@ -25,3 +25,4 @@ export * from './call';
|
|||
export * from './wrappers';
|
||||
export * from './utilities';
|
||||
export * from './date';
|
||||
export * from './relations';
|
||||
|
|
35
test/test-utils/relations.ts
Normal file
35
test/test-utils/relations.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Relations } from "matrix-js-sdk/src/models/relations";
|
||||
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||
|
||||
import { PublicInterface } from "../@types/common";
|
||||
|
||||
export const mkRelations = (): Relations => {
|
||||
return {
|
||||
|
||||
} as PublicInterface<Relations> as Relations;
|
||||
};
|
||||
|
||||
export const mkRelationsContainer = (): RelationsContainer => {
|
||||
return {
|
||||
aggregateChildEvent: jest.fn(),
|
||||
aggregateParentEvent: jest.fn(),
|
||||
getAllChildEventsForEvent: jest.fn(),
|
||||
getChildEventsForEvent: jest.fn(),
|
||||
} as PublicInterface<RelationsContainer> as RelationsContainer;
|
||||
};
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { ReactElement } from "react";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
@ -31,6 +31,8 @@ import {
|
|||
} from "../../../src/voice-broadcast";
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
||||
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
|
||||
import { RoomPermalinkCreator } from "../../../src/utils/permalinks/Permalinks";
|
||||
|
||||
jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({
|
||||
VoiceBroadcastRecordingBody: jest.fn(),
|
||||
|
@ -40,8 +42,13 @@ jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlayb
|
|||
VoiceBroadcastPlaybackBody: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/utils/permalinks/Permalinks");
|
||||
jest.mock("../../../src/utils/MediaEventHelper");
|
||||
|
||||
describe("VoiceBroadcastBody", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let userId: string;
|
||||
let deviceId: string;
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let infoEvent: MatrixEvent;
|
||||
|
@ -52,62 +59,75 @@ describe("VoiceBroadcastBody", () => {
|
|||
const renderVoiceBroadcast = () => {
|
||||
render(<VoiceBroadcastBody
|
||||
mxEvent={infoEvent}
|
||||
mediaEventHelper={null}
|
||||
mediaEventHelper={new MediaEventHelper(infoEvent)}
|
||||
onHeightChanged={() => {}}
|
||||
onMessageAllowed={() => {}}
|
||||
permalinkCreator={null}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
/>);
|
||||
testRecording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(infoEvent, client);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = new Room(roomId, client, client.getUserId());
|
||||
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
|
||||
userId = client.getUserId() || "";
|
||||
deviceId = client.getDeviceId() || "";
|
||||
mocked(client.relations).mockClear();
|
||||
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||
room = new Room(roomId, client, userId);
|
||||
mocked(client.getRoom).mockImplementation((getRoomId?: string) => {
|
||||
if (getRoomId === roomId) return room;
|
||||
return null;
|
||||
});
|
||||
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId(),
|
||||
client.getDeviceId(),
|
||||
userId,
|
||||
deviceId,
|
||||
);
|
||||
stoppedEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
client.getUserId(),
|
||||
client.getDeviceId(),
|
||||
userId,
|
||||
deviceId,
|
||||
infoEvent,
|
||||
);
|
||||
room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline());
|
||||
testRecording = new VoiceBroadcastRecording(infoEvent, client);
|
||||
testPlayback = new VoiceBroadcastPlayback(infoEvent, client);
|
||||
mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }) => {
|
||||
mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }): ReactElement | null => {
|
||||
if (testRecording === recording) {
|
||||
return <div data-testid="voice-broadcast-recording-body" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }) => {
|
||||
mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }): ReactElement | null => {
|
||||
if (testPlayback === playback) {
|
||||
return <div data-testid="voice-broadcast-playback-body" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
jest.spyOn(VoiceBroadcastRecordingsStore.instance(), "getByInfoEvent").mockImplementation(
|
||||
(getEvent: MatrixEvent, getClient: MatrixClient) => {
|
||||
(getEvent: MatrixEvent, getClient: MatrixClient): VoiceBroadcastRecording => {
|
||||
if (getEvent === infoEvent && getClient === client) {
|
||||
return testRecording;
|
||||
}
|
||||
|
||||
throw new Error("unexpected event");
|
||||
},
|
||||
);
|
||||
|
||||
jest.spyOn(VoiceBroadcastPlaybacksStore.instance(), "getByInfoEvent").mockImplementation(
|
||||
(getEvent: MatrixEvent) => {
|
||||
(getEvent: MatrixEvent): VoiceBroadcastPlayback => {
|
||||
if (getEvent === infoEvent) {
|
||||
return testPlayback;
|
||||
}
|
||||
|
||||
throw new Error("unexpected event");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -48,6 +48,9 @@ describe("VoiceBroadcastPlaybackBody", () => {
|
|||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
mocked(client.relations).mockClear();
|
||||
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||
|
||||
infoEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,6 +35,8 @@ import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
|
|||
describe("VoiceBroadcastPlaybacksStore", () => {
|
||||
const roomId = "!room:example.com";
|
||||
let client: MatrixClient;
|
||||
let userId: string;
|
||||
let deviceId: string;
|
||||
let room: Room;
|
||||
let infoEvent1: MatrixEvent;
|
||||
let infoEvent2: MatrixEvent;
|
||||
|
@ -45,24 +47,31 @@ describe("VoiceBroadcastPlaybacksStore", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
userId = client.getUserId() || "";
|
||||
deviceId = client.getDeviceId() || "";
|
||||
mocked(client.relations).mockClear();
|
||||
mocked(client.relations).mockResolvedValue({ events: [] });
|
||||
|
||||
room = mkStubRoom(roomId, "test room", client);
|
||||
mocked(client.getRoom).mockImplementation((roomId: string) => {
|
||||
mocked(client.getRoom).mockImplementation((roomId: string): Room | null => {
|
||||
if (roomId === room.roomId) {
|
||||
return room;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
infoEvent1 = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId(),
|
||||
client.getDeviceId(),
|
||||
userId,
|
||||
deviceId,
|
||||
);
|
||||
infoEvent2 = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
client.getUserId(),
|
||||
client.getDeviceId(),
|
||||
userId,
|
||||
deviceId,
|
||||
);
|
||||
playback1 = new VoiceBroadcastPlayback(infoEvent1, client);
|
||||
jest.spyOn(playback1, "off");
|
||||
|
|
Loading…
Reference in a new issue