Better screen sharing with Conditional Focus

François Beaufort
François Beaufort

Browser Support

  • 109
  • 109
  • x
  • x

Source

The Screen Capture API lets the user select a tab, window, or screen to capture as a media stream. This stream can then be recorded or shared with others over the network. This documentation introduces Conditional Focus, a mechanism for web apps to control whether the captured tab or window will be focused when capture starts, or whether the capturing page will remain focused.

Browser support

Conditional Focus is available from Chrome 109.

Background

When a web app starts capturing a tab or a window, the browser faces a decision—should the captured surface be brought to the forefront, or should the capturing page remain focused? The answer depends on the reason for calling getDisplayMedia(), and on the surface the user ends up selecting.

Consider a hypothetical video conferencing web app. By reading track.getSettings().displaySurface and potentially examining the Capture Handle, the video conferencing web app can understand what the user chose to share. Then:

  • If the captured tab or window can be remotely controlled, keep the video conference in focus.
  • Otherwise, focus the captured tab or window.

In the example above, the video conferencing web app would retain focus if sharing a slides deck, allowing the user to remotely flip through the slides; but if the user chose to share a text editor, the video conferencing web app would immediately switch focus to the captured tab or window.

Using the Conditional Focus API

Instantiate a CaptureController and pass it to getDisplayMedia(). By calling setFocusBehavior() immediately after the getDiplayMedia() returned promise resolves, you can control whether the captured tab or window will be focused or not. This can only be done if the user shared a tab or a window.

const controller = new CaptureController();

// Prompt the user to share a tab, a window or a screen.
const stream =
    await navigator.mediaDevices.getDisplayMedia({ controller });

const [track] = stream.getVideoTracks();
const displaySurface = track.getSettings().displaySurface;
if (displaySurface == "browser") {
  // Focus the captured tab.
  controller.setFocusBehavior("focus-captured-surface");
} else if (displaySurface == "window") {
  // Do not move focus to the captured window.
  // Keep the capturing page focused.
  controller.setFocusBehavior("focus-capturing-application");
}

When deciding whether to focus, it is possible to take the Capture Handle into account.

// Retain focus if capturing a tab dialed to example.com.
// Focus anything else.
const origin = track.getCaptureHandle().origin;
if (displaySurface == "browser" && origin == "https://example.com") {
  controller.setFocusBehavior("focus-capturing-application");
} else if (displaySurface != "monitor") {
  controller.setFocusBehavior("focus-captured-surface");
}

It is even possible to decide whether to focus before calling getDisplayMedia().

// Focus the captured tab or window when capture starts.
const controller = new CaptureController();
controller.setFocusBehavior("focus-captured-surface");

// Prompt the user to share their screen.
const stream =
    await navigator.mediaDevices.getDisplayMedia({ controller });

You can call setFocusBehavior() arbitrarily many times before the promise resolves, or at most once immediately after the promise resolves. The last invocation overrides all previous invocations.

More precisely: - The getDisplayMedia() returned promise resolves on a microtask. Calling setFocusBehavior() after that microtask completes throws an error. - Calling setFocusBehavior() more than a second after capture starts is no-op.

That is, both of the following snippets will fail:

// Prompt the user to share their screen.
const stream =
    await navigator.mediaDevices.getDisplayMedia({ controller });

// Too late, because it follows the completion of the task
// on which the getDisplayMedia() promise resolved.
// This will throw.
setTimeout(() => {
  controller.setFocusBehavior("focus-captured-surface");
});
// Prompt the user to share their screen.
const stream =
    await navigator.mediaDevices.getDisplayMedia({ controller });

const start = new Date();
while (new Date() - start <= 1000) {
  // Idle for ≈1s.
}

// Because too much time has elapsed, the browser will have
// already decided whether to focus.
// This fails silently.
controller.setFocusBehavior("focus-captured-surface");

Calling setFocusBehavior() also throws in the following cases:

  • the video track of the stream returned by getDisplayMedia() is not "live".
  • after the getDisplayMedia() returned promise resolves, if the user shared a screen (not a tab or a window).

Sample

You can play with Conditional Focus by running the demo on Glitch. Be sure to check out the source code.

Feature detection

To check if CaptureController.setFocusBehavior() is supported, use:

if (
  "CaptureController" in window &&
  "setFocusBehavior" in CaptureController.prototype
) {
  // CaptureController.setFocusBehavior() is supported.
}

Feedback

The Chrome team and the web standards community want to hear about your experiences with Conditional Focus.

Tell us about the design

Is there something about Conditional Focus that doesn't work as you expected? Or are there missing methods or properties that you need to implement your idea? Have a question or comment on the security model?

  • File a spec issue on the GitHub repo, or add your thoughts to an existing issue.

Problem with the implementation?

Did you find a bug with Chrome's implementation? Or is the implementation different from the spec?

  • File a bug at https://new.crbug.com. Be sure to include as much detail as you can, and simple instructions for reproducing. Glitch works well for sharing code.

Show support

Are you planning to use Conditional Focus? Your public support helps the Chrome team prioritize features and shows other browser vendors how critical it is to support them.

Send a tweet to @ChromiumDev and let us know where and how you are using it.

Acknowledgements

Hero image by Elena Taranenko.

Thanks to Rachel Andrew for reviewing this article.