keyboard_backspace wifi_off

Capture Handle and Captured Surface Control

Screen capturing is not (yet) supported on your device

Captured Surface Control is not (yet) supported on your device

Capture Handle is not (yet) supported on your device

Capture Handle and Captured Surface Control enable screen capturing web apps to remotely control captured web apps.

The capturing web app can send arbitrary commands to the capturing web app, scroll it and zoom it in and out. A great use-case for this is a screen capture web app that can remotely control a presentation inside the web app that is captured.

The user of the capturing web app then doesn't need to switch between the capturing and captured app anymore to control that presentation.

The captured app can opt-in through a call to navigator.mediaDevices.setCaptureHandleConfig(config). The config parameter has a handle property containing a string that uniquely identifies the captured app.

The capturing app retrieves the handle by calling getCaptureHandle on the VideoTrack of the screen capturing MediaStream. It then uses that handle in messages it sends to the captured app through BroadcastChannel. These messages instruct the captured app to go to the previous or next image in the gallery.

Demo

Click the button "Open page" below to open a page containing an image gallery. Then capture the screen of that page by clicking the button "Share screen".

After that, you can go back and forth between the images in the gallery from this page by clicking the "Previous" and "Next" buttons, scroll the captured page and zoom it in and out. No need to switch between this app and the captured app anymore!

zoom_out zoom_in
// capturing side let controller; // CaptureController keeps the focus on the capturing web app if ('CaptureController' in window && 'setFocusBehavior' in CaptureController.prototype) { controller = new CaptureController(); controller.setFocusBehavior('no-focus-change'); } const stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: 'browser', }, audio: true, surfaceSwitching: 'exclude', selfBrowserSurface: 'exclude', preferCurrentTab: false, systemAudio: 'include', monitorTypeSurfaces: "exclude", ...(controller && {controller}) }); const [videoTrack] = stream.getVideoTracks(); let captureHandle = videoTrack.getCaptureHandle(); if (captureHandle) { previousButton.disabled = false; nextButton.disabled = false; } videoTrack.addEventListener('capturehandlechange', (e) => { captureHandle = e.target.getCaptureHandle(); }); const broadcastChannel = new BroadcastChannel("capture-handle"); previousButton.addEventListener('click', () => { broadcastChannel.postMessage({ handle: captureHandle.handle, command: 'previous', }); }); nextButton.addEventListener('click', () => { broadcastChannel.postMessage({ handle: captureHandle.handle, command: 'next', }); }); // captured side const config = { handle: crypto.randomUUID(), exposeOrigin: true, permittedOrigins: ['*'], }; navigator.mediaDevices.setCaptureHandleConfig(config); const gallery = document.querySelector('image-gallery'); const broadcastChannel = new BroadcastChannel("capture-handle"); broadcastChannel.addEventListener('message', ({data}) => { const {handle, command} = data; // only accept commands if the handle matches if(handle === config.handle) { switch(command) { case 'previous': gallery.previous(); break; case 'next': gallery.next(); break; } } }); // trigger permission prompt for Captured Surface Control enableScrollingButton.onclick = (e) => { captureController.sendWheel({}); } // get available zoom levels const zoomLevels = CaptureController.getSupportedZoomLevels(); // zoom in zoomInButton.addEventListener('click', async () => { const index = zoomLevels.indexOf(captureController.getZoomLevel()); const newZoomLevel = zoomLevels[Math.min(index + 1, zoomLevels.length - 1)]; try { await captureController.setZoomLevel(newZoomLevel); } catch(err) { console.log('zoom in error', err); } }); // zoom out zoomOutButton.addEventListener('click', async () => { const index = zoomLevels.indexOf(captureController.getZoomLevel()); const newZoomLevel = zoomLevels[Math.max(index - 1, 0)]; try { await captureController.setZoomLevel(newZoomLevel); } catch(err) { console.log('zoom out error', err); } }); // scroll captured side by scrolling the video track preview.onwheel = async (e) => { const {offsetX, offsetY, deltaX, deltaY} = e; const [x, y] = translateCoordinates(offsetX, offsetY); const [wheelDeltaX, wheelDeltaY] = [-deltaX, -deltaY]; try { await captureController.sendWheel({ x, y, wheelDeltaX, wheelDeltaY }); } catch (error) { console.log(error); } }; // translate coordinates between preview and captured side function translateCoordinates(offsetX, offsetY) { const previewDimensions = preview.getBoundingClientRect(); const trackSettings = preview.srcObject.getVideoTracks()[0].getSettings(); const x = (trackSettings.width * offsetX) / previewDimensions.width; const y = (trackSettings.height * offsetY) / previewDimensions.height; return [Math.floor(x), Math.floor(y)]; }

Documentation

Capture Handle on Chrome Developers.

Captured Surface Control on Chrome Developers.

Browser support

Capture Handle is supported in Chrome and Edge 102+.

Captured Surface Control is supported in Chrome and Edge 122+ as an Origin Trial.

What PWA Can Do Today A showcase of what is possible with Progressive Web Apps today