add allowJoinUnmutedViaIntent to the bridge

This allows us to control to never start unmuted in spa but be able to
start unmuted in widget and component mode.
This commit is contained in:
Timo K.
2026-09-08 18:12:40 +02:00
parent 486ecbfda0
commit 273ee3b632
7 changed files with 79 additions and 23 deletions
+12 -12
View File
@@ -12,21 +12,21 @@ import { calculateInitialMuteState } from "./initialMuteState";
test.each<{
callIntent: RTCCallIntent;
isWidgetMode: boolean;
allowJoinUnmutedViaIntent: boolean;
}>([
{ callIntent: "audio", isWidgetMode: false },
{ callIntent: "audio", isWidgetMode: true },
{ callIntent: "video", isWidgetMode: false },
{ callIntent: "video", isWidgetMode: true },
{ callIntent: "unknown", isWidgetMode: false },
{ callIntent: "unknown", isWidgetMode: true },
{ callIntent: "audio", allowJoinUnmutedViaIntent: false },
{ callIntent: "audio", allowJoinUnmutedViaIntent: true },
{ callIntent: "video", allowJoinUnmutedViaIntent: false },
{ callIntent: "video", allowJoinUnmutedViaIntent: true },
{ callIntent: "unknown", allowJoinUnmutedViaIntent: false },
{ callIntent: "unknown", allowJoinUnmutedViaIntent: true },
])(
"Should allow to unmute on start if not skipping lobby (callIntent: $callIntent, packageType: $packageType)",
({ callIntent, isWidgetMode }) => {
"Should allow to unmute on start if not skipping lobby (callIntent: $callIntent, allowJoinUnmutedViaIntent: $allowJoinUnmutedViaIntent)",
({ callIntent, allowJoinUnmutedViaIntent }) => {
const { audioEnabled, videoEnabled } = calculateInitialMuteState(
false,
callIntent,
isWidgetMode,
allowJoinUnmutedViaIntent,
);
expect(audioEnabled).toBe(true);
expect(videoEnabled).toBe(callIntent !== "audio");
@@ -40,7 +40,7 @@ test.each<{
{ callIntent: "video" },
{ callIntent: "unknown" },
])(
"Should always mute on start if skipping lobby on non widget mode (callIntent: $callIntent)",
"Should always mute on start if skipping lobby and the host does not vouch for the intent (callIntent: $callIntent)",
({ callIntent }) => {
const { audioEnabled, videoEnabled } = calculateInitialMuteState(
true,
@@ -59,7 +59,7 @@ test.each<{
{ callIntent: "video" },
{ callIntent: "unknown" },
])(
"Can start unmuted if skipping lobby on widget mode (callIntent: $callIntent)",
"Can start unmuted if skipping lobby and the host vouches for the intent (callIntent: $callIntent)",
({ callIntent }) => {
const { audioEnabled, videoEnabled } = calculateInitialMuteState(
true,
+17 -10
View File
@@ -11,30 +11,37 @@ import { type RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc";
/**
* Calculates the initial mute state for media devices based on configuration.
*
* It is not always possible to start the widget with audio/video unmuted due to privacy concerns.
* This function encapsulates the logic to determine the appropriate initial state.
* It is not always possible to start the call with audio/video unmuted due to
* privacy concerns. This function encapsulates the logic to determine the
* appropriate initial state.
*
* @param allowJoinUnmutedViaIntent Whether the host vouches for the intent
* enough to start the user unmuted without a lobby (see
* `HostBridge.allowJoinUnmutedViaIntent`).
*/
export function calculateInitialMuteState(
skipLobby: boolean,
callIntent: RTCCallIntent | undefined,
isWidgetMode: boolean,
allowJoinUnmutedViaIntent: boolean,
): { audioEnabled: boolean; videoEnabled: boolean } {
logger.debug(
`calculateInitialMuteState: skipLobby=${skipLobby}, callIntent=${callIntent} isWidgetMode=${isWidgetMode}`,
`calculateInitialMuteState: skipLobby=${skipLobby}, callIntent=${callIntent} allowJoinUnmutedViaIntent=${allowJoinUnmutedViaIntent}`,
);
if (skipLobby && !isWidgetMode) {
// If not in widget mode and lobby is skipped, default to muted to protect user privacy.
// In the SPA context we don't want to unmute users without giving them a chance to adjust their settings first.
if (skipLobby && !allowJoinUnmutedViaIntent) {
// The lobby is skipped, so the user gets no chance to adjust their devices
// before joining, and nobody has vouched for the intent: default to muted
// to protect their privacy.
return {
audioEnabled: false,
videoEnabled: false,
};
}
// Embedded contexts are trusted environments, so they allow unmuted by default.
// Same for when showing a lobby, as users can adjust their settings there.
// Additionally, if the call intent is "audio", we disable video by default.
// A host that vouches for the intent is a trusted environment, so it allows
// unmuted by default. Same for when showing a lobby, as users can adjust
// their settings there. Additionally, if the call intent is "audio", we
// disable video by default.
return {
audioEnabled: true,
videoEnabled: callIntent != "audio",
+1 -1
View File
@@ -39,7 +39,7 @@ export function useMuteStates(): MuteStates | null {
calculateInitialMuteState(
urlParams.skipLobby,
urlParams.callIntent,
urlParams.isWidget,
hostBridge.allowJoinUnmutedViaIntent,
),
hostBridge,
),