From f46468296798de1582075baf0f7e72a6f2f8caae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 20 Oct 2022 20:39:24 +0200 Subject: [PATCH] Make Element Call screensharing work on desktop (#9476) --- src/models/Call.ts | 28 ++++++++-- src/stores/widgets/ElementWidgetActions.ts | 1 + test/models/Call-test.ts | 65 ++++++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/models/Call.ts b/src/models/Call.ts index fd207cf1be..ed9e227d24 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -43,6 +43,8 @@ import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widge import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore"; import PlatformPeg from "../PlatformPeg"; import { getCurrentLanguage } from "../languageHandler"; +import DesktopCapturerSourcePicker from "../components/views/elements/DesktopCapturerSourcePicker"; +import Modal from "../Modal"; const TIMEOUT_MS = 16000; @@ -639,10 +641,6 @@ export class ElementCall extends Call { baseUrl: client.baseUrl, lang: getCurrentLanguage().replace("_", "-"), }); - // Currently, the screen-sharing support is the same is it is for Jitsi - if (!PlatformPeg.get().supportsJitsiScreensharing()) { - params.append("hideScreensharing", ""); - } url.hash = `#?${params.toString()}`; // To use Element Call without touching room state, we create a virtual @@ -818,6 +816,7 @@ export class ElementCall extends Call { this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout); this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); + this.messaging!.on(`action:${ElementWidgetActions.Screenshare}`, this.onScreenshare); } protected async performDisconnection(): Promise { @@ -831,8 +830,9 @@ export class ElementCall extends Call { public setDisconnected() { this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); - this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout); - this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); + this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout); + this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout); + this.messaging!.off(`action:${ElementWidgetActions.Screenshare}`, this.onSpotlightLayout); super.setDisconnected(); } @@ -951,4 +951,20 @@ export class ElementCall extends Call { this.layout = Layout.Spotlight; await this.messaging!.transport.reply(ev.detail, {}); // ack }; + + private onScreenshare = async (ev: CustomEvent) => { + ev.preventDefault(); + + if (PlatformPeg.get().supportsDesktopCapturer()) { + const { finished } = Modal.createDialog(DesktopCapturerSourcePicker); + const [source] = await finished; + + await this.messaging!.transport.reply(ev.detail, { + failed: !source, + desktopCapturerSourceId: source, + }); + } else { + await this.messaging!.transport.reply(ev.detail, {}); + } + }; } diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts index 5e9451efa0..fa60b9ea82 100644 --- a/src/stores/widgets/ElementWidgetActions.ts +++ b/src/stores/widgets/ElementWidgetActions.ts @@ -29,6 +29,7 @@ export enum ElementWidgetActions { // Actions for switching layouts TileLayout = "io.element.tile_layout", SpotlightLayout = "io.element.spotlight_layout", + Screenshare = "io.element.screenshare", OpenIntegrationManager = "integration_manager_open", diff --git a/test/models/Call-test.ts b/test/models/Call-test.ts index df57472638..3134adf111 100644 --- a/test/models/Call-test.ts +++ b/test/models/Call-test.ts @@ -38,6 +38,8 @@ import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingSt import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../src/stores/ActiveWidgetStore"; import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions"; import SettingsStore from "../../src/settings/SettingsStore"; +import Modal, { IHandle } from "../../src/Modal"; +import PlatformPeg from "../../src/PlatformPeg"; jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ [MediaDeviceKindEnum.AudioInput]: [ @@ -807,6 +809,69 @@ describe("ElementCall", () => { call.off(CallEvent.Layout, onLayout); }); + describe("screensharing", () => { + it("passes source id if we can get it", async () => { + const sourceId = "source_id"; + jest.spyOn(Modal, "createDialog").mockReturnValue( + { finished: new Promise((r) => r([sourceId])) } as IHandle, + ); + jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(true); + + await call.connect(); + + messaging.emit( + `action:${ElementWidgetActions.Screenshare}`, + new CustomEvent("widgetapirequest", { detail: {} }), + ); + + waitFor(() => { + expect(messaging!.transport.reply).toHaveBeenCalledWith( + expect.objectContaining({}), + expect.objectContaining({ desktopCapturerSourceId: sourceId }), + ); + }); + }); + + it("passes failed if we couldn't get a source id", async () => { + jest.spyOn(Modal, "createDialog").mockReturnValue( + { finished: new Promise((r) => r([null])) } as IHandle, + ); + jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(true); + + await call.connect(); + + messaging.emit( + `action:${ElementWidgetActions.Screenshare}`, + new CustomEvent("widgetapirequest", { detail: {} }), + ); + + waitFor(() => { + expect(messaging!.transport.reply).toHaveBeenCalledWith( + expect.objectContaining({}), + expect.objectContaining({ failed: true }), + ); + }); + }); + + it("passes an empty object if we don't support desktop capturer", async () => { + jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(false); + + await call.connect(); + + messaging.emit( + `action:${ElementWidgetActions.Screenshare}`, + new CustomEvent("widgetapirequest", { detail: {} }), + ); + + waitFor(() => { + expect(messaging!.transport.reply).toHaveBeenCalledWith( + expect.objectContaining({}), + expect.objectContaining({}), + ); + }); + }); + }); + it("ends the call immediately if we're the last participant to leave", async () => { await call.connect(); const onDestroy = jest.fn();