api-media-handler-spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import { BrowserWindow, session, desktopCapturer } from 'electron/main';
  2. import { expect } from 'chai';
  3. import * as http from 'node:http';
  4. import { ifit, listen } from './lib/spec-helpers';
  5. import { closeAllWindows } from './lib/window-helpers';
  6. describe('setDisplayMediaRequestHandler', () => {
  7. afterEach(closeAllWindows);
  8. // These tests are done on an http server because navigator.userAgentData
  9. // requires a secure context.
  10. let server: http.Server;
  11. let serverUrl: string;
  12. before(async () => {
  13. server = http.createServer((req, res) => {
  14. res.setHeader('Content-Type', 'text/html');
  15. res.end('');
  16. });
  17. serverUrl = (await listen(server)).url;
  18. });
  19. after(() => {
  20. server.close();
  21. });
  22. ifit(process.platform !== 'darwin')('works when calling getDisplayMedia', async function () {
  23. if ((await desktopCapturer.getSources({ types: ['screen'] })).length === 0) {
  24. return this.skip();
  25. }
  26. const ses = session.fromPartition('' + Math.random());
  27. let requestHandlerCalled = false;
  28. let mediaRequest: any = null;
  29. ses.setDisplayMediaRequestHandler((request, callback) => {
  30. requestHandlerCalled = true;
  31. mediaRequest = request;
  32. desktopCapturer.getSources({ types: ['screen'] }).then((sources) => {
  33. // Grant access to the first screen found.
  34. const { id, name } = sources[0];
  35. callback({
  36. video: { id, name }
  37. // TODO: 'loopback' and 'loopbackWithMute' are currently only supported on Windows.
  38. // audio: { id: 'loopback', name: 'System Audio' }
  39. });
  40. });
  41. });
  42. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  43. await w.loadURL(serverUrl);
  44. const { ok, message } = await w.webContents.executeJavaScript(`
  45. navigator.mediaDevices.getDisplayMedia({
  46. video: true,
  47. audio: false,
  48. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  49. `, true);
  50. expect(requestHandlerCalled).to.be.true();
  51. expect(mediaRequest.videoRequested).to.be.true();
  52. expect(mediaRequest.audioRequested).to.be.false();
  53. expect(ok).to.be.true(message);
  54. });
  55. it('does not crash when using a bogus ID', async () => {
  56. const ses = session.fromPartition('' + Math.random());
  57. let requestHandlerCalled = false;
  58. ses.setDisplayMediaRequestHandler((request, callback) => {
  59. requestHandlerCalled = true;
  60. callback({
  61. video: { id: 'bogus', name: 'whatever' }
  62. });
  63. });
  64. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  65. await w.loadURL(serverUrl);
  66. const { ok, message } = await w.webContents.executeJavaScript(`
  67. navigator.mediaDevices.getDisplayMedia({
  68. video: true,
  69. audio: true,
  70. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  71. `, true);
  72. expect(requestHandlerCalled).to.be.true();
  73. expect(ok).to.be.false();
  74. expect(message).to.equal('Could not start video source');
  75. });
  76. it('successfully returns a capture handle', async () => {
  77. let w: BrowserWindow | null = null;
  78. const ses = session.fromPartition('' + Math.random());
  79. let requestHandlerCalled = false;
  80. let mediaRequest: any = null;
  81. ses.setDisplayMediaRequestHandler((request, callback) => {
  82. requestHandlerCalled = true;
  83. mediaRequest = request;
  84. callback({ video: w?.webContents.mainFrame });
  85. });
  86. w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  87. await w.loadURL(serverUrl);
  88. const { ok, handleID, captureHandle, message } = await w.webContents.executeJavaScript(`
  89. const handleID = crypto.randomUUID();
  90. navigator.mediaDevices.setCaptureHandleConfig({
  91. handle: handleID,
  92. exposeOrigin: true,
  93. permittedOrigins: ["*"],
  94. });
  95. navigator.mediaDevices.getDisplayMedia({
  96. video: true,
  97. audio: false
  98. }).then(stream => {
  99. const [videoTrack] = stream.getVideoTracks();
  100. const captureHandle = videoTrack.getCaptureHandle();
  101. return { ok: true, handleID, captureHandle, message: null }
  102. }, e => ({ ok: false, message: e.message }))
  103. `, true);
  104. expect(requestHandlerCalled).to.be.true();
  105. expect(mediaRequest.videoRequested).to.be.true();
  106. expect(mediaRequest.audioRequested).to.be.false();
  107. expect(ok).to.be.true();
  108. expect(captureHandle.handle).to.be.a('string');
  109. expect(handleID).to.eq(captureHandle.handle);
  110. expect(message).to.be.null();
  111. });
  112. it('does not crash when providing only audio for a video request', async () => {
  113. const ses = session.fromPartition('' + Math.random());
  114. let requestHandlerCalled = false;
  115. let callbackError: any;
  116. ses.setDisplayMediaRequestHandler((request, callback) => {
  117. requestHandlerCalled = true;
  118. try {
  119. callback({
  120. audio: 'loopback'
  121. });
  122. } catch (e) {
  123. callbackError = e;
  124. }
  125. });
  126. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  127. await w.loadURL(serverUrl);
  128. const { ok } = await w.webContents.executeJavaScript(`
  129. navigator.mediaDevices.getDisplayMedia({
  130. video: true,
  131. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  132. `, true);
  133. expect(requestHandlerCalled).to.be.true();
  134. expect(ok).to.be.false();
  135. expect(callbackError?.message).to.equal('Video was requested, but no video stream was provided');
  136. });
  137. it('does not crash when providing only an audio stream for an audio+video request', async () => {
  138. const ses = session.fromPartition('' + Math.random());
  139. let requestHandlerCalled = false;
  140. let callbackError: any;
  141. ses.setDisplayMediaRequestHandler((request, callback) => {
  142. requestHandlerCalled = true;
  143. try {
  144. callback({
  145. audio: 'loopback'
  146. });
  147. } catch (e) {
  148. callbackError = e;
  149. }
  150. });
  151. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  152. await w.loadURL(serverUrl);
  153. const { ok } = await w.webContents.executeJavaScript(`
  154. navigator.mediaDevices.getDisplayMedia({
  155. video: true,
  156. audio: true,
  157. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  158. `, true);
  159. expect(requestHandlerCalled).to.be.true();
  160. expect(ok).to.be.false();
  161. expect(callbackError?.message).to.equal('Video was requested, but no video stream was provided');
  162. });
  163. it('does not crash when providing a non-loopback audio stream', async () => {
  164. const ses = session.fromPartition('' + Math.random());
  165. let requestHandlerCalled = false;
  166. ses.setDisplayMediaRequestHandler((request, callback) => {
  167. requestHandlerCalled = true;
  168. callback({
  169. video: w.webContents.mainFrame,
  170. audio: 'default' as any
  171. });
  172. });
  173. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  174. await w.loadURL(serverUrl);
  175. const { ok } = await w.webContents.executeJavaScript(`
  176. navigator.mediaDevices.getDisplayMedia({
  177. video: true,
  178. audio: true,
  179. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  180. `, true);
  181. expect(requestHandlerCalled).to.be.true();
  182. expect(ok).to.be.true();
  183. });
  184. it('does not crash when providing no streams', async () => {
  185. const ses = session.fromPartition('' + Math.random());
  186. let requestHandlerCalled = false;
  187. let callbackError: any;
  188. ses.setDisplayMediaRequestHandler((request, callback) => {
  189. requestHandlerCalled = true;
  190. try {
  191. callback({});
  192. } catch (e) {
  193. callbackError = e;
  194. }
  195. });
  196. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  197. await w.loadURL(serverUrl);
  198. const { ok } = await w.webContents.executeJavaScript(`
  199. navigator.mediaDevices.getDisplayMedia({
  200. video: true,
  201. audio: true,
  202. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  203. `, true);
  204. expect(requestHandlerCalled).to.be.true();
  205. expect(ok).to.be.false();
  206. expect(callbackError.message).to.equal('Video was requested, but no video stream was provided');
  207. });
  208. it('does not crash when using a bogus web-contents-media-stream:// ID', async () => {
  209. const ses = session.fromPartition('' + Math.random());
  210. let requestHandlerCalled = false;
  211. ses.setDisplayMediaRequestHandler((request, callback) => {
  212. requestHandlerCalled = true;
  213. callback({
  214. video: { id: 'web-contents-media-stream://9999:9999', name: 'whatever' }
  215. });
  216. });
  217. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  218. await w.loadURL(serverUrl);
  219. const { ok, message } = await w.webContents.executeJavaScript(`
  220. navigator.mediaDevices.getDisplayMedia({
  221. video: true,
  222. audio: true,
  223. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  224. `, true);
  225. expect(requestHandlerCalled).to.be.true();
  226. expect(ok).to.be.false();
  227. expect(message).to.equal('Could not start video source');
  228. });
  229. it('is not called when calling getUserMedia', async () => {
  230. const ses = session.fromPartition('' + Math.random());
  231. ses.setDisplayMediaRequestHandler(() => {
  232. throw new Error('bad');
  233. });
  234. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  235. await w.loadURL(serverUrl);
  236. const { ok, message } = await w.webContents.executeJavaScript(`
  237. navigator.mediaDevices.getUserMedia({
  238. video: true,
  239. audio: true,
  240. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  241. `);
  242. expect(ok).to.be.true(message);
  243. });
  244. it('works when calling getDisplayMedia with preferCurrentTab', async () => {
  245. const ses = session.fromPartition('' + Math.random());
  246. let requestHandlerCalled = false;
  247. ses.setDisplayMediaRequestHandler((request, callback) => {
  248. requestHandlerCalled = true;
  249. callback({ video: w.webContents.mainFrame });
  250. });
  251. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  252. await w.loadURL(serverUrl);
  253. const { ok, message } = await w.webContents.executeJavaScript(`
  254. navigator.mediaDevices.getDisplayMedia({
  255. preferCurrentTab: true,
  256. video: true,
  257. audio: true,
  258. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  259. `, true);
  260. expect(requestHandlerCalled).to.be.true();
  261. expect(ok).to.be.true(message);
  262. });
  263. it('returns a MediaStream with BrowserCaptureMediaStreamTrack when the current tab is selected', async () => {
  264. const ses = session.fromPartition('' + Math.random());
  265. let requestHandlerCalled = false;
  266. ses.setDisplayMediaRequestHandler((request, callback) => {
  267. requestHandlerCalled = true;
  268. callback({ video: w.webContents.mainFrame });
  269. });
  270. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  271. await w.loadURL(serverUrl);
  272. const { ok, message } = await w.webContents.executeJavaScript(`
  273. navigator.mediaDevices.getDisplayMedia({
  274. preferCurrentTab: true,
  275. video: true,
  276. audio: false,
  277. }).then(stream => {
  278. const [videoTrack] = stream.getVideoTracks();
  279. return { ok: videoTrack instanceof BrowserCaptureMediaStreamTrack, message: null };
  280. }, e => ({ok: false, message: e.message}))
  281. `, true);
  282. expect(requestHandlerCalled).to.be.true();
  283. expect(ok).to.be.true(message);
  284. });
  285. ifit(process.platform !== 'darwin')('can supply a screen response to preferCurrentTab', async () => {
  286. const ses = session.fromPartition('' + Math.random());
  287. let requestHandlerCalled = false;
  288. ses.setDisplayMediaRequestHandler(async (request, callback) => {
  289. requestHandlerCalled = true;
  290. const sources = await desktopCapturer.getSources({ types: ['screen'] });
  291. callback({ video: sources[0] });
  292. });
  293. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  294. await w.loadURL(serverUrl);
  295. const { ok, message } = await w.webContents.executeJavaScript(`
  296. navigator.mediaDevices.getDisplayMedia({
  297. preferCurrentTab: true,
  298. video: true,
  299. audio: true,
  300. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  301. `, true);
  302. expect(requestHandlerCalled).to.be.true();
  303. expect(ok).to.be.true(message);
  304. });
  305. it('can supply a frame response', async () => {
  306. const ses = session.fromPartition('' + Math.random());
  307. let requestHandlerCalled = false;
  308. ses.setDisplayMediaRequestHandler(async (request, callback) => {
  309. requestHandlerCalled = true;
  310. callback({ video: w.webContents.mainFrame });
  311. });
  312. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  313. await w.loadURL(serverUrl);
  314. const { ok, message } = await w.webContents.executeJavaScript(`
  315. navigator.mediaDevices.getDisplayMedia({
  316. video: true,
  317. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  318. `, true);
  319. expect(requestHandlerCalled).to.be.true();
  320. expect(ok).to.be.true(message);
  321. });
  322. it('is not called when calling legacy getUserMedia', async () => {
  323. const ses = session.fromPartition('' + Math.random());
  324. ses.setDisplayMediaRequestHandler(() => {
  325. throw new Error('bad');
  326. });
  327. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  328. await w.loadURL(serverUrl);
  329. const { ok, message } = await w.webContents.executeJavaScript(`
  330. new Promise((resolve, reject) => navigator.getUserMedia({
  331. video: true,
  332. audio: true,
  333. }, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
  334. `);
  335. expect(ok).to.be.true(message);
  336. });
  337. it('is not called when calling legacy getUserMedia with desktop capture constraint', async () => {
  338. const ses = session.fromPartition('' + Math.random());
  339. ses.setDisplayMediaRequestHandler(() => {
  340. throw new Error('bad');
  341. });
  342. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  343. await w.loadURL(serverUrl);
  344. const { ok, message } = await w.webContents.executeJavaScript(`
  345. new Promise((resolve, reject) => navigator.getUserMedia({
  346. video: {
  347. mandatory: {
  348. chromeMediaSource: 'desktop'
  349. }
  350. },
  351. }, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
  352. `);
  353. expect(ok).to.be.true(message);
  354. });
  355. it('works when calling getUserMedia without a media request handler', async () => {
  356. const w = new BrowserWindow({ show: false });
  357. await w.loadURL(serverUrl);
  358. const { ok, message } = await w.webContents.executeJavaScript(`
  359. navigator.mediaDevices.getUserMedia({
  360. video: true,
  361. audio: true,
  362. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  363. `);
  364. expect(ok).to.be.true(message);
  365. });
  366. it('works when calling legacy getUserMedia without a media request handler', async () => {
  367. const w = new BrowserWindow({ show: false });
  368. await w.loadURL(serverUrl);
  369. const { ok, message } = await w.webContents.executeJavaScript(`
  370. new Promise((resolve, reject) => navigator.getUserMedia({
  371. video: true,
  372. audio: true,
  373. }, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
  374. `);
  375. expect(ok).to.be.true(message);
  376. });
  377. it('throws an error when calling legacy getUserMedia with invalid chromeMediaSourceId', async () => {
  378. const w = new BrowserWindow({ show: false });
  379. await w.loadURL(serverUrl);
  380. const { ok, message } = await w.webContents.executeJavaScript(`
  381. new Promise((resolve, reject) => navigator.getUserMedia({
  382. video: {
  383. mandatory: {
  384. chromeMediaSource: 'desktop',
  385. chromeMediaSourceId: undefined,
  386. },
  387. },
  388. }, x => resolve({ok: x instanceof MediaStream}), e => resolve({ ok: false, message: e.message })))
  389. `);
  390. expect(ok).to.be.false();
  391. expect(message).to.equal('Invalid state');
  392. });
  393. it('can remove a displayMediaRequestHandler', async () => {
  394. const ses = session.fromPartition('' + Math.random());
  395. ses.setDisplayMediaRequestHandler(() => {
  396. throw new Error('bad');
  397. });
  398. ses.setDisplayMediaRequestHandler(null);
  399. const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
  400. await w.loadURL(serverUrl);
  401. const { ok, message } = await w.webContents.executeJavaScript(`
  402. navigator.mediaDevices.getDisplayMedia({
  403. video: true,
  404. }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
  405. `, true);
  406. expect(ok).to.be.false();
  407. expect(message).to.equal('Not supported');
  408. });
  409. });