mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
refactor(audio): [PKG-AUDIO-04] migrate strict source and compatible type entries
This commit is contained in:
1 parent
9c07ea830c
commit
13c898c149
31 files changed
+18923
-90
No files matched your search
+48
-36
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-7
@@ -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)
|
||||
},
|
||||
+7
-5
@@ -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'
|
||||
+15
-4
@@ -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'
|
||||
@@ -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 的回归已验证。
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 模块;修复原生暂停/结束后
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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())
|
||||
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
Reference in new issue
Block a user