default mute states (unmuted!) in widget mode (embedded + intent)

Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
Timo K
2025-09-17 19:12:14 +02:00
parent 4be395500f
commit dc5203a8fe
3 changed files with 120 additions and 29 deletions

View File

@@ -228,13 +228,20 @@ export interface UrlConfiguration {
*/ */
waitForCallPickup: boolean; waitForCallPickup: boolean;
} }
interface IntentDerivedConfiguration {
defaultAudioEnabled?: boolean;
defaultVideoEnabled?: boolean;
}
// If you need to add a new flag to this interface, prefer a name that describes // If you need to add a new flag to this interface, prefer a name that describes
// a specific behavior (such as 'confineToRoom'), rather than one that describes // a specific behavior (such as 'confineToRoom'), rather than one that describes
// the situations that call for this behavior ('isEmbedded'). This makes it // the situations that call for this behavior ('isEmbedded'). This makes it
// clearer what each flag means, and helps us avoid coupling Element Call's // clearer what each flag means, and helps us avoid coupling Element Call's
// behavior to the needs of specific consumers. // behavior to the needs of specific consumers.
export interface UrlParams extends UrlProperties, UrlConfiguration {} export interface UrlParams
extends UrlProperties,
UrlConfiguration,
IntentDerivedConfiguration {}
// This is here as a stopgap, but what would be far nicer is a function that // This is here as a stopgap, but what would be far nicer is a function that
// takes a UrlParams and returns a query string. That would enable us to // takes a UrlParams and returns a query string. That would enable us to
@@ -343,8 +350,7 @@ export const getUrlParams = (
? UserIntent.Unknown ? UserIntent.Unknown
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown); : (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
// Here we only use constants and `platform` to determine the intent preset. // Here we only use constants and `platform` to determine the intent preset.
let intentPreset: UrlConfiguration; let intentPreset: UrlConfiguration = {
const inAppDefault = {
confineToRoom: true, confineToRoom: true,
appPrompt: false, appPrompt: false,
preload: false, preload: false,
@@ -362,31 +368,22 @@ export const getUrlParams = (
}; };
switch (intent) { switch (intent) {
case UserIntent.StartNewCall: case UserIntent.StartNewCall:
intentPreset = { intentPreset.skipLobby = true;
...inAppDefault,
skipLobby: true,
};
break; break;
case UserIntent.JoinExistingCall: case UserIntent.JoinExistingCall:
intentPreset = { // On desktop this will be overridden based on which button was used to join the call
...inAppDefault, intentPreset.skipLobby = false;
skipLobby: false,
};
break; break;
case UserIntent.StartNewCallDM: case UserIntent.StartNewCallDM:
intentPreset = { intentPreset.skipLobby = true;
...inAppDefault, intentPreset.autoLeaveWhenOthersLeft = true;
skipLobby: true, intentPreset.waitForCallPickup = true;
autoLeaveWhenOthersLeft: true,
waitForCallPickup: true,
};
break; break;
case UserIntent.JoinExistingCallDM: case UserIntent.JoinExistingCallDM:
intentPreset = { // On desktop this will be overridden based on which button was used to join the call
...inAppDefault, intentPreset.skipLobby = true;
skipLobby: true, intentPreset.autoLeaveWhenOthersLeft = true;
autoLeaveWhenOthersLeft: true,
};
break; break;
// Non widget usecase defaults // Non widget usecase defaults
default: default:
@@ -408,6 +405,22 @@ export const getUrlParams = (
}; };
} }
const intentDerivedConfiguration: IntentDerivedConfiguration = {};
const desktopMobile = platform === "desktop" ? "desktop" : "mobile";
switch (desktopMobile) {
case "desktop":
case "mobile":
switch (intent) {
case UserIntent.StartNewCall:
case UserIntent.JoinExistingCall:
case UserIntent.StartNewCallDM:
case UserIntent.JoinExistingCallDM:
intentDerivedConfiguration.defaultAudioEnabled = true;
intentDerivedConfiguration.defaultVideoEnabled = true;
break;
}
}
const properties: UrlProperties = { const properties: UrlProperties = {
widgetId, widgetId,
parentUrl, parentUrl,
@@ -464,6 +477,7 @@ export const getUrlParams = (
...properties, ...properties,
...intentPreset, ...intentPreset,
...pickBy(configuration, (v?: unknown) => v !== undefined), ...pickBy(configuration, (v?: unknown) => v !== undefined),
...intentDerivedConfiguration,
}; };
}; };

View File

