The element

@mattebox/player@0.9.1Needs @mattebox/player-core ^0.6.0 and mattebox ^0.14.0.

This chapter explains <mattebox-player>: its attributes, its properties and its events. It then explains the controls placed inside it, which show over the picture under controls="custom".

Attributes

Attribute What it does
src Sets the source URL. A new value loads the new source. The same value again loads it again
type Sets the MIME type of the source. Optional when the extension is known
preset Names the engine preset. The default is full
license-url Sets the DRM license URL. The player gives it to engine.drm.setLicenseUrl, or to attachEme for a native session
certificate-url Sets the FairPlay application certificate URL. The player gives it to engine.drm.setCertificateUrl, or to attachEme for a native session.
native-hls off keeps HLS on the engine in Safari. By default Safari plays HLS itself. Changes reload the source
tooltips off hides the name each button shows over itself on hover and keyboard focus. The name is the button’s label, so a translated label translates it
thumbnails Sets a thumbnail track URL. The player gives it to engine.thumbnails.load
chapters Sets a chapters file URL. A session with engine.chapters loads it there. Any other session gets a hidden <track kind="chapters"> on the video. Changing it never reloads
controls Selects the controls: native (the default), custom for the control elements inside the player, or none to hide every child but the video. Changing it never reloads
subtitle-size Sets the subtitle size: small, medium, large or xlarge. The subtitles menu writes it. The default is medium
subtitle-background Sets the subtitle background: none, dark or solid. The default is dark
layout The size tier the controls draw for: compact, regular or wide. The player sets it from its own size. A value you write stays. See Size tiers
playsinline Plays inline on an iPhone. On by default. playsinline="false" lets the iPhone go to fullscreen on play. Changing it never reloads
autoplay Copied onto the video as an attribute
muted Copied onto the video, as an attribute and as the muted state. The player also writes the video’s muted state back to it
poster Copied onto the video as an attribute
crossorigin Copied onto the video as an attribute. A chapters track from another origin needs it

The player reports its state as attributes on itself, for the page’s stylesheet and for the controls. mattebox-player[playing] matches while the video plays.

State attribute Set by While
paused, playing, ended The player The video is in that state
muted The player The video is muted
audio The player The source has no picture
started The player The video has played once since its source was set
waiting The player The video waits for data
fullscreen The bar, the button The player is the fullscreen element
pip The picture-in-picture button The video is in the floating window
airplay The AirPlay button The video plays on an AirPlay target
casting @mattebox/player-cast A Chromecast receiver plays the source, and the video is paused
idle The bar The bar is hidden after a time without input
live The seek bar The stream has an availability window
seekable The seek bar The viewer can seek: VOD, or a live window that is wide enough
scrubbing The seek bar A pointer holds the seek bar and the video is paused for the drag. The value is the state before the drag, playing or paused, which the play buttons show

audio comes from the type of the source until the metadata arrives: the type attribute, or the URL’s extension. After that it comes from the size of the video. Zero by zero means sound only. The attribute follows a stream that moves to or from an audio-only rendition.

The player only reports audio. The box stays at 16:9 until the page changes it, for example with mattebox-player[audio] video { aspect-ratio: auto; height: 0; }.

Several sources

The player reads <source> children when it has no src attribute. It tries them in their order, as a video does.

<mattebox-player controls="custom">
  <source
    src="vod/master.m3u8"
    type="application/vnd.apple.mpegurl"
    license-url="https://drm.example/fps"
    certificate-url="https://drm.example/fps.cer"
  />
  <source src="vod/manifest.mpd" type="application/dash+xml" license-url="https://drm.example/widevine" />
</mattebox-player>
Attribute of <source> What it does
src Sets the URL of the source
type Sets its MIME type. Optional when the extension is known
license-url Sets the DRM license URL of this source. The player’s own is the default
certificate-url Sets the FairPlay certificate URL of this source. The player’s own is the default
  • The src attribute wins over the sources property, and the property wins over the children.
  • A source the browser cannot play is replaced by the next one, until the video’s first playing event.
  • Each replaced source dispatches error with fatal: false and its source. The last one is fatal.
  • The player does not read the media attribute of a <source>.
  • The children are read when a load starts. A <source> added while nothing is loaded starts a load.

Chapter 02 has the rules of the list.

Properties

Property What it is
player The Player of the core
sources The sources to try in order, set from script. Setting it loads. Null returns to the <source> children
engine The engine of the current session, or null
video The <video> inside, in light DOM
error The fatal error of the current load. Null after a load starts and after playback resumes
markers The ranges the page named in this source: see Markers

Events

The player dispatches the core’s sourcechange and error again as CustomEvents on itself. They bubble and they are composed. detail is the core’s payload. When sourcechange fires, engine already returns the engine of the new session.

player.addEventListener('error', (event) => {
  console.error(event.detail.code);
});

layoutchange fires when the size tier changes. Its detail is the new tier.

