api-media-handler-spec.ts 17 KB

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