@optimist-video/mistplayer-core
v1.1.4
Published
Adaptive streaming media player with automatic protocol selection, multi-player support, and fully customizable.
Readme
@optimist-video/mistplayer-core
MistServer's streaming player as an ESM package. Supports HLS, DASH, WebRTC (WHEP), MEWS, raw WebSocket (H.265), and native HTML5 playback with automatic protocol selection.
- Install · Quick Start · API Reference · Options
- Features: Accessibility · i18n · Keyboard · Thumbnails · AirPlay · Audio Gain · Mobile · Breakpoints
- Theming · TypeScript
- Advanced: Custom Skins · Blueprints · Wrappers · CSS Architecture · Legacy
Install
npm install @optimist-video/mistplayer-coreOr use directly from your MistServer instance — no install needed:
<!-- ESM -->
<script type="module">
import { createPlayer } from 'https://myserver:8080/player.mjs';
</script>
<!-- IIFE -->
<script src="https://myserver:8080/player.js"></script>Quick Start
import { createPlayer } from '@optimist-video/mistplayer-core';
const player = createPlayer({
target: document.getElementById('player'),
host: 'https://myserver:8080',
stream: 'live',
});
player.on('initialized', () => console.log('Ready'));
player.play();For IIFE (no build step): mistPlay('live', { target: document.getElementById('player') }).
API Reference
createPlayer(options) → Player
Creates a player instance. This is the recommended entry point.
Queries (read state)
| Property | Type | Description |
|----------|------|-------------|
| player.currentTime | number | Current playback position (seconds) |
| player.duration | number | Stream duration (seconds, Infinity for live) |
| player.volume | number | Volume level (0.0 to 1.0) |
| player.muted | boolean | Whether audio is muted |
| player.paused | boolean | Whether playback is paused |
| player.playbackRate | number | Playback speed (1 = normal) |
| player.loop | boolean | Whether looping is enabled |
| player.buffered | TimeRanges | Buffered time ranges |
| player.fullscreen | boolean | Whether fullscreen is active |
| player.pip | boolean | Whether picture-in-picture is active |
| player.tracks | object\|null | Available tracks: {video, audio, subtitle} |
| player.size | object\|null | Player dimensions: {width, height} |
| player.capabilities | object | What the active wrapper supports |
| player.quality | number\|null | Playback quality score (0.0 to 1.0) |
| player.streamState | string\|null | Stream status |
| player.video | HTMLVideoElement | The underlying <video> element |
| player.info | object | Stream metadata (tracks, sources, type) |
| player.source | object | Currently active source ({url, type}) |
| player.playerName | string | Active wrapper name ("html5", "wheprtc", etc.) |
Mutations (change state)
| Method / Setter | Description |
|--------|-------------|
| player.play() | Start or resume playback (returns Promise) |
| player.pause() | Pause playback |
| player.currentTime = 30 | Seek to position (seconds) |
| player.volume = 0.5 | Set volume (0.0 to 1.0) |
| player.muted = true | Mute/unmute audio |
| player.playbackRate = 2 | Set playback speed |
| player.loop = true | Enable/disable looping |
| player.fullscreen = true | Enter/exit fullscreen |
| player.pip = true | Enter/exit picture-in-picture |
| player.setTrack(type, id) | Select a track: setTrack('video', '1') |
| player.destroy() | Tear down the player and clean up |
| player.reload() | Rebuild the player from scratch |
| player.setTheme(name, mode?) | Apply a theme: setTheme('dracula') or setTheme('catppuccin', 'light') |
| player.setTheme(tokens) | Apply custom tokens: setTheme({ 'color-accent': '#ff6600' }) |
| player.translate(key, fallback?) | Look up an i18n string |
Subscriptions (react to changes)
Subscribe to state changes reactively. Callbacks fire immediately with the current value, then on every change:
const unsub = player.state.on('paused', (isPaused) => {
myButton.textContent = isPaused ? 'Play' : 'Pause';
});
player.state.get('currentTime'); // read without subscribing
unsub(); // unsubscribeAvailable state properties: paused, playing, currentTime, duration, volume, muted, playbackRate, loop, buffered, seeking, ended, loading, fullscreen, pip, tracks, streamState, error.
Events
player.on('initialized', () => { /* player is ready */ });
player.on('error', (e) => { /* playback error */ });
player.on('ended', () => { /* stream ended */ });
player.off('ended', myCallback);| Event | When |
|-------|------|
| initialized | Player built and ready |
| play, playing, pause, ended | Playback state changes |
| seeking, seeked | Seek started / completed |
| timeupdate, durationchange | Time / duration changed |
| volumechange | Volume or mute changed |
| waiting, canplay, error | Buffering / ready / error |
| playerUpdate_trackChanged | Track selection changed |
| player_resize | Player container resized |
Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| target | HTMLElement | required | Container element |
| stream | string | required | Stream name |
| host | string | auto-detected | MistServer URL |
| autoplay | boolean | true | Auto-start playback |
| controls | boolean\|"stock" | true | Show controls. "stock" for native browser controls |
| muted | boolean | false | Start muted |
| loop | boolean | false | Loop playback |
| poster | string | — | Poster image URL |
| skin | string\|object | "default" | Skin name or custom skin object |
| fillSpace | boolean | false | Fill parent container |
| width / height | number | — | Fixed dimensions (px) |
| maxwidth / maxheight | number | — | Maximum dimensions (px) |
| forcePlayer | string | — | Force wrapper ("html5", "wheprtc", etc.) |
| forceType | string | — | Force MIME type |
| setTracks | object | — | Initial track selection: {video: 1, audio: 2} |
| ABR_resize | boolean | true | Match video resolution to player size |
| ABR_bitrate | boolean | true | Lower bitrate on playback issues |
| keyControls | boolean\|"focus" | true | Keyboard controls. "focus" = only when focused |
| keyMap | object | — | Override default keyboard bindings (see below) |
| translations | string\|object | — | Language code ("nl") or custom strings object (see below) |
| thumbnails | string | — | URL to a WebVTT thumbnail sprite file |
Features
Accessibility
All interactive controls include ARIA attributes (role, aria-label, tabindex) for screen readers and keyboard navigation. Sliders use role="slider" with live value updates. Loading and error overlays use aria-live regions. Tooltips on every button show the label and keyboard shortcut.
Internationalization
Use a built-in locale by passing a language code:
createPlayer({ target: el, stream: 'live', translations: 'nl' });Built-in locales: en (English), nl (Dutch), de (German), fr (French), es (Spanish).
Or pass a custom object to override individual strings:
createPlayer({
target: el,
stream: 'live',
translations: {
play: 'Afspelen',
pause: 'Pauzeren',
mute: 'Dempen',
fullscreen: 'Volledig scherm',
'exit fullscreen': 'Volledig scherm sluiten',
},
});You can also import locale objects directly and extend them:
import { createPlayer, locales } from '@optimist-video/mistplayer-core';
createPlayer({
target: el,
stream: 'live',
translations: { ...locales.fr, play: 'Jouer' },
});For IIFE usage, locales are available as window.MistLocales:
mistPlay('live', { target: el, translations: MistLocales.de });Read strings programmatically with player.translate('play').
Playback controls
| Key | English default |
|-----|----------------|
| play | Play |
| pause | Pause |
| mute | Mute |
| unmute | Unmute |
| volume | Volume |
| fullscreen | Full screen |
| exit fullscreen | Exit full screen |
| pip | Picture in picture |
| loop | Loop |
| settings | Settings |
| automatic | Automatic |
| none | None |
| live | live |
| current time | Current time |
| total time | Total time |
| seek bar | Seek bar |
| seek forward | Seek forward |
| seek backward | Seek backward |
| -10s | -10s |
| +10s | +10s |
| airplay | AirPlay |
Keyboard overlay feedback
| Key | English default |
|-----|----------------|
| speed | Speed |
| speed doubled | Speed doubled |
| muted | Muted |
| (muted) | (muted) |
| seek backward seconds | - 10 seconds |
| seek forward seconds | + 10 seconds |
| to start | To start.. |
| to end | To end.. |
| frame forward | Frame +1 |
| frame backward | Frame -1 |
Error overlay & casting
| Key | English default |
|-----|----------------|
| reload video | Reload video |
| reload player | Reload player |
| next source | Next source |
| ignore | Ignore |
| player encountered a problem | The player has encountered a problem |
| chromecast encountered a problem | The chromecast has encountered a problem |
| stop casting | Stop casting |
| chromecast | Chromecast |
| select cast device | Select a device to cast to |
| casting to | Casting to |
Track selection
| Key | English default |
|-----|----------------|
| the current track | The current |
| track | Track |
Time formatting
| Key | English default |
|-----|----------------|
| n sec ago | {n} sec ago |
| in n sec | in {n} sec |
| at | at |
Dev skin
| Key | English default |
|-----|----------------|
| logs | Logs |
| player | Player |
| protocol | Protocol |
| language | Language |
| theme | Theme |
| is playing | is playing |
| player control | Player control |
| reload | Reload |
| next combo | Next combo |
Dev diagnostics
| Key | English default |
|-----|----------------|
| playback score | Playback score |
| corrupted frames | Corrupted frames |
| dropped frames | Dropped frames |
| total frames | Total frames |
| decoded audio | Decoded audio |
| decoded video | Decoded video |
| nack | Negative acknowledgements |
| picture losses | Picture losses |
| packets lost | Packets lost |
| packets received | Packets received |
| bytes received | Bytes received |
| local latency | Local latency |
| messages received | Messages received |
| messages sent | Messages sent |
| current bitrate | Current bitrate |
| framerate in | Framerate in |
| framerate out | Framerate out |
Keyboard Controls
| Action | Default keys | Description |
|--------|-------------|-------------|
| togglePlay | k, K, Space | Toggle play/pause. Hold Space for 2x speed |
| seekForward / seekBackward | l/j, ArrowRight/ArrowLeft | Seek +/- 10 seconds |
| volumeUp / volumeDown | ArrowUp/ArrowDown, +/- | Volume +/- 10% |
| toggleFullscreen | f | Toggle fullscreen |
| toggleMute | m | Toggle mute |
| togglePip | i | Toggle picture-in-picture |
| cycleSubtitle | c / C | Cycle subtitles (Shift for reverse) |
| speedUp / speedDown | >/<, ./, | Playback speed (./, when paused = frame step) |
| seekToStart / seekToEnd | Home / End | Jump to start/end |
| seekPercent | 0–9 | Jump to 0%–90% |
Override with keyMap. Set an action to false to disable it:
createPlayer({
target: el,
stream: 'live',
keyMap: {
togglePlay: ['k', 'K'], // remove Space
seekForward: ['ArrowRight'], // only arrow key
togglePip: false, // disable
},
});Thumbnail Previews
Show thumbnail frames when hovering the progress bar. Provide a WebVTT file:
createPlayer({ target: el, stream: 'myvod', thumbnails: '/thumbs.vtt' });Supports both individual images and sprite sheets with xywh= spatial fragments:
WEBVTT
00:00:00.000 --> 00:00:05.000
/thumbs/sprite.jpg#xywh=0,0,160,90
00:00:05.000 --> 00:00:10.000
/thumbs/sprite.jpg#xywh=160,0,160,90AirPlay
On WebKit browsers, an AirPlay button appears automatically when AirPlay devices are detected. No configuration needed.
Audio Gain (Volume Boost)
Scroll the mouse wheel on the volume slider or speaker icon past 100% to boost volume up to 200% via the Web Audio API. The tooltip shows the boosted percentage. Dragging the slider resets gain to 1x.
Mobile
- Double-tap to seek: Tap the left/right side of the video twice to seek -/+ 10 seconds
- Orientation lock: Entering fullscreen locks to landscape; exiting unlocks
- Seek buttons: +10s / -10s buttons appear on small screens (
xs,smbreakpoints)
Responsive Breakpoints
The player sets a data-size attribute on .mistvideo based on width:
| Attribute | Width |
|-----------|-------|
| xs | < 384px |
| sm | 384 – 575px |
| md | 576 – 767px |
| lg | 768 – 959px |
| xl | >= 960px |
At xs, time display and volume slider hide; seek buttons appear. Target these in CSS:
.mistvideo[data-size="xs"] .my-control { display: none; }Theming
CSS Custom Properties
The fastest way to brand the player — pure CSS:
.mistvideo {
--mist-color-accent: #e63946;
--mist-color-surface: rgba(20, 20, 30, 0.95);
--mist-color-text: #f1faee;
}Or at runtime: player.setTheme({ 'color-accent': '#e63946' }).
Preset Themes
| Theme | Dark | Light | Accent |
|-------|:----:|:-----:|--------|
| default | yes | yes | green (#0f0) |
| tokyo-night | yes | yes | blue (#7aa2f7) |
| dracula | yes | — | purple (#bd93f9) |
| nord | yes | — | frost (#88c0d0) |
| catppuccin | yes | yes | mauve (#cba6f7) |
| gruvbox | yes | yes | yellow (#d79921) |
| one-dark | yes | — | blue (#61afef) |
| github-dark | yes | — | blue (#58a6ff) |
| rose-pine | yes | — | iris (#c4a7e7) |
| solarized | yes | yes | cyan (#2aa198) |
| ayu-mirage | yes | — | orange (#ffad66) |
player.setTheme('dracula');
player.setTheme('catppuccin', 'light');The player auto-detects prefers-color-scheme for light/dark mode.
Colors
| Token | Default | Controls |
|-------|---------|----------|
| --mist-color-text | #fff | Text, labels |
| --mist-color-text-muted | rgba(255,255,255,0.5) | Secondary text |
| --mist-color-icon-fill | #fff | SVG icon fill |
| --mist-color-icon-stroke | #fff | SVG icon stroke |
| --mist-color-surface | rgba(0,0,0,0.8) | Control bar, menus, tooltips |
| --mist-color-accent | #0f0 | Active states, progress fill |
| --mist-color-accent-hover | derived | Hover state (via color-mix()) |
| --mist-color-progress-track | #333 | Progress bar background |
| --mist-color-progress-fill | var(--mist-color-accent) | Progress bar filled portion |
| --mist-color-progress-buffer | var(--mist-color-text-muted) | Buffered range |
| --mist-color-border | var(--mist-color-text-muted) | Borders |
| --mist-color-link | var(--mist-color-accent) | Links |
| --mist-icon-stroke-width | 1.5 | SVG stroke thickness |
Typography
| Token | Default |
|-------|---------|
| --mist-font-family | sans-serif |
| --mist-font-size | 14.5px |
| --mist-font-size-sm | 0.9em |
| --mist-font-size-lg | 1.5em |
Spacing & Sizing
| Token | Default |
|-------|---------|
| --mist-space-xs | 2.5px |
| --mist-space-sm | 5px |
| --mist-space-md | 10px |
| --mist-control-height | 42px |
| --mist-icon-size | 22px |
| --mist-progress-height | 2px |
| --mist-progress-height-hover | 10px |
| --mist-radius-sm / md / lg | 0 / 4px / 8px |
| --mist-transition-fast / normal / slow | 0.1s / 0.25s / 0.5s |
TypeScript
Type declarations ship at dist/index.d.ts:
import { createPlayer, type Player, type PlayerOptions } from '@optimist-video/mistplayer-core';
const player: Player = createPlayer({
target: document.getElementById('player')!,
stream: 'live',
translations: { play: 'Start' },
keyMap: { togglePlay: ['k', ' '] },
});
player.state.on('currentTime', (time: number) => console.log(time));Distribution
| Format | File | Served at | Use case |
|--------|------|-----------|----------|
| ESM | dist/index.js | /player.mjs | import in apps/bundlers |
| IIFE | dist/player.iife.js | /player.js | <script> tag, no build step |
Advanced: Extending the Player
The sections below cover skin authoring, custom blueprint development, wrapper registration, and CSS architecture. Most users don't need these.
Register a custom skin, then use it by name:
import { MistSkins } from '@optimist-video/mistplayer-core';
MistSkins.mytheme = {
inherit: 'default',
tokens: {
'--mist-radius-sm': '8px',
'--mist-progress-height': '4px',
},
css: { skin: '/path/to/mytheme.css' },
};
createPlayer({ ..., skin: 'mytheme' });Skin object shape:
{
inherit: 'default', // inherit from another skin
structure: { main: {...} }, // DOM structure tree
blueprints: { ... }, // UI component functions
icons: { blueprints: {...} }, // SVG icon definitions
colors: { ... }, // legacy color tokens (auto-bridged to CSS vars)
tokens: { ... }, // direct --mist-* overrides
css: { skin: 'url' }, // stylesheet URL(s)
}Icon overrides:
MistSkins.mytheme = {
inherit: 'default',
icons: {
blueprints: {
play: { size: 45, svg: '<path d="M10 5 L35 22.5 L10 40 Z" class="fill" />' },
},
},
};Icons use CSS classes: .fill, .stroke, .semiFill, .toggle — colored by --mist-color-icon-* tokens.
Legacy color properties (still supported, auto-bridged to CSS vars):
| Property | CSS custom property |
|----------|-------------------|
| fill | --mist-color-icon-fill |
| semiFill | --mist-color-text-muted |
| stroke | --mist-color-icon-stroke |
| background | --mist-color-surface |
| accent | --mist-color-accent |
Blueprints are factory functions that create UI components. Inside a blueprint, this is the MistVideo instance:
| Property | Description |
|----------|-------------|
| this.video | The <video> element |
| this.api | Normalized playback API (play/pause/seek/volume) |
| this.playerState | Reactive state: .on(prop, cb), .get(prop) |
| this.fullscreen | .supported, .active, .toggle(), .request(), .exit() |
| this.pip | Same as fullscreen |
| this.info | Stream metadata |
| this.options | Player options |
| this.skin.icons.build(type, size) | Build an SVG icon |
| this.container | Root player DOM element |
| this.translate(key, fallback?) | i18n string lookup |
| this.gain | Audio gain: .value (0–2), .init() |
| this.log(msg, type) | Player logging |
| this.timers | Timer management (auto-cleanup) |
Example: Skip Intro button
MistSkins.myplayer = {
inherit: 'default',
blueprints: {
skipIntro: function() {
var MistVideo = this;
var btn = document.createElement('button');
btn.textContent = 'Skip Intro';
btn.style.display = 'none';
this.playerState.on('currentTime', function(time) {
btn.style.display = (time < 45) ? '' : 'none';
});
btn.addEventListener('click', function() { MistVideo.api.currentTime = 45; });
return btn;
}
},
structure: {
main: {
type: 'container', classes: ['mistvideo'],
children: [
{ type: 'videocontainer' },
{ type: 'skipIntro' },
{ type: 'controls' },
{ type: 'loading' },
{ type: 'error' },
],
},
},
};Hide or replace built-in controls:
blueprints: {
volume: function() { return null; }, // hide
play: function() { // replace
var btn = document.createElement('button');
// ... your implementation
return btn;
},
}Structure descriptor:
{
type: 'container', // blueprint type (required)
classes: ['my-class'], // CSS classes
children: [{ type: 'play' }, ...], // child nodes
if: function() { return this.info.hasVideo; }, // conditional
then: { type: 'videocontainer' },
}Available blueprint types:
| Category | Blueprints |
|----------|------------|
| Layout | container, video, videocontainer, secondaryVideo, switchVideo |
| Controls | controls, submenu, hoverWindow, draggable |
| Playback | progress, play, seekBackward, seekForward, speaker, volume, currentTime, totalTime |
| Settings | playername, mimetype, logo, settings, loop, fullscreen, picture-in-picture, tracks |
| Overlay | text, placeholder, timeout, polling, loading, error, tooltip, button |
| Media | subtitles, chromecast, airplay, keyControls |
import { registerWrapper } from '@optimist-video/mistplayer-core';
registerWrapper('myprotocol', {
name: 'My Protocol',
priority: 50,
isMimeSupported: function(mime) { return mime === 'myprotocol/video'; },
isBrowserSupported: function(mime, source, MistVideo) {
if (!window.MyProtocolSupport) return false;
return ['video', 'audio'];
},
player: function() {
this.build = function(MistVideo, callback) {
var video = document.createElement('video');
new MyProtocolConnection(MistVideo.source.url).attachTo(video);
callback(video);
};
this.api = {
play: function() { /* ... */ },
pause: function() { /* ... */ },
get paused() { /* ... */ },
get currentTime() { /* ... */ },
set currentTime(v) { /* ... */ },
get duration() { /* ... */ },
get volume() { /* ... */ },
set volume(v) { /* ... */ },
get muted() { /* ... */ },
set muted(v) { /* ... */ },
unload: function() { /* cleanup */ },
};
},
});Required API: play(), pause(), paused, currentTime (get/set), duration, volume (get/set), muted (get/set). Optional: buffered, unload(), setTracks(obj), ABR_resize(size).
The player CSS uses @layer for specificity management:
@layer mist.tokens → default custom property values
@layer mist.base → general.css (base styles)
@layer mist.skin → default.css / dev.css (skin-specific)Unlayered CSS always wins:
.mistvideo { --mist-color-accent: red; }Or use @layer mist.overrides for organized overrides.
mistPlay() (legacy) — returns the raw MistVideo instance instead of the clean facade:
var mv = mistPlay('live', { target: el }); // legacy
var player = createPlayer({ target: el, stream: 'live' }); // recommendedIIFE globals — when using /player.js, exports are on window: createPlayer, mistPlay, MistThemes, MistSkins, MistUtil, PlayerState, registerWrapper.
$variable CSS migration — old $variable syntax in skin CSS still works via auto-bridge:
| Old | New |
|-----|-----|
| $fill | var(--mist-color-icon-fill) |
| $accent | var(--mist-color-accent) |
| $background | var(--mist-color-surface) |
| $stroke | var(--mist-color-icon-stroke) |
| $progressBackground | var(--mist-color-progress-track) |