castload comes from the button of @mattebox/player-cast. It fires before the request goes to the receiver. It bubbles, it is composed, and it is cancelable. Its detail is the load request of the sender SDK. The page sets customData on it for its receiver, or cancels the load. See Casting.

seekkey comes from <mbx-control-bar> when a key would seek: the arrows, or fast forward and rewind. It fires on the player before the seek, and it is cancelable. detail.by is the seconds to move, seek-step or its negative. A cancelled seekkey does not seek. The seek bar cancels it under key-mode="preview" to aim instead. A page cancels it to seek its own way.

Markers

Markers are ranges of the media that you name from your own content API: the opening and closing credits, a recap, or a part that must not play. No stream format carries credits as a standard, so the page sets them. They work the same for an engine session and a native one.

player.markers.set([
  { start: 0, end: 52, kind: 'opening-credits' },
  { start: 600, end: 630, kind: 'blocked', label: 'Not available for legal reasons' },
  { start: 1380, end: 1440, kind: 'closing-credits' },
]);
kind What happens
opening-credits <mbx-marker-button> shows “Skip intro” inside the range
closing-credits <mbx-marker-button> shows “Skip credits” inside the range, unless the player carries upnext
blocked Playback never enters the range: the player moves past its end. The seek bar shows a hollow in the track there, and its preview shows the label, or “Not available”
Any other word <mbx-marker-button> shows “Skip” and the marker’s label
  • Set the markers after src: a new source clears them.
  • player.markers.list gives them by start, and player.markers.within(time) the ones that hold a time.
  • markerschange fires when the list changes, with the list as detail.
  • blocked fires each time the player moves past a blocked range, with the marker as detail. Show a notice there if your viewers need one.

Stages

With attributes only, the player uses the preset the preset attribute names, and full by default. full has every stage the engine ships, so every control has its namespace and the player plays every kind of source.

A smaller preset or a list of stages downloads fewer bytes, and loses the features of the stages left out. The engine’s guide, chapter 02, has the matrix.

From JavaScript, the constructor and the static define() take a handler list or a stage list. The page then decides which stages the engine has.

import { MatteboxPlayerElement } from '@mattebox/player';
import hlsCmaf from 'mattebox/protocols/hls-cmaf';
import abr from 'mattebox/stages/abr';

MatteboxPlayerElement.define({ stages: [hlsCmaf(), abr()] });

define() also reaches a player that is already in the page’s markup, as long as it has not loaded a source yet. A player keeps the stages of its first load.

config tunes the kernel in the same way. It works with a preset from the attribute and with a stage list. traceCapacity makes the engine keep its trace for engine.error, and bufferGoalSeconds sets the buffer goal. Every member of the engine’s KernelConfig is accepted. With a handler list, the config goes on the handler.

MatteboxPlayerElement.define({ config: { traceCapacity: 500 } });

drm sets the DRM of a native session. It takes the options of attachEme from mattebox/eme. See Native HLS in Safari.

Native HLS in Safari

Safari plays an HLS source itself, through video.src. The engine does not run. FairPlay, AirPlay, Low-Latency HLS and the iPhone’s fullscreen then work as Apple built them.

The player prefers native HLS when all of these hold:

  • The source is HLS.
  • navigator.vendor starts with Apple. Every browser on WebKit reports it.
  • video.canPlayType('application/vnd.apple.mpegurl') is not empty.

Android Chrome also answers canPlayType for HLS. The vendor check keeps it on the engine. DASH stays on the engine in every browser.

native-hls="off" keeps HLS on the engine. A page with its own handlers decides the order itself.

A native session has no engine, so player.engine is null. The controls read the video instead:

Control In a native session
mbx-audio-menu Lists video.audioTracks. A choice sets enabled
mbx-subtitles-menu Lists the subtitles and captions tracks of video.textTracks. A choice sets mode
mbx-drm-badge Reads session.eme.drm when the session attached DRM
mbx-chapters-menu Reads the chapters track of the video, as for any session
mbx-quality-menu Hidden. Safari picks the rendition
mbx-scan-button Hidden
Thumbnails and previews None

A menu hides when the browser has no such list. Forced subtitles show by Safari’s own choice. A captions track carries the SDH badge. An audio track of kind description carries the AD badge.

Errors are the video’s MediaError codes, plus the DRM errors of the attachment.

DRM in a native session

The player attaches the engine’s DRM to the video before it sets src, with attachEme from mattebox/eme. It detaches it when the session ends.

  • The key systems are the eme-fairplay and eme-cenc stages of the engine’s stack: the preset’s, or the stages option’s.
  • license-url sets the license URL, and certificate-url the FairPlay certificate.
  • The drm option adds to them: keySystems, licenseUrl, licenseUrls, requestHook.
  • The engine’s request hooks do not apply. requestHook rewrites the license and certificate requests.
  • A browser without EME attaches no DRM, as for the engine.

