refactor(audio): [PKG-AUDIO-04] migrate strict source and compatible type entries

This commit is contained in:
Harvey Zhao committed 2026-09-12 02:58:44 +08:00
1 parent 9c07ea830c
commit 13c898c149
31 files changed
+18923 -90

No files matched your search

+48 -36
View File
@@ -1,40 +1,52 @@
interface Option {
/**
* Audio track URL
*/
url: string
/**
* Time offset in seconds between video and audio
* Positive value means audio plays ahead of video
* Negative value means audio plays behind video
* @default 0
*/
offset?: number
/**
* Synchronization threshold in seconds
* @default 0.3
*/
sync?: number
// Generated from the package public declaration by yarn build:ts. Do not edit.
/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */
declare namespace artplayerPluginAudioTrackDefinitions {
export interface Option {
/**
* Audio track URL
*/
url: string
/**
* Time offset in seconds between video and audio
* Positive value means audio plays ahead of video
* Negative value means audio plays behind video
* @default 0
*/
offset?: number
/**
* Synchronization threshold in seconds
* @default 0.3
*/
sync?: number
}
export type UpdateOption = Partial<Option>
export interface Result {
name: 'artplayerPluginAudioTrack'
/**
* The audio element
*/
audio: HTMLAudioElement
/**
* Historical update signature. Runtime also accepts partial options.
* Import the /runtime entry for the precise partial-update signature.
*/
update: (option: Option) => void
}
export interface RuntimeResult extends Result {
/** Update selected fields without replacing the audio element. */
update: (option: UpdateOption) => void
}
/** Precise typing for the same runtime factory, without changing legacy inference. */
export type RuntimeFactory = (option: Option) => (art: Artplayer) => RuntimeResult
export function artplayerPluginAudioTrack(option: Option): (art: Artplayer) => Result
}
interface Result {
name: 'artplayerPluginAudioTrack'
/**
* The audio element
*/
audio: HTMLAudioElement
/**
* Update option
*/
update: (option: Option) => void
declare const artplayerPluginAudioTrack: typeof artplayerPluginAudioTrackDefinitions.artplayerPluginAudioTrack
declare namespace artplayerPluginAudioTrack {
export type Option = artplayerPluginAudioTrackDefinitions.Option
export type UpdateOption = artplayerPluginAudioTrackDefinitions.UpdateOption
export type Result = artplayerPluginAudioTrackDefinitions.Result
export type RuntimeResult = artplayerPluginAudioTrackDefinitions.RuntimeResult
export type RuntimeFactory = artplayerPluginAudioTrackDefinitions.RuntimeFactory
}
declare const artplayerPluginAudioTrack: (option: Option) => (art: Artplayer) => Result
export default artplayerPluginAudioTrack
export = artplayerPluginAudioTrack
export as namespace artplayerPluginAudioTrack;
@@ -2,4 +2,5 @@ node_modules
.DS_Store
yarn.lock
package-lock.json
src
src
tsconfig.json
@@ -1,16 +1,17 @@
# Audio Track maintenance
The default factory in src/index.js keeps the public `(option) => (art) => result`
The default factory in src/index.ts keeps the public `(option) => (art) => result`
contract. The result exposes the same HTMLAudioElement for its entire lifetime, a
literal name, and synchronous update. Public declarations remain authored in types/
until the separate type migration; do not infer new public methods from internal helpers.
literal name, and synchronous update. Public declarations are authored in types/ and checked
against the strict source by test/types/audio-track-source.ts. Do not infer public methods
from internal helpers. Both source files are strict TS without ambient Node types or any escapes.
## Responsibilities
- src/index.js owns ArtPlayer subscriptions, reads host media/volume/rate, adapts public
- src/index.ts owns ArtPlayer subscriptions, reads host media/volume/rate, adapts public
update, and rolls back partial installation. It uses only on/off and existing media
properties, so it does not require new core lifecycle utilities.
- src/track.js creates the Audio element and owns URL/offset/threshold and closed state.
- src/track.ts creates the Audio element and owns URL/offset/threshold and closed state.
It handles synchronization, play rejection and source updates. It does not import ArtPlayer,
inspect DOM registries or install listeners. Dependencies flow from the entry to the track.
@@ -39,7 +40,7 @@ change; their remaining media validation is tracked in refactor/audio-validation
## Change and verify
Edit track.js for URL/sync/media ownership and index.js for host event handling. Keep
Edit track.ts for URL/sync/media ownership and index.ts for host event handling. Keep
published-only defect observations in the tests when adding candidate regressions.
```sh
@@ -55,4 +56,29 @@ source. These are explicit local artifacts, not isolated installed npm packages.
The browser suite includes frozen old core/plugin combinations, real AAC/decoded video,
source failure recovery, and old/native pause/end plus corrected candidate cleanup.
See refactor/audio-validation.md for failed baseline probes, Windows WebKit WAV/size limits,
physical-device gaps and the separate 04/05/06 type/combination/distribution gates.
physical-device gaps and the remaining 05/06 combination/distribution gates.
## Public type compatibility
The default and /legacy entries preserve the original Result.update(Option) signature,
including required URL inference in Parameters and contextual mock implementations.
Simply widening that argument or adding overloads broke verified old consumers, even though
ordinary old calls still compiled. Do not reintroduce that change without these fixtures.
The additive /runtime entry points to the exact same JS/mjs files and exposes RuntimeFactory
and RuntimeResult.update(UpdateOption), where UpdateOption is Partial<Option>. It does not
add a second player/plugin implementation. Result remains the legacy result type; RuntimeResult
is the opt-in precise update contract. Both keep factory options required and update synchronous.
The .d.cts/.d.mts bridges describe actual CJS/ESM identities; typesVersions keeps old Node10
resolution for /legacy and /runtime. TS 4.3 and 5.9 consumers exercise both paths.
The editor global keeps legacy inference, with an explicit RuntimeFactory type assertion for
scripts opting into partial-update inference. The assertion selects a contract checked against
the same implementation; it does not bypass the plugin's strict source checks. build:ts uses
the shared AST generator for this package and rejects standalone declaration diagnostics in
both compilers. Do not manually edit docs/assets/ts output.
The package .npmignore excludes the implementation tsconfig along with src. Yarn pack must
retain all six declaration files and every historical dist/type path. Audio-04 verified these
files and identical JavaScript bytes to Audio-03; isolated installed consumers and the 8082
demo still belong to Audio-06. See refactor/baselines/audio-types-validation.json for evidence.
@@ -4,7 +4,40 @@ audio-track plugin for ArtPlayer
## Demo
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-audio-track/index.js&example=audio-track)
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-audio-track/index.js&example=audio.track)
## TypeScript
The default import and `/legacy` preserve existing TypeScript behavior, including the
historical required `url` in the `update` parameter type. JavaScript still supports updating
only selected fields. Use `/runtime` for precise partial-update types; it loads the same
implementation and adds no runtime dependency:
```ts
import Artplayer from 'artplayer'
import audioTrack from 'artplayer-plugin-audio-track/runtime'
const installTrack = audioTrack({ url: '/assets/sample/sprite-fight.aac' })
const art = new Artplayer({
container: '.artplayer-app',
url: '/assets/sample/sprite-fight.mp4',
plugins: [(art) => {
const track = installTrack(art)
track.update({ offset: 0.25 })
return track
}],
})
```
Named types include `Option`, `UpdateOption`, `Result` (legacy), `RuntimeResult`, and
`RuntimeFactory`. `update` returns `void`; `audio` remains the same `HTMLAudioElement`.
For global scripts in the online editor, explicitly opt into the same precise typing with
`artplayerPluginAudioTrack as artplayerPluginAudioTrack.RuntimeFactory`.
The plugin follows the main video's playback, volume and rate, but does not automatically
mute the main video. Destroying ArtPlayer releases the plugin's audio and subscriptions;
retained plugin updates become inert. See [ARCHITECTURE.md](ARCHITECTURE.md) for module
ownership, compatibility decisions and regression commands.
## License
@@ -19,19 +19,49 @@
],
"exports": {
".": {
"types": "./types/artplayer-plugin-audio-track.d.ts",
"import": "./dist/artplayer-plugin-audio-track.mjs",
"require": "./dist/artplayer-plugin-audio-track.js"
"import": {
"types": "./types/artplayer-plugin-audio-track.d.mts",
"default": "./dist/artplayer-plugin-audio-track.mjs"
},
"require": {
"types": "./types/artplayer-plugin-audio-track.d.cts",
"default": "./dist/artplayer-plugin-audio-track.js"
}
},
"./legacy": {
"types": "./types/artplayer-plugin-audio-track.d.ts",
"import": "./dist/artplayer-plugin-audio-track.legacy.js",
"require": "./dist/artplayer-plugin-audio-track.legacy.js"
"import": {
"types": "./types/artplayer-plugin-audio-track.d.cts",
"default": "./dist/artplayer-plugin-audio-track.legacy.js"
},
"require": {
"types": "./types/artplayer-plugin-audio-track.d.cts",
"default": "./dist/artplayer-plugin-audio-track.legacy.js"
}
},
"./runtime": {
"import": {
"types": "./types/runtime/index.d.mts",
"default": "./dist/artplayer-plugin-audio-track.mjs"
},
"require": {
"types": "./types/runtime/index.d.cts",
"default": "./dist/artplayer-plugin-audio-track.js"
}
}
},
"main": "dist/artplayer-plugin-audio-track.js",
"module": "./dist/artplayer-plugin-audio-track.mjs",
"types": "types/artplayer-plugin-audio-track.d.ts",
"legacy": "dist/artplayer-plugin-audio-track.legacy.js",
"browserslist": "last 1 Chrome version"
"browserslist": "last 1 Chrome version",
"typesVersions": {
"*": {
"legacy": [
"types/artplayer-plugin-audio-track.d.ts"
],
"runtime": [
"types/runtime/index.d.ts"
]
}
}
}
@@ -1,10 +1,14 @@
import type Artplayer from 'artplayer'
import type { Option, RuntimeResult, UpdateOption } from '../types/artplayer-plugin-audio-track'
import { createAudioTrack } from './track'
export default function artplayerPluginAudioTrack(option) {
return (art) => {
type AudioEvent = 'play' | 'pause' | 'seek' | 'destroy' | 'video:pause' | 'video:ended' | 'video:waiting' | 'video:emptied' | 'video:seeking' | 'video:seeked' | 'video:timeupdate' | 'video:ratechange' | 'video:volumechange' | 'video:playing'
export default function artplayerPluginAudioTrack(option: Option) {
return (art: Artplayer): RuntimeResult => {
const track = createAudioTrack(option)
const { audio } = track
const subscriptions = []
const subscriptions: [AudioEvent, () => void][] = []
let active = true
function syncAudio() {
@@ -12,7 +16,7 @@ export default function artplayerPluginAudioTrack(option) {
track.sync(art.currentTime)
}
function listen(event, callback) {
function listen(event: AudioEvent, callback: () => void) {
const listener = () => {
if (active)
callback()
@@ -25,7 +29,7 @@ export default function artplayerPluginAudioTrack(option) {
if (!active)
return
active = false
const failures = []
const failures: unknown[] = []
for (const [event, listener] of subscriptions.splice(0)) {
try {
art.off(event, listener)
@@ -45,7 +49,7 @@ export default function artplayerPluginAudioTrack(option) {
syncAudio()
track.play()
})
for (const event of ['pause', 'video:pause', 'video:ended', 'video:waiting', 'video:emptied', 'video:seeking'])
for (const event of ['pause', 'video:pause', 'video:ended', 'video:waiting', 'video:emptied', 'video:seeking'] as const)
listen(event, track.pause)
listen('seek', syncAudio)
listen('video:seeked', () => {
@@ -86,7 +90,7 @@ export default function artplayerPluginAudioTrack(option) {
return {
name: 'artplayerPluginAudioTrack',
audio,
update(newOption) {
update(newOption: UpdateOption) {
if (active)
track.update(newOption, art.playing)
},
@@ -1,4 +1,6 @@
export function createAudioTrack(option) {
import type { Option, UpdateOption } from '../types/artplayer-plugin-audio-track'
export function createAudioTrack(option: Option) {
let { url, offset = 0, sync = 0.3 } = option
const audio = new Audio()
let closed = false
@@ -6,7 +8,7 @@ export function createAudioTrack(option) {
function play() {
if (closed || !url)
return
audio.play().catch((error) => {
audio.play().catch((error: unknown) => {
if (!closed)
console.warn(error)
})
@@ -21,7 +23,7 @@ export function createAudioTrack(option) {
if (closed)
return
closed = true
const failures = []
const failures: unknown[] = []
for (const release of [() => audio.pause(), () => audio.removeAttribute('src'), () => audio.load()]) {
try {
release()
@@ -50,14 +52,14 @@ export function createAudioTrack(option) {
play,
pause,
destroy,
sync(time) {
sync(time: number) {
if (closed || !url)
return
const target = time + offset
if (Math.abs(audio.currentTime - target) > sync)
audio.currentTime = target
},
update(newOption, playing) {
update(newOption: UpdateOption, playing: boolean) {
if (closed)
return
if (newOption.url && newOption.url !== url) {
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "allowJs": false },
"include": ["src/**/*.ts", "types/**/*.d.ts", "types/**/*.d.mts", "types/**/*.d.cts"]
}
@@ -0,0 +1,14 @@
/* eslint-disable ts/no-redeclare -- CommonJS callable export and named-type namespace intentionally merge. */
import type * as Definition from './artplayer-plugin-audio-track.js'
declare const artplayerPluginAudioTrack: typeof Definition.default
declare namespace artplayerPluginAudioTrack {
type Option = Definition.Option
type UpdateOption = Definition.UpdateOption
type Result = Definition.Result
type RuntimeResult = Definition.RuntimeResult
type RuntimeFactory = Definition.RuntimeFactory
}
export = artplayerPluginAudioTrack
@@ -0,0 +1,2 @@
export { default } from './artplayer-plugin-audio-track.cjs'
export type { Option, Result, RuntimeFactory, RuntimeResult, UpdateOption } from './artplayer-plugin-audio-track.js'
@@ -1,6 +1,6 @@
import type Artplayer from 'artplayer'
interface Option {
export interface Option {
/**
* Audio track URL
*/
@@ -21,7 +21,9 @@ interface Option {
sync?: number
}
interface Result {
export type UpdateOption = Partial<Option>
export interface Result {
name: 'artplayerPluginAudioTrack'
/**
* The audio element
@@ -29,11 +31,20 @@ interface Result {
audio: HTMLAudioElement
/**
* Update option
* Historical update signature. Runtime also accepts partial options.
* Import the /runtime entry for the precise partial-update signature.
*/
update: (option: Option) => void
}
declare const artplayerPluginAudioTrack: (option: Option) => (art: Artplayer) => Result
export interface RuntimeResult extends Result {
/** Update selected fields without replacing the audio element. */
update: (option: UpdateOption) => void
}
/** Precise typing for the same runtime factory, without changing legacy inference. */
export type RuntimeFactory = (option: Option) => (art: Artplayer) => RuntimeResult
declare function artplayerPluginAudioTrack(option: Option): (art: Artplayer) => Result
export default artplayerPluginAudioTrack
@@ -0,0 +1,14 @@
/* eslint-disable ts/no-redeclare -- CommonJS callable export and named-type namespace intentionally merge. */
import type * as Definition from '../artplayer-plugin-audio-track.js'
declare const artplayerPluginAudioTrack: Definition.RuntimeFactory
declare namespace artplayerPluginAudioTrack {
type Option = Definition.Option
type UpdateOption = Definition.UpdateOption
type Result = Definition.Result
type RuntimeResult = Definition.RuntimeResult
type RuntimeFactory = Definition.RuntimeFactory
}
export = artplayerPluginAudioTrack
@@ -0,0 +1,2 @@
export type { Option, Result, RuntimeFactory, RuntimeResult, UpdateOption } from '../artplayer-plugin-audio-track.js'
export { default } from './index.cjs'
@@ -0,0 +1,5 @@
import type { RuntimeFactory } from '../artplayer-plugin-audio-track'
declare const artplayerPluginAudioTrack: RuntimeFactory
export default artplayerPluginAudioTrack
export type { Option, Result, RuntimeFactory, RuntimeResult, UpdateOption } from '../artplayer-plugin-audio-track'
+12
View File
@@ -70,3 +70,15 @@ Windows WebKit 也给出错误 4;AAC 矩阵通过不能覆盖该 WAV 或真实
浏览器测试均共用端口 8084 和输出目录;不要同时执行多个 suite。开始下一次前复制报告
和结果目录(包含 trace),并记录对应测试输入。Node CI 可独立并行执行。
## PKG-AUDIO-04 类型与结束事件用例补强
源文件已迁移为严格 TS。默认/legacy 保留旧 update 的必填 URL 推断;/runtime 与编辑器
RuntimeFactory 提供准确的部分更新契约,实际运行函数不变。五组编译消费和 Monaco
三引擎验证见 [类型交付](changes/2026-09-12-PKG-AUDIO-04-types.md) 及
[冻结证据](baselines/audio-types-validation.json),不要用早期 JS 状态覆盖当前状态。
legacy 首次 WebKit 的“候选核心 + 已发布 audio”结束观察失败,原始证据保留。
两种结束用例现统一先确认视频/音频 seek 完成且就绪,再确认音频实际恢复推进后才
观测 video.ended,并记录宿主事件;避免原测试因音频早已暂停而误判。相关 WebKit
12 次及最终 main/legacy 各 42 项通过。未证明首次暂停的确切来源,未改生产代码。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,66 @@
# PKG-AUDIO-04:严格 TS 与兼容公开类型
状态:done;起点 9c07ea83;完成后独立本地提交,不推送或发布。
迁移 index/track 两个职责模块,不改变上一任务已验证的运行时逻辑。
公开 Option/Result/UpdateOption,保留必传工厂参数、同一 HTMLAudioElement 和同步 update。
默认入口保留旧 Result 类型;新增 /runtime 类型入口描述部分 update,检验 Parameters
提取、旧结构赋值和无注解回调推断。两入口复用相同 JS/mjs 文件。
保持 root/legacy 运行时路径,补 CJS/ESM 类型桥及由公开声明生成的编辑器全局类型。
严格源码、TS 4.3/5.9 消费模式、负例、Monaco 语义/运行、实际产物浏览器回归已通过。
冻结证据见 [audio-types-validation.json](../baselines/audio-types-validation.json)。
## 兼容方案依据
实际编译用例证明直接 Partial 或“Partial 在前、Option 在后”重载会使旧上下文实现
`update(options) { audio.src = options.url }` 推断 url 为 string | undefined。
虽然显式注解和 Parameters 通过,它仍是公开类型回归。泛型替代还产生 implicit any
或放过未知字段,因此未采用;保留了这些失败日志,不通过删用例宣称兼容。
最终沿用核心的默认兼容类型 + /runtime 精确类型方式,运行入口完全相同,源码可严格
赋值为两种工厂契约,无 any 或方法双变协变豁免。编辑器通过 RuntimeFactory 显式选择
精确语义,原全局类型仍保留旧推断。
## 实施与验收
- index.ts 管理宿主事件及订阅;track.ts 管理音频元素与状态。事件使用明确的字面量
联合,异常使用 unknown。独立 tsconfig 启用严格检查且不接受 JS 源码。
- 保留原 root/legacy 运行文件与 main/module/types/legacy 字段;新增 CJS/ESM 声明桥、
TS 4.3 的 typesVersions 和复用原 JS 的 /runtime 入口。未新增依赖。
- `yarn build:ts` 生成编辑器声明,独立 TS 5.9.3/4.3.5 检查通过。五组消费方式各
12 个错误用法被拒绝;旧显式/上下文实现、Parameters 提取、README 真实 TS 示例、
部分 update 和同步 void 通过。三个专用类型测试通过。
- 实际 require/import 证明 root 与 /runtime 是同一个函数;legacy 可调用。
Monaco worker 三引擎验证 core/HLS/audio,共 9 项通过,包含语义正反例与脚本运行。
- 完整 `yarn ci:check` 586 项通过(540 单元、11 工程、35 基线),256 个生产 TS 文件
纳入严格检查。随后增加的 README 编译断言通过专用测试,最终浏览器辅助函数另经
定向 lint 和两个完整产物矩阵验证。既有核心声明有一条未使用禁用指令警告。
- `yarn build artplayer-plugin-audio-track` 和三格式 Node 89 项通过;正式 main/legacy
三引擎最终各 42 项通过,无重试或跳过。每组包含 9 项旧缺陷观察、3 项 WAV 能力诊断,
不将它们全部计为候选无缺陷验收。三种 JS 与 Audio-03 SHA256 完全相同,docs/compiled
与包产物逐字节一致,类型迁移没有引入运行时代码。
- 实际 Yarn pack 曾检出 tsconfig.json 泄漏,已加入该包 .npmignore。修正后 12 个文件
通过既有 checkFiles/packedFiles 校验:6 份声明全量包含且与源码相同、所有 exports
有目标、历史分发文件保留、无 src/tsconfig。完整隔离安装消费仍由 Audio-06 验收。
## 浏览器失败的处理
首次 legacy 为 41 通过、1 失败:WebKit 上“候选核心 + 已发布 audio”的音频在视频
结束断言前已暂停。这一组合未加载迁移后的 audio;首次 trace 未记录宿主事件,不能
断言已证明具体原因。原始报告与 trace 保留在 .cache/audio04-browser-legacy-first*。
原辅助流程将暂停后 seek 与重新播放挨在一起,缺少 seek 完成和音频实际恢复前提,
既可能误报旧版,也可能让候选因早已暂停而假通过。现在两种用例统一等待双方 seek
完成、readyState >= 3,并确认音频已恢复且时间推进,再观测视频结束;新增宿主事件
记录,未放宽超时、增加重试或修改生产代码。WebKit 相关 4 项各重复 3 次通过,随后
main/legacy 两套完整矩阵通过。增强后的用例通过不等于原生暂停的确切原因已定位。
## 兼容范围、后续与回退
API-09/11 采用 ADR-023:默认旧类型刻意保持 url 必填推断,准确的部分更新类型通过
/runtime 或编辑器 RuntimeFactory 显式选择;AUDIO-TYPE-01 按此兼容范围接受。
README demo 名称改为现存 audio.track,尚无本次 8082 页面验收,AUDIO-DEMO-01 保持开放。
负偏移/时长边界、真实缓冲与连续切源继续 Audio-05,隔离安装/实际 demo 继续 Audio-06。
未修改版本;各包下一 major 由 REL-09 落地,物理设备及发布复盘门槛不变。
回退本任务独立提交可恢复 Audio-03 的 JS 与旧声明;正式 JS 产物未变,不需回退
音频生命周期修复。共享生成器对 core/HLS 的回归已验证。
+1
View File
@@ -27,6 +27,7 @@
| ADR-021 | 采用方向 | 用户指定 Yarn,固定 Classic 1.22.22 和唯一 yarn.lock | 与原 v1 锁格式一致;保留 Node 24.21.0 和既有依赖版本,ENG-PM-01 验证;替代 ENG-01 的 npm 默认工具选择 |
| ADR-022 | 采用方向 | Chrome 不可用时使用 Codex 内置浏览器 | 用户明确授权;记录实际环境和能力范围,必要设备测试仍独立验收 |
| ADR-023 | 已验证于 Audio Track | 参数类型纠正会破坏旧推断时,默认入口保留兼容类型,/runtime 复用相同运行文件提供精确类型 | PKG-AUDIO-04 的 Parameters 与无注解 update 实现证明 Partial/重载仍可能破坏旧用户;沿用核心的类型分层方向,具体包需独立编译与入口身份验证,不增加第二套实现或用 any 绕过 |
## 开始实现前需补齐的决定
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 216 项,范围 22 个包及工作区/示例。
状态:todo 147 / doing 1 / blocked 0 / done 68 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 146 / doing 1 / blocked 0 / done 69 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -158,7 +158,7 @@
| PKG-AUDIO-01 | artplayer-plugin-audio-track<br>核对包契约与历史用法 | BASE-05 | url/offset/sync、audio 实例暴露、update 与音量语义 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-AUDIO-02 | artplayer-plugin-audio-track<br>建立特有行为与错误测试 | PKG-AUDIO-01, ENG-03, ENG-05 | 偏移、倍率、seek、waiting/playing、加载错误和 autoplay 拒绝 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-AUDIO-03 | artplayer-plugin-audio-track<br>整理内部职责与资源 | PKG-AUDIO-02, CORE-10 | 外部 audio 的同步策略、监听与源更新生命周期 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-AUDIO-04 | artplayer-plugin-audio-track<br>迁移自有源码和公开类型 | PKG-AUDIO-03, ENG-04, ENG-06, CORE-07 | Option/Result/audio、update 输入和事件类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-AUDIO-04 | artplayer-plugin-audio-track<br>迁移自有源码和公开类型 | PKG-AUDIO-03, ENG-04, ENG-06, CORE-07 | Option/Result/audio、update 输入和事件类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-AUDIO-05 | artplayer-plugin-audio-track<br>验证新旧核心和组合 | PKG-AUDIO-04, CORE-22 | 主视频切源和缓冲恢复,多实例独立音频、旧核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-AUDIO-06 | artplayer-plugin-audio-track<br>验证分发并同步文档 | PKG-AUDIO-05, ENG-07 | audio.track.js 示例与运行返回值一致的声明 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -469,6 +469,7 @@
- PKG-AUDIO-01: [记录](changes/2026-09-12-PKG-AUDIO-01-contract.md) [记录](baselines/audio-track-contract.md) [记录](baselines/audio-track-release.json)
- PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.json)
- PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json)
- PKG-AUDIO-04: [记录](changes/2026-09-12-PKG-AUDIO-04-types.md) [记录](baselines/audio-types-validation.json)
- PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md)
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
+17
View File
@@ -1,5 +1,22 @@
# 进度与证据
## 最新完成:PKG-AUDIO-04 严格 TS 与兼容类型入口
Audio 两个职责模块已迁移 TS,累计 256 个生产 TS。默认/legacy 保留原 update 类型
推断;新增 /runtime 精确入口支持部分配置,复用同一 JS/mjs。直接拓宽类型会破坏旧
上下文实现的证据及决定已写入 ADR-023,编辑器生成和 README 同步。
完整 CI 586 项通过,五组 TS 消费各 12 个负例、专用类型 3 项、Monaco 三引擎 9 项
通过;三格式 Node 89 项、最终 main/legacy 各 42 项通过,三种 JS 与 Audio-03 字节
相同。结束用例首轮 WebKit 失败保留并补强 seek/恢复前提,未靠重试关闭问题。
真实 Yarn pack 修正 tsconfig 泄漏后保留全部 6 份声明及历史文件;隔离安装仍待 Audio-06。
当前 216 项、69 done、1 doing、146 todo。Audio-03 提交 9c07ea83 已核实,Audio-04
独立提交核实后进入 05 的边界/缓冲/切源/多实例。AUDIO-TYPE-01 按兼容范围接受,
AUDIO-SYNC-01/DEMO-01、HLS 两项 Firefox 风险和全局发布门槛仍开放。
详见 [交付记录](changes/2026-09-12-PKG-AUDIO-04-types.md) 和
[冻结证据](baselines/audio-types-validation.json)。无 push/tag/publish/merge。
## 最新完成:PKG-AUDIO-03 源码拆分与生命周期修复
Audio Track 已拆为入口事件管理与独立媒体状态两个 JS 模块;修复原生暂停/结束后
+1 -1
View File
@@ -128,5 +128,5 @@
| HLS-PLAYBACK-01 | open / 已复现 | Firefox 旧核心与 Hls 1.7.2 切组后视频档位未完成切换 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 |
| AUDIO-LIFE-01 | resolved / 已复现 | Audio Track 销毁后 update 与媒体/订阅缺少终止守卫 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 |
| AUDIO-SYNC-01 | open / 已复现 | Audio Track 未同步原生暂停/结束,偏移边界与切源/缓冲仍待完整验证 | PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-05 |
| AUDIO-TYPE-01 | open / 源码/产物事实 | Audio Track update 部分配置与公开/编辑器声明不一致 | PKG-AUDIO-04 |
| AUDIO-TYPE-01 | accepted-with-scope / 源码/产物事实 | Audio Track update 部分配置与公开/编辑器声明不一致 | PKG-AUDIO-04 |
| AUDIO-DEMO-01 | open / 源码/产物事实 | Audio Track README 示例链接名称不匹配且缺维护说明 | PKG-AUDIO-06 |
+9 -3
View File
@@ -2695,7 +2695,7 @@
"id": "AUDIO-TYPE-01",
"title": "Audio Track update 部分配置与公开/编辑器声明不一致",
"confirmation": "source-observed",
"status": "open",
"status": "accepted-with-scope",
"owners": [
"PKG-AUDIO-04"
],
@@ -2703,8 +2703,14 @@
"refactor/baselines/audio-track-contract.md",
"refactor/baselines/audio-track-release.json"
],
"compatibleResolution": "拓宽已有 JS 合法部分 update,保留 url/offset/sync 字段精度和 Parameters/ReturnType 提取,生成语义正确编辑器声明。",
"closureCriteria": "当前与旧编译器的正反例、CJS/ESM/legacy 入口及实际编辑器通过。"
"compatibleResolution": "ADR-023:默认/legacy 保留旧必填 url 推断与上下文实现;新增 /runtime 复用同一 JS,精确支持部分 update,编辑器通过 RuntimeFactory 显式选择。",
"closureCriteria": "当前与旧编译器的正反例、CJS/ESM/legacy 入口及实际编辑器通过。",
"resolutionRationale": "直接拓宽或重载会使旧上下文实现的 url 成为 string | undefined,已用实际编译复现。五组 TS 正反例、README、CJS/ESM 同一函数、真实 Monaco 三引擎已通过。接受默认旧声明有意保持窄签名的范围,不声称所有入口默认支持部分 update;隔离安装和完整分发仍由 Audio-06 验证。",
"resolutionEvidence": [
"refactor/changes/2026-09-12-PKG-AUDIO-04-types.md",
"refactor/baselines/audio-types-validation.json",
"refactor/decisions.md"
]
},
{
"id": "AUDIO-DEMO-01",
+76
View File
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import { createRequire } from 'node:module'
// eslint-disable-next-line test/no-import-node-test -- Package consumers run in the baseline runner.
import test from 'node:test'
import { ESLint } from 'eslint'
import ts from 'typescript'
import compat from 'typescript-compat'
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
import { checkConsumer } from '../../scripts/typecheck.mjs'
test('Audio Track preserves root/legacy, historical extracted inputs and partial update across compilers', () => {
const source = fs.readFileSync('test/types/audio-track.ts', 'utf8')
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]) {
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
assert.equal(checkConsumer(compiler, mode, source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')).length, 12, `${compiler.version} ${mode} must reject invalid inputs`)
}
const cjs = `import audio = require('artplayer-plugin-audio-track');
import legacy = require('artplayer-plugin-audio-track/legacy');
import runtime = require('artplayer-plugin-audio-track/runtime');
import Artplayer = require('artplayer');
const option: audio.Option = { url: 'audio.aac' };
const update: audio.UpdateOption = { offset: 1 };
const art = new Artplayer({ container: '#player', url: 'video.mp4' });
const result: audio.Result = audio(option)(art);
result.update(option); legacy(option)(art).update(option);
const precise: runtime.RuntimeResult = runtime(option)(art); precise.update(update);`
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', cjs), [])
const readme = fs.readFileSync('packages/artplayer-plugin-audio-track/README.md', 'utf8').match(/```ts\n([\s\S]*?)```/)?.[1]
assert(readme, 'Keep the documented runtime type example runnable')
assert.deepEqual(checkConsumer(ts, 'bundler-esm', readme), [])
assert.deepEqual(checkConsumer(compat, 'node10-commonjs', readme), [])
})
test('Audio Track editor declaration is reproducible and validates partial/legacy calls without leaked types', async () => {
const source = fs.readFileSync('packages/artplayer-plugin-audio-track/types/artplayer-plugin-audio-track.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginAudioTrack')
const file = 'docs/assets/ts/artplayer-plugin-audio-track.d.ts'
const [formatted] = await new ESLint({ fix: true, fixTypes: ['layout'] }).lintText(generated, { filePath: file })
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated, 'Run yarn build:ts')
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
const consumer = `
const option: artplayerPluginAudioTrack.Option = { url: 'audio.aac' };
const part: artplayerPluginAudioTrack.UpdateOption = { offset: 1 };
const factory = artplayerPluginAudioTrack as artplayerPluginAudioTrack.RuntimeFactory;
const result = factory(option)(new Artplayer({ container: '#player', url: 'video.mp4' }));
result.update(part);
const historical: Parameters<artplayerPluginAudioTrack.Result['update']>[0] = { url: 'old.aac' };
const oldURL: string = historical.url;
// @ts-expect-error Update fields stay typed.
result.update({ offset: 'bad' });
// @ts-expect-error Factory options are required.
artplayerPluginAudioTrack();
// @ts-expect-error Private definitions do not become globals.
const hidden: artplayerPluginAudioTrackDefinitions.Option = { url: 'audio.aac' };
void [oldURL, hidden];`
for (const compiler of [ts, compat]) {
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [], compiler.version)
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 3)
assert(checkPluginEditorDeclaration(`${generated}\nexport interface Broken { value: MissingType }`, core, '', compiler).some(item => item.code === 2309))
}
})
test('Audio Track runtime type entry reuses the exact root runtime for CJS and ESM', async () => {
const manifest = JSON.parse(fs.readFileSync('packages/artplayer-plugin-audio-track/package.json'))
assert.equal(manifest.exports['./runtime'].require.default, manifest.exports['.'].require.default)
assert.equal(manifest.exports['./runtime'].import.default, manifest.exports['.'].import.default)
const require = createRequire(import.meta.url)
assert.equal(require('artplayer-plugin-audio-track/runtime'), require('artplayer-plugin-audio-track'))
const [root, runtime] = await Promise.all([import('artplayer-plugin-audio-track'), import('artplayer-plugin-audio-track/runtime')])
assert.equal(runtime.default, root.default)
assert.equal(typeof root.default, 'function')
const legacy = require('artplayer-plugin-audio-track/legacy')
assert.equal(typeof legacy, 'function')
})
+5 -2
View File
@@ -1533,11 +1533,14 @@
"ENG-06",
"CORE-07"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "Option/Result/audio、update 输入和事件类型",
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-AUDIO-04-types.md",
"baselines/audio-types-validation.json"
]
},
{
"id": "PKG-AUDIO-05",
+6
View File
@@ -80,4 +80,10 @@ allowJs=true、checkJs=false 允许渐进迁移时引用旧 JS;只将迁移的
## 后续接入
Audio Track 的自有源码由 packages/artplayer-plugin-audio-track/tsconfig.json 严格检查。
默认/legacy 类型保留历史 update 的完整 Option;新增 /runtime 复用同一运行文件,
提供 UpdateOption/RuntimeResult。test/types/audio-track.ts 同时验证旧 Parameters、
有/无注解结构实现及精确部分调用;audio-track-source.ts 检查实现满足两种声明。
详见包内 ARCHITECTURE.md;不能将部分参数直接覆盖默认签名而破坏旧推断。
新迁移包复制分包配置并按实际运行环境调整,加入特有正反例;编译器入口自动发现包配置。TS 构建入口与声明生成由 ENG-06/07 和各包迁移处理;不能只增加一个空 config 就宣布包迁移完成。所有生产变更仍按每任务独立提交和包内架构文档规则交付。
+3 -2
View File
@@ -43,10 +43,11 @@ console.log(`✨ Built ${artplayerTSoutput}`);
const type = pluginsTS[index]
const { name, file } = parsePluginInfo(type)
const source = String(fs.readFileSync(type))
const code = name === 'artplayerPluginHlsControl'
const semanticPlugin = ['artplayerPluginHlsControl', 'artplayerPluginAudioTrack'].includes(name)
const code = semanticPlugin
? generatePluginEditorDeclaration(source, name)
: `${source.replace(reg, '')}\nexport = ${name};\nexport as namespace ${name};\n`
if (name === 'artplayerPluginHlsControl') {
if (semanticPlugin) {
const core = fs.readFileSync(artplayerTSoutput, 'utf8')
const diagnostics = [...checkPluginEditorDeclaration(code, core), ...checkPluginEditorDeclaration(code, core, '', compat)]
if (diagnostics.length)
+1 -1
View File
@@ -73,7 +73,7 @@ export function runTypechecks() {
console.log(`Strict project passed: ${relative(config)} (${result.files.length} root files)`)
}
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'bundler-esm'], [ts, 'nodenext-esm'], [compat, 'node10-commonjs']]) {
for (const fixture of ['test/types/public.ts', 'test/types/declaration-inputs.ts', 'test/types/declaration-legacy.ts', 'test/types/plugins-public.ts', 'test/types/playback-public.ts', 'test/types/chapter-options.ts', 'test/types/chapter-exports.ts', 'test/types/hls-control.ts', 'test/types/language-value.ts', 'refactor/fixtures/consumers/language.ts', 'refactor/fixtures/consumers/legacy-plugin.ts']) {
for (const fixture of ['test/types/public.ts', 'test/types/declaration-inputs.ts', 'test/types/declaration-legacy.ts', 'test/types/plugins-public.ts', 'test/types/playback-public.ts', 'test/types/chapter-options.ts', 'test/types/chapter-exports.ts', 'test/types/hls-control.ts', 'test/types/audio-track.ts', 'test/types/language-value.ts', 'refactor/fixtures/consumers/language.ts', 'refactor/fixtures/consumers/legacy-plugin.ts']) {
assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, fixture), 'utf8')), [], `Consumer failed: TS ${compiler.version} ${mode} ${fixture}`)
}
if (mode === 'nodenext-cjs')
+77
View File
@@ -0,0 +1,77 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
test('Monaco preserves legacy audio inference and runs the precise typed factory', async ({ page }, testInfo) => {
const code = process.env.ARTPLAYER_AUDIO_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_AUDIO_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-audio-track', 'umd')
const declarations = Object.fromEntries(['artplayer', 'artplayer-plugin-audio-track'].map(name => [name, hash(fs.readFileSync(new URL(`../../docs/assets/ts/${name}.d.ts`, import.meta.url)))]))
await testInfo.attach('audio-editor-inputs', { contentType: 'application/json', body: JSON.stringify({ candidate: process.env.ARTPLAYER_AUDIO_ARTIFACT || 'workspace source build', sha256: hash(code), declarations }) })
await page.goto('/test/player.html?core=candidate&chapter=published')
await page.addScriptTag({ content: code })
await page.addScriptTag({ url: '/assets/js/vs/loader.js' })
const result = await page.evaluate(async () => {
window.require.config({ paths: { vs: '/assets/js/vs' } })
await new Promise((resolve, reject) => window.require(['vs/editor/editor.main'], resolve, reject))
const api = window.monaco
const types = api.languages.typescript
types.typescriptDefaults.setCompilerOptions({ target: types.ScriptTarget.ES2020, strict: true, skipLibCheck: false, noEmit: false, types: [], allowNonTsExtensions: true })
const libraries = await Promise.all(['artplayer', 'artplayer-plugin-audio-track'].map(async (name) => {
const uri = `file:///${name}.d.ts`
const source = await (await fetch(`/assets/ts/${name}.d.ts`)).text()
return { uri, handle: types.typescriptDefaults.addExtraLib(source, uri) }
}))
const model = api.editor.createModel(`
const typedFactory = artplayerPluginAudioTrack as artplayerPluginAudioTrack.RuntimeFactory;
const editorArt = new Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true });
const option: artplayerPluginAudioTrack.Option = { url: '/test/audio-tone.m4a' };
const legacyInput: Parameters<artplayerPluginAudioTrack.Result['update']>[0] = option;
const legacyURL: string = legacyInput.url;
const implemented: ReturnType<ReturnType<typeof artplayerPluginAudioTrack>> = {
name: 'artplayerPluginAudioTrack', audio: new Audio(), update(options) { this.audio.src = options.url; }
};
const pluginResult = typedFactory(option)(editorArt);
const done: void = pluginResult.update({ offset: 0.25 });
`, 'typescript', api.Uri.parse('file:///audio-consumer.ts'))
const invalid = api.editor.createModel(`typedFactory({});
pluginResult.update({ sync: 'fast' });
artplayerPluginAudioTrack();`, 'typescript', api.Uri.parse('file:///audio-invalid.ts'))
try {
const worker = await (await types.getTypeScriptWorker())(model.uri, invalid.uri)
const syntax = await worker.getSyntacticDiagnostics(model.uri.toString())
const semantic = await worker.getSemanticDiagnostics(model.uri.toString())
const declarations = (await Promise.all(libraries.map(lib => worker.getSemanticDiagnostics(lib.uri)))).flat()
const bad = await worker.getSemanticDiagnostics(invalid.uri.toString())
if (syntax.length || semantic.length || declarations.length)
return { syntax, semantic, declarations, invalid: bad.map(item => item.code), executed: false }
const output = await worker.getEmitOutput(model.uri.toString())
const script = output.outputFiles.find(file => file.name.endsWith('.js'))
if (!script)
throw new Error('Monaco did not emit the audio consumer')
// eslint-disable-next-line no-new-func -- Run the real locally emitted editor script.
const execution = new Function(`${script.text}\nreturn { art: editorArt, plugin: pluginResult, done }`)()
try {
if (!execution.art.isReady)
await new Promise(resolve => execution.art.on('ready', resolve))
return { syntax, semantic, declarations, invalid: bad.map(item => item.code), executed: execution.art.isReady && execution.plugin.audio instanceof HTMLAudioElement && execution.done === undefined }
}
finally {
execution.art.destroy()
}
}
finally {
model.dispose()
invalid.dispose()
for (const library of libraries)
library.handle.dispose()
}
})
expect(result.syntax).toEqual([])
expect(result.semantic).toEqual([])
expect(result.declarations).toEqual([])
expect(result.invalid).toEqual([2345, 2322, 2554])
expect(result.executed).toBe(true)
})
+17 -11
View File
@@ -46,7 +46,7 @@ test.afterEach(async ({ page }, testInfo) => {
const audio = window.audioPlugin?.audio
const video = window.art?.video
const media = element => element && ({ src: element.currentSrc, attribute: element.getAttribute('src'), time: element.currentTime, paused: element.paused, readyState: element.readyState, networkState: element.networkState, error: element.error?.code, rate: element.playbackRate, seekable: Array.from({ length: element.seekable.length }, (_, index) => [element.seekable.start(index), element.seekable.end(index)]) })
return { audio: media(audio), video: media(video), events: window.audioEvents, warnings: window.audioWarnings }
return { audio: media(audio), video: media(video), events: window.audioEvents, hostEvents: window.audioHostEvents, warnings: window.audioWarnings }
}).catch(error => ({ error: error.message }))
await testInfo.attach('audio-state', { contentType: 'application/json', body: JSON.stringify(state) })
})
@@ -57,6 +57,7 @@ async function openAudio(page, core, plugin, testInfo, url = '/test/audio-tone.m
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
await page.evaluate((url) => {
window.audioEvents = []
window.audioHostEvents = []
window.audioWarnings = []
const warn = console.warn.bind(console)
console.warn = (...args) => {
@@ -70,6 +71,8 @@ async function openAudio(page, core, plugin, testInfo, url = '/test/audio-tone.m
plugins: [window.artplayerPluginAudioTrack({ url, sync: 0.1 })],
})
window.audioPlugin = window.art.plugins.artplayerPluginAudioTrack
for (const event of ['play', 'pause', 'seek', 'video:waiting', 'video:playing', 'video:seeking', 'video:seeked', 'video:pause', 'video:ended'])
window.art.on(event, () => window.audioHostEvents.push({ event, videoTime: window.art.currentTime, audioTime: window.audioPlugin.audio.currentTime, audioPaused: window.audioPlugin.audio.paused }))
for (const event of ['loadedmetadata', 'playing', 'pause', 'seeked', 'error', 'emptied'])
window.audioPlugin.audio.addEventListener(event, () => window.audioEvents.push({ event, time: window.audioPlugin.audio.currentTime, src: window.audioPlugin.audio.currentSrc }))
document.querySelector('#play').onclick = () => window.art.play()
@@ -79,6 +82,17 @@ async function openAudio(page, core, plugin, testInfo, url = '/test/audio-tone.m
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.readyState)).toBeGreaterThanOrEqual(2)
}
async function playToEnd(page) {
await page.evaluate(() => {
window.art.pause()
window.art.seek = 7
})
await expect.poll(() => page.evaluate(() => !window.art.video.seeking && !window.audioPlugin.audio.seeking && window.art.video.readyState >= 3 && window.audioPlugin.audio.readyState >= 3)).toBe(true)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => !window.audioPlugin.audio.paused && window.audioPlugin.audio.currentTime > 7.05)).toBe(true)
await expect.poll(() => page.evaluate(() => window.art.video.ended)).toBe(true)
}
test('native WAV diagnostic isolates sample decoding from ArtPlayer and plugin', async ({ page }, testInfo) => {
await page.route('**/native-wave.wav', route => route.fulfill({ contentType: 'audio/wav', body: wav }))
await page.goto('/test/player.html')
@@ -126,11 +140,7 @@ for (const core of ['published', 'candidate']) {
window.art.video.currentTime = 4
})
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.currentTime)).toBeCloseTo(4, 1)
await page.evaluate(() => {
window.art.seek = 7.8
window.art.play()
})
await expect.poll(() => page.evaluate(() => window.art.video.ended)).toBe(true)
await playToEnd(page)
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.paused)).toBe(true)
await page.evaluate(() => window.art.destroy())
await expect.poll(() => page.evaluate(() => window.audioPlugin.audio.networkState)).toBe(0)
@@ -148,11 +158,7 @@ for (const core of ['published', 'candidate']) {
await page.evaluate(() => window.art.video.pause())
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(false)
await page.evaluate(() => {
window.art.seek = 7.8
window.art.play()
})
await expect.poll(() => page.evaluate(() => window.art.video.ended)).toBe(true)
await playToEnd(page)
expect(await page.evaluate(() => window.audioPlugin.audio.paused)).toBe(false)
await testInfo.attach('published-native-pause-end', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ videoEnded: window.art.video.ended, videoPaused: window.art.video.paused, audioPaused: window.audioPlugin.audio.paused, audioTime: window.audioPlugin.audio.currentTime }))) })
await page.evaluate(() => window.art.destroy())
+7
View File
@@ -0,0 +1,7 @@
import type artplayerPluginAudioTrack from 'artplayer-plugin-audio-track'
import type { RuntimeFactory } from 'artplayer-plugin-audio-track'
import factory from '../../packages/artplayer-plugin-audio-track/src/index'
const legacyContract: typeof artplayerPluginAudioTrack = factory
const runtimeContract: RuntimeFactory = factory
void [legacyContract, runtimeContract]
+63
View File
@@ -0,0 +1,63 @@
import type { Option, Result, RuntimeFactory, RuntimeResult, UpdateOption } from 'artplayer-plugin-audio-track'
import Artplayer from 'artplayer'
import audioTrack from 'artplayer-plugin-audio-track'
import legacy from 'artplayer-plugin-audio-track/legacy'
import runtime from 'artplayer-plugin-audio-track/runtime'
const option: Option = { url: 'audio.aac', offset: 0, sync: 0.3 }
const art = new Artplayer({ container: '#player', url: 'video.mp4', plugins: [audioTrack(option), legacy(option)] })
const plugin: RuntimeResult = runtime(option)(art)
const precise: RuntimeFactory = runtime
const rootResult: Result = audioTrack(option)(art)
rootResult.update(option)
const element: HTMLAudioElement = plugin.audio
const update: UpdateOption = { offset: -0.5, sync: undefined }
const done: void = plugin.update(update)
plugin.update({})
plugin.update({ url: 'next.aac' })
plugin.update({ offset: undefined })
const historicalOption: Parameters<typeof audioTrack>[0] = option
const historicalUpdate: Parameters<Result['update']>[0] = option
const historicalURL: string = historicalUpdate.url
type HistoricalResult = ReturnType<ReturnType<typeof audioTrack>>
const implemented: HistoricalResult = {
name: 'artplayerPluginAudioTrack',
audio: element,
update: (options: Option) => { element.src = options.url },
}
const contextual: HistoricalResult = {
name: 'artplayerPluginAudioTrack',
audio: element,
update(options) {
element.src = options.url
},
}
const unbound = plugin.update
unbound({ sync: 0 })
const legacyPlugin: Result = legacy(option)(art)
// @ts-expect-error The factory still requires its options object.
audioTrack()
// @ts-expect-error A factory configuration still requires a URL.
audioTrack({})
// @ts-expect-error URLs are strings.
audioTrack({ url: 123 })
// @ts-expect-error update still requires an object argument.
plugin.update()
// @ts-expect-error Threshold values are numbers.
plugin.update({ sync: 'fast' })
// @ts-expect-error Null URLs are not part of the old public contract.
plugin.update({ url: null })
// @ts-expect-error The exposed media remains a real audio element.
plugin.audio = 123
// @ts-expect-error update stays synchronous.
const promise: Promise<void> = plugin.update({ offset: 1 })
// @ts-expect-error Plugin name stays a literal.
const name: Result['name'] = 'audio'
// @ts-expect-error Historical extracted factory URL stays required.
const missingURL: Parameters<typeof audioTrack>[0]['url'] = undefined
// @ts-expect-error Unknown option fields do not gain an index signature.
plugin.update({ unknown: true })
// @ts-expect-error Legacy import retains the same strict configuration.
legacy({ url: 'audio.aac', offset: 'later' })
void [done, historicalOption, historicalURL, implemented, contextual, legacyPlugin, promise, name, missingURL, precise]