api-media-handler-spec.ts 17 KB

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