mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-14 08:26:16 -08:00
test(auto-thumbnail): [PKG-AUTO-THUMB-02] reproduce extraction and lifecycle failures
This commit is contained in:
1 parent
cdc252b158
commit
9cd6659ab4
11 files changed
+2441
-14
No files matched your search
@@ -0,0 +1,195 @@
|
||||
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')
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user