Abstract

This document proposes a mechanism by which an application APP can opt-in to exposing certain information with another application CAPTR, if CAPTR is screen-capturing the tab in which APP is running.

Problem Description

Generic Problem Description

Consider a web-application, running in one tab, which we’ll name "main_app." Assume main_app calls getDisplayMedia and the user chooses to share another tab, where an application is running which we’ll call "captured_app."

Note that:

  1. main_app does not know what it is capturing.
  2. captured_app does not know that it is being captured; let alone by whom.

Both these traits are desirable for the general case, but there exist legitimate use cases where the browser would want to allow applications to opt-in to bridging that gap and enable a connection.

We wish to enable the legitimate use cases while keeping the general case as it was before.

Use-case #1: Cross-App Communications

Consider two applications that wish to cooperate, for example a VC app and a presentation app. Assume the user is in a VC session. The user starts sharing a presentation. Both applications are interested in letting the VC app discover that it is capturing a slides session, which application, and even which session, so that the VC application will be able to expose controls to the user for flipping through slides. When the user clicks those controls, the VC app will be able to send messages to the presentation app (either through a service worker or through a shared back-end infrastructure). These messages will instruct the presentation app to flip through slides, enter/leave presentation-mode, etc.

Use-case #2: Analytics

Capturing applications often wish to gather statistics over what applications their users tend to capture. For example, VC applications would like to know how often their users share presentation applications from specific providers, Wikipedia, CNN, etc. Gathering such information can be used to improve service for the users by introducing new collaborations, such as the one described above.

Use-case #3: Detecting Unintended Captures

Users sometimes choose to share the wrong tab. Sometimes they switch to sharing the wrong tab by clicking the share-this-tab-instead button by mistake. A benevolent application could try to protect the user by presenting an in-app dialog for re-confirmation, if they believe that the user may have made a mistake.

Use-case #4: Avoiding "Hall of Mirrors"

This use-case is a sub-case of #3, but deserves its own section due to its importance. The "Hall of Mirrors" effect occurs when users choose to share the tab in which the VC call takes place. When detecting self-capture, a VC application can avoid displaying the captured stream back to the user, thereby avoiding the dreaded effect.

The Capture-Handle Identification Mechanism

The capture-handle identification mechanism consists of two main parts - one on the captured side, one on the capturing side.

Captured Side for Identification

Applications are allowed to expose information to capturing applications. They would typically do so before knowing if they even are captured. The mechanism used is calling {{MediaDevices/setCaptureHandleConfig}} with an appropriate {{CaptureHandleConfig}}.

CaptureHandleConfig

The CaptureHandleConfig dictionary is used to instruct the user agent what information the captured application intends to expose, and to which applications it is willing to expose said information.

          dictionary CaptureHandleConfig {
            boolean exposeOrigin = false;
            DOMString handle = "";
            sequence<DOMString> permittedOrigins = [];
          };          
        
exposeOrigin

If true, the user agent MUST expose the captured application's origin through the {{CaptureHandle/origin}} field of {{CaptureHandle}}. If false, the user agent MUST NOT expose the captured application's origin.

handle

The user agent MUST expose this value as {{CaptureHandle/handle}}.

Note: Values to this field are limited to 1024 16-bit characters. This limitation is specified further in {{MediaDevices/setCaptureHandleConfig}}.

permittedOrigins

Legal values of this field include:

  • The empty list.
  • A list with the single item "*"
  • A list consisting of valid origins.

If {{CaptureHandleConfig/permittedOrigins}} consists of the single item "*", then the {{CaptureHandle}} is observable by all capturers. Otherwise, {{CaptureHandle}} is [=observable=] only to capturers whose origin is lists in {{CaptureHandleConfig/permittedOrigins}}.

MediaDevices.setCaptureHandleConfig()

{{MediaDevices}} is extended with a method - {{MediaDevices/setCaptureHandleConfig}} - which accepts a {{CaptureHandleConfig}} object. By calling this method, an application informs the user agent which information it permits capturing applications to observe.

          partial interface MediaDevices {
            undefined setCaptureHandleConfig(optional CaptureHandleConfig config = {});
          };
        
setCaptureHandleConfig

The user agent MUST run the following validations:

  • If {{CaptureHandleConfig/handle}} is set to an invalid value, the user agent MUST reject by raising {{TypeError}}.
  • If {{CaptureHandleConfig/permittedOrigins}} is set to an invalid value, the user agent MUST reject by raising {{NotSupportedError}}.
  • If the call to {{MediaDevices/setCaptureHandleConfig()}} is not from the [=top-level browsing context=], the user agent MUST reject by raising {{InvalidStateError}}.

If all validations passed, the user agent MUST accept the new config. The user agent MUST forget any previous call to {{MediaDevices/setCaptureHandleConfig}}; from now on, the application's {{CaptureHandleConfig}} is config.