FairPlay needs the application certificate:

import { MatteboxPlayerElement } from '@mattebox/player';
import emeFairplay from 'mattebox/stages/eme-fairplay';

MatteboxPlayerElement.define({
  drm: {
    keySystems: [emeFairplay({ certificateUrl: 'https://drm.example/fps.cer' })],
    licenseUrl: 'https://drm.example/fps',
    requestHook: (request) => {
      request.headers.Authorization = `Bearer ${token}`;
    },
  },
});

The controls

The quality menu open over the picture, listing Auto and five renditions.
A menu opens over the picture and never leaves it.

controls="custom" removes the browser’s controls from the video. The controls are then elements placed inside the player, next to the video.

The bar shows its children in their order. A control left out does not appear. Each control takes its parameters and its labels as attributes, and its icon through a slot.

<mattebox-player src="…" controls="custom">
  <mbx-title></mbx-title>
  <mbx-start-button></mbx-start-button>
  <mbx-marker-button></mbx-marker-button>
  <mbx-error-screen></mbx-error-screen>
  <mbx-spinner></mbx-spinner>
  <mbx-control-bar>
    <mbx-current-time></mbx-current-time>
    <mbx-seek-bar></mbx-seek-bar>
    <mbx-duration></mbx-duration>
    <mbx-play-button label-play="Reproduir" label-pause="Pausa"></mbx-play-button>
    <mbx-skip-button seconds="15"></mbx-skip-button>
    <mbx-spacer></mbx-spacer>
    <mbx-fullscreen-button>
      <svg slot="icon-enter" viewBox="0 0 32 32">…</svg>
    </mbx-fullscreen-button>
  </mbx-control-bar>
</mattebox-player>

The player appends nothing under custom. A player with no controls inside stays empty, so the composition is always the page’s. A composition to start from, with most of the controls:

<mbx-title></mbx-title>
<mbx-start-button></mbx-start-button>
<mbx-error-screen></mbx-error-screen>
<mbx-spinner></mbx-spinner>
<mbx-control-bar>
  <mbx-current-time></mbx-current-time>
  <mbx-seek-bar></mbx-seek-bar>
  <mbx-duration></mbx-duration>
  <mbx-live-button></mbx-live-button>
  <mbx-skip-button seconds="-10"></mbx-skip-button>
  <mbx-play-button></mbx-play-button>
  <mbx-skip-button seconds="10"></mbx-skip-button>
  <mbx-volume></mbx-volume>
  <mbx-spacer></mbx-spacer>
  <mbx-speed-menu></mbx-speed-menu>
  <mbx-chapters-menu></mbx-chapters-menu>
  <mbx-subtitles-menu></mbx-subtitles-menu>
  <mbx-audio-menu></mbx-audio-menu>
  <mbx-quality-menu></mbx-quality-menu>
  <mbx-airplay-button></mbx-airplay-button>
  <mbx-pip-button></mbx-pip-button>
  <mbx-fullscreen-button></mbx-fullscreen-button>
</mbx-control-bar>

The bar has two rows: the seek row above the buttons row.

  • A child with slot="seek" goes in the seek row.
  • The times, the seek bar and the live button go in the seek row by themselves, unless the page wrote a slot on them.
  • Every other child goes in the buttons row, in its order.
  • <mbx-spacer> pushes the controls after it to the right.

Import only what you use

The root entry registers every element, and the CDN bundle contains them all. A page on a bundler imports the player alone and one entry per control it places. The bundle then contains only those controls.

import '@mattebox/player/element';
import '@mattebox/player/elements/control-bar';
import '@mattebox/player/elements/play-button';
import '@mattebox/player/elements/seek-bar';

Each control entry registers its element and the player.

An element the page did not import renders nothing.

The elements

Each element reads and writes the video from outside, as a page would. It reads the engine’s namespaces through the player.

An element that reads a namespace hides while the session has none. A native session shows no quality menu and no live button. Its audio and subtitles menus read the video’s own track lists. See Native HLS in Safari.

