From 455d4e4963c979a70f5aeef4bc6b28482f88e77c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 10 May 2026 19:04:59 +0000 Subject: [PATCH] quick experimental kiosk mode which hides local tiles. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/UrlParams.ts | 11 ++++++ src/state/CallViewModel/CallViewModel.ts | 43 +++++++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index f4ea840de..c2eb42a8c 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -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"), diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index e298bcfdb..4c8602d7c 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -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$ } =