fix(multiple-subtitles): [PKG-MULTI-SUB-07] preserve inline timestamps and caption text

This commit is contained in:
Harvey Zhao committed 2026-09-13 11:50:50 +08:00
1 parent da64aae267
commit 7f226ff822
24 files changed
+8660 -132

No files matched your search

@@ -0,0 +1,21 @@
import type Artplayer from 'artplayer'
import type { Lifetime } from './types'
import { TIMESTAMP_CLASS } from './merge'
export default function installCaptionView(art: Pick<Artplayer, 'on' | 'off' | 'template'>, lifetime: Lifetime): void {
function update(): void {
if (lifetime.closed)
return
// HTML does not pair <c.class> with </c>; unwrap while preserving following caption nodes.
for (const marker of Array.from(art.template.$subtitle.getElementsByTagName(`c.${TIMESTAMP_CLASS}`))) {
const first = marker.firstChild
if (first?.nodeType === 3)
first.nodeValue = (first.nodeValue || '').replace(/^<(?:\d+:)?\d{2}:\d{2}\.\d{3}>/, '')
marker.replaceWith(...Array.from(marker.childNodes))
}
}
if (lifetime.closed)
return
lifetime.own(() => art.off('subtitleAfterUpdate', update))
art.on('subtitleAfterUpdate', update)
}
@@ -1,5 +1,6 @@
import type Artplayer from 'artplayer'
import type { Option, Result, Track } from './types'
import installCaptionView from './caption'
import createLifetime from './lifetime'
import { parseTracks, serializeTracks } from './merge'
import createRenderer from './render'
@@ -33,6 +34,7 @@ export default function artplayerPluginMultipleSubtitles({ subtitles = [] }: Opt
return result
// A live completed request yields text; cancellation was checked above.
trees = parseTracks(vtts as string[], subtitles)
installCaptionView(art, lifetime)
setTracks(trees)
return result
}
@@ -1,7 +1,21 @@
import type { Cue, WrappedNode } from './parser'
import type { Cue, CueNode, TimestampNode, WrappedNode } from './parser'
import type { Track, TrackOption } from './types'
import { WebVTTParser, WebVTTSerializer } from './parser'
export const TIMESTAMP_CLASS = 'artplayer-multiple-subtitles-timestamp'
function markTimestamp(node: TimestampNode): CueNode {
return { type: 'object', name: 'c', classes: [TIMESTAMP_CLASS], children: [node] }
}
function markNestedTimestamps(node: CueNode): CueNode {
if (node.type === 'timestamp')
return markTimestamp(node)
if (node.type === 'object')
return { ...node, children: node.children.map(markNestedTimestamps) }
return node
}
export function parseTracks(vtts: readonly string[], subtitles: readonly TrackOption[]): Track[] {
const parser = new WebVTTParser()
return vtts.map((vtt, index) => {
@@ -21,10 +35,14 @@ export function serializeTracks(trees: readonly (Track | undefined)[]): string {
...cue,
tree: {
...cue.tree,
children: cue.tree.children.map(child => ({
...child,
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`,
})),
children: cue.tree.children.map((child) => {
if (child.type === 'timestamp')
return markTimestamp(child)
const value = `<div class="art-subtitle-${tree.name}">${child.value}</div>`
return child.type === 'object'
? { ...child, value, children: child.children.map(markNestedTimestamps) }
: { ...child, value }
}),
},
})
}
@@ -17,8 +17,7 @@ export interface TimestampNode {
}
export type CueNode = TextNode | ObjectNode | TimestampNode
// The historical plugin wraps every top-level value, including timestamp nodes.
export type WrappedNode = TextNode | (Omit<ObjectNode, 'value'> & { value: string }) | { type: 'timestamp', value: string }
export type WrappedNode = CueNode
export interface Cue<Node = CueNode> {
id: string