From 66c20a07983b1b4092facf240b414878a03dde5c Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 31 Oct 2022 18:35:02 +0100 Subject: [PATCH] Display voice broadcast total length (#9517) --- .../molecules/_VoiceBroadcastBody.pcss | 7 ++ .../molecules/VoiceBroadcastPlaybackBody.tsx | 7 ++ .../hooks/useVoiceBroadcastPlayback.ts | 8 ++ .../models/VoiceBroadcastPlayback.ts | 64 ++++++----- .../utils/VoiceBroadcastChunkEvents.ts | 99 +++++++++++++++++ .../VoiceBroadcastPlaybackBody-test.tsx | 38 +++++-- .../VoiceBroadcastPlaybackBody-test.tsx.snap | 104 ++++++++++++++++++ .../models/VoiceBroadcastPlayback-test.ts | 75 ++++--------- .../utils/VoiceBroadcastChunkEvents-test.ts | 99 +++++++++++++++++ test/voice-broadcast/utils/test-utils.ts | 36 +++++- 10 files changed, 443 insertions(+), 94 deletions(-) create mode 100644 src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts create mode 100644 test/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss index 37606f993c..c399200638 100644 --- a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss +++ b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss @@ -17,7 +17,9 @@ limitations under the License. .mx_VoiceBroadcastBody { background-color: $quinary-content; border-radius: 8px; + color: $secondary-content; display: inline-block; + font-size: $font-12px; padding: $spacing-12; } @@ -37,3 +39,8 @@ limitations under the License. display: flex; justify-content: space-around; } + +.mx_VoiceBroadcastBody_timerow { + display: flex; + justify-content: flex-end; +} diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx index e6f2e343cb..1d6b89dca9 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx @@ -27,6 +27,7 @@ import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback import { Icon as PlayIcon } from "../../../../res/img/element-icons/play.svg"; import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg"; import { _t } from "../../../languageHandler"; +import Clock from "../../../components/views/audio_messages/Clock"; interface VoiceBroadcastPlaybackBodyProps { playback: VoiceBroadcastPlayback; @@ -36,6 +37,7 @@ export const VoiceBroadcastPlaybackBody: React.FC { const { + length, live, room, sender, @@ -73,6 +75,8 @@ export const VoiceBroadcastPlaybackBody: React.FC; } + const lengthSeconds = Math.round(length / 1000); + return (
{ control }
+
+ +
); }; diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts index 452035fbb9..7ed2b5682f 100644 --- a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts @@ -50,7 +50,15 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => { }, ); + const [length, setLength] = useState(playback.getLength()); + useTypedEventEmitter( + playback, + VoiceBroadcastPlaybackEvent.LengthChanged, + length => setLength(length), + ); + return { + length, live: playbackInfoState !== VoiceBroadcastInfoState.Stopped, room: room, sender: playback.infoEvent.sender, diff --git a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts index dcecaa7282..a3834a7e79 100644 --- a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts @@ -31,6 +31,7 @@ import { IDestroyable } from "../../utils/IDestroyable"; import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper"; import { getReferenceRelationsForEvent } from "../../events"; +import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents"; export enum VoiceBroadcastPlaybackState { Paused, @@ -59,9 +60,9 @@ export class VoiceBroadcastPlayback implements IDestroyable { private state = VoiceBroadcastPlaybackState.Stopped; private infoState: VoiceBroadcastInfoState; - private chunkEvents = new Map(); - private queue: Playback[] = []; - private currentlyPlaying: Playback; + private chunkEvents = new VoiceBroadcastChunkEvents(); + private playbacks = new Map(); + private currentlyPlaying: MatrixEvent; private lastInfoEvent: MatrixEvent; private chunkRelationHelper: RelationsHelper; private infoRelationHelper: RelationsHelper; @@ -101,11 +102,12 @@ export class VoiceBroadcastPlayback if (!eventId || eventId.startsWith("~!") // don't add local events || event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event - || this.chunkEvents.has(eventId)) { + ) { return false; } - this.chunkEvents.set(eventId, event); + this.chunkEvents.addEvent(event); + this.emit(VoiceBroadcastPlaybackEvent.LengthChanged, this.chunkEvents.getLength()); if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) { await this.enqueueChunk(event); @@ -143,6 +145,8 @@ export class VoiceBroadcastPlayback return; } + this.chunkEvents.addEvents(chunkEvents); + for (const chunkEvent of chunkEvents) { await this.enqueueChunk(chunkEvent); } @@ -158,7 +162,7 @@ export class VoiceBroadcastPlayback const playback = PlaybackManager.instance.createPlaybackInstance(buffer); await playback.prepare(); playback.clockInfo.populatePlaceholdersFrom(chunkEvent); - this.queue[sequenceNumber - 1] = playback; // -1 because the sequence number starts at 1 + this.playbacks.set(chunkEvent.getId(), playback); playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, state)); } @@ -167,16 +171,18 @@ export class VoiceBroadcastPlayback return; } - await this.playNext(playback); + await this.playNext(); } - private async playNext(current: Playback): Promise { - const next = this.queue[this.queue.indexOf(current) + 1]; + private async playNext(): Promise { + if (!this.currentlyPlaying) return; + + const next = this.chunkEvents.getNext(this.currentlyPlaying); if (next) { this.setState(VoiceBroadcastPlaybackState.Playing); this.currentlyPlaying = next; - await next.play(); + await this.playbacks.get(next.getId())?.play(); return; } @@ -188,19 +194,25 @@ export class VoiceBroadcastPlayback } } + public getLength(): number { + return this.chunkEvents.getLength(); + } + public async start(): Promise { - if (this.queue.length === 0) { + if (this.playbacks.size === 0) { await this.loadChunks(); } - const toPlayIndex = this.getInfoState() === VoiceBroadcastInfoState.Stopped - ? 0 // start at the beginning for an ended voice broadcast - : this.queue.length - 1; // start at the current chunk for an ongoing voice broadcast + const chunkEvents = this.chunkEvents.getEvents(); - if (this.queue[toPlayIndex]) { + const toPlay = this.getInfoState() === VoiceBroadcastInfoState.Stopped + ? chunkEvents[0] // start at the beginning for an ended voice broadcast + : chunkEvents[chunkEvents.length - 1]; // start at the current chunk for an ongoing voice broadcast + + if (this.playbacks.has(toPlay?.getId())) { this.setState(VoiceBroadcastPlaybackState.Playing); - this.currentlyPlaying = this.queue[toPlayIndex]; - await this.currentlyPlaying.play(); + this.currentlyPlaying = toPlay; + await this.playbacks.get(toPlay.getId()).play(); return; } @@ -208,14 +220,14 @@ export class VoiceBroadcastPlayback } public get length(): number { - return this.chunkEvents.size; + return this.chunkEvents.getLength(); } public stop(): void { this.setState(VoiceBroadcastPlaybackState.Stopped); if (this.currentlyPlaying) { - this.currentlyPlaying.stop(); + this.playbacks.get(this.currentlyPlaying.getId()).stop(); } } @@ -225,7 +237,7 @@ export class VoiceBroadcastPlayback this.setState(VoiceBroadcastPlaybackState.Paused); if (!this.currentlyPlaying) return; - this.currentlyPlaying.pause(); + this.playbacks.get(this.currentlyPlaying.getId()).pause(); } public resume(): void { @@ -236,7 +248,7 @@ export class VoiceBroadcastPlayback } this.setState(VoiceBroadcastPlaybackState.Playing); - this.currentlyPlaying.play(); + this.playbacks.get(this.currentlyPlaying.getId()).play(); } /** @@ -285,15 +297,13 @@ export class VoiceBroadcastPlayback this.emit(VoiceBroadcastPlaybackEvent.InfoStateChanged, state); } - private destroyQueue(): void { - this.queue.forEach(p => p.destroy()); - this.queue = []; - } - public destroy(): void { this.chunkRelationHelper.destroy(); this.infoRelationHelper.destroy(); this.removeAllListeners(); - this.destroyQueue(); + + this.chunkEvents = new VoiceBroadcastChunkEvents(); + this.playbacks.forEach(p => p.destroy()); + this.playbacks = new Map(); } } diff --git a/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts b/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts new file mode 100644 index 0000000000..ac7e90361d --- /dev/null +++ b/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts @@ -0,0 +1,99 @@ +/* +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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { VoiceBroadcastChunkEventType } from ".."; + +/** + * Voice broadcast chunk collection. + * Orders chunks by sequence (if available) or timestamp. + */ +export class VoiceBroadcastChunkEvents { + private events: MatrixEvent[] = []; + + public getEvents(): MatrixEvent[] { + return [...this.events]; + } + + public getNext(event: MatrixEvent): MatrixEvent | undefined { + return this.events[this.events.indexOf(event) + 1]; + } + + public addEvent(event: MatrixEvent): void { + if (this.addOrReplaceEvent(event)) { + this.sort(); + } + } + + public addEvents(events: MatrixEvent[]): void { + const atLeastOneNew = events.reduce((newSoFar: boolean, event: MatrixEvent): boolean => { + return this.addOrReplaceEvent(event) || newSoFar; + }, false); + + if (atLeastOneNew) { + this.sort(); + } + } + + public includes(event: MatrixEvent): boolean { + return !!this.events.find(e => e.getId() === event.getId()); + } + + public getLength(): number { + return this.events.reduce((length: number, event: MatrixEvent) => { + return length + this.calculateChunkLength(event); + }, 0); + } + + private calculateChunkLength(event: MatrixEvent): number { + return event.getContent()?.["org.matrix.msc1767.audio"]?.duration + || event.getContent()?.info?.duration + || 0; + } + + private addOrReplaceEvent = (event: MatrixEvent): boolean => { + this.events = this.events.filter(e => e.getId() !== event.getId()); + this.events.push(event); + return true; + }; + + /** + * Sort by sequence, if available for all events. + * Else fall back to timestamp. + */ + private sort(): void { + const compareFn = this.allHaveSequence() ? this.compareBySequence : this.compareByTimestamp; + this.events.sort(compareFn); + } + + private compareBySequence = (a: MatrixEvent, b: MatrixEvent): number => { + const aSequence = a.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0; + const bSequence = b.getContent()?.[VoiceBroadcastChunkEventType]?.sequence || 0; + return aSequence - bSequence; + }; + + private compareByTimestamp = (a: MatrixEvent, b: MatrixEvent): number => { + return a.getTs() - b.getTs(); + }; + + private allHaveSequence(): boolean { + return !this.events.some((event: MatrixEvent) => { + const sequence = event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence; + return parseInt(sequence, 10) !== sequence; + }); + } +} diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx index 7cd5acd295..3b30f461f7 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx @@ -16,17 +16,19 @@ limitations under the License. import React from "react"; import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { render, RenderResult } from "@testing-library/react"; +import { act, render, RenderResult } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { mocked } from "jest-mock"; import { - VoiceBroadcastInfoEventType, + VoiceBroadcastInfoState, VoiceBroadcastPlayback, VoiceBroadcastPlaybackBody, + VoiceBroadcastPlaybackEvent, VoiceBroadcastPlaybackState, } from "../../../../src/voice-broadcast"; -import { mkEvent, stubClient } from "../../../test-utils"; +import { stubClient } from "../../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils"; // mock RoomAvatar, because it is doing too much fancy stuff jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({ @@ -46,19 +48,19 @@ describe("VoiceBroadcastPlaybackBody", () => { beforeAll(() => { client = stubClient(); - infoEvent = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - content: {}, - room: roomId, - user: userId, - }); + infoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + userId, + client.getDeviceId(), + ); }); beforeEach(() => { playback = new VoiceBroadcastPlayback(infoEvent, client); - jest.spyOn(playback, "toggle"); + jest.spyOn(playback, "toggle").mockImplementation(() => Promise.resolve()); jest.spyOn(playback, "getState"); + jest.spyOn(playback, "getLength").mockReturnValue((23 * 60 + 42) * 1000); // 23:42 }); describe("when rendering a buffering voice broadcast", () => { @@ -72,7 +74,7 @@ describe("VoiceBroadcastPlaybackBody", () => { }); }); - describe(`when rendering a ${VoiceBroadcastPlaybackState.Stopped} broadcast`, () => { + describe(`when rendering a stopped broadcast`, () => { beforeEach(() => { mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped); renderResult = render(); @@ -87,6 +89,18 @@ describe("VoiceBroadcastPlaybackBody", () => { expect(playback.toggle).toHaveBeenCalled(); }); }); + + describe("and the length updated", () => { + beforeEach(() => { + act(() => { + playback.emit(VoiceBroadcastPlaybackEvent.LengthChanged, 42000); // 00:42 + }); + }); + + it("should render as expected", () => { + expect(renderResult.container).toMatchSnapshot(); + }); + }); }); describe.each([ diff --git a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap index 1eace91e48..9f9793bfeb 100644 --- a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap +++ b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap @@ -64,6 +64,15 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 0 broadcast should render a /> +
+ + 23:42 + +
`; @@ -132,6 +141,15 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 1 broadcast should render a /> +
+ + 23:42 + +
`; @@ -201,6 +219,92 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s /> +
+ + 23:42 + +
+ + +`; + +exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast and the length updated should render as expected 1`] = ` +
+
+
+
+ room avatar: + My room +
+
+
+ My room +
+
+
+ + @user:example.com + +
+
+
+ Voice broadcast +
+
+
+
+ Live +
+
+
+
+
+
+
+
+ + 00:42 + +
`; diff --git a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts index 8471727026..cffd10857c 100644 --- a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { EventType, MatrixClient, MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix"; import { Relations } from "matrix-js-sdk/src/models/relations"; import { Playback, PlaybackState } from "../../../src/audio/Playback"; @@ -24,7 +24,6 @@ import { getReferenceRelationsForEvent } from "../../../src/events"; import { RelationsHelperEvent } from "../../../src/events/RelationsHelper"; import { MediaEventHelper } from "../../../src/utils/MediaEventHelper"; import { - VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState, VoiceBroadcastPlayback, @@ -33,6 +32,7 @@ import { } from "../../../src/voice-broadcast"; import { mkEvent, stubClient } from "../../test-utils"; import { createTestPlayback } from "../../test-utils/audio"; +import { mkVoiceBroadcastChunkEvent } from "../utils/test-utils"; jest.mock("../../../src/events/getReferenceRelationsForEvent", () => ({ getReferenceRelationsForEvent: jest.fn(), @@ -49,19 +49,15 @@ describe("VoiceBroadcastPlayback", () => { let infoEvent: MatrixEvent; let playback: VoiceBroadcastPlayback; let onStateChanged: (state: VoiceBroadcastPlaybackState) => void; - let chunk0Event: MatrixEvent; let chunk1Event: MatrixEvent; let chunk2Event: MatrixEvent; let chunk3Event: MatrixEvent; - const chunk0Data = new ArrayBuffer(1); const chunk1Data = new ArrayBuffer(2); const chunk2Data = new ArrayBuffer(3); const chunk3Data = new ArrayBuffer(3); - let chunk0Helper: MediaEventHelper; let chunk1Helper: MediaEventHelper; let chunk2Helper: MediaEventHelper; let chunk3Helper: MediaEventHelper; - let chunk0Playback: Playback; let chunk1Playback: Playback; let chunk2Playback: Playback; let chunk3Playback: Playback; @@ -96,21 +92,6 @@ 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: { @@ -152,25 +133,20 @@ describe("VoiceBroadcastPlayback", () => { beforeAll(() => { client = stubClient(); - // crap event to test 0 as first sequence number - chunk0Event = mkChunkEvent(0); - chunk1Event = mkChunkEvent(1); - chunk2Event = mkChunkEvent(2); - chunk3Event = mkChunkEvent(3); + chunk1Event = mkVoiceBroadcastChunkEvent(userId, roomId, 23, 1); + chunk2Event = mkVoiceBroadcastChunkEvent(userId, roomId, 23, 2); + chunk3Event = mkVoiceBroadcastChunkEvent(userId, roomId, 23, 3); - chunk0Helper = mkChunkHelper(chunk0Data); chunk1Helper = mkChunkHelper(chunk1Data); chunk2Helper = mkChunkHelper(chunk2Data); chunk3Helper = mkChunkHelper(chunk3Data); - chunk0Playback = createTestPlayback(); chunk1Playback = createTestPlayback(); chunk2Playback = createTestPlayback(); chunk3Playback = 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; if (buffer === chunk3Data) return chunk3Playback; @@ -178,7 +154,6 @@ describe("VoiceBroadcastPlayback", () => { ); mocked(MediaEventHelper).mockImplementation((event: MatrixEvent) => { - if (event === chunk0Event) return chunk0Helper; if (event === chunk1Event) return chunk1Helper; if (event === chunk2Event) return chunk2Helper; if (event === chunk3Event) return chunk3Helper; @@ -240,7 +215,7 @@ describe("VoiceBroadcastPlayback", () => { beforeEach(() => { infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Resumed); playback = mkPlayback(); - setUpChunkEvents([chunk2Event, chunk0Event, chunk1Event]); + setUpChunkEvents([chunk2Event, chunk1Event]); }); describe("and calling start", () => { @@ -282,20 +257,9 @@ describe("VoiceBroadcastPlayback", () => { playback = mkPlayback(); }); - describe("and there is only a 0 sequence event", () => { - beforeEach(() => { - setUpChunkEvents([chunk0Event]); - }); - - describe("and calling start", () => { - startPlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering); - }); - }); - describe("and there are some chunks", () => { beforeEach(() => { - setUpChunkEvents([chunk2Event, chunk0Event, chunk1Event]); + setUpChunkEvents([chunk2Event, chunk1Event]); }); it("should expose the info event", () => { @@ -337,6 +301,21 @@ describe("VoiceBroadcastPlayback", () => { 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(); + }); + }); }); describe("and calling toggle for the first time", () => { @@ -378,16 +357,6 @@ describe("VoiceBroadcastPlayback", () => { itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing); }); }); - - describe("and calling destroy", () => { - beforeEach(() => { - playback.destroy(); - }); - - it("should call removeAllListeners", () => { - expect(playback.removeAllListeners).toHaveBeenCalled(); - }); - }); }); }); }); diff --git a/test/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts b/test/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts new file mode 100644 index 0000000000..1c09c94d91 --- /dev/null +++ b/test/voice-broadcast/utils/VoiceBroadcastChunkEvents-test.ts @@ -0,0 +1,99 @@ +/* +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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { VoiceBroadcastChunkEvents } from "../../../src/voice-broadcast/utils/VoiceBroadcastChunkEvents"; +import { mkVoiceBroadcastChunkEvent } from "./test-utils"; + +describe("VoiceBroadcastChunkEvents", () => { + const userId = "@user:example.com"; + const roomId = "!room:example.com"; + let eventSeq1Time1: MatrixEvent; + let eventSeq2Time4: MatrixEvent; + let eventSeq3Time2: MatrixEvent; + let eventSeq4Time1: MatrixEvent; + let eventSeqUTime3: MatrixEvent; + let eventSeq2Time4Dup: MatrixEvent; + let chunkEvents: VoiceBroadcastChunkEvents; + + beforeEach(() => { + eventSeq1Time1 = mkVoiceBroadcastChunkEvent(userId, roomId, 7, 1, 1); + eventSeq2Time4 = mkVoiceBroadcastChunkEvent(userId, roomId, 23, 2, 4); + eventSeq2Time4Dup = mkVoiceBroadcastChunkEvent(userId, roomId, 3141, 2, 4); + jest.spyOn(eventSeq2Time4Dup, "getId").mockReturnValue(eventSeq2Time4.getId()); + eventSeq3Time2 = mkVoiceBroadcastChunkEvent(userId, roomId, 42, 3, 2); + eventSeq4Time1 = mkVoiceBroadcastChunkEvent(userId, roomId, 69, 4, 1); + eventSeqUTime3 = mkVoiceBroadcastChunkEvent(userId, roomId, 314, undefined, 3); + chunkEvents = new VoiceBroadcastChunkEvents(); + }); + + describe("when adding events that all have a sequence", () => { + beforeEach(() => { + chunkEvents.addEvent(eventSeq2Time4); + chunkEvents.addEvent(eventSeq1Time1); + chunkEvents.addEvents([ + eventSeq4Time1, + eventSeq2Time4Dup, + eventSeq3Time2, + ]); + }); + + it("should provide the events sort by sequence", () => { + expect(chunkEvents.getEvents()).toEqual([ + eventSeq1Time1, + eventSeq2Time4Dup, + eventSeq3Time2, + eventSeq4Time1, + ]); + }); + + it("getLength should return the total length of all chunks", () => { + expect(chunkEvents.getLength()).toBe(3259); + }); + + it("should return the expected next chunk", () => { + expect(chunkEvents.getNext(eventSeq2Time4Dup)).toBe(eventSeq3Time2); + }); + + it("should return undefined for next last chunk", () => { + expect(chunkEvents.getNext(eventSeq4Time1)).toBeUndefined(); + }); + }); + + describe("when adding events where at least one does not have a sequence", () => { + beforeEach(() => { + chunkEvents.addEvent(eventSeq2Time4); + chunkEvents.addEvent(eventSeq1Time1); + chunkEvents.addEvents([ + eventSeq4Time1, + eventSeqUTime3, + eventSeq2Time4Dup, + eventSeq3Time2, + ]); + }); + + it("should provide the events sort by timestamp without duplicates", () => { + expect(chunkEvents.getEvents()).toEqual([ + eventSeq1Time1, + eventSeq4Time1, + eventSeq3Time2, + eventSeqUTime3, + eventSeq2Time4Dup, + ]); + }); + }); +}); diff --git a/test/voice-broadcast/utils/test-utils.ts b/test/voice-broadcast/utils/test-utils.ts index 09a2ba0ed3..0a0f53ee28 100644 --- a/test/voice-broadcast/utils/test-utils.ts +++ b/test/voice-broadcast/utils/test-utils.ts @@ -14,9 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix"; -import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../src/voice-broadcast"; +import { + VoiceBroadcastChunkEventType, + VoiceBroadcastInfoEventType, + VoiceBroadcastInfoState, +} from "../../../src/voice-broadcast"; import { mkEvent } from "../../test-utils"; export const mkVoiceBroadcastInfoStateEvent = ( @@ -48,3 +52,31 @@ export const mkVoiceBroadcastInfoStateEvent = ( }, }); }; + +export const mkVoiceBroadcastChunkEvent = ( + userId: string, + roomId: string, + duration: number, + sequence?: number, + timestamp?: number, +): MatrixEvent => { + return mkEvent({ + event: true, + user: userId, + room: roomId, + type: EventType.RoomMessage, + content: { + msgtype: MsgType.Audio, + ["org.matrix.msc1767.audio"]: { + duration, + }, + info: { + duration, + }, + [VoiceBroadcastChunkEventType]: { + ...(sequence ? { sequence } : {}), + }, + }, + ts: timestamp, + }); +};