Files
ArtPlayer/test/browser/canvas-lifecycle.spec.js

135 lines
8.4 KiB
JavaScript

import { hash } from '../../refactor/scripts/releases.mjs'
import { canvasCandidate } from '../helpers/canvas.js'
import { expect, test } from './fixtures.js'
const candidate = await canvasCandidate()
async function setup(page, core, testInfo) {
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: `(() => {
const pending = new Set();
const state = { requested: 0, fired: 0, cancelled: 0 };
const requestAnimationFrame = callback => {
const id = window.requestAnimationFrame(time => { pending.delete(id); state.fired++; callback(time); });
pending.add(id); state.requested++; return id;
};
const cancelAnimationFrame = id => { pending.delete(id); state.cancelled++; window.cancelAnimationFrame(id); };
const module = { exports: {} }; const exports = module.exports;
${candidate.source};
window.canvasFactory = module.exports.default || module.exports;
window.canvasFrames = { pending, state };
})();` })
await page.evaluate(() => {
window.proxyDraws = 0
window.proxyErrors = []
window.proxyErrorStates = []
window.proxyEvents = []
window.art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4',
muted: true,
loop: true,
proxy: window.canvasFactory((context, video) => {
window.proxyContext = context
window.proxyVideo = video
window.proxyDraws++
if (window.destroyNextDraw) {
window.destroyNextDraw = false
window.art.destroy(false)
window.destroyedInCallback = true
}
}),
})
for (const name of ['video:loadedmetadata', 'video:loadeddata', 'video:canplay', 'video:seeked', 'artplayerProxyCanvas:draw', 'destroy'])
window.art.on(name, () => window.proxyEvents.push(name))
window.art.on('artplayerProxyCanvas:error', (error) => {
window.proxyErrors.push({ name: error.name, message: error.message })
const video = window.proxyVideo || window.art?.template.$player.querySelector('video')
let direct
if (video) {
const canvas = document.createElement('canvas')
try {
const context = canvas.getContext('2d')
context.drawImage(video, 0, 0, 300, 150)
direct = [...context.getImageData(0, 0, 1, 1).data]
}
catch (failure) { direct = failure.name }
}
window.proxyErrorStates.push({ readyState: video?.readyState, seeking: video?.seeking, paused: video?.paused, currentTime: video?.currentTime, source: video?.currentSrc, dimensions: [video?.videoWidth, video?.videoHeight], frames: video?.getVideoPlaybackQuality?.().totalVideoFrames, direct })
})
document.querySelector('#play').onclick = () => window.art.play()
document.querySelector('#pause').onclick = () => window.art.pause()
})
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.proxyDraws)).toBeGreaterThan(1)
await expect.poll(() => page.evaluate(() => window.proxyVideo.currentTime)).toBeGreaterThan(0)
await testInfo.attach('canvas-candidate-input', { contentType: 'application/json', body: JSON.stringify({ core, sha256: hash(candidate.source), scope: 'actual candidate source bundle, native media/Canvas/bitmap; lexical RAF observer delegates native scheduling' }) })
}
test.afterEach(async ({ page }, testInfo) => {
await testInfo.attach('canvas-candidate-error-states', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => window.proxyErrorStates)) })
await testInfo.attach('canvas-candidate-state', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ version: window.Artplayer?.version, destroyed: window.art?.isDestroy, connected: window.proxyVideo?.isConnected, currentSrc: window.proxyVideo?.currentSrc, currentTime: window.proxyVideo?.currentTime, frames: window.canvasFrames?.state, pending: window.canvasFrames?.pending.size, draws: window.proxyDraws, errors: window.proxyErrors, events: window.proxyEvents }))) })
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
})
})
for (const core of ['published', 'candidate']) {
test(`${core}: candidate canvas paints native pixels, resizes and switches media`, async ({ page }, testInfo) => {
await setup(page, core, testInfo)
expect(await page.evaluate(() => window.art.template.$video.nodeName)).toBe('CANVAS')
expect(await page.evaluate(() => window.proxyVideo.isConnected)).toBe(true)
expect(await page.evaluate(() => [window.proxyVideo.videoWidth, window.proxyVideo.videoHeight])).toEqual([320, 180])
await expect.poll(() => page.evaluate(() => window.proxyContext.getImageData(0, 0, 1, 1).data[3])).toBe(255)
expect(await page.evaluate(() => {
const { canvas } = window.proxyContext
const values = [0.2, 0.5, 0.8].map(ratio => [...window.proxyContext.getImageData(Math.floor(canvas.width * ratio), Math.floor(canvas.height / 2), 1, 1).data].join(','))
return new Set(values).size
})).toBeGreaterThan(1)
await page.evaluate(() => {
window.art.template.$player.style.width = '320px'
window.art.template.$player.style.height = '480px'
window.art.emit('resize')
})
await testInfo.attach('canvas-resize-dimensions', { contentType: 'application/json', body: JSON.stringify(await page.evaluate(() => ({ media: [window.proxyVideo.videoWidth, window.proxyVideo.videoHeight], css: [window.proxyVideo.clientWidth, window.proxyVideo.clientHeight], player: [window.art.template.$player.clientWidth, window.art.template.$player.clientHeight], canvas: [window.proxyContext.canvas.width, window.proxyContext.canvas.height], padding: window.proxyContext.canvas.style.padding }))) })
expect(await page.evaluate(() => window.art.template.$video.style.padding)).toBe('150px 0px')
await expect.poll(() => page.evaluate(() => window.proxyContext.getImageData(0, 0, 1, 1).data[3])).toBe(255)
await page.evaluate(() => window.art.switchUrl('/assets/sample/video.mp4'))
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.proxyVideo.currentSrc.endsWith('/assets/sample/video.mp4') && window.proxyVideo.currentTime > 0)).toBe(true)
await expect.poll(() => page.evaluate(() => window.proxyContext.getImageData(0, 0, 1, 1).data[3])).toBe(255)
expect(await page.evaluate(() => window.proxyErrors)).toEqual([])
})
test(`${core}: candidate paused seek draws the new video frame without restarting RAF`, async ({ page }, testInfo) => {
await setup(page, core, testInfo)
await page.click('#pause')
await expect.poll(() => page.evaluate(() => window.canvasFrames.pending.size)).toBe(0)
const count = await page.evaluate(() => window.proxyDraws)
await page.evaluate(() => window.art.currentTime = 0.5)
await expect.poll(() => page.evaluate(() => window.proxyDraws)).toBeGreaterThan(count)
await expect.poll(() => page.evaluate(() => window.proxyContext.getImageData(0, 0, 1, 1).data[3])).toBe(255)
expect(await page.evaluate(() => ({ paused: window.proxyVideo.paused, pending: window.canvasFrames.pending.size }))).toEqual({ paused: true, pending: 0 })
})
test(`${core}: candidate destroy inside callback leaves no media or native RAF and escaped play is inert`, async ({ page }, testInfo) => {
await setup(page, core, testInfo)
await page.evaluate(() => {
window.escapedCanvas = window.art.template.$video
window.escapedPlay = window.escapedCanvas.play
window.destroyNextDraw = true
})
await expect.poll(() => page.evaluate(() => window.destroyedInCallback && window.art.isDestroy)).toBe(true)
expect(await page.evaluate(() => window.canvasFrames.pending.size)).toBe(0)
const before = await page.evaluate(() => window.canvasFrames.state.fired)
await page.evaluate(async () => {
window.escapedCanvas.src = '/assets/sample/video.mp4'
await window.escapedPlay()
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
})
expect(await page.evaluate(() => ({ connected: window.proxyVideo.isConnected, paused: window.proxyVideo.paused, source: window.proxyVideo.getAttribute('src'), pending: window.canvasFrames.pending.size, fired: window.canvasFrames.state.fired, last: window.proxyEvents.at(-1) }))).toEqual({ connected: false, paused: true, source: null, pending: 0, fired: before, last: 'destroy' })
})
}