docs(site): [SITE-04] document PiP and ASR contracts in both languages

This commit is contained in:
Harvey Zhao committed 2026-09-16 02:45:57 +08:00
1 parent 3138812610
commit 94adb77fd6
97 files changed
+4318 -1817

No files matched your search

+11 -2
View File
@@ -17,6 +17,8 @@ not a VitePress page. Keep both URL surfaces compatible.
| `docs/plugin/vtt-thumbnail.md`, `docs/en/plugin/vtt-thumbnail.md` | Sprite index format, timing boundaries, asynchronous registration and types | `/document/plugin/vtt-thumbnail.html`, `/document/en/plugin/vtt-thumbnail.html` |
| `docs/plugin/chapter.md`, `docs/en/plugin/chapter.md` | Chapter ranges, caller-data mutation, replacement updates, cleanup and types | `/document/plugin/chapter.html`, `/document/en/plugin/chapter.html` |
| `docs/plugin/ambilight.md`, `docs/en/plugin/ambilight.md` | Sampling options, start/stop ownership, pixel access and compatible type entries | `/document/plugin/ambilight.html`, `/document/en/plugin/ambilight.html` |
| `docs/plugin/document-pip.md`, `docs/en/plugin/document-pip.md` | Window ownership, video fallback, events and compatible async type views | `/document/plugin/document-pip.html`, `/document/en/plugin/document-pip.html` |
| `docs/plugin/asr.md`, `docs/en/plugin/asr.md` | PCM/WAV callbacks, subtitle rendering, audio routing, lifecycle and runtime types | `/document/plugin/asr.html`, `/document/en/plugin/asr.html` |
| `docs/.vitepress/config.js` | Navigation, base URL, output path, page head | Repository `docs/document/` |
| `docs/vite.config.ts` | Search plugin configuration | VitePress/Vite integration; real search acceptance remains pending |
| `docs/public/main.js` | Run Code links and first-visit language redirect | Copied into the built site |
@@ -30,11 +32,18 @@ and run `node refactor/scripts/demos.mjs --check`. Preserve the frozen BASE-04
inventory. Also refresh and check the current site inventory; the two inventories
serve different purposes. Neither path check replaces browser navigation tests.
The generated site lives in `docs/document/`. The current inventory contains 40 Markdown pages: 20 Chinese
and 20 English. Danmuku, HLS, DASH, Audio Track, VTT Thumbnail, Chapter and Ambilight have dedicated guides and navigation in both languages. Other plugins currently
The generated site lives in `docs/document/`. The current inventory contains 44 Markdown pages: 22 Chinese
and 22 English. Danmuku, HLS, DASH, Audio Track, VTT Thumbnail, Chapter, Ambilight, Document PiP and ASR have dedicated guides and navigation in both languages. Other plugins currently
use their package README, architecture notes and linked editor examples;
navigation links alone are not complete API documentation.
Document PiP keeps its required root factory and void historical result types;
the guide explains the accurate opt-in views without inventing a runtime subpath.
ASR documents the actual `/runtime` entry, nonterminal stop, direct/capture audio
ownership, CORS and trusted HTML rendering. Its embedded `asr.local` demo uses
simulated subtitles and never claims to run a recognition service. Preserve both
original examples and the distinction between navigation and media verification.
Chapter and Ambilight guides preserve their original demo code and explicitly
document the mutation, lifecycle and historical type boundaries. Keep their
language pairs together; the content mapping is in `refactor/site-content-review.md`.
@@ -79,7 +79,8 @@ const config = {
{ text: 'VAST 广告', link: 'https://artplayer.org/?libs=./uncompiled/artplayer-plugin-vast/index.js&example=vast' },
{ text: '视频章节', link: '/plugin/chapter' },
{ text: '视频背光', link: '/plugin/ambilight' },
{ text: '文档画中画', link: 'https://artplayer.org/?libs=./uncompiled/artplayer-plugin-document-pip/index.js&example=document.pip' },
{ text: '文档画中画', link: '/plugin/document-pip' },
{ text: '音频采集与识别字幕', link: '/plugin/asr' },
{ text: '独立音轨', link: '/plugin/audio-track' },
],
},
@@ -151,7 +152,8 @@ const config = {
{ text: 'VAST', link: 'https://artplayer.org/?libs=./uncompiled/artplayer-plugin-vast/index.js&example=vast' },
{ text: 'Video Chapters', link: '/en/plugin/chapter' },
{ text: 'Video Ambilight', link: '/en/plugin/ambilight' },
{ text: 'Document PIP', link: 'https://artplayer.org/?libs=./uncompiled/artplayer-plugin-document-pip/index.js&example=document.pip' },
{ text: 'Document PIP', link: '/en/plugin/document-pip' },
{ text: 'Audio Capture and Subtitles', link: '/en/plugin/asr' },
{ text: 'Audio Track', link: '/en/plugin/audio-track' },
],
},
@@ -0,0 +1,151 @@
# Audio Capture and Recognition Subtitles
[简体中文](../../plugin/asr.md)
Capture audio from the player's video, pass PCM/WAV chunks to your recognition callback, and display the returned subtitles. The plugin includes no recognition model or network service and does not access the microphone. This page describes the unreleased refactor branch; the new type entry and capture option are not claims about the live release.
## Installation and local example
```sh
yarn add artplayer artplayer-plugin-asr
```
```js
import Artplayer from 'artplayer';
import artplayerPluginAsr from 'artplayer-plugin-asr';
```
For script usage, load ArtPlayer before `dist/artplayer-plugin-asr.js`. The global is `artplayerPluginAsr`. This preserves the original `asr.local` example: it captures the site's sample video and displays statistics. Subtitles are simulated, audio is not uploaded, and the example does not measure recognition accuracy. Integrate your own recognition service inside the callback.
<div className="run-code" data-libs="./uncompiled/artplayer-plugin-asr/index.js">▶ Run Code</div>
```js
/* global Artplayer, artplayerPluginAsr */
// Local audio capture demo. The subtitles below are simulated, not recognized speech.
// No audio is uploaded; only the sample media is loaded from this local site.
const statistics = document.createElement('div')
statistics.textContent = 'Local ASR demo: press play. No recognition service is used.'
let chunks = 0
let pcmBytes = 0
let wavBytes = 0
const art = new Artplayer({
container: '.artplayer-app',
url: '/assets/sample/steve-jobs.mp4',
autoSize: true,
fullscreen: true,
fullscreenWeb: true,
layers: [{
name: 'asr-local-statistics',
html: statistics,
style: {
position: 'absolute',
top: '12px',
left: '12px',
right: '12px',
padding: '8px 12px',
background: 'rgba(0, 0, 0, 0.65)',
color: '#fff',
fontSize: '12px',
whiteSpace: 'pre-line',
pointerEvents: 'none',
},
}],
controls: [{
name: 'asr-local-stop',
position: 'right',
html: 'Stop ASR',
tooltip: 'Stop capture; pause and play to restart',
async click() {
await art.plugins.artplayerPluginAsr.stop()
if (!art.isDestroy)
statistics.textContent = 'Local capture stopped. Pause and play to restart. Nothing was uploaded.'
},
}],
plugins: [artplayerPluginAsr({
length: 2,
interval: 250,
sampleRate: 16000,
autoHideTimeout: 5000,
onAudioChunk({ pcm, wav }) {
if (art.isDestroy)
return
chunks++
pcmBytes += pcm.byteLength
wavBytes += wav.byteLength
const sampleRate = new DataView(wav).getUint32(24, true)
const samples = new DataView(pcm)
let peak = 0
for (let offset = 0; offset < pcm.byteLength; offset += 2)
peak = Math.max(peak, Math.abs(samples.getInt16(offset, true)))
const duration = (pcmBytes / 2 / sampleRate).toFixed(2)
statistics.textContent = [
'Local capture only - simulated subtitles, no speech recognition',
`Chunks: ${chunks} | ${sampleRate} Hz mono PCM16 | ${duration} seconds captured`,
`PCM: ${pcmBytes} bytes | WAV: ${wavBytes} bytes | Current peak: ${peak}`,
].join('\n')
return `Simulated local subtitle: audio chunk ${chunks} received.`
},
})],
})
```
## Options and audio chunks
| Field | Default | Meaning |
| --- | --- | --- |
| `length` | `3` | Number of final nonempty punctuation-separated segments displayed from the current subtitle, not seconds of audio |
| `interval` | `100` | Consumption timer interval and target audio duration per chunk, in milliseconds |
| `sampleRate` | `16000` | Requested sample rate in Hz; requires environment support |
| `autoHideTimeout` | `10000` | Delay before hiding an accepted subtitle, in milliseconds |
| `onAudioChunk` | Function returning `null` | Receives `{ pcm, wav }`; a returned string displays subtitles; Promises are supported |
| `audioInput` | Unset | Direct Web Audio by default; `{ type: 'capture' }` explicitly selects a captured stream |
Both `pcm` and `wav` are `ArrayBuffer` values. Audio comes from the first channel. PCM is signed 16-bit little-endian; WAV adds a 44-byte mono header. A chunk contains `Math.floor(sampleRate * interval / 1000)` samples, requiring at least one sample and a positive, finite interval. Audio availability, browser scheduling and recognition latency affect callback frequency; exact timing is not guaranteed.
At most one recognition callback is pending within the active capture generation. Partial chunks stay queued. Rejected callbacks log an error and allow later processing. A backlog exceeding the larger of about one minute of audio or two chunks pauses capture and logs an error to bound memory. Pause, source changes, stop and destroy invalidate old results. They do not cancel network requests already sent by your callback; manage those resources in your application.
## Subtitles and lifecycle
Access the result through `art.plugins.artplayerPluginAsr`; its `name` is always `artplayerPluginAsr`:
| Method | Behavior |
| --- | --- |
| `append(text)` | Returns `undefined` synchronously; displays the final `length` segments of this text and resets the hide timer, replacing earlier subtitles rather than accumulating history |
| `hide()` | Returns `undefined` synchronously; hides subtitles without stopping capture or clearing their text |
| `stop()` | Actually returns `Promise<void>`; stops the current capture and discards old results; a later play event can restart it |
Both `append` and returned strings preserve historical HTML rendering and are not automatically escaped. Supply trusted subtitles; escape or sanitize external recognition text in your application. `null`, `undefined` and other non-string callback results do not update subtitles. `stop()` does not immediately hide existing subtitles; their original auto-hide deadline remains active.
Play starts capture; pause stops recording and discards obsolete work. Source changes rebuild the applicable capture state, and media errors use a nonterminal stop. The default direct connection retains the video's audio output route until `art.destroy()`, allowing playback after stop. Destroy releases subscriptions, timers, recording resources and audio contexts. There is no separate public plugin `start()` or `destroy()`.
## Audio routing and cross-origin media
The default route reads `art.video`, not the independent Audio Track plugin's audio. The video's own volume and mute affect captured data. Cross-origin media requires `moreVideoAttr: { crossOrigin: 'anonymous' }` before loading and appropriate server CORS headers. Playable media is not necessarily readable by Web Audio: without access, the clock can advance while output is silent, samples are zero or no chunks arrive. Cross-origin redirects are also restricted.
If your application already owns a Web Audio graph, explicitly choose `audioInput: { type: 'capture' }`. It uses `captureStream/mozCaptureStream`, does not take over the existing output, and never falls back to a direct connection on failure. It releases only its own contexts and captured tracks. Pause retains this graph; stop or source changes release it, and later playback obtains a fresh stream. Unsupported capability or initialization failure is logged and resources are cleaned up.
Captured data can remain nonzero despite video volume or mute and does not include an external effects graph or independent audio mix. CORS still applies: capture may be rejected, or a track may exist without readable audio. Desktop results do not establish Safari, mobile, every proxy or physical speaker behavior.
## TypeScript entries
The root preserves historical `AsrPluginOption`, `AsrPluginInstance` and `AudioChunk` types: callbacks return `void | Promise<void>`, and stop returns void. To type returned recognition strings, await stop or select captured input, use `/runtime`, which points to the same implementation:
```ts
import asr from 'artplayer-plugin-asr/runtime';
import type { RuntimeResult } from 'artplayer-plugin-asr/runtime';
const installAsr = asr({
audioInput: { type: 'capture' },
onAudioChunk({ pcm, wav }) {
console.log(pcm.byteLength, wav.byteLength);
return null; // Replace with your recognizer; null displays no subtitle.
},
});
async function stopAsr(plugin: RuntimeResult): Promise<void> {
await plugin.stop();
}
```
Precise types are `RuntimeOption`, `RuntimeResult` and `RuntimeFactory`, with `AudioChunk` also exported. Some NodeNext ESM consumers retain the old root namespace shape; choose `/runtime` for a callable default import. CommonJS runtime supports the function itself and its `.default` self-alias; `/runtime` also supports TypeScript `import = require`.
@@ -0,0 +1,95 @@
# Document Picture-in-Picture
[简体中文](../../plugin/document-pip.md)
Move the whole player, including its controls, into a browser Document Picture-in-Picture window. Closing the window restores the same player node to its original position. This page describes the unreleased refactor branch; the online example and unpinned npm/CDN packages are not the current candidate.
## Installation and example
```sh
yarn add artplayer artplayer-plugin-document-pip
```
```js
import Artplayer from 'artplayer';
import artplayerPluginDocumentPip from 'artplayer-plugin-document-pip';
```
For script usage, load ArtPlayer before `dist/artplayer-plugin-document-pip.js`. The global is `artplayerPluginDocumentPip`. The following code preserves the original [online example](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-document-pip/index.js&example=document.pip).
<div className="run-code" data-libs="./uncompiled/artplayer-plugin-document-pip/index.js">▶ Run Code</div>
```js
// npm i artplayer-plugin-document-pip
// import artplayerPluginDocumentPip from 'artplayer-plugin-document-pip';
const art = new Artplayer({
container: '.artplayer-app',
url: '/assets/sample/video.mp4',
plugins: [
artplayerPluginDocumentPip({
width: 480,
height: 270,
fallbackToVideoPiP: true,
placeholder: `Playing in Document Picture-in-Picture`,
}),
],
})
art.on('document-pip', (state) => {
console.log('Document Picture-in-Picture', state)
})
```
## Options
| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `width` | `number` | `480` | Requested window width; the browser determines the actual size |
| `height` | `number` | `270` | Requested window height |
| `placeholder` | `string` | `'Playing in Document Picture-in-Picture'` | Placeholder text at the original player position |
| `fallbackToVideoPiP` | `boolean` | `true` | Try setting `art.pip = true` when the Document PiP API is absent |
The plugin registers as `artplayerPluginDocumentPip` and adds a PiP control button. It detects `documentPictureInPicture.requestWindow`; a positive result does not guarantee that permissions, the calling context or user activation will allow opening. Call `open()` or `toggle()` directly from a user click handler, before awaiting unrelated work.
Video PiP fallback also depends on the browser and media. It does not move the whole player, set the plugin's `isActive` flag or emit a Document PiP activation event. `close()` manages the Document PiP window; use `art.pip = false` to exit video PiP. Disabling fallback does not add Document PiP support to the browser.
## State, methods and events
Access the result through `art.plugins.artplayerPluginDocumentPip`:
| Member | Runtime behavior |
| --- | --- |
| `name` | Always `artplayerPluginDocumentPip` |
| `isSupported` | Readonly getter; Document PiP API capability snapshot taken at plugin creation |
| `isActive` | Readonly getter; whether a Document PiP session is held, not video PiP state |
| `open()` | Returns `Promise<void>`; requests a window and moves the player, coalesces pending requests and does nothing when already open |
| `close()` | Returns `Promise<void>`; cancels a pending request or restores the node and closes the window |
| `toggle()` | Returns `undefined` synchronously; opens or closes the active/pending window |
Successful activation and normal closure emit the player's `document-pip` event with `true` and `false`, respectively. The plugin updates the existing `artplayer-document-pip` class, rebinds document events and schedules resize. This event does not signal successful media loading or playback.
Normal window request/restoration failures display a notice and console warning; awaiting `open()` alone is not proof that a window opened. An error thrown by the video PiP fallback setter can still reject its Promise. A window arriving after cancellation is closed without adopting the player. Destroying the player releases the window, control, subscriptions and timers, suppressing further plugin state events. There is no separate public plugin `destroy()`.
Styles are copied from the player's document on a best-effort basis. Inaccessible cross-origin styles and externally managed DOM need validation in your environment. Canvas, other proxies, keyboard focus and continuous playback also need their own checks; the capability flag cannot establish those results.
## Compatible TypeScript views
The root and `/legacy` declarations preserve the old required options object, writable `Result` flags and void actions. JavaScript permits omitted options, the runtime flags are readonly getters, and `open/close` return Promises. The old shape preserves consumer and replacement-function compatibility; it does not make the runtime getters writable.
This package has no `/runtime` subpath. For accurate types, explicitly view the real factory as `RuntimeFactory`:
```ts
import documentPip from 'artplayer-plugin-document-pip';
import type { AsyncResult, RuntimeFactory } from 'artplayer-plugin-document-pip';
const runtimeFactory = documentPip as RuntimeFactory;
const installPip = runtimeFactory();
const installDefaultPip = runtimeFactory.default({ width: 480 });
async function closePip(pip: AsyncResult): Promise<void> {
await pip.close();
}
```
Named types are `Option`, `Result`, `AsyncResult`, `Factory` and `RuntimeFactory`. Apply the precise view only to the unmodified implementation, not a void-returning mock or replaced method. CommonJS runtime supports both `require(package)(options)` and `.default(options)`. Historical `import = require` types use `.default`; use the precise factory view when direct calling is needed.
@@ -0,0 +1,151 @@
# 音频采集与识别字幕
[English](../en/plugin/asr.md)
从播放器视频采集音频,将 PCM/WAV 片段交给应用提供的识别回调,并显示回调返回的字幕。插件不内置识别模型或网络服务,也不读取麦克风。本页描述当前未发布的重构分支;新增类型入口和采集选项不等于线上版本已具备这些能力。
## 安装和本地示例
```sh
yarn add artplayer artplayer-plugin-asr
```
```js
import Artplayer from 'artplayer';
import artplayerPluginAsr from 'artplayer-plugin-asr';
```
使用 script 时先加载 ArtPlayer,再加载 `dist/artplayer-plugin-asr.js`,全局名为 `artplayerPluginAsr`。以下保留 `asr.local` 原始示例:只采集本地站点的样本视频并显示统计,字幕是模拟文字,不上传音频,也不代表识别准确率。识别服务由应用在回调内自行接入。
<div className="run-code" data-libs="./uncompiled/artplayer-plugin-asr/index.js">▶ Run Code</div>
```js
/* global Artplayer, artplayerPluginAsr */
// Local audio capture demo. The subtitles below are simulated, not recognized speech.
// No audio is uploaded; only the sample media is loaded from this local site.
const statistics = document.createElement('div')
statistics.textContent = 'Local ASR demo: press play. No recognition service is used.'
let chunks = 0
let pcmBytes = 0
let wavBytes = 0
const art = new Artplayer({
container: '.artplayer-app',
url: '/assets/sample/steve-jobs.mp4',
autoSize: true,
fullscreen: true,
fullscreenWeb: true,
layers: [{
name: 'asr-local-statistics',
html: statistics,
style: {
position: 'absolute',
top: '12px',
left: '12px',
right: '12px',
padding: '8px 12px',
background: 'rgba(0, 0, 0, 0.65)',
color: '#fff',
fontSize: '12px',
whiteSpace: 'pre-line',
pointerEvents: 'none',
},
}],
controls: [{
name: 'asr-local-stop',
position: 'right',
html: 'Stop ASR',
tooltip: 'Stop capture; pause and play to restart',
async click() {
await art.plugins.artplayerPluginAsr.stop()
if (!art.isDestroy)
statistics.textContent = 'Local capture stopped. Pause and play to restart. Nothing was uploaded.'
},
}],
plugins: [artplayerPluginAsr({
length: 2,
interval: 250,
sampleRate: 16000,
autoHideTimeout: 5000,
onAudioChunk({ pcm, wav }) {
if (art.isDestroy)
return
chunks++
pcmBytes += pcm.byteLength
wavBytes += wav.byteLength
const sampleRate = new DataView(wav).getUint32(24, true)
const samples = new DataView(pcm)
let peak = 0
for (let offset = 0; offset < pcm.byteLength; offset += 2)
peak = Math.max(peak, Math.abs(samples.getInt16(offset, true)))
const duration = (pcmBytes / 2 / sampleRate).toFixed(2)
statistics.textContent = [
'Local capture only - simulated subtitles, no speech recognition',
`Chunks: ${chunks} | ${sampleRate} Hz mono PCM16 | ${duration} seconds captured`,
`PCM: ${pcmBytes} bytes | WAV: ${wavBytes} bytes | Current peak: ${peak}`,
].join('\n')
return `Simulated local subtitle: audio chunk ${chunks} received.`
},
})],
})
```
## 配置和音频片段
| 字段 | 默认值 | 含义 |
| --- | --- | --- |
| `length` | `3` | 从本次字幕按标点拆分的非空片段中保留最后多少段,不是音频秒数 |
| `interval` | `100` | 采集消费定时器间隔和每片音频的目标时长,单位毫秒 |
| `sampleRate` | `16000` | 请求的采样率,单位 Hz;需要环境支持 |
| `autoHideTimeout` | `10000` | 接受字幕后自动隐藏的等待时间,单位毫秒 |
| `onAudioChunk` | 返回 `null` 的函数 | 接收 `{ pcm, wav }`;返回字符串时显示字幕,可返回 Promise |
| `audioInput` | 未设置 | 默认直接 Web Audio 路径;`{ type: 'capture' }` 显式选择捕获流 |
`pcm` 和 `wav` 都是 `ArrayBuffer`。音频来自第一声道,PCM 为有符号 16 位小端,WAV 包含 44 字节单声道头。每片样本数为 `Math.floor(sampleRate * interval / 1000)`,需要至少一个样本和正的有限 interval。实际回调频率受音频就绪、浏览器调度和识别耗时影响,不承诺精确间隔。
当前采集代内一次最多等待一个识别回调;未满一片的样本继续排队。回调拒绝会记录错误并允许后续处理。积压超过约一分钟音频或两片中较大的上限时,采集暂停并记录错误,避免无限占用内存。暂停、切源、停止或销毁使旧回调结果失效;插件不会取消应用已经发送的网络请求,应用应自行管理请求资源。
## 字幕与生命周期
插件结果在 `art.plugins.artplayerPluginAsr`,`name` 固定为 `artplayerPluginAsr`:
| 方法 | 行为 |
| --- | --- |
| `append(text)` | 同步返回 `undefined`;显示本次文本的最后 `length` 段并重置隐藏定时器,替换已有字幕,不累积历史 |
| `hide()` | 同步返回 `undefined`;隐藏字幕,不停止采集或清空文字 |
| `stop()` | 实际返回 `Promise<void>`;停止本次采集并丢弃旧结果,之后的播放事件可以重新开始 |
`append` 和回调返回的字符串保留历史 HTML 渲染行为,不会自动转义。只传入可信字幕;识别服务返回的外部文字需要由应用转义或清理。`null`、`undefined` 或其他非字符串回调结果不会更新字幕。`stop()` 不立即隐藏已有字幕,原自动隐藏时间仍然有效。
播放事件启动采集,暂停停止记录并丢弃待处理结果,切源重新建立适用的采集状态,媒体错误执行非终止的停止。默认直接连接会保留视频的音频输出路线直到 `art.destroy()`,因此 stop 后仍可正常播放;这不是资源未清理。销毁释放插件监听器、定时器、录音资源和音频上下文。插件没有单独的公开 `start()` 或 `destroy()`。
## 音频路径与跨域
默认路径读取 `art.video`,不混合独立 Audio Track 插件的音轨;视频自身的音量和静音会影响所采音频。跨域媒体需要加载前设置 `moreVideoAttr: { crossOrigin: 'anonymous' }`,同时服务端提供允许的 CORS 响应。仅视频可播放不代表 Web Audio 可以读取:无访问权限时可能仍走时间但音频输出静音、产生全零数据或收不到片段,重定向后的跨域同样受限。
如果应用已有 Web Audio 图,请显式选择 `audioInput: { type: 'capture' }`。此模式通过 `captureStream/mozCaptureStream` 采集,不接管已有播放输出,也不会在失败时退回直接连接。它只释放自己创建的上下文和捕获轨道;暂停保留采集图,stop 或切源释放,后续播放重新获取。缺少能力或初始化失败会记录错误并清理资源。
捕获流数据可能不受视频音量或静音影响,不包含外部音效图或独立音轨的最终混音。CORS 仍适用:可能拒绝捕获,也可能有轨道却没有可读音频。不要把某个桌面浏览器通过解释为 Safari、手机、所有代理或物理扬声器均已验证。
## TypeScript 入口
根入口保留历史 `AsrPluginOption`、`AsrPluginInstance` 和 `AudioChunk`:回调为 `void | Promise<void>`,stop 为 void。要在类型中返回识别字符串、等待 stop 或设置捕获流,使用同一运行时实现的 `/runtime`:
```ts
import asr from 'artplayer-plugin-asr/runtime';
import type { RuntimeResult } from 'artplayer-plugin-asr/runtime';
const installAsr = asr({
audioInput: { type: 'capture' },
onAudioChunk({ pcm, wav }) {
console.log(pcm.byteLength, wav.byteLength);
return null; // Replace with your recognizer; null displays no subtitle.
},
});
async function stopAsr(plugin: RuntimeResult): Promise<void> {
await plugin.stop();
}
```
精确类型为 `RuntimeOption`、`RuntimeResult`、`RuntimeFactory`,并导出 `AudioChunk`。旧根入口在部分 NodeNext ESM 消费中保留模块命名空间形状;需要可调用默认导入时选择 `/runtime`。CommonJS 运行时同时支持函数本身和 `.default` 自引用,`/runtime` 也支持 TypeScript 的 `import = require`。
@@ -0,0 +1,95 @@
# 文档画中画
[English](../en/plugin/document-pip.md)
将整个播放器移到浏览器的 Document Picture-in-Picture 窗口,保留播放器控件。关闭窗口时将同一个播放器节点移回原位置。本页描述当前未发布的重构分支;在线示例和未固定版本的 npm/CDN 包不等于当前候选。
## 安装和示例
```sh
yarn add artplayer artplayer-plugin-document-pip
```
```js
import Artplayer from 'artplayer';
import artplayerPluginDocumentPip from 'artplayer-plugin-document-pip';
```
使用 script 时先加载 ArtPlayer,再加载 `dist/artplayer-plugin-document-pip.js`,全局名为 `artplayerPluginDocumentPip`。下面保留[在线示例](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-document-pip/index.js&example=document.pip)的原始代码。
<div className="run-code" data-libs="./uncompiled/artplayer-plugin-document-pip/index.js">▶ Run Code</div>
```js
// npm i artplayer-plugin-document-pip
// import artplayerPluginDocumentPip from 'artplayer-plugin-document-pip';
const art = new Artplayer({
container: '.artplayer-app',
url: '/assets/sample/video.mp4',
plugins: [
artplayerPluginDocumentPip({
width: 480,
height: 270,
fallbackToVideoPiP: true,
placeholder: `Playing in Document Picture-in-Picture`,
}),
],
})
art.on('document-pip', (state) => {
console.log('Document Picture-in-Picture', state)
})
```
## 配置
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
| `width` | `number` | `480` | 请求的窗口宽度,由浏览器决定实际尺寸 |
| `height` | `number` | `270` | 请求的窗口高度 |
| `placeholder` | `string` | `'Playing in Document Picture-in-Picture'` | 原播放器位置的占位文字 |
| `fallbackToVideoPiP` | `boolean` | `true` | 未检测到 Document PiP API 时尝试设置 `art.pip = true` |
插件安装名为 `artplayerPluginDocumentPip`,并添加画中画控制按钮。Document PiP 需要浏览器提供 `documentPictureInPicture.requestWindow`;能力检测为真仍可能因权限、调用环境或用户操作条件而打开失败。请在用户点击处理函数内直接调用 `open()` 或 `toggle()`,避免先等待网络等异步工作而丢失用户激活。
视频 PiP 降级也受浏览器及当前媒体能力限制。它不会移走整个播放器,不会将插件的 `isActive` 设为真,也不会发出 Document PiP 的激活事件。`close()` 管理 Document PiP 窗口;需要退出视频 PiP 时使用 `art.pip = false`。关闭降级选项不代表浏览器自动获得 Document PiP 能力。
## 状态、方法和事件
从 `art.plugins.artplayerPluginDocumentPip` 取得结果:
| 成员 | 实际行为 |
| --- | --- |
| `name` | 固定为 `artplayerPluginDocumentPip` |
| `isSupported` | 只读 getter,插件创建时的 Document PiP API 能力快照 |
| `isActive` | 只读 getter,当前是否持有 Document PiP 会话;不是视频 PiP 状态 |
| `open()` | 实际返回 `Promise<void>`;请求并移入播放器,已打开时不重复创建,请求中重复调用共享该次请求 |
| `close()` | 实际返回 `Promise<void>`;取消待处理请求或恢复节点并关闭窗口 |
| `toggle()` | 同步返回 `undefined`;打开,或关闭当前/正在申请的窗口 |
成功激活及正常关闭时,播放器发出 `document-pip` 事件,参数分别为 `true` 和 `false`。插件同步更新原有 `artplayer-document-pip` 类并重新绑定跨文档事件,随后安排 resize;不要把此事件理解为一次媒体加载或播放成功。
正常窗口请求/还原失败会显示 notice 并写入控制台警告,`open()` 不是“返回即保证窗口打开”的成功标记。视频降级设置器抛出的错误仍可使 Promise 拒绝。关闭待处理请求后,迟到的窗口会被关闭,不再接管播放器。销毁播放器会释放窗口、控件、订阅和定时器,不再发出新的插件状态事件。插件没有单独的公开 `destroy()`。
插件尽力复制播放器所在文档的样式;无法读取的跨域样式及应用自行管理的外部 DOM 仍需在目标环境验证。Canvas、其他代理、键盘焦点和持续播放的支持也应逐一验证,不能仅凭能力字段推断。
## TypeScript 兼容类型
根入口和 `/legacy` 保留旧声明:工厂参数对象必填、`Result` 的状态字段可写、方法返回 `void`。JavaScript 实际允许省略配置,状态 getter 不可赋值,`open/close` 返回 Promise。旧类型保留是为了兼容已有消费者和替换函数,不表示运行时字段变成可写。
本包没有 `/runtime` 子路径。需要精确类型时,将真实工厂显式看作 `RuntimeFactory`:
```ts
import documentPip from 'artplayer-plugin-document-pip';
import type { AsyncResult, RuntimeFactory } from 'artplayer-plugin-document-pip';
const runtimeFactory = documentPip as RuntimeFactory;
const installPip = runtimeFactory();
const installDefaultPip = runtimeFactory.default({ width: 480 });
async function closePip(pip: AsyncResult): Promise<void> {
await pip.close();
}
```
命名类型为 `Option`、`Result`、`AsyncResult`、`Factory`、`RuntimeFactory`。精确视图仅用于未经替换的真实实现,不能将返回 void 的 mock 或被替换方法强转为异步实现。CommonJS 运行时支持 `require(package)(options)` 和 `.default(options)`;旧 `import = require` 类型视图使用 `.default`,需要直接调用时选择精确工厂视图。
@@ -50,5 +50,5 @@
}
// <stdin>
installNavigation(window, ["advanced/built-in.html", "advanced/built-in", "advanced/class.html", "advanced/class", "advanced/event.html", "advanced/event", "advanced/global.html", "advanced/global", "advanced/plugin.html", "advanced/plugin", "advanced/property.html", "advanced/property", "component/contextmenu.html", "component/contextmenu", "component/controls.html", "component/controls", "component/layers.html", "component/layers", "component/setting.html", "component/setting", "", "index.html", "plugin/ambilight.html", "plugin/ambilight", "plugin/audio-track.html", "plugin/audio-track", "plugin/chapter.html", "plugin/chapter", "plugin/danmuku.html", "plugin/danmuku", "plugin/dash-control.html", "plugin/dash-control", "plugin/hls-control.html", "plugin/hls-control", "plugin/vtt-thumbnail.html", "plugin/vtt-thumbnail", "start/i18n.html", "start/i18n", "start/option.html", "start/option"]);
installNavigation(window, ["advanced/built-in.html", "advanced/built-in", "advanced/class.html", "advanced/class", "advanced/event.html", "advanced/event", "advanced/global.html", "advanced/global", "advanced/plugin.html", "advanced/plugin", "advanced/property.html", "advanced/property", "component/contextmenu.html", "component/contextmenu", "component/controls.html", "component/controls", "component/layers.html", "component/layers", "component/setting.html", "component/setting", "", "index.html", "plugin/ambilight.html", "plugin/ambilight", "plugin/asr.html", "plugin/asr", "plugin/audio-track.html", "plugin/audio-track", "plugin/chapter.html", "plugin/chapter", "plugin/danmuku.html", "plugin/danmuku", "plugin/dash-control.html", "plugin/dash-control", "plugin/document-pip.html", "plugin/document-pip", "plugin/hls-control.html", "plugin/hls-control", "plugin/vtt-thumbnail.html", "plugin/vtt-thumbnail", "start/i18n.html", "start/i18n", "start/option.html", "start/option"]);
})();