fix(core): [CORE-SUBTITLE-OFFSET-01] refresh paused native cue membership and order

This commit is contained in:
Harvey Zhao committed 2026-09-14 12:49:22 +08:00
1 parent b5dee252d4
commit efed847cdf
23 files changed
+6789 -27

No files matched your search

@@ -38,7 +38,7 @@ async function seek(page, time) {
}
for (const core of cores.filter(core => !['published-5.1.2', 'published-5.1.7'].includes(core))) {
test(`Multiple subtitles candidate / ${core}: timed VTT and SRT, offsets, fullscreen and source change`, async ({ page, browser }, testInfo) => {
test(`Multiple subtitles candidate / ${core}: timed VTT and SRT, offsets, fullscreen and source change`, async ({ page, browser, browserName }, testInfo) => {
await prepare(page, core, candidate, browser, testInfo)
await page.evaluate(async () => {
await window.art.plugins.add(window.multipleFactory({ subtitles: [{ name: 'a', url: '/test/combination.vtt' }, { name: 'b', url: '/test/combination.srt' }] }))
@@ -54,14 +54,17 @@ for (const core of cores.filter(core => !['published-5.1.2', 'published-5.1.7'].
window.art.subtitleOffset = 1
})
expect(await page.evaluate(() => Array.from(window.art.template.$track.track.cues, cue => [cue.startTime, cue.endTime]))).toEqual([[2, 4], [2, 4], [5, 7]])
await expect(page.locator('.art-subtitle-a')).toHaveCount(0)
const staleHistoricalOffset = core !== 'candidate' && browserName === 'firefox'
await expect(page.locator('.art-subtitle-a')).toHaveCount(staleHistoricalOffset ? 1 : 0)
await seek(page, 2.5)
await expect(page.locator('.art-subtitle-a')).toHaveText('First')
await page.evaluate(() => {
window.art.subtitleOffset = -1
})
expect(await page.evaluate(() => Array.from(window.art.template.$track.track.cues, cue => [cue.startTime, cue.endTime]))).toEqual([[0, 2], [0, 2], [3, 5]])
await expect(page.locator('.art-subtitle-a')).toHaveCount(0)
await expect(page.locator('.art-subtitle-a')).toHaveCount(staleHistoricalOffset ? 1 : 0)
if (staleHistoricalOffset)
await testInfo.attach('historical-paused-offset-defect', { contentType: 'application/json', body: JSON.stringify({ core, browser: browser.version(), acceptedAsCorrect: false, observation: 'Published core keeps a stale active caption after paused offsets; candidate must clear it. Dedicated native/offset probes retain exact state.' }) })
await page.evaluate(() => {
window.art.subtitleOffset = 0
})
@@ -0,0 +1,58 @@
import { expect, test } from './fixtures.js'
for (const invalidation of ['time-only', 'remove-add', 'mode-toggle']) {
test(`Native paused cue timing probe: ${invalidation}`, async ({ page, browser }, testInfo) => {
await page.route('**/test/native-offset.vtt', route => route.fulfill({ contentType: 'text/vtt', body: 'WEBVTT\n\n00:01.000 --> 00:03.000\ncaption\n' }))
await page.goto('/test/player.html')
await page.evaluate(async () => {
const video = document.createElement('video')
video.muted = true
video.src = '/test/pattern.mp4'
document.body.append(video)
window.probeVideo = video
await new Promise(resolve => video.addEventListener('loadedmetadata', resolve, { once: true }))
const element = document.createElement('track')
element.kind = 'metadata'
element.src = '/test/native-offset.vtt'
const loaded = new Promise(resolve => element.addEventListener('load', resolve, { once: true }))
video.append(element)
const track = element.track
track.mode = 'hidden'
await loaded
window.probeTrack = track
window.probeCue = track.cues[0]
await video.play()
video.pause()
const sought = new Promise(resolve => video.addEventListener('seeked', resolve, { once: true }))
video.currentTime = 1.5
await sought
})
await expect.poll(() => page.evaluate(() => window.probeTrack.activeCues.length)).toBe(1)
const state = await page.evaluate(async (invalidation) => {
const track = window.probeTrack
const cue = window.probeCue
const read = () => ({ time: window.probeVideo.currentTime, paused: window.probeVideo.paused, active: Array.from(track.activeCues || [], cue => [cue.startTime, cue.endTime]), sameCue: track.cues[0] === cue })
const before = read()
cue.startTime = 2
cue.endTime = 4
const changed = read()
if (invalidation === 'remove-add') {
track.removeCue(cue)
track.addCue(cue)
}
if (invalidation === 'mode-toggle') {
track.mode = 'disabled'
track.mode = 'hidden'
}
const immediate = read()
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
const painted = read()
window.probeVideo.remove()
return { before, changed, immediate, painted }
}, invalidation)
await testInfo.attach('native-offset-probe', { contentType: 'application/json', body: JSON.stringify({ invalidation, browser: browser.version(), state, scope: 'Native video/TextTrack only, no Artplayer instance; two paint callbacks observe invalidation without moving playback time' }) })
expect(state.before.time).toBeGreaterThanOrEqual(1)
expect(state.before.time).toBeLessThan(2)
expect(state.before).toMatchObject({ paused: true, active: [[1, 3]], sameCue: true })
})
}
+65
View File
@@ -0,0 +1,65 @@
import { expect, test } from './fixtures.js'
const vtt = 'WEBVTT\n\n00:01.000 --> 00:03.000\nFirst\n\n00:01.000 --> 00:03.000\nOverlap\n\n00:04.000 --> 00:06.000\nLater\n'
for (const core of ['candidate', 'published']) {
test(`${core}: ${core === 'candidate' ? 'paused subtitle offsets refresh native membership without moving media' : 'historical paused subtitle offset behavior'}`, async ({ page, browserName }, testInfo) => {
await page.route('**/test/offset.vtt', route => route.fulfill({ contentType: 'text/vtt', body: vtt }))
await page.goto(`/test/player.html?core=${core}`)
await page.evaluate(() => window.createPlayer('/test/pattern.mp4'))
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.evaluate(() => window.art.subtitle.switch('/test/offset.vtt'))
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(3)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.locator('#pause').click()
await page.evaluate(() => new Promise((resolve) => {
window.art.once('video:seeked', resolve)
window.art.seek = 1.5
}))
await expect(page.locator('.art-subtitle-line')).toHaveCount(2)
const records = await page.evaluate(() => {
const art = window.art
const track = art.template.$track.track
const cues = Array.from(track.cues)
const events = []
art.on('subtitleOffset', value => events.push(value))
const states = []
const time = art.currentTime
for (const offset of [1, -1, 0, -3, 0]) {
art.subtitleOffset = offset
states.push({ offset, time: art.currentTime, paused: art.video.paused, native: Array.from(track.activeCues, cue => cue.text), public: art.subtitle.activeCues.map(cue => cue.text), rendered: Array.from(art.template.$subtitle.querySelectorAll('.art-subtitle-line'), line => line.textContent.trim()), sameCues: cues.every(cue => Array.from(track.cues).includes(cue)), mode: track.mode })
}
return { states, events, time }
})
await testInfo.attach('paused-offset', { contentType: 'application/json', body: JSON.stringify({ core, records }) })
expect(records.events).toEqual([1, -1, 0, -3, 0])
const texts = core === 'published' && browserName === 'firefox'
? [['First', 'Overlap'], ['First', 'Overlap'], ['First', 'Overlap'], ['First', 'Overlap', 'Later'], ['First', 'Overlap', 'Later']]
: [[], ['First', 'Overlap'], ['First', 'Overlap'], ['Later'], ['First', 'Overlap']]
expect(records.time).toBeCloseTo(1.5, 2)
for (const [index, state] of records.states.entries()) {
expect(state).toEqual({ offset: [1, -1, 0, -3, 0][index], time: records.time, paused: true, native: texts[index], public: texts[index], rendered: texts[index], sameCues: true, mode: 'hidden' })
}
if (core === 'candidate') {
const disabled = await page.evaluate(() => {
const track = window.art.template.$track.track
track.mode = 'disabled'
window.art.subtitleOffset = 2
return { mode: track.mode, active: window.art.subtitle.activeCues.length, offset: window.art.subtitleOffset }
})
expect(disabled).toEqual({ mode: 'disabled', active: 0, offset: 0 })
await page.evaluate(() => {
window.art.template.$track.track.mode = 'hidden'
})
await page.locator('#play').click()
await page.evaluate(() => {
window.art.subtitleOffset = 1
})
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(2.25)
await expect(page.locator('.art-subtitle-line')).toHaveCount(2)
await page.locator('#pause').click()
}
await page.evaluate(() => window.art.destroy())
})
}