@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
import { import {
afterAll, afterAll,
afterEach, afterEach,
beforeEach,
describe, describe,
expect, expect,
it, it,
@@ -26,7 +27,6 @@ import { MediaDevicesContext } from "../MediaDevicesContext";
import { mockConfig } from "../utils/test"; import { mockConfig } from "../utils/test";
import { MediaDevices } from "../state/MediaDevices"; import { MediaDevices } from "../state/MediaDevices";
import { ObservableScope } from "../state/ObservableScope"; import { ObservableScope } from "../state/ObservableScope";
vi.mock("@livekit/components-core"); vi.mock("@livekit/components-core");
interface TestComponentProps { interface TestComponentProps {
@@ -110,9 +110,10 @@ function mockMediaDevices(
return new MediaDevices(scope); return new MediaDevices(scope);
} }
describe("useMuteStates", () => { describe("useMuteStates in full mode", () => {
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
vi.stubEnv("VITE_PACKAGE", "full");
}); });
afterAll(() => { afterAll(() => {
@@ -256,3 +257,67 @@ describe("useMuteStates", () => {
expect(screen.getByTestId("video-enabled").textContent).toBe("true"); expect(screen.getByTestId("video-enabled").textContent).toBe("true");
}); });
}); });
describe("useMuteStates in embedded mode", () => {
beforeEach(() => {
vi.stubEnv("VITE_PACKAGE", "embedded");
});
it("uses defaults from config", () => {
mockConfig({
media_devices: {
enable_audio: false,
enable_video: false,
},
});
render(
<MemoryRouter>
<MediaDevicesContext value={mockMediaDevices()}>
<TestComponent />
</MediaDevicesContext>
</MemoryRouter>,
);
expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
expect(screen.getByTestId("video-enabled").textContent).toBe("false");
});
it("skipLobby does not mute inputs", () => {
mockConfig();
render(
<MemoryRouter
initialEntries={[
"/room/?skipLobby=true&widgetId=1234&parentUrl=www.parent.org",
]}
>
<MediaDevicesContext value={mockMediaDevices()}>
<TestComponent />
</MediaDevicesContext>
</MemoryRouter>,
);
expect(screen.getByTestId("audio-enabled").textContent).toBe("true");
expect(screen.getByTestId("video-enabled").textContent).toBe("true");
});
it("url params win over config", () => {
// The config sets audio and video to disabled
mockConfig({ media_devices: { enable_audio: false, enable_video: false } });
render(
<MemoryRouter
initialEntries={[
// The Intent sets both audio and video enabled to true via the url param configuration
"/room/?intent=start_call_dm&widgetId=1234&parentUrl=www.parent.org",
]}
>
<MediaDevicesContext value={mockMediaDevices()}>
<TestComponent />
</MediaDevicesContext>
</MemoryRouter>,
);
// At the end we expect the url param to take precedence, resulting in true
expect(screen.getByTestId("audio-enabled").textContent).toBe("true");
expect(screen.getByTestId("video-enabled").textContent).toBe("true");
});
});

View File

@@ -81,11 +81,15 @@ function useMuteState(
export function useMuteStates(isJoined: boolean): MuteStates { export function useMuteStates(isJoined: boolean): MuteStates {
const devices = useMediaDevices(); const devices = useMediaDevices();
const { skipLobby } = useUrlParams(); const { skipLobby, defaultAudioEnabled, defaultVideoEnabled } =
useUrlParams();
const audio = useMuteState(devices.audioInput, () => { const audio = useMuteState(
return Config.get().media_devices.enable_audio && !skipLobby && !isJoined; devices.audioInput,
}); () =>
(defaultAudioEnabled ?? Config.get().media_devices.enable_audio) &&
allowJoinUnmuted(skipLobby, isJoined),
);
useEffect(() => { useEffect(() => {
// If audio is enabled, we need to request the device names again, // If audio is enabled, we need to request the device names again,
// because iOS will not be able to switch to the correct device after un-muting. // because iOS will not be able to switch to the correct device after un-muting.
@@ -97,7 +101,9 @@ export function useMuteStates(isJoined: boolean): MuteStates {
const isEarpiece = useIsEarpiece(); const isEarpiece = useIsEarpiece();
const video = useMuteState( const video = useMuteState(
devices.videoInput, devices.videoInput,
() => Config.get().media_devices.enable_video && !skipLobby && !isJoined, () =>
(defaultVideoEnabled ?? Config.get().media_devices.enable_video) &&
allowJoinUnmuted(skipLobby, isJoined),
isEarpiece, // Force video to be unavailable if using earpiece isEarpiece, // Force video to be unavailable if using earpiece
); );
@@ -164,3 +170,9 @@ export function useMuteStates(isJoined: boolean): MuteStates {
return useMemo(() => ({ audio, video }), [audio, video]); return useMemo(() => ({ audio, video }), [audio, video]);
} }
function allowJoinUnmuted(skipLobby: boolean, isJoined: boolean): boolean {
return (
(!skipLobby && !isJoined) || import.meta.env.VITE_PACKAGE === "embedded"
);
}