mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-21 21:01:20 -08:00
fix(multiple-subtitles): [PKG-MULTI-SUB-07] preserve inline timestamps and caption text
This commit is contained in:
1 parent
da64aae267
commit
7f226ff822
24 files changed
+8660
-132
No files matched your search
@@ -166,4 +166,49 @@ for (const core of ['published', 'candidate']) {
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await evidence(page, testInfo, core, { requests, nativeSourceSwitch: true })
|
||||
})
|
||||
|
||||
test(`Multiple subtitles candidate ${core} core: inline timestamps remain native instructions without visible time tags`, async ({ page }, testInfo) => {
|
||||
const timed = 'WEBVTT\n\n00:00.000 --> 00:08.000\n<00:01.250>Before <00:02.000> after<00:03.500>\n'
|
||||
await page.route('**/test/multiple-timed.vtt', route => route.fulfill({ body: timed, contentType: 'text/vtt' }))
|
||||
await prepare(page, core)
|
||||
await page.addStyleTag({ content: '.art-subtitle-timed { color: rgb(255, 0, 0); }' })
|
||||
await page.evaluate(async () => {
|
||||
await window.art.plugins.add(window.artplayerPluginMultipleSubtitles({ subtitles: [{ url: '/test/multiple-timed.vtt', name: 'timed' }] }))
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.subtitle.cues.length)).toBe(1)
|
||||
const native = await page.evaluate(() => {
|
||||
const cue = window.art.subtitle.cues[0]
|
||||
const fragment = cue.getCueAsHTML()
|
||||
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_PROCESSING_INSTRUCTION)
|
||||
const timestamps = []
|
||||
while (walker.nextNode()) timestamps.push(walker.currentNode.nodeValue)
|
||||
return { text: cue.text, timestamps }
|
||||
})
|
||||
expect(native.text).toContain('<00:02.000>')
|
||||
expect(native.text).not.toContain('NaN')
|
||||
expect(native.timestamps).toHaveLength(3)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
await expect(page.locator('.art-subtitle-timed').first()).toHaveCSS('color', 'rgb(255, 0, 0)')
|
||||
const timedLine = page.locator('.art-subtitle-line').filter({ has: page.locator('.art-subtitle-timed') })
|
||||
const visible = await timedLine.textContent()
|
||||
expect(visible).toContain('Before')
|
||||
expect(visible).toContain('after')
|
||||
expect(visible).not.toMatch(/<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/)
|
||||
// Public cue edits exercise literal display without relying on the vendor's entity decoder.
|
||||
await page.evaluate(() => {
|
||||
window.art.subtitle.cues[0].text += '<div class="art-subtitle-literal">Literal <00:02.000> example</div>'
|
||||
window.art.subtitle.update()
|
||||
})
|
||||
await expect(page.locator('.art-subtitle-literal')).toHaveText('Literal <00:02.000> example')
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.tracks([]))
|
||||
await expect(page.locator('.art-subtitle-timed')).toHaveCount(0)
|
||||
await page.evaluate(() => window.art.plugins.multipleSubtitles.reset())
|
||||
await expect(timedLine).toContainText('Before')
|
||||
await expect(timedLine).not.toContainText(/<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/)
|
||||
await expect(page.locator('.art-subtitle-literal')).toHaveCount(0)
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await evidence(page, testInfo, core, { native, visible })
|
||||
})
|
||||
}
|
||||
@@ -263,4 +263,10 @@ for (const implementation of implementations) {
|
||||
await env.factory(options(subtitle()))(env.art)
|
||||
assert((await env.latestText()).includes('<NaN:NaN.NaN>'))
|
||||
})
|
||||
|
||||
test(`Multiple-subtitles ${implementation.name} failure baseline: entity decoding retains stray semicolons`, async () => {
|
||||
const env = create({ responses: { 'a.vtt': subtitleVtt('Literal <00:02.000> example &') } })
|
||||
await env.factory(options(subtitle()))(env.art)
|
||||
assert(env.utils.unescape(await env.latestText()).includes('Literal <;00:02.000>; example &;'))
|
||||
})
|
||||
}
|
||||
@@ -21,7 +21,7 @@ async function flush() {
|
||||
}
|
||||
function empty(env) {
|
||||
assert.equal(env.liveBlobs.size, 0)
|
||||
assert.equal(env.listeners.get('destroy')?.size || 0, 0)
|
||||
for (const listeners of env.listeners.values()) assert.equal(listeners.size, 0)
|
||||
}
|
||||
|
||||
for (const abortController of [true, false]) {
|
||||
|
||||
@@ -74,3 +74,48 @@ test('Multiple subtitles request: real core SRT/ASS conversion and encoded text
|
||||
for (const value of ['SRT text', 'ASS text', 'café', '日本語']) assert(text.includes(value))
|
||||
current.emit('destroy')
|
||||
})
|
||||
|
||||
for (const [text, timestamps] of [
|
||||
['Before <00:02.000> after', ['<00:02.000>']],
|
||||
['<00:01.250> one <00:03.500> two', ['<00:01.250>', '<00:03.500>']],
|
||||
['end <00:07.000>', ['<00:07.000>']],
|
||||
['甲<00:02.000>乙', ['<00:02.000>']],
|
||||
['<b>bold <00:02.000> after</b>', ['<00:02.000>']],
|
||||
]) {
|
||||
test(`Multiple subtitles timestamps: valid nodes survive selection and reset (${text})`, async () => {
|
||||
const input = `WEBVTT\n\n00:00.000 --> 00:08.000\n${text}\n`
|
||||
const env = multipleSubtitlesEnvironment(candidate, { responses: { 'a.vtt': input, 'b.vtt': subtitleVtt('Other') } })
|
||||
const result = await env.factory({ subtitles: [{ url: 'a.vtt', name: 'a' }, { url: 'b.vtt', name: 'b' }] })(env.art)
|
||||
result.tracks(['a'])
|
||||
const selected = await env.latestText()
|
||||
for (const timestamp of timestamps) assert(selected.includes(timestamp))
|
||||
assert(!selected.includes('NaN'))
|
||||
const reparsed = parseTracks([selected], [{ name: 'reparsed' }])
|
||||
assert.deepEqual(reparsed[0].errors, [])
|
||||
const numbers = []
|
||||
function collect(nodes) {
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'timestamp')
|
||||
numbers.push(node.value)
|
||||
if (node.children)
|
||||
collect(node.children)
|
||||
}
|
||||
}
|
||||
collect(reparsed[0].cues[0].tree.children)
|
||||
assert.equal(numbers.length, timestamps.length)
|
||||
result.tracks(['b'])
|
||||
assert(!(await env.latestText()).includes(timestamps[0]))
|
||||
result.reset()
|
||||
for (const timestamp of timestamps) assert((await env.latestText()).includes(timestamp))
|
||||
env.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('Multiple subtitles timestamps: frozen numeric nodes stay unchanged across duplicate selection', () => {
|
||||
const trees = freeze(parseTracks(['WEBVTT\n\n00:00.000 --> 00:08.000\nBefore <00:02.000> after\n'], [{ name: 'a' }]))
|
||||
const original = JSON.stringify(trees)
|
||||
const output = serializeTracks([trees[0], trees[0]])
|
||||
assert.equal((output.match(/<00:02\.000>/g) || []).length, 2)
|
||||
assert.equal((output.match(/<c\.artplayer-multiple-subtitles-timestamp>/g) || []).length, 2)
|
||||
assert.equal(JSON.stringify(trees), original)
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { CueNode, TimestampNode } from '../../packages/artplayer-plugin-multiple-subtitles/src/parser'
|
||||
import type { CueNode, TimestampNode, WrappedNode } from '../../packages/artplayer-plugin-multiple-subtitles/src/parser'
|
||||
import type { Option, Result } from '../../packages/artplayer-plugin-multiple-subtitles/src/types'
|
||||
import factory from '../../packages/artplayer-plugin-multiple-subtitles/src/index'
|
||||
import createLifetime from '../../packages/artplayer-plugin-multiple-subtitles/src/lifetime'
|
||||
@@ -52,6 +52,9 @@ void timestamp
|
||||
// @ts-expect-error Parsed timestamps are numbers, not wrapped serialized values.
|
||||
const wrongTimestamp: TimestampNode = { type: 'timestamp', value: '2' }
|
||||
void wrongTimestamp
|
||||
// @ts-expect-error Serialized timestamp nodes must also keep numeric values.
|
||||
const wrongWrapped: WrappedNode = { type: 'timestamp', value: '<div>2</div>' }
|
||||
void wrongWrapped
|
||||
const trees = parseTracks(['WEBVTT\n\n'], [{}])
|
||||
const vtt: string = serializeTracks(trees)
|
||||
void vtt
|
||||
|
||||
Reference in new issue
Block a user