mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
266 lines
9.7 KiB
JavaScript
266 lines
9.7 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import process from 'node:process'
|
|
import { fileURLToPath } from 'node:url'
|
|
import vm from 'node:vm'
|
|
import { build, transform } from 'esbuild'
|
|
import less from 'less'
|
|
import { verifyDpipContract } from '../../refactor/scripts/dpip-contract.mjs'
|
|
import { readMember } from '../../refactor/scripts/releases.mjs'
|
|
import { verifyInstalledArtifacts } from '../../scripts/installed-artifacts.mjs'
|
|
import { getEntryFile } from '../../scripts/projects.js'
|
|
|
|
export async function dpipCandidate() {
|
|
const root = fileURLToPath(new URL('../../', import.meta.url))
|
|
if (process.env.ARTPLAYER_BROWSER_ARTIFACTS) {
|
|
assert.notEqual(process.env.ARTPLAYER_DPIP_BASELINE, '1', 'Installed candidate cannot use the frozen workspace')
|
|
assert(!process.env.ARTPLAYER_DPIP_ARTIFACT, 'Installed candidate cannot override Document PiP artifact')
|
|
const { inputs } = verifyInstalledArtifacts(root, process.env.ARTPLAYER_BROWSER_ARTIFACTS, ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-document-pip'])
|
|
const input = inputs.find(item => item.name === 'artplayer-plugin-document-pip')
|
|
return { name: 'candidate-installed', code: await fs.readFile(input.file, 'utf8'), provenance: input }
|
|
}
|
|
if (process.env.ARTPLAYER_DPIP_BASELINE === '1')
|
|
return (await dpipHistorical()).find(item => item.name === 'frozen-workspace')
|
|
if (process.env.ARTPLAYER_DPIP_ARTIFACT)
|
|
return { name: 'candidate-artifact', code: await fs.readFile(process.env.ARTPLAYER_DPIP_ARTIFACT, 'utf8') }
|
|
const output = await build({ entryPoints: [getEntryFile(path.join(root, 'packages/artplayer-plugin-document-pip'))], bundle: true, write: false, format: 'cjs', target: 'es2020', plugins: [{
|
|
name: 'dpip-less-inline',
|
|
setup(build) {
|
|
build.onLoad({ filter: /\.less$/ }, async ({ path: filename }) => {
|
|
const source = await fs.readFile(filename.replace(/\?inline$/, ''), 'utf8')
|
|
return { contents: (await less.render(source)).css, loader: 'text' }
|
|
})
|
|
},
|
|
}] })
|
|
return { name: 'candidate-source', code: output.outputFiles[0].text }
|
|
}
|
|
|
|
export async function dpipHistorical() {
|
|
const contract = await verifyDpipContract()
|
|
const css = (await less.render(contract.sources.get('packages/artplayer-plugin-document-pip/src/style.less'))).css
|
|
const source = contract.sources.get('packages/artplayer-plugin-document-pip/src/index.js').replace('import style from \'./style.less?inline\'', `const style = ${JSON.stringify(css)}`)
|
|
return [
|
|
...[contract.baseline.release, ...contract.baseline.previous].filter(release => !release.missingEntrypoints.main).map(release => ({ name: `published-${release.version}`, code: readMember(contract.archives.get(release.version), `package/${release.manifest.main.replace(/^\.\//, '')}`).toString() })),
|
|
{ name: 'frozen-workspace', code: (await transform(source, { format: 'cjs', target: 'es2020' })).code },
|
|
]
|
|
}
|
|
|
|
function eventTarget() {
|
|
const listeners = new Map()
|
|
return {
|
|
listeners,
|
|
addEventListener(name, callback) {
|
|
if (!listeners.has(name))
|
|
listeners.set(name, new Set())
|
|
listeners.get(name).add(callback)
|
|
},
|
|
removeEventListener(name, callback) { listeners.get(name)?.delete(callback) },
|
|
dispatchEvent(event) {
|
|
for (const callback of [...(listeners.get(event.type) || [])]) callback(event)
|
|
},
|
|
}
|
|
}
|
|
|
|
function adopt(node, document) {
|
|
node.ownerDocument = document
|
|
for (const child of node.children) adopt(child, document)
|
|
}
|
|
|
|
function createNode(tag, document) {
|
|
return {
|
|
...eventTarget(),
|
|
nodeName: tag.toUpperCase(),
|
|
ownerDocument: document,
|
|
parentNode: null,
|
|
children: [],
|
|
style: {},
|
|
className: '',
|
|
textContent: '',
|
|
get nextSibling() {
|
|
return this.parentNode?.children[this.parentNode.children.indexOf(this) + 1] || null
|
|
},
|
|
appendChild(child) { return this.insertBefore(child, null) },
|
|
insertBefore(child, before) {
|
|
assert(before === null || before.parentNode === this, 'Reference node must belong to this parent')
|
|
child.remove()
|
|
const index = before === null ? this.children.length : this.children.indexOf(before)
|
|
this.children.splice(index, 0, child)
|
|
child.parentNode = this
|
|
adopt(child, this.ownerDocument)
|
|
return child
|
|
},
|
|
removeChild(child) {
|
|
assert.equal(child.parentNode, this)
|
|
this.children.splice(this.children.indexOf(child), 1)
|
|
child.parentNode = null
|
|
return child
|
|
},
|
|
remove() { this.parentNode?.removeChild(this) },
|
|
}
|
|
}
|
|
|
|
function createDocument(readyState = 'complete') {
|
|
const doc = { ...eventTarget(), readyState }
|
|
doc.createElement = tag => createNode(tag, doc)
|
|
doc.documentElement = doc.createElement('html')
|
|
doc.head = doc.createElement('head')
|
|
doc.body = doc.createElement('body')
|
|
doc.documentElement.appendChild(doc.head)
|
|
doc.documentElement.appendChild(doc.body)
|
|
const nodes = () => {
|
|
const result = []
|
|
const walk = (node) => {
|
|
result.push(node)
|
|
for (const child of node.children) walk(child)
|
|
}
|
|
walk(doc.documentElement)
|
|
return result
|
|
}
|
|
doc.getElementById = id => nodes().find(node => node.id === id) || null
|
|
doc.querySelectorAll = (selector) => {
|
|
if (selector === 'style')
|
|
return nodes().filter(node => node.nodeName === 'STYLE')
|
|
if (selector === 'link[rel="stylesheet"]')
|
|
return nodes().filter(node => node.nodeName === 'LINK' && node.rel === 'stylesheet')
|
|
if (selector === 'meta[name="viewport"]')
|
|
return nodes().filter(node => node.nodeName === 'META' && node.name === 'viewport')
|
|
throw new Error(`Unimplemented controlled selector: ${selector}`)
|
|
}
|
|
doc.querySelector = selector => doc.querySelectorAll(selector)[0] || null
|
|
doc.adoptNode = (node) => {
|
|
node.remove()
|
|
adopt(node, doc)
|
|
return node
|
|
}
|
|
return doc
|
|
}
|
|
|
|
export function dpipEnvironment(implementation, settings = {}) {
|
|
const document = createDocument(settings.readyState)
|
|
const windows = []
|
|
const requests = []
|
|
const sleeps = []
|
|
const emitted = []
|
|
const warnings = []
|
|
const subscriptions = new Map()
|
|
const controls = new Map()
|
|
const proxies = []
|
|
const rebinds = []
|
|
const timers = new Map()
|
|
let timerId = 0
|
|
const window = { ...eventTarget(), document }
|
|
document.defaultView = window
|
|
function createWindow() {
|
|
const win = { ...eventTarget(), document: createDocument(), closed: false, close() {
|
|
this.closed = true
|
|
} }
|
|
win.document.defaultView = win
|
|
windows.push(win)
|
|
return win
|
|
}
|
|
if (settings.supported !== false) {
|
|
window.documentPictureInPicture = { requestWindow(option) {
|
|
requests.push(option)
|
|
return settings.requestWindow ? settings.requestWindow(option, createWindow) : Promise.resolve(createWindow())
|
|
} }
|
|
}
|
|
const parent = document.createElement('div')
|
|
const player = document.createElement('div')
|
|
player.className = 'art-video-player'
|
|
const sibling = document.createElement('span')
|
|
document.body.appendChild(parent)
|
|
parent.appendChild(player)
|
|
parent.appendChild(sibling)
|
|
const utils = {
|
|
append(node, content) { node.content = content },
|
|
tooltip(node, value) { node.tooltip = value },
|
|
addClass(node, name) { node.className = `${node.className} ${name}`.trim() },
|
|
removeClass(node, name) { node.className = node.className.split(' ').filter(value => value !== name).join(' ') },
|
|
sleep(ms) {
|
|
sleeps.push(ms)
|
|
return settings.sleep ? settings.sleep(ms) : Promise.resolve()
|
|
},
|
|
}
|
|
const art = {
|
|
template: { $player: player },
|
|
constructor: { utils },
|
|
icons: { pip: 'pip-icon' },
|
|
i18n: { get: name => name },
|
|
notice: { show: '' },
|
|
pip: false,
|
|
isDestroy: false,
|
|
proxy(target, name, callback) {
|
|
target.addEventListener(name, callback)
|
|
const dispose = () => target.removeEventListener(name, callback)
|
|
proxies.push(dispose)
|
|
return dispose
|
|
},
|
|
controls: { add(spec) {
|
|
const node = document.createElement('button')
|
|
controls.set(spec.name, { spec, node })
|
|
spec.mounted?.(node)
|
|
return node
|
|
}, remove(name) { controls.delete(name) } },
|
|
events: { bindGlobalEvents: (...args) => rebinds.push({ args, document: player.ownerDocument }) },
|
|
on(name, callback) {
|
|
if (!subscriptions.has(name))
|
|
subscriptions.set(name, new Set())
|
|
subscriptions.get(name).add(callback)
|
|
return art
|
|
},
|
|
off(name, callback) { subscriptions.get(name)?.delete(callback) },
|
|
emit(name, ...args) {
|
|
emitted.push({ name, args })
|
|
for (const callback of [...(subscriptions.get(name) || [])]) callback(...args)
|
|
},
|
|
destroy() {
|
|
art.isDestroy = true
|
|
art.emit('destroy')
|
|
},
|
|
}
|
|
const context = vm.createContext({ window, document, console: { warn: (...args) => warnings.push(args) }, module: { exports: {} }, setTimeout(callback, ms) {
|
|
const id = timerId++
|
|
timers.set(id, callback)
|
|
sleeps.push(ms)
|
|
const delay = settings.sleep ? settings.sleep(ms) : Promise.resolve()
|
|
void delay.then(() => {
|
|
const run = timers.get(id)
|
|
timers.delete(id)
|
|
run?.()
|
|
})
|
|
return id
|
|
}, clearTimeout(id) { timers.delete(id) } })
|
|
context.exports = context.module.exports
|
|
const evaluate = () => vm.runInContext(`(() => { ${implementation.code}\n })()`, context, { timeout: 5000 })
|
|
evaluate()
|
|
return {
|
|
factory: context.module.exports.default || context.module.exports,
|
|
exported: context.module.exports,
|
|
document,
|
|
window,
|
|
art,
|
|
parent,
|
|
player,
|
|
sibling,
|
|
controls,
|
|
windows,
|
|
requests,
|
|
sleeps,
|
|
emitted,
|
|
warnings,
|
|
proxies,
|
|
subscriptions,
|
|
rebinds,
|
|
timers,
|
|
evaluate,
|
|
createWindow,
|
|
async flush() {
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
},
|
|
}
|
|
}
|