mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-07 05:47:03 +00:00
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;
|
||||
}
|
||||
interface IntentAndPlatformDerivedConfiguration {
|
||||
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,
|
||||
IntentAndPlatformDerivedConfiguration {}
|
||||
|
||||
// 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
|
||||
@@ -310,6 +317,11 @@ class ParamParser {
|
||||
}
|
||||
}
|
||||
|
||||
let urlParamCache: {
|
||||
search?: string;
|
||||
hash?: string;
|
||||
params?: UrlParams;
|
||||
} = {};
|
||||
/**
|
||||
* Gets the app parameters for the current URL.
|
||||
* @param search The URL search string
|
||||
@@ -319,7 +331,18 @@ class ParamParser {
|
||||
export const getUrlParams = (
|
||||
search = window.location.search,
|
||||
hash = window.location.hash,
|
||||
/** Skipping the cache might be needed in tests, to allow recomputing based on mocked platform changes. */
|
||||
skipCache = false,
|
||||
): 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 fontScale = parseFloat(parser.getParam("fontScale") ?? "");
|
||||
@@ -343,8 +366,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 +384,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 +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 = {
|
||||
widgetId,
|
||||
parentUrl,
|
||||
@@ -460,11 +491,29 @@ export const getUrlParams = (
|
||||
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,
|
||||
...intentPreset,
|
||||
...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 {
|
||||
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 VITE_PACKAGE='full' (SPA) 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 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 {
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user