mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-14 00:06:17 -08:00
refactor(jassub): [PKG-JASSUB-04] migrate owned TypeScript and preserve public types
This commit is contained in:
1 parent
38e7b37878
commit
5e445b1af6
33 files changed
+3636
-61
No files matched your search
@@ -0,0 +1,236 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Accurate runtime contracts use the repository runner.
|
||||
import test from 'node:test'
|
||||
import { isPromise } from 'node:util/types'
|
||||
import vm from 'node:vm'
|
||||
import { jassubCandidate, jassubEnvironment, jassubHistorical } from './helpers/jassub.js'
|
||||
|
||||
const candidate = await jassubCandidate()
|
||||
const frozen = (await jassubHistorical()).find(item => item.name === 'frozen-workspace-source')
|
||||
|
||||
function environment(implementation) {
|
||||
const code = `${implementation.code}\nObject.defineProperty(module.exports.default || module.exports, '__runtimePlatform', { value: { ErrorEvent, setGlobal: (name, value) => { globalThis[name] = value } } });`
|
||||
const env = jassubEnvironment({ ...implementation, code })
|
||||
env.platform = env.factory.__runtimePlatform
|
||||
return env
|
||||
}
|
||||
|
||||
function requestListeners(worker) {
|
||||
const listeners = new Map()
|
||||
worker.addEventListener = (type, callback) => {
|
||||
if (!listeners.has(type))
|
||||
listeners.set(type, new Set())
|
||||
listeners.get(type).add(callback)
|
||||
}
|
||||
worker.removeEventListener = (type, callback) => listeners.get(type)?.delete(callback)
|
||||
return {
|
||||
size: () => [...listeners.values()].reduce((sum, entries) => sum + entries.size, 0),
|
||||
emit(type, event) {
|
||||
for (const callback of [...listeners.get(type) || []]) callback(event)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test('JASSUB accurate runtime resize uses width/height/top/left/force and lifecycle methods return synchronously', async () => {
|
||||
const env = environment(candidate)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
assert.equal(instance.resize(320, 180, 4, 7, false), undefined)
|
||||
assert.equal(instance._canvas.style.top, '4px')
|
||||
assert.equal(instance._canvas.style.left, '7px')
|
||||
await env.flush()
|
||||
const resize = env.workers[0].messages.at(-1)
|
||||
assert.equal(resize.target, 'canvas')
|
||||
assert.equal(resize.width, 320)
|
||||
assert.equal(resize.height, 180)
|
||||
assert.equal(resize.force, false)
|
||||
const video = env.createVideo()
|
||||
assert.equal(instance.setVideo(video), undefined)
|
||||
assert.equal(instance._video, video)
|
||||
assert.equal(instance.destroy(), undefined)
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.some(child => child.className === 'JASSUB'), false)
|
||||
})
|
||||
|
||||
test('JASSUB accurate sendMessage returns a Promise waiting for ready and resolving after the actual post', async () => {
|
||||
const env = environment(candidate)
|
||||
const { instance } = env.factory()(env.art)
|
||||
const payload = { value: 42 }
|
||||
const pending = instance.sendMessage('runtime-probe', payload)
|
||||
assert(isPromise(pending))
|
||||
let settled = false
|
||||
pending.then(() => {
|
||||
settled = true
|
||||
})
|
||||
await env.flush()
|
||||
assert.equal(settled, false)
|
||||
assert.deepEqual(env.workers[0].messages.map(message => message.target), ['init'])
|
||||
await env.ready()
|
||||
assert.equal(await pending, undefined)
|
||||
assert.equal(settled, true)
|
||||
assert.equal(env.workers[0].messages.at(-1).target, 'runtime-probe')
|
||||
assert.equal(env.workers[0].messages.at(-1).value, 42)
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test('JASSUB accurate font options preserve mixed sources and font methods return synchronously', async () => {
|
||||
const env = environment(candidate)
|
||||
const bytes = new Uint8Array([1, 2, 3])
|
||||
const fonts = ['fixture.woff2', bytes]
|
||||
const availableFonts = { fixture: bytes, fallback: 'fallback.woff2' }
|
||||
const { instance } = env.factory({ fonts, availableFonts })(env.art)
|
||||
await env.ready()
|
||||
const init = env.workers[0].messages[0]
|
||||
assert.equal(init.fonts, fonts)
|
||||
assert.equal(init.fonts[1], bytes)
|
||||
assert.equal(init.availableFonts, availableFonts)
|
||||
assert.equal(instance.addFont(bytes), undefined)
|
||||
assert.equal(instance.setDefaultFont('fixture'), undefined)
|
||||
await env.flush()
|
||||
const added = env.workers[0].messages.find(message => message.target === 'addFont')
|
||||
assert.equal(added.font, bytes)
|
||||
assert.equal(env.workers[0].messages.at(-1).target, 'defaultFont')
|
||||
assert.equal(env.workers[0].messages.at(-1).font, 'fixture')
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
const subtitles = `[Script Info]
|
||||
ScriptType: v4.00+
|
||||
PlayResX: 640
|
||||
PlayResY: 360
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Arial,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0,0,0,,Runtime fixture`
|
||||
|
||||
test('JASSUB real Worker JS/WASM queries return numeric style indices and full event/style data through controlled transport', { timeout: 10000 }, async (context) => {
|
||||
const env = environment(candidate)
|
||||
env.platform.setGlobal('setTimeout', setTimeout)
|
||||
env.platform.setGlobal('clearTimeout', clearTimeout)
|
||||
const { instance } = env.factory({ subContent: subtitles, availableFonts: {}, fallbackFont: 'fixture', wasmUrl: '/runtime-fixture.wasm', onDemandRender: false, offscreenRender: false, asyncRender: false })(env.art)
|
||||
const worker = env.workers[0]
|
||||
const listeners = requestListeners(worker)
|
||||
const wasm = fs.readFileSync('packages/artplayer-plugin-jassub/worker/jassub-worker.wasm')
|
||||
const source = fs.readFileSync('packages/artplayer-plugin-jassub/worker/jassub-worker.js', 'utf8')
|
||||
const requests = []
|
||||
const messages = []
|
||||
let initialized = false
|
||||
let stopped = false
|
||||
const workerContext = {
|
||||
WebAssembly,
|
||||
TextDecoder,
|
||||
TextEncoder,
|
||||
console,
|
||||
onmessage: null,
|
||||
async fetch(url) {
|
||||
assert.equal(url, '/runtime-fixture.wasm')
|
||||
requests.push(url)
|
||||
return new Response(wasm, { headers: { 'Content-Type': 'application/wasm' } })
|
||||
},
|
||||
postMessage(message) {
|
||||
const data = structuredClone(message)
|
||||
messages.push(data)
|
||||
if (data.target === 'ready')
|
||||
initialized = true
|
||||
queueMicrotask(() => {
|
||||
if (stopped)
|
||||
return
|
||||
worker.onmessage({ data })
|
||||
listeners.emit('message', { data })
|
||||
})
|
||||
},
|
||||
}
|
||||
workerContext.self = workerContext
|
||||
vm.createContext(workerContext)
|
||||
// Execute complete production Worker JS and actual WASM. Only transport and fetch are controlled.
|
||||
vm.runInContext(source, workerContext, { timeout: 5000 })
|
||||
const post = worker.postMessage
|
||||
worker.postMessage = function (message) {
|
||||
post.call(this, message)
|
||||
if (!stopped)
|
||||
workerContext.onmessage({ data: structuredClone(message) })
|
||||
}
|
||||
const terminate = worker.terminate
|
||||
worker.terminate = function () {
|
||||
if (initialized && !stopped)
|
||||
workerContext.onmessage({ data: { target: 'destroy' } })
|
||||
stopped = true
|
||||
terminate.call(this)
|
||||
}
|
||||
context.after(() => instance.destroy())
|
||||
let readyEvents = 0
|
||||
instance.addEventListener('ready', (event) => {
|
||||
readyEvents++
|
||||
assert.equal(event.detail, null)
|
||||
assert.equal(event.target, instance)
|
||||
})
|
||||
assert(instance instanceof EventTarget)
|
||||
await new Promise(resolve => instance.addEventListener('ready', resolve, { once: true }))
|
||||
assert.equal(readyEvents, 1)
|
||||
assert.deepEqual(requests, ['/runtime-fixture.wasm'])
|
||||
const query = method => new Promise((resolve, reject) => {
|
||||
assert.equal(instance[method]((error, rows) => {
|
||||
if (error !== null) {
|
||||
reject(error ?? new Error('Successful queries must pass null as their error argument'))
|
||||
return
|
||||
}
|
||||
resolve(rows)
|
||||
}), undefined)
|
||||
})
|
||||
const events = await query('getEvents')
|
||||
const styles = await query('getStyles')
|
||||
assert.equal(events.length, 1)
|
||||
assert.equal(events[0].Start, 1000)
|
||||
assert.equal(events[0].Duration, 2000)
|
||||
assert.equal(events[0].Text, 'Runtime fixture')
|
||||
assert.equal(typeof events[0].Style, 'number')
|
||||
assert.equal(Object.hasOwn(events[0], '_index'), false)
|
||||
assert.equal(styles[events[0].Style].FontName, 'Arial')
|
||||
assert.equal(styles[events[0].Style].FontSize, 24)
|
||||
assert.equal(typeof styles[events[0].Style].PrimaryColour, 'number')
|
||||
assert.equal(listeners.size(), 0)
|
||||
assert.deepEqual(messages.find(message => message.target === 'getEvents').events, events)
|
||||
assert.deepEqual(messages.find(message => message.target === 'getStyles').styles, styles)
|
||||
assert.equal(instance.setEvent({ Text: 'Updated through Worker', Style: events[0].Style }, 0), undefined)
|
||||
await env.flush()
|
||||
assert.equal((await query('getEvents'))[0].Text, 'Updated through Worker')
|
||||
assert.equal(instance.setStyle({ FontSize: 32 }, events[0].Style), undefined)
|
||||
await env.flush()
|
||||
assert.equal((await query('getStyles'))[events[0].Style].FontSize, 32)
|
||||
})
|
||||
|
||||
for (const method of ['getEvents', 'getStyles']) {
|
||||
for (const mode of ['timeout', 'worker-error']) {
|
||||
test(`JASSUB frozen PKG-JASSUB-07 defect: ${method} ${mode} throws before the public error callback`, async () => {
|
||||
const env = environment(frozen)
|
||||
const timers = new Map()
|
||||
let next = 0
|
||||
env.platform.setGlobal('setTimeout', (callback) => {
|
||||
timers.set(++next, callback)
|
||||
return next
|
||||
})
|
||||
env.platform.setGlobal('clearTimeout', id => timers.delete(id))
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const listeners = requestListeners(env.workers[0])
|
||||
let calls = 0
|
||||
instance[method](() => calls++)
|
||||
assert.equal(listeners.size(), 2)
|
||||
const trigger = mode === 'timeout'
|
||||
? () => {
|
||||
const [id, callback] = [...timers][0]
|
||||
timers.delete(id)
|
||||
callback()
|
||||
}
|
||||
: () => listeners.emit('error', new env.platform.ErrorEvent('error', { error: new Error('controlled Worker error') }))
|
||||
assert.throws(trigger, { name: 'TypeError', message: /Cannot destructure property/ })
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(listeners.size(), 2)
|
||||
assert.equal(timers.size, mode === 'timeout' ? 0 : 1)
|
||||
instance.destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { JassubInstance, JassubOption } from 'artplayer-plugin-jassub'
|
||||
import jassub from 'artplayer-plugin-jassub'
|
||||
import legacy from 'artplayer-plugin-jassub/legacy'
|
||||
|
||||
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
|
||||
type Assert<T extends true> = T
|
||||
interface OldResult { name: 'artplayerPluginJassub', instance: JassubInstance }
|
||||
type OldFactory = (option: JassubOption) => (art: Artplayer) => OldResult
|
||||
type FactoryCheck = Assert<Equal<typeof jassub, OldFactory>>
|
||||
type ArgumentCheck = Assert<Equal<Parameters<typeof jassub>, [option: JassubOption]>>
|
||||
type ReturnCheck = Assert<Equal<ReturnType<ReturnType<typeof jassub>>, OldResult>>
|
||||
type ResizeCheck = Assert<Equal<Parameters<JassubInstance['resize']>, [force?: boolean, width?: number, height?: number, top?: number, left?: number]>>
|
||||
|
||||
declare const art: Artplayer
|
||||
declare const instance: JassubInstance
|
||||
const option: JassubOption = { workerUrl: '/worker.js', wasmUrl: '/worker.wasm', modernWasmUrl: '/modern.wasm', futureOption: true }
|
||||
const replacement: typeof jassub = (_option: JassubOption) => (_art: Artplayer) => ({ name: 'artplayerPluginJassub', instance })
|
||||
const reverse: OldFactory = jassub
|
||||
const legacyReplacement: typeof legacy = replacement
|
||||
const result: OldResult = jassub(option)(art)
|
||||
legacy(option)(art)
|
||||
const resize: Promise<void> = result.instance.resize(true, 640, 360, 0, 0)
|
||||
const video: Promise<void> = result.instance.setVideo(art.video)
|
||||
const destroy: Promise<void> = result.instance.destroy()
|
||||
const extension: string = result.instance.futureMethod(option.futureOption)
|
||||
export type { ArgumentCheck, FactoryCheck, ResizeCheck, ReturnCheck }
|
||||
export { destroy, extension, legacyReplacement, replacement, resize, reverse, video }
|
||||
@@ -0,0 +1,101 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { AssEvent, AssStyle, RuntimeFactory, RuntimeInstance, RuntimeOption, RuntimeResult } from 'artplayer-plugin-jassub/runtime'
|
||||
import runtime from 'artplayer-plugin-jassub/runtime'
|
||||
|
||||
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
|
||||
type Assert<T extends true> = T
|
||||
export type HostCheck = Assert<Equal<Parameters<ReturnType<RuntimeFactory>>, [art: Artplayer]>>
|
||||
|
||||
declare const art: Artplayer
|
||||
declare const canvas: HTMLCanvasElement
|
||||
const option: RuntimeOption = { canvas, fonts: ['/font.woff2', new Uint8Array([0])], availableFonts: { fallback: new Uint8Array([0]) }, onDemandRender: false }
|
||||
const factory: RuntimeFactory = runtime
|
||||
const result: RuntimeResult = runtime(option)(art)
|
||||
const replacement: typeof runtime = (_option?: RuntimeOption) => (_art: Artplayer) => result
|
||||
const defaults: RuntimeResult = runtime()(art)
|
||||
runtime(undefined)(art)
|
||||
runtime({ video: null, canvas })(art)
|
||||
const instance: RuntimeInstance = result.instance
|
||||
const target: EventTarget = instance
|
||||
const resize: void = instance.resize(640, 360, 0, 0, true)
|
||||
const video: void = instance.setVideo(art.video)
|
||||
const destroyed: void = instance.destroy()
|
||||
const error: Error = instance.destroy(new Error('test'))
|
||||
const errorOrEmpty: Error | '' = instance.destroy('failure')
|
||||
const sent: Promise<void> = instance.sendMessage('custom', { payload: new Uint8Array([1]) }, [])
|
||||
instance.setCurrentTime(false, 10, 2)
|
||||
instance.setTrackByUrl('/sub.ass')
|
||||
instance.setTrack('[Script Info]')
|
||||
instance.freeTrack()
|
||||
instance.setIsPaused(true)
|
||||
instance.setRate(2)
|
||||
instance.createEvent({ Start: 1000, Duration: 2000, Style: 0, Text: 'Subtitle' })
|
||||
instance.setEvent({ Text: 'Replacement' }, 0)
|
||||
instance.removeEvent(0)
|
||||
instance.createStyle({ Name: 'Default', FontSize: 24 })
|
||||
instance.setStyle({ Bold: 1 }, 0)
|
||||
instance.removeStyle(0)
|
||||
instance.styleOverride({ FontSize: 30 })
|
||||
instance.disableStyleOverride()
|
||||
instance.addFont(new Uint8Array([0]))
|
||||
instance.setDefaultFont('fallback')
|
||||
instance.runBenchmark()
|
||||
instance.getEvents((failure, events) => {
|
||||
const caught: Error | ErrorEvent | null = failure
|
||||
const values: AssEvent[] | undefined = events
|
||||
if (events) {
|
||||
for (const event of events) {
|
||||
const style: number = event.Style
|
||||
const start: number = event.Start
|
||||
void [style, start]
|
||||
}
|
||||
}
|
||||
void [caught, values]
|
||||
})
|
||||
instance.getStyles((_failure, styles) => {
|
||||
const values: AssStyle[] | undefined = styles
|
||||
void values
|
||||
})
|
||||
instance.addEventListener('ready', function (event) {
|
||||
function acceptsInstance(_instance: RuntimeInstance) {}
|
||||
acceptsInstance(this)
|
||||
const detail: null = event.detail
|
||||
void detail
|
||||
})
|
||||
instance.addEventListener('error', (event) => {
|
||||
const message: string = event.message
|
||||
void message
|
||||
})
|
||||
instance.addEventListener('custom', { handleEvent(event) {
|
||||
const type: string = event.type
|
||||
void type
|
||||
} })
|
||||
// @ts-expect-error Runtime URL fields are optional strings, not numbers.
|
||||
runtime({ workerUrl: 42 })
|
||||
// @ts-expect-error Accurate entry does not silently accept unknown options.
|
||||
runtime({ typoOption: true })
|
||||
// @ts-expect-error Resize starts with width; force is the fifth parameter.
|
||||
instance.resize(true)
|
||||
// @ts-expect-error Actual destroy is synchronous.
|
||||
const asyncDestroy: Promise<void> = instance.destroy()
|
||||
// @ts-expect-error Actual setVideo is synchronous.
|
||||
const asyncVideo: Promise<void> = instance.setVideo(art.video)
|
||||
// @ts-expect-error Actual resize is synchronous.
|
||||
const asyncResize: Promise<void> = instance.resize()
|
||||
// @ts-expect-error Registration is synchronous.
|
||||
runtime()(art).then(() => {})
|
||||
// @ts-expect-error sendMessage waits for initialization and returns a Promise.
|
||||
const syncMessage: void = instance.sendMessage('custom')
|
||||
// @ts-expect-error Style is the numeric libass index.
|
||||
instance.createEvent({ Style: 'Default' })
|
||||
// @ts-expect-error Worker responses do not contain _index.
|
||||
const index: number = ({} as AssEvent)._index
|
||||
// @ts-expect-error Callback errors may omit the event list.
|
||||
instance.getEvents((_error, events) => { const required: AssEvent[] = events; void required }) // eslint-disable-line style/max-statements-per-line -- Keep this rejected callback on one diagnostic line.
|
||||
// @ts-expect-error Font data must be a URL or bytes.
|
||||
instance.addFont(1)
|
||||
// @ts-expect-error Unknown instance methods are not hidden by an any index.
|
||||
instance.nonexistentMethod()
|
||||
// @ts-expect-error Only two blend modes are supported.
|
||||
runtime({ blendMode: 'gpu' })
|
||||
void [asyncDestroy, asyncVideo, asyncResize, defaults, destroyed, error, errorOrEmpty, factory, index, replacement, resize, sent, syncMessage, target, video]
|
||||
Reference in new issue
Block a user