mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
fix(auto-thumbnail): [PKG-AUTO-THUMB-12] release stalled metadata loads
This commit is contained in:
1 parent
d3aa4bbfe4
commit
42ddead0cd
20 files changed
+6048
-20
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify metadata wait ownership and cancellation.
|
||||
import test from 'node:test'
|
||||
import { autoThumbnailCandidate, autoThumbnailEnvironment } from './helpers/auto-thumbnail.js'
|
||||
|
||||
const implementation = await autoThumbnailCandidate()
|
||||
async function setup() {
|
||||
const env = autoThumbnailEnvironment(implementation)
|
||||
const result = await env.factory({ width: 80, number: 1 })(env.art)
|
||||
assert.equal(result.name, 'artplayerPluginAutoThumbnail')
|
||||
env.art.emit('video:loadedmetadata')
|
||||
return env
|
||||
}
|
||||
|
||||
function released(env, video = env.videos[0]) {
|
||||
assert.equal(env.attached.has(video), false)
|
||||
assert.equal(video.src, undefined)
|
||||
assert.equal(video.onloadedmetadata, null)
|
||||
assert.equal(video.onerror, null)
|
||||
}
|
||||
|
||||
test('Auto-thumbnail metadata timeout releases a stalled decoder and ignores late metadata', async () => {
|
||||
const env = await setup()
|
||||
const metadata = env.videos[0].onloadedmetadata
|
||||
const deadline = [...env.timers.values()][0]
|
||||
assert(deadline, 'Metadata loading must have an owned deadline')
|
||||
assert.equal(deadline.delay, 30000)
|
||||
deadline.callback()
|
||||
released(env)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.warnings.length, 1)
|
||||
assert.match(env.warnings[0][1].message, /metadata timed out/)
|
||||
metadata()
|
||||
deadline.callback()
|
||||
assert.equal(env.canvases.length, 0)
|
||||
assert.equal(env.updates.length, 0)
|
||||
assert.equal(env.warnings.length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const scenario of ['destroy', 'restart', 'error', 'metadata']) {
|
||||
test(`Auto-thumbnail metadata deadline becomes inert after ${scenario}`, async () => {
|
||||
const env = await setup()
|
||||
const deadline = [...env.timers.values()][0]
|
||||
assert(deadline, 'Metadata loading must have an owned deadline')
|
||||
if (scenario === 'metadata') {
|
||||
env.metadata()
|
||||
const frameDeadline = [...env.timers.values()][0]
|
||||
assert.notEqual(frameDeadline, deadline)
|
||||
deadline.callback()
|
||||
assert.equal(env.timers.size, 1, 'A queued metadata timeout cannot cancel the new frame wait')
|
||||
env.seeked()
|
||||
env.finish()
|
||||
assert.equal(env.updates.length, 1)
|
||||
}
|
||||
else if (scenario === 'error') {
|
||||
env.videos[0].error = new Error('Media failed')
|
||||
env.videos[0].onerror()
|
||||
}
|
||||
else {
|
||||
env.art.emit(scenario)
|
||||
}
|
||||
deadline.callback()
|
||||
released(env)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.warnings.length, scenario === 'error' ? 1 : 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('Auto-thumbnail stalled replacement retains the previous usable sheet', async () => {
|
||||
const env = await setup()
|
||||
env.metadata()
|
||||
env.seeked()
|
||||
env.finish()
|
||||
const sheet = env.art.thumbnails
|
||||
env.art.emit('restart')
|
||||
env.art.emit('video:loadedmetadata')
|
||||
const deadline = [...env.timers.values()][0]
|
||||
assert(deadline, 'Replacement metadata loading must have a deadline')
|
||||
deadline.callback()
|
||||
released(env, env.videos[1])
|
||||
assert.equal(env.art.thumbnails, sheet)
|
||||
assert.equal(env.urls.has(sheet.url), true)
|
||||
assert.equal(env.updates.length, 1)
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.urls.size, 0)
|
||||
})
|
||||
|
||||
test('Auto-thumbnail metadata timer registration cannot start loading after destruction', async () => {
|
||||
const env = autoThumbnailEnvironment(implementation)
|
||||
await env.factory({ width: 80, number: 1 })(env.art)
|
||||
const schedule = env.box.setTimeout
|
||||
env.box.setTimeout = (...args) => {
|
||||
env.art.emit('destroy')
|
||||
return schedule(...args)
|
||||
}
|
||||
env.art.emit('video:loadedmetadata')
|
||||
released(env)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.canvases.length, 0)
|
||||
})
|
||||
@@ -3,6 +3,73 @@ import { autoThumbnailCandidate } from '../helpers/auto-thumbnail.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = await autoThumbnailCandidate()
|
||||
for (const scenario of ['timeout', 'destroy', 'restart']) {
|
||||
test(`Auto-thumbnail candidate native pending metadata releases on ${scenario}`, async ({ page, request }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=published')
|
||||
await page.setContent('<!doctype html><div></div>')
|
||||
const caseId = `${testInfo.testId}-${testInfo.retry}`
|
||||
await page.addScriptTag({ content: implementation.code })
|
||||
await page.evaluate(async (caseId) => {
|
||||
const schedule = window.setTimeout
|
||||
const clear = window.clearTimeout
|
||||
const warn = console.warn
|
||||
const probe = window.metadataProbe = { timers: new Map(), warnings: [], updates: [], listeners: new Map() }
|
||||
window.setTimeout = (callback, delay, ...args) => {
|
||||
const id = schedule(callback, delay, ...args)
|
||||
if (delay === 30000)
|
||||
probe.timers.set(id, callback)
|
||||
return id
|
||||
}
|
||||
window.clearTimeout = (id) => {
|
||||
probe.timers.delete(id)
|
||||
clear(id)
|
||||
}
|
||||
console.warn = (...args) => probe.warnings.push(args.map(String))
|
||||
probe.restore = () => {
|
||||
window.setTimeout = schedule
|
||||
window.clearTimeout = clear
|
||||
console.warn = warn
|
||||
}
|
||||
probe.art = {
|
||||
option: { url: `/test/pending-thumbnail-metadata.mp4?case=${encodeURIComponent(caseId)}` },
|
||||
on(name, callback) { probe.listeners.set(name, callback) },
|
||||
off(name) { probe.listeners.delete(name) },
|
||||
get thumbnails() { return probe.updates.at(-1) },
|
||||
set thumbnails(value) { probe.updates.push(value) },
|
||||
}
|
||||
probe.result = await window.artplayerPluginAutoThumbnail({ width: 80, number: 1 })(probe.art)
|
||||
probe.listeners.get('video:loadedmetadata')()
|
||||
probe.video = document.querySelector('video')
|
||||
}, caseId)
|
||||
const requestLog = `/test/requests.json?case=${encodeURIComponent(caseId)}`
|
||||
await expect.poll(async () => (await (await request.get(requestLog)).json()).length).toBeGreaterThan(0)
|
||||
const mediaRequests = await (await request.get(requestLog)).json()
|
||||
const evidence = await page.evaluate((scenario) => {
|
||||
const probe = window.metadataProbe
|
||||
const video = probe.video
|
||||
const before = { connected: video.isConnected, readyState: video.readyState, source: video.getAttribute('src'), timers: probe.timers.size }
|
||||
const deadline = [...probe.timers.values()][0]
|
||||
if (scenario === 'timeout')
|
||||
deadline?.()
|
||||
else probe.listeners.get(scenario)()
|
||||
const after = { connected: video.isConnected, readyState: video.readyState, source: video.getAttribute('src'), metadataHandler: video.onloadedmetadata, errorHandler: video.onerror, timers: probe.timers.size, canvases: document.querySelectorAll('canvas').length }
|
||||
deadline?.()
|
||||
probe.listeners.get('destroy')?.()
|
||||
probe.restore()
|
||||
return { before, after, result: probe.result, warnings: probe.warnings, updates: probe.updates.length }
|
||||
}, scenario)
|
||||
await testInfo.attach('auto-thumbnail-metadata-deadline', { contentType: 'application/json', body: JSON.stringify({ provenance: implementation.provenance, sha256: hash(implementation.code), scenario, mediaRequests, virtualDeadline: true, ...evidence }) })
|
||||
expect(evidence.result).toEqual({ name: 'artplayerPluginAutoThumbnail' })
|
||||
expect(evidence.before).toMatchObject({ connected: true, readyState: 0, timers: 1 })
|
||||
expect(evidence.before.source).toContain('pending-thumbnail-metadata.mp4')
|
||||
expect(evidence.after).toEqual({ connected: false, readyState: 0, source: null, metadataHandler: null, errorHandler: null, timers: 0, canvases: 0 })
|
||||
expect(evidence.updates).toBe(0)
|
||||
expect(evidence.warnings).toHaveLength(scenario === 'timeout' ? 1 : 0)
|
||||
if (scenario === 'timeout')
|
||||
expect(evidence.warnings[0].join(' ')).toContain('metadata timed out')
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -181,6 +181,12 @@ const server = http.createServer((req, res) => {
|
||||
res.writeHead(503, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store' }).end('Intentional media failure')
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/test/pending-thumbnail-metadata.mp4') {
|
||||
// Native media backends may bypass browser request interception. Keep this
|
||||
// real HTTP response pending, with a server-side bound if a test fails.
|
||||
res.setTimeout(10000, () => res.destroy())
|
||||
return
|
||||
}
|
||||
if (files.has(url.pathname)) {
|
||||
if (url.pathname === '/test/vast-skippable.xml' && ['http://imasdk.googleapis.com', 'https://imasdk.googleapis.com'].includes(req.headers.origin)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
|
||||
|
||||
Reference in new issue
Block a user