mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-20 20:21:20 -08:00
refactor(danmuku): [PKG-DANMUKU-06] migrate runtime and preserve public types
This commit is contained in:
1 parent
abc8008da7
commit
24b883d862
64 files changed
+14020
-633
No files matched your search
@@ -70,3 +70,15 @@ volume/rate, independent instances and cleanup. Published-only tests retain defe
|
||||
See [audio validation](../refactor/audio-validation.md) for actual media tests and limitations.
|
||||
|
||||
Document PiP: `yarn test:dpip` runs 48 historical window/DOM/lifecycle cases. Native DOM iframe checks use `yarn test:browser test/browser/dpip.spec.js`; controlled window APIs do not establish native Document PiP support. See [validation notes](../refactor/dpip-validation.md).
|
||||
|
||||
Danmuku: `yarn test:danmuku` covers historical behavior and current input, scheduling,
|
||||
settings, rendering and heatmap boundaries. `ARTPLAYER_DANMUKU_ARTIFACT` selects a
|
||||
built candidate for integration cases; direct internal tests remain source tests.
|
||||
The candidate Worker helper executes the selected artifact's Blob or data URL bytes;
|
||||
only source-mode controlled imports compile `worker.ts` separately. The frozen
|
||||
published helper remains unchanged. `yarn test:danmuku-types` checks the unchanged
|
||||
npm root, the optional accurate `/runtime` entry, implementation assignability and
|
||||
semantic editor declarations. `yarn test:danmuku-types-package` packs and installs
|
||||
the packages outside the workspace, verifies exact member bytes and frozen offline
|
||||
reinstallation, and checks historical and current compiler consumers. These type
|
||||
checks do not replace the native browser suite or final distribution acceptance.
|
||||
@@ -128,3 +128,14 @@ both response modes. A passing diagnostic only proves observation plus successfu
|
||||
playback after release; inspect its progressed/waiting flags before claiming buffering.
|
||||
These capability observation timeouts do not relax the integration assertions.
|
||||
Archive the report and results (including traces) before starting another browser suite.
|
||||
|
||||
Danmuku input, scheduler, resources and heatmap-density specs accept
|
||||
`ARTPLAYER_DANMUKU_ARTIFACT` for a built main/legacy file; unset it to compile the
|
||||
current JS/TS entry. Run each format sequentially and wait for the command's final
|
||||
exit before copying reports or launching the next format. A visible last test line
|
||||
does not prove reporter/trace teardown is finished. These runs share the server port
|
||||
and output directory, so simultaneous invocations can corrupt failure evidence.
|
||||
For a timeout, inspect the retained trace before changing the test or implementation;
|
||||
browser startup and pre-plugin playback delays are different from plugin execution.
|
||||
The density regression retains 16000 rows, real playback and SVG/pixel geometry at
|
||||
two widths. A diagnostic pass does not waive the full matrix or long-load acceptance.
|
||||
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Deterministic owned-node tests use the Node runner.
|
||||
import test from 'node:test'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import Renderer from '../packages/artplayer-plugin-danmuku/src/renderer.js'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { Renderer } = await loadModules({ Renderer: 'packages/artplayer-plugin-danmuku/src/renderer' })
|
||||
|
||||
function fixture(t) {
|
||||
const { document } = parseHTML('<html><body><div id="layer"><i id="foreign"></i></div></body></html>')
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Deterministic candidate scheduling uses the Node runner.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import { danmukuCandidate, danmukuCandidateEnvironment } from './helpers/danmuku-candidate.js'
|
||||
import { danmukuCandidate, danmukuCandidateEnvironment, danmukuWorkerCode } from './helpers/danmuku-candidate.js'
|
||||
import { deferred } from './helpers/danmuku.js'
|
||||
|
||||
const implementation = await danmukuCandidate()
|
||||
@@ -44,10 +43,7 @@ async function fixture(options = {}, { automatic = false } = {}) {
|
||||
return child
|
||||
}
|
||||
const worker = env.workers[0]
|
||||
const actualBlob = env.urls.get(worker.url)
|
||||
const workerCode = actualBlob
|
||||
? await actualBlob.text()
|
||||
: fs.readFileSync(new URL('../packages/artplayer-plugin-danmuku/src/worker.js', import.meta.url), 'utf8')
|
||||
const workerCode = await danmukuWorkerCode(implementation, env, worker)
|
||||
const responses = []
|
||||
const workerContext = vm.createContext({ postMessage: data => responses.push(structuredClone(data)), URL: { revokeObjectURL() {} }, location: { href: worker.url } })
|
||||
workerContext.self = workerContext
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Controlled client faults and real worker geometry use the Node runner.
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
import { danmukuCandidate, danmukuCandidateEnvironment } from './helpers/danmuku-candidate.js'
|
||||
import { danmukuCandidate, danmukuCandidateEnvironment, danmukuWorkerCode } from './helpers/danmuku-candidate.js'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { WorkerClient } = await loadModules({ WorkerClient: 'packages/artplayer-plugin-danmuku/src/worker-client' })
|
||||
@@ -223,13 +222,18 @@ test('danmuku worker client: independent clients never share pending ownership',
|
||||
second.client.dispose()
|
||||
})
|
||||
|
||||
// Unlike the client double above, this executes the actual unmodified worker
|
||||
// script. Numeric geometry inputs are controlled; no browser layout is claimed.
|
||||
const workerCode = fs.readFileSync(new URL('../packages/artplayer-plugin-danmuku/src/worker.js', import.meta.url), 'utf8')
|
||||
// Unlike the client double above, this executes the actual source or artifact
|
||||
// worker. Numeric geometry inputs are controlled; no browser layout is claimed.
|
||||
const workerEnvironment = danmukuCandidateEnvironment(implementation)
|
||||
workerEnvironment.factory({ danmuku: [] })(workerEnvironment.art)
|
||||
const actualWorker = workerEnvironment.workers[0]
|
||||
const workerCode = await danmukuWorkerCode(implementation, workerEnvironment, actualWorker)
|
||||
workerEnvironment.destroy()
|
||||
|
||||
function calculateGeometry(input) {
|
||||
const replies = []
|
||||
const context = vm.createContext({ input, postMessage: data => replies.push(data) })
|
||||
const context = vm.createContext({ input, postMessage: data => replies.push(data), URL: { revokeObjectURL() {} }, location: { href: actualWorker.url } })
|
||||
context.self = context
|
||||
vm.runInContext(workerCode, context, { timeout: 1000 })
|
||||
vm.runInContext('onmessage({ data: input })', context, { timeout: 1000 })
|
||||
assert.equal(replies.length, 1)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
@@ -6,8 +7,31 @@ import { build } from 'esbuild'
|
||||
import { ensureArchive, readMember, refactorDir } from '../../refactor/scripts/releases.mjs'
|
||||
import { getEntryFile } from '../../scripts/projects.js'
|
||||
import { danmukuEnvironment } from './danmuku.js'
|
||||
import { resolveSource } from './load.js'
|
||||
|
||||
const workspace = fileURLToPath(new URL('../../', import.meta.url))
|
||||
let sourceWorker
|
||||
|
||||
export async function danmukuWorkerCode(implementation, env, worker) {
|
||||
const blob = env.urls.get(worker.url)
|
||||
if (blob)
|
||||
return blob.text()
|
||||
// Production worker-loader emits a percent-encoded data URL. Execute those
|
||||
// exact shipped bytes instead of silently substituting the current source.
|
||||
const dataPrefix = 'data:text/javascript;charset=utf-8,'
|
||||
if (typeof worker.url === 'string' && worker.url.startsWith(dataPrefix))
|
||||
return decodeURIComponent(worker.url.slice(dataPrefix.length))
|
||||
assert.equal(implementation.name, 'candidate-source', 'Artifact Worker tests must execute the Worker embedded in that artifact')
|
||||
sourceWorker ??= build({
|
||||
entryPoints: [resolveSource('packages/artplayer-plugin-danmuku/src/worker')],
|
||||
bundle: true,
|
||||
write: false,
|
||||
format: 'iife',
|
||||
platform: 'browser',
|
||||
target: 'es2020',
|
||||
}).then(result => result.outputFiles[0].text)
|
||||
return sourceWorker
|
||||
}
|
||||
|
||||
export async function danmukuCandidate() {
|
||||
const coreRelease = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases.find(item => item.name === 'artplayer')
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Option, Result } from 'artplayer-plugin-danmuku'
|
||||
import danmuku from 'artplayer-plugin-danmuku'
|
||||
import legacy from 'artplayer-plugin-danmuku/legacy'
|
||||
|
||||
const legacyFactory: typeof danmuku = legacy
|
||||
const rootFactory: typeof legacy = danmuku
|
||||
const option: Option = { danmuku: [], points: [{ time: 1, value: 2 }] }
|
||||
danmuku(option)
|
||||
legacy(option)
|
||||
declare const result: Result
|
||||
const synchronous: Result = result.emit({ text: 'historical declaration' })
|
||||
void [legacyFactory, rootFactory, synchronous]
|
||||
|
||||
// @ts-expect-error The published root requires its option argument.
|
||||
danmuku()
|
||||
// @ts-expect-error The published root requires danmuku in Option.
|
||||
danmuku({})
|
||||
// @ts-expect-error Preserve historical object points at the root.
|
||||
danmuku({ danmuku: [], points: [[1, 2]] })
|
||||
// @ts-expect-error Root emit keeps its published synchronous Result shape.
|
||||
const asynchronous: Promise<Result> = result.emit({ text: 'old emit' })
|
||||
void asynchronous
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Danmu, EventMap, Icons, Input, Item, NormalizedDanmu, NormalizedOption, Owner, Point, RuntimeFactory, RuntimeOption, RuntimeResult, Slider } from 'artplayer-plugin-danmuku/runtime'
|
||||
import Artplayer from 'artplayer'
|
||||
import runtime from 'artplayer-plugin-danmuku/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
|
||||
type FactoryArguments = Assert<Equal<Parameters<typeof runtime>, [option: RuntimeOption]>>
|
||||
type FactoryReturns = Assert<Equal<ReturnType<ReturnType<typeof runtime>>, RuntimeResult>>
|
||||
type EmitReturns = Assert<Equal<ReturnType<RuntimeResult['emit']>, Promise<Owner>>>
|
||||
type LoadReturns = Assert<Equal<ReturnType<RuntimeResult['load']>, Promise<Owner>>>
|
||||
type CommandReturns = Assert<Equal<ReturnType<RuntimeResult['config']>, Owner>>
|
||||
type OwnerHasNoFacadeKeys = Assert<Equal<Extract<'name' | 'mount', keyof Owner>, never>>
|
||||
type VisiblePayload = Assert<Equal<EventMap['artplayerPluginDanmuku:visible'], [item: Item]>>
|
||||
type LoadedPayload = Assert<Equal<EventMap['artplayerPluginDanmuku:loaded'], [queue: Item[]]>>
|
||||
|
||||
function expectOption(_option: NormalizedOption) {}
|
||||
const row: Danmu = { text: 'extended input', id: 'message-1', author: { name: 'test' }, style: { fontWeight: 'bold' } }
|
||||
const input: Input = () => [row]
|
||||
const asyncInput: Input = async () => [row]
|
||||
const point: Point = [10, 5]
|
||||
const margin: Slider<[number | string, number | string]> = { steps: [{ name: 'half', value: [10, '50%'], hide: true, show: false }] }
|
||||
const options: RuntimeOption = {
|
||||
danmuku: input,
|
||||
points: [point],
|
||||
MARGIN: margin,
|
||||
SPEED: { steps: [{ name: 'fast', value: 2.5, hide: true }] },
|
||||
filter(danmu) {
|
||||
expectOption(this)
|
||||
const normalized: NormalizedDanmu = danmu
|
||||
const time: number = danmu.time
|
||||
const extension: unknown = danmu.author
|
||||
// @ts-expect-error Queue allocation has not run when filter is invoked.
|
||||
const state: string = danmu.$state
|
||||
void [normalized, time, extension, state]
|
||||
return true
|
||||
},
|
||||
async beforeEmit(danmu) {
|
||||
expectOption(this)
|
||||
const text: string = danmu.text
|
||||
void text
|
||||
return true
|
||||
},
|
||||
async beforeVisible(item) {
|
||||
expectOption(this)
|
||||
const state: 'wait' | 'ready' | 'emit' | 'stop' = item.$state
|
||||
const index: number = item.$index
|
||||
const node: HTMLDivElement | null = item.$ref
|
||||
const extra: unknown = item.author
|
||||
void [state, index, node, extra]
|
||||
return true
|
||||
},
|
||||
}
|
||||
const art = new Artplayer({ container: '#player', url: '/video.mp4', plugins: [runtime(options)] })
|
||||
const factory: RuntimeFactory = runtime
|
||||
const result = runtime({})(art)
|
||||
const registered: 'artplayerPluginDanmuku' = result.name
|
||||
const icon: string = runtime.icons.$check_on
|
||||
const icons: Icons = runtime.icons
|
||||
runtime.icons = icons
|
||||
runtime.icons.$on = '<svg></svg>'
|
||||
const promise: Promise<Owner> = result.emit(row)
|
||||
const loading: Promise<Owner> = result.load(asyncInput)
|
||||
const replacing: Promise<Owner> = result.load()
|
||||
const owner: Owner = result.config({ opacity: 0.5 })
|
||||
const stopped: Owner = owner.stop().start().suspend().continue().reset()
|
||||
const configured: NormalizedOption = result.option
|
||||
result.option.visible = false
|
||||
result.mount(document.createElement('div'))
|
||||
result.mount('#danmuku-mount')
|
||||
const loadedPayload: EventMap['artplayerPluginDanmuku:loaded'] = [owner.queue]
|
||||
const pointsPayload: EventMap['artplayerPluginDanmuku:points'] = [[point]]
|
||||
const failedPayload: EventMap['artplayerPluginDanmuku:error'] = [new Error('failed')]
|
||||
|
||||
async function consume() {
|
||||
const emitted = await result.emit({ text: 'await owner' })
|
||||
const rows: Item[] = emitted.queue
|
||||
const next: Owner = emitted.hide().show()
|
||||
// @ts-expect-error The resolved owner does not expose the facade name.
|
||||
void emitted.name
|
||||
// @ts-expect-error The resolved owner does not own setting.mount.
|
||||
emitted.mount('#mount')
|
||||
return [rows, next]
|
||||
}
|
||||
// @ts-expect-error The factory still requires its options object.
|
||||
runtime()
|
||||
// @ts-expect-error Undefined is not a valid options object.
|
||||
runtime(undefined)
|
||||
// @ts-expect-error An emitted row requires text.
|
||||
result.emit({ id: 'missing-text' })
|
||||
// @ts-expect-error The runtime heatmap uses mutable pairs, not historical object points.
|
||||
runtime({ points: [{ time: 1, value: 2 }] })
|
||||
// @ts-expect-error Margin sliders carry pairs rather than scalar numbers.
|
||||
runtime({ MARGIN: { steps: [{ value: 10 }] } })
|
||||
// @ts-expect-error A live mount needs an explicit destination.
|
||||
result.mount()
|
||||
// @ts-expect-error Facade option is a getter, although the returned options object is mutable.
|
||||
result.option = configured
|
||||
// @ts-expect-error Facade isHide is a getter.
|
||||
result.isHide = true
|
||||
// @ts-expect-error Facade isStop is a getter.
|
||||
result.isStop = true
|
||||
// @ts-expect-error emit returns a Promise; synchronous chaining is not available.
|
||||
result.emit(row).hide()
|
||||
// @ts-expect-error The factory has icons but no default self-reference.
|
||||
void runtime.default
|
||||
|
||||
export type { CommandReturns, EmitReturns, FactoryArguments, FactoryReturns, LoadedPayload, LoadReturns, OwnerHasNoFacadeKeys, VisiblePayload }
|
||||
void [factory, registered, icon, promise, loading, replacing, stopped, loadedPayload, pointsPayload, failedPayload, consume]
|
||||
Reference in new issue
Block a user