mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
test(jassub): [PKG-JASSUB-09] isolate Firefox bitmap readback stalls
This commit is contained in:
1 parent
7f185f698d
commit
7ef605fba7
9 files changed
+5843
-13
No files matched your search
@@ -0,0 +1,14 @@
|
||||
import base from './playwright.config.js'
|
||||
|
||||
// Diagnostic profile only; canonical browser acceptance uses playwright.config.js.
|
||||
export default {
|
||||
...base,
|
||||
testMatch: /jassub-(?:native|hybrid|platform)\.spec\.js$/,
|
||||
projects: base.projects.filter(project => project.name === 'firefox').map(project => ({
|
||||
...project,
|
||||
use: {
|
||||
...project.use,
|
||||
launchOptions: { firefoxUserPrefs: { 'gfx.webrender.software': true } },
|
||||
},
|
||||
})),
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,72 @@
|
||||
# PKG-JASSUB-09 Firefox 绘制诊断检查点
|
||||
|
||||
任务保持 doing。本轮未修改生产源码、默认选项、公开类型或发布产物;没有把一次
|
||||
对照通过当作修复。接续 HEAD `7f185f698dbd303b00ca7bc0c21f51cf65c69a04` 的 hybrid
|
||||
修复检查点,调查 JASSUB-FIREFOX-OFFSCREEN-01。
|
||||
|
||||
## 新证据
|
||||
|
||||
默认 Firefox 155.0、Windows、单 worker、不并行运行安装/构建。以前的失败仍保留。
|
||||
|
||||
| 对照 | 结果 | 可以支持的结论 |
|
||||
| --- | --- | --- |
|
||||
| JASSUB 读取前等待 rAF,前置字幕切换 | 1 通过 / 3 失败 | 单等一帧不是修复 |
|
||||
| JASSUB 请求软件 WebRender,前置字幕切换 | 4 通过 | 仅该次诊断成功,不能确定硬件原因 |
|
||||
| JASSUB 显式 asyncRender=false,前置字幕切换 | 4 通过 | 同步路径可作为异步绘制的对照,不改变消费者默认值 |
|
||||
| 原生 Worker fillRect,前置字幕切换 | 4 通过 | 简单绘制对照成功 |
|
||||
| 原生 Worker ImageBitmap,初版较小 native 尺寸 | 2 通过 / 2 失败 | 两个核心宿主失败,native 通过;初版报告误写未使用 ImageBitmap,已在测试及此处更正 |
|
||||
| ImageBitmap,统一 viewport 尺寸且 native 不加载核心脚本,前置字幕切换 | 2 通过 / 2 失败 | 字幕切换与 candidate 控制失败,published/native 控制通过 |
|
||||
| 同样控制,请求软件 WebRender | 3 通过 / 1 失败 | 软件选项也不保证消除问题 |
|
||||
| ImageBitmap,等待 Worker 全部完成再读像素,无前置字幕切换 | 3 通过 | 受控无重叠读取路径通过 |
|
||||
| 同一最终控制取消读取等待,无前置字幕切换 | 2 通过 / 1 失败 | candidate seek 后仍可失败,前置字幕切换并非必要条件 |
|
||||
| 最终 fillRect 控制,三种浏览器,无前置字幕切换 | 9 通过 | Chromium/Firefox 验证 Worker;WebKit 无 transfer,只验证主线程能力回退 |
|
||||
|
||||
统一尺寸的失败记录中,Worker `bitmap-start` 时间为 500 ms,`bitmap-ready`
|
||||
为 10510 ms,随后立即 `drawn`。这把一次约十秒停顿定位到原生
|
||||
createImageBitmap Promise 的等待区间。该页面没有加载 JASSUB wrapper、WASM
|
||||
或字体,不能把停顿归因于本次 hybrid guard。另一方面,纯原生页面迄今通过,
|
||||
因此尚不能断言彻底排除了核心宿主、样式或合成调度的影响。
|
||||
|
||||
读取与绘制重叠是下一步的具体假设,尚未证明因果。需要验证不复制转移画布的
|
||||
真实显示证据,例如浏览器截图中的字幕像素,同时保留原失败用例。不能简单删除
|
||||
像素断言、扩展超时、全局关闭 offscreen/asyncRender,或更改默认 Firefox 配置。
|
||||
修改后的检查必须仍证明播放、seek、布局后的真实字幕,而不只是 Worker 发回通知。
|
||||
|
||||
## 可维护的测试入口
|
||||
|
||||
`test/browser/jassub-native.spec.js` 保留原默认行为,增加显式 rAF 读取和同步绘制
|
||||
诊断参数,并在报告内记录选择。`jassub-platform.spec.js` 只负责分离视频、
|
||||
ImageBitmap、转移画布与宿主影响,不承担 ASS 渲染正确性。它使用原生 Worker,
|
||||
记录创建/绘制时点,释放 Worker、URL、帧回调和宿主;失败收尾读取已保存的像素
|
||||
及当前计数,不再次触发可能阻塞的画布复制。
|
||||
|
||||
`playwright.jassub-diagnostic.config.js` 单独请求软件 WebRender。早期运行使用
|
||||
同偏好的 cache 配置,原文件和指纹保存在验证记录中;正式文件的 reporter 路径
|
||||
继承根配置。这里只确认设置了偏好,没有从 about:support 验证实际 GPU 后端。
|
||||
|
||||
在固定 Node 24.21.0 / Yarn Classic 1.22.22 下复跑:
|
||||
|
||||
```powershell
|
||||
$env:ARTPLAYER_JASSUB_ARTIFACT = 'packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.js'
|
||||
$env:ARTPLAYER_JASSUB_OFFSCREEN = 'default'
|
||||
yarn test:browser test/browser/jassub-hybrid.spec.js test/browser/jassub-native.spec.js --project=firefox --workers=1
|
||||
$env:ARTPLAYER_JASSUB_CONTROL_BITMAP = 'true'
|
||||
$env:ARTPLAYER_JASSUB_CONTROL_IDLE_READBACK = 'false'
|
||||
yarn test:browser test/browser/jassub-platform.spec.js --project=firefox --workers=1
|
||||
$env:ARTPLAYER_JASSUB_CONTROL_IDLE_READBACK = 'true'
|
||||
yarn test:browser test/browser/jassub-platform.spec.js --project=firefox --workers=1
|
||||
```
|
||||
|
||||
各命令之间先归档共享 report/results。使用软件对照时指定
|
||||
`--config playwright.jassub-diagnostic.config.js`;不要混入默认验收统计。
|
||||
关闭诊断时清除对应环境变量。完整路径、报告哈希、选择的参数与失败附件摘要见
|
||||
[机器记录](../baselines/jassub-firefox-diagnostics.json)。原始 trace、截图和报告
|
||||
保留在 `.cache/`;提交的机器记录用于缓存清理后的证据摘要和复跑定位,不代替原始 trace。
|
||||
|
||||
本检查点不关闭 09/05,也不计入 npm、设备、完整组合或三轮发布复盘通过。
|
||||
回退本诊断提交只移除测试入口及记录,不改变上一提交的生产实现。
|
||||
|
||||
最终只读 ESLint(两个 spec 和诊断配置)、固定 Yarn 严格工具链检查、计划与风险
|
||||
登记校验、git diff --check 通过。正式软件配置 `--list` 发现 Firefox 三文件七项;
|
||||
此命令仅验证发现/配置,不声称重新运行其播放。没有依赖/锁文件变更,因此没有
|
||||
重新安装或重跑全部生产单测/类型打包。上一个生产提交的验证仍只代表其既定范围。
|
||||
@@ -1,5 +1,19 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-JASSUB-09 Firefox 原生绘制诊断(doing)
|
||||
|
||||
新增不加载 JASSUB 的 Worker/ImageBitmap 控制,按新核心、已发布核心及无脚本的
|
||||
原生页面验证 seek/viewport 布局。一次失败的 createImageBitmap 等待区间约十秒;
|
||||
统一尺寸后纯原生仍通过,尚不能完全排除核心宿主/样式/合成调度影响。前置切字幕
|
||||
已不是必要条件:无前置测试的重叠读取 2 通过/1 失败,等待 Worker 完成再读取
|
||||
三项通过。软件渲染对照也有失败,rAF 等待亦未解决,均不作为默认配置替代。
|
||||
最终 fillRect 三浏览器控制九项通过;这是原生控制,不是 JASSUB 字幕验收。
|
||||
本轮只改测试和诊断文档,生产代码/类型/产物未变,保留 09/05 和风险 open。
|
||||
[诊断记录](changes/2026-09-14-PKG-JASSUB-09-firefox-diagnostics.md)及
|
||||
[机器证据](baselines/jassub-firefox-diagnostics.json)保存不同参数和所有红绿结果。
|
||||
下一步验证画布像素复制与异步绘制竞争的假设,补充无需复制转移画布的实际显示
|
||||
证据;仍须证明字幕在播放、seek、布局后真实可见,不能只检查 Worker 通知。
|
||||
|
||||
## PKG-JASSUB-09 hybrid 归属修复检查点(doing)
|
||||
|
||||
修复旧混合帧干扰新 offscreen 画布、被丢弃完成通知导致等待卡死,以及销毁后
|
||||
|
||||
@@ -251,4 +251,4 @@
|
||||
| JASSUB-QUERY-01 | resolved / 已复现 | Subtitle query errors throw before calling the consumer and leave request listeners attached | PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
| JASSUB-OFFSCREEN-01 | resolved / 已复现 | Default offscreen initialization lets queued resize render before Worker canvas ownership, raising clearRect on false context | PKG-JASSUB-08, PKG-JASSUB-05 |
|
||||
| JASSUB-HYBRID-01 | open / 已复现 | Late hybrid frames mutate the new offscreen canvas or throw; terminal track calls recreate transferred canvases | PKG-JASSUB-09, PKG-JASSUB-05 |
|
||||
| JASSUB-FIREFOX-OFFSCREEN-01 | open / 已复现 | Windows Firefox default offscreen readback/playback can stall after a preceding subtitle-switch test and web fullscreen | PKG-JASSUB-09, PKG-JASSUB-05 |
|
||||
| JASSUB-FIREFOX-OFFSCREEN-01 | open / 已复现 | Windows Firefox offscreen readback and asynchronous ImageBitmap creation can stall through seek/layout, including a minimal Worker control | PKG-JASSUB-09, PKG-JASSUB-05 |
|
||||
+4
-1
@@ -5640,7 +5640,7 @@
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-FIREFOX-OFFSCREEN-01",
|
||||
"title": "Windows Firefox default offscreen readback/playback can stall after a preceding subtitle-switch test and web fullscreen",
|
||||
"title": "Windows Firefox offscreen readback and asynchronous ImageBitmap creation can stall through seek/layout, including a minimal Worker control",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
@@ -5650,6 +5650,9 @@
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-09-hybrid.md",
|
||||
"refactor/baselines/jassub-hybrid-validation.json",
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-09-firefox-diagnostics.md",
|
||||
"refactor/baselines/jassub-firefox-diagnostics.json",
|
||||
"test/browser/jassub-platform.spec.js",
|
||||
"test/browser/jassub-native.spec.js",
|
||||
"test/browser/jassub-hybrid.spec.js"
|
||||
],
|
||||
|
||||
@@ -185,3 +185,23 @@ cleanup. ARTPLAYER_JASSUB_CUSTOM_CANVAS=true supplies a caller-owned canvas and
|
||||
asserts it remains connected after destroy(false). The native report records the
|
||||
artifact hash and both mode choices. This does not change the default published
|
||||
baseline or establish ESM/offscreen/device acceptance.
|
||||
|
||||
JASSUB Firefox diagnostics retain the canonical failing cases. Set
|
||||
`ARTPLAYER_JASSUB_OFFSCREEN=default` to exercise native capability selection.
|
||||
`ARTPLAYER_JASSUB_ASYNC_RENDER=false` explicitly selects synchronous rendering;
|
||||
`ARTPLAYER_JASSUB_READBACK_FRAME=true` waits one animation frame before readback.
|
||||
Neither diagnostic changes the default candidate or constitutes a runtime fix.
|
||||
`playwright.jassub-diagnostic.config.js` requests Firefox software WebRender for
|
||||
comparison only; it is not the canonical browser acceptance configuration.
|
||||
|
||||
`jassub-platform.spec.js` isolates native video and a minimal canvas Worker under
|
||||
candidate/published cores and a script-free native host. All hosts resize to the
|
||||
viewport. Default drawing uses fillRect; `ARTPLAYER_JASSUB_CONTROL_BITMAP=true`
|
||||
selects actual createImageBitmap/drawImage/close and records worker stage times.
|
||||
`ARTPLAYER_JASSUB_CONTROL_IDLE_READBACK=true` pauses submissions while waiting for
|
||||
all draw acknowledgements before copying the display canvas. This is a deliberate
|
||||
scheduling diagnostic, not unchanged production timing. Hosts without canvas
|
||||
transfer use main-thread fillRect and do not verify the Worker/ImageBitmap path.
|
||||
The report records these distinctions and never infers a JASSUB fix from a control
|
||||
pass. Preserve each raw report/results directory before the next browser command.
|
||||
See [the isolation record](../../refactor/changes/2026-09-14-PKG-JASSUB-09-firefox-diagnostics.md).
|
||||
@@ -12,6 +12,15 @@ const customCanvas = process.env.ARTPLAYER_JASSUB_CUSTOM_CANVAS === 'true'
|
||||
const code = artifact ? fs.readFileSync(artifact, 'utf8') : readMember(archive, member).toString()
|
||||
const onDemandRender = process.env.ARTPLAYER_JASSUB_ON_DEMAND !== 'false'
|
||||
const defaultOffscreen = process.env.ARTPLAYER_JASSUB_OFFSCREEN === 'default'
|
||||
const readbackFrame = process.env.ARTPLAYER_JASSUB_READBACK_FRAME === 'true'
|
||||
const synchronousRender = process.env.ARTPLAYER_JASSUB_ASYNC_RENDER === 'false'
|
||||
async function pixels(page, phase) {
|
||||
return page.evaluate(async ({ phase, readbackFrame }) => {
|
||||
if (readbackFrame)
|
||||
await new Promise(requestAnimationFrame)
|
||||
return window.jassubPixels(phase)
|
||||
}, { phase, readbackFrame })
|
||||
}
|
||||
const subtitles = `[Script Info]
|
||||
ScriptType: v4.00+
|
||||
PlayResX: 640
|
||||
@@ -71,7 +80,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
}
|
||||
|
||||
postMessage(message, ...rest) {
|
||||
this.observation.sent.push({ target: message.target, time: message.time, width: message.width, height: message.height })
|
||||
this.observation.sent.push({ target: message.target, time: message.time, width: message.width, height: message.height, asyncRender: message.asyncRender })
|
||||
return super.postMessage(message, ...rest)
|
||||
}
|
||||
|
||||
@@ -87,7 +96,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
await page.evaluate(async ({ subContent, onDemandRender, customCanvas, defaultOffscreen }) => {
|
||||
await page.evaluate(async ({ subContent, onDemandRender, customCanvas, defaultOffscreen, synchronousRender }) => {
|
||||
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
|
||||
let canvas
|
||||
if (customCanvas) {
|
||||
@@ -106,6 +115,7 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' },
|
||||
fonts: ['/assets/jassub/default.woff2'],
|
||||
...(defaultOffscreen ? {} : { offscreenRender: false }),
|
||||
...(synchronousRender ? { asyncRender: false } : {}),
|
||||
onDemandRender,
|
||||
...(canvas ? { canvas } : {}),
|
||||
}))
|
||||
@@ -147,12 +157,12 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
window.jassubNative.snapshots.push(state)
|
||||
return state
|
||||
}
|
||||
}, { subContent: subtitles, onDemandRender, customCanvas, defaultOffscreen })
|
||||
}, { subContent: subtitles, onDemandRender, customCanvas, defaultOffscreen, synchronousRender })
|
||||
const offscreen = await page.evaluate(() => window.jassubNative.offscreen)
|
||||
expect(offscreen.selected).toBe(defaultOffscreen && !customCanvas && offscreen.available)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
|
||||
const before = await page.evaluate(() => window.jassubPixels('before-seek'))
|
||||
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
|
||||
const before = await pixels(page, 'before-seek')
|
||||
expect(before.connected).toBe(true)
|
||||
await page.locator('#pause').click()
|
||||
await page.evaluate(async () => {
|
||||
@@ -161,17 +171,17 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
await seeked
|
||||
})
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().signature)).not.toBe(before.signature)
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
|
||||
const after = await page.evaluate(() => window.jassubPixels('after-seek'))
|
||||
await expect.poll(async () => (await pixels(page)).signature).not.toBe(before.signature)
|
||||
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
|
||||
const after = await pixels(page, 'after-seek')
|
||||
expect(after.time).toBeGreaterThanOrEqual(10)
|
||||
expect(after.signature).not.toBe(before.signature)
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().cssWidth)).toBeGreaterThan(before.cssWidth)
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
|
||||
await page.evaluate(() => window.jassubPixels('fullscreen'))
|
||||
await expect.poll(async () => (await pixels(page)).cssWidth).toBeGreaterThan(before.cssWidth)
|
||||
await expect.poll(async () => (await pixels(page)).visible).toBeGreaterThan(100)
|
||||
await pixels(page, 'fullscreen')
|
||||
await testInfo.attach('actual-jassub-page', { body: await page.screenshot(), contentType: 'image/png' })
|
||||
if (artifact)
|
||||
await page.evaluate(() => window.jassub.destroy())
|
||||
@@ -187,6 +197,8 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
}
|
||||
finally {
|
||||
const state = await page.evaluate(() => window.jassubNative).catch(error => ({ unavailable: error.message }))
|
||||
state.readbackFrame = readbackFrame
|
||||
state.synchronousRender = synchronousRender
|
||||
await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, customCanvas, defaultOffscreen, source: artifact ? { artifact, sha256: hash(code) } : { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: `Actual worker, WASM and local font; offscreen option ${defaultOffscreen ? 'omitted, actual capability/selection recorded' : 'explicitly false'}. Canvas pixels are read from a separate canvas copying the native display bitmap. No full failure recovery, sustained GPU/memory or physical-device acceptance; candidate adds direct-then-host destruction.` }, null, 2), contentType: 'application/json' })
|
||||
if (!page.isClosed()) {
|
||||
await page.evaluate(() => {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import process from 'node:process'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const bitmap = process.env.ARTPLAYER_JASSUB_CONTROL_BITMAP === 'true'
|
||||
const idleReadback = process.env.ARTPLAYER_JASSUB_CONTROL_IDLE_READBACK === 'true'
|
||||
|
||||
for (const host of ['candidate', 'published', 'native']) {
|
||||
test(`Native canvas and video control through seek/layout without JASSUB: ${host}`, async ({ page }, testInfo) => {
|
||||
if (host === 'native') {
|
||||
await page.route('**/test/native-canvas.html', route => route.fulfill({
|
||||
contentType: 'text/html',
|
||||
body: '<!doctype html><html><head><title>Native canvas control</title></head><body></body></html>',
|
||||
}))
|
||||
await page.goto('/test/native-canvas.html')
|
||||
}
|
||||
else {
|
||||
await page.goto(`/test/player.html?core=${host}`)
|
||||
}
|
||||
await page.evaluate(async ({ host, bitmap, idleReadback }) => {
|
||||
let video
|
||||
if (host === 'native') {
|
||||
video = document.createElement('video')
|
||||
video.src = '/assets/sample/steve-jobs.mp4'
|
||||
video.muted = true
|
||||
video.style.width = '640px'
|
||||
video.style.height = '360px'
|
||||
document.body.append(video)
|
||||
}
|
||||
else {
|
||||
window.createPlayer('/assets/sample/steve-jobs.mp4')
|
||||
video = window.art.video
|
||||
}
|
||||
const canvas = document.createElement('canvas')
|
||||
document.body.append(canvas)
|
||||
const readback = document.createElement('canvas')
|
||||
const probe = window.nativeCanvasProbe = { video, canvas, received: 0, frames: 0, sent: 0, bitmap, idleReadback, reading: false, stages: [], errors: [], pixel: null, offscreen: 'transferControlToOffscreen' in canvas, closed: false }
|
||||
const draw = (ctx, canvas, message) => {
|
||||
if (canvas.width !== message.width)
|
||||
canvas.width = message.width
|
||||
if (canvas.height !== message.height)
|
||||
canvas.height = message.height
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.fillStyle = message.time < 5 ? 'red' : 'blue'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
if (probe.offscreen) {
|
||||
const source = `let canvas, ctx; const draw = ${draw.toString()}; onmessage = async ({data}) => {
|
||||
if (data.canvas) { canvas = data.canvas; ctx = canvas.getContext('2d'); return; }
|
||||
if (${bitmap}) {
|
||||
const pixels = new Uint8ClampedArray(data.width * data.height * 4);
|
||||
for (let i = 0; i < pixels.length; i += 4) { pixels[i + (data.time < 5 ? 0 : 2)] = 255; pixels[i + 3] = 255; }
|
||||
postMessage({stage: 'bitmap-start', time: data.time, at: performance.now()});
|
||||
const image = await createImageBitmap(new ImageData(pixels, data.width, data.height));
|
||||
postMessage({stage: 'bitmap-ready', time: data.time, at: performance.now()});
|
||||
if (canvas.width !== data.width) canvas.width = data.width;
|
||||
if (canvas.height !== data.height) canvas.height = data.height;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(image, 0, 0); image.close();
|
||||
} else { draw(ctx, canvas, data); }
|
||||
postMessage({stage: 'drawn', time: data.time, at: performance.now()});
|
||||
};`
|
||||
probe.workerUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }))
|
||||
probe.worker = new Worker(probe.workerUrl)
|
||||
const transferred = canvas.transferControlToOffscreen()
|
||||
probe.worker.postMessage({ canvas: transferred }, [transferred])
|
||||
probe.worker.onmessage = ({ data }) => {
|
||||
probe.stages.push(data)
|
||||
if (data.stage === 'drawn')
|
||||
probe.received++
|
||||
if (probe.received === probe.sent)
|
||||
probe.drain?.()
|
||||
}
|
||||
probe.worker.onerror = event => probe.errors.push(event.message)
|
||||
probe.post = message => probe.worker.postMessage(message)
|
||||
}
|
||||
else {
|
||||
const ctx = canvas.getContext('2d')
|
||||
probe.post = (message) => {
|
||||
draw(ctx, canvas, message)
|
||||
probe.received++
|
||||
}
|
||||
}
|
||||
const render = () => {
|
||||
if (probe.closed)
|
||||
return
|
||||
probe.frames++
|
||||
if (!probe.reading) {
|
||||
const rect = video.getBoundingClientRect()
|
||||
probe.post({ width: Math.round(rect.width), height: Math.round(rect.height), time: video.currentTime })
|
||||
probe.sent++
|
||||
}
|
||||
if (video.requestVideoFrameCallback)
|
||||
probe.handle = video.requestVideoFrameCallback(render)
|
||||
else
|
||||
probe.handle = requestAnimationFrame(render)
|
||||
}
|
||||
probe.state = () => ({ pixel: probe.pixel, width: canvas.width, height: canvas.height, time: video.currentTime, frames: probe.frames, sent: probe.sent, received: probe.received, quality: video.getVideoPlaybackQuality?.().totalVideoFrames })
|
||||
probe.sample = async () => {
|
||||
if (idleReadback) {
|
||||
probe.reading = true
|
||||
if (probe.received < probe.sent)
|
||||
await new Promise(resolve => probe.drain = resolve)
|
||||
}
|
||||
readback.width = canvas.width
|
||||
readback.height = canvas.height
|
||||
const ctx = readback.getContext('2d')
|
||||
ctx.drawImage(canvas, 0, 0)
|
||||
probe.pixel = [...ctx.getImageData(0, 0, 1, 1).data]
|
||||
probe.reading = false
|
||||
return probe.state()
|
||||
}
|
||||
await video.play()
|
||||
render()
|
||||
}, { host, bitmap, idleReadback })
|
||||
try {
|
||||
await expect.poll(async () => (await page.evaluate(() => window.nativeCanvasProbe.sample())).pixel).toEqual([255, 0, 0, 255])
|
||||
const before = await page.evaluate(() => window.nativeCanvasProbe.sample())
|
||||
await page.evaluate(async () => {
|
||||
const video = window.nativeCanvasProbe.video
|
||||
video.pause()
|
||||
const seeked = new Promise(resolve => video.addEventListener('seeked', resolve, { once: true }))
|
||||
video.currentTime = 10
|
||||
await seeked
|
||||
await video.play()
|
||||
})
|
||||
await expect.poll(async () => (await page.evaluate(() => window.nativeCanvasProbe.sample())).pixel).toEqual([0, 0, 255, 255])
|
||||
await page.evaluate((host) => {
|
||||
if (host === 'native') {
|
||||
Object.assign(window.nativeCanvasProbe.video.style, { position: 'fixed', inset: '0', width: '100vw', height: '100vh' })
|
||||
}
|
||||
else {
|
||||
window.art.fullscreenWeb = true
|
||||
}
|
||||
}, host)
|
||||
await expect.poll(async () => (await page.evaluate(() => window.nativeCanvasProbe.sample())).width).toBeGreaterThan(before.width)
|
||||
await expect.poll(async () => (await page.evaluate(() => window.nativeCanvasProbe.sample())).time).toBeGreaterThan(10.2)
|
||||
await expect.poll(async () => (await page.evaluate(() => window.nativeCanvasProbe.sample())).pixel).toEqual([0, 0, 255, 255])
|
||||
}
|
||||
finally {
|
||||
const evidence = await page.evaluate(() => {
|
||||
const probe = window.nativeCanvasProbe
|
||||
const state = { offscreen: probe.offscreen, bitmap: probe.bitmap, idleReadback: probe.idleReadback, stages: probe.stages, errors: probe.errors, coreLoaded: typeof window.Artplayer !== 'undefined', ...probe.state() }
|
||||
probe.closed = true
|
||||
if (probe.video.cancelVideoFrameCallback)
|
||||
probe.video.cancelVideoFrameCallback(probe.handle)
|
||||
else
|
||||
cancelAnimationFrame(probe.handle)
|
||||
probe.video.pause()
|
||||
probe.worker?.terminate()
|
||||
if (probe.workerUrl)
|
||||
URL.revokeObjectURL(probe.workerUrl)
|
||||
probe.canvas.remove()
|
||||
if (window.art)
|
||||
window.art.destroy(false)
|
||||
else
|
||||
probe.video.remove()
|
||||
return state
|
||||
})
|
||||
await testInfo.attach('native-canvas-control', { body: JSON.stringify({ host, evidence, limitation: 'Native video/frame callbacks and a minimal Worker painting opaque colors. Bitmap mode uses createImageBitmap/drawImage/close; otherwise fillRect. JASSUB wrapper, WASM and fonts are absent. The native host loads no ArtPlayer scripts. Capability fallback without transfer is main-thread fillRect, not offscreen or ImageBitmap verification.' }), contentType: 'application/json' })
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user