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

@sweet-player/core

v0.9.0

Published

Framework-agnostic HLS video player with built-in UI

Readme

@sweet-player/core

A custom video player built on hls.js. 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

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 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
  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, 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 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 multi-level quality/audio tracks auto-populate menus; also supports custom lists
  • Persistence: Volume/playback rate stored in localStorage; pass id for resume playback
  • Heatmap: Optional "most replayed" curve above the progress bar (pass heatmap)
  • 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)

Quality / Audio Tracks

  • Auto mode (default): HLS multi-level quality/audio tracks auto-populate menus. Selecting "Auto" lets hls.js 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.

Instance API

play() pause() toggle() seek(t) seekBy(±s) setRate(r) setVolume(0-100) setMuted(b) setAspectRatio(r) setQualities(list) setAudioTracks(list) toggleFullscreen() togglePip() screenshot() load(src) setTitle(s) use(plugin) addSettingsRow(section) addContextMenuItem(item, index?) on/off(event, fn) destroy()

Events: ready play pause ended timeupdate ratechange volumechange fullscreenchange pipchange aspectratiochange qualitychange audiotrackchange error destroy

License

MIT