123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- import { expect } from 'chai';
- import { BrowserWindow, session, desktopCapturer } from 'electron/main';
- import { closeAllWindows } from './lib/window-helpers';
- import * as http from 'node:http';
- import { ifit, listen } from './lib/spec-helpers';
- describe('setDisplayMediaRequestHandler', () => {
- afterEach(closeAllWindows);
- // These tests are done on an http server because navigator.userAgentData
- // requires a secure context.
- let server: http.Server;
- let serverUrl: string;
- before(async () => {
- server = http.createServer((req, res) => {
- res.setHeader('Content-Type', 'text/html');
- res.end('');
- });
- serverUrl = (await listen(server)).url;
- });
- after(() => {
- server.close();
- });
- // FIXME(nornagon): this test fails on our macOS CircleCI runners with the
- // error message:
- // [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)
- // This is possibly related to the OS/VM setup that CircleCI uses for macOS.
- // Our arm64 runners are in @jkleinsc's office, and are real machines, so the
- // test works there.
- ifit(!(process.platform === 'darwin' && process.arch === 'x64'))('works when calling getDisplayMedia', async function () {
- if ((await desktopCapturer.getSources({ types: ['screen'] })).length === 0) {
- return this.skip();
- }
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- let mediaRequest: any = null;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- mediaRequest = request;
- desktopCapturer.getSources({ types: ['screen'] }).then((sources) => {
- // Grant access to the first screen found.
- const { id, name } = sources[0];
- callback({
- video: { id, name }
- // TODO: 'loopback' and 'loopbackWithMute' are currently only supported on Windows.
- // audio: { id: 'loopback', name: 'System Audio' }
- });
- });
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- audio: false,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(mediaRequest.videoRequested).to.be.true();
- expect(mediaRequest.audioRequested).to.be.false();
- expect(ok).to.be.true(message);
- });
- it('does not crash when using a bogus ID', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- callback({
- video: { id: 'bogus', name: 'whatever' }
- });
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(ok).to.be.false();
- expect(message).to.equal('Could not start video source');
- });
- it('successfully returns a capture handle', async () => {
- let w: BrowserWindow | null = null;
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- let mediaRequest: any = null;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- mediaRequest = request;
- callback({ video: w?.webContents.mainFrame });
- });
- w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, handleID, captureHandle, message } = await w.webContents.executeJavaScript(`
- const handleID = crypto.randomUUID();
- navigator.mediaDevices.setCaptureHandleConfig({
- handle: handleID,
- exposeOrigin: true,
- permittedOrigins: ["*"],
- });
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- audio: false
- }).then(stream => {
- const [videoTrack] = stream.getVideoTracks();
- const captureHandle = videoTrack.getCaptureHandle();
- return { ok: true, handleID, captureHandle, message: null }
- }, e => ({ ok: false, message: e.message }))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(mediaRequest.videoRequested).to.be.true();
- expect(mediaRequest.audioRequested).to.be.false();
- expect(ok).to.be.true();
- expect(captureHandle.handle).to.be.a('string');
- expect(handleID).to.eq(captureHandle.handle);
- expect(message).to.be.null();
- });
- it('does not crash when providing only audio for a video request', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- let callbackError: any;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- try {
- callback({
- audio: 'loopback'
- });
- } catch (e) {
- callbackError = e;
- }
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(ok).to.be.false();
- expect(callbackError?.message).to.equal('Video was requested, but no video stream was provided');
- });
- it('does not crash when providing only an audio stream for an audio+video request', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- let callbackError: any;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- try {
- callback({
- audio: 'loopback'
- });
- } catch (e) {
- callbackError = e;
- }
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(ok).to.be.false();
- expect(callbackError?.message).to.equal('Video was requested, but no video stream was provided');
- });
- it('does not crash when providing a non-loopback audio stream', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- callback({
- video: w.webContents.mainFrame,
- audio: 'default' as any
- });
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(ok).to.be.true();
- });
- it('does not crash when providing no streams', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- let callbackError: any;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- try {
- callback({});
- } catch (e) {
- callbackError = e;
- }
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(ok).to.be.false();
- expect(callbackError.message).to.equal('Video was requested, but no video stream was provided');
- });
- it('does not crash when using a bogus web-contents-media-stream:// ID', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- callback({
- video: { id: 'web-contents-media-stream://9999:9999', name: 'whatever' }
- });
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- // This is a little surprising... apparently chrome will generate a stream
- // for this non-existent web contents?
- expect(ok).to.be.true();
- });
- it('is not called when calling getUserMedia', async () => {
- const ses = session.fromPartition('' + Math.random());
- ses.setDisplayMediaRequestHandler(() => {
- throw new Error('bad');
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getUserMedia({
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `);
- expect(ok).to.be.true(message);
- });
- it('works when calling getDisplayMedia with preferCurrentTab', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- ses.setDisplayMediaRequestHandler((request, callback) => {
- requestHandlerCalled = true;
- callback({ video: w.webContents.mainFrame });
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- preferCurrentTab: true,
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(ok).to.be.true(message);
- });
- ifit(!(process.platform === 'darwin' && process.arch === 'x64'))('can supply a screen response to preferCurrentTab', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- ses.setDisplayMediaRequestHandler(async (request, callback) => {
- requestHandlerCalled = true;
- const sources = await desktopCapturer.getSources({ types: ['screen'] });
- callback({ video: sources[0] });
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- preferCurrentTab: true,
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(ok).to.be.true(message);
- });
- it('can supply a frame response', async () => {
- const ses = session.fromPartition('' + Math.random());
- let requestHandlerCalled = false;
- ses.setDisplayMediaRequestHandler(async (request, callback) => {
- requestHandlerCalled = true;
- callback({ video: w.webContents.mainFrame });
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(requestHandlerCalled).to.be.true();
- expect(ok).to.be.true(message);
- });
- it('is not called when calling legacy getUserMedia', async () => {
- const ses = session.fromPartition('' + Math.random());
- ses.setDisplayMediaRequestHandler(() => {
- throw new Error('bad');
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- new Promise((resolve, reject) => navigator.getUserMedia({
- video: true,
- audio: true,
- }, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
- `);
- expect(ok).to.be.true(message);
- });
- it('is not called when calling legacy getUserMedia with desktop capture constraint', async () => {
- const ses = session.fromPartition('' + Math.random());
- ses.setDisplayMediaRequestHandler(() => {
- throw new Error('bad');
- });
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- new Promise((resolve, reject) => navigator.getUserMedia({
- video: {
- mandatory: {
- chromeMediaSource: 'desktop'
- }
- },
- }, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
- `);
- expect(ok).to.be.true(message);
- });
- it('works when calling getUserMedia without a media request handler', async () => {
- const w = new BrowserWindow({ show: false });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getUserMedia({
- video: true,
- audio: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `);
- expect(ok).to.be.true(message);
- });
- it('works when calling legacy getUserMedia without a media request handler', async () => {
- const w = new BrowserWindow({ show: false });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- new Promise((resolve, reject) => navigator.getUserMedia({
- video: true,
- audio: true,
- }, x => resolve({ok: x instanceof MediaStream}), e => reject({ok: false, message: e.message})))
- `);
- expect(ok).to.be.true(message);
- });
- it('can remove a displayMediaRequestHandler', async () => {
- const ses = session.fromPartition('' + Math.random());
- ses.setDisplayMediaRequestHandler(() => {
- throw new Error('bad');
- });
- ses.setDisplayMediaRequestHandler(null);
- const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
- await w.loadURL(serverUrl);
- const { ok, message } = await w.webContents.executeJavaScript(`
- navigator.mediaDevices.getDisplayMedia({
- video: true,
- }).then(x => ({ok: x instanceof MediaStream}), e => ({ok: false, message: e.message}))
- `, true);
- expect(ok).to.be.false();
- expect(message).to.equal('Not supported');
- });
- });
|