docs(site): [SITE-04] document Iframe and Thumbnail tools

This commit is contained in:
Harvey Zhao committed 2026-09-16 03:49:38 +08:00
1 parent 3aad294aa5
commit 484a8c903c
111 files changed
+5088 -2141

No files matched your search

@@ -95,8 +95,9 @@ arbitrary new metadata deadline; waiting can be cancelled by destroy/replacement
Sheet extraction preserves fractional coordinates, historical `creat*` names,
the 30-pixel footer and the filename algorithm. The temporary download anchor is
removed even if click throws. Default aspect-derived height and synchronous video
events currently match workspace 4.4.0; recovered 3.5.31 differs. See the frozen
removed even if click throws. Aspect-derived height and synchronous video events
apply only to explicit workspace-4.4 mode; the default preserves recovered
3.5.31 fixed height and delays. See the frozen
[contract](../../refactor/baselines/thumbnail-contract.md) before changing defaults.
## Validation and continuation
+13 -3
View File
@@ -26,6 +26,8 @@ not a VitePress page. Keep both URL surfaces compatible.
| `docs/plugin/jassub.md`, `docs/en/plugin/jassub.md` | Worker resources, ASS queries, actual method signatures, ownership and compatible declarations | `/document/plugin/jassub.html`, `/document/en/plugin/jassub.html` |
| `docs/plugin/danmuku-mask.md`, `docs/en/plugin/danmuku-mask.md` | Segmentation configuration, start/stop, mask ownership and preserved type shape | `/document/plugin/danmuku-mask.html`, `/document/en/plugin/danmuku-mask.html` |
| `docs/plugin/chromecast.md`, `docs/en/plugin/chromecast.md` | SDK loading, callbacks, shared sessions, receiver limitations and runtime types | `/document/plugin/chromecast.html`, `/document/en/plugin/chromecast.html` |
| `docs/tool/iframe.md`, `docs/en/tool/iframe.md` | Parent/child setup, serialized requests, navigation cancellation, trust and type views | `/document/tool/iframe.html`, `/document/en/tool/iframe.html` |
| `docs/tool/thumbnail.md`, `docs/en/tool/thumbnail.md` | Local file extraction, approved defaults, PNG sheets, events and URL ownership | `/document/tool/thumbnail.html`, `/document/en/tool/thumbnail.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 |
@@ -39,12 +41,20 @@ 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 58 Markdown pages: 29 Chinese
and 29 English. All 16 plugins have dedicated guides and navigation in both languages.
The two proxies and two tools still need dedicated bilingual guides. Core member
The generated site lives in `docs/document/`. The current inventory contains 62 Markdown pages: 31 Chinese
and 31 English. All 16 plugins and both tools have dedicated guides and navigation in both languages.
The two proxies still need dedicated bilingual guides. Core member
mapping and the current cross-check of earlier guides remain in SITE-04;
navigation links alone are not complete API documentation.
Tools have their own navigation group. Iframe explains the required child inject
step and serialized commit body, without treating the protocol as a sandbox or
promising legacy document identity. Thumbnail explains published fixed-height
defaults, explicit workspace mode, synchronous failures, density limits and owned
Blob URLs. Original examples are retained; their navigation is not extraction or
cross-window player acceptance. The thumbnail package's stale default-behavior
sentence was corrected alongside these guides, requiring its candidate refresh.
JASSUB documents actual resize argument order, query callbacks and resource
ownership separately from preserved historical declarations. Danmuku Mask keeps
both plugin dependencies in its Run Code link and explains initialization versus
@@ -69,7 +69,6 @@ const config = {
{ text: '🎉 弹幕库', link: '/plugin/danmuku' },
{ text: '弹幕遮罩', link: '/plugin/danmuku-mask' },
{ text: '视频广告', link: '/plugin/ads' },
{ text: 'Iframe 控制', link: 'https://artplayer.org/?libs=./uncompiled/artplayer-tool-iframe/index.js&example=iframe' },
{ text: 'HLS 控制', link: '/plugin/hls-control' },
{ text: 'DASH 控制', link: '/plugin/dash-control' },
{ text: 'VTT 缩略图', link: '/plugin/vtt-thumbnail' },
@@ -85,6 +84,14 @@ const config = {
{ text: '独立音轨', link: '/plugin/audio-track' },
],
},
{
text: '工具',
collapsible: true,
items: [
{ text: 'Iframe 控制', link: '/tool/iframe' },
{ text: '本地视频缩略图', link: '/tool/thumbnail' },
],
},
{
text: '第三方库',
collapsible: true,
@@ -144,7 +151,6 @@ const config = {
{ text: 'Danmuku', link: '/en/plugin/danmuku' },
{ text: 'Danmuku Mask', link: '/en/plugin/danmuku-mask' },
{ text: 'Advertise', link: '/en/plugin/ads' },
{ text: 'Iframe Control', link: 'https://artplayer.org/?libs=./uncompiled/artplayer-tool-iframe/index.js&example=iframe' },
{ text: 'HLS Control', link: '/en/plugin/hls-control' },
{ text: 'DASH Control', link: '/en/plugin/dash-control' },
{ text: 'VTT Thumbnail', link: '/en/plugin/vtt-thumbnail' },
@@ -160,6 +166,14 @@ const config = {
{ text: 'Audio Track', link: '/en/plugin/audio-track' },
],
},
{
text: 'Tools',
collapsible: true,
items: [
{ text: 'Iframe Control', link: '/en/tool/iframe' },
{ text: 'Local Video Thumbnails', link: '/en/tool/thumbnail' },
],
},
{
text: 'Libraries',
collapsible: true,
@@ -0,0 +1,153 @@
# Iframe communication tool
[简体中文](../../tool/iframe.md)
Control a player inside an iframe and receive child-page notifications. This is a standalone constructor, not an ArtPlayer plugins-array factory. This page describes the unreleased branch; deploy both parent and child pages yourself.
## Install and connect both pages
```sh
yarn add artplayer-tool-iframe
```
The parent uses `import ArtplayerToolIframe from 'artplayer-tool-iframe'`. For scripts, load `dist/artplayer-tool-iframe.js`; its global is `ArtplayerToolIframe`.
The child must also load the tool and call inject. To create a player there, load ArtPlayer and provide a container too. The repository's `/iframe.html` uses:
```html
<div class="artplayer-app" style="width:100%;height:100%"></div>
<script src="./uncompiled/artplayer/index.js"></script>
<script src="./uncompiled/artplayer-tool-iframe/index.js"></script>
<script>ArtplayerToolIframe.inject();</script>
```
These are this site's development paths; replace them with deployed build paths. The [original parent example](https://artplayer.org/?libs=./uncompiled/artplayer-tool-iframe/index.js&example=iframe) below creates an iframe pointing to `/iframe.html`:
<div className="run-code" data-libs="./uncompiled/artplayer-tool-iframe/index.js">▶ Run Code</div>
```js
// npm i artplayer-tool-iframe
// import ArtplayerToolIframe from 'artplayer-tool-iframe';
const $iframe = document.createElement('iframe')
$iframe.allowFullscreen = true
$iframe.width = '100%'
$iframe.height = '100%'
const $container = document.querySelector('.artplayer-app')
$container.innerHTML = ''
$container.appendChild($iframe)
const iframe = new ArtplayerToolIframe({
iframe: $iframe,
url: '/iframe.html',
})
window.addEventListener('artplayer:example:cleanup', () => {
iframe.destroy()
$iframe.remove()
}, { once: true })
iframe.message(({ type, data }) => {
switch (type) {
case 'fullscreenWeb':
if (data) {
$iframe.classList.add('fullscreenWeb')
}
else {
$iframe.classList.remove('fullscreenWeb')
}
break
default:
break
}
})
iframe.commit(() => {
const art = new Artplayer({
container: '.artplayer-app',
url: '/assets/sample/video.mp4',
fullscreen: true,
fullscreenWeb: true,
})
art.on('fullscreenWeb', (state) => {
ArtplayerToolIframe.postMessage({
type: 'fullscreenWeb',
data: state,
})
})
}).catch((error) => {
if (!iframe.destroyed)
console.error(error)
})
```
## Parent instance
Both options are required: `{ iframe: HTMLIFrameElement, url: string }`. Construction sets iframe.src and subscribes to messages; it does not install scripts in the child. The caller creates and removes the iframe element.
| Member | Behavior |
| --- | --- |
| `commit(callback)` | Extract the function body and execute it in the child; returns a response Promise |
| `postMessage({ type, data, id? })` | Send a request and await its response; allocate a numeric id when sending, ignoring a supplied id |
| `message(callback)` | Set one notification callback; the next call replaces it; synchronous void |
| `onMessage(event)` | Parent receiver, normally invoked by the installed native message listener |
| `destroy()` | Synchronous and idempotent; remove owned listeners/navigation observation and reject sent/waiting requests |
| `url` / `$iframe` | Initial configured address and element; changing url alone does not navigate |
| `injected` / `destroyed` | Injection and destruction state |
| `promises` | Pending request object, retaining the historical `resove` and `reject` callback keys |
| `messageCallback` | Current callback; initially a function, also nullable at runtime |
Notifications receive only `{ type, data }`, with the tool as this, without id or private document metadata. Any matching non-error response settles its request; error responses reject with Error. Custom message types remain valid, but the application must implement their child responses. There is no default response timeout.
Before injection, requests poll every200ms; inject does not flush them synchronously. IDs are correlation numbers, not necessarily Date.now. Destruction rejects unfinished requests with `The instance has been destroyed`; handle Promise failures.
## How commit executes
A normal function body can return a structured-cloneable result. Asynchronous results use the historical literal `resolve(...)` convention:
```js
const title = await iframe.commit(() => {
return document.title; // Read in the child page.
});
const answer = await iframe.commit((resolve) => {
setTimeout(() => resolve(42), 100);
});
```
The body is sliced from a string and evaluated with new Function. Closures, parent locals, function arguments and imports are not transferred. Keep braces; do not use expression arrows, async function bodies, top-level await or a renamed asynchronous resolve parameter. Detection is a text match, not a JavaScript parser. Results must support postMessage; a Promise object itself cannot be sent as a normal result. Execution errors send an error response and also reject the child's receiver.
## Child statics and trust
| Member | Behavior |
| --- | --- |
| `ArtplayerToolIframe.iframe` | Runtime readonly getter: whether this page is inside an iframe |
| `inject()` | Announce injection and install the receiver; repeated calls retain one listener set |
| `postMessage({ type, data, id? })` | Send a parent notification/response; id defaults0; returns void |
| `onMessage(event)` | Actually async; handles commit, while custom types need application handling |
The three methods require an iframe context; top-level use throws or rejects. Receivers check the selected window peer and basic packet shape, retaining wildcard targetOrigin rather than pinning the initial URL's origin. Trust the parent and child content. Commit executes code and requires a compatible CSP; it is neither a sandbox nor a payload validator.
## Navigation, cleanup and types
Upgraded peers negotiate document markers to reject obsolete replies and cancel work when document departure is confirmed. These markers are not authentication credentials. A src replacement pauses sends; confirmed cross-document departure cancels old requests, while a matching same-document hash change preserves them. Work queued for the next page is tracked separately. Legacy children cannot identify same-address reloads or internal navigation reliably; ordinary navigation tests do not establish BFCache restoration.
Destroy does not remove the iframe, destroy its player or provide a new static child destroy method. The parent can remove its iframe after releasing the tool. Player events, fullscreen permissions and physical devices require actual integration checks.
Root and `/legacy` preserve the historical class, including required Message.data, readonly fields, void static onMessage and ReturnType-based commit inference. Import accurate erased views from the same entry; there is no `/runtime` subpath:
```ts
import Iframe from 'artplayer-tool-iframe';
import type { ResolverInstance, RuntimeConstructor } from 'artplayer-tool-iframe';
function connectFrame(element: HTMLIFrameElement) {
const Runtime = Iframe as RuntimeConstructor;
const tool = new Runtime({ iframe: element, url: '/iframe.html' }) as ResolverInstance;
tool.message(function (message) { console.log(this.url, message.type, message.data); });
const answer = tool.commit<number>((resolve) => { resolve(42); });
return { tool, answer };
}
```
Public types are Option, Message, Callbacks, Notification, MessageCallback, OutboundMessage, ProtocolMessage, Resolve, ResolverCallback, RuntimeInstance, ResolverInstance and RuntimeConstructor. Views neither validate responses nor alter execution. Modern CommonJS TS can use `import Iframe = require('artplayer-tool-iframe')`; ESM uses the default import. The constructor has no `.default` self-alias. The old `artplayer-plugin-iframe` name, namespace and separate helper are not identical to this tool; renaming the dependency alone does not preserve every old entrypoint.
@@ -0,0 +1,181 @@
# Local video thumbnail tool
[简体中文](../../tool/thumbnail.md)
Generate a PNG thumbnail sheet from a selected local video, then download it or use it with the player's thumbnails option. This is a standalone constructor, not the Auto Thumbnail plugin. This page describes the unreleased branch and its approved compatibility modes.
## Install and example
```sh
yarn add artplayer-tool-thumbnail
```
ESM uses `import ArtplayerToolThumbnail from 'artplayer-tool-thumbnail'`. Scripts load `dist/artplayer-tool-thumbnail.js`, exposing `ArtplayerToolThumbnail`. The tool does not depend on the player; the [original example](https://artplayer.org/?libs=./uncompiled/artplayer-tool-thumbnail/index.js&example=tool.thumbnail) below uses this site's DOM and ArtPlayer to display the result:
<div className="run-code" data-libs="./uncompiled/artplayer-tool-thumbnail/index.js">▶ Run Code</div>
```js
if (window.lastThumbnail) {
window.lastThumbnail.destroy();
}
var $popups = document.querySelector('.popups');
var $popinner = document.querySelector('.popinner');
var $artplayer = document.querySelector('.artplayer-app');
$artplayer.innerHTML = 'Drop video file here or click to upload.';
var thumbnail = new ArtplayerToolThumbnail({
fileInput: $artplayer,
number: 60, // 数量
width: 160, // 宽度
column: 10, // 列数
begin: 0, // 开始
end: NaN, // 结束
});
window.lastThumbnail = thumbnail;
thumbnail.on('file', function (file) {
console.log('Read video successfully: ' + file.name);
});
thumbnail.on('video', function (video) {
console.log('Video size: ' + video.videoWidth + ' x ' + video.videoHeight);
console.log('Video duration: ' + video.duration + 's');
thumbnail.start();
});
thumbnail.on('canvas', function (canvas) {
console.log('Build canvas successfully');
console.log('Canvas size: ' + canvas.width + ' x ' + canvas.height);
console.log('Preview density: ' + thumbnail.density + ' p/s');
});
thumbnail.on('update', function (url, percentage) {
console.log('Processing: ' + Math.floor(percentage.toFixed(2) * 100) + '%');
$popups.style.display = 'flex';
$popinner.style.backgroundImage = 'url(' + url + ')';
});
thumbnail.on('download', function (name) {
console.log('Start download preview: ' + name);
});
thumbnail.on('done', function () {
$popups.style.display = 'none';
thumbnail.download();
console.log('Build preview image complete');
[...Artplayer.instances].forEach(function (art) {
art.destroy(true);
});
new Artplayer({
container: $artplayer,
url: thumbnail.videoUrl,
autoSize: true,
poster: thumbnail.thumbnailUrl,
thumbnails: {
url: thumbnail.thumbnailUrl,
number: thumbnail.option.number,
column: thumbnail.option.column,
},
});
console.log('Build player complete');
});
```
File selection loads the video without automatically extracting images; the example calls start from the video event. Application code should handle both synchronous start errors and Promise rejection. The video notification does not guarantee metadata readiness; start waits for it. Subscribe before loading, especially with synchronous workspace-mode notifications.
## Options and defaults
Successful construction needs fileInput: an existing file input or an Element upload wrapper. Missing input throws synchronously even though the type allows omitted constructor options. A wrapper receives an owned transparent input; caller inputs remain caller-owned. Selection/drop reads only the first file.
| Field | Default | Meaning |
| --- | --- | --- |
| `fileInput` | Required for construction | File input or upload wrapper |
| `compatibility` | published-3.5 behavior | Alternatively choose `workspace-4.4` |
| `number` | `60` | Frame count, numerically clamped to10–1000 |
| `width` | `160` | Frame width, clamped to10–1000 |
| `height` | `90` | Fixed default-mode height, clamped to10–1000 |
| `column` | `10` | Columns, clamped to1–1000 |
| `begin` | `0` | Start time in seconds |
| `end` | `NaN` | End time in seconds; NaN/0 uses media duration |
| `delay` | `300` | Default-mode milliseconds, clamped to10–1000 |
Use valid finite dimensions and integer counts/columns; historical numeric checks are not integer validation. Start normalizes the interval against media duration and requires end greater than begin, finite duration and `number / intervalSeconds <= 1`. The default60 frames therefore needs an interval of at least60 seconds; change the count for shorter files.
| Behavior | Default / published-3.5 | workspace-4.4 |
| --- | --- | --- |
| Height | Keep configured height | Derive from video aspect ratio at start and update option.height |
| video event | After src assignment plus delay | Synchronously after src assignment |
| Frame wait | Policy delay after each seek plus frame readiness | Frame readiness without a fixed extra delay |
| done | Another delay × 2 after the last update | No fixed final wait |
| Input value | Retained | Cleared after reading the selected file |
Consumers of unpublished4.4 workspace behavior add `compatibility: 'workspace-4.4'`; this mode ignores delay. Static DEFAULTS returns a fresh default object including published delay on every access, regardless of instance mode. Browser scheduling means delays are not exact timestamps.
## Methods, state and output
| Method | Behavior |
| --- | --- |
| `setup(options?)` | Merge partial options, retain extra fields and transfer input listeners when needed; returns this |
| `loadVideo(file?)` | Accept File; absent input is a no-op; check canPlayType and create a Blob URL |
| `start()` | One extraction job returning `Promise<void>`; duplicates and ready-metadata preflight can throw synchronously |
| `creatScreenshotDate()` | Historical spelling; return `{ time, x, y }[]`, with time in seconds |
| `creatCanvas()` | Historical spelling; create the sheet with black background and footer text |
| `download()` | Trigger PNG download when idle with file/image available; returns this, throws if not ready |
| `inputChange(event)` / `ondrop(event)` | Bound input handlers, normally installed by the tool |
| `errorHandle(condition, message)` | Emit error and throw when the condition fails |
| `destroy()` | Synchronous, idempotent cancellation and owned-resource cleanup |
Static creatVideo creates an offscreen muted/controls video in the document; callers invoking it directly own that extra node. Static ondragover calls preventDefault. Historical creat* names remain unchanged.
Fields include processing, option, video, duration, density, file, videoUrl, thumbnailUrl and optional event registry e. Duration is the selected interval, not necessarily full media duration; density is frames per interval second. File/URLs/density may be absent before their operation. Canvas listeners observe processing=false, update listeners true, and done listeners false.
Frames sample interval midpoints: `begin + (i + 0.5) * duration / number`. Sheet width is width × column; height is ceil(number / column) × height + 30. The30px footer retains source/layout text. Fractional coordinates keep historical behavior. Each frame produces a PNG update; previous thumbnail Blob URLs are revoked, leaving the latest thumbnailUrl. Download naming removes the last extension segment and adds `.png`; extensionless names retain the historical `.png` result.
## Events and cleanup
on/once/emit/off return this. The third on/once argument sets callback this. off(name) removes all listeners for that event; off(name, callback) removes matches. Custom string, number and symbol events are supported. A listener exception stops the remaining callbacks in that dispatch.
| Event | Arguments and timing |
| --- | --- |
| `file` | File, synchronously before video.src assignment |
| `video` | HTMLVideoElement; timing depends on mode |
| `canvas` | HTMLCanvasElement before extraction |
| `update` | Latest URL and0–1 progress |
| `done` | No arguments, before the start Promise resolves |
| `download` | Filename after link click; not proof of completed disk writing |
| `error` | Usually a message string; user callback failures can carry other values |
| `destroy` | No arguments, once after resource cleanup |
Start can wait for the first file selection with no new metadata deadline. Replacing an existing source or destroying the instance rejects old work with AbortError without an error event for cancellation. Source, seek, draw, encoding and callback failures settle the job. Stale callbacks cannot update a newer result.
Destroy removes the owned video/generated input/listeners/Blob URLs and restores wrapper position if the tool still owns that write. Caller inputs and the emitter registry remain. Cleanup attempts all steps, then throws its first failure. Destroyed instances cannot recreate input/source resources; start rejects cancellation. Keep the tool alive while a player depends on videoUrl/thumbnailUrl: those URLs remain tool-owned.
MIME canPlayType, actual decoding, Canvas encoding and Blob URL support are separate conditions. Windows WebKit has a recorded native Blob-loading gap; successful guide navigation or HTTP video playback is not proof of local-file extraction.
## TypeScript
Root and `/legacy` share one class declaration, with no `/runtime` or runtime `.default` self-alias. CommonJS TS supports `import Thumbnail = require('artplayer-tool-thumbnail')`; ESM uses default/type imports:
```ts
import Thumbnail, { type Option } from 'artplayer-tool-thumbnail';
function createTool(input: HTMLInputElement) {
const options: Option = { fileInput: input, number: 10, height: 90 };
const tool = new Thumbnail(options);
tool.on('update', (url, progress) => console.log(url, progress));
tool.on('video', () => {
void (async () => {
try { await tool.start(); }
catch (error) { console.error(error); }
})();
});
return tool;
}
```
Types include SheetOptions, Compatibility, DefaultOptions, Option, ResolvedOption, ScreenshotPoint, Events, EventArgs, Listener and EventRegistry. Known events have precise arguments; custom protocols remain application-defined. The old workspace referenced a missing declaration, so the new types are not claimed as a recovered historical TS baseline. Missing complete old npm archives and rollback acceptance remain separately tracked.
@@ -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/ads.html", "plugin/ads", "plugin/ambilight.html", "plugin/ambilight", "plugin/asr.html", "plugin/asr", "plugin/audio-track.html", "plugin/audio-track", "plugin/auto-thumbnail.html", "plugin/auto-thumbnail", "plugin/chapter.html", "plugin/chapter", "plugin/chromecast.html", "plugin/chromecast", "plugin/danmuku-mask.html", "plugin/danmuku-mask", "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/jassub.html", "plugin/jassub", "plugin/multiple-subtitles.html", "plugin/multiple-subtitles", "plugin/vast.html", "plugin/vast", "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/ads.html", "plugin/ads", "plugin/ambilight.html", "plugin/ambilight", "plugin/asr.html", "plugin/asr", "plugin/audio-track.html", "plugin/audio-track", "plugin/auto-thumbnail.html", "plugin/auto-thumbnail", "plugin/chapter.html", "plugin/chapter", "plugin/chromecast.html", "plugin/chromecast", "plugin/danmuku-mask.html", "plugin/danmuku-mask", "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/jassub.html", "plugin/jassub", "plugin/multiple-subtitles.html", "plugin/multiple-subtitles", "plugin/vast.html", "plugin/vast", "plugin/vtt-thumbnail.html", "plugin/vtt-thumbnail", "start/i18n.html", "start/i18n", "start/option.html", "start/option", "tool/iframe.html", "tool/iframe", "tool/thumbnail.html", "tool/thumbnail"]);
})();
@@ -0,0 +1,153 @@
# Iframe 通信工具
[English](../en/tool/iframe.md)
在父页面控制 iframe 内的播放器,并把子页面事件传回父页面。它是独立构造器,不是放进 ArtPlayer plugins 数组的插件。本页描述当前未发布分支;父子页面都需要自行部署。
## 安装与双端接入
```sh
yarn add artplayer-tool-iframe
```
父页面使用默认导入 `import ArtplayerToolIframe from 'artplayer-tool-iframe'`。script 使用 `dist/artplayer-tool-iframe.js`,全局名为 `ArtplayerToolIframe`。
子页面也必须加载此工具并主动调用 inject;如果需要创建播放器,还要在子页面加载 ArtPlayer 和提供容器。例如仓库的 `/iframe.html` 使用:
```html
<div class="artplayer-app" style="width:100%;height:100%"></div>
<script src="./uncompiled/artplayer/index.js"></script>
<script src="./uncompiled/artplayer-tool-iframe/index.js"></script>
<script>ArtplayerToolIframe.inject();</script>
```
以上路径属于本站开发输出,部署应用时替换成实际构建路径。下面保留[父页面原始示例](https://artplayer.org/?libs=./uncompiled/artplayer-tool-iframe/index.js&example=iframe),它创建 iframe 并指向 `/iframe.html`:
<div className="run-code" data-libs="./uncompiled/artplayer-tool-iframe/index.js">▶ Run Code</div>
```js
// npm i artplayer-tool-iframe
// import ArtplayerToolIframe from 'artplayer-tool-iframe';
const $iframe = document.createElement('iframe')
$iframe.allowFullscreen = true
$iframe.width = '100%'
$iframe.height = '100%'
const $container = document.querySelector('.artplayer-app')
$container.innerHTML = ''
$container.appendChild($iframe)
const iframe = new ArtplayerToolIframe({
iframe: $iframe,
url: '/iframe.html',
})
window.addEventListener('artplayer:example:cleanup', () => {
iframe.destroy()
$iframe.remove()
}, { once: true })
iframe.message(({ type, data }) => {
switch (type) {
case 'fullscreenWeb':
if (data) {
$iframe.classList.add('fullscreenWeb')
}
else {
$iframe.classList.remove('fullscreenWeb')
}
break
default:
break
}
})
iframe.commit(() => {
const art = new Artplayer({
container: '.artplayer-app',
url: '/assets/sample/video.mp4',
fullscreen: true,
fullscreenWeb: true,
})
art.on('fullscreenWeb', (state) => {
ArtplayerToolIframe.postMessage({
type: 'fullscreenWeb',
data: state,
})
})
}).catch((error) => {
if (!iframe.destroyed)
console.error(error)
})
```
## 父页面实例
构造参数为 `{ iframe: HTMLIFrameElement, url: string }`,两项必填。构造时设置 iframe.src 并注册消息监听器;不会自动安装子页面脚本。iframe 元素由调用方创建和移除。
| 成员 | 行为 |
| --- | --- |
| `commit(callback)` | 提取函数体字符串,在子页面执行;返回响应 Promise |
| `postMessage({ type, data, id? })` | 向子页面发送请求并等待响应;发送时分配自己的数值 id,忽略传入 id |
| `message(callback)` | 设置一个消息通知回调,后一次覆盖前一次;同步返回 void |
| `onMessage(event)` | 父页面接收器;通常由工具监听原生 message 事件 |
| `destroy()` | 同步、幂等;移除本实例监听和导航观察,拒绝已发送及等待中的请求 |
| `url` / `$iframe` | 初始配置地址及 iframe 元素;修改 url 字段本身不等于导航 |
| `injected` / `destroyed` | 注入及销毁状态 |
| `promises` | 待处理请求对象;历史回调字段拼写为 `resove` 和 `reject` |
| `messageCallback` | 当前通知回调;默认函数,运行时也可为 null |
通知只接收 `{ type, data }`,this 为该工具实例,不带 id 或私有文档标记。匹配请求 id 的非 error 消息会正常结算;error 响应转换为 Error 拒绝。自定义消息类型仍可使用,但子页面需实现对应响应;未响应的请求没有默认超时。
注入前请求每 200ms 检查一次,不在 inject 事件里同步清空队列。数值 id 是关联编号,不保证等于 Date.now。销毁时未完成请求以 `The instance has been destroyed` 拒绝;调用方应处理 Promise 失败。
## commit 的执行方式
普通函数体可 return 可结构化克隆的结果。异步结果使用历史的字面量 `resolve(...)` 约定:
```js
const title = await iframe.commit(() => {
return document.title; // 在子页面读取
});
const answer = await iframe.commit((resolve) => {
setTimeout(() => resolve(42), 100);
});
```
函数体通过字符串截取和 new Function 执行,不传递闭包、父页面局部变量、函数参数或 import。保留花括号;不要使用表达式箭头函数、async 函数体、顶层 await,或重命名异步 resolve 参数。该约定使用文本匹配,并非完整 JavaScript 语法分析。返回值需适合 postMessage,Promise 对象本身不能当作普通返回值发送。执行错误会发送 error 响应,子页面接收器也会拒绝。
## 子页面静态方法与信任边界
| 成员 | 行为 |
| --- | --- |
| `ArtplayerToolIframe.iframe` | 只读运行时 getter:当前页面是否位于 iframe 内 |
| `inject()` | 发送注入通知并安装子页面接收器;重复调用保留单份监听 |
| `postMessage({ type, data, id? })` | 向 parent 发送通知/响应,默认 id 为 0,返回 void |
| `onMessage(event)` | 实际为 async 接收器;处理 commit,普通自定义类型需应用处理 |
后三项只能在 iframe 中使用;顶层调用抛错或拒绝。两端检查所选窗口与基本消息形状,继续使用 wildcard targetOrigin,未将初始 URL 的 origin 固定为白名单。父子页面内容必须可信;commit 会执行代码,需要允许对应执行方式的 CSP,它不是隔离沙箱或消息数据验证器。
## 导航、清理和兼容类型
双方升级后会协商文档标记,用于拒绝旧文档响应和取消确认离开页面的请求;标记不是认证凭证。替换 src 暂停发送,确认跨文档离开后取消旧请求;匹配的同文档 hash 变化保留请求。新页面排队请求与旧页面请求区分处理。旧子页面缺少文档标记时,同地址 reload 或内部导航的识别有局限,不能视为已验证 BFCache 恢复。
destroy 不移除 iframe、不销毁其播放器,也不新增静态子页面 destroy API。父页面可在释放工具后自行移除 iframe。独立页面的播放器事件、全屏权限及设备行为需要实际集成测试。
根和 `/legacy` 保留旧类声明,包括必填 Message.data、只读实例字段、同步静态 onMessage 和 `commit` 的 ReturnType 推导。准确运行时视图从同一入口按类型导入;没有 `/runtime` 子路径:
```ts
import Iframe from 'artplayer-tool-iframe';
import type { ResolverInstance, RuntimeConstructor } from 'artplayer-tool-iframe';
function connectFrame(element: HTMLIFrameElement) {
const Runtime = Iframe as RuntimeConstructor;
const tool = new Runtime({ iframe: element, url: '/iframe.html' }) as ResolverInstance;
tool.message(function (message) { console.log(this.url, message.type, message.data); });
const answer = tool.commit<number>((resolve) => { resolve(42); });
return { tool, answer };
}
```
公开类型包括 Option、Message、Callbacks、Notification、MessageCallback、OutboundMessage、ProtocolMessage、Resolve、ResolverCallback、RuntimeInstance、ResolverInstance、RuntimeConstructor。类型视图不验证响应数据,也不改变执行协议。现代 CommonJS TS 可用 `import Iframe = require('artplayer-tool-iframe')`;ESM 使用默认导入。构造器没有 `.default` 自引用。旧 `artplayer-plugin-iframe` 的包名、命名空间和独立 helper 不等同于本工具,不能仅换依赖名就认为所有入口兼容。
@@ -0,0 +1,181 @@
# 本地视频缩略图工具
[English](../en/tool/thumbnail.md)
从用户选择的本地视频生成 PNG 缩略图拼图,可下载或交给播放器的 thumbnails 配置使用。这是独立构造器,不是 Auto Thumbnail 插件。本页描述未发布分支及已经批准的两种兼容模式。
## 安装和示例
```sh
yarn add artplayer-tool-thumbnail
```
ESM 使用 `import ArtplayerToolThumbnail from 'artplayer-tool-thumbnail'`。script 使用 `dist/artplayer-tool-thumbnail.js`,全局为 `ArtplayerToolThumbnail`。工具自身不依赖播放器;下面的[原始示例](https://artplayer.org/?libs=./uncompiled/artplayer-tool-thumbnail/index.js&example=tool.thumbnail)使用本站 DOM 和 ArtPlayer 展示结果:
<div className="run-code" data-libs="./uncompiled/artplayer-tool-thumbnail/index.js">▶ Run Code</div>
```js
if (window.lastThumbnail) {
window.lastThumbnail.destroy();
}
var $popups = document.querySelector('.popups');
var $popinner = document.querySelector('.popinner');
var $artplayer = document.querySelector('.artplayer-app');
$artplayer.innerHTML = 'Drop video file here or click to upload.';
var thumbnail = new ArtplayerToolThumbnail({
fileInput: $artplayer,
number: 60, // 数量
width: 160, // 宽度
column: 10, // 列数
begin: 0, // 开始
end: NaN, // 结束
});
window.lastThumbnail = thumbnail;
thumbnail.on('file', function (file) {
console.log('Read video successfully: ' + file.name);
});
thumbnail.on('video', function (video) {
console.log('Video size: ' + video.videoWidth + ' x ' + video.videoHeight);
console.log('Video duration: ' + video.duration + 's');
thumbnail.start();
});
thumbnail.on('canvas', function (canvas) {
console.log('Build canvas successfully');
console.log('Canvas size: ' + canvas.width + ' x ' + canvas.height);
console.log('Preview density: ' + thumbnail.density + ' p/s');
});
thumbnail.on('update', function (url, percentage) {
console.log('Processing: ' + Math.floor(percentage.toFixed(2) * 100) + '%');
$popups.style.display = 'flex';
$popinner.style.backgroundImage = 'url(' + url + ')';
});
thumbnail.on('download', function (name) {
console.log('Start download preview: ' + name);
});
thumbnail.on('done', function () {
$popups.style.display = 'none';
thumbnail.download();
console.log('Build preview image complete');
[...Artplayer.instances].forEach(function (art) {
art.destroy(true);
});
new Artplayer({
container: $artplayer,
url: thumbnail.videoUrl,
autoSize: true,
poster: thumbnail.thumbnailUrl,
thumbnails: {
url: thumbnail.thumbnailUrl,
number: thumbnail.option.number,
column: thumbnail.option.column,
},
});
console.log('Build player complete');
});
```
选择文件只加载视频,不自动开始提取;示例在 video 事件里调用 start。应用代码应处理 start 的同步异常和 Promise 拒绝。video 通知不是 metadata 已就绪的保证,start 自己等待媒体信息。可在加载文件前注册监听,尤其是同步通知的工作区模式。
## 配置与默认行为
成功构造需要 `fileInput`:现有 `<input type="file">` 或用作上传区域的 Element。没有有效元素会同步抛错,即使类型中的构造参数可省略。包装元素会获得一个透明文件输入,原生 input 由调用方拥有。拖放和选择只读取第一个文件。
| 字段 | 默认值 | 含义 |
| --- | --- | --- |
| `fileInput` | 必须提供 | 文件输入或上传区域 |
| `compatibility` | `published-3.5` 行为 | 也可显式选 `workspace-4.4` |
| `number` | `60` | 截图数量,数值钳制到10–1000 |
| `width` | `160` | 单张宽度,钳制到10–1000 |
| `height` | `90` | 默认模式固定高度,钳制到10–1000 |
| `column` | `10` | 列数,钳制到1–1000 |
| `begin` | `0` | 片段起点,秒 |
| `end` | `NaN` | 片段终点,秒;NaN/0使用媒体时长 |
| `delay` | `300` | 默认模式的等待毫秒,钳制到10–1000 |
使用有限、有效的尺寸和整数数量/列数;历史数值检查不是整数校验器。start 会依据视频时长归一化区间,要求终点大于起点、时长有限且 `number / 区间秒数 <= 1`。例如默认60张需要至少60秒区间,不适合不修改参数就处理数秒视频。
| 行为 | 默认 / published-3.5 | workspace-4.4 |
| --- | --- | --- |
| 高度 | 保留配置 height | start 时按视频宽高比计算并写回 option.height |
| video 事件 | 设置 src 后等待 delay | 设置 src 后同步通知 |
| 截图等待 | 每次 seek 后至少经过 delay,并等待可用帧 | 等待可用帧,无固定额外延迟 |
| done | 最后 update 后再等待 delay × 2 | 没有该固定等待 |
| 文件 input 值 | 保留 | 读取文件后清空 |
依赖未发布4.4工作区行为的代码增加 `compatibility: 'workspace-4.4'`;delay 在该模式中被忽略。静态 `DEFAULTS` 每次返回一份默认配置对象,始终包含 published 模式的 delay,不随实例改变。实际定时器受浏览器调度影响,不保证精确毫秒。
## 方法、状态与输出
| 方法 | 行为 |
| --- | --- |
| `setup(options?)` | 合并部分配置、保留额外字段,返回 this;更换输入时迁移监听器 |
| `loadVideo(file?)` | 接受 File;空值无操作;检查 canPlayType 后创建 Blob URL |
| `start()` | 一次提取任务,返回 `Promise<void>`;重复任务或已就绪预检可能同步抛错 |
| `creatScreenshotDate()` | 历史拼写;返回 `{ time, x, y }[]`,time 为秒 |
| `creatCanvas()` | 历史拼写;创建带黑色背景及底部文字的拼图画布 |
| `download()` | 完成且存在文件/图片后触发 PNG 下载,返回 this;未就绪抛错 |
| `inputChange(event)` / `ondrop(event)` | 已绑定的输入处理器;通常由工具自己监听 |
| `errorHandle(condition, message)` | 条件失败时发 error 并抛错 |
| `destroy()` | 同步、幂等;取消任务并释放自有资源 |
静态 `creatVideo()` 创建并插入页面外的 muted/controls video;直接调用时由调用方管理该额外节点。静态 `ondragover(event)` 调用 preventDefault。这些历史名称没有改成 create*。
状态包括 processing、option、video、duration、density、file、videoUrl、thumbnailUrl,以及可选的事件注册表 e。duration 是选取区间长度,不一定是完整视频长度;density 为截图数/区间秒数。file/URL/density 在相应操作前可能不存在。processing 在 canvas 事件时仍为 false,在 update 时为 true,在 done 前恢复 false。
截图取各等分区间的中点:`begin + (i + 0.5) * duration / number`。拼图宽度为 width × column,高度为 ceil(number / column) × height + 30;底部30像素保留来源及布局文字。小数坐标沿用历史行为。逐张编码 PNG 并发 update,旧图片 Blob URL 被撤销;只保留最新 thumbnailUrl。下载名去掉原文件最后一段扩展名后加 `.png`,无扩展名文件沿用历史的 `.png` 名称。
## 事件和清理
on/once/emit/off 都返回 this;on/once 的第三个参数设置回调 this。off(name) 移除该事件全部监听,off(name, callback) 移除匹配监听。支持自定义字符串、数值和 symbol 事件;监听器异常会停止本次剩余回调,不会自动吞掉。
| 事件 | 参数与时机 |
| --- | --- |
| `file` | File;同步且早于 video.src 赋值 |
| `video` | HTMLVideoElement;时序由模式决定 |
| `canvas` | HTMLCanvasElement;开始提取前 |
| `update` | 最新 URL、0–1进度 |
| `done` | 无参数;先发事件,再结算 start Promise |
| `download` | 下载文件名;触发链接点击后,不保证磁盘写入完成 |
| `error` | 通常为消息字符串;用户回调异常可能带来其他值 |
| `destroy` | 无参数;资源清理后发出一次 |
可以先 start 等待第一次文件选择;没有额外 metadata 超时。替换已有来源或 destroy 会取消旧任务,以 AbortError 拒绝且不为取消发 error。源加载、seek、绘制、编码或回调失败会结算任务;忽略过期回调,避免旧帧更新新结果。
destroy 移除自有 video、生成 input、监听器及 Blob URL,恢复仍由工具控制的包装元素 position;保留调用方 input 和事件注册表。清理会尽量全部执行,再抛出首个清理错误。已销毁实例不会重新创建输入或来源;start 返回取消拒绝。不要在依赖 videoUrl/thumbnailUrl 的播放器仍需要它们时销毁工具;URL 生命周期仍归工具所有。
文件 MIME 的 canPlayType、实际解码、Canvas 编码和 Blob URL 能力是不同条件。Windows WebKit 已有原生 Blob 加载缺口,不能把页面导航或 HTTP 视频加载成功当作本地截图成功。
## TypeScript
根和 `/legacy` 共享同一类类型,没有 `/runtime` 或运行时 `.default` 自引用。CommonJS TS 支持 `import Thumbnail = require('artplayer-tool-thumbnail')`,ESM 默认导入及类型导入如下:
```ts
import Thumbnail, { type Option } from 'artplayer-tool-thumbnail';
function createTool(input: HTMLInputElement) {
const options: Option = { fileInput: input, number: 10, height: 90 };
const tool = new Thumbnail(options);
tool.on('update', (url, progress) => console.log(url, progress));
tool.on('video', () => {
void (async () => {
try { await tool.start(); }
catch (error) { console.error(error); }
})();
});
return tool;
}
```
类型包括 SheetOptions、Compatibility、DefaultOptions、Option、ResolvedOption、ScreenshotPoint、Events、EventArgs、Listener、EventRegistry。已知事件有精确参数;自定义协议仍需应用约定。旧工作区曾指向不存在的声明,当前类型不冒充已恢复的历史 TS 基线;完整旧 npm 归档缺口与回退验收仍独立记录。