@sweet-player/core

Framework-agnostic HLS video player with built-in UI
7,825 25
Install
npm install @sweet-player/core
Latest Version:1.6.0
License:MIT
Last Updated:Aug 4, 2026
Links: GitHub  ·  npm
Maintainer: liuxionghui

@sweet-player/core

A custom video player supporting HLS (hls.js) and MPEG-DASH (dashjs). Zero framework dependency, written in TypeScript. Supports vanilla JS / React / Vue.

Live Demo: player.sweetui.com

React wrapper: @sweet-player/react. Vue wrapper: @sweet-player/vue.

Install

npm install @sweet-player/core

hls.js and dashjs are loaded on demand: .m3u8 sources pull in hls.js, .mpd sources pull in dashjs, any other source plays through the native <video>. Only the engine you actually use is downloaded.

Or include via <script> tag (IIFE build):

<script src="https://unpkg.com/@sweet-player/core/dist/sweet-player.global.js"></script>
<script>
  const player = new SweetPlayer({ container: '#player', src: '...' });
</script>

Usage

import { SweetPlayer } from '@sweet-player/core';

const player = new SweetPlayer({
  container: '#player',          // Element or CSS selector
  src: 'https://example.com/video.m3u8',
  title: 'Video Title',
  id: 'ep-01',                   // Enables resume playback from last position
  volume: 80,                    // 0-100 (localStorage preference takes priority)
  seekStep: 10,                  // Seek step in seconds
  longSeek: { steps: [10, 30, 60], stepUpInterval: 2000 },
  playbackRates: [0.5, 1, 1.5, 2],
  autoQuality: true,             // Default true: auto-populate quality menu from HLS/DASH levels (includes "Auto")
  persist: true,                 // Default true: remember volume/mute/playback rate in localStorage
  autoNext: 5,                   // Auto-play next after 5s countdown on ended (requires onNext)
  locale: 'en',                  // Built-in: 'zh-CN' / 'en'; extend with registerLocale
  hlsConfig: {},                 // Passed through to `new Hls(config)` for .m3u8 sources
  dashConfig: {},                // Passed through to dashjs `updateSettings` for .mpd sources
  onPrev: () => {},
  onNext: () => {},
  onQualityChange: (q) => {},
  onAudioTrackChange: (t) => {},
});

player.on('timeupdate', ({ currentTime, duration }) => {});
player.destroy();

Features

  • Full UI controls: Play/pause, seek, progress bar (drag + buffer), playback rate, quality, aspect ratio, audio track, volume, browser & web fullscreen, PiP
  • Keyboard shortcuts: Space for play/pause, arrow keys for seeking (hold for accelerating seek 10→30→60 s/s), ↑↓ for volume, F for browser fullscreen, W for web fullscreen (works inside iframes without allow="fullscreen"), M for mute
  • Touch gestures: Horizontal swipe to seek, right-half vertical swipe for volume, double-tap to seek/fullscreen, single tap to toggle controls
  • Auto quality: HLS / DASH multi-level quality/audio tracks auto-populate menus; also supports custom lists
  • Persistence: Volume/playback rate stored in localStorage; pass id and resume position is kept in sessionStorage for the current tab. To outlive the tab or sync across devices, route it through your own backend with onSavePrefs / onSaveProgress + player.restore() — throttling, save timing and failure handling stay inside the player
  • Heatmap: Optional "most replayed" curve above the progress bar (pass heatmap)
  • Poster & preview thumbnails: Cover image before playback (poster), progress-bar hover preview from a WebVTT track (thumbnails)
  • State overlays: Buffering spinner, error retry, ended replay + auto-next countdown
  • i18n: Built-in zh-CN/en, custom languages supported
  • Plugin system: plugins option or player.use(plugin) for runtime installation
  • Theming: Override CSS variables like --sp-accent
  • Context menu: Custom right-click menu with changelog (current version, links to npm), video info (YouTube-style stats panel), shortcuts panel, screenshot
  • Hide controls: hiddenControls: ['ratio', 'audioTrack', ...] to hide specific features (UI only, API and shortcuts unaffected)

Scope

Built for on-demand video. DRM / encrypted playback and live streaming are deliberately out of scope and unsupported — for those, use Shaka Player or Video.js. A live source will play, but the progress bar stays at 0% and the duration reads 0:00, since the UI assumes a fixed, known length. Issues and feature requests in this area won't be worked on.

Quality / Audio Tracks

  • Auto mode (default): HLS / DASH multi-level quality/audio tracks auto-populate menus. Selecting "Auto" lets the underlying engine's ABR decide.
  • Manual mode: Pass qualities / audioTracks for custom lists; switching triggers callbacks. Use setQualities() / setAudioTracks() to update at runtime.

Plugins

import type { SweetPlayerPlugin } from '@sweet-player/core';

const myPlugin: SweetPlayerPlugin = {
  name: 'my-plugin',
  apply(player) {
    // player.video / player.container / player.on available
    return () => { /* cleanup on destroy */ };
  },
};

new SweetPlayer({ ..., plugins: [myPlugin] });
// Or at runtime: player.use(myPlugin);

sweet-subtitle Integration

npm install sweet-subtitle

A plugin factory holds the subtitle instance and exposes a switching API. Swap subtitles at runtime without rebuilding the player:

import { SweetSubtitle } from 'sweet-subtitle';
import type { SweetPlayerPlugin } from '@sweet-player/core';

function createSubtitlePlugin(src?: string) {
  let sub: SweetSubtitle | null = null;
  const plugin: SweetPlayerPlugin = {
    name: 'sweet-subtitle',
    apply(player) {
      sub = new SweetSubtitle(player.video, src ? { src } : {});
      return () => { sub?.destroy(); sub = null; };
    },
  };
  return {
    plugin,
    load: (url: string) => sub?.loadFromUrl(url),
    show: () => sub?.show(),
    hide: () => sub?.hide(),
    setOffset: (s: number) => sub?.setOffset(s),
  };
}

const subtitle = createSubtitlePlugin('/subs/ep-01.ass');
const player = new SweetPlayer({ ..., plugins: [subtitle.plugin] });

await subtitle.load('/subs/ep-02.ass');
subtitle.hide();

You can also manage it independently: new SweetSubtitle(player.video, ...) — just call sub.destroy() before destroying the player.

sweet-player-gif GIF Capture

npm install sweet-player-gif

Once the plugin is registered, a "Capture GIF" option appears in the context menu. Click to capture the last N seconds as a GIF download:

import { SweetPlayerGif } from 'sweet-player-gif';
import type { SweetPlayerPlugin } from '@sweet-player/core';

function createGifPlugin(duration = 3): SweetPlayerPlugin {
  return {
    name: 'sweet-player-gif',
    apply(player) {
      const gif = new SweetPlayerGif(player.video, { duration, fps: 10, maxWidth: 480 });
      let started = false;

      const offPlay = player.on('play', () => {
        if (!started) { gif.start(); started = true; }
      });

      const removeMenu = player.addContextMenuItem({
        label: 'Capture GIF',
        async onClick() {
          if (!started) { gif.start(); started = true; }
          const blob = await gif.capture();
          const a = document.createElement('a');
          a.href = URL.createObjectURL(blob);
          a.download = `capture-${Date.now()}.gif`;
          a.click();
          URL.revokeObjectURL(a.href);
        },
      }, 1);

      return () => { offPlay(); removeMenu(); gif.destroy(); };
    },
  };
}

const player = new SweetPlayer({ ..., plugins: [createGifPlugin(3)] });

Without the plugin, the context menu item won't appear and the core bundle size stays unchanged. See sweet-player-gif docs for options.

sweet-danmaku Danmaku (Bullet Comments)

npm install sweet-danmaku

The built-in plugin factory adds a danmaku overlay synced with video playback, with toggle and opacity controls in the settings panel:

import { createDanmakuPlugin } from 'sweet-danmaku';

const danmaku = createDanmakuPlugin({
  speed: 1,
  area: 0.5,
  comments: [
    { text: 'Hello!', time: 1 },
    { text: 'Great scene', time: 5, color: '#ff4d6d' },
  ],
});

const player = new SweetPlayer({ ..., plugins: [danmaku.plugin] });

danmaku.send({ text: 'New comment', time: player.video.currentTime });

See sweet-danmaku docs for all options.

Heatmap (Most replayed)

Pass heatmap to show a "most replayed" curve above the progress bar. It appears when you hover the progress bar, and can be toggled from the settings panel.

new SweetPlayer({
  container: '#player',
  src: '.../video.m3u8',
  heatmap: [
    { time: 0, value: 3201 },   // time in seconds; value is any non-negative number
    { time: 5, value: 8850 },
    { time: 10, value: 4120 },
  ],
});
  • time — seconds; mapped onto the progress bar using the video duration
  • value — replay/heat intensity, any non-negative number (normalized internally by the max)
  • Denser samples produce a smoother curve

Fetch aggregated play counts from your backend, then create the player — the response is a plain JSON array where value can be the raw count per time bucket:

const heatmap = await fetch(`/api/videos/${id}/heatmap`).then((r) => r.json());
new SweetPlayer({ container: '#player', src, heatmap });

To disable it entirely (no curve logic initialized), add 'heatmap' to hiddenControls.

Poster & Preview Thumbnails

poster sets a cover image shown before playback starts:

new SweetPlayer({ container: '#player', src, poster: '/poster.webp' });

thumbnails shows a preview image when hovering the progress bar. Point it at a WebVTT file where each cue's payload is an image URL, optionally with a #xywh=x,y,w,h fragment to crop a region out of a sprite sheet:

new SweetPlayer({ container: '#player', src, thumbnails: '/thumbs.vtt' });
WEBVTT

00:00:00.000 --> 00:00:10.000
sprite.jpg#xywh=0,0,160,90

00:00:10.000 --> 00:00:20.000
sprite.jpg#xywh=160,0,160,90

Image URLs in the VTT are resolved relative to the VTT file's own URL. To disable it entirely, add 'thumbnails' to hiddenControls.

Copy Video URL

The right-click menu's Copy video URL item copies a link to the clipboard and flashes a confirmation, like taking a screenshot. It copies the current page URL (location.href) by default; pass shareUrl to copy a custom link, or call player.copyShareUrl() yourself:

new SweetPlayer({ container: '#player', src, shareUrl: 'https://example.com/watch?v=123' });

Instance API

Methods

Method Description
play() Start playback. Returns a Promise.
pause() Pause playback.
toggle() Toggle play / pause.
seek(time) Jump to an absolute time in seconds.
seekBy(delta) Seek by a relative offset in seconds (positive or negative).
setRate(rate) Set playback rate, e.g. 1.5.
setVolume(volume) Set volume in the 0–100 range.
setMuted(muted) Mute or unmute.
setAspectRatio(ratio) 'original' | '21:9' | '16:9' | '4:3'.
setQualities(list, active?) Replace the quality list; optionally set active.
setAudioTracks(list, active?) Replace the audio-track list; optionally set active.
toggleFullscreen() Enter / exit browser fullscreen (Fullscreen API).
toggleWebFullscreen() Enter / exit web fullscreen — CSS-only, fills the browser viewport. Works inside iframes without allow="fullscreen".
togglePip() Enter / exit Picture-in-Picture.
setLoop(loop) Toggle loop playback. When on, the browser does not fire ended, so autoNext and plugins depending on ended won't trigger.
restore(state) Apply saved { volume?, muted?, rate?, time? }. Safe to call before metadata loads — the seek waits.
screenshot() Copy the current frame to the clipboard, or download it.
copyShareUrl() Copy the video URL (shareUrl, or location.href) to the clipboard.
load(src) Load a new source without recreating the player.
setTitle(title) Update the top-left title text.
use(plugin) Install a plugin at runtime; returns an uninstall function.
addSettingsRow(section) Register a settings-panel row; returns a remove function.
addContextMenuItem(item, index?) Register a right-click menu item; returns a remove function.
on(event, fn) Subscribe to an event; returns an unsubscribe function.
off(event, fn) Remove an event listener.
destroy() Tear down the player and release all resources.

Events

Event Payload Fires when
ready — Metadata is loaded and the player is ready to play.
play — Playback starts or resumes.
pause — Playback pauses.
ended — Playback reaches the end of the video.
timeupdate { currentTime, duration } The current playback position changes.
ratechange number (new rate) The playback rate changes.
volumechange { volume, muted } Volume or mute state changes.
fullscreenchange boolean (is fullscreen) Browser fullscreen state changes.
webfullscreenchange boolean (is web fullscreen) Web fullscreen state changes.
pipchange boolean (is in PiP) Picture-in-Picture state changes.
loopchange boolean (is loop on) Loop toggle changes.
aspectratiochange AspectRatio The forced aspect ratio changes.
qualitychange QualityLevel Quality is switched.
audiotrackchange AudioTrackInfo Audio track is switched.
error { type, detail? } Playback or network error occurs.
destroy — destroy() is called.

License

MIT

Related Packages

@vue-player/core

Framework-agnostic video player engine powering @vue-player/vue

5,394 32
@sweet-player/vue

Vue component for sweet-player

6,615 25
nplayer

powerful danmaku video player

7,545 1,394
@nplayer/danmaku

powerful danmaku video player

2,862 1,394
@nplayer/vue

powerful danmaku video player

1,175 1,395

Version History

Version Released License
1.6.0 MIT
1.5.1 MIT
1.5.0 MIT
1.4.0 MIT
1.3.0 MIT