Display voice broadcast total length (#9517)

pull/28788/head^2
Michael Weimann 2022-10-31 18:35:02 +01:00 committed by GitHub
parent 9b644844da
commit 66c20a0798
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 443 additions and 94 deletions

View File

@ -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;
}

View File

@ -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<VoiceBroadcastPlaybackBodyProp
playback,
}) => {
const {
length,
live,
room,
sender,
@ -73,6 +75,8 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
/>;
}
const lengthSeconds = Math.round(length / 1000);
return (
<div className="mx_VoiceBroadcastBody">
<VoiceBroadcastHeader
@ -84,6 +88,9 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
<div className="mx_VoiceBroadcastBody_controls">
{ control }
</div>
<div className="mx_VoiceBroadcastBody_timerow">
<Clock seconds={lengthSeconds} />
</div>
</div>
);
};

View File

@ -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,

View File

@ -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<string, MatrixEvent>();
private queue: Playback[] = [];
private currentlyPlaying: Playback;
private chunkEvents = new VoiceBroadcastChunkEvents();
private playbacks = new Map<string, Playback>();
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<void> {
const next = this.queue[this.queue.indexOf(current) + 1];
private async playNext(): Promise<void> {
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<void> {
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<string, Playback>();
}
}

View File

@ -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;
});
}
}

View File

@ -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(<VoiceBroadcastPlaybackBody playback={playback} />);
@ -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([

View File

@ -64,6 +64,15 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 0 broadcast should render a
/>
</div>
</div>
<div
class="mx_VoiceBroadcastBody_timerow"
>
<span
class="mx_Clock"
>
23:42
</span>
</div>
</div>
</div>
`;
@ -132,6 +141,15 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 1 broadcast should render a
/>
</div>
</div>
<div
class="mx_VoiceBroadcastBody_timerow"
>
<span
class="mx_Clock"
>
23:42
</span>
</div>
</div>
</div>
`;
@ -201,6 +219,92 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
/>
</div>
</div>
<div
class="mx_VoiceBroadcastBody_timerow"
>
<span
class="mx_Clock"
>
23:42
</span>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast and the length updated should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
<div
class="mx_LiveBadge"
>
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
</div>
<div
class="mx_VoiceBroadcastBody_controls"
>
<div
aria-label="play voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
<div
class="mx_VoiceBroadcastBody_timerow"
>
<span
class="mx_Clock"
>
00:42
</span>
</div>
</div>
</div>
`;

View File

@ -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();
});
});
});
});
});

View File

@ -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,
]);
});
});
});

View File

@ -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,
});
};