sweet-danmaku
v0.1.2
Published
Lightweight video danmaku (bullet comments) engine for web: scroll/top/bottom modes, React/Vue adapters, sweet-player plugin
Downloads
451
Maintainers
Readme
sweet-danmaku
Lightweight danmaku (bullet comment) engine for web video. Zero runtime dependencies. Works with a plain <video>, and optionally plugs into sweet-player.
One package for all usage modes:
- Core / Vanilla JS:
sweet-danmaku - React:
sweet-danmaku/react - Vue:
sweet-danmaku/vue - sweet-player plugin:
createDanmakuPlugin
Features
- Modes — scroll (right → left), top fixed, bottom fixed
- Track allocation — collision-aware lanes; pending retry when full
- Timeline sync — cursor-based scan on sorted list; seek clears stage; late items are not back-filled
- Runtime controls — opacity / fontSize / speed / display area / filter
- Send & load — batch
load()or realtimesend()(binary insert) - Events —
ready/show/click/destroy(onreturns unsubscribe; latereadyis replayed) - Framework adapters — React hook & Vue composable
- sweet-player plugin — optional settings rows (toggle + opacity), no hard dependency on
@sweet-player/core - Lightweight — ESM/CJS dual build, tree-shakeable, no WASM
Install
npm install sweet-danmakuQuick Start
import { SweetDanmaku } from 'sweet-danmaku'
const video = document.querySelector('video')!
const dm = new SweetDanmaku(video, {
comments: [
{ text: '前排', time: 1 },
{ text: '高能', time: 3, mode: 'top', color: '#ff4d6d' },
],
opacity: 1,
fontSize: 25,
speed: 1,
area: 0.85,
})
// ready 在 microtask 触发,构造后立刻订阅即可
dm.on('ready', () => console.log('danmaku ready', dm.ready))
dm.on('show', (item) => console.log('show', item.text))
dm.on('destroy', () => console.log('destroyed'))
dm.send({ text: '我来了', time: video.currentTime, border: true })Core options (SweetDanmakuOptions)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| comments | DanmakuItem[] | [] | Initial list |
| opacity | number | 1 | 0–1 |
| fontSize | number | 25 | Default font size (px) |
| speed | number | 1 | Scroll speed multiplier |
| fixedDuration | number | 4 | top/bottom hold seconds |
| area | number | 1 | Vertical usable ratio from top (0.1–1) |
| maxTracks | number | auto | Max lanes |
| visible | boolean | true | Initial visibility |
| container | HTMLElement | video parent | Overlay host |
| filter | (item) => boolean | — | Return false to skip |
Core methods
| Method | Description |
|--------|-------------|
| load(comments) | Replace all comments (sorted by time) |
| send(item) | Append one (binary insert); shows immediately if near current time |
| clear() | Clear list + stage |
| show() / hide() | Toggle visibility |
| setOpacity(0–1) | Opacity |
| setFontSize(px) | Default font size |
| setSpeed(n) | Scroll speed multiplier |
| setArea(0.1–1) | Vertical usable ratio from top |
| setFilter(fn?) | Skip items returning false |
| on / off / once | Events |
| destroy() | Teardown (emits destroy first) |
| ready | boolean getter — whether ready already fired |
| layer | Overlay root element |
Events
| Event | Payload | When |
|-------|---------|------|
| ready | void | After construct (microtask). Late subscribers are replayed once. |
| show | DanmakuItem | A bullet starts displaying |
| click | DanmakuItem | User clicks a bullet |
| destroy | void | destroy() called, before listeners are cleared |
| error | Error | Reserved for recoverable failures. Fatal construct errors throw instead. |
DanmakuItem:
{
id?: string
text: string
time: number // video seconds
mode?: 'scroll' | 'top' | 'bottom'
color?: string
fontSize?: number
border?: boolean // self-sent highlight
}React
import { useRef } from 'react'
import { useSweetDanmaku } from 'sweet-danmaku/react'
function Player() {
const videoRef = useRef<HTMLVideoElement>(null)
const { danmaku, ready, send, show, hide } = useSweetDanmaku(videoRef, {
comments: [{ text: 'hello', time: 0 }],
opacity: 1,
fontSize: 25,
onReady: () => console.log('ready'),
onShow: (item) => console.log(item.text),
onClick: (item) => console.log('click', item.text),
// onError: reserved; construct failures throw
})
return (
<div style={{ position: 'relative' }}>
<video ref={videoRef} src="..." controls />
<button
disabled={!ready || !danmaku}
onClick={() => send({ text: 'hi', time: videoRef.current!.currentTime })}
>
Send
</button>
</div>
)
}useSweetDanmaku options
Inherits core options (opacity, fontSize, speed, fixedDuration, area, maxTracks, container, filter, …) plus:
| Option | Type | Description |
|--------|------|-------------|
| comments | DanmakuItem[] | Reloads when reference changes |
| visible | boolean | show/hide when changes |
| onReady | () => void | Ready callback |
| onError | (err) => void | Reserved (error event) |
| onShow | (item) => void | Bullet shown |
| onClick | (item) => void | Bullet clicked |
Returns: { danmaku, ready, load, send, show, hide, clear, setOpacity, setFontSize, setSpeed, destroy }.
Vue
<script setup lang="ts">
import { ref } from 'vue'
import { useSweetDanmaku } from 'sweet-danmaku/vue'
const videoRef = ref<HTMLVideoElement | null>(null)
const { danmaku, ready, send } = useSweetDanmaku(videoRef, {
comments: [{ text: 'hello', time: 0 }],
onReady: () => console.log('ready'),
})
</script>
<template>
<div style="position: relative">
<video ref="videoRef" src="..." controls />
<button v-if="ready" @click="send({ text: 'hi', time: videoRef!.currentTime })">
Send
</button>
</div>
</template>useSweetDanmaku (Vue) options
Same as React, but comments / visible accept Ref / plain values (MaybeRef).
sweet-player plugin
import { SweetPlayer } from '@sweet-player/core'
import { createDanmakuPlugin } from 'sweet-danmaku'
const danmaku = createDanmakuPlugin({
comments: [/* ... */],
opacity: 1,
fontSize: 25,
speed: 1,
area: 0.85,
settings: true, // register toggle + opacity in settings panel (default true)
labels: {
toggle: '弹幕',
opacity: '弹幕透明度',
},
})
const player = new SweetPlayer({
container: '#player',
src: 'https://example.com/video.m3u8',
plugins: [danmaku.plugin],
})
danmaku.send({ text: '来了', time: 0, border: true })
danmaku.hide()
danmaku.getInstance() // SweetDanmaku | null (after apply)CreateDanmakuPluginOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| …core options | | | All SweetDanmakuOptions except container (auto = player.container) |
| comments | DanmakuItem[] | — | Initial list |
| settings | boolean | true | Register settings rows if addSettingsRow exists |
| labels.toggle | string | 弹幕 | Toggle label |
| labels.opacity | string | 弹幕透明度 | Opacity slider label |
Handle methods: plugin, getInstance, load, send, show, hide, clear, setOpacity, setFontSize, setSpeed.
The plugin only duck-types player.container / player.video / player.on / optional addSettingsRow — no package dependency on @sweet-player/core.
Playground
pnpm install
pnpm devOpens Vite at http://localhost:5184.
Release
# bump version in packages/core/package.json, update CHANGELOG.md
pnpm run release:one
# = version:sync → typecheck → build → release:dry → publish| Script | Description |
|--------|-------------|
| pnpm build | Build npm package |
| pnpm test | Vitest |
| pnpm typecheck | tsc --noEmit |
| pnpm release:dry | npm publish dry-run |
| pnpm release | Publish sweet-danmaku to npm |
Release notes template: .github/RELEASE_TEMPLATE.md
Non-goals (for now)
- Built into
@sweet-player/core(stays a separate package) - Server protocol / WebSocket (bring your own; call
send/load) - Canvas / WASM renderer (DOM + measureText first)
License
MIT
