diff --git a/src/lotus/lotusActions.test.ts b/src/lotus/lotusActions.test.ts index e1b8a9a9..1431777f 100644 --- a/src/lotus/lotusActions.test.ts +++ b/src/lotus/lotusActions.test.ts @@ -30,6 +30,7 @@ describe("LotusWidgetActions", () => { LotusWidgetActions.SetLayout, LotusWidgetActions.OpenSettings, LotusWidgetActions.ToggleReactions, + LotusWidgetActions.SetScreenshare, ]; expect(new Set(LOTUS_TO_WIDGET_ACTIONS)).toEqual(new Set(expectedToWidget)); diff --git a/src/lotus/lotusActions.ts b/src/lotus/lotusActions.ts index 67e31bdc..2074448b 100644 --- a/src/lotus/lotusActions.ts +++ b/src/lotus/lotusActions.ts @@ -75,7 +75,15 @@ export enum LotusWidgetActions { /** toWidget: toggle the reactions / raise-hand menu (cinny #43). */ ToggleReactions = "io.lotus.toggle_reactions", /** - * fromWidget: `{ screensharing: boolean, layout: "grid" | "spotlight" | null }` + * toWidget: start/stop sharing `{ on?: boolean }` (omit to toggle; cinny #43). + * Starting calls getDisplayMedia, which needs the user's click: the host + * sends this with Capability Delegation (`delegate: "display-capture"`), and + * without it the browser rejects the share. + */ + SetScreenshare = "io.lotus.set_screenshare", + /** + * fromWidget: `{ screensharing: boolean, layout: "grid" | "spotlight" | null, + * screenshareAction: true }` * whenever either changes (cinny #43), so the host stops reading EC's DOM for * them. Its arrival also tells the host this fork supports the actions above. */ @@ -98,4 +106,5 @@ export const LOTUS_TO_WIDGET_ACTIONS: LotusWidgetActions[] = [ LotusWidgetActions.SetLayout, LotusWidgetActions.OpenSettings, LotusWidgetActions.ToggleReactions, + LotusWidgetActions.SetScreenshare, ]; diff --git a/src/lotus/lotusControls.test.ts b/src/lotus/lotusControls.test.ts index 8b542aa5..7f3eeeff 100644 --- a/src/lotus/lotusControls.test.ts +++ b/src/lotus/lotusControls.test.ts @@ -7,7 +7,11 @@ Please see LICENSE in the repository root for full details. import { describe, expect, it } from "vitest"; -import { parseLayoutPayload, parseSettingsPayload } from "./lotusControls"; +import { + parseLayoutPayload, + parseSettingsPayload, + shouldToggleScreenshare, +} from "./lotusControls"; describe("parseLayoutPayload", () => { it("accepts grid and spotlight", () => { @@ -33,3 +37,17 @@ describe("parseSettingsPayload", () => { expect(parseSettingsPayload({ open: "yes" }, true)).toBe(false); }); }); + +describe("shouldToggleScreenshare", () => { + it("toggles only when an explicit on differs from the current state", () => { + expect(shouldToggleScreenshare({ on: true }, false)).toBe(true); + expect(shouldToggleScreenshare({ on: true }, true)).toBe(false); + expect(shouldToggleScreenshare({ on: false }, true)).toBe(true); + expect(shouldToggleScreenshare({ on: false }, false)).toBe(false); + }); + it("toggles when on is missing or not a boolean", () => { + expect(shouldToggleScreenshare({}, false)).toBe(true); + expect(shouldToggleScreenshare({ on: "yes" }, true)).toBe(true); + expect(shouldToggleScreenshare(null, true)).toBe(true); + }); +}); diff --git a/src/lotus/lotusControls.ts b/src/lotus/lotusControls.ts index 3f426fa2..17d544eb 100644 --- a/src/lotus/lotusControls.ts +++ b/src/lotus/lotusControls.ts @@ -21,6 +21,8 @@ export interface LotusControlsState { screensharing: boolean; /** Null while EC offers no layout switch (e.g. PiP or a 1:1 layout). */ layout: LayoutMode | null; + /** This fork handles `io.lotus.set_screenshare` (always true). */ + screenshareAction: true; } /** `{ layout }` payload → a layout mode, or undefined if invalid. Exported for tests. */ @@ -39,13 +41,28 @@ export function parseSettingsPayload(data: unknown, current: boolean): boolean { return !current; } +/** + * `{ on? }` payload + current sharing state → whether to call the toggle. + * Exported for tests. + */ +export function shouldToggleScreenshare( + data: unknown, + sharing: boolean, +): boolean { + if (typeof data === "object" && data !== null && "on" in data) { + const { on } = data as { on?: unknown }; + if (typeof on === "boolean") return on !== sharing; + } + return true; +} + /** * [cinny #43] Widget-API replacements for the host's DOM access to EC's * controls: layout switch, settings modal and reactions menu, plus a * `controls_state` report (screensharing + layout) so the host no longer reads - * EC's DOM for them. Screensharing itself stays host-DOM driven for now: - * `getDisplayMedia` needs the user's click to reach this frame (Capability - * Delegation), which a plain widget message doesn't carry. + * EC's DOM for them. Screenshare start/stop too: `getDisplayMedia` needs the + * user's click in this frame, so the host sends `set_screenshare` with + * Capability Delegation (Chromium). Elsewhere it keeps clicking EC's button. * * No effect unless the host sends the actions; registering is safe whenever * we're a widget. Returns a teardown function. @@ -69,10 +86,19 @@ export function startLotusControls(vm: CallViewModel): () => void { w.api.transport.reply(ev.detail, {}); window.dispatchEvent(new Event(LOTUS_TOGGLE_REACTIONS_EVENT)); }; + const onSetScreenshare = (ev: CustomEvent): void => { + w.api.transport.reply(ev.detail, {}); + // Call synchronously: the delegated activation is only good for a few + // seconds after the message arrived. + if (shouldToggleScreenshare(ev.detail.data, vm.sharingScreen$.value)) { + vm.toggleScreenSharing?.(); + } + }; w.lazyActions.on(LotusWidgetActions.SetLayout, onSetLayout); w.lazyActions.on(LotusWidgetActions.OpenSettings, onOpenSettings); w.lazyActions.on(LotusWidgetActions.ToggleReactions, onToggleReactions); + w.lazyActions.on(LotusWidgetActions.SetScreenshare, onSetScreenshare); const sub: Subscription = combineLatest([ vm.sharingScreen$, @@ -85,6 +111,7 @@ export function startLotusControls(vm: CallViewModel): () => void { ([screensharing, layout]): LotusControlsState => ({ screensharing, layout, + screenshareAction: true, }), ), distinctUntilChanged( @@ -100,5 +127,6 @@ export function startLotusControls(vm: CallViewModel): () => void { w.lazyActions.off(LotusWidgetActions.SetLayout, onSetLayout); w.lazyActions.off(LotusWidgetActions.OpenSettings, onOpenSettings); w.lazyActions.off(LotusWidgetActions.ToggleReactions, onToggleReactions); + w.lazyActions.off(LotusWidgetActions.SetScreenshare, onSetScreenshare); }; }