Prevent Element appearing in system media controls (#10995)
* Use WebAudio API to play notification sound So that it won't appear in system media control. * Run prettier * Chosse from mp3 and ogg * Run prettier * Use WebAudioAPI everywhere There's still one remoteAudio. I'm not sure what it does. It seems it's only used in tests... * Run prettier * Eliminate a stupid error * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update setupManualMocks.ts * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * mocks Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * mocks Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Simplify Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * covg Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
c61eca8c24
commit
e288f61f0a
12 changed files with 255 additions and 164 deletions
|
@ -28,16 +28,18 @@ import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/we
|
|||
import EventEmitter from "events";
|
||||
import { mocked } from "jest-mock";
|
||||
import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
|
||||
import LegacyCallHandler, {
|
||||
LegacyCallHandlerEvent,
|
||||
AudioID,
|
||||
LegacyCallHandlerEvent,
|
||||
PROTOCOL_PSTN,
|
||||
PROTOCOL_PSTN_PREFIXED,
|
||||
PROTOCOL_SIP_NATIVE,
|
||||
PROTOCOL_SIP_VIRTUAL,
|
||||
} from "../src/LegacyCallHandler";
|
||||
import { stubClient, mkStubRoom, untilDispatch } from "./test-utils";
|
||||
import { mkStubRoom, stubClient, untilDispatch } from "./test-utils";
|
||||
import { MatrixClientPeg } from "../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../src/utils/DMRoomMap";
|
||||
import SdkConfig from "../src/SdkConfig";
|
||||
|
@ -49,6 +51,7 @@ import { VoiceBroadcastInfoState, VoiceBroadcastPlayback, VoiceBroadcastRecordin
|
|||
import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils";
|
||||
import { SdkContextClass } from "../src/contexts/SDKContext";
|
||||
import Modal from "../src/Modal";
|
||||
import { createAudioContext } from "../src/audio/compat";
|
||||
|
||||
jest.mock("../src/Modal");
|
||||
|
||||
|
@ -72,6 +75,11 @@ jest.mock("../src/utils/room/getFunctionalMembers", () => ({
|
|||
getFunctionalMembers: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../src/audio/compat", () => ({
|
||||
...jest.requireActual("../src/audio/compat"),
|
||||
createAudioContext: jest.fn(),
|
||||
}));
|
||||
|
||||
// The Matrix IDs that the user sees when talking to Alice & Bob
|
||||
const NATIVE_ALICE = "@alice:example.org";
|
||||
const NATIVE_BOB = "@bob:example.org";
|
||||
|
@ -449,6 +457,20 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
let audioElement: HTMLAudioElement;
|
||||
let fakeCall: MatrixCall | null;
|
||||
|
||||
const mockAudioBufferSourceNode = {
|
||||
addEventListener: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
const mockAudioContext = {
|
||||
decodeAudioData: jest.fn().mockResolvedValue({}),
|
||||
suspend: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode),
|
||||
currentTime: 1337,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
fakeCall = null;
|
||||
|
@ -464,6 +486,7 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
throw new Error("Endpoint unsupported.");
|
||||
};
|
||||
|
||||
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
|
||||
callHandler = new LegacyCallHandler();
|
||||
callHandler.start();
|
||||
|
||||
|
@ -506,6 +529,12 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
|
||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent();
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
|
||||
|
||||
fetchMock.get(
|
||||
"/media/ring.mp3",
|
||||
{ body: new Blob(["1", "2", "3", "4"], { type: "audio/mpeg" }) },
|
||||
{ sendAsJson: false },
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -520,6 +549,20 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
SdkConfig.reset();
|
||||
});
|
||||
|
||||
it("should cache sounds between playbacks", async () => {
|
||||
await callHandler.play(AudioID.Ring);
|
||||
expect(mockAudioBufferSourceNode.start).toHaveBeenCalled();
|
||||
expect(fetchMock.calls("/media/ring.mp3")).toHaveLength(1);
|
||||
await callHandler.play(AudioID.Ring);
|
||||
expect(fetchMock.calls("/media/ring.mp3")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should allow silencing an incoming call ring", async () => {
|
||||
await callHandler.play(AudioID.Ring);
|
||||
await callHandler.silenceCall("call123");
|
||||
expect(mockAudioBufferSourceNode.stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should still start a native call", async () => {
|
||||
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
|
||||
|
||||
|
@ -537,13 +580,6 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
describe("incoming calls", () => {
|
||||
const roomId = "test-room-id";
|
||||
|
||||
const mockAudioElement = {
|
||||
play: jest.fn(),
|
||||
pause: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
muted: false,
|
||||
} as unknown as HTMLMediaElement;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === UIFeature.Voip);
|
||||
|
@ -571,8 +607,6 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
},
|
||||
};
|
||||
|
||||
jest.spyOn(document, "getElementById").mockReturnValue(mockAudioElement);
|
||||
|
||||
// silence local notifications by default
|
||||
jest.spyOn(MatrixClientPeg.safeGet(), "getAccountData").mockImplementation((eventType) => {
|
||||
if (eventType.includes(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
|
||||
|
@ -586,19 +620,6 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should unmute <audio> before playing", () => {
|
||||
// Test setup: set the audio element as muted
|
||||
mockAudioElement.muted = true;
|
||||
expect(mockAudioElement.muted).toStrictEqual(true);
|
||||
|
||||
callHandler.play(AudioID.Ring);
|
||||
|
||||
// Ensure audio is no longer muted
|
||||
expect(mockAudioElement.muted).toStrictEqual(false);
|
||||
// Ensure the audio was played
|
||||
expect(mockAudioElement.play).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("listens for incoming call events when voip is enabled", () => {
|
||||
const call = new MatrixCall({
|
||||
client: MatrixClientPeg.safeGet(),
|
||||
|
@ -612,7 +633,7 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
expect(callHandler.getCallForRoom(roomId)).toEqual(call);
|
||||
});
|
||||
|
||||
it("rings when incoming call state is ringing and notifications set to ring", () => {
|
||||
it("rings when incoming call state is ringing and notifications set to ring", async () => {
|
||||
// remove local notification silencing mock for this test
|
||||
jest.spyOn(MatrixClientPeg.safeGet(), "getAccountData").mockReturnValue(undefined);
|
||||
const call = new MatrixCall({
|
||||
|
@ -627,8 +648,8 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
expect(callHandler.getCallForRoom(roomId)).toEqual(call);
|
||||
call.emit(CallEvent.State, CallState.Ringing, CallState.Connected, fakeCall!);
|
||||
|
||||
// ringer audio element started
|
||||
expect(mockAudioElement.play).toHaveBeenCalled();
|
||||
// ringer audio started
|
||||
await waitFor(() => expect(mockAudioBufferSourceNode.start).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("does not ring when incoming call state is ringing but local notifications are silenced", () => {
|
||||
|
@ -645,7 +666,7 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
call.emit(CallEvent.State, CallState.Ringing, CallState.Connected, fakeCall!);
|
||||
|
||||
// ringer audio element started
|
||||
expect(mockAudioElement.play).not.toHaveBeenCalled();
|
||||
expect(mockAudioBufferSourceNode.start).not.toHaveBeenCalled();
|
||||
expect(callHandler.isCallSilenced(call.callId)).toEqual(true);
|
||||
});
|
||||
|
||||
|
@ -679,7 +700,7 @@ describe("LegacyCallHandler without third party protocols", () => {
|
|||
// call still silenced
|
||||
expect(callHandler.isCallSilenced(call.callId)).toEqual(true);
|
||||
// ringer not played
|
||||
expect(mockAudioElement.play).not.toHaveBeenCalled();
|
||||
expect(mockAudioBufferSourceNode.start).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -60,6 +60,11 @@ jest.mock("../src/utils/notifications", () => ({
|
|||
createLocalNotificationSettingsIfNeeded: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../src/audio/compat", () => ({
|
||||
...jest.requireActual("../src/audio/compat"),
|
||||
createAudioContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("Notifier", () => {
|
||||
const roomId = "!room1:server";
|
||||
const testEvent = mkEvent({
|
||||
|
@ -103,6 +108,19 @@ describe("Notifier", () => {
|
|||
});
|
||||
};
|
||||
|
||||
const mockAudioBufferSourceNode = {
|
||||
addEventListener: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
start: jest.fn(),
|
||||
};
|
||||
const mockAudioContext = {
|
||||
decodeAudioData: jest.fn(),
|
||||
suspend: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode),
|
||||
currentTime: 1337,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
accountDataStore = {};
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
|
@ -144,6 +162,9 @@ describe("Notifier", () => {
|
|||
if (id) return new Room(id, mockClient, mockClient.getSafeUserId());
|
||||
return null;
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
Notifier.backgroundAudio.audioContext = mockAudioContext;
|
||||
});
|
||||
|
||||
describe("triggering notification from events", () => {
|
||||
|
|
35
test/setup/mocks.ts
Normal file
35
test/setup/mocks.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2024 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 const mocks = {
|
||||
AudioBufferSourceNode: {
|
||||
connect: jest.fn(),
|
||||
start: jest.fn(),
|
||||
} as unknown as AudioBufferSourceNode,
|
||||
AudioContext: {
|
||||
close: jest.fn(),
|
||||
createMediaElementSource: jest.fn(),
|
||||
createMediaStreamDestination: jest.fn(),
|
||||
createMediaStreamSource: jest.fn(),
|
||||
createStreamTrackSource: jest.fn(),
|
||||
createBufferSource: jest.fn((): AudioBufferSourceNode => ({ ...mocks.AudioBufferSourceNode })),
|
||||
getOutputTimestamp: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
setSinkId: jest.fn(),
|
||||
suspend: jest.fn(),
|
||||
decodeAudioData: jest.fn(),
|
||||
},
|
||||
};
|
|
@ -18,6 +18,8 @@ import fetchMock from "fetch-mock-jest";
|
|||
import { TextDecoder, TextEncoder } from "util";
|
||||
import { Response } from "node-fetch";
|
||||
|
||||
import { mocks } from "./mocks";
|
||||
|
||||
// Stub ResizeObserver
|
||||
// @ts-ignore - we know it's a duplicate (that's why we're stubbing it)
|
||||
class ResizeObserver {
|
||||
|
@ -76,6 +78,7 @@ global.TextDecoder = TextDecoder;
|
|||
|
||||
// prevent errors whenever a component tries to manually scroll.
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
window.HTMLAudioElement.prototype.canPlayType = jest.fn((format) => (format === "audio/mpeg" ? "probably" : ""));
|
||||
|
||||
// set up fetch API mock
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
|
@ -87,3 +90,6 @@ window.fetch = fetchMock.sandbox();
|
|||
|
||||
// @ts-ignore
|
||||
window.Response = Response;
|
||||
|
||||
// set up AudioContext API mock
|
||||
global.AudioContext = jest.fn().mockImplementation(() => ({ ...mocks.AudioContext }));
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
SyncState,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { EncryptedFile } from "matrix-js-sdk/src/types";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { uploadFile } from "../../../src/ContentMessages";
|
||||
import { createVoiceMessageContent } from "../../../src/utils/createVoiceMessageContent";
|
||||
|
@ -49,6 +50,7 @@ import {
|
|||
import { mkEvent, mkStubRoom, stubClient } from "../../test-utils";
|
||||
import dis from "../../../src/dispatcher/dispatcher";
|
||||
import { VoiceRecording } from "../../../src/audio/VoiceRecording";
|
||||
import { createAudioContext } from "../../../src/audio/compat";
|
||||
|
||||
jest.mock("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder", () => ({
|
||||
...(jest.requireActual("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder") as object),
|
||||
|
@ -79,6 +81,11 @@ jest.mock("../../../src/utils/createVoiceMessageContent", () => ({
|
|||
createVoiceMessageContent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/audio/compat", () => ({
|
||||
...jest.requireActual("../../../src/audio/compat"),
|
||||
createAudioContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("VoiceBroadcastRecording", () => {
|
||||
const roomId = "!room:example.com";
|
||||
const uploadedUrl = "mxc://example.com/vb";
|
||||
|
@ -198,6 +205,19 @@ describe("VoiceBroadcastRecording", () => {
|
|||
});
|
||||
};
|
||||
|
||||
const mockAudioBufferSourceNode = {
|
||||
addEventListener: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
start: jest.fn(),
|
||||
};
|
||||
const mockAudioContext = {
|
||||
decodeAudioData: jest.fn(),
|
||||
suspend: jest.fn(),
|
||||
resume: jest.fn(),
|
||||
createBufferSource: jest.fn().mockReturnValue(mockAudioBufferSourceNode),
|
||||
currentTime: 1337,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = mkStubRoom(roomId, "Test Room", client);
|
||||
|
@ -265,6 +285,8 @@ describe("VoiceBroadcastRecording", () => {
|
|||
|
||||
return null;
|
||||
});
|
||||
|
||||
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -546,12 +568,13 @@ describe("VoiceBroadcastRecording", () => {
|
|||
beforeEach(() => {
|
||||
mocked(client.sendMessage).mockRejectedValue("Error");
|
||||
emitFirsChunkRecorded();
|
||||
fetchMock.get("media/error.mp3", 200);
|
||||
});
|
||||
|
||||
itShouldBeInState("connection_error");
|
||||
|
||||
it("should play a notification", () => {
|
||||
expect(audioElement.play).toHaveBeenCalled();
|
||||
expect(mockAudioBufferSourceNode.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("and the connection is back", () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue