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

+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]