aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
m---------recipes0
-rw-r--r--src/components/services/content/ServiceWebview.js2
-rw-r--r--src/index.js30
-rw-r--r--src/models/Service.js21
-rw-r--r--src/webview/badge.js33
-rw-r--r--src/webview/find.js23
-rw-r--r--src/webview/lib/RecipeWebview.js48
-rw-r--r--src/webview/notifications.js88
-rw-r--r--src/webview/recipe.js132
-rw-r--r--src/webview/screenshare.js81
10 files changed, 286 insertions, 172 deletions
diff --git a/recipes b/recipes
Subproject ebb2cc3c68f74ce1d8b8a61d128078753d9a039 Subproject 3108591b234d011e9681bf9aca366555b64cb8e
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index 9e5fed996..4edbde5e2 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -83,7 +83,7 @@ class ServiceWebview extends Component {
83 useragent={service.userAgent} 83 useragent={service.userAgent}
84 disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined} 84 disablewebsecurity={service.recipe.disablewebsecurity ? true : undefined}
85 allowpopups 85 allowpopups
86 webpreferences={`spellcheck=${isSpellcheckerEnabled ? 1 : 0}, contextIsolation=false`} 86 webpreferences={`spellcheck=${isSpellcheckerEnabled ? 1 : 0}`}
87 /> 87 />
88 ); 88 );
89 } 89 }
diff --git a/src/index.js b/src/index.js
index 91004aac7..63e6e3d0f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -449,6 +449,36 @@ ipcMain.on('feature-basic-auth-cancel', () => {
449 authCallback = noop; 449 authCallback = noop;
450}); 450});
451 451
452// Handle synchronous messages from service webviews.
453
454ipcMain.on('find-in-page', (e, text, options) => {
455 const { sender: webContents } = e;
456 if (webContents !== mainWindow.webContents && typeof (text) === 'string') {
457 const sanitizedOptions = {};
458 for (const option of ['forward', 'findNext', 'matchCase']) {
459 if (option in options) {
460 sanitizedOptions[option] = !!options[option];
461 }
462 }
463 const requestId = webContents.findInPage(text, sanitizedOptions);
464 debug('Find in page', text, options, requestId);
465 e.returnValue = requestId;
466 } else {
467 e.returnValue = null;
468 }
469});
470
471ipcMain.on('stop-find-in-page', (e, action) => {
472 const { sender: webContents } = e;
473 if (webContents !== mainWindow.webContents) {
474 const validActions = ['clearSelection', 'keepSelection', 'activateSelection'];
475 if (validActions.includes(action)) {
476 webContents.stopFindInPage(action);
477 }
478 }
479 e.returnValue = null;
480});
481
452// Quit when all windows are closed. 482// Quit when all windows are closed.
453app.on('window-all-closed', () => { 483app.on('window-all-closed', () => {
454 // On OS X it is common for applications and their menu bar 484 // On OS X it is common for applications and their menu bar
diff --git a/src/models/Service.js b/src/models/Service.js
index 74e100ea4..36b310da1 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -275,11 +275,17 @@ export default class Service {
275 debug(this.name, 'modifyRequestHeaders is not defined in the recipe'); 275 debug(this.name, 'modifyRequestHeaders is not defined in the recipe');
276 } 276 }
277 277
278 this.webview.addEventListener('ipc-message', e => handleIPCMessage({ 278 this.webview.addEventListener('ipc-message', async (e) => {
279 serviceId: this.id, 279 if (e.channel === 'inject-js-unsafe') {
280 channel: e.channel, 280 await Promise.all(e.args.map(script => this.webview.executeJavaScript(`"use strict"; (() => { ${script} })();`)));
281 args: e.args, 281 } else {
282 })); 282 handleIPCMessage({
283 serviceId: this.id,
284 channel: e.channel,
285 args: e.args,
286 });
287 }
288 });
283 289
284 this.webview.addEventListener('new-window', (event, url, frameName, options) => { 290 this.webview.addEventListener('new-window', (event, url, frameName, options) => {
285 debug('new-window', event, url, frameName, options); 291 debug('new-window', event, url, frameName, options);
@@ -334,6 +340,11 @@ export default class Service {
334 this.hasCrashed = true; 340 this.hasCrashed = true;
335 }); 341 });
336 342
343 this.webview.addEventListener('found-in-page', ({ result }) => {
344 debug('Found in page', result);
345 this.webview.send('found-in-page', result);
346 });
347
337 webviewWebContents.on('login', (event, request, authInfo, callback) => { 348 webviewWebContents.on('login', (event, request, authInfo, callback) => {
338 // const authCallback = callback; 349 // const authCallback = callback;
339 debug('browser login event', authInfo); 350 debug('browser login event', authInfo);
diff --git a/src/webview/badge.js b/src/webview/badge.js
new file mode 100644
index 000000000..1e02fb56a
--- /dev/null
+++ b/src/webview/badge.js
@@ -0,0 +1,33 @@
1const { ipcRenderer } = require('electron');
2
3const debug = require('debug')('Ferdi:Plugin:BadgeHandler');
4
5export class BadgeHandler {
6 constructor() {
7 this.countCache = {
8 direct: 0,
9 indirect: 0,
10 };
11 }
12
13 setBadge(direct, indirect) {
14 if (this.countCache.direct === direct
15 && this.countCache.indirect === indirect) return;
16
17 // Parse number to integer
18 // This will correct errors that recipes may introduce, e.g.
19 // by sending a String instead of an integer
20 const directInt = parseInt(direct, 10);
21 const indirectInt = parseInt(indirect, 10);
22
23 const count = {
24 direct: Math.max(directInt, 0),
25 indirect: Math.max(indirectInt, 0),
26 };
27
28 ipcRenderer.sendToHost('message-counts', count);
29 Object.assign(this.countCache, count);
30
31 debug('Sending badge count to host', count);
32 }
33}
diff --git a/src/webview/find.js b/src/webview/find.js
new file mode 100644
index 000000000..040811d68
--- /dev/null
+++ b/src/webview/find.js
@@ -0,0 +1,23 @@
1import { ipcRenderer } from 'electron';
2import { FindInPage as ElectronFindInPage } from 'electron-find';
3
4// Shim to expose webContents functionality to electron-find without @electron/remote
5const webContentsShim = {
6 findInPage: (text, options = {}) => ipcRenderer.sendSync('find-in-page', text, options),
7 stopFindInPage: (action) => {
8 ipcRenderer.sendSync('stop-find-in-page', action);
9 },
10 on: (eventName, listener) => {
11 if (eventName === 'found-in-page') {
12 ipcRenderer.on('found-in-page', (_, result) => {
13 listener({ sender: this }, result);
14 });
15 }
16 },
17};
18
19export default class FindInPage extends ElectronFindInPage {
20 constructor(options = {}) {
21 super(webContentsShim, options);
22 }
23}
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js
index b8fe7dc52..3bb9352f6 100644
--- a/src/webview/lib/RecipeWebview.js
+++ b/src/webview/lib/RecipeWebview.js
@@ -1,14 +1,12 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { pathExistsSync, readFile } from 'fs-extra'; 2import { exists, pathExistsSync, readFile } from 'fs-extra';
3 3
4const debug = require('debug')('Ferdi:Plugin:RecipeWebview'); 4const debug = require('debug')('Ferdi:Plugin:RecipeWebview');
5 5
6class RecipeWebview { 6class RecipeWebview {
7 constructor() { 7 constructor(badgeHandler, notificationsHandler) {
8 this.countCache = { 8 this.badgeHandler = badgeHandler;
9 direct: 0, 9 this.notificationsHandler = notificationsHandler;
10 indirect: 0,
11 };
12 10
13 ipcRenderer.on('poll', () => { 11 ipcRenderer.on('poll', () => {
14 this.loopFunc(); 12 this.loopFunc();
@@ -45,24 +43,7 @@ class RecipeWebview {
45 * me directly to me eg. in a channel 43 * me directly to me eg. in a channel
46 */ 44 */
47 setBadge(direct = 0, indirect = 0) { 45 setBadge(direct = 0, indirect = 0) {
48 if (this.countCache.direct === direct 46 this.badgeHandler.setBadge(direct, indirect);
49 && this.countCache.indirect === indirect) return;
50
51 // Parse number to integer
52 // This will correct errors that recipes may introduce, e.g.
53 // by sending a String instead of an integer
54 const directInt = parseInt(direct, 10);
55 const indirectInt = parseInt(indirect, 10);
56
57 const count = {
58 direct: Math.max(directInt, 0),
59 indirect: Math.max(indirectInt, 0),
60 };
61
62 ipcRenderer.sendToHost('message-counts', count);
63 Object.assign(this.countCache, count);
64
65 debug('Sending badge count to host', count);
66 } 47 }
67 48
68 /** 49 /**
@@ -85,6 +66,23 @@ class RecipeWebview {
85 }); 66 });
86 } 67 }
87 68
69 injectJSUnsafe(...files) {
70 Promise.all(files.map(async (file) => {
71 if (await exists(file)) {
72 const data = await readFile(file, 'utf8');
73 return data;
74 }
75 debug('Script not found', file);
76 return null;
77 })).then(async (scripts) => {
78 const scriptsFound = scripts.filter(script => script !== null);
79 if (scriptsFound.length > 0) {
80 debug('Inject scripts to main world', scriptsFound);
81 ipcRenderer.sendToHost('inject-js-unsafe', ...scriptsFound);
82 }
83 });
84 }
85
88 /** 86 /**
89 * Set a custom handler for turning on and off dark mode 87 * Set a custom handler for turning on and off dark mode
90 * 88 *
@@ -96,7 +94,7 @@ class RecipeWebview {
96 94
97 onNotify(fn) { 95 onNotify(fn) {
98 if (typeof fn === 'function') { 96 if (typeof fn === 'function') {
99 window.Notification.prototype.onNotify = fn; 97 this.notificationsHandler.onNotify = fn;
100 } 98 }
101 } 99 }
102 100
diff --git a/src/webview/notifications.js b/src/webview/notifications.js
index 021f05cc3..39a515143 100644
--- a/src/webview/notifications.js
+++ b/src/webview/notifications.js
@@ -3,49 +3,65 @@ import uuidV1 from 'uuid/v1';
3 3
4const debug = require('debug')('Ferdi:Notifications'); 4const debug = require('debug')('Ferdi:Notifications');
5 5
6class Notification { 6export class NotificationsHandler {
7 static permission = 'granted'; 7 onNotify = data => data;
8 8
9 constructor(title = '', options = {}) { 9 displayNotification(title, options) {
10 debug('New notification', title, options); 10 return new Promise((resolve) => {
11 this.title = title; 11 debug('New notification', title, options);
12 this.options = options; 12
13 this.notificationId = uuidV1(); 13 const notificationId = uuidV1();
14 14
15 ipcRenderer.sendToHost('notification', this.onNotify({ 15 ipcRenderer.sendToHost('notification', this.onNotify({
16 title: this.title, 16 title,
17 options: this.options, 17 options,
18 notificationId: this.notificationId, 18 notificationId,
19 })); 19 }));
20 20
21 ipcRenderer.once(`notification-onclick:${this.notificationId}`, () => { 21 ipcRenderer.once(`notification-onclick:${notificationId}`, () => {
22 if (typeof this.onclick === 'function') { 22 resolve();
23 this.onclick(); 23 });
24 }
25 }); 24 });
26 } 25 }
26}
27 27
28 static requestPermission(cb = null) { 28export const notificationsClassDefinition = `(() => {
29 if (!cb) { 29 class Notification {
30 return new Promise((resolve) => { 30 static permission = 'granted';
31 resolve(Notification.permission);
32 });
33 }
34 31
35 if (typeof (cb) === 'function') { 32 constructor(title = '', options = {}) {
36 return cb(Notification.permission); 33 this.title = title;
34 this.options = options;
35 window.ferdi.displayNotification(title, options)
36 .then(() => {
37 if (typeof (this.onClick) === 'function') {
38 this.onClick();
39 }
40 });
37 } 41 }
38 42
39 return Notification.permission; 43 static requestPermission(cb = null) {
40 } 44 if (!cb) {
45 return new Promise((resolve) => {
46 resolve(Notification.permission);
47 });
48 }
41 49
42 onNotify(data) { 50 if (typeof (cb) === 'function') {
43 return data; 51 return cb(Notification.permission);
44 } 52 }
45 53
46 onClick() {} 54 return Notification.permission;
55 }
47 56
48 close() {} 57 onNotify(data) {
49} 58 return data;
59 }
60
61 onClick() {}
62
63 close() {}
64 }
50 65
51window.Notification = Notification; 66 window.Notification = Notification;
67})();`;
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
index 8da45864b..d143675dc 100644
--- a/src/webview/recipe.js
+++ b/src/webview/recipe.js
@@ -1,11 +1,9 @@
1/* eslint-disable import/first */ 1/* eslint-disable import/first */
2import { ipcRenderer } from 'electron'; 2import { contextBridge, ipcRenderer } from 'electron';
3import { getCurrentWebContents } from '@electron/remote';
4import path from 'path'; 3import path from 'path';
5import { autorun, computed, observable } from 'mobx'; 4import { autorun, computed, observable } from 'mobx';
6import fs from 'fs-extra'; 5import fs from 'fs-extra';
7import { debounce } from 'lodash'; 6import { debounce } from 'lodash';
8import { FindInPage } from 'electron-find';
9 7
10// For some services darkreader tries to use the chrome extension message API 8// For some services darkreader tries to use the chrome extension message API
11// This will cause the service to fail loading 9// This will cause the service to fail loading
@@ -23,16 +21,81 @@ import customDarkModeCss from './darkmode/custom';
23import RecipeWebview from './lib/RecipeWebview'; 21import RecipeWebview from './lib/RecipeWebview';
24import Userscript from './lib/Userscript'; 22import Userscript from './lib/Userscript';
25 23
26import { switchDict, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker'; 24import { BadgeHandler } from './badge';
27import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
28import contextMenu from './contextMenu'; 25import contextMenu from './contextMenu';
29import './notifications'; 26import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode';
30import { screenShareCss } from './screenshare'; 27import FindInPage from './find';
28import { NotificationsHandler, notificationsClassDefinition } from './notifications';
29import { getDisplayMediaSelector, screenShareCss, screenShareJs } from './screenshare';
30import { switchDict, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker';
31 31
32import { DEFAULT_APP_SETTINGS, isDevMode } from '../environment'; 32import { DEFAULT_APP_SETTINGS } from '../environment';
33 33
34const debug = require('debug')('Ferdi:Plugin'); 34const debug = require('debug')('Ferdi:Plugin');
35 35
36const badgeHandler = new BadgeHandler();
37
38const notificationsHandler = new NotificationsHandler();
39
40// Patching window.open
41const originalWindowOpen = window.open;
42
43window.open = (url, frameName, features) => {
44 debug('window.open', url, frameName, features);
45 if (!url) {
46 // The service hasn't yet supplied a URL (as used in Skype).
47 // Return a new dummy window object and wait for the service to change the properties
48 const newWindow = {
49 location: {
50 href: '',
51 },
52 };
53
54 const checkInterval = setInterval(() => {
55 // Has the service changed the URL yet?
56 if (newWindow.location.href !== '') {
57 if (features) {
58 originalWindowOpen(newWindow.location.href, frameName, features);
59 } else {
60 // Open the new URL
61 ipcRenderer.sendToHost('new-window', newWindow.location.href);
62 }
63 clearInterval(checkInterval);
64 }
65 }, 0);
66
67 setTimeout(() => {
68 // Stop checking for location changes after 1 second
69 clearInterval(checkInterval);
70 }, 1000);
71
72 return newWindow;
73 }
74
75 // We need to differentiate if the link should be opened in a popup or in the systems default browser
76 if (!frameName && !features && typeof features !== 'string') {
77 return ipcRenderer.sendToHost('new-window', url);
78 }
79
80 if (url) {
81 return originalWindowOpen(url, frameName, features);
82 }
83};
84
85// We can't override APIs here, so we first expose functions via window.ferdi,
86// then overwrite the corresponding field of the window object by injected JS.
87contextBridge.exposeInMainWorld('ferdi', {
88 open: window.open,
89 setBadge: (direct, indirect) => badgeHandler.setBadge(direct || 0, indirect || 0),
90 displayNotification: (title, options) => notificationsHandler.displayNotification(title, options),
91 getDisplayMediaSelector,
92});
93
94ipcRenderer.sendToHost('inject-js-unsafe',
95 'window.open = window.ferdi.open;',
96 notificationsClassDefinition,
97 screenShareJs);
98
36class RecipeController { 99class RecipeController {
37 @observable settings = { 100 @observable settings = {
38 overrideSpellcheckerLanguage: false, 101 overrideSpellcheckerLanguage: false,
@@ -97,7 +160,7 @@ class RecipeController {
97 autorun(() => this.update()); 160 autorun(() => this.update());
98 161
99 document.addEventListener('DOMContentLoaded', () => { 162 document.addEventListener('DOMContentLoaded', () => {
100 this.findInPage = new FindInPage(getCurrentWebContents(), { 163 this.findInPage = new FindInPage({
101 inputFocusColor: '#CE9FFC', 164 inputFocusColor: '#CE9FFC',
102 textColor: '#212121', 165 textColor: '#212121',
103 }); 166 });
@@ -111,7 +174,7 @@ class RecipeController {
111 // Delete module from cache 174 // Delete module from cache
112 delete require.cache[require.resolve(modulePath)]; 175 delete require.cache[require.resolve(modulePath)];
113 try { 176 try {
114 this.recipe = new RecipeWebview(); 177 this.recipe = new RecipeWebview(badgeHandler, notificationsHandler);
115 // eslint-disable-next-line 178 // eslint-disable-next-line
116 require(modulePath)(this.recipe, {...config, recipe,}); 179 require(modulePath)(this.recipe, {...config, recipe,});
117 debug('Initialize Recipe', config, recipe); 180 debug('Initialize Recipe', config, recipe);
@@ -327,52 +390,3 @@ class RecipeController {
327/* eslint-disable no-new */ 390/* eslint-disable no-new */
328new RecipeController(); 391new RecipeController();
329/* eslint-enable no-new */ 392/* eslint-enable no-new */
330
331// Patching window.open
332const originalWindowOpen = window.open;
333
334window.open = (url, frameName, features) => {
335 debug('window.open', url, frameName, features);
336 if (!url) {
337 // The service hasn't yet supplied a URL (as used in Skype).
338 // Return a new dummy window object and wait for the service to change the properties
339 const newWindow = {
340 location: {
341 href: '',
342 },
343 };
344
345 const checkInterval = setInterval(() => {
346 // Has the service changed the URL yet?
347 if (newWindow.location.href !== '') {
348 if (features) {
349 originalWindowOpen(newWindow.location.href, frameName, features);
350 } else {
351 // Open the new URL
352 ipcRenderer.sendToHost('new-window', newWindow.location.href);
353 }
354 clearInterval(checkInterval);
355 }
356 }, 0);
357
358 setTimeout(() => {
359 // Stop checking for location changes after 1 second
360 clearInterval(checkInterval);
361 }, 1000);
362
363 return newWindow;
364 }
365
366 // We need to differentiate if the link should be opened in a popup or in the systems default browser
367 if (!frameName && !features && typeof features !== 'string') {
368 return ipcRenderer.sendToHost('new-window', url);
369 }
370
371 if (url) {
372 return originalWindowOpen(url, frameName, features);
373 }
374};
375
376if (isDevMode) {
377 window.log = console.log;
378}
diff --git a/src/webview/screenshare.js b/src/webview/screenshare.js
index 84d2e1e95..ab548a625 100644
--- a/src/webview/screenshare.js
+++ b/src/webview/screenshare.js
@@ -2,6 +2,27 @@ import { desktopCapturer } from 'electron';
2 2
3const CANCEL_ID = 'desktop-capturer-selection__cancel'; 3const CANCEL_ID = 'desktop-capturer-selection__cancel';
4 4
5export async function getDisplayMediaSelector() {
6 const sources = await desktopCapturer.getSources({ types: ['screen', 'window'] });
7 return `<div class="desktop-capturer-selection__scroller">
8 <ul class="desktop-capturer-selection__list">
9 ${sources.map(({ id, name, thumbnail }) => `
10 <li class="desktop-capturer-selection__item">
11 <button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}">
12 <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" />
13 <span class="desktop-capturer-selection__name">${name}</span>
14 </button>
15 </li>
16 `).join('')}
17 <li class="desktop-capturer-selection__item">
18 <button class="desktop-capturer-selection__btn" data-id="${CANCEL_ID}" title="Cancel">
19 <span class="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">Cancel</span>
20 </button>
21 </li>
22 </ul>
23</div>`;
24}
25
5export const screenShareCss = ` 26export const screenShareCss = `
6.desktop-capturer-selection { 27.desktop-capturer-selection {
7 position: fixed; 28 position: fixed;
@@ -72,38 +93,12 @@ export const screenShareCss = `
72} 93}
73`; 94`;
74 95
75// Patch getDisplayMedia for screen sharing 96export const screenShareJs = `
76window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) => { 97window.navigator.mediaDevices.getDisplayMedia = () => new Promise(async (resolve, reject) => {
77 try { 98 try {
78 const sources = await desktopCapturer.getSources({
79 types: ['screen', 'window'],
80 });
81
82 const selectionElem = document.createElement('div'); 99 const selectionElem = document.createElement('div');
83 selectionElem.classList = 'desktop-capturer-selection'; 100 selectionElem.classList = ['desktop-capturer-selection'];
84 selectionElem.innerHTML = ` 101 selectionElem.innerHTML = await window.ferdi.getDisplayMediaSelector();
85 <div class="desktop-capturer-selection__scroller">
86 <ul class="desktop-capturer-selection__list">
87 ${sources
88 .map(
89 ({ id, name, thumbnail }) => `
90 <li class="desktop-capturer-selection__item">
91 <button class="desktop-capturer-selection__btn" data-id="${id}" title="${name}">
92 <img class="desktop-capturer-selection__thumbnail" src="${thumbnail.toDataURL()}" />
93 <span class="desktop-capturer-selection__name">${name}</span>
94 </button>
95 </li>
96 `,
97 )
98 .join('')}
99 <li class="desktop-capturer-selection__item">
100 <button class="desktop-capturer-selection__btn" data-id="${CANCEL_ID}" title="Cancel">
101 <span class="desktop-capturer-selection__name desktop-capturer-selection__name--cancel">Cancel</span>
102 </button>
103 </li>
104 </ul>
105 </div>
106 `;
107 document.body.appendChild(selectionElem); 102 document.body.appendChild(selectionElem);
108 103
109 document 104 document
@@ -112,25 +107,18 @@ window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) =>
112 button.addEventListener('click', async () => { 107 button.addEventListener('click', async () => {
113 try { 108 try {
114 const id = button.getAttribute('data-id'); 109 const id = button.getAttribute('data-id');
115 if (id === CANCEL_ID) { 110 if (id === '${CANCEL_ID}') {
116 reject(new Error('Cancelled by user')); 111 reject(new Error('Cancelled by user'));
117 } else { 112 } else {
118 const mediaSource = sources.find((source) => source.id === id); 113 const stream = await window.navigator.mediaDevices.getUserMedia({
119 if (!mediaSource) { 114 audio: false,
120 throw new Error(`Source with id ${id} does not exist`); 115 video: {
121 } 116 mandatory: {
122 117 chromeMediaSource: 'desktop',
123 const stream = await window.navigator.mediaDevices.getUserMedia( 118 chromeMediaSourceId: id,
124 {
125 audio: false,
126 video: {
127 mandatory: {
128 chromeMediaSource: 'desktop',
129 chromeMediaSourceId: mediaSource.id,
130 },
131 }, 119 },
132 }, 120 },
133 ); 121 });
134 resolve(stream); 122 resolve(stream);
135 } 123 }
136 } catch (err) { 124 } catch (err) {
@@ -143,4 +131,5 @@ window.navigator.mediaDevices.getDisplayMedia = () => async (resolve, reject) =>
143 } catch (err) { 131 } catch (err) {
144 reject(err); 132 reject(err);
145 } 133 }
146}; 134});
135`;