Element Attributes Labels Icon slots What it does
mbx-control-bar idle-ms, seek-step Lays out the controls over the picture. It fades, handles the shortcuts and the click on the video, and moves the subtitles above itself
mbx-spacer Takes the free room in a row
mbx-play-button label-play, label-pause, label-replay icon-play, icon-pause, icon-replay Plays and pauses. Replays after the end
mbx-mute-button label-mute, label-unmute icon-mute, icon-low, icon-high Mutes and unmutes. The icon shows the level. Sets aria-pressed
mbx-volume-slider step, page label Sets the volume. It shows zero while muted, and a level above zero unmutes. Has dragging during a drag
mbx-volume Groups the mute button with a slider that unfolds under the pointer or on focus. It fills itself with both when left empty
mbx-skip-button seconds, negative for back label with {seconds} icon Moves the playhead by seconds, within the range the video can seek
mbx-scan-button direction, forward or backward; rates, space-separated, each above 2 label icon Scans through the I-frame track with engine.trick.setRate. Each press steps up through rates, and a press past the last plays at normal speed. Hidden without an I-frame track. Has scanning and aria-pressed while it scans
mbx-current-time Shows the position, such as “1:23”. On live, it shows the wall clock or the distance behind the edge. Hidden on a live stream that is not seekable
mbx-duration Shows the duration, such as “4:56”. Hidden on live
mbx-remaining-time Shows the time left, such as “-4:53”. Hidden on live. The one for a bar that shows a single number
mbx-seek-bar step, page, live-window, chapters, scrub, preview, key-mode label, label-of with {current} and {duration}, label-behind with {time}, label-blocked Shows the position, the buffered ranges, the live window and its edge, and the preview. It is divided at the chapters, and the preview names the chapter. chapters takes divided, titles, both (the default), or none. A drag scrubs through the I-frame track when there is one. scrub="false" turns that off. preview orders the preview’s picture sources, frames tiles by default. key-mode="preview" makes the keys aim a target instead of seeking. Sets live, seekable and scrubbing on the player. Has dragging during a drag
mbx-live-button text, label-live, label-at-edge Shows on a live stream and seeks to the edge. Disabled at the edge, where it has at-edge
mbx-speed-menu rates, space-separated label, label-normal, label-back with {page} icon Sets video.playbackRate. Works for every session
mbx-chapters-menu label, label-back icon Lists the chapters with their start times, and checks the current one. Each item shows the chapter’s picture when every chapter has one. A choice seeks. Hidden without chapters. Works for every session
mbx-quality-menu label, label-auto, label-back icon Pins a rendition through engine.quality. Auto means no pin
mbx-audio-menu label, label-back, label-ad, label-original, label-track icon Selects the audio track through engine.tracks, or video.audioTracks in a native session. Lists only tracks the engine can play. Shown when there is more than one. Marks an audio description track and the original language with a badge
mbx-subtitles-menu label, label-back, label-off, label-track, label-settings, label-size, label-background, label-small, label-medium, label-large, label-xlarge, label-none, label-dark, label-solid, label-sdh, label-cc icon, icon-on Selects the text track, or off, through engine.tracks, or video.textTracks in a native session. Lists only tracks the engine can play, in-band captions included. Forced tracks are not listed, and the menu reads off while one shows. Marks SDH subtitles with a badge. Its Settings page sets the size and the background, and writes subtitle-size and subtitle-background on the player
mbx-drm-badge label with {system} and {keys}, label-key and label-keys with {count} and {statuses}, label-no-key icon Shows a lock for engine.drm, or session.eme.drm in a native session, with a tooltip on hover and on focus
mbx-airplay-button label, label-active icon, icon-active Opens Safari’s AirPlay picker. Hidden without AirPlay and while the video offers no target. Sets airplay on the player
mbx-pip-button label-enter, label-exit icon-enter, icon-exit Toggles picture in picture. Hidden without the API. Sets pip on the player
mbx-fullscreen-button label-enter, label-exit icon-enter, icon-exit Toggles fullscreen on the player. Hidden without the API. Sets fullscreen on the player
mbx-start-button label-play, label-replay icon-play, icon-replay Shows a large play button over the picture while paused, and a replay after the end. Hidden while playing, behind an error, and in a box too short to keep it above the bar
mbx-marker-button label-opening-credits, label-closing-credits, label-skip Shows a skip button over the picture inside a marker the page set, other than blocked. A press moves to the marker’s end. It sits above the bar, and hides once the media has ended. While the player carries upnext, a card such as the playlist’s <mbx-up-next> covers the closing credits, and the button leaves them to it
mbx-error-screen label-title, label-retry Shows a fatal error over the picture, with the category, the code and a retry that loads src again
mbx-spinner label icon Shows a turning ring in the place of the start button while the player has waiting
mbx-title heading, subheading, artwork Shows the artwork next to the heading and subheading, above the bar. Beside artwork, a long heading wraps to three lines, or two above a subheading; without artwork it keeps one line. Shown while the video is paused, including before the first play. Has playing for the page’s own rule

The title’s content comes from its attributes, because the stream has none of it. It goes first among the screens, so its band paints under the start button and the spinner.

CSS decides when the title shows. By default it shows while the video is paused, which includes the time before the first play.

  • mattebox-player[started] mbx-title { display: none } shows it only before the first play, like the poster.
  • mattebox-player:hover mbx-title { display: flex } shows it again on hover.

Every menu has open on itself while its popup shows. The bar does not fade while an element inside it has open.

A popup never leaves the picture. It uses the room above its button as its height, and scrolls when it needs more.

Casting

Chromecast is @mattebox/player-cast, a package of its own. Its button loads Google’s sender SDK onto the page, and the player never loads third-party code for a page. The AirPlay button uses Safari’s own API, so it is part of the player.

