feat(call): screenshare over the widget API with Capability Delegation (#43)
CI / Build & Quality Checks (push) Successful in 2m50s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 11s
CI / Trigger Desktop Build (push) Successful in 13s
CI / Playwright smoke (e2e) (push) Successful in 16m14s
CI / Build & Quality Checks (push) Successful in 2m50s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 11s
CI / Trigger Desktop Build (push) Successful in 13s
CI / Playwright smoke (e2e) (push) Successful in 16m14s
The call bar's Start/Stop Screenshare no longer clicks EC's hidden button
on Chromium (incl. WebView2): it sends io.lotus.set_screenshare with
postMessage `{ delegate: "display-capture" }` inside the user's click, so
the frame can call getDisplayMedia on engines that require the click.
matrix-widget-api has no postMessage options, so its sendInternal is
swapped for that one synchronous send; delegation needs the frame's real
origin, not `*`.
Firefox, Safari and WebKitGTK (Linux desktop) have no delegation and
still click EC's button (needs same-origin, which is still on). Gated on
the fork reporting `screenshareAction` in controls_state.
Pins @lotusguild/element-call-embedded 0.25.0-lotus.17.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ba5b1ffe7d
commit
024edbf546
@@ -19,6 +19,16 @@ export type LotusQualityPayload = {
|
||||
screenshareMaxFramerate?: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Capability Delegation (`postMessage(msg, { delegate })`) ships only in
|
||||
* Chromium; other engines silently ignore the option, so the frame would get
|
||||
* no activation. `navigator.userAgentData` is likewise Chromium-only, which
|
||||
* makes it the practical feature test.
|
||||
*/
|
||||
function canDelegateCapability(): boolean {
|
||||
return typeof navigator !== 'undefined' && 'userAgentData' in navigator;
|
||||
}
|
||||
|
||||
export class CallControl extends EventEmitter implements CallControlState {
|
||||
private state: CallControlState;
|
||||
|
||||
@@ -336,11 +346,20 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
// send it and keep the DOM path below.
|
||||
private forkControls = false;
|
||||
|
||||
// [Gitea #43] The fork handles io.lotus.set_screenshare (reported in
|
||||
// controls_state). Used only where Capability Delegation exists.
|
||||
private forkScreenshare = false;
|
||||
|
||||
/** [Gitea #43] The fork's `io.lotus.controls_state` report. */
|
||||
public onControlsState(data: unknown) {
|
||||
if (typeof data !== 'object' || data === null) return;
|
||||
const { screensharing, layout } = data as { screensharing?: unknown; layout?: unknown };
|
||||
const { screensharing, layout, screenshareAction } = data as {
|
||||
screensharing?: unknown;
|
||||
layout?: unknown;
|
||||
screenshareAction?: unknown;
|
||||
};
|
||||
this.forkControls = true;
|
||||
this.forkScreenshare = screenshareAction === true;
|
||||
this.applyControls(
|
||||
typeof screensharing === 'boolean' ? screensharing : this.screenshare,
|
||||
layout === 'spotlight' || layout === 'grid' ? layout === 'spotlight' : this.spotlight,
|
||||
@@ -453,10 +472,51 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
this.applyScreenshareAudioMuted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called synchronously inside the user's click: starting a share
|
||||
* calls getDisplayMedia in the frame, which needs that click. Where the
|
||||
* browser can hand the click over (Capability Delegation, Chromium incl.
|
||||
* WebView2) this goes over the widget API; elsewhere (Firefox, Safari,
|
||||
* WebKitGTK desktop) it still clicks EC's hidden button, which needs
|
||||
* same-origin access to the frame.
|
||||
*/
|
||||
public toggleScreenshare() {
|
||||
if (this.forkScreenshare && canDelegateCapability()) {
|
||||
this.sendDelegated('io.lotus.set_screenshare', { on: !this.screenshare }, 'display-capture');
|
||||
return;
|
||||
}
|
||||
this.screenshareButton?.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a widget action with `postMessage(…, { delegate })` so the frame
|
||||
* receives the user's activation for `capability`. matrix-widget-api has no
|
||||
* postMessage options, so swap its `sendInternal` for this one synchronous
|
||||
* send (the request is posted before `send()` returns). Delegation refuses a
|
||||
* `*` target origin, so the frame's real origin is used.
|
||||
*/
|
||||
private sendDelegated(action: string, data: Record<string, unknown>, capability: string): void {
|
||||
const target = this.iframe.contentWindow;
|
||||
if (!target) return;
|
||||
const targetOrigin = new URL(this.iframe.src, window.location.href).origin;
|
||||
const transport = this.call.transport as unknown as {
|
||||
sendInternal: (message: unknown) => void;
|
||||
};
|
||||
const hadOwn = Object.prototype.hasOwnProperty.call(transport, 'sendInternal');
|
||||
const original = transport.sendInternal;
|
||||
transport.sendInternal = (message: unknown) =>
|
||||
target.postMessage(message, {
|
||||
targetOrigin,
|
||||
delegate: capability,
|
||||
} as WindowPostMessageOptions);
|
||||
try {
|
||||
this.call.transport.send(action, data).catch(() => undefined);
|
||||
} finally {
|
||||
if (hadOwn) transport.sendInternal = original;
|
||||
else delete (transport as { sendInternal?: unknown }).sendInternal;
|
||||
}
|
||||
}
|
||||
|
||||
public toggleSpotlight() {
|
||||
if (this.forkControls) {
|
||||
this.sendForkAction('io.lotus.set_layout', {
|
||||
|
||||
Reference in New Issue
Block a user