quick experimental kiosk mode which hides local tiles.

src/UrlParams.ts — added hideLocalTiles: boolean to UrlConfiguration, parsed it via parser.getFlag(hideLocalTiles), and defaulted it to false in both the SPA fallback and the widget intent preset.

  src/state/CallViewModel/CallViewModel.ts — in the userMedia$ generator:
  - Added a module-level PUBLISH_SUFFIX = +publish constant.
  - Added base-device-ID and +publish-twin helpers.
  - Always: when a +publish membership exists for (userId, baseDeviceId), the non-suffixed sibling is dropped (it would just be an empty receive-only tile).
  - When hideLocalTiles is set: every membership whose (userId, baseDeviceId) matches the local user/device is dropped — covering both the local camera tile and any +publish twin from this device.
This commit is contained in:
Matthew Hodgson
2026-05-10 19:04:59 +00:00
parent ec20a636d3
commit 455d4e4963
2 changed files with 50 additions and 4 deletions

View File

@@ -179,6 +179,14 @@ export interface UrlConfiguration {
* Whether to hide the screen-sharing button.
*/
hideScreensharing: boolean;
/**
* Whether to hide tiles representing the local device. This includes the
* local camera tile and any other MatrixRTC memberships originating from
* the same device (e.g. a `+publish` twin used to publish media). Useful
* for kiosk-style embeddings where the embedder does not want the local
* user to see themselves.
*/
hideLocalTiles: boolean;
/**
* Whether the app is allowed to use fallback STUN servers for ICE in case the
@@ -369,6 +377,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
header: platform === "desktop" ? HeaderStyle.None : HeaderStyle.AppBar,
showControls: true,
hideScreensharing: false,
hideLocalTiles: false,
allowIceFallback: true,
perParticipantE2EE: true,
controlledAudioDevices: platform === "desktop" ? false : true,
@@ -424,6 +433,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
header: HeaderStyle.Standard,
showControls: true,
hideScreensharing: false,
hideLocalTiles: false,
allowIceFallback: false,
perParticipantE2EE: false,
controlledAudioDevices: false,
@@ -470,6 +480,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
header: parser.getEnumParam("header", HeaderStyle),
showControls: parser.getFlag("showControls"),
hideScreensharing: parser.getFlag("hideScreensharing"),
hideLocalTiles: parser.getFlag("hideLocalTiles"),
allowIceFallback: parser.getFlag("allowIceFallback"),
perParticipantE2EE: parser.getFlag("perParticipantE2EE"),
controlledAudioDevices: parser.getFlag("controlledAudioDevices"),

View File

@@ -197,6 +197,11 @@ const smallMobileCallThreshold = 3;
// with the interface
const showFooterMs = 4000;
// Suffix used by MatrixRTC members who participate twice from the same
// physical device — once with the bare device ID (typically receive-only)
// and once with this suffix appended (the publishing twin).
const PUBLISH_SUFFIX = "+publish";
export type GridMode = "grid" | "spotlight";
export type WindowMode = "normal" | "narrow" | "flat" | "pip";
@@ -704,6 +709,18 @@ export function createCallViewModel$(
]) {
const computeMediaId = (m: MatrixLivekitMember): string =>
`${m.userId}:${m.membership$.value.deviceId}`;
// A member may participate twice from the same physical device by
// suffixing their device ID with "+publish" — one membership for
// receiving and one for publishing media. We treat both as the same
// logical device for de-duplication and local-tile filtering.
const baseDeviceId = (deviceId: string): string =>
deviceId.endsWith(PUBLISH_SUFFIX)
? deviceId.slice(0, -PUBLISH_SUFFIX.length)
: deviceId;
const isPublishTwin = (m: MatrixLivekitMember): boolean =>
m.membership$.value.deviceId.endsWith(PUBLISH_SUFFIX);
const computeBaseId = (m: MatrixLivekitMember): string =>
`${m.userId}:${baseDeviceId(m.membership$.value.deviceId)}`;
const localUserMediaId = localMatrixLivekitMember
? computeMediaId(localMatrixLivekitMember)
@@ -715,10 +732,28 @@ export function createCallViewModel$(
const remoteWithoutLocal = matrixLivekitMembers.value.filter(
(m) => computeMediaId(m) !== localUserMediaId,
);
const allMatrixLivekitMembers = [
...localAsArray,
...remoteWithoutLocal,
];
const candidates = [...localAsArray, ...remoteWithoutLocal];
// For any (user, device) that has a +publish membership, hide its
// non-suffixed sibling — the publishing twin is the one carrying
// the media, so the receive-only twin's tile would just be empty.
const publishTwinBaseIds = new Set(
candidates.filter(isPublishTwin).map(computeBaseId),
);
const { hideLocalTiles } = getUrlParams();
const localBaseId = `${userId}:${deviceId}`;
const allMatrixLivekitMembers = candidates.filter((m) => {
// Hide all tiles originating from the local device, including
// the +publish twin.
if (hideLocalTiles && computeBaseId(m) === localBaseId)
return false;
// Drop the receive-only sibling when a +publish twin is present.
if (!isPublishTwin(m) && publishTwinBaseIds.has(computeBaseId(m)))
return false;
return true;
});
for (const matrixLivekitMember of allMatrixLivekitMembers) {
const { userId, participant, connection$, membership$ } =