mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-19 03:11:20 -08:00
fix(canvas): [PKG-CANVAS-SUBTITLE-01] attach native subtitle tracks
This commit is contained in:
1 parent
d89728c0a6
commit
c3967bcbf4
23 files changed
+1459
-11
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# Browser regression entry
|
||||
|
||||
`canvas-subtitles.spec.js` covers native track bootstrap/load/seek/replacement and
|
||||
cleanup on old/new cores, including ordinary Canvas child operations. It is part
|
||||
of both browser scopes. `ARTPLAYER_CANVAS_SUBTITLE_BASELINE=1.1.0` runs the exact
|
||||
published proxy as a failing historical control; it cannot be combined with an
|
||||
installed candidate map. This is a real media/track test, not a simulated cue list.
|
||||
|
||||
Chapter pointer geometry is shared by `chapter-hover.ts` and the combination
|
||||
suite. It awaits locator actionability with `hover({ trial: true })` before
|
||||
reading coordinates; `art.fullscreen` can become true while controls still move.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { canvasCandidate, canvasHistorical } from '../helpers/canvas.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const baseline = process.env.ARTPLAYER_CANVAS_SUBTITLE_BASELINE
|
||||
assert(!baseline || baseline === '1.1.0', 'Only the frozen published 1.1.0 subtitle baseline is supported')
|
||||
assert(!baseline || !process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Historical subtitles cannot claim installed candidate provenance')
|
||||
const candidate = baseline ? (await canvasHistorical()).find(item => item.name === 'published-1.1.0') : await canvasCandidate()
|
||||
assert(candidate)
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: canvas proxy loads and seeks native subtitle cues`, async ({ page }, testInfo) => {
|
||||
await page.route('**/canvas-captions.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nOpening caption\n\n00:00:02.000 --> 00:00:07.000\nLater caption\n' }))
|
||||
await page.route('**/canvas-replacement.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:00:00.000 --> 00:00:07.000\nReplacement caption\n' }))
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${candidate.source}; window.canvasFactory = module.exports.default || module.exports; })();` })
|
||||
await page.evaluate(() => {
|
||||
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, proxy: window.canvasFactory(), subtitle: { url: '/canvas-captions.vtt' } })
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
document.querySelector('#pause').onclick = () => window.art.pause()
|
||||
})
|
||||
let completed = false
|
||||
try {
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.click('#pause')
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(2)
|
||||
await expect(page.locator('.art-subtitle')).toHaveText('Opening caption')
|
||||
await page.evaluate(() => window.art.currentTime = 3)
|
||||
await expect(page.locator('.art-subtitle')).toHaveText('Later caption')
|
||||
await page.evaluate(async () => {
|
||||
window.previousCanvasTrack = window.art.template.$track
|
||||
await window.art.subtitle.switch('/canvas-replacement.vtt')
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
||||
await page.evaluate(() => window.art.currentTime = 4)
|
||||
await expect(page.locator('.art-subtitle')).toHaveText('Replacement caption')
|
||||
expect(await page.evaluate(() => ({ previousConnected: window.previousCanvasTrack.isConnected, count: window.art.video.textTracks.length, parent: window.art.template.$track.parentNode === window.art.template.$player.querySelector('video') }))).toEqual({ previousConnected: false, count: 1, parent: true })
|
||||
expect(await page.evaluate(() => {
|
||||
const canvas = window.art.video
|
||||
const marker = document.createElement('span')
|
||||
const append = canvas.appendChild
|
||||
const returned = append(marker)
|
||||
const same = returned === marker && marker.parentNode === canvas
|
||||
canvas.removeChild(marker)
|
||||
window.canvasAppend = append
|
||||
window.backingCanvasVideo = window.art.template.$player.querySelector('video')
|
||||
return same && !marker.parentNode && typeof canvas.toDataURL() === 'string'
|
||||
})).toBe(true)
|
||||
completed = true
|
||||
}
|
||||
finally {
|
||||
await testInfo.attach('canvas-subtitle-state', { contentType: 'application/json', body: JSON.stringify({ core, implementation: candidate.name, sha256: hash(candidate.source), ...await page.evaluate(() => ({ renderer: window.art.video.nodeName, trackParent: window.art.template.$track.parentNode?.nodeName, trackState: window.art.template.$track.readyState, cues: window.art.subtitle.cues.length, backingTracks: window.art.template.$player.querySelector('video')?.textTracks.length, text: window.art.template.$subtitle.textContent, time: window.art.currentTime })) }) })
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
if (completed) {
|
||||
expect(await page.evaluate(() => {
|
||||
const track = document.createElement('track')
|
||||
window.canvasAppend?.(track)
|
||||
const video = window.backingCanvasVideo
|
||||
return video ? { connected: video.isConnected, paused: video.paused, source: video.getAttribute('src'), restarted: track.parentNode === video } : null
|
||||
})).toEqual({ connected: false, paused: true, source: null, restarted: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -5,6 +5,22 @@ import { canvasCandidate, canvasEnvironment, canvasHistorical } from './helpers/
|
||||
|
||||
const implementation = await canvasCandidate()
|
||||
const listenerCount = env => [...env.listeners.values()].reduce((count, set) => count + set.size, 0)
|
||||
|
||||
test('Canvas rolls back private media when initial subtitle track insertion fails', async () => {
|
||||
const env = await canvasEnvironment(implementation)
|
||||
const failure = new Error('Initial track insertion failed')
|
||||
env.video.appendChild = () => {
|
||||
throw failure
|
||||
}
|
||||
assert.throws(() => env.factory()(env.art), error => error === failure)
|
||||
assert.equal(listenerCount(env), 0)
|
||||
assert.equal(env.timers.size, 0)
|
||||
assert.equal(env.frames.size, 0)
|
||||
assert.equal(env.video.parentNode, undefined)
|
||||
assert.equal(env.video.src, undefined)
|
||||
assert.equal(env.video.paused, true)
|
||||
assert.deepEqual([env.canvas.width, env.canvas.height], [0, 0])
|
||||
})
|
||||
function deferred() {
|
||||
let resolve, reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
|
||||
+23
-1
@@ -75,6 +75,24 @@ export async function canvasEnvironment(implementation, settings = {}) {
|
||||
paused: true,
|
||||
muted: false,
|
||||
style: {},
|
||||
children: [],
|
||||
textTracks: [],
|
||||
appendChild(node) {
|
||||
node.parentNode?.removeChild(node)
|
||||
this.children.push(node)
|
||||
node.parentNode = this
|
||||
if (node.nodeName === 'TRACK')
|
||||
this.textTracks.push(node.track)
|
||||
return node
|
||||
},
|
||||
removeChild(node) {
|
||||
this.children.splice(this.children.indexOf(node), 1)
|
||||
const index = this.textTracks.indexOf(node.track)
|
||||
if (index >= 0)
|
||||
this.textTracks.splice(index, 1)
|
||||
node.parentNode = null
|
||||
return node
|
||||
},
|
||||
play(...args) {
|
||||
calls.push({ name: 'play', receiver: this, args })
|
||||
this.paused = false
|
||||
@@ -111,7 +129,11 @@ export async function canvasEnvironment(implementation, settings = {}) {
|
||||
},
|
||||
}
|
||||
const art = {
|
||||
constructor: { config: { events: ['loadedmetadata', 'play', 'pause', 'seeked', 'error'] }, utils: { createElement: tag => tag === 'canvas' ? canvas : video } },
|
||||
constructor: { config: { events: ['loadedmetadata', 'play', 'pause', 'seeked', 'error'] }, utils: { createElement: (tag) => {
|
||||
if (tag === 'track')
|
||||
return { nodeName: 'TRACK', nodeType: 1, namespaceURI: 'http://www.w3.org/1999/xhtml', track: { mode: 'disabled', cues: [], activeCues: [] }, remove() { this.parentNode?.removeChild(this) } }
|
||||
return tag === 'canvas' ? canvas : video
|
||||
} } },
|
||||
option: { autoSize: false },
|
||||
template: { $player: player },
|
||||
isDestroy: false,
|
||||
|
||||
Reference in new issue
Block a user