@flow-player/browser-runtime
v0.14.2
Published
Rust/WASM-powered browser video player runtime with FLV/HLS/MP4 demux, WebCodecs decode, WebGL2 render, and HEVC software decode fallback.
Maintainers
Readme
@flow-player/browser-runtime
ESM browser runtime for H.264 / H.265 FLV live and VOD playback with WebCodecs, WebGL2, and a Rust-powered WASM demux core. Zero npm dependencies.
0.4.0 新增(@beta):HEVC 软件解码兜底。
decodeMode: "auto"在浏览器 WebCodecs HEVC 不可用时自动软解;默认decodeMode: "webcodecs"零回归。详见 HEVC 软解(@beta)。
Quick Start (Live)
import { flowLivePlayer } from "@flow-player/browser-runtime";
import PlayerWorker from "@flow-player/browser-runtime/worker?worker";
import wasmUrl from "@flow-player/browser-runtime/wasm/flow_player_wasm.wasm?url";
const player = new flowLivePlayer({
canvas: document.getElementById("player-canvas") as HTMLCanvasElement,
workerFactory: () => new PlayerWorker(),
wasmUrl, // 生产构建必须显式声明(见下方说明)
useWCS: true,
});
player.on("play", () => console.log("playing"));
player.on("stats", (stats) => console.log(stats.fps, "fps"));
player.on("diagnostic", (detail) => console.warn("normalization:", detail));
await player.play("https://example.com/live.flv");接入说明:
wasmUrl和workerFactory在生产构建中必须显式声明。new URL(variable, import.meta.url)自动推断在 dev 模式可用,但生产构建因 bundler 资产检测只识别字面量而失效。详见 Consumer 接入体验优化 PRD。?worker/?url需要/// <reference types="vite/client" />类型声明(vite 模板默认已有)。
Quick Start (VOD)
import { flowVodPlayer } from "@flow-player/browser-runtime";
import PlayerWorker from "@flow-player/browser-runtime/worker?worker";
import wasmUrl from "@flow-player/browser-runtime/wasm/flow_player_wasm.wasm?url";
const player = flowVodPlayer({
canvas: document.getElementById("vod-canvas") as HTMLCanvasElement,
workerFactory: () => new PlayerWorker(),
wasmUrl,
});
player.playList([
{
url: "/vod/1.mp4",
startTimestamp: 0,
endTimestamp: 60000,
timeLen: 60,
fileSize: 1024000,
channelID: 1,
RecordType: 0,
},
]);Worker Setup
The module worker is at @flow-player/browser-runtime/worker.
| Bundler | Import |
| ------------- | ------------------------------------------------------------------------------------------------- |
| Vite | import PlayerWorker from "@flow-player/browser-runtime/worker?worker" |
| webpack 5 | new Worker(new URL("@flow-player/browser-runtime/worker", import.meta.url)) |
| esbuild | new Worker(new URL("@flow-player/browser-runtime/worker", import.meta.url), { type: "module" }) |
WASM 部署
.wasm 产物(~147 KB release)包含在 npm 包的 wasm/flow_player_wasm.wasm,通过 ./wasm/* 子路径暴露。
加载机制
提供两种互斥的 wasm 来源(按优先级):
| 选项 | 模式 | 生产构建 | 适用场景 |
| ----------- | -------------------- | --------------------------------------- | ------------------------ |
| wasmUrl | URL 字符串 | consumer ?url 导入 | 推荐(Vite/Webpack) |
| wasmBytes | BufferSource | consumer ?arraybuffer 导入 | 需绕过 URL(如内联) |
| 不传 | new URL() 自动推断 | 不可用(非字面量不被 bundler 识别) | 仅 dev |
另有独立并行通道 wasmModule(WebAssembly.Module):作为 init payload 独立字段 postMessage 到 worker,与上述选项可共存(Safari fallback:wasmModule 失败时回落到 wasm config)。不是互斥选项的第三选。
Vite / webpack 5(推荐)
用 ?url 导入让 bundler 自动拷贝:
import wasmUrl from "@flow-player/browser-runtime/wasm/flow_player_wasm.wasm?url";
new flowLivePlayer({ wasmUrl /* ... */ });手动部署(其他 bundler)
拷贝 wasm/flow_player_wasm.wasm 到静态资源并传 URL:
new flowLivePlayer({
wasmUrl: "https://cdn.example.com/flow_player_wasm.wasm",
// ...
});API Reference
flowLivePlayer
| Method | Description |
| --------------------------------------------------------------------- | ---------------------------------------- |
| play(url) | Start streaming |
| pause() | Pause |
| close() / destroy() | Stop and release resources |
| screenshot(filename?, format?, quality?, type?) | Capture frame → string \| Blob \| void |
| startRecord(name?, type?) / stopRecordAndSave() / isRecording() | Recording |
| mute() / cancelMute() / setVolume(v) / isMute() | Audio control |
| setScaleMode(m) / setRotate(deg) / resize(w, h) / clearView() | Display |
| setFullscreen(f) / toggleControlBar(b) / getControlBarShow() | Fullscreen |
| setKeepScreenOn() | Prevent screen sleep |
| Property | Type |
| -------------------------------------- | ------------------------------------------------------ |
| metrics | RuntimeMetrics (chunksPushed, firstFrameAt, ...) |
| loaded, playing, muted, volume | State booleans/numbers |
| scaleMode, fullscreen, rotate | Display state |
| Key Events | Payload |
| ------------------------------------------- | --------------------------------------------------------------- |
| play, pause, destroy | void |
| start | void (play request started; not a rendered-frame signal) |
| stats | FlowLivePlayerStats {buf, fps, abps, vbps, ts} |
| videoInfo | FlowLivePlayerVideoInfo {width, height, codec, codecString} |
| error | unknown |
| diagnostic | unknown (stream normalization detail) |
| timeout | FlowLivePlayerTimeoutDetail |
| resize | FlowLivePlayerResizeDetail |
| clearView | FlowLivePlayerClearViewDetail |
| mute / fullscreen | boolean |
| volume | number |
| scaleMode | FlowLivePlayerScaleMode |
| rotate | number |
| recordStart / recordEnd / recordError | unknown |
| recordingTimestamp | number (seconds) |
Live 事件与恢复契约
on、once、off 共用 FlowLivePlayerEventMap,回调参数可自动推导。
off(event, handler) 可用原回调取消普通订阅及一次性订阅;off(event) 清除此事件全部订阅。
事件同步派发,once 在回调重入触发同名事件时也最多执行一次。
const onTime = (timestampMs: number) => console.log(timestampMs);
player.on("timeUpdate", onTime);
player.off("timeUpdate", onTime);play() 返回的 Promise、start 和 play 表示播放请求已启动,不保证首帧。
首帧使用 render 事件或 metrics.firstFrameAt !== null 判断。
timeUpdate 单位是毫秒;recordingTimestamp 单位是秒。
同一 URL 再次 play() 或自动重连会复用已保存的 headers、format、retry、
readTimeoutMs 快照;显式传 {} 可清空配置。切换到不同 URL 且未传 options 时
使用空配置,避免旧源的鉴权信息泄漏到新源。主动播放会取消旧退避任务,
在 retry 回调内主动换流同样生效。双层重试限流和错误触发矩阵见
Live 重连契约。
HEVC 软解(@beta)
⚠️ 商用前置:HEVC/H.265 涉及标准必要专利(HEVC Advance、MPEG LA、独立持有人),无论用哪种 codec 实现,只要发生 HEVC 解码就触发专利授权义务。商用前必须完成专利授权确认。详见 HEVC 专利授权与商用合规。默认
decodeMode: "webcodecs"不加载软解;启用auto/software前请确认授权。
flowLivePlayer 支持 HEVC 软件解码兜底,软解后端由独立 npm 包 @flow-player/software-decoder-ffmpeg(FFmpeg→WASM / LGPL v2.1+)交付,不进入本包(包隔离 release gate 强约束)。接入需额外安装该包。
| decodeMode | 行为 |
| --------------------- | ------------------------------------------------------------------------------------------------------- |
| "webcodecs"(默认) | 仅 WebCodecs 硬解;不加载软解资源,零回归 |
| "auto" | WebCodecs 不可用时自动软解兜底(仅 decoder-capability 错误触发,排除 keyframe/seek/network 暂态错误) |
| "software" | 强制软解(HEVC);不支持的 codec(如 H.264)自动回退 WebCodecs |
import { flowLivePlayer } from "@flow-player/browser-runtime";
import PlayerWorker from "@flow-player/browser-runtime/worker?worker";
import wasmUrl from "@flow-player/browser-runtime/wasm/flow_player_wasm.wasm?url";
import { ffmpegSoftwareDecoderOptions } from "@flow-player/software-decoder-ffmpeg";
const player = new flowLivePlayer({
canvas,
workerFactory: () => new PlayerWorker(),
wasmUrl,
decodeMode: "auto", // @beta
...ffmpegSoftwareDecoderOptions(), // 提供 softwareDecoderUrl + softwareDecoderWorkerUrl
});安装:
npm i @flow-player/browser-runtime @flow-player/software-decoder-ffmpeg软解触发时 metrics.decoderLoadingState 流转:idle → loading → decoder-ready / decoder-error;metrics.decoderWasmMemoryPeakMb 记录 WASM 内存峰值。decode-error 事件携带 classifyWebCodecsError 分类(decoder-capability / keyframe-required / keyframe-wait / seek-flush / network)。
⚠️ 软解包运行时产物内含 FFmpeg 9.0.1 WASM 构建(LGPL v2.1+),分发含软解的产物时请确认 LGPL 合规义务(提供对应源代码 / 重新链接机制)。HEVC 标准必要专利授权独立于软件 license(详见 专利文档)。API 标注
@beta,后续版本可能调整。
flowVodPlayer
| Method | Description |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| playList(segments: FlowVodRecordSegment[]) | Load and start VOD playback |
| seek(timestampMs) | Seek to absolute millisecond timestamp |
| pause() / resume() | Playback control |
| nextSegment() / previousSegment() | Segment navigation |
| nextFrame() / previousFrame() | Frame-stepping |
| setRate(rate) | Playback speed. rate > 0 = forward (e.g. 2 = 2×), rate < 0 = reverse (e.g. -4 = backward 4×, \|rate\| must be 1/2/4/8). 0 rejects. |
| setPlaybackDirection("forward"\|"backward", speed?) | Reverse playback control. Equivalent to setRate(-speed) for backward. |
| setGapPolicy(policy: FlowVodGapPolicy) | Gap handling ("jump" or "hold") |
| syncTo(targetMs, options?) | Multi-player sync. Default smooth trim uses an adaptive absolute delta: max(baseRate × 0.4, 1×); pass smoothRates to override. |
seek() / hard syncTo() targets in a gap or outside the playlist use the configured
gapPolicy consistently instead of entering the pipeline error state. Every seek settles
exactly once via seek-settled with
{ targetMs, actualMs, errorMs, latencyMs, direction, generation, cancelled?, reason? }:
either a real arrival, or a cancelled: true settlement (reason: "superseded" when a
newer seek on the same player took over, "direction-change" when a true direction flip
voided the in-flight seek, "reset" when playList() replaced the list), so multi-player
consumers can stop polling getPrecisePosition() to detect settlement and never stall on a
superseded seek.
方向切换(setPlaybackDirection 真翻转 / setRate 负数)立即下发 worker:directionchange
即代表解码方向开始切换,不再等待在途 seek 落定。在途 seek 被翻转作废时以
seek-settled { cancelled: true, reason: "direction-change" } 结算;同方向仅切档(speed-only)
遇在途 seek 仍延后到落定后生效。
MP4 分片加载、解码或渲染失败时,播放器在保留既有 error 事件的同时派发
segment-load-error。其 detail 包含脱敏后的 segmentUrl / nextSegmentUrl、
segmentIndex、httpStatus、从 0 开始的 retryCount、isLastSegment、layer、
cause 与 message。同一播放列表内同一分片连续失败达到
segmentLoadFailureThreshold(默认 3)后,额外派发一次 unrecoverable-error;该阈值只
用于错误升级观测,播放器不会自动重试或跳过分片。成功出帧或重新 playList() 会清空
连续失败序列和 snapshot.lastLoadError。
import {
flowVodPlayer,
} from "@flow-player/browser-runtime";
const player = flowVodPlayer({
segmentLoadFailureThreshold: 3,
});
player.addEventListener("segment-load-error", (event) => {
const detail = event.detail;
console.warn(detail.cause, detail.httpStatus, detail.retryCount);
});
player.addEventListener("unrecoverable-error", (event) => {
const detail = event.detail;
console.error(detail.segmentUrl, detail.totalRetries, detail.lastError);
});| Property | Type |
| -------------- | ----------------------------------------------------- |
| state | "idle" \| "loading" \| "playing" \| "paused" \| ... |
| rate | number |
| snapshot | FlowVodPlayerSnapshot |
| effectiveSpeed | number(正放为正,倒放为负) |
VOD API 与事件契约
根入口和 /vod 子路径均导出 FlowVodPlayUrlOptions、FlowVodSeekMapping、
FlowVodPlayerEventMap。new FlowVodPlayer(options) 与 flowVodPlayer(options) 等价。
| 操作 / 事件 | 返回值或载荷与含义 |
| --- | --- |
| playList / playUrl / pause / resume / setRate / setPlaybackSpeed | 同步返回 FlowVodPlayerSnapshot,不等待网络或目标帧 |
| seek / nextSegment / previousSegment / nextFrame / previousFrame | 返回 FlowVodSeekMapping 或 null(缺口 hold 或无相邻片段) |
| statechange | 完整快照;UI 状态以此为准 |
| seek | 请求已映射到片段;尚未保证目标帧呈现 |
| seek-settled | 每个 seek 恰好一次:真实落定含 target/actual/error/latency/generation;被顶替/方向切换/列表重置/素材末端时 cancelled: true + reason(actual/error/latency 为占位值) |
| stream-end | 播放列表素材末端到达(state 进入 ended)时一次性派发:{ finalPositionMs, segmentIndex, direction, source }。消费方据此渲染"录像结束"占位,与网络卡顿(error/buffering)区分 |
| timeupdate | { positionMs, rate };位置为绝对时间轴毫秒,rate 为有符号倍率(direction 模式下倒放为负、档位切换立即可见),与 snapshot.rate/effectiveSpeed 同口径 |
| buffering | { buffering } 后端 stall 通知;loading/paused 等状态下可能不改变播放状态 |
| destroy | 销毁完成时的快照;现有契约不额外发送 destroyed 的 statechange |
addEventListener / removeEventListener 根据事件名推导 CustomEvent.detail,
保留标准 EventTarget 的 { once, signal }、对象监听器及自定义事件支持。
异步播放错误通过 error 观测;同步参数错误通过异常反馈。
无效 playList 不改变旧列表、pending seek 或暂停意图。
当前 runtime/worker 通过消息 envelope 的 seekRequestId 隔离旧进度和迟到超时,
旧 worker 缺少该字段时保持原有兼容行为。停止时钟确认不能结算 pending seek;
runtime 的一次 progress 消息只派发一次载荷一致的事件。
seek-error 恢复契约(自动恢复与 recover())
seek 超时以 error { source: "seek", code: "seek-error", targetMs, generation }
派发,失败目标保留在播放器内。恢复配额关系如下:
- 上游自动恢复:每次 seek-error 事故自动以重启管线路径重发同一目标一次
(300ms 退避)。settle 成功或重新
playList()后配额刷新;底层原因不消除时 不会形成重试风暴。 recover():显式恢复入口,每次调用重发最后一次失败目标(无失败记录时 仅重建当前段管线)。与自动恢复配额相互独立——自动恢复已消耗后仍可调用。- 消费方指引:收到 seek-error 后推荐调用一次
recover()而非重建会话; 同一目标连续第 3 次 seek-error(自动恢复与recover()均已失败)说明会话 已僵死,建议重建会话(重新playList()或新建实例)。多路同步场景不要在 syncTo 失败路径上再自行包一层重试——与recover()叠加会形成双层重试。 - 慢供给预算:decoder 重建挂起(等目标段数据到位才能开始重配)的超时预算
与数据供给节奏解耦:供给仍在推进(字节持续到达/请求在途)时挂起等待自动
顺延(每次续期派发
diagnostic"Seek suspension extended",总上限 30s); 只有数据已到位而重配仍未完成,才在一个预算窗(8s)内判seek-error。 NVR 夜间高峰等慢供给场景的初始 seek 不再被 8s 预算误杀。
player.addEventListener("error", (event) => {
const detail = (event as CustomEvent).detail;
if (detail?.code === "seek-error") {
// 推荐动作:一次 recover() 而非重建会话;连续第 3 次失败再重建。
player.recover();
}
});player.addEventListener("seek-settled", (event) => {
console.log(event.detail.actualMs, event.detail.errorMs);
}, { once: true });参数与事件质量门禁:新增或变更公共 API 必须同时通过源码、workspace 声明、
发布生成声明三种入口的 consumer 编译测试,并对事件次数、取消与重入行为补回归。
现有用例为 test/public-api-types.test.ts 与 test/player-events.test.ts,随包单测执行。
FlowVodRecordSegment
interface FlowVodRecordSegment {
url: string;
startTimestamp: number;
endTimestamp: number;
timeLen: number;
fileSize: number;
channelID: number | string;
RecordType: number | string;
}Tree Shaking
Package is "sideEffects": false with per-module output. Modern bundlers (Vite, webpack 5, Rollup, esbuild) will remove unused code automatically. Importing only flowLivePlayer excludes VOD from your bundle.
License
MIT
Development
# Build WASM (requires Rust wasm32-unknown-unknown target)
npm run build:wasm
# Build npm-grade package
npm run build:package
# Verify before publish
npm run publish:check
# Full test suite
npm run test:smoke
# Type check
npm run typecheck