Display voice broadcast total length (#9517)

This commit is contained in:
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

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