= ({
sender,
showBroadcast = false,
showClose = false,
+ timeLeft,
}) => {
const broadcast = showBroadcast
?
@@ -54,6 +59,13 @@ export const VoiceBroadcastHeader: React.FC
= ({
: null;
+ const timeLeftLine = timeLeft
+ ?
+
+
+
+ : null;
+
return
@@ -64,6 +76,7 @@ export const VoiceBroadcastHeader: React.FC = ({
{ sender.name }
+ { timeLeftLine }
{ broadcast }
{ liveBadge }
diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx
index d737c6aaa2..fdf0e7a224 100644
--- a/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx
+++ b/src/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip.tsx
@@ -35,6 +35,7 @@ interface VoiceBroadcastRecordingPipProps {
export const VoiceBroadcastRecordingPip: React.FC = ({ recording }) => {
const {
live,
+ timeLeft,
recordingState,
room,
sender,
@@ -58,6 +59,7 @@ export const VoiceBroadcastRecordingPip: React.FC
diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx
index 209b539bf6..07c4427361 100644
--- a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx
+++ b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx
@@ -65,6 +65,13 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) =
},
);
+ const [timeLeft, setTimeLeft] = useState(recording.getTimeLeft());
+ useTypedEventEmitter(
+ recording,
+ VoiceBroadcastRecordingEvent.TimeLeftChanged,
+ setTimeLeft,
+ );
+
const live = [
VoiceBroadcastInfoState.Started,
VoiceBroadcastInfoState.Paused,
@@ -73,6 +80,7 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) =
return {
live,
+ timeLeft,
recordingState,
room,
sender: recording.infoEvent.sender,
diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts
index ac38f96307..87ccd77e9f 100644
--- a/src/voice-broadcast/index.ts
+++ b/src/voice-broadcast/index.ts
@@ -41,6 +41,7 @@ export * from "./stores/VoiceBroadcastPreRecordingStore";
export * from "./stores/VoiceBroadcastRecordingsStore";
export * from "./utils/checkVoiceBroadcastPreConditions";
export * from "./utils/getChunkLength";
+export * from "./utils/getMaxBroadcastLength";
export * from "./utils/hasRoomLiveVoiceBroadcast";
export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice";
export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile";
diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts
index dbab9fb6b8..e080c87224 100644
--- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts
+++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts
@@ -15,12 +15,20 @@ limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
-import { MatrixClient, MatrixEvent, MatrixEventEvent, RelationType } from "matrix-js-sdk/src/matrix";
+import {
+ EventType,
+ MatrixClient,
+ MatrixEvent,
+ MatrixEventEvent,
+ MsgType,
+ RelationType,
+} from "matrix-js-sdk/src/matrix";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import {
ChunkRecordedPayload,
createVoiceBroadcastRecorder,
+ getMaxBroadcastLength,
VoiceBroadcastInfoEventContent,
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
@@ -33,13 +41,17 @@ import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent
import { IDestroyable } from "../../utils/IDestroyable";
import dis from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
+import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
+import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
export enum VoiceBroadcastRecordingEvent {
StateChanged = "liveness_changed",
+ TimeLeftChanged = "time_left_changed",
}
interface EventMap {
[VoiceBroadcastRecordingEvent.StateChanged]: (state: VoiceBroadcastInfoState) => void;
+ [VoiceBroadcastRecordingEvent.TimeLeftChanged]: (timeLeft: number) => void;
}
export class VoiceBroadcastRecording
@@ -49,6 +61,10 @@ export class VoiceBroadcastRecording
private recorder: VoiceBroadcastRecorder;
private sequence = 1;
private dispatcherRef: string;
+ private chunkEvents = new VoiceBroadcastChunkEvents();
+ private chunkRelationHelper: RelationsHelper;
+ private maxLength: number;
+ private timeLeft: number;
public constructor(
public readonly infoEvent: MatrixEvent,
@@ -56,6 +72,8 @@ export class VoiceBroadcastRecording
initialState?: VoiceBroadcastInfoState,
) {
super();
+ this.maxLength = getMaxBroadcastLength();
+ this.timeLeft = this.maxLength;
if (initialState) {
this.state = initialState;
@@ -64,11 +82,41 @@ export class VoiceBroadcastRecording
}
// TODO Michael W: listen for state updates
- //
+
this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.dispatcherRef = dis.register(this.onAction);
+ this.chunkRelationHelper = this.initialiseChunkEventRelation();
}
+ private initialiseChunkEventRelation(): RelationsHelper {
+ const relationsHelper = new RelationsHelper(
+ this.infoEvent,
+ RelationType.Reference,
+ EventType.RoomMessage,
+ this.client,
+ );
+ relationsHelper.on(RelationsHelperEvent.Add, this.onChunkEvent);
+
+ relationsHelper.emitFetchCurrent().catch((err) => {
+ logger.warn("error fetching server side relation for voice broadcast chunks", err);
+ // fall back to local events
+ relationsHelper.emitCurrent();
+ });
+
+ return relationsHelper;
+ }
+
+ private onChunkEvent = (event: MatrixEvent): void => {
+ if (
+ (!event.getId() && !event.getTxnId())
+ || event.getContent()?.msgtype !== MsgType.Audio // don't add non-audio event
+ ) {
+ return;
+ }
+
+ this.chunkEvents.addEvent(event);
+ };
+
private setInitialStateFromInfoEvent(): void {
const room = this.client.getRoom(this.infoEvent.getRoomId());
const relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
@@ -82,6 +130,23 @@ export class VoiceBroadcastRecording
}) ? VoiceBroadcastInfoState.Started : VoiceBroadcastInfoState.Stopped;
}
+ public getTimeLeft(): number {
+ return this.timeLeft;
+ }
+
+ private async setTimeLeft(timeLeft: number): Promise {
+ if (timeLeft <= 0) {
+ // time is up - stop the recording
+ return await this.stop();
+ }
+
+ // do never increase time left; no action if equals
+ if (timeLeft >= this.timeLeft) return;
+
+ this.timeLeft = timeLeft;
+ this.emit(VoiceBroadcastRecordingEvent.TimeLeftChanged, timeLeft);
+ }
+
public async start(): Promise {
return this.getRecorder().start();
}
@@ -127,20 +192,23 @@ export class VoiceBroadcastRecording
if (!this.recorder) {
this.recorder = createVoiceBroadcastRecorder();
this.recorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, this.onChunkRecorded);
+ this.recorder.on(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, this.onCurrentChunkLengthUpdated);
}
return this.recorder;
}
- public destroy(): void {
+ public async destroy(): Promise {
if (this.recorder) {
- this.recorder.off(VoiceBroadcastRecorderEvent.ChunkRecorded, this.onChunkRecorded);
this.recorder.stop();
+ this.recorder.destroy();
}
this.infoEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.removeAllListeners();
dis.unregister(this.dispatcherRef);
+ this.chunkEvents = new VoiceBroadcastChunkEvents();
+ this.chunkRelationHelper.destroy();
}
private onBeforeRedaction = () => {
@@ -163,6 +231,10 @@ export class VoiceBroadcastRecording
this.emit(VoiceBroadcastRecordingEvent.StateChanged, this.state);
}
+ private onCurrentChunkLengthUpdated = (currentChunkLength: number) => {
+ this.setTimeLeft(this.maxLength - this.chunkEvents.getLengthSeconds() - currentChunkLength);
+ };
+
private onChunkRecorded = async (chunk: ChunkRecordedPayload): Promise => {
const { url, file } = await this.uploadFile(chunk);
await this.sendVoiceMessage(chunk, url, file);
diff --git a/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts b/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts
index ad0a109513..f4243cff6b 100644
--- a/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts
+++ b/src/voice-broadcast/utils/VoiceBroadcastChunkEvents.ts
@@ -62,6 +62,10 @@ export class VoiceBroadcastChunkEvents {
}, 0);
}
+ public getLengthSeconds(): number {
+ return this.getLength() / 1000;
+ }
+
/**
* Returns the accumulated length to (excl.) a chunk event.
*/
diff --git a/src/voice-broadcast/utils/getMaxBroadcastLength.ts b/src/voice-broadcast/utils/getMaxBroadcastLength.ts
new file mode 100644
index 0000000000..15eb83b4a9
--- /dev/null
+++ b/src/voice-broadcast/utils/getMaxBroadcastLength.ts
@@ -0,0 +1,29 @@
+/*
+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 SdkConfig, { DEFAULTS } from "../../SdkConfig";
+
+/**
+ * Returns the max length for voice broadcasts:
+ * - Tries to get the value from the voice_broadcast.max_length config
+ * - If that fails from DEFAULTS
+ * - If that fails fall back to four hours
+ */
+export const getMaxBroadcastLength = (): number => {
+ return SdkConfig.get("voice_broadcast")?.max_length
+ || DEFAULTS.voice_broadcast?.max_length
+ || 4 * 60 * 60;
+};
diff --git a/test/SdkConfig-test.ts b/test/SdkConfig-test.ts
index a497946b8f..a6ac58e9c5 100644
--- a/test/SdkConfig-test.ts
+++ b/test/SdkConfig-test.ts
@@ -27,14 +27,16 @@ describe("SdkConfig", () => {
beforeEach(() => {
SdkConfig.put({
voice_broadcast: {
- chunk_length: 1337,
+ chunk_length: 42,
+ max_length: 1337,
},
});
});
it("should return the custom config", () => {
const customConfig = JSON.parse(JSON.stringify(DEFAULTS));
- customConfig.voice_broadcast.chunk_length = 1337;
+ customConfig.voice_broadcast.chunk_length = 42;
+ customConfig.voice_broadcast.max_length = 1337;
expect(SdkConfig.get()).toEqual(customConfig);
});
});
diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts
index 27c33c900e..5b75d87a53 100644
--- a/test/test-utils/test-utils.ts
+++ b/test/test-utils/test-utils.ts
@@ -171,7 +171,9 @@ export function createTestClient(): MatrixClient {
setPusher: jest.fn().mockResolvedValue(undefined),
setPushRuleEnabled: jest.fn().mockResolvedValue(undefined),
setPushRuleActions: jest.fn().mockResolvedValue(undefined),
- relations: jest.fn().mockRejectedValue(undefined),
+ relations: jest.fn().mockResolvedValue({
+ events: [],
+ }),
isCryptoEnabled: jest.fn().mockReturnValue(false),
hasLazyLoadMembersEnabled: jest.fn().mockReturnValue(false),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
diff --git a/test/utils/DateUtils-test.ts b/test/utils/DateUtils-test.ts
index 784be85ea7..2815b972d2 100644
--- a/test/utils/DateUtils-test.ts
+++ b/test/utils/DateUtils-test.ts
@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { formatSeconds, formatRelativeTime, formatDuration, formatFullDateNoDayISO } from "../../src/DateUtils";
+import {
+ formatSeconds,
+ formatRelativeTime,
+ formatDuration,
+ formatFullDateNoDayISO,
+ formatTimeLeft,
+} from "../../src/DateUtils";
import { REPEATABLE_DATE } from "../test-utils";
describe("formatSeconds", () => {
@@ -99,3 +105,17 @@ describe("formatFullDateNoDayISO", () => {
expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z");
});
});
+
+describe("formatTimeLeft", () => {
+ it.each([
+ [null, "0s left"],
+ [0, "0s left"],
+ [23, "23s left"],
+ [60 + 23, "1m 23s left"],
+ [60 * 60, "1h 0m 0s left"],
+ [60 * 60 + 23, "1h 0m 23s left"],
+ [5 * 60 * 60 + 7 * 60 + 23, "5h 7m 23s left"],
+ ])("should format %s to %s", (seconds: number, expected: string) => {
+ expect(formatTimeLeft(seconds)).toBe(expected);
+ });
+});
diff --git a/test/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts b/test/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts
index df7da24ce5..29d96235e0 100644
--- a/test/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts
+++ b/test/voice-broadcast/audio/VoiceBroadcastRecorder-test.ts
@@ -26,7 +26,19 @@ import {
VoiceBroadcastRecorderEvent,
} from "../../../src/voice-broadcast";
-jest.mock("../../../src/audio/VoiceRecording");
+// mock VoiceRecording because it contains all the audio APIs
+jest.mock("../../../src/audio/VoiceRecording", () => ({
+ VoiceRecording: jest.fn().mockReturnValue({
+ disableMaxLength: jest.fn(),
+ emit: jest.fn(),
+ liveData: {
+ onUpdate: jest.fn(),
+ },
+ start: jest.fn(),
+ stop: jest.fn(),
+ destroy: jest.fn(),
+ }),
+}));
describe("VoiceBroadcastRecorder", () => {
describe("createVoiceBroadcastRecorder", () => {
@@ -46,7 +58,6 @@ describe("VoiceBroadcastRecorder", () => {
it("should return a VoiceBroadcastRecorder instance with targetChunkLength from config", () => {
const voiceBroadcastRecorder = createVoiceBroadcastRecorder();
- expect(mocked(VoiceRecording).mock.instances[0].disableMaxLength).toHaveBeenCalled();
expect(voiceBroadcastRecorder).toBeInstanceOf(VoiceBroadcastRecorder);
expect(voiceBroadcastRecorder.targetChunkLength).toBe(1337);
});
diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx
index f07b7dd0bd..6a0c1016ff 100644
--- a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx
+++ b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx
@@ -37,7 +37,16 @@ jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
}),
}));
-jest.mock("../../../../src/audio/VoiceRecording");
+// mock VoiceRecording because it contains all the audio APIs
+jest.mock("../../../../src/audio/VoiceRecording", () => ({
+ VoiceRecording: jest.fn().mockReturnValue({
+ disableMaxLength: jest.fn(),
+ liveData: {
+ onUpdate: jest.fn(),
+ },
+ start: jest.fn(),
+ }),
+}));
describe("VoiceBroadcastRecordingPip", () => {
const roomId = "!room:example.com";
diff --git a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap
index ee034a082e..f17f59ef3d 100644
--- a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap
+++ b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastRecordingPip-test.tsx.snap
@@ -32,6 +32,18 @@ exports[`VoiceBroadcastRecordingPip when rendering a paused recording should ren
@userId:matrix.org
+
+
({
...jest.requireActual("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder") as object,
createVoiceBroadcastRecorder: jest.fn(),
}));
+// mock VoiceRecording because it contains all the audio APIs
+jest.mock("../../../src/audio/VoiceRecording", () => ({
+ VoiceRecording: jest.fn().mockReturnValue({
+ disableMaxLength: jest.fn(),
+ liveData: {
+ onUpdate: jest.fn(),
+ },
+ off: jest.fn(),
+ on: jest.fn(),
+ start: jest.fn(),
+ stop: jest.fn(),
+ destroy: jest.fn(),
+ contentType: "audio/ogg",
+ }),
+}));
+
jest.mock("../../../src/ContentMessages", () => ({
uploadFile: jest.fn(),
}));
@@ -61,13 +79,13 @@ describe("VoiceBroadcastRecording", () => {
const roomId = "!room:example.com";
const uploadedUrl = "mxc://example.com/vb";
const uploadedFile = { file: true } as unknown as IEncryptedFile;
+ const maxLength = getMaxBroadcastLength();
let room: Room;
let client: MatrixClient;
let infoEvent: MatrixEvent;
let voiceBroadcastRecording: VoiceBroadcastRecording;
let onStateChanged: (state: VoiceBroadcastInfoState) => void;
let voiceBroadcastRecorder: VoiceBroadcastRecorder;
- let onChunkRecorded: (chunk: ChunkRecordedPayload) => Promise;
const mkVoiceBroadcastInfoEvent = (content: VoiceBroadcastInfoEventContent) => {
return mkEvent({
@@ -83,6 +101,7 @@ describe("VoiceBroadcastRecording", () => {
voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client);
voiceBroadcastRecording.on(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged);
jest.spyOn(voiceBroadcastRecording, "destroy");
+ jest.spyOn(voiceBroadcastRecording, "emit");
jest.spyOn(voiceBroadcastRecording, "removeAllListeners");
};
@@ -111,6 +130,58 @@ describe("VoiceBroadcastRecording", () => {
});
};
+ const itShouldSendAVoiceMessage = (data: number[], size: number, duration: number, sequence: number) => {
+ // events contain milliseconds
+ duration *= 1000;
+
+ it("should send a voice message", () => {
+ expect(uploadFile).toHaveBeenCalledWith(
+ client,
+ roomId,
+ new Blob([new Uint8Array(data)], { type: voiceBroadcastRecorder.contentType }),
+ );
+
+ expect(mocked(client.sendMessage)).toHaveBeenCalledWith(
+ roomId,
+ {
+ body: "Voice message",
+ file: {
+ file: true,
+ },
+ info: {
+ duration,
+ mimetype: "audio/ogg",
+ size,
+ },
+ ["m.relates_to"]: {
+ event_id: infoEvent.getId(),
+ rel_type: "m.reference",
+ },
+ msgtype: "m.audio",
+ ["org.matrix.msc1767.audio"]: {
+ duration,
+ waveform: undefined,
+ },
+ ["org.matrix.msc1767.file"]: {
+ file: {
+ file: true,
+ },
+ mimetype: "audio/ogg",
+ name: "Voice message.ogg",
+ size,
+ url: "mxc://example.com/vb",
+ },
+ ["org.matrix.msc1767.text"]: "Voice message",
+ ["org.matrix.msc3245.voice"]: {},
+ url: "mxc://example.com/vb",
+ ["io.element.voice_broadcast_chunk"]: {
+ sequence,
+ },
+ },
+ );
+ });
+ };
+
beforeEach(() => {
client = stubClient();
room = mkStubRoom(roomId, "Test Room", client);
@@ -120,23 +191,11 @@ describe("VoiceBroadcastRecording", () => {
}
});
onStateChanged = jest.fn();
- voiceBroadcastRecorder = {
- contentType: "audio/ogg",
- on: jest.fn(),
- off: jest.fn(),
- start: jest.fn(),
- stop: jest.fn(),
- } as unknown as VoiceBroadcastRecorder;
+ voiceBroadcastRecorder = new VoiceBroadcastRecorder(new VoiceRecording(), getChunkLength());
+ jest.spyOn(voiceBroadcastRecorder, "start");
+ jest.spyOn(voiceBroadcastRecorder, "stop");
+ jest.spyOn(voiceBroadcastRecorder, "destroy");
mocked(createVoiceBroadcastRecorder).mockReturnValue(voiceBroadcastRecorder);
- onChunkRecorded = jest.fn();
-
- mocked(voiceBroadcastRecorder.on).mockImplementation((event: any, listener: any): VoiceBroadcastRecorder => {
- if (event === VoiceBroadcastRecorderEvent.ChunkRecorded) {
- onChunkRecorded = listener;
- }
-
- return voiceBroadcastRecorder;
- });
mocked(uploadFile).mockResolvedValue({
url: uploadedUrl,
@@ -240,68 +299,64 @@ describe("VoiceBroadcastRecording", () => {
itShouldBeInState(VoiceBroadcastInfoState.Stopped);
});
- describe("and a chunk has been recorded", () => {
- beforeEach(async () => {
- await onChunkRecorded({
- buffer: new Uint8Array([1, 2, 3]),
- length: 23,
- });
+ describe("and a chunk time update occurs", () => {
+ beforeEach(() => {
+ voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, 10);
});
- it("should send a voice message", () => {
- expect(uploadFile).toHaveBeenCalledWith(
- client,
- roomId,
- new Blob([new Uint8Array([1, 2, 3])], { type: voiceBroadcastRecorder.contentType }),
+ it("should update time left", () => {
+ expect(voiceBroadcastRecording.getTimeLeft()).toBe(maxLength - 10);
+ expect(voiceBroadcastRecording.emit).toHaveBeenCalledWith(
+ VoiceBroadcastRecordingEvent.TimeLeftChanged,
+ maxLength - 10,
);
+ });
- expect(mocked(client.sendMessage)).toHaveBeenCalledWith(
- roomId,
+ describe("and a chunk time update occurs, that would increase time left", () => {
+ beforeEach(() => {
+ mocked(voiceBroadcastRecording.emit).mockClear();
+ voiceBroadcastRecorder.emit(VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated, 5);
+ });
+
+ it("should not change time left", () => {
+ expect(voiceBroadcastRecording.getTimeLeft()).toBe(maxLength - 10);
+ expect(voiceBroadcastRecording.emit).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe("and a chunk has been recorded", () => {
+ beforeEach(async () => {
+ voiceBroadcastRecorder.emit(
+ VoiceBroadcastRecorderEvent.ChunkRecorded,
{
- body: "Voice message",
- file: {
- file: true,
- },
- info: {
- duration: 23000,
- mimetype: "audio/ogg",
- size: 3,
- },
- ["m.relates_to"]: {
- event_id: infoEvent.getId(),
- rel_type: "m.reference",
- },
- msgtype: "m.audio",
- ["org.matrix.msc1767.audio"]: {
- duration: 23000,
- waveform: undefined,
- },
- ["org.matrix.msc1767.file"]: {
- file: {
- file: true,
- },
- mimetype: "audio/ogg",
- name: "Voice message.ogg",
- size: 3,
- url: "mxc://example.com/vb",
- },
- ["org.matrix.msc1767.text"]: "Voice message",
- ["org.matrix.msc3245.voice"]: {},
- url: "mxc://example.com/vb",
- ["io.element.voice_broadcast_chunk"]: {
- sequence: 1,
- },
+ buffer: new Uint8Array([1, 2, 3]),
+ length: 23,
},
);
});
+
+ itShouldSendAVoiceMessage([1, 2, 3], 3, 23, 1);
+
+ describe("and another chunk has been recorded, that exceeds the max time", () => {
+ beforeEach(() => {
+ mocked(voiceBroadcastRecorder.stop).mockResolvedValue({
+ buffer: new Uint8Array([23, 24, 25]),
+ length: getMaxBroadcastLength(),
+ });
+ voiceBroadcastRecorder.emit(
+ VoiceBroadcastRecorderEvent.CurrentChunkLengthUpdated,
+ getMaxBroadcastLength(),
+ );
+ });
+
+ itShouldBeInState(VoiceBroadcastInfoState.Stopped);
+ itShouldSendAVoiceMessage([23, 24, 25], 3, getMaxBroadcastLength(), 2);
+ });
});
describe("and calling stop", () => {
beforeEach(async () => {
- await onChunkRecorded({
- buffer: new Uint8Array([1, 2, 3]),
- length: 23,
- });
mocked(voiceBroadcastRecorder.stop).mockResolvedValue({
buffer: new Uint8Array([4, 5, 6]),
length: 42,
@@ -309,52 +364,7 @@ describe("VoiceBroadcastRecording", () => {
await voiceBroadcastRecording.stop();
});
- it("should send the last chunk", () => {
- expect(uploadFile).toHaveBeenCalledWith(
- client,
- roomId,
- new Blob([new Uint8Array([4, 5, 6])], { type: voiceBroadcastRecorder.contentType }),
- );
-
- expect(mocked(client.sendMessage)).toHaveBeenCalledWith(
- roomId,
- {
- body: "Voice message",
- file: {
- file: true,
- },
- info: {
- duration: 42000,
- mimetype: "audio/ogg",
- size: 3,
- },
- ["m.relates_to"]: {
- event_id: infoEvent.getId(),
- rel_type: "m.reference",
- },
- msgtype: "m.audio",
- ["org.matrix.msc1767.audio"]: {
- duration: 42000,
- waveform: undefined,
- },
- ["org.matrix.msc1767.file"]: {
- file: {
- file: true,
- },
- mimetype: "audio/ogg",
- name: "Voice message.ogg",
- size: 3,
- url: "mxc://example.com/vb",
- },
- ["org.matrix.msc1767.text"]: "Voice message",
- ["org.matrix.msc3245.voice"]: {},
- url: "mxc://example.com/vb",
- ["io.element.voice_broadcast_chunk"]: {
- sequence: 2,
- },
- },
- );
- });
+ itShouldSendAVoiceMessage([4, 5, 6], 3, 42, 1);
});
describe.each([
@@ -384,10 +394,7 @@ describe("VoiceBroadcastRecording", () => {
it("should stop the recorder and remove all listeners", () => {
expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled();
- expect(mocked(voiceBroadcastRecorder.off)).toHaveBeenCalledWith(
- VoiceBroadcastRecorderEvent.ChunkRecorded,
- onChunkRecorded,
- );
+ expect(mocked(voiceBroadcastRecorder.destroy)).toHaveBeenCalled();
expect(mocked(voiceBroadcastRecording.removeAllListeners)).toHaveBeenCalled();
});
});
diff --git a/test/voice-broadcast/utils/getMaxBroadcastLength-test.ts b/test/voice-broadcast/utils/getMaxBroadcastLength-test.ts
new file mode 100644
index 0000000000..f2dd138954
--- /dev/null
+++ b/test/voice-broadcast/utils/getMaxBroadcastLength-test.ts
@@ -0,0 +1,60 @@
+/*
+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 { mocked } from "jest-mock";
+
+import SdkConfig, { DEFAULTS } from "../../../src/SdkConfig";
+import { getMaxBroadcastLength } from "../../../src/voice-broadcast";
+
+jest.mock("../../../src/SdkConfig");
+
+describe("getMaxBroadcastLength", () => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe("when there is a value provided by Sdk config", () => {
+ beforeEach(() => {
+ mocked(SdkConfig.get).mockReturnValue({ max_length: 42 });
+ });
+
+ it("should return this value", () => {
+ expect(getMaxBroadcastLength()).toBe(42);
+ });
+ });
+
+ describe("when Sdk config does not provide a value", () => {
+ beforeEach(() => {
+ DEFAULTS.voice_broadcast = {
+ max_length: 23,
+ };
+ });
+
+ it("should return this value", () => {
+ expect(getMaxBroadcastLength()).toBe(23);
+ });
+ });
+
+ describe("if there are no defaults", () => {
+ beforeEach(() => {
+ DEFAULTS.voice_broadcast = undefined;
+ });
+
+ it("should return the fallback value", () => {
+ expect(getMaxBroadcastLength()).toBe(4 * 60 * 60);
+ });
+ });
+});