aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar André Oliveira <37463445+SpecialAro@users.noreply.github.com>2024-05-02 17:38:48 +0100
committerLibravatar GitHub <noreply@github.com>2024-05-02 17:38:48 +0100
commit92d845adc3c07bcea290eb6fefe0a998cd8f7a98 (patch)
tree1133efe2b1bd9f678cf912bb83f87e8e005bf1f1
parentUpgrade electron to '30.0.2' (diff)
downloadferdium-app-92d845adc3c07bcea290eb6fefe0a998cd8f7a98.tar.gz
ferdium-app-92d845adc3c07bcea290eb6fefe0a998cd8f7a98.tar.zst
ferdium-app-92d845adc3c07bcea290eb6fefe0a998cd8f7a98.zip
fix: screenshare feature not working on Teams (#1733)
-rw-r--r--src/components/MediaSource.tsx82
-rw-r--r--src/components/services/content/ServiceView.tsx2
-rw-r--r--src/config.ts3
-rw-r--r--src/index.ts5
-rw-r--r--src/stores/ServicesStore.ts10
-rw-r--r--src/styles/capture-sources.scss75
-rw-r--r--src/styles/main.scss1
-rw-r--r--src/webview/recipe.ts7
-rw-r--r--src/webview/screenshare.ts154
9 files changed, 207 insertions, 132 deletions
diff --git a/src/components/MediaSource.tsx b/src/components/MediaSource.tsx
new file mode 100644
index 000000000..ceb7701b9
--- /dev/null
+++ b/src/components/MediaSource.tsx
@@ -0,0 +1,82 @@
1import { ipcRenderer } from 'electron';
2import { useEffect, useState } from 'react';
3import { SCREENSHARE_CANCELLED_BY_USER } from '../config';
4import type Service from '../models/Service';
5
6export interface IProps {
7 service: Service;
8}
9
10export default function MediaSource(props: IProps) {
11 const { service } = props;
12 const [sources, setSources] = useState<any>([]);
13 const [show, setShow] = useState<boolean>(false);
14 const [trackerId, setTrackerId] = useState<string | null>(null);
15
16 ipcRenderer.on(`select-capture-device:${service.id}`, (_event, data) => {
17 setShow(true);
18 setTrackerId(data.trackerId);
19 });
20
21 useEffect(() => {
22 ipcRenderer
23 .invoke('get-desktop-capturer-sources')
24 .then(sources => setSources(sources));
25 }, []);
26
27 if (sources.length === 0 || !show) {
28 return null;
29 }
30
31 const handleOnClick = (e: any) => {
32 const { id } = e.currentTarget.dataset;
33 window['ferdium'].actions.service.sendIPCMessage({
34 serviceId: service.id,
35 channel: `selected-media-source:${trackerId}`,
36 args: {
37 mediaSourceId: id,
38 },
39 });
40
41 setShow(false);
42 setTrackerId(null);
43 };
44
45 return (
46 <div className="desktop-capturer-selection">
47 <ul className="desktop-capturer-selection__list">
48 {sources.map(({ id, name, thumbnail }) => (
49 <li className="desktop-capturer-selection__item" key={id}>
50 <button
51 type="button" // Add explicit type attribute
52 className="desktop-capturer-selection__btn"
53 data-id={id}
54 title={name}
55 onClick={handleOnClick}
56 >
57 <img
58 alt="Desktop capture preview"
59 className="desktop-capturer-selection__thumbnail"
60 src={thumbnail.toDataURL()}
61 />
62 <span className="desktop-capturer-selection__name">{name}</span>
63 </button>
64 </li>
65 ))}
66 <li className="desktop-capturer-selection__item">
67 <button
68 type="button" // Add explicit type attribute
69 className="desktop-capturer-selection__btn"
70 data-id={SCREENSHARE_CANCELLED_BY_USER}
71 title="Cancel"
72 onClick={handleOnClick}
73 >
74 <span className="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">
75 Cancel
76 </span>
77 </button>
78 </li>
79 </ul>
80 </div>
81 );
82}
diff --git a/src/components/services/content/ServiceView.tsx b/src/components/services/content/ServiceView.tsx
index 577473b5d..b7f539a5d 100644
--- a/src/components/services/content/ServiceView.tsx
+++ b/src/components/services/content/ServiceView.tsx
@@ -7,6 +7,7 @@ import { CUSTOM_WEBSITE_RECIPE_ID } from '../../../config';
7import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen'; 7import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen';
8import type ServiceModel from '../../../models/Service'; 8import type ServiceModel from '../../../models/Service';
9import type { RealStores } from '../../../stores'; 9import type { RealStores } from '../../../stores';
10import MediaSource from '../../MediaSource';
10import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl'; 11import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl';
11import WebviewLoader from '../../ui/WebviewLoader'; 12import WebviewLoader from '../../ui/WebviewLoader';
12import ServiceDisabled from './ServiceDisabled'; 13import ServiceDisabled from './ServiceDisabled';
@@ -164,6 +165,7 @@ class ServiceView extends Component<IProps, IState> {
164 ) : ( 165 ) : (
165 <> 166 <>
166 {showNavBar && <WebControlsScreen service={service} />} 167 {showNavBar && <WebControlsScreen service={service} />}
168 <MediaSource service={service} />
167 <ServiceWebview 169 <ServiceWebview
168 service={service} 170 service={service}
169 setWebviewReference={setWebviewRef} 171 setWebviewReference={setWebviewRef}
diff --git a/src/config.ts b/src/config.ts
index f086c54ee..612aa6871 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -49,6 +49,9 @@ export const WEBRTC_IP_HANDLING_POLICY = {
49 [disableWebRTCIPHandlingPolicy]: 'Do not expose public or local IPs', 49 [disableWebRTCIPHandlingPolicy]: 'Do not expose public or local IPs',
50}; 50};
51 51
52export const SCREENSHARE_CANCELLED_BY_USER =
53 'desktop-capturer-selection__cancel';
54
52// TODO: Need to convert many of these to i18n 55// TODO: Need to convert many of these to i18n
53export const HIBERNATION_STRATEGIES = { 56export const HIBERNATION_STRATEGIES = {
54 10: 'Extremely Fast Hibernation (10sec)', 57 10: 'Extremely Fast Hibernation (10sec)',
diff --git a/src/index.ts b/src/index.ts
index 999d84348..cccf0ef66 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -610,6 +610,11 @@ ipcMain.on('feature-basic-auth-cancel', () => {
610 authCallback = noop; 610 authCallback = noop;
611}); 611});
612 612
613ipcMain.on('load-available-displays', (_e, data) => {
614 debug('MAIN PROCESS: Received load-desktop-capturer-sources');
615 mainWindow?.webContents.send(`select-capture-device:${data.serviceId}`, data);
616});
617
613// Handle synchronous messages from service webviews. 618// Handle synchronous messages from service webviews.
614 619
615ipcMain.on('find-in-page', (e, text, options) => { 620ipcMain.on('find-in-page', (e, text, options) => {
diff --git a/src/stores/ServicesStore.ts b/src/stores/ServicesStore.ts
index 6c7a55d6b..fd7414004 100644
--- a/src/stores/ServicesStore.ts
+++ b/src/stores/ServicesStore.ts
@@ -838,6 +838,16 @@ export default class ServicesStore extends TypedStore {
838 838
839 break; 839 break;
840 } 840 }
841
842 case 'load-available-displays': {
843 debug('Received request for capture devices from', serviceId);
844 ipcRenderer.send('load-available-displays', {
845 serviceId,
846 ...args[0],
847 });
848 break;
849 }
850
841 case 'notification': { 851 case 'notification': {
842 const { notificationId, options } = args[0]; 852 const { notificationId, options } = args[0];
843 853
diff --git a/src/styles/capture-sources.scss b/src/styles/capture-sources.scss
new file mode 100644
index 000000000..c9ee69f09
--- /dev/null
+++ b/src/styles/capture-sources.scss
@@ -0,0 +1,75 @@
1.desktop-capturer-selection {
2 position: absolute;
3 top: 0;
4 left: 0;
5 width: 100%;
6 height: 100vh;
7 background: rgba(30, 30, 30, 0.75);
8 color: #fff;
9 z-index: 10000000;
10 display: flex;
11 align-items: center;
12 justify-content: center;
13}
14
15.desktop-capturer-selection__scroller {
16 width: 100%;
17 max-height: 100vh;
18 overflow-y: auto;
19}
20.desktop-capturer-selection__list {
21 max-width: calc(100% - 100px);
22 margin: 50px;
23 padding: 0;
24 display: flex;
25 flex-wrap: wrap;
26 list-style: none;
27 overflow: hidden;
28 justify-content: center;
29}
30.desktop-capturer-selection__item {
31 display: flex;
32 margin: 4px;
33}
34.desktop-capturer-selection__btn {
35 display: flex;
36 flex-direction: column;
37 align-items: stretch;
38 width: 145px;
39 margin: 0;
40 border: 0;
41 border-radius: 3px;
42 padding: 4px;
43 background: #252626;
44 text-align: left;
45 @media (prefers-reduced-motion: no-preference) {
46 transition:
47 background-color 0.15s,
48 box-shadow 0.15s,
49 color 0.15s;
50 }
51 color: #dedede;
52}
53.desktop-capturer-selection__btn:hover,
54.desktop-capturer-selection__btn:focus {
55 background: rgba(98, 100, 167, 0.8);
56 box-shadow:
57 0 0 4px rgba(0, 0, 0, 0.45),
58 0 0 2px rgba(0, 0, 0, 0.25);
59 color: #fff;
60}
61.desktop-capturer-selection__thumbnail {
62 width: 100%;
63 height: 81px;
64 object-fit: cover;
65}
66.desktop-capturer-selection__name {
67 margin: 6px 0;
68 white-space: nowrap;
69 text-overflow: ellipsis;
70 text-align: center;
71 overflow: hidden;
72}
73.desktop-capturer-selection__name--cancel {
74 margin: auto 0;
75}
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 8369c9298..fafb1b861 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -23,6 +23,7 @@
23@import './invite.scss'; 23@import './invite.scss';
24@import './title-bar.scss'; 24@import './title-bar.scss';
25@import './features.scss'; 25@import './features.scss';
26@import './capture-sources.scss';
26 27
27// form 28// form
28@import './input.scss'; 29@import './input.scss';
diff --git a/src/webview/recipe.ts b/src/webview/recipe.ts
index d6db39779..a35a99699 100644
--- a/src/webview/recipe.ts
+++ b/src/webview/recipe.ts
@@ -32,11 +32,7 @@ import {
32 NotificationsHandler, 32 NotificationsHandler,
33 notificationsClassDefinition, 33 notificationsClassDefinition,
34} from './notifications'; 34} from './notifications';
35import { 35import { getDisplayMediaSelector, screenShareJs } from './screenshare';
36 getDisplayMediaSelector,
37 screenShareCss,
38 screenShareJs,
39} from './screenshare';
40import SessionHandler from './sessionHandler'; 36import SessionHandler from './sessionHandler';
41import { 37import {
42 getSpellcheckerLocaleByFuzzyIdentifier, 38 getSpellcheckerLocaleByFuzzyIdentifier,
@@ -267,7 +263,6 @@ class RecipeController {
267 263
268 async loadUserFiles(recipe, config) { 264 async loadUserFiles(recipe, config) {
269 const styles = document.createElement('style'); 265 const styles = document.createElement('style');
270 styles.innerHTML = screenShareCss;
271 266
272 const userCss = join(recipe.path, 'user.css'); 267 const userCss = join(recipe.path, 'user.css');
273 if (pathExistsSync(userCss)) { 268 if (pathExistsSync(userCss)) {
diff --git a/src/webview/screenshare.ts b/src/webview/screenshare.ts
index e631ce52f..521f95bd6 100644
--- a/src/webview/screenshare.ts
+++ b/src/webview/screenshare.ts
@@ -1,139 +1,41 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { v4 as uuidV4 } from 'uuid';
2 3
3const CANCEL_ID = 'desktop-capturer-selection__cancel'; 4const debug = require('../preload-safe-debug')('Ferdium:Screenshare');
4 5
5export async function getDisplayMediaSelector() { 6export async function getDisplayMediaSelector() {
6 const sources = await ipcRenderer.invoke('get-desktop-capturer-sources'); 7 return new Promise((resolve, reject) => {
7 return `<div class="desktop-capturer-selection__scroller"> 8 const trackerId = uuidV4();
8 <ul class="desktop-capturer-selection__list"> 9 debug('New screenshare request', trackerId);
9 ${sources
10 .map(
11 ({ id, name, thumbnail }) => `
12 <li class="desktop-capturer-selection__item">
13 <button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}">
14 <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" />
15 <span class="desktop-capturer-selection__name">${name}</span>
16 </button>
17 </li>
18 `,
19 )
20 .join('')}
21 <li class="desktop-capturer-selection__item">
22 <button class="desktop-capturer-selection__btn" data-id="${CANCEL_ID}" title="Cancel">
23 <span class="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">Cancel</span>
24 </button>
25 </li>
26 </ul>
27</div>`;
28}
29 10
30export const screenShareCss = ` 11 ipcRenderer.sendToHost('load-available-displays', {
31.desktop-capturer-selection { 12 trackerId,
32 position: fixed; 13 });
33 top: 0; 14
34 left: 0; 15 ipcRenderer.once(`selected-media-source:${trackerId}`, (_e, data) => {
35 width: 100%; 16 if (data.mediaSourceId === 'desktop-capturer-selection__cancel') {
36 height: 100vh; 17 return reject(new Error('Cancelled by user'));
37 background: rgba(30,30,30,.75); 18 }
38 color: #fff; 19
39 z-index: 10000000; 20 return resolve(data.mediaSourceId);
40 display: flex; 21 });
41 align-items: center; 22 });
42 justify-content: center;
43}
44.desktop-capturer-selection__scroller {
45 width: 100%;
46 max-height: 100vh;
47 overflow-y: auto;
48}
49.desktop-capturer-selection__list {
50 max-width: calc(100% - 100px);
51 margin: 50px;
52 padding: 0;
53 display: flex;
54 flex-wrap: wrap;
55 list-style: none;
56 overflow: hidden;
57 justify-content: center;
58}
59.desktop-capturer-selection__item {
60 display: flex;
61 margin: 4px;
62}
63.desktop-capturer-selection__btn {
64 display: flex;
65 flex-direction: column;
66 align-items: stretch;
67 width: 145px;
68 margin: 0;
69 border: 0;
70 border-radius: 3px;
71 padding: 4px;
72 background: #252626;
73 text-align: left;
74 @media (prefers-reduced-motion: no-preference) {
75 transition: background-color .15s, box-shadow .15s, color .15s;
76 }
77 color: #dedede;
78}
79.desktop-capturer-selection__btn:hover,
80.desktop-capturer-selection__btn:focus {
81 background: rgba(98,100,167,.8);
82 box-shadow: 0 0 4px rgba(0,0,0,0.45), 0 0 2px rgba(0,0,0,0.25);
83 color: #fff;
84}
85.desktop-capturer-selection__thumbnail {
86 width: 100%;
87 height: 81px;
88 object-fit: cover;
89}
90.desktop-capturer-selection__name {
91 margin: 6px 0;
92 white-space: nowrap;
93 text-overflow: ellipsis;
94 text-align: center;
95 overflow: hidden;
96}
97.desktop-capturer-selection__name--cancel {
98 margin: auto 0;
99} 23}
100`;
101 24
102export const screenShareJs = ` 25export const screenShareJs = `
103window.navigator.mediaDevices.getDisplayMedia = () => new Promise(async (resolve, reject) => { 26window.navigator.mediaDevices.getDisplayMedia = () => new Promise(async (resolve, reject) => {
104 try { 27 try {
105 const selectionElem = document.createElement('div'); 28 const displayId = await window.ferdium.getDisplayMediaSelector();
106 selectionElem.classList = ['desktop-capturer-selection']; 29 const stream = await window.navigator.mediaDevices.getUserMedia({
107 selectionElem.innerHTML = await window.ferdium.getDisplayMediaSelector(); 30 audio: false,
108 document.body.appendChild(selectionElem); 31 video: {
109 32 mandatory: {
110 document 33 chromeMediaSource: 'desktop',
111 .querySelectorAll('.desktop-capturer-selection__btn') 34 chromeMediaSourceId: displayId,
112 .forEach((button) => { 35 },
113 button.addEventListener('click', async () => { 36 },
114 try { 37 });
115 const id = button.getAttribute('data-id'); 38 resolve(stream);
116 if (id === '${CANCEL_ID}') {
117 reject(new Error('Cancelled by user'));
118 } else {
119 const stream = await window.navigator.mediaDevices.getUserMedia({
120 audio: false,
121 video: {
122 mandatory: {
123 chromeMediaSource: 'desktop',
124 chromeMediaSourceId: id,
125 },
126 },
127 });
128 resolve(stream);
129 }
130 } catch (err) {
131 reject(err);
132 } finally {
133 selectionElem.remove();
134 }
135 });
136 });
137 } catch (err) { 39 } catch (err) {
138 reject(err); 40 reject(err);
139 } 41 }