api-media-handler-spec.ts 14 KB

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