mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
194 lines
6.7 KiB
JavaScript
194 lines
6.7 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import process from 'node:process'
|
|
import vm from 'node:vm'
|
|
import { transform } from 'esbuild'
|
|
import { verifyAutoThumbnailContract } from '../../refactor/scripts/auto-thumbnail-contract.mjs'
|
|
import { readMember } from '../../refactor/scripts/releases.mjs'
|
|
import { browserCandidate } from './browser-candidate.js'
|
|
|
|
export async function autoThumbnailHistorical() {
|
|
const { baseline, archives, sources } = await verifyAutoThumbnailContract()
|
|
const name = 'artplayer-plugin-auto-thumbnail'
|
|
const implementations = []
|
|
for (const release of [baseline.release, ...baseline.previous]) {
|
|
if (release.missingEntrypoints.main)
|
|
continue
|
|
for (const field of ['main', 'legacy']) {
|
|
implementations.push({ name: `published-${release.version}-${field}`, code: readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() })
|
|
}
|
|
}
|
|
implementations.push({ name: 'published-1.0.0-source-only', code: (await transform(readMember(archives.get('1.0.0'), 'package/src/index.js').toString(), { format: 'cjs', target: 'es2020' })).code })
|
|
implementations.push({ name: 'frozen-workspace-source', code: (await transform(sources.get(`packages/${name}/src/index.js`), { format: 'cjs', target: 'es2020' })).code })
|
|
for (const suffix of ['js', 'legacy.js']) {
|
|
implementations.push({ name: `frozen-workspace-${suffix}`, code: sources.get(`packages/${name}/dist/${name}.${suffix}`) })
|
|
}
|
|
return implementations
|
|
}
|
|
|
|
export async function autoThumbnailCandidate() {
|
|
if (process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE === '1') {
|
|
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Installed auto-thumbnail checks cannot use the frozen workspace')
|
|
return (await autoThumbnailHistorical()).find(item => item.name === 'frozen-workspace-js')
|
|
}
|
|
return { name: 'candidate', ...await browserCandidate('artplayer-plugin-auto-thumbnail', process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT) }
|
|
}
|
|
|
|
// Controlled media events and deferred encodes are not evidence of native decoding.
|
|
export function autoThumbnailEnvironment(implementation, { script = false } = {}) {
|
|
const operations = []
|
|
const videos = []
|
|
const canvases = []
|
|
const blobs = []
|
|
const urls = new Map()
|
|
const listeners = new Map()
|
|
const updates = []
|
|
const warnings = []
|
|
const timers = new Map()
|
|
let nextTimer = 0
|
|
let nextUrl = 0
|
|
const controls = { nullContext: false, drawError: null, encodeError: null, updateError: null, onUpdate: null, beforeAppend: null, afterAppend: null }
|
|
const attached = new Set()
|
|
const document = {
|
|
documentElement: {
|
|
appendChild(video) {
|
|
controls.beforeAppend?.(video)
|
|
attached.add(video)
|
|
controls.afterAppend?.(video)
|
|
return video
|
|
},
|
|
},
|
|
createElement(tag) {
|
|
if (tag === 'video') {
|
|
const video = {
|
|
style: {},
|
|
setAttribute(name, value) { this[name] = value },
|
|
remove() {
|
|
attached.delete(this)
|
|
operations.push({ name: 'remove', video: this })
|
|
},
|
|
duration: 120,
|
|
timeOffset: 0,
|
|
seeking: false,
|
|
videoWidth: 1920,
|
|
videoHeight: 1080,
|
|
onloadedmetadata: null,
|
|
onloadeddata: null,
|
|
onseeked: null,
|
|
onerror: null,
|
|
pause() { operations.push({ name: 'pause', video }) },
|
|
load() { operations.push({ name: 'load', video }) },
|
|
removeAttribute(name) {
|
|
delete this[name]
|
|
operations.push({ name: 'removeAttribute', attribute: name, video })
|
|
},
|
|
}
|
|
let time = 0
|
|
Object.defineProperty(video, 'currentTime', {
|
|
get: () => time + video.timeOffset,
|
|
set(value) {
|
|
time = value
|
|
operations.push({ name: 'seek', value, handlerInstalled: typeof video.onseeked === 'function', video })
|
|
},
|
|
})
|
|
videos.push(video)
|
|
return video
|
|
}
|
|
if (tag === 'canvas') {
|
|
const draws = []
|
|
const ctx = { drawImage(...args) {
|
|
if (controls.drawError)
|
|
throw controls.drawError
|
|
draws.push(args)
|
|
operations.push({ name: 'draw', args })
|
|
} }
|
|
const canvas = {
|
|
width: 0,
|
|
height: 0,
|
|
draws,
|
|
getContext() { return controls.nullContext ? null : ctx },
|
|
toBlob(callback, type) {
|
|
if (controls.encodeError)
|
|
throw controls.encodeError
|
|
const encode = { callback, type, draws: draws.map(args => [...args]), canvas }
|
|
blobs.push(encode)
|
|
operations.push({ name: 'encode', encode })
|
|
},
|
|
}
|
|
canvases.push(canvas)
|
|
return canvas
|
|
}
|
|
throw new Error(`Unexpected element: ${tag}`)
|
|
},
|
|
}
|
|
const module = { exports: {} }
|
|
const box = { document, setTimeout(callback, delay) {
|
|
const id = nextTimer++
|
|
timers.set(id, { callback, delay })
|
|
return id
|
|
}, clearTimeout(id) { timers.delete(id) }, URL: {
|
|
createObjectURL(blob) {
|
|
if (blob == null)
|
|
throw new TypeError('Blob required')
|
|
const url = `blob:auto-thumbnail-${++nextUrl}`
|
|
urls.set(url, blob)
|
|
operations.push({ name: 'createURL', url, blob })
|
|
return url
|
|
},
|
|
revokeObjectURL(url) {
|
|
urls.delete(url)
|
|
operations.push({ name: 'revokeURL', url })
|
|
},
|
|
}, window: {}, console: { warn: (...args) => warnings.push(args) } }
|
|
if (!script)
|
|
Object.assign(box, { module, exports: module.exports })
|
|
vm.runInNewContext(implementation.code, box)
|
|
const exported = script ? box.artplayerPluginAutoThumbnail || box.window.artplayerPluginAutoThumbnail : module.exports
|
|
const factory = typeof exported === 'function' ? exported : exported.default
|
|
const art = {
|
|
option: { url: 'original.mp4' },
|
|
on(name, fn) {
|
|
if (!listeners.has(name))
|
|
listeners.set(name, new Set())
|
|
listeners.get(name).add(fn)
|
|
return this
|
|
},
|
|
off(name, fn) {
|
|
listeners.get(name)?.delete(fn)
|
|
return this
|
|
},
|
|
emit(name, ...args) {
|
|
for (const fn of [...(listeners.get(name) || [])]) fn(...args)
|
|
return this
|
|
},
|
|
}
|
|
Object.defineProperty(art, 'thumbnails', {
|
|
get: () => updates.at(-1),
|
|
set(value) {
|
|
if (controls.updateError)
|
|
throw controls.updateError
|
|
updates.push(value)
|
|
controls.onUpdate?.(value)
|
|
},
|
|
})
|
|
return {
|
|
art,
|
|
factory,
|
|
exported,
|
|
box,
|
|
operations,
|
|
videos,
|
|
canvases,
|
|
blobs,
|
|
urls,
|
|
listeners,
|
|
updates,
|
|
warnings,
|
|
timers,
|
|
controls,
|
|
attached,
|
|
metadata(video = videos.at(-1)) { video.onloadedmetadata?.() },
|
|
seeked(video = videos.at(-1)) { video.onseeked?.() },
|
|
finish(index = 0, blob = { type: 'image/jpeg' }) { blobs[index].callback(blob) },
|
|
}
|
|
}
|