The [=observable=] {{CaptureHandle}} is re-evaluated for all capturing applications.

  1. For every capturing application for which the new [=observable=] {{CaptureHandle}} is different than prior to the call to {{MediaDevices/setCaptureHandleConfig}}, an event of type {{CaptureHandleChangeEvent}} must be fired with {{CaptureHandleChangeEvent/captureHandle}} set to the new [=observable=] {{CaptureHandle}}.
  2. The user agent MUST report the new [=observable=] {{CaptureHandle}} whenever {{MediaStreamTrack/getCaptureHandle}} is called.

Capturing Side for Identification

Capturing applications who are permitted to [=observable|observe=] a track's {{CaptureHandle}} have two ways of reading it.

  1. Reading the current value returned by {{MediaStreamTrack/getCaptureHandle}}.
  2. Registering an {{EventListener}} at {{MediaStreamTrack/oncapturehandlechange}}.

CaptureHandle

The user agent exposes information about the captured application to the capturing application through the {{CaptureHandle}} dictionary. Note that a {{CaptureHandle}} object MUST NOT be given to a capturing application that is not permited to [=observable|observe=] it.

          dictionary CaptureHandle {
            DOMString origin;
            DOMString handle;
          };
        
origin

If the captured application opted-in to exposing its origin (by setting {{CaptureHandleConfig/exposeOrigin}} to true), then the user agent MUST set {{CaptureHandle/origin}} to the origin of the captured application. Otherwise, {{CaptureHandle/origin}} is not set.

handle

The user agent MUST set this field to the value which the captured application set in {{CaptureHandleConfig/handle}}.

MediaStreamTrack.getCaptureHandle()

Extend {{MediaStreamTrack}} with a method called {{MediaStreamTrack/getCaptureHandle}}. When the {{MediaStreamTrack}} is a video track derived of screen-capture, {{MediaStreamTrack/getCaptureHandle}} returns the latest [=observable=] {{CaptureHandle}}. Otherwise it returns null.

          partial interface MediaStreamTrack {
            CaptureHandle? getCaptureHandle();
          };
        
getCaptureHandle

If the track in question is not a video track, or is not the result of a capture of a display surface, then the user agent MUST return null.

If the captured application did not set a {{CaptureHandleConfig}}, or if the last time it set it to the empty {{CaptureHandleConfig}}, then the user agent MUST return null.

The user agent MUST compare the origin of the capturing document to those which the captured application listed in {{CaptureHandleConfig/permittedOrigins}}. If the capturing origin is not permitted to [=observable|observe=] the {{CaptureHandle}}, then the user agent MUST return null.

If all previous validations passed, then the user agent MUST return a {{CaptureHandle}} dictionary with the values derived of the last {{CaptureHandleConfig}} set by the captured application.

On-Change Event

CaptureHandleChangeEvent

Whenever the [=observable=] {{CaptureHandle}} for a given capturing application changes, the user agent fires an event of type CaptureHandleChangeEvent. This can happen in the following cases:

  1. The captured application call {{MediaDevices/setCaptureHandleConfig()}} with a new {{CaptureHandleConfig}}. (Note that the new {{CaptureHandleConfig}} might or might not cause the [=observable=] {{CaptureHandle}} to change, e.g. if changing {{CaptureHandleConfig/permittedOrigins}}.)
  2. The captured application's [=top-level browsing context=] is navigated cross-document.
  3. The user agent switches the track to follow a new application.
            [Exposed=Window]
            interface CaptureHandleChangeEvent : Event {
              constructor(CaptureHandleChangeEventInit init);
              [SameObject] CaptureHandle captureHandle();
            };
          
captureHandle

The track's {{CaptureHandle}} at the time the event was fired, as [=observable=] by the capturing application. If not [=observable=] by the capturing application, all of {{CaptureHandle}}'s fields will be set to their default value - the empty {{DOMString}}.

CaptureHandleChangeEventInit
            dictionary CaptureHandleChangeEventInit : EventInit {
              CaptureHandle captureHandle;
            };
          
captureHandle

The track's {{CaptureHandle}} at the time the event was fired.

oncapturehandlechange

{{MediaStreamTrack}} is extended with an {{EventListener}} called {{oncapturehandlechange}}.

            partial interface MediaStreamTrack {
              attribute EventHandler oncapturehandlechange;
            };
          
oncapturehandlechange

{{EventHandler}} for events of type {{CaptureHandleChangeEvent}}.

The Capture-Handle Actions Mechanism

The capture-handle actions mechanism consists of two parts - one on the captured side, one on the capturing side.

There is disagreement on whether actions should be specified here or in a separate document.

Captured Side for Actions

Applications in top-level documents can declare the [=capture actions=] they support, if any. They would typically do so before even knowing if they are being captured. The intended use is for an application to expect to receive these actions from capturer applications wishing to control the progression of the captured session, in response to interaction with the user. Supported actions are declared by calling {{MediaDevices/setSupportedCaptureActions}} with an array of the names of actions the application is prepared to respond to.

Registering and responding to capture actions

{{MediaDevices}} is extended with a method - {{MediaDevices/setSupportedCaptureActions}} - which accepts an array of {{DOMString}}s. By calling this method, an application registers with the user agent a set of zero or more [=capture actions=] it wishes to respond to.

