npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

npm install @optimist-video/mistplayer-core

Or 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();                          // unsubscribe

Available 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 | 09 | 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,90

AirPlay

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, sm breakpoints)

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' });  // recommended

IIFE 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) |