The element
@mattebox/player@0.7.0Needs @mattebox/player-core ^0.4.0 and mattebox ^0.8.2.
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 |
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 |
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; }.
Properties
| Property | What it is |
|---|---|
player |
The Player of the core |
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 |
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);
});
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.
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()] });
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 } });
The controls

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-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
sloton 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, no audio menu, no subtitles menu and no live button.
| 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} |
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 |
icon |
Selects the audio track through engine.tracks. Shown when there is more than one |
|
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 |
icon, icon-on |
Selects the text track, or off. 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, 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-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 two lines of text, above the bar. 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
LIVEfor a live stream. - It sets
castingon 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 its language, then its role, then its id, whichever the manifest gives. The quality menu names a rendition by its height or its bitrate. 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 come from Vidstack’s media-icons, under the MIT
license. The package ships the notice as NOTICE.
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-playerabove it, through shadow hosts. Wait forcustomElements.whenDefined('mattebox-player')before reading it. player.videofor playback,player.enginefor the namespaces (null for a native session),player.playerfor the core, andplayer.error.sourcechangeon the player for a new session,errorfor a failure.openon 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, and moves them back when it hides. 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", thethumbnailsattribute loads the track and the player draws nothing. The app callsengine.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
chaptersattribute with a URL. A session withengine.chaptersloads 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 withaddTextTrack.
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!importantinside 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, 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, section, section-label, item, item-image, item-detail, page-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.
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.
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-ratioon the player,object-fit: containletterboxes 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>