diff --git a/src/app/hooks/useCallSpeakers.ts b/src/app/hooks/useCallSpeakers.ts index 836621bf6..3c5a3d255 100644 --- a/src/app/hooks/useCallSpeakers.ts +++ b/src/app/hooks/useCallSpeakers.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { CallEmbed } from '../plugins/call'; +import { getCallDocument } from '../plugins/call/utils'; import { isUserId } from '../utils/matrix'; import { nextSpeakerSet } from '../utils/speakerSet'; import { useCallMembers, useCallSession } from './useCall'; @@ -38,8 +39,7 @@ export const useCallSpeakers = (callEmbed: CallEmbed): Set => { return undefined; } - const getDoc = (): Document | undefined => - callEmbed.iframe.contentDocument ?? callEmbed.iframe.contentWindow?.document ?? undefined; + const getDoc = (): Document | undefined => getCallDocument(callEmbed.iframe); let tileObserver: MutationObserver | undefined; @@ -163,8 +163,7 @@ export const useRemoteAllMuted = (callEmbed: CallEmbed | undefined): boolean => useEffect(() => { if (!callEmbed) return undefined; - const getDoc = (): Document | undefined => - callEmbed.iframe.contentDocument ?? callEmbed.iframe.contentWindow?.document ?? undefined; + const getDoc = (): Document | undefined => getCallDocument(callEmbed.iframe); const localUserId = callEmbed.room.client?.getUserId() ?? ''; diff --git a/src/app/plugins/call/CallControl.ts b/src/app/plugins/call/CallControl.ts index 9108c240d..db6554d4a 100644 --- a/src/app/plugins/call/CallControl.ts +++ b/src/app/plugins/call/CallControl.ts @@ -2,6 +2,7 @@ import { ClientWidgetApi } from 'matrix-widget-api'; import { EventEmitter } from 'events'; import { CallControlState } from './CallControlState'; import { ElementMediaStateDetail, ElementMediaStatePayload, ElementWidgetActions } from './types'; +import { getCallDocument } from './utils'; export enum CallControlEvent { StateUpdate = 'state_update', @@ -62,7 +63,7 @@ export class CallControl extends EventEmitter implements CallControlState { private joined = false; private get document(): Document | undefined { - return this.iframe.contentDocument ?? this.iframe.contentWindow?.document; + return getCallDocument(this.iframe); } private get screenshareButton(): HTMLElement | undefined { @@ -265,7 +266,7 @@ export class CallControl extends EventEmitter implements CallControlState { } private setSound(sound: boolean): void { - const callDocument = this.iframe.contentDocument ?? this.iframe.contentWindow?.document; + const callDocument = getCallDocument(this.iframe); if (callDocument) { callDocument.querySelectorAll('audio').forEach((el) => { const isScreenshareAudio = el.getAttribute('data-lk-source') === 'screen_share_audio'; @@ -277,7 +278,7 @@ export class CallControl extends EventEmitter implements CallControlState { private applyScreenshareAudioMuted(): void { if (!this.sound) return; - const callDocument = this.iframe.contentDocument ?? this.iframe.contentWindow?.document; + const callDocument = getCallDocument(this.iframe); if (callDocument) { callDocument .querySelectorAll('audio[data-lk-source="screen_share_audio"]') diff --git a/src/app/plugins/call/CallEmbed.ts b/src/app/plugins/call/CallEmbed.ts index aec4d8efc..f6dd11e9a 100644 --- a/src/app/plugins/call/CallEmbed.ts +++ b/src/app/plugins/call/CallEmbed.ts @@ -28,6 +28,7 @@ import { import { CallControl } from './CallControl'; import { CallControlState } from './CallControlState'; import { verifyDenoiseAssets } from './denoiseSmokeCheck'; +import { getCallDocument } from './utils'; // Maximum time to wait for the embedded Element Call iframe to progress from // initial load to a ready/joined state. If it hasn't by then, we assume the @@ -326,7 +327,7 @@ export class CallEmbed { } get document(): Document | undefined { - return this.iframe.contentDocument ?? this.iframe.contentWindow?.document; + return getCallDocument(this.iframe); } public setTheme(theme: ElementCallThemeKind) { diff --git a/src/app/plugins/call/utils.ts b/src/app/plugins/call/utils.ts index 7841a5fa2..8ab35377a 100644 --- a/src/app/plugins/call/utils.ts +++ b/src/app/plugins/call/utils.ts @@ -116,3 +116,19 @@ export function getCallCapabilities( return capabilities; } + +/** + * The EC iframe's document, or undefined when it cannot be read. The widget is + * same-origin, but when its navigation fails (offline, blocked) the frame + * becomes a cross-origin error page and `contentWindow.document` THROWS a + * SecurityError — which surfaced as page errors (and a React "Should not + * already be working" cascade) from every DOM-driven call hook the moment the + * load watchdog fired. Treat "can't read" the same as "not loaded yet". + */ +export const getCallDocument = (iframe: HTMLIFrameElement): Document | undefined => { + try { + return iframe.contentDocument ?? iframe.contentWindow?.document ?? undefined; + } catch { + return undefined; + } +};