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

@@ -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'