mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-25 08:01:20 -08:00
fix(jassub): [PKG-JASSUB-10] release bitmap batches on rendering errors
This commit is contained in:
1 parent
04060aa8b6
commit
7b0e1d156b
22 files changed
+5795
-22
No files matched your search
@@ -216,6 +216,13 @@ 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-render-failure.spec.js` uses an actual Worker/WASM bitmap and native copies.
|
||||
It intentionally closes one bitmap to provoke native Canvas InvalidStateError,
|
||||
checks release of the entire batch, then pauses and waits for pending demand before
|
||||
public resize(force) verifies a visible subtitle again. Select actual main/legacy
|
||||
with ARTPLAYER_JASSUB_ARTIFACT. This main-thread fault injection does not reproduce
|
||||
spontaneous Worker corruption or resolve the separate Firefox offscreen stall.
|
||||
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const artifact = process.env.ARTPLAYER_JASSUB_ARTIFACT || 'packages/artplayer-plugin-jassub/dist/artplayer-plugin-jassub.js'
|
||||
const code = fs.readFileSync(artifact, 'utf8')
|
||||
const subtitle = `[Script Info]
|
||||
ScriptType: v4.00+
|
||||
PlayResX: 640
|
||||
PlayResY: 360
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Liberation Sans,40,&H0000FF00,&H00000000,&H00000000,0,0,1,0,0,2,10,10,10,1
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:00.00,0:02:00.00,Default,,0,0,0,,Resource recovery subtitle
|
||||
`
|
||||
|
||||
test('JASSUB closes the entire bitmap batch after a native draw failure and renders the next frame', async ({ page }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=candidate')
|
||||
await page.addScriptTag({ content: code })
|
||||
await page.evaluate(() => window.createPlayer('/assets/sample/steve-jobs.mp4'))
|
||||
await page.locator('#play').click()
|
||||
try {
|
||||
await page.evaluate((subtitle) => {
|
||||
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
|
||||
window.art.plugins.add(register({
|
||||
offscreenRender: false,
|
||||
asyncRender: true,
|
||||
workerUrl: '/assets/jassub/jassub-worker.js',
|
||||
wasmUrl: '/assets/jassub/jassub-worker.wasm',
|
||||
modernWasmUrl: '/assets/jassub/jassub-worker-modern.wasm',
|
||||
availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' },
|
||||
fonts: ['/assets/jassub/default.woff2'],
|
||||
subContent: subtitle,
|
||||
}))
|
||||
const { instance } = window.art.plugins.artplayerPluginJassub
|
||||
const probe = window.bitmapFailureProbe = { instance, held: [], holding: true, errors: [] }
|
||||
instance.addEventListener('error', event => probe.errors.push(String(event.error)))
|
||||
probe.dispatch = instance._worker.onmessage
|
||||
instance._worker.onmessage = (event) => {
|
||||
if (probe.holding && event.data.target === 'render' && event.data.images.some(item => item.image instanceof ImageBitmap))
|
||||
probe.held.push(event)
|
||||
else
|
||||
probe.dispatch(event)
|
||||
}
|
||||
}, subtitle)
|
||||
await expect.poll(() => page.evaluate(() => window.bitmapFailureProbe.held.length)).toBeGreaterThan(0)
|
||||
const evidence = await page.evaluate(async () => {
|
||||
const probe = window.bitmapFailureProbe
|
||||
window.art.video.pause()
|
||||
const event = probe.held.shift()
|
||||
const first = event.data.images.find(item => item.image instanceof ImageBitmap)
|
||||
const copies = await Promise.all([createImageBitmap(first.image), createImageBitmap(first.image)])
|
||||
for (const item of event.data.images) {
|
||||
if (item !== first)
|
||||
item.image?.close?.()
|
||||
}
|
||||
event.data.images = [first, ...copies.map(image => ({ image, x: 0, y: 0 }))]
|
||||
const before = event.data.images.map(item => item.image.width)
|
||||
first.image.close()
|
||||
let failure
|
||||
try {
|
||||
probe.dispatch(event)
|
||||
}
|
||||
catch (error) { failure = { name: error.name, message: error.message } }
|
||||
const after = event.data.images.map(item => item.image.width)
|
||||
// Clean the failing historical candidate too, after recording its resource state.
|
||||
for (const item of event.data.images)
|
||||
item.image.close()
|
||||
for (const held of probe.held.splice(0)) {
|
||||
for (const item of held.data.images)
|
||||
item.image?.close?.()
|
||||
}
|
||||
probe.holding = false
|
||||
return { before, after, failure, asyncRender: event.data.asyncRender }
|
||||
})
|
||||
await testInfo.attach('bitmap-failure', { contentType: 'application/json', body: JSON.stringify({ artifact, sha256: hash(code), evidence, limitation: 'Actual JASSUB Worker/WASM bitmap, plus two native bitmap copies. Deliberately closing the first bitmap injects a native draw error; this does not claim spontaneous Worker corruption or Firefox offscreen recovery.' }) })
|
||||
expect(evidence.before.every(width => width > 0)).toBe(true)
|
||||
expect(evidence.asyncRender).toBe(true)
|
||||
expect(evidence.failure?.name).toBe('InvalidStateError')
|
||||
expect(evidence.after).toEqual([0, 0, 0])
|
||||
// A busy renderer intentionally ignores resize(force); let its pending demand finish first.
|
||||
await expect.poll(() => page.evaluate(() => window.bitmapFailureProbe.instance.busy)).toBe(false)
|
||||
await page.evaluate(() => window.bitmapFailureProbe.instance.resize(0, 0, 0, 0, true))
|
||||
await expect.poll(() => page.evaluate(() => {
|
||||
const { instance } = window.bitmapFailureProbe
|
||||
const pixels = instance._ctx.getImageData(0, 0, instance._canvas.width, instance._canvas.height).data
|
||||
let green = 0
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
if (pixels[i + 1] > 150 && pixels[i] < 100 && pixels[i + 2] < 100 && pixels[i + 3] > 100)
|
||||
green++
|
||||
}
|
||||
return green
|
||||
})).toBeGreaterThan(100)
|
||||
expect(await page.evaluate(() => window.bitmapFailureProbe.errors)).toEqual([])
|
||||
}
|
||||
finally {
|
||||
await page.evaluate(() => {
|
||||
for (const event of window.bitmapFailureProbe?.held || []) {
|
||||
for (const item of event.data.images)
|
||||
item.image?.close?.()
|
||||
}
|
||||
window.art?.destroy(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -144,3 +144,54 @@ for (const method of ['setTrack', 'setTrackByUrl']) {
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
})
|
||||
}
|
||||
|
||||
for (const phase of ['clear', 'first-image', 'second-image', 'resize']) {
|
||||
test(`JASSUB releases every received bitmap when rendering fails during ${phase}`, async () => {
|
||||
const env = environment(false)
|
||||
const { instance } = env.factory({ offscreenRender: false })(env.art)
|
||||
await env.ready()
|
||||
const failure = new Error(`render ${phase}`)
|
||||
const closed = [0, 0, 0]
|
||||
const images = closed.map((_, index) => ({
|
||||
image: { close() { closed[index]++ } },
|
||||
x: index,
|
||||
y: 0,
|
||||
}))
|
||||
let draws = 0
|
||||
instance._ctx.drawImage = () => {
|
||||
draws++
|
||||
if (draws === (phase === 'first-image' ? 1 : phase === 'second-image' ? 2 : 0))
|
||||
throw failure
|
||||
}
|
||||
if (phase === 'clear')
|
||||
instance._ctx.clearRect = () => { throw failure }
|
||||
if (phase === 'resize') {
|
||||
Object.defineProperty(instance._canvasctrl, 'width', {
|
||||
configurable: true,
|
||||
get() { return 300 },
|
||||
set() { throw failure },
|
||||
})
|
||||
}
|
||||
try {
|
||||
assert.throws(() => env.workers[0].onmessage({ data: { target: 'render', images, asyncRender: true, width: 640, height: 360 } }), error => error === failure)
|
||||
assert.deepEqual(closed, [1, 1, 1])
|
||||
assert.equal(draws, phase === 'first-image' ? 1 : phase === 'second-image' ? 2 : 0)
|
||||
}
|
||||
finally {
|
||||
instance.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('JASSUB synchronous pixel buffers are drawn without bitmap disposal', async () => {
|
||||
const env = environment(false)
|
||||
const { instance } = env.factory({ offscreenRender: false })(env.art)
|
||||
await env.ready()
|
||||
const image = new Uint8ClampedArray([0, 255, 0, 255]).buffer
|
||||
image.close = () => assert.fail('Synchronous buffers are not owned ImageBitmaps')
|
||||
const draws = []
|
||||
instance._ctx.drawImage = (...args) => draws.push(args)
|
||||
env.workers[0].onmessage({ data: { target: 'render', images: [{ image, x: 2, y: 3, w: 1, h: 1 }], asyncRender: false, width: 640, height: 360 } })
|
||||
assert.deepEqual(draws, [[instance._bufferCanvas, 2, 3]])
|
||||
instance.destroy()
|
||||
})
|
||||
Reference in new issue
Block a user