Install the package and place its two elements, the way <mbx-diagnostics> is placed.

<mattebox-player src="…" controls="custom">
  <mbx-cast-screen></mbx-cast-screen>
  <mbx-control-bar>
    …
    <mbx-cast-button></mbx-cast-button>
  </mbx-control-bar>
</mattebox-player>

<script type="module">
  import '@mattebox/player';
  import '@mattebox/player-cast';
</script>

<mbx-cast-button> starts and ends a Chromecast session. <mbx-cast-screen> controls the receiver during the session.

The button loads the SDK from gstatic the first time it connects. It does not load the SDK when the page already has it, or when the button has sdk="none". In that case the page loads the SDK.

receiver is the id of the receiver application. Without it, the button uses the Default Media Receiver. The first button on the page decides the receiver, because the SDK keeps one context per page.

Element Attributes Labels Icon slots
mbx-cast-button receiver, sdk label-start, label-stop icon, icon-active
mbx-cast-screen label with {device}, label-stop, label-play, label-pause, label-seek icon-play, icon-pause

When a session starts, the player does this:

  • It pauses the video.
  • It suspends the engine, so the engine requests nothing during the cast.
  • It sends the source of the session at the video’s current time, with the subtitle tracks of the video, and LIVE for a live stream.
  • It sets casting on itself. The default stylesheet then hides the bar and the start button, and the cast screen shows the receiver’s controls.

When the session ends, the engine resumes. The video seeks to the time where the receiver stopped, and plays if the receiver was playing. A live stream returns to the live edge instead.

The receiver plays the URL by itself, so DRM and signed URLs are the page’s to arrange, in castload. The Default Media Receiver plays clear HLS and DASH. A protected stream needs a receiver of the page’s own, which reads what the page put in customData.

button.addEventListener('castload', (event) => {
  event.detail.customData = { licenseUrl: 'https://drm.example/widevine' };
});

Labels

Every text a control shows, and every name it gives itself, is an attribute of that control. English is the value when the attribute is absent. Each state has its own attribute: label-play, label-pause, label-replay.

A label can contain a value in braces: label="Enrere {seconds} segons". A name in braces the control does not know stays as written.

Translation is the page’s, with whatever it uses for its strings. The player ships no locales.

<mbx-play-button label-play="Reproduir" label-pause="Pausa" label-replay="Torna a reproduir"></mbx-play-button>
<mbx-skip-button seconds="-10" label="Enrere {seconds} segons"></mbx-skip-button>
<mbx-seek-bar label="Cerca" label-of="{current} de {duration}"></mbx-seek-bar>

The audio and subtitles menus name a track by the first of these it has:

  1. The name in the manifest (HLS NAME, DASH Label), or the label of a native track.
  2. The language’s name in the page’s language (lang on <html>, else the browser’s). This needs Intl.DisplayNames, which Chromium 76 and 79 do not have.
  3. The language code.
  4. “Track” (label-track) and the track’s place in the list.

The role main is not shown. Two tracks that read the same take a hint in parentheses: the audio channel layout (“Stereo”, “5.1”) when the engine gives one, then the role, then the place in the list.

A badge after the name marks a standard characteristic, from the engine’s track functions.

Badge Attribute Menu Track
AD label-ad audio Audio description
Original label-original audio The original language
SDH label-sdh subtitles Subtitles for the deaf and hard of hearing
CC label-cc subtitles Closed captions carried in the video

The player draws AD, SDH and CC as a glyph. The label is the glyph’s tooltip, and the text a screen reader says.

The quality menu lists engine.quality.selectable, best first: what the browser can play, one entry per video stream. A name grows only as far as it must to tell entries apart.

  1. The height, such as 1080p, or the bitrate without one.
  2. The frame rate above 30, such as 1080p60.
  3. Dolby Vision or HDR, such as 1080p HDR.
  4. The bitrate, when two entries still read the same: 1080p60 · 6.2 Mbps.
  5. The codec, when they still do: 1080p · 6.1 Mbps · HEVC.

Entries that read the same after that, such as one stream on two CDNs, show once.

Under Auto, the Auto row names what plays in its detail, such as Auto 720p. These names come from the stream and have no attribute.

Icons

Every button has one named slot per state for its icon. The button shows its own icon when the slot is empty.

A page puts its own SVG in the slot. The button keeps its name: the icon is decoration with aria-hidden, and the name is on the button, where a screen reader reads it.

<mbx-play-button>
  <svg slot="icon-play" viewBox="0 0 24 24"><path d="…" /></svg>
  <svg slot="icon-pause" viewBox="0 0 24 24"><path d="…" /></svg>
</mbx-play-button>

The player’s own icons are Mattebox’s drawings: one filled path each, on a 32-unit grid, so they scale to any size and take the text colour.

A control of your own

