Files
ArtPlayer/test/helpers/auto-thumbnail.js

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) },
}
}