@sweet-player/core
v1.6.0
Published
Framework-agnostic HLS video player with built-in UI
Downloads
173
Maintainers
Readme
@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/corehls.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
idand resume position is kept in sessionStorage for the current tab. To outlive the tab or sync across devices, route it through your own backend withonSavePrefs/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:
pluginsoption orplayer.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/audioTracksfor custom lists; switching triggers callbacks. UsesetQualities()/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-subtitleA 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-gifOnce 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-danmakuThe 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 durationvalue— 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,90Image 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