Any element inside the bar is a control. It shows in the row where the page put it. It finds its player the way the player’s own controls do:

  • The nearest mattebox-player above it, through shadow hosts. Wait for customElements.whenDefined('mattebox-player') before reading it.
  • player.video for playback, player.engine for the namespaces (null for a native session), player.player for the core, and player.error.
  • sourcechange on the player for a new session, error for a failure.
  • open on itself while it shows a popup, so the bar does not fade.
  • A real <button> for anything Space must activate. The bar leaves Space to a focused button.

A control gets nothing else, and the player’s own controls use nothing else. A control of the player that needed more would be a bug in this API.

@mattebox/player-diagnostics and @mattebox/player-cast are controls written this way, in packages of their own. Chapter 05 covers the diagnostics, and Casting covers the cast.

The bar

The bar fades on one timer. Any pointer movement or press, any key, and the start of playback restart the timer for idle-ms. When the timer ends, the bar hides, except in three cases:

  • The video is paused.
  • An element inside the bar has open.
  • Keyboard focus is inside the bar.

The bar never tracks whether the pointer is over it. The fade respects prefers-reduced-motion.

A narrow bar hides controls from its buttons row instead of overflowing. Each child has a priority, and the bar hides the highest number first:

Priority Controls
6 The scan buttons
5 The volume slider
4 The diagnostics, the DRM lock, the speed and chapters menus
3 The audio and quality menus, picture in picture
2 The skips
1 The subtitles menu, the mute button and the volume group
0 Everything else. Never hidden: play and fullscreen stay at every width

A priority attribute on a control replaces its default. A page’s own control in the bar starts at 0.

While the buttons need more room than the row has, minus a few pixels of tolerance, the bar sets collapsed on the highest numbers, one at a time. Among equal numbers it starts from the right. It removes the attribute again when the bar gets wider.

The bar counts the folded volume slider at its unfolded width, so the row has room for the slider when it unfolds. The slider is the first control to go.

collapsed is an attribute, and one rule of the bar hides a collapsed control. mbx-speed-menu[collapsed] is therefore the page’s to style. A page with its own breakpoints sets every priority to 0 and writes container queries on the player.

Fullscreen goes on the player itself. Every control placed inside stays visible, and mattebox-player:fullscreen matches. The player also sets fullscreen on itself, for the Safari versions that only have the prefixed API. mattebox-player[fullscreen] > video is where a page lifts a height limit it set on the video.

The subtitles are the browser’s own cues on the video. The bar moves the active cues above itself while it shows. When it hides, they rest one line above the picture’s bottom edge. It only moves a cue the author did not position. Two cues that show at once stack, each by its measured height.

The bar styles the cues with a small stylesheet it adds to the document once, with one rule per size and one per background. The stylesheet must be in the document: ::cue only accepts rules from the document that holds the video, and it accepts no custom properties.

The sizes are percentages of the browser’s own cue size, which follows the height of the video. A page that wants more writes its own mattebox-player > video::cue rule. That rule wins because it comes later.

On a live stream, the seek bar shows the availability window and marks the edge. The live button seeks to the edge on a click. The time and the hover preview show the wall clock when the stream has an anchor (a program date time or an availability start). Otherwise they show the distance behind the edge.

A window shorter than live-window buffer goals gets no seek bar, because the viewer has no range to seek in. The live button then stays red and disabled.

Every control is reachable with the keyboard, and every control has a name. The sliders announce their value in words. While focus is inside the player, these keys work from every control:

Key What it does
Space, k Plays or pauses. Space on a focused button activates that button
m Mutes or unmutes
f Enters or leaves fullscreen
Left, Right Seeks by the seek-step of the bar
Play/Pause, Play, Pause The media keys a remote or a keyboard sends: play or pause
Stop Pauses. The source stays
Fast Forward, Rewind Seek by the seek-step of the bar
Up, Down, Page keys, Home, End On a slider, moves it by its step, by its page, or to its end
Up, Down, Home, End, Escape In a menu, moves the selection. Escape closes the menu

A click on the video plays or pauses. Under custom the player sets tabindex="0" on itself, so the shortcuts still work after a click on the video. A page that set its own tabindex keeps it.

A key another handler already prevented is left alone. A handler that listens on the player in the capture phase runs first, and takes a key by preventing it.

Native controls

Under controls="native", the video shows the browser’s own controls and the player appends nothing. The quality, the tracks and the rest have no control, because the browser’s controls do not show them. A page that wants them uses controls="custom".

Under native controls, a fatal error shows in the player’s own row under the video, the error surface, with the category, the code and a retry. Under custom, <mbx-error-screen> shows the error instead.

controls="none" takes the browser’s controls off the video, appends nothing, and hides every child but the video. A page keeps its composition in place and turns it off with the one attribute. The bar’s shortcuts still listen while hidden, and the error surface still shows a fatal error.

