Activation of [[mediacapture-extensions]]'s [[[mediacapture-extensions#media-capture-html-elements|<usermedia>]]] element should only be allowed by an intentional click (or other gesture) by the user. Programmatic activation or tricking a user in activating the element ("clickjacking") should not activate the element. This note proposes specific mechanisms to accomplish this, based on the original [[pepc]] design. The intent of this note is to provide a basis of discussion for standardization of these measures.

This is a purely informational note.

Introduction

[[mediacapture-extensions]]' definition of the trusted algorithm contains two notes, reproduced here:

This document proposes those guidelines.

Integration with <usermedia> element.

The usermedia element's initialization steps should also:
  1. Initialize the internal {{ActivationBlocker/[[BlockerList]]}} to «».
  2. Initialize the internal {{ActivationBlocker/[[IntersectionRect]]}} to undefined.
  3. Initialize the internal {{ActivationBlocker/[[IntersectionObserver]]}} to undefined.
The usermedia element's [=insertion steps=] should also:
  1. Initialize the internal {{ActivationBlocker/[[BlockerList]]}} to «».
  2. [=set/Append=] [=this=] to [=node navigable=]'s {{navigable/[[ActivationBlockableElements]]}}.
  3. Initialize the internal {{ActivationBlocker/[[IntersectionRect]]}} with undefined.
  4. Initialize the internal {{ActivationBlocker/[[IntersectionObserver]]}} with the result of constructing a new {{IntersectionObserver}} with [=ActivationBlocker/IntersectionObserver callback=] and «[ "{{IntersectionObserverInit/rootMargin}}" → `"-4px"` ]».
  5. Call {{ActivationBlocker/[[IntersectionObserver]]}}.observe([=this=]).
  6. [=ActivationBlocker/Add a temporary blocker=] with reason [=ActivationBlocker/recently_attached=].
The usermedia element's [=removing steps=] should also:
  1. [=list/Remove=] [=this=] from [=node navigable=]'s {{navigable/[[ActivationBlockableElements]]}}.
The trusted algorithm, step 2, currently reads: User agents should run additional, implementation-defined steps to determine trusted-ness of |event|. Replace this step with the following step:
  1. If [=this=] is [=ActivationBlocker/blocked=], then return.
usermedia does not provide a hook to observe changes to its styling properties. While the usermedia element is inserted into a document, i.e. between its [=insertion steps=] and [=removing steps=], it should [=ActivationBlocker/check the style=] of the element whenever its styling properties change, including the inherited ones.

Activation Blockers

To guarantee meaningful user intent, we will assume that an activation is — generally — what the user means to do. We also define circumstances that where the user agent assumes non-intentional activation, and call these activation blockers. A usermedia element then maintains a set of current activation blockers, and will disregard an activation if the set of current blockers is non-empty.

Internal Slots

Supporting activation blockers requires the following internal slots at the element:

[[BlockerList]]
A list of records, containing a timestamp and a reason. The [=ActivationBlocker/reason=] is an [=ActivationBlocker/activation blocker=].
[[IntersectionObserver]]
A reference to an IntersectionObserver.
[[IntersectionRect]]
A {{DOMRectReadOnly}} that stores the most recently seen intersection, i.e. the position of the element relative to the viewport.

To prevent a page from "spamming" the user with a large number of elements, we'd like to keep track of the number of active elements. To accomplish this, the [=/navigable=] maintains an [=ordered set=] of elements, [[ActivationBlockableElements]]. This [=ordered set=] is used to evaluate the blockers of type [=ActivationBlocker/unsuccessful_registration=].

Activation Blockers, Blocker Reasons, and Blocker Lifetimes

The key goal of the elements in this specification is to reflect a user's conscious choice, and we need to make sure the user cannot easily be tricked into activating it. To do so, we maintain a list of blocker reasons, which may - permanently or temporarily - prevent the element from being activated.

These blockers come with two lifetimes: temporary, or until revoked.

Temporary blocker
This is a blocker that is valid for a fixed period of time.
An example is an element that has recently been moved.
Revocable blocker
This is a blocker that can be revoked again, e.g. when the condition for which is was set no longer applies. Oftentime, the revocation will incur an additional, fixed period of time before it becomes valid.
An example is an element that is occluded by other content.
Blocker name Blocker type Example condition
unsuccessful_registration [=revocable blocker|revocable=] When too many other elements for the same [=powerful feature=] have been inserted into the same document.
recently_attached [=temporary blocker|temporary=] When the element has just been attached to the DOM.
intersection_changed [=temporary blocker|temporary=] When the element is being moved.
intersection_out_of_viewport_or_clipped [=revocable blocker|revocable=] When the element is not or not fully in the viewport.
intersection_occluded_or_distorted [=revocable blocker|revocable=] When the element is fully in the viewport, but still not fully visible (e.g. because it's partly behind other content).
style_invalid [=revocable blocker|revocable=]

Styling, and Styling Restrictions

usermedia elements constrain the styling that can be applied to them. These are expressed as conditions on their styling, and come in two flavours:

To check the style of a usermedia element:
  1. Check the conditions in [[[#style-blockers]]]. Let |block| be a boolean, which is true when those conditions are met, and false otherwise.
  2. If |block|, then [=ActivationBlocker/add a revocable blocker=] with reason [=ActivationBlocker/style_invalid=].
  3. Otherwise, [=ActivationBlocker/remove blockers=] with reason [=ActivationBlocker/style_invalid=].

Blocking conditions

If the following conditions aren't met, a [=ActivationBlocker/style_invalid=] [=revocable blocker=] is added:

color
background-color
Set by default to the user agent's default `button` colors. The contrast ratio between the 2 colors needs to be at least 3. Alpha has to be 1.
font-size
If [=specified value=] is expressed as absolute-size:
  • Then font-size must be between small and xx-large.
  • Otherwise, the [=computed value=] must be larger than or equal to the [=computed value=] for small and less than or equal to the [=computed value=] of xx-large.
Define "alpha" and "contrast ratio".

User-agent defined stylesheet

A usermedia element is expected to render with the following [=user-agent style sheet=]:

          @namespace "http://www.w3.org/1999/xhtml";
          usermedia {
            opacity: 1.0;
            line-height: normal !important;
            white-space: nowrap !important;
            user-select: none !important;
            appearance: auto !important;
            box-sizing: content-box !important;
            vertical-align: middle !important;
            text-emphasis: initial !important;
            text-shadow: initial !important;
            text-decoration: initial !important;
          }
        

Additional, stylesheet-like conditions

In addition to the [=user-agent style sheet=] in subsection [[[#style-stylesheet]]], we would like to express some style bounds based on the computed style of the element. These should be treated as a [=user-agent style sheet=], too.

For notational convenience, we imagine that the [=computed value=] of an element could be accessed in CSS rules with computed, just like the inherited value of an element can via the [=CSS/inherit=] keyword. Then the following sheet expresses the style bounds:

          @namespace "http://www.w3.org/1999/xhtml";
          usermedia {
            outline-offset: clamp(0, computed, none); /* No negative outline-offsets. */
            font-weight: clamp(200, computed, none);  /* No font-weights below 200. */
            word-spacing: clamp(0, computed, 0.5em);  /* Word-spacing between 0..0.5em */
            letter-spacing: clamp(-0.05em, computed, 0.2em);  /* Letter spacing between -0.05..0.2em */

            min-height: clamp(1em, computed, none);
            max-height: clamp(none, computed, 3em);
            min-width: clamp(none, computed, calc(fit-content));

            border-width: clamp(none, computed, 1em);

            font-style: if(computed = "normal" or computed = "italic", computed, "normal");
            display: if (computed = "inline-block" or computed = "none", computed, "inline-block");
            cursor: if (computed = "pointer" or computed = "not-allowed", computed, "pointer");
            box-shadow: if (computed = "inset", "none", computed);
          }
        

Additionally, some rules apply based on conditions not easily expressible as CSS.

If height is `auto`, then apply:

          @namespace "http://www.w3.org/1999/xhtml";
          usermedia {
            padding-top: clamp(1em, computed, none);
            padding-bottom: calc(padding-top);
          }
        

If width is `auto`, then apply:

          @namespace "http://www.w3.org/1999/xhtml";
          usermedia {
            padding-left: clamp(none, computed, 5em);
            padding-right: calc(padding-left);
          }
        

Apply the following sheet, if the element does not have all of the following:

          @namespace "http://www.w3.org/1999/xhtml";
          usermedia {
            max-width: clamp(none, computed, calc(3 * fit-content));
          }
        

Unaffected properties

The following CSS properties can be used normally:

Properties that are not listed above, or in the rules in this section, and aren't logically equivalent to one of the properties mentioned here, will be ignored.

Algorithms

To add a blocker with a [=activation blocker=] |reason| and an optional flag |temporary|:
  1. Let |timestamp| be None.
  2. If |temporary|, then set |timestamp| to [=current high resolution time=] plus the [=ActivationBlocker/blocker delay=].
  3. [=list/Append=] an entry to the internal {{ActivationBlocker/[[BlockerList]]}} with |reason| and |timestamp|.
The blocker delay is 500ms.
To add a temporary blocker with a [=activation blocker=] |reason|:
  1. [=Assert=]: |reason| is listed as "[=temporary blocker|temporary=]" in the [=ActivationBlocker/activation blockers=] table.
  2. [=ActivationBlocker/Add a blocker=] with |reason| and true.
To add a revocable blocker with a [=activation blocker=] |reason|:
  1. [=Assert=]: |reason| is listed as "[=revocable blocker|revocable=]" in the [=ActivationBlocker/activation blockers=] table.
  2. [=ActivationBlocker/Add a blocker=] with |reason| and false.
To remove blockers with [=activation blocker=] |reason| from an |element|:
  1. [=Assert=]: |reason| is listed as "[=revocable blocker|revocable=]" in the [=ActivationBlocker/activation blockers=] table.
  2. [=list/iterate|For each=] |entry| in |element|'s {{ActivationBlocker/[[BlockerList]]}}:
    1. If |entry|'s reason [=string/is|equals=] |reason|, then [=list/remove=] |entry| from |element|'s {{ActivationBlocker/[[BlockerList]]}}.
  3. [=ActivationBlocker/Add a blocker=] with |reason| and true.
To determine if an |element| is blocked:
  1. If {{ActivationBlocker/[[BlockerList]]}} [=list/contains=] at least one blocker that is [=ActivationBlocker/blocking=], then return true.
  2. Return false.
An element's blocker list's |entry| is blocking if:
  1. |entry| has no [=ActivationBlocker/timestamp=],
  2. or |entry| has a [=ActivationBlocker/timestamp=], and the [=ActivationBlocker/timestamp=] is greater or equal to the [=current high resolution time=].
This spec maintains blockers as a list {{ActivationBlocker/[[BlockerList]]}}, which may potentially grow indefinitely (since some blocker types simply expire, but are not removed). This structure is chosen for the simplicity of explanation, rather than for efficiency. The details of this blocker structure are not observable except for a handful of algorithms defined here, which should open plenty of opportunities for implementations to handle this more efficiently.
The IntersectionObserver callback implements {{IntersectionObserverCallback}} and runs the following steps:
  1. [=Assert=]: The {{IntersectionObserver}}'s {{IntersectionObserver/root}} is the [=/document=].
  2. Let |entries| be the value of the first callback parameter, the [=/list=] of {{IntersectionObserverEntry}}.
  3. [=Assert=]: |entries| is not [=list/is empty|empty=].
  4. Let |entry| be |entries|'s last [=list/item=].
  5. If |entry|.{{IntersectionObserverEntry/isVisible}}, then:
    1. [=ActivationBlocker/Remove blockers=] with [=ActivationBlocker/intersection_occluded_or_distorted=].
    2. [=ActivationBlocker/Remove blockers=] with [=ActivationBlocker/intersection_out_of_viewport_or_clipped=].
  6. Otherwise:
    1. If |entry|.{{IntersectionObserverEntry/intersectionRatio}} >= 1, then:
      1. Let |reason| be [=ActivationBlocker/intersection_occluded_or_distorted=].
    2. Otherwise:
      1. Let |reason| be [=ActivationBlocker/intersection_out_of_viewport_or_clipped=].
    3. [=ActivationBlocker/Add a revocable blocker=] with |reason|.
  7. If {{ActivationBlocker/[[IntersectionRect]]}} does not equal |entry|.{{IntersectionObserverEntry/intersectionRect}} then [=ActivationBlocker/add a temporary blocker=] with [=ActivationBlocker/intersection_changed=].
  8. Set {{ActivationBlocker/[[IntersectionRect]]}} to |entry|.{{IntersectionObserverEntry/intersectionRect}}.