Capture actions are values defined in {{CaptureAction}}. They are meant to be interpreted as instructions from the capturing application to control the advancement of the presentation of the captured session, however the captured application wishes to define this. The intent is to support capturer applications implementing interactive controls for these actions, whose sending requires [=transient activation=] and [=consume user activation=].

            partial interface MediaDevices {
              undefined setSupportedCaptureActions(sequence<DOMString> actions);
              attribute EventHandler oncaptureaction;
            };

            enum CaptureAction {
              "next",
              "previous",
              "first",
              "last"
            };
          
setSupportedCaptureActions

When this method is invoked, the user agent MUST run the following steps:

  1. If the [=relevant settings object=]'s [=environment settings object/responsible document=] is either not [=Document/fully active=] or its [=browsing context=] is not a [=top-level browsing context=], then throw {{InvalidAccessError}}.
  2. Let |actions| be the method's first argument.
  3. If |actions| is non-empty, and this method was previously called with a non-empty array on [=this=] {{MediaDevices}} object, then throw {{InvalidStateError}}.

  4. Remove from |actions| any value not found in {{CaptureAction}}.
  5. Remove from |actions| any duplicates.
  6. Set [=this=]'s {{MediaDevices/[[RegisteredCaptureActions]]}} to |actions|.
  7. return `undefined` and run the remaining step [=in parallel=].
  8. If this document is currently being captured as part of a browser display surface, then for each capturer of that surface, queue a task on that capturer's task-list to set all associated video {{MediaStreamTrack}}s' {{MediaDevices/[[AvailableCaptureActions]]}} to |actions|.
oncaptureaction of type {{EventHandler}}

The event type of this event handler is `"captureaction"`.

When {{MediaDevices}} is created, give it a [[\RegisteredCaptureActions]] internal slot, initialized to an empty list.

Capture Action Event

CaptureActionEvent

This event is fired on the captured application's {{MediaDevices}} object whenever an action it registered with {{MediaDevices/setSupportedCaptureActions}} has been triggered. This lets the application respond by executing its implementation of this action.

              [Exposed=Window]
              interface CaptureActionEvent : Event {
                constructor(CaptureActionEventInit init);
                readonly attribute CaptureAction action;
              };
            
action
The {{CaptureAction}} that was triggered.

CaptureActionEventInit

              dictionary CaptureActionEventInit : EventInit {
                DOMString action;
              };
            
action
The {{CaptureAction}} to initialize the event with.

Capturing Side for Actions

Capturing applications can enumerate available [=capture actions=] that are supported on the video track they have obtained, by using {{MediaStreamTrack/getSupportedCaptureActions}}, and can trigger those actions by using {{MediaStreamTrack/sendCaptureAction}}.

Enumerating supported actions and triggering them

When a {{MediaStreamTrack}} is a video track derived from screen-capture of a browser display surface, {{MediaStreamTrack/getSupportedCaptureActions}} returns the set of available [=capture actions=], if any, supported by the captured application associated with this video track.

            partial interface MediaStreamTrack {
              sequence<DOMString> getSupportedCaptureActions();
              Promise<undefined> sendCaptureAction(CaptureAction action);
            };
          
getSupportedCaptureActions

When this method is invoked, the user agent MUST return [=this=]' {{MediaDevices/[[AvailableCaptureActions]]}} if defined, or `[]` if not defined.

sendCaptureAction

When this method is invoked, the user agent MUST run the following steps:

  1. If the [=relevant global object=] of [=this=] does not have [=transient activation=], return a promise [=rejected=] with {{InvalidStateError}}.
  2. [=Consume user activation=].
  3. Let |action| be the method's first argument.
  4. If |action| is not in [=this=]' {{MediaDevices/[[AvailableCaptureActions]]}}, return a promise [=rejected=] with {{NotFoundError}}.
  5. Let |p| be a new promise.
  6. Run the following steps [=in parallel=]:
    1. Queue a task on the task-list of the captured browser display surface's [=top-level browsing context=]'s [=active document=] to run the following steps:

      1. Let |target| be the the [=relevant settings object=]'s [=environment settings object/responsible document=]'s associated navigator's {{MediaDevices}} object.
      2. If |action| is not in |target|'s {{MediaDevices/[[RegisteredCaptureActions]]}}, abort these steps.
      3. [=Fire an event=] named `"captureaction"`, using a {{CaptureActionEvent}} with {{CaptureActionEventInit/action}} set to |action|, at |target|.
    2. Wait for the event to have been fired.
    3. Resolve |p|.
  7. Return |p|.

When a video {{MediaStreamTrack}} is created as part of the getDisplayMedia algorithm, whose source is a browser display surface, give it an [[\AvailableCaptureActions]] internal slot, initialized to the captured browser display surface's [=top-level browsing context=]'s [=Browsing context/active window=]'s associated navigator's {{MediaDevices}} object's {{MediaDevices/[[RegisteredCaptureActions]]}}.

While capture of a browser display surface is occurring, whenever that surface's [=top-level browsing context=] is navigated, then for each capturer of that surface, queue a task on that capturer's task-list to set all associated video {{MediaStreamTrack}}s' {{MediaDevices/[[AvailableCaptureActions]]}} to `[]`.