The live button needs more than its namespace. full has both live stages, so engine.live exists for a VOD stream too. A stream is live when it has an availability window, so the button shows only after edge is set.

Native controls do not report the scrub position, so thumbnails have no preview there.

  • Under controls="native", the thumbnails attribute loads the track and the player draws nothing. The app calls engine.thumbnails.at(time).
  • Under controls="custom", the preview of the seek bar shows a picture above the pointer, scaled to --mbx-preview-width, 160px by default.

On a TV, a seek per key press stutters. key-mode="preview" on the seek bar makes the keys aim instead:

  • The arrows, Page Up, Page Down, Home, End, fast forward and rewind move a target. The thumb and the preview show it, and the video keeps playing.
  • A held key’s step grows, so a long film takes seconds to cross.
  • Enter seeks to the target. So does moving focus off the bar, or a second without a key.
  • Escape, Back, or a click outside the seek bar drops the target.

The default, key-mode="instant", seeks on every key.

The preview’s picture comes from the sources the seek bar’s preview attribute names, in order of preference. The first one that answers for the time wins. Without a picture, the preview shows the time and the chapter.

preview Picture
frames tiles (default) The decoded I-frame; the tile until it arrives, or without one
tiles frames The tile; the decoded I-frame where the stream has no tile
tiles The tile only
frames The decoded I-frame only
none No picture

A decoded I-frame comes from engine.trick.frameAt. It needs an I-frame track, WebCodecs, and clear fMP4. A tile comes from engine.thumbnails.

A drag on the seek bar scrubs when the session has an I-frame track. The video pauses and shows the I-frame at each position, and the release seeks there. scrub="false" on the seek bar turns this off. Without scrubbing, the video pauses and seeks as the pointer moves, and the release resumes playback if it was playing. Thumbnail tiles never drive a drag: they are previews, not frames of the video.

<mbx-scan-button> shows only with an I-frame track. Place one of each direction around the play button:

<mbx-scan-button direction="backward"></mbx-scan-button>
<mbx-play-button></mbx-play-button>
<mbx-scan-button></mbx-scan-button>

Chapters come from the engine when the session holds some, and from a text track of kind chapters on the video otherwise. There are four ways to add them:

  • The chapters attribute with a URL. A session with engine.chapters loads it there, in every format the engine reads. Any other session gets a hidden <track>.
  • The manifest. The engine reads the chapters an HLS playlist names.
  • engine.chapters.set(list) with chapters the page already holds, such as ones from a content API.
  • A <track> on the video written by the page, or a track built with addTextTrack.

The seek bar leaves a gap at each chapter boundary and names the chapter in the preview. <mbx-chapters-menu> lists the chapters, with their pictures when every chapter has one. Chapters work for a native session too, from a text track.

<mattebox-player src="https://example.com/vod/master.m3u8" chapters="/vod/chapters.vtt" controls="custom"></mattebox-player>

Styling

The controls are elements in the page’s own light DOM. The page’s stylesheet selects each one by its tag: mbx-play-button { margin: 0 4px }.

The inside of a control is in shadow DOM. ::part() reaches it, and accepts every CSS property. The state is an attribute on the element, so a selector can read it.

mbx-play-button::part(button) { border-radius: 50%; }
mattebox-player[playing] mbx-play-button::part(icon) { fill: rebeccapurple; }
mbx-quality-menu::part(item):hover { background: rebeccapurple; }
mbx-live-button[at-edge]::part(dot) { box-shadow: none; }
mbx-control-bar { background: #000; }

Two rules make this work:

  • No stylesheet of the player uses !important. For normal declarations, a rule from the page wins over a rule from the shadow tree, whatever its specificity. An !important inside the shadow tree would reverse that.
  • Every element a control draws has a part, because ::part() cannot select the descendants of a part.
Element Parts
mattebox-player stage, and the error surface: error, error-category, error-code, error-retry, value, button
mbx-control-bar row, seek-row, buttons-row
Every button control button, icon
mbx-volume-slider, mbx-seek-bar slider, rail, track, fill, thumb
mbx-seek-bar buffered, buffered-range, blocked, blocked-range, hover, edge, preview, preview-frame, preview-image, preview-tile, preview-title, preview-time
mbx-scan-button button, icon, rate
mbx-live-button button, dot, text
Every menu button, icon, popup, title, section, section-label, item, item-image, item-badge, item-glyph, badge-text, item-detail, page-item, glyph-item, back-item, and <name>-item and <name>-section per group
The chosen item of a menu checked, added to the part of the item. It is the only state in a part name, because a page cannot otherwise select the state of an item inside a shadow root
mbx-drm-badge icon, tooltip, tooltip-title, tooltip-text
mbx-error-screen box, title, detail, category, separator, code, retry

Part names are public API. A release can add names. A name is only renamed in a major version.

Size

Every size in the controls is an em of the player’s font size, which is 16 pixels. One rule scales the whole interface: the buttons, the bar, the text, the menus.

mattebox-player {
  font-size: 20px;
}

Borders and outlines stay one or two pixels at every size.

Size tiers

The player on a phone: the title at the top, one time readout, and fewer controls.
The compact tier, on a phone held upright.

The player measures its own box and sets layout on itself. The controls draw for that tier. The box decides, not the screen: a small embed on a desktop page is compact, and a phone in fullscreen is not.

Tier When What the controls do
compact Under 480 pixels wide, or under 320 high One time readout, the time left. The title is one line at the top. Menus and the playlist open as sheets from the bottom of the player, over the controls. A menu’s first row is the way back
regular Between the two The design at 16 pixels
wide From 1280 pixels wide and 700 high The same design at 20 pixels, for a TV or a fullscreen

You write the same markup for all three. A stylesheet reads the tier from the player, and every control carries it too:

mattebox-player[layout="compact"] mbx-skip-button {
  display: none;
}

To fix the tier, write the attribute. The player then leaves it alone. Remove it to hand it back.

<mattebox-player layout="compact" src="…"></mattebox-player>

A font size of your own on the player wins over the tier’s.

Tokens

Custom properties set on the player are inherited by every control: --mbx-surface, --mbx-text, --mbx-muted, --mbx-accent, --mbx-error, --mbx-radius, --mbx-gap, --mbx-pad, --mbx-font, --mbx-live, --mbx-preview-width (in pixels, 10em by default), --mbx-menu-max-height (in pixels, 22em by default), --mbx-tooltip (none hides the tooltips), --mbx-blocked (the dashes of a blocked range).

The shadow roots are open. When ::part() is not enough, a page adds a stylesheet inside a control: control.shadowRoot.append(style) in every browser, or adoptedStyleSheets in a modern one.

The <video> is in light DOM, so the page styles it directly. By default it takes the full width at 16:9 until the media’s own ratio is known, so the box does not change size when the metadata arrives. A page that knows the ratio of its media sets aspect-ratio on the video.

The box of the player is black, and the picture is centred in it.

  • With a height or an aspect-ratio on the player, object-fit: contain letterboxes or pillarboxes the picture inside the box. The bar stays at the bottom of the box, as in fullscreen.
  • Without a height, the box is as tall as the picture and no black shows.
  • mattebox-player { background: … } changes the colour.

Browser support

The player is a custom element, so it needs Custom Elements v1: Chrome 54, Safari 10.1, Firefox 63, Edge 79. Every browser has Shadow DOM v1 at or before those versions.

::part() needs Chrome 73, Safari 13.1, Firefox 72. An older browser still applies the custom properties above.

The engine supports older browsers than the player: it needs ES2015 only. A page for a browser the player does not support takes @mattebox/player-core and writes its own UI.

Lifecycle

disconnectedCallback disposes the session. Reconnecting loads src again.

A control attaches when it is connected inside a player, and detaches when it is removed. A framework that reorders or replaces children is therefore safe.

Example

A player with its own bar in Catalan, its own play icons, and a control the page wrote.

<mattebox-player src="https://example.com/vod/master.m3u8" controls="custom">
  <mbx-start-button label-play="Reproduir"></mbx-start-button>
  <mbx-error-screen label-title="Error de reproducció" label-retry="Torna-ho a provar"></mbx-error-screen>
  <mbx-control-bar>
    <mbx-current-time></mbx-current-time>
    <mbx-seek-bar label="Cerca" label-of="{current} de {duration}"></mbx-seek-bar>
    <mbx-play-button label-play="Reproduir" label-pause="Pausa">
      <svg slot="icon-play" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
      <svg slot="icon-pause" viewBox="0 0 24 24"><path d="M6 5h4v14H6zm8 0h4v14h-4z" /></svg>
    </mbx-play-button>
    <mbx-volume>
      <mbx-mute-button label-mute="Silencia" label-unmute="Activa el so"></mbx-mute-button>
      <mbx-volume-slider label="Volum"></mbx-volume-slider>
    </mbx-volume>
    <mbx-spacer></mbx-spacer>
    <my-share-button></my-share-button>
    <mbx-fullscreen-button label-enter="Pantalla completa" label-exit="Surt"></mbx-fullscreen-button>
  </mbx-control-bar>
</mattebox-player>

<script type="module">
  import '@mattebox/player';

  class MyShareButton extends HTMLElement {
    async connectedCallback() {
      await customElements.whenDefined('mattebox-player');
      const player = this.closest('mattebox-player');
      const button = document.createElement('button');
      button.type = 'button';
      button.setAttribute('aria-label', 'Comparteix');
      button.textContent = '↗';
      button.addEventListener('click', () => {
        navigator.share({ url: player.getAttribute('src') });
      });
      this.append(button);
    }
  }
  customElements.define('my-share-button', MyShareButton);
</script>

Read this page's source on GitHub