mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
240 lines
8.1 KiB
JavaScript
240 lines
8.1 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { Buffer } from 'node:buffer'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import vm from 'node:vm'
|
|
import { build } from 'esbuild'
|
|
import { verifyDanmukuContract } from '../../refactor/scripts/danmuku-contract.mjs'
|
|
import { ensureArchive, readMember, refactorDir } from '../../refactor/scripts/releases.mjs'
|
|
|
|
export function deferred() {
|
|
let resolve
|
|
let reject
|
|
const promise = new Promise((yes, no) => {
|
|
resolve = yes
|
|
reject = no
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
export async function danmukuHistorical() {
|
|
const { baseline, archives, sources } = await verifyDanmukuContract()
|
|
const prefix = 'packages/artplayer-plugin-danmuku/'
|
|
const compiled = await build({ entryPoints: [`${prefix}src/index.js`], bundle: true, write: false, format: 'cjs', platform: 'browser', plugins: [{ name: 'frozen-danmuku-failures', setup(build) {
|
|
build.onResolve({ filter: /.*/ }, args => ({ path: path.posix.normalize(args.importer ? path.posix.join(path.posix.dirname(args.importer), args.path) : args.path), namespace: 'frozen' }))
|
|
build.onLoad({ filter: /.*/, namespace: 'frozen' }, (args) => {
|
|
if (args.path.includes('?worker'))
|
|
return { contents: 'export default Worker', loader: 'js' }
|
|
const file = args.path.split('?')[0]
|
|
const found = sources.get(file) ?? sources.get(`${file}.js`)
|
|
assert.notEqual(found, undefined, file)
|
|
return { contents: args.path.includes('?') ? `export default ${JSON.stringify(found)}` : found, loader: 'js' }
|
|
})
|
|
} }] })
|
|
const coreRelease = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases.find(item => item.name === 'artplayer')
|
|
const coreCode = readMember(await ensureArchive(coreRelease), 'package/dist/artplayer.js').toString()
|
|
return [
|
|
{ name: 'frozen-source', code: compiled.outputFiles[0].text, coreCode },
|
|
...['main', 'legacy'].map(field => ({ name: `published-5.3.0-${field}`, code: readMember(archives.get('5.3.0'), `package/${baseline.release.manifest[field].replace(/^\.\//u, '')}`).toString(), coreCode })),
|
|
]
|
|
}
|
|
|
|
class Element {
|
|
constructor() {
|
|
this.style = {}
|
|
this.dataset = {}
|
|
this.children = []
|
|
this.nodes = new Map()
|
|
this.clientWidth = 640
|
|
this.clientHeight = 360
|
|
this.offsetWidth = 640
|
|
this.offsetHeight = 100
|
|
this.offsetTop = 0
|
|
this.className = ''
|
|
this.textContent = ''
|
|
this.value = ''
|
|
}
|
|
|
|
get [Symbol.toStringTag]() { return 'HTMLDivElement' }
|
|
appendChild(child) {
|
|
child.parentElement = this
|
|
this.children.push(child)
|
|
return child
|
|
}
|
|
|
|
querySelector(selector) {
|
|
if (!this.nodes.has(selector))
|
|
this.nodes.set(selector, new Element())
|
|
return this.nodes.get(selector)
|
|
}
|
|
|
|
getBoundingClientRect() { return { top: 0, left: 0, right: this.clientWidth, bottom: this.clientHeight, width: this.clientWidth, height: this.clientHeight } }
|
|
setAttribute(key, value) { this[key] = value }
|
|
addEventListener() {}
|
|
}
|
|
|
|
export function danmukuEnvironment(implementation, settings = {}) {
|
|
const coreModule = { exports: {} }
|
|
vm.runInNewContext(implementation.coreCode, { module: coreModule, exports: coreModule.exports }, { timeout: 1000 })
|
|
const events = []
|
|
const listeners = new Map()
|
|
const workers = []
|
|
const frames = new Map()
|
|
const timers = new Map()
|
|
const proxies = []
|
|
const controls = []
|
|
const requests = []
|
|
const urls = new Map()
|
|
const revoked = []
|
|
const consoleErrors = []
|
|
let id = 0
|
|
let now = 1000
|
|
const host = {
|
|
requestAnimationFrame(callback) {
|
|
frames.set(++id, callback)
|
|
return id
|
|
},
|
|
cancelAnimationFrame(id) { frames.delete(id) },
|
|
}
|
|
class Worker {
|
|
constructor(url) {
|
|
this.url = url
|
|
this.messages = []
|
|
this.listeners = new Map()
|
|
this.terminated = false
|
|
workers.push(this)
|
|
}
|
|
|
|
terminate() { this.terminated = true }
|
|
postMessage(message) { this.messages.push({ message, afterTerminate: this.terminated }) }
|
|
addEventListener(name, callback) { this.listeners.set(name, [...this.listeners.get(name) || [], callback]) }
|
|
deliver(data) { this.onmessage?.({ data }) }
|
|
async execute(message) {
|
|
const blob = urls.get(this.url)
|
|
assert(blob, 'Expected the parser actual Blob source')
|
|
const workerContext = vm.createContext({ postMessage: data => this.deliver(data) })
|
|
vm.runInContext(await blob.text(), workerContext, { timeout: 1000 })
|
|
workerContext.input = message
|
|
vm.runInContext('onmessage({ data: input })', workerContext, { timeout: 1000 })
|
|
}
|
|
|
|
fail(error) {
|
|
this.onerror?.(error)
|
|
for (const callback of this.listeners.get('error') || [])
|
|
callback(error)
|
|
}
|
|
}
|
|
class Clock extends Date {
|
|
static now() { return now }
|
|
}
|
|
const module = { exports: {} }
|
|
const context = vm.createContext({
|
|
window: host,
|
|
Worker,
|
|
Blob,
|
|
Date: Clock,
|
|
URL: {
|
|
createObjectURL(blob) {
|
|
const url = `blob:danmuku-${++id}`
|
|
urls.set(url, blob)
|
|
return url
|
|
},
|
|
revokeObjectURL(url) { revoked.push(url) },
|
|
},
|
|
atob: value => Buffer.from(value, 'base64').toString('binary'),
|
|
setTimeout(callback, delay) {
|
|
timers.set(++id, { callback, delay })
|
|
return id
|
|
},
|
|
clearTimeout(id) { timers.delete(id) },
|
|
fetch: async (url) => {
|
|
requests.push(url)
|
|
if (settings.fetch)
|
|
return settings.fetch(url)
|
|
return { text: async () => '<i><d p="10,1,25,16777215,1,0,u,1">xml</d></i>' }
|
|
},
|
|
console: { error: (...args) => consoleErrors.push(args) },
|
|
module,
|
|
exports: module.exports,
|
|
})
|
|
vm.runInContext(implementation.code, context, { timeout: 1000 })
|
|
const factory = typeof module.exports === 'function' ? module.exports : module.exports.default
|
|
const body = new Element()
|
|
context.document = { querySelector: selector => selector === '#external' ? body : null, createElement: () => new Element() }
|
|
const utils = {
|
|
clamp: (value, min, max) => Math.min(Math.max(value, min), max),
|
|
setStyle: (element, key, value) => element.style[key] = value,
|
|
setStyles: (element, style) => Object.assign(element.style, style),
|
|
errorHandle: (value, message) => {
|
|
if (!value)
|
|
throw new Error(message)
|
|
return value
|
|
},
|
|
createElement: () => new Element(),
|
|
query: (selector, element) => element.querySelector(selector),
|
|
append: (element, child) => element.appendChild(child),
|
|
tooltip() {},
|
|
inverseClass() {},
|
|
addClass() {},
|
|
removeClass() {},
|
|
}
|
|
const art = {
|
|
constructor: { utils, validator: coreModule.exports.validator },
|
|
template: { $danmuku: new Element(), $player: new Element(), $controlsCenter: new Element() },
|
|
currentTime: 10,
|
|
duration: 60,
|
|
played: 0,
|
|
option: { isLive: false },
|
|
width: 640,
|
|
playbackRate: 1,
|
|
playing: false,
|
|
plugins: {},
|
|
controls: { add(option) {
|
|
const element = new Element()
|
|
element.offsetWidth = settings.heatmapWidth ?? 640
|
|
controls.push({ option, element })
|
|
option.mounted?.(element)
|
|
return undefined
|
|
} },
|
|
on(name, callback) { listeners.set(name, [...listeners.get(name) || [], callback]) },
|
|
off(name, callback) { listeners.set(name, (listeners.get(name) || []).filter(item => item !== callback)) },
|
|
emit(name, ...args) {
|
|
events.push({ name, args })
|
|
for (const callback of listeners.get(name) || [])
|
|
callback(...args)
|
|
},
|
|
proxy(target, name, callback) { proxies.push({ target, name, callback }) },
|
|
}
|
|
return {
|
|
context,
|
|
factory,
|
|
art,
|
|
workers,
|
|
events,
|
|
listeners,
|
|
frames,
|
|
timers,
|
|
proxies,
|
|
controls,
|
|
requests,
|
|
urls,
|
|
revoked,
|
|
consoleErrors,
|
|
async flush() {
|
|
for (let step = 0; step < 12; step++)
|
|
await Promise.resolve()
|
|
},
|
|
frame(frameId = frames.keys().next().value) {
|
|
const callback = frames.get(frameId)
|
|
assert.equal(typeof callback, 'function', 'Expected one scheduled animation frame')
|
|
frames.delete(frameId)
|
|
return callback(now)
|
|
},
|
|
tick(milliseconds) { now += milliseconds },
|
|
destroy() {
|
|
art.isDestroy = true
|
|
art.emit('destroy')
|
|
},
|
|
}
|
|
}
|