diff options
author | André Oliveira <37463445+SpecialAro@users.noreply.github.com> | 2024-05-02 17:38:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-02 17:38:48 +0100 |
commit | 92d845adc3c07bcea290eb6fefe0a998cd8f7a98 (patch) | |
tree | 1133efe2b1bd9f678cf912bb83f87e8e005bf1f1 /src | |
parent | Upgrade electron to '30.0.2' (diff) | |
download | ferdium-app-92d845adc3c07bcea290eb6fefe0a998cd8f7a98.tar.gz ferdium-app-92d845adc3c07bcea290eb6fefe0a998cd8f7a98.tar.zst ferdium-app-92d845adc3c07bcea290eb6fefe0a998cd8f7a98.zip |
fix: screenshare feature not working on Teams (#1733)
Diffstat (limited to 'src')
-rw-r--r-- | src/components/MediaSource.tsx | 82 | ||||
-rw-r--r-- | src/components/services/content/ServiceView.tsx | 2 | ||||
-rw-r--r-- | src/config.ts | 3 | ||||
-rw-r--r-- | src/index.ts | 5 | ||||
-rw-r--r-- | src/stores/ServicesStore.ts | 10 | ||||
-rw-r--r-- | src/styles/capture-sources.scss | 75 | ||||
-rw-r--r-- | src/styles/main.scss | 1 | ||||
-rw-r--r-- | src/webview/recipe.ts | 7 | ||||
-rw-r--r-- | src/webview/screenshare.ts | 154 |
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 @@ | |||
1 | import { ipcRenderer } from 'electron'; | ||
2 | import { useEffect, useState } from 'react'; | ||
3 | import { SCREENSHARE_CANCELLED_BY_USER } from '../config'; | ||
4 | import type Service from '../models/Service'; | ||
5 | |||
6 | export interface IProps { | ||
7 | service: Service; | ||
8 | } | ||
9 | |||
10 | export 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'; | |||
7 | import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen'; | 7 | import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen'; |
8 | import type ServiceModel from '../../../models/Service'; | 8 | import type ServiceModel from '../../../models/Service'; |
9 | import type { RealStores } from '../../../stores'; | 9 | import type { RealStores } from '../../../stores'; |
10 | import MediaSource from '../../MediaSource'; | ||
10 | import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl'; | 11 | import StatusBarTargetUrl from '../../ui/StatusBarTargetUrl'; |
11 | import WebviewLoader from '../../ui/WebviewLoader'; | 12 | import WebviewLoader from '../../ui/WebviewLoader'; |
12 | import ServiceDisabled from './ServiceDisabled'; | 13 | import 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 | ||
52 | export 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 |
53 | export const HIBERNATION_STRATEGIES = { | 56 | export 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 | ||
613 | ipcMain.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 | ||
615 | ipcMain.on('find-in-page', (e, text, options) => { | 620 | ipcMain.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'; |
35 | import { | 35 | import { getDisplayMediaSelector, screenShareJs } from './screenshare'; |
36 | getDisplayMediaSelector, | ||
37 | screenShareCss, | ||
38 | screenShareJs, | ||
39 | } from './screenshare'; | ||
40 | import SessionHandler from './sessionHandler'; | 36 | import SessionHandler from './sessionHandler'; |
41 | import { | 37 | import { |
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 @@ | |||
1 | import { ipcRenderer } from 'electron'; | 1 | import { ipcRenderer } from 'electron'; |
2 | import { v4 as uuidV4 } from 'uuid'; | ||
2 | 3 | ||
3 | const CANCEL_ID = 'desktop-capturer-selection__cancel'; | 4 | const debug = require('../preload-safe-debug')('Ferdium:Screenshare'); |
4 | 5 | ||
5 | export async function getDisplayMediaSelector() { | 6 | export 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 | ||
30 | export 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 | ||
102 | export const screenShareJs = ` | 25 | export const screenShareJs = ` |
103 | window.navigator.mediaDevices.getDisplayMedia = () => new Promise(async (resolve, reject) => { | 26 | window.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 | } |