mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 17:46:17 -08:00
refactor(canvas): [PKG-CANVAS-03] own media and frame lifecycle in strict TypeScript
This commit is contained in:
1 parent
e931520e66
commit
2b319109cd
29 files changed
+2015
-362
No files matched your search
@@ -1,12 +1,11 @@
|
||||
import fs from 'node:fs'
|
||||
import { transform } from 'esbuild'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { ambilightCandidate } from '../helpers/ambilight.js'
|
||||
import { canvasCandidate } from '../helpers/canvas.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = await ambilightCandidate()
|
||||
const proxySource = fs.readFileSync('packages/artplayer-proxy-canvas/src/index.js', 'utf8')
|
||||
const proxy = (await transform(proxySource, { format: 'cjs', target: 'es2020' })).code
|
||||
const proxySource = (await canvasCandidate()).source
|
||||
const proxy = proxySource
|
||||
|
||||
for (const core of ['published-5.1.7', 'published', 'candidate']) {
|
||||
const scenario = core === 'published-5.1.7' ? 'historical core has no proxy option and retains native Ambilight playback' : 'Ambilight samples all nine canvas proxy regions after output resizing'
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
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' })
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user