mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-13 07:46:17 -08:00
refactor(auto-thumbnail): [PKG-AUTO-THUMB-10] own JPEG encoding cancellation and deadlines
This commit is contained in:
1 parent
afb632d16f
commit
4acf85ea69
21 files changed
+652
-35
No files matched your search
@@ -55,6 +55,13 @@ is in `test/react/`; unique reports and frozen consumer locks are retained under
|
||||
the recorded old wrapper's callback-exception leak. See the example README for
|
||||
the compatibility contract and the capabilities covered by other package tasks.
|
||||
|
||||
`test/auto-thumbnail-encoding.test.js` verifies a separate30-second JPEG deadline,
|
||||
withheld and duplicate callbacks, replacement/destruction, synchronous completion
|
||||
and timer cleanup failures. It runs in `test:auto-thumbnail` and `test:unit`.
|
||||
Native lifecycle tests deliberately hold an actual JPEG callback and invoke the
|
||||
captured deadline; that proves cleanup with native resources, not a spontaneous
|
||||
encoder hang. First-frame pixel and device limitations remain separate gates.
|
||||
|
||||
`yarn test:auto-thumbnail-types` checks the preserved npm 1.1.0 declaration,
|
||||
accurate `/runtime` Promise types, exact rejected consumer lines, and generated
|
||||
editor globals. It also runs in `test:auto-thumbnail` and `test:baseline`.
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify bounded encoding ownership with controlled native callbacks.
|
||||
import test from 'node:test'
|
||||
import { autoThumbnailCandidate, autoThumbnailEnvironment } from './helpers/auto-thumbnail.js'
|
||||
|
||||
const implementation = await autoThumbnailCandidate()
|
||||
async function setup(number = 2) {
|
||||
const env = autoThumbnailEnvironment(implementation)
|
||||
await env.factory({ width: 80, number })(env.art)
|
||||
env.art.emit('video:loadedmetadata')
|
||||
env.metadata()
|
||||
env.seeked()
|
||||
return env
|
||||
}
|
||||
|
||||
function closed(env) {
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.attached.size, 0)
|
||||
assert.equal(env.videos[0].src, undefined)
|
||||
assert.equal(env.canvases[0].width, 0)
|
||||
assert.equal(env.canvases[0].height, 0)
|
||||
}
|
||||
|
||||
test('Auto-thumbnail stalled encoding closes owned resources and ignores a later Blob', async () => {
|
||||
const env = await setup()
|
||||
const timeout = [...env.timers.values()][0]
|
||||
assert(timeout, 'Encoding must have an owned deadline after the frame wait finishes')
|
||||
assert.equal(timeout.delay, 30000)
|
||||
timeout.callback()
|
||||
closed(env)
|
||||
assert.equal(env.warnings.length, 1)
|
||||
assert.match(env.warnings[0][1].message, /encoding timed out/)
|
||||
env.finish()
|
||||
timeout.callback()
|
||||
assert.equal(env.updates.length, 0)
|
||||
assert.equal(env.urls.size, 0)
|
||||
assert.equal(env.warnings.length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('Auto-thumbnail later encoding timeout preserves the last published preview', async () => {
|
||||
const env = await setup()
|
||||
env.finish()
|
||||
const previous = env.art.thumbnails
|
||||
env.seeked()
|
||||
const timeout = [...env.timers.values()][0]
|
||||
assert(timeout, 'The second encode needs a new deadline')
|
||||
timeout.callback()
|
||||
closed(env)
|
||||
assert.equal(env.art.thumbnails, previous)
|
||||
assert.equal(env.urls.has(previous.url), true)
|
||||
env.finish(1)
|
||||
assert.equal(env.updates.length, 1)
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.urls.size, 0)
|
||||
})
|
||||
|
||||
for (const action of ['finish', 'destroy', 'restart']) {
|
||||
test(`Auto-thumbnail ${action} invalidates an encoding deadline already queued for delivery`, async () => {
|
||||
const env = await setup(1)
|
||||
const timeout = [...env.timers.values()][0]
|
||||
assert(timeout, 'Capture a real registered encoding deadline')
|
||||
if (action === 'finish')
|
||||
env.finish()
|
||||
else env.art.emit(action)
|
||||
timeout.callback()
|
||||
env.finish()
|
||||
closed(env)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
assert.equal(env.updates.length, action === 'finish' ? 1 : 0)
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.urls.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('Auto-thumbnail completion clears only its own timeout before the next frame', async () => {
|
||||
const env = await setup()
|
||||
const timeout = [...env.timers.values()][0]
|
||||
assert(timeout)
|
||||
env.finish()
|
||||
timeout.callback()
|
||||
assert.equal(env.timers.size, 1, 'The next seek still owns its frame deadline')
|
||||
env.seeked()
|
||||
assert.equal(env.timers.size, 1, 'One encoding deadline replaces that frame deadline')
|
||||
env.finish(1)
|
||||
closed(env)
|
||||
assert.equal(env.updates.length, 2)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('Auto-thumbnail synchronous Blob delivery cancels its timeout and publishes once', async () => {
|
||||
const env = autoThumbnailEnvironment(implementation)
|
||||
await env.factory({ width: 80, number: 1 })(env.art)
|
||||
env.art.emit('video:loadedmetadata')
|
||||
env.metadata()
|
||||
env.canvases[0].toBlob = (callback) => {
|
||||
callback({ type: 'image/jpeg' })
|
||||
callback({ type: 'image/jpeg' })
|
||||
}
|
||||
env.seeked()
|
||||
closed(env)
|
||||
assert.equal(env.updates.length, 1)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('Auto-thumbnail deadline registration cannot start encoding across synchronous destruction', async () => {
|
||||
const env = autoThumbnailEnvironment(implementation)
|
||||
await env.factory({ width: 80, number: 1 })(env.art)
|
||||
env.art.emit('video:loadedmetadata')
|
||||
env.metadata()
|
||||
const schedule = env.box.setTimeout
|
||||
env.box.setTimeout = (...args) => {
|
||||
env.art.emit('destroy')
|
||||
return schedule(...args)
|
||||
}
|
||||
env.seeked()
|
||||
closed(env)
|
||||
assert.equal(env.blobs.length, 0)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
})
|
||||
|
||||
test('Auto-thumbnail failed encoding deadline cleanup still releases canvas and decoder', async () => {
|
||||
const env = await setup()
|
||||
const clear = env.box.clearTimeout
|
||||
const failure = new Error('Encoding timeout cleanup failed')
|
||||
env.box.clearTimeout = (id) => {
|
||||
clear(id)
|
||||
throw failure
|
||||
}
|
||||
env.finish()
|
||||
closed(env)
|
||||
assert.equal(env.updates.length, 0)
|
||||
assert.equal(env.warnings.length, 1)
|
||||
assert.equal(env.warnings[0][1], failure)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
@@ -71,7 +71,7 @@ for (const order of ['seek-first', 'frame-first']) {
|
||||
env.present()
|
||||
else env.seeked()
|
||||
assert.equal(env.blobs.length, 1)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.timers.size, 1, 'Encoding now owns a deadline after the frame wait ends')
|
||||
seeked()
|
||||
frame()
|
||||
assert.equal(env.blobs.length, 1)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { autoThumbnailCandidate } from '../helpers/auto-thumbnail.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = await autoThumbnailCandidate()
|
||||
for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame-restart', 'alias-complete']) {
|
||||
for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame-restart', 'alias-complete', 'encoding-timeout']) {
|
||||
const useDefault = variant === 'alias-complete'
|
||||
const scenario = useDefault ? 'complete' : variant
|
||||
test(`Auto-thumbnail candidate native ${variant} owns decoder and encoded URLs`, async ({ page }, testInfo) => {
|
||||
@@ -11,11 +11,25 @@ for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame
|
||||
await page.setContent('<!doctype html><div></div>')
|
||||
await page.addScriptTag({ content: implementation.code })
|
||||
await page.evaluate(async ({ scenario, useDefault }) => {
|
||||
const probe = window.probe = { videos: [], canvases: [], updates: [], urls: new Set(), blobs: [], frames: [], frameSupport: false, listeners: new Map(), hold: scenario !== 'complete' }
|
||||
const probe = window.probe = { videos: [], canvases: [], updates: [], urls: new Set(), blobs: [], frames: [], timers: new Map(), warnings: [], frameSupport: false, listeners: new Map(), hold: scenario !== 'complete' }
|
||||
const createElement = document.createElement.bind(document)
|
||||
const createUrl = URL.createObjectURL.bind(URL)
|
||||
const revokeUrl = URL.revokeObjectURL.bind(URL)
|
||||
const toBlob = HTMLCanvasElement.prototype.toBlob
|
||||
const schedule = window.setTimeout
|
||||
const clear = window.clearTimeout
|
||||
const warn = console.warn
|
||||
window.setTimeout = (callback, delay, ...args) => {
|
||||
const id = schedule(callback, delay, ...args)
|
||||
if (delay === 30000)
|
||||
probe.timers.set(id, { callback, delay })
|
||||
return id
|
||||
}
|
||||
window.clearTimeout = (id) => {
|
||||
probe.timers.delete(id)
|
||||
clear(id)
|
||||
}
|
||||
console.warn = (...args) => probe.warnings.push(args.map(String))
|
||||
document.createElement = function (name, ...args) {
|
||||
const element = createElement(name, ...args)
|
||||
if (name === 'canvas')
|
||||
@@ -33,6 +47,7 @@ for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame
|
||||
return element
|
||||
}
|
||||
HTMLCanvasElement.prototype.toBlob = function (callback, type) {
|
||||
probe.encodingDeadline = [...probe.timers.values()][0]
|
||||
return toBlob.call(this, (blob) => {
|
||||
const record = { blob, callback }
|
||||
probe.blobs.push(record)
|
||||
@@ -54,6 +69,9 @@ for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame
|
||||
HTMLCanvasElement.prototype.toBlob = toBlob
|
||||
URL.createObjectURL = createUrl
|
||||
URL.revokeObjectURL = revokeUrl
|
||||
window.setTimeout = schedule
|
||||
window.clearTimeout = clear
|
||||
console.warn = warn
|
||||
}
|
||||
probe.art = {
|
||||
option: { url: '/test/pattern.mp4' },
|
||||
@@ -85,7 +103,12 @@ for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame
|
||||
const probe = window.probe
|
||||
const before = { urls: probe.urls.size, updates: probe.updates.length }
|
||||
if (scenario !== 'complete') {
|
||||
probe.art.emit(scenario.replace('frame-', ''))
|
||||
if (scenario === 'encoding-timeout') {
|
||||
if (!probe.encodingDeadline || probe.encodingDeadline.delay !== 30000)
|
||||
throw new Error('No owned encoding deadline')
|
||||
probe.encodingDeadline.callback()
|
||||
}
|
||||
else probe.art.emit(scenario.replace('frame-', ''))
|
||||
if (probe.blobs[0]) {
|
||||
probe.blobs[0].callback(probe.blobs[0].blob)
|
||||
probe.blobs[0].callback(probe.blobs[0].blob)
|
||||
@@ -111,10 +134,16 @@ for (const variant of ['destroy', 'restart', 'complete', 'frame-destroy', 'frame
|
||||
const final = { urls: probe.urls.size, listeners: [...probe.listeners.values()].reduce((sum, set) => sum + set.size, 0), updates: probe.updates.length }
|
||||
const blobs = probe.blobs.map(({ blob }) => ({ type: blob.type, bytes: blob.size }))
|
||||
probe.restore()
|
||||
return { before, after, video, canvasDimensions, publishedImage, final, blobs, heldFrames: probe.frames.length, frameSupport: probe.frameSupport, result: probe.result }
|
||||
return { before, after, video, canvasDimensions, publishedImage, final, blobs, pendingDeadlines: probe.timers.size, warnings: probe.warnings, heldFrames: probe.frames.length, frameSupport: probe.frameSupport, result: probe.result }
|
||||
}, scenario)
|
||||
await testInfo.attach('auto-thumbnail-candidate-native-lifecycle', { contentType: 'application/json', body: JSON.stringify({ scenario, useDefault, sha256: hash(implementation.code), scope: 'Actual HTTP decoding/seek/JPEG and held native presentation callback lifecycle with a stub player host. Frame scenarios use a held Blob fallback only when native frame callbacks are absent. Pixel acceptance is separate; AUTO-THUMB-PIXEL-01 remains open.', ...state }) })
|
||||
expect(state.result).toEqual({ name: 'artplayerPluginAutoThumbnail' })
|
||||
expect(state.pendingDeadlines).toBe(0)
|
||||
if (scenario === 'encoding-timeout') {
|
||||
expect(state.warnings).toHaveLength(1)
|
||||
expect(state.warnings[0][1]).toContain('encoding timed out')
|
||||
}
|
||||
else expect(state.warnings).toEqual([])
|
||||
expect(state.before).toEqual(scenario === 'complete' ? { urls: 1, updates: 2 } : { urls: 0, updates: 0 })
|
||||
expect(state.after).toEqual(state.before)
|
||||
expect(state.video).toEqual({ connected: false, src: null, readyState: 0, paused: true, handlers: [true, true, true, true] })
|
||||
|
||||
Reference in new issue
Block a user