mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
196 lines
9.4 KiB
JavaScript
196 lines
9.4 KiB
JavaScript
import process from 'node:process'
|
|
import { hash } from '../../refactor/scripts/releases.mjs'
|
|
import { autoThumbnailHistorical } from '../helpers/auto-thumbnail.js'
|
|
import { expect, test } from './fixtures.js'
|
|
|
|
const implementations = (await autoThumbnailHistorical()).filter(item => ['published-1.0.1-main', 'published-1.1.0-main', 'frozen-workspace-js'].includes(item.name))
|
|
|
|
for (const implementation of implementations) {
|
|
test(`Auto-thumbnail ${implementation.name}: native JPEG callbacks outlive destroy and can restore the blank sheet`, async ({ page, browserName }, testInfo) => {
|
|
await page.goto('/test/player.html?core=published')
|
|
await page.setContent('<!doctype html><div id="probe"></div>')
|
|
await page.addScriptTag({ content: implementation.code })
|
|
await page.evaluate(async () => {
|
|
const createElement = document.createElement.bind(document)
|
|
const createUrl = URL.createObjectURL.bind(URL)
|
|
const revokeUrl = URL.revokeObjectURL.bind(URL)
|
|
const toBlob = HTMLCanvasElement.prototype.toBlob
|
|
const probe = window.probe = { videos: [], canvases: [], encodes: [], updates: [], urls: new Map(), revoked: [], seeks: [], destroyed: false }
|
|
document.createElement = function (name, ...args) {
|
|
const element = createElement(name, ...args)
|
|
if (name === 'video') {
|
|
probe.videos.push(element)
|
|
element.addEventListener('seeked', () => probe.seeks.push(element.currentTime))
|
|
}
|
|
if (name === 'canvas')
|
|
probe.canvases.push(element)
|
|
return element
|
|
}
|
|
HTMLCanvasElement.prototype.toBlob = function (callback, type, quality) {
|
|
const index = probe.encodes.length
|
|
const record = { index, blob: null, callback, type, width: this.width, height: this.height }
|
|
probe.encodes.push(record)
|
|
return toBlob.call(this, blob => record.blob = blob, type, quality)
|
|
}
|
|
URL.createObjectURL = (blob) => {
|
|
const url = createUrl(blob)
|
|
probe.urls.set(url, blob)
|
|
return url
|
|
}
|
|
URL.revokeObjectURL = (url) => {
|
|
probe.revoked.push(url)
|
|
probe.urls.delete(url)
|
|
revokeUrl(url)
|
|
}
|
|
const listeners = new Map()
|
|
const art = {
|
|
option: { url: '/test/pattern.mp4' },
|
|
on(name, callback) {
|
|
if (!listeners.has(name))
|
|
listeners.set(name, new Set())
|
|
listeners.get(name).add(callback)
|
|
},
|
|
emit(name) {
|
|
for (const callback of listeners.get(name) || []) callback()
|
|
},
|
|
get thumbnails() { return probe.updates.at(-1) },
|
|
set thumbnails(value) { probe.updates.push({ ...value, afterDestroy: probe.destroyed }) },
|
|
}
|
|
probe.art = art
|
|
probe.nativeToBlob = toBlob
|
|
probe.restore = () => {
|
|
document.createElement = createElement
|
|
HTMLCanvasElement.prototype.toBlob = toBlob
|
|
URL.createObjectURL = createUrl
|
|
URL.revokeObjectURL = revokeUrl
|
|
}
|
|
probe.result = await window.artplayerPluginAutoThumbnail({ width: 80, number: 4 })(art)
|
|
art.emit('video:loadedmetadata')
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.probe.encodes.filter(item => item.blob).length)).toBe(5)
|
|
const evidence = await page.evaluate(async () => {
|
|
const probe = window.probe
|
|
async function pixels(blob) {
|
|
const image = document.createElement('img')
|
|
const url = URL.createObjectURL(blob)
|
|
image.src = url
|
|
await image.decode()
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = image.naturalWidth
|
|
canvas.height = image.naturalHeight
|
|
const ctx = canvas.getContext('2d')
|
|
ctx.drawImage(image, 0, 0)
|
|
const samples = Array.from({ length: 4 }, (_, index) => [...ctx.getImageData(40 + index * 80, 22, 1, 1).data])
|
|
URL.revokeObjectURL(url)
|
|
return { width: canvas.width, height: canvas.height, pixel: samples[0], samples, type: blob.type }
|
|
}
|
|
const blanks = await pixels(probe.encodes[0].blob)
|
|
const final = await pixels(probe.encodes[4].blob)
|
|
const controlVideo = document.createElement('video')
|
|
controlVideo.crossOrigin = 'anonymous'
|
|
controlVideo.muted = true
|
|
document.body.append(controlVideo)
|
|
await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => reject(new Error('Native control did not load')), 5000)
|
|
controlVideo.onloadeddata = () => {
|
|
clearTimeout(timeout)
|
|
resolve()
|
|
}
|
|
controlVideo.onerror = () => {
|
|
clearTimeout(timeout)
|
|
reject(new Error(`Native control media error ${controlVideo.error?.code}`))
|
|
}
|
|
controlVideo.src = '/test/pattern.mp4'
|
|
})
|
|
await controlVideo.play()
|
|
await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => reject(new Error('Native control playback did not advance')), 5000)
|
|
const check = () => {
|
|
if (controlVideo.currentTime >= 0.25) {
|
|
clearTimeout(timeout)
|
|
resolve()
|
|
}
|
|
else {
|
|
requestAnimationFrame(check)
|
|
}
|
|
}
|
|
check()
|
|
})
|
|
controlVideo.pause()
|
|
const controlCanvas = document.createElement('canvas')
|
|
controlCanvas.width = 800
|
|
controlCanvas.height = 45
|
|
const controlContext = controlCanvas.getContext('2d')
|
|
controlContext.drawImage(controlVideo, 0, 0, 80, 45)
|
|
const rawPixel = [...controlContext.getImageData(40, 22, 1, 1).data]
|
|
const controlBlob = await new Promise(resolve => probe.nativeToBlob.call(controlCanvas, resolve, 'image/jpeg'))
|
|
const native = { readyState: controlVideo.readyState, currentTime: controlVideo.currentTime, rawPixel, ...await pixels(controlBlob) }
|
|
controlContext.fillStyle = 'red'
|
|
controlContext.fillRect(0, 0, 80, 45)
|
|
const staticBlob = await new Promise(resolve => probe.nativeToBlob.call(controlCanvas, resolve, 'image/jpeg'))
|
|
const staticControl = await pixels(staticBlob)
|
|
probe.encodes[4].callback(probe.encodes[4].blob)
|
|
const good = probe.updates.at(-1).url
|
|
probe.destroyed = true
|
|
probe.art.emit('destroy')
|
|
const outstandingAfterDestroy = probe.urls.size
|
|
probe.encodes[0].callback(probe.encodes[0].blob)
|
|
const bad = probe.updates.at(-1).url
|
|
const result = {
|
|
blanks,
|
|
final,
|
|
native,
|
|
staticControl,
|
|
seeks: probe.seeks,
|
|
updates: probe.updates.map(({ url, ...rest }) => rest),
|
|
outstandingAfterDestroy,
|
|
goodRevoked: !probe.urls.has(good),
|
|
staleUrlLive: probe.urls.has(bad),
|
|
result: probe.result,
|
|
videoState: { src: probe.videos[0].getAttribute('src'), readyState: probe.videos[0].readyState, width: probe.videos[0].videoWidth, seekHandler: typeof probe.videos[0].onseeked, metadataHandler: typeof probe.videos[0].onloadedmetadata },
|
|
}
|
|
// The test owns the cleanup because the historical plugin does not.
|
|
for (const video of probe.videos) {
|
|
video.pause()
|
|
video.removeAttribute('src')
|
|
video.load()
|
|
video.remove()
|
|
}
|
|
for (const url of [...probe.urls.keys()]) URL.revokeObjectURL(url)
|
|
probe.restore()
|
|
return result
|
|
})
|
|
await testInfo.attach('auto-thumbnail-native-historical', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scope: 'Native HTTP video, seeking and JPEG pixels with controlled delivery of native toBlob results; player-facing host is a stub, not a core integration result.', ...evidence }) })
|
|
expect(evidence.result).toEqual({ name: 'artplayerPluginAutoThumbnail' })
|
|
expect(evidence.blanks.type).toBe('image/jpeg')
|
|
expect(evidence.final.width).toBe(800)
|
|
expect(evidence.final.height).toBe(45)
|
|
expect(evidence.blanks.pixel.slice(0, 3).every(value => value < 5)).toBe(true)
|
|
expect(evidence.staticControl.pixel[0]).toBeGreaterThan(240)
|
|
expect(evidence.staticControl.pixel[1]).toBeLessThan(10)
|
|
expect(evidence.staticControl.pixel[2]).toBeLessThan(10)
|
|
expect(evidence.native.currentTime).toBeGreaterThanOrEqual(0.25)
|
|
expect(evidence.native.rawPixel.slice(0, 3).some(value => value > 30)).toBe(true)
|
|
expect(evidence.native.pixel.slice(0, 3).some(value => value > 30)).toBe(true)
|
|
const blackSamples = evidence.final.samples.every(pixel => pixel.slice(0, 3).every(value => value < 5))
|
|
if (browserName === 'webkit' && process.platform === 'win32')
|
|
expect(blackSamples).toBe(true)
|
|
if (blackSamples) {
|
|
expect(browserName).toBe('webkit')
|
|
testInfo.annotations.push({ type: 'historical-media-defect', description: 'Historical plugin JPEG samples are black, while independent played HTTP video and static red canvas yield colored JPEGs. This is an extraction-path defect, not a waived browser capability.' })
|
|
}
|
|
else {
|
|
expect(evidence.final.samples.every(pixel => pixel.slice(0, 3).some(value => value > 30))).toBe(true)
|
|
}
|
|
expect(evidence.seeks).toHaveLength(4)
|
|
expect(evidence.outstandingAfterDestroy).toBe(1)
|
|
expect(evidence.updates.map(item => item.afterDestroy)).toEqual([false, true])
|
|
expect(evidence.goodRevoked).toBe(true)
|
|
expect(evidence.staleUrlLive).toBe(true)
|
|
expect(evidence.videoState.src).toBe('/test/pattern.mp4')
|
|
expect(evidence.videoState.width).toBeGreaterThan(0)
|
|
expect(evidence.videoState.seekHandler).toBe('function')
|
|
expect(evidence.videoState.metadataHandler).toBe('function')
|
|
})
|
|
}
|