default mute states (unmuted!) in widget mode (embedded + intent) (#3494)
* default mute states (unmuted!) in widget mode (embedded + intent) Signed-off-by: Timo K <toger5@hotmail.de> * review Signed-off-by: Timo K <toger5@hotmail.de> * introduce a cache for the url params. Signed-off-by: Timo K <toger5@hotmail.de> * Add an option to skip the cache. Signed-off-by: Timo K <toger5@hotmail.de> --------- Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
@@ -228,13 +228,20 @@ export interface UrlConfiguration {
|
|||||||
*/
|
*/
|
||||||
waitForCallPickup: boolean;
|
waitForCallPickup: boolean;
|
||||||
}
|
}
|
||||||
|
interface IntentAndPlatformDerivedConfiguration {
|
||||||
|
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,
|
||||||
|
IntentAndPlatformDerivedConfiguration {}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -310,6 +317,11 @@ class ParamParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let urlParamCache: {
|
||||||
|
search?: string;
|
||||||
|
hash?: string;
|
||||||
|
params?: UrlParams;
|
||||||
|
} = {};
|
||||||
/**
|
/**
|
||||||
* Gets the app parameters for the current URL.
|
* Gets the app parameters for the current URL.
|
||||||
* @param search The URL search string
|
* @param search The URL search string
|
||||||
@@ -319,7 +331,18 @@ class ParamParser {
|
|||||||
export const getUrlParams = (
|
export const getUrlParams = (
|
||||||
search = window.location.search,
|
search = window.location.search,
|
||||||
hash = window.location.hash,
|
hash = window.location.hash,
|
||||||
|
/** Skipping the cache might be needed in tests, to allow recomputing based on mocked platform changes. */
|
||||||
|
skipCache = false,
|
||||||
): UrlParams => {
|
): UrlParams => {
|
||||||
|
// Only run the param configuration if we do not yet have it cached for this url.
|
||||||
|
if (
|
||||||
|
urlParamCache.search === search &&
|
||||||
|
urlParamCache.hash === hash &&
|
||||||
|
urlParamCache.params &&
|
||||||
|
!skipCache
|
||||||
|
) {
|
||||||
|
return urlParamCache.params;
|
||||||
|
}
|
||||||
const parser = new ParamParser(search, hash);
|
const parser = new ParamParser(search, hash);
|
||||||
|
|
||||||
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
const fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
||||||
@@ -343,8 +366,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 +384,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 +421,24 @@ export const getUrlParams = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const intentAndPlatformDerivedConfiguration: IntentAndPlatformDerivedConfiguration =
|
||||||
|
{};
|
||||||
|
// Desktop also includes web. Its anything that is not mobile.
|
||||||
|
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:
|
||||||
|
intentAndPlatformDerivedConfiguration.defaultAudioEnabled = true;
|
||||||
|
intentAndPlatformDerivedConfiguration.defaultVideoEnabled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const properties: UrlProperties = {
|
const properties: UrlProperties = {
|
||||||
widgetId,
|
widgetId,
|
||||||
parentUrl,
|
parentUrl,
|
||||||
@@ -460,11 +491,29 @@ export const getUrlParams = (
|
|||||||
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
|
autoLeaveWhenOthersLeft: parser.getFlag("autoLeave"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
// Log the final configuration for debugging purposes.
|
||||||
|
// This will only log when the cache is not yet set.
|
||||||
|
logger.info(
|
||||||
|
"UrlParams: final set of url params\n",
|
||||||
|
"intent:",
|
||||||
|
intent,
|
||||||
|
"\nproperties:",
|
||||||
|
properties,
|
||||||
|
"configuration:",
|
||||||
|
configuration,
|
||||||
|
"intentAndPlatformDerivedConfiguration:",
|
||||||
|
intentAndPlatformDerivedConfiguration,
|
||||||
|
);
|
||||||
|
|
||||||
|
const params = {
|
||||||
...properties,
|
...properties,
|
||||||
...intentPreset,
|
...intentPreset,
|
||||||
...pickBy(configuration, (v?: unknown) => v !== undefined),
|
...pickBy(configuration, (v?: unknown) => v !== undefined),
|
||||||
|
...intentAndPlatformDerivedConfiguration,
|
||||||
};
|
};
|
||||||
|
urlParamCache = { search, hash, params };
|
||||||
|
|
||||||
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 VITE_PACKAGE='full' (SPA) 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 VITE_PACKAGE='embedded' (widget) 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user