mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
Merge remote-tracking branch 'origin/main' into fkwp/explore/quick-audio-menu
This commit is contained in:
@@ -19,7 +19,7 @@ services:
|
|||||||
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||||
element-web:
|
element-web:
|
||||||
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
||||||
image: ghcr.io/element-hq/element-web:develop@sha256:c76d29903090eeb08ff625272277ede02ea8ee93cbaa067b77adfa89c89a494d
|
image: ghcr.io/element-hq/element-web:develop@sha256:3993d53bc2c784c0376b86e00230aef0ee2c9a705b2166e9229555117e8db167
|
||||||
element-web-1:
|
element-web-1:
|
||||||
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
||||||
image: ghcr.io/element-hq/element-web:develop@sha256:c76d29903090eeb08ff625272277ede02ea8ee93cbaa067b77adfa89c89a494d
|
image: ghcr.io/element-hq/element-web:develop@sha256:3993d53bc2c784c0376b86e00230aef0ee2c9a705b2166e9229555117e8db167
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as ComponentsCore from "@livekit/components-core";
|
|||||||
|
|
||||||
import { ObservableScope } from "./ObservableScope";
|
import { ObservableScope } from "./ObservableScope";
|
||||||
import { AudioOutput } from "./MediaDevices";
|
import { AudioOutput } from "./MediaDevices";
|
||||||
|
import { audioOutput as audioOutputSetting } from "../settings/settings";
|
||||||
import { withTestScheduler } from "../utils/test";
|
import { withTestScheduler } from "../utils/test";
|
||||||
|
|
||||||
const BT_SPEAKER = {
|
const BT_SPEAKER = {
|
||||||
@@ -93,6 +94,8 @@ describe("AudioOutput Tests", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testScope = new ObservableScope();
|
testScope = new ObservableScope();
|
||||||
|
// Device preferences persist in localStorage across tests
|
||||||
|
audioOutputSetting.setValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -150,7 +153,9 @@ describe("AudioOutput Tests", () => {
|
|||||||
|
|
||||||
expectObservable(audioOutput.selected$).toBe("abcde", {
|
expectObservable(audioOutput.selected$).toBe("abcde", {
|
||||||
a: undefined,
|
a: undefined,
|
||||||
b: { id: LAPTOP_SPEAKER.deviceId, virtualEarpiece: false },
|
// No "default" pseudo-device (Firefox): the virtual browser default is
|
||||||
|
// selected until the user picks something.
|
||||||
|
b: { id: "", virtualEarpiece: false },
|
||||||
c: { id: MONITOR_SPEAKER.deviceId, virtualEarpiece: false },
|
c: { id: MONITOR_SPEAKER.deviceId, virtualEarpiece: false },
|
||||||
d: { id: LAPTOP_SPEAKER.deviceId, virtualEarpiece: false },
|
d: { id: LAPTOP_SPEAKER.deviceId, virtualEarpiece: false },
|
||||||
e: { id: MONITOR_SPEAKER.deviceId, virtualEarpiece: false },
|
e: { id: MONITOR_SPEAKER.deviceId, virtualEarpiece: false },
|
||||||
@@ -158,6 +163,61 @@ describe("AudioOutput Tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to the browser default when the selected device disappears", () => {
|
||||||
|
withTestScheduler(({ behavior, cold, schedule, expectObservable }) => {
|
||||||
|
vi.mocked(ComponentsCore.createMediaDeviceObserver).mockReturnValue(
|
||||||
|
cold("a---b", {
|
||||||
|
a: DEVICE_LIST_B,
|
||||||
|
// The monitor is unplugged (or a Bluetooth sink changes profile)
|
||||||
|
b: [LAPTOP_SPEAKER],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const audioOutput = new AudioOutput(
|
||||||
|
behavior("a", { a: true }),
|
||||||
|
testScope,
|
||||||
|
);
|
||||||
|
|
||||||
|
schedule("--a", {
|
||||||
|
a: () => audioOutput.select(MONITOR_SPEAKER.deviceId),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectObservable(audioOutput.selected$).toBe("a-b-c", {
|
||||||
|
a: { id: "", virtualEarpiece: false },
|
||||||
|
b: { id: MONITOR_SPEAKER.deviceId, virtualEarpiece: false },
|
||||||
|
// Rather than pinning some other physical device, let the browser route
|
||||||
|
c: { id: "", virtualEarpiece: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists the virtual browser default first when there is no default pseudo-device", () => {
|
||||||
|
withTestScheduler(({ behavior, cold, expectObservable }) => {
|
||||||
|
vi.mocked(ComponentsCore.createMediaDeviceObserver).mockReturnValue(
|
||||||
|
cold("a", { a: DEVICE_LIST_B }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const audioOutput = new AudioOutput(
|
||||||
|
behavior("a", { a: true }),
|
||||||
|
testScope,
|
||||||
|
);
|
||||||
|
|
||||||
|
expectObservable(audioOutput.available$).toBe("a", {
|
||||||
|
a: new Map([
|
||||||
|
["", { type: "default", name: null }],
|
||||||
|
[
|
||||||
|
LAPTOP_SPEAKER.deviceId,
|
||||||
|
{ type: "name", name: LAPTOP_SPEAKER.label },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
MONITOR_SPEAKER.deviceId,
|
||||||
|
{ type: "name", name: MONITOR_SPEAKER.label },
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("Test mappings", () => {
|
it("Test mappings", () => {
|
||||||
// In a real life setup there would be first a blanked list
|
// In a real life setup there would be first a blanked list
|
||||||
// then the real one.
|
// then the real one.
|
||||||
|
|||||||
@@ -563,8 +563,14 @@ export function createCallViewModel$(
|
|||||||
const ownMembershipIdentity: CallMembershipIdentityParts = {
|
const ownMembershipIdentity: CallMembershipIdentityParts = {
|
||||||
userId,
|
userId,
|
||||||
deviceId,
|
deviceId,
|
||||||
// This will only be consumed by the sticky membership manager. So it has no impact on legacy calls.
|
// Consumed by the sticky membership manager as `member.id`, *and* stamped
|
||||||
memberId: uuidv4(),
|
// into every to-device key event by the key transport. A pre-sticky
|
||||||
|
// membership advertises `${userId}:${deviceId}` as its `membershipID`
|
||||||
|
// instead, so a uuid there names a member no peer can resolve.
|
||||||
|
memberId:
|
||||||
|
matrixRTCMode === MatrixRTCMode.Matrix_2_0
|
||||||
|
? uuidv4()
|
||||||
|
: `${userId}:${deviceId}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const localTransport =
|
const localTransport =
|
||||||
|
|||||||
@@ -257,14 +257,21 @@ export class AudioOutput implements MediaDevice<
|
|||||||
map((availableRaw) => {
|
map((availableRaw) => {
|
||||||
let available: Map<string, AudioOutputDeviceLabel> =
|
let available: Map<string, AudioOutputDeviceLabel> =
|
||||||
buildDeviceMap(availableRaw);
|
buildDeviceMap(availableRaw);
|
||||||
// Create a virtual default audio output for browsers that don't have one.
|
// Create a virtual default audio output for browsers that don't have one
|
||||||
// Its device ID must be the empty string because that's what setSinkId
|
// (Firefox, Safari). Its device ID must be the empty string because
|
||||||
// recognizes.
|
// that's what setSinkId recognizes. It goes first so that it is the
|
||||||
|
// fallback when no output has been explicitly chosen (or the chosen
|
||||||
|
// one disappears), rather than pinning the first physical device
|
||||||
|
// with setSinkId: pinned sinks are not re-routed by the browser, and
|
||||||
|
// Firefox leaves the audio elements silent when a pinned sink goes
|
||||||
|
// away (e.g. a Bluetooth headset switching profile when its
|
||||||
|
// microphone is opened). We can't know which physical device the
|
||||||
|
// browser default resolves to, so the entry carries no name.
|
||||||
if (available.size && !available.has("") && !available.has("default"))
|
if (available.size && !available.has("") && !available.has("default"))
|
||||||
available.set("", {
|
available = new Map<string, AudioOutputDeviceLabel>([
|
||||||
type: "default",
|
["", { type: "default", name: null }],
|
||||||
name: availableRaw[0]?.label || null,
|
...available,
|
||||||
});
|
]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const isSafari = !!(window as any).GestureEvent; // non standard api only found on Safari. https://developer.mozilla.org/en-US/docs/Web/API/GestureEvent#browser_compatibility
|
const isSafari = !!(window as any).GestureEvent; // non standard api only found on Safari. https://developer.mozilla.org/en-US/docs/Web/API/GestureEvent#browser_compatibility
|
||||||
if (isSafari) {
|
if (isSafari) {
|
||||||
|
|||||||
Reference in New Issue
Block a user