diff --git a/src/UrlParams.ts b/src/UrlParams.ts
index f5369765..c99d5c9d 100644
--- a/src/UrlParams.ts
+++ b/src/UrlParams.ts
@@ -228,13 +228,20 @@ export interface UrlConfiguration {
*/
waitForCallPickup: boolean;
}
+interface IntentDerivedConfiguration {
+ defaultAudioEnabled?: boolean;
+ defaultVideoEnabled?: boolean;
+}
// 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
// the situations that call for this behavior ('isEmbedded'). This makes it
// clearer what each flag means, and helps us avoid coupling Element Call's
// 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
// takes a UrlParams and returns a query string. That would enable us to
@@ -343,8 +350,7 @@ export const getUrlParams = (
? UserIntent.Unknown
: (parser.getEnumParam("intent", UserIntent) ?? UserIntent.Unknown);
// Here we only use constants and `platform` to determine the intent preset.
- let intentPreset: UrlConfiguration;
- const inAppDefault = {
+ let intentPreset: UrlConfiguration = {
confineToRoom: true,
appPrompt: false,
preload: false,
@@ -362,31 +368,22 @@ export const getUrlParams = (
};
switch (intent) {
case UserIntent.StartNewCall:
- intentPreset = {
- ...inAppDefault,
- skipLobby: true,
- };
+ intentPreset.skipLobby = true;
break;
case UserIntent.JoinExistingCall:
- intentPreset = {
- ...inAppDefault,
- skipLobby: false,
- };
+ // On desktop this will be overridden based on which button was used to join the call
+ intentPreset.skipLobby = false;
break;
case UserIntent.StartNewCallDM:
- intentPreset = {
- ...inAppDefault,
- skipLobby: true,
- autoLeaveWhenOthersLeft: true,
- waitForCallPickup: true,
- };
+ intentPreset.skipLobby = true;
+ intentPreset.autoLeaveWhenOthersLeft = true;
+ intentPreset.waitForCallPickup = true;
+
break;
case UserIntent.JoinExistingCallDM:
- intentPreset = {
- ...inAppDefault,
- skipLobby: true,
- autoLeaveWhenOthersLeft: true,
- };
+ // On desktop this will be overridden based on which button was used to join the call
+ intentPreset.skipLobby = true;
+ intentPreset.autoLeaveWhenOthersLeft = true;
break;
// Non widget usecase defaults
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 = {
widgetId,
parentUrl,
@@ -464,6 +477,7 @@ export const getUrlParams = (
...properties,
...intentPreset,
...pickBy(configuration, (v?: unknown) => v !== undefined),
+ ...intentDerivedConfiguration,
};
};
diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx
index d349a5c6..7a134b5e 100644
--- a/src/room/MuteStates.test.tsx
+++ b/src/room/MuteStates.test.tsx
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
import {
afterAll,
afterEach,
+ beforeEach,
describe,
expect,
it,
@@ -26,7 +27,6 @@ import { MediaDevicesContext } from "../MediaDevicesContext";
import { mockConfig } from "../utils/test";
import { MediaDevices } from "../state/MediaDevices";
import { ObservableScope } from "../state/ObservableScope";
-
vi.mock("@livekit/components-core");
interface TestComponentProps {
@@ -110,9 +110,10 @@ function mockMediaDevices(
return new MediaDevices(scope);
}
-describe("useMuteStates", () => {
+describe("useMuteStates in full mode", () => {
afterEach(() => {
vi.clearAllMocks();
+ vi.stubEnv("VITE_PACKAGE", "full");
});
afterAll(() => {
@@ -256,3 +257,67 @@ describe("useMuteStates", () => {
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(
+
+
+
+
+ ,
+ );
+ expect(screen.getByTestId("audio-enabled").textContent).toBe("false");
+ expect(screen.getByTestId("video-enabled").textContent).toBe("false");
+ });
+
+ it("skipLobby does not mute inputs", () => {
+ mockConfig();
+
+ render(
+
+
+
+
+ ,
+ );
+ 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(
+
+
+
+
+ ,
+ );
+ // 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");
+ });
+});
diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts
index 1f4b8e82..e89d13d9 100644
--- a/src/room/MuteStates.ts
+++ b/src/room/MuteStates.ts
@@ -81,11 +81,15 @@ function useMuteState(
export function useMuteStates(isJoined: boolean): MuteStates {
const devices = useMediaDevices();
- const { skipLobby } = useUrlParams();
+ const { skipLobby, defaultAudioEnabled, defaultVideoEnabled } =
+ useUrlParams();
- const audio = useMuteState(devices.audioInput, () => {
- return Config.get().media_devices.enable_audio && !skipLobby && !isJoined;
- });
+ const audio = useMuteState(
+ devices.audioInput,
+ () =>
+ (defaultAudioEnabled ?? Config.get().media_devices.enable_audio) &&
+ allowJoinUnmuted(skipLobby, isJoined),
+ );
useEffect(() => {
// 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.
@@ -97,7 +101,9 @@ export function useMuteStates(isJoined: boolean): MuteStates {
const isEarpiece = useIsEarpiece();
const video = useMuteState(
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
);
@@ -164,3 +170,9 @@ export function useMuteStates(isJoined: boolean): MuteStates {
return useMemo(() => ({ audio, video }), [audio, video]);
}
+
+function allowJoinUnmuted(skipLobby: boolean, isJoined: boolean): boolean {
+ return (
+ (!skipLobby && !isJoined) || import.meta.env.VITE_PACKAGE === "embedded"
+ );
+}