mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-02 12:26:36 +08:00
支持 ASS 字幕原样渲染与字幕显示设置
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
export type SubtitleCueAlignment = 'start' | 'center' | 'end' | 'left' | 'right'
|
||||
export type SubtitleCueVertical = 'rl' | 'lr'
|
||||
export type SubtitleCuePositionAlignment = SubtitleCueAlignment | 'line-left' | 'line-right' | 'auto'
|
||||
|
||||
export type SubtitleCueSettings = {
|
||||
align?: SubtitleCueAlignment
|
||||
line?: string
|
||||
lineAlign?: SubtitleCueAlignment
|
||||
position?: number
|
||||
positionAlign?: SubtitleCuePositionAlignment
|
||||
size?: number
|
||||
vertical?: SubtitleCueVertical
|
||||
}
|
||||
|
||||
export type SubtitleCue = {
|
||||
startTime: number
|
||||
endTime: number
|
||||
text: string
|
||||
settings: SubtitleCueSettings
|
||||
}
|
||||
|
||||
function parseVTTTimestamp(value: string): number {
|
||||
const parts = value.trim().replace(',', '.').split(':')
|
||||
if (parts.length !== 2 && parts.length !== 3) return Number.NaN
|
||||
const seconds = Number(parts.pop())
|
||||
const minutes = Number(parts.pop())
|
||||
const hours = parts.length > 0 ? Number(parts.pop()) : 0
|
||||
if (![hours, minutes, seconds].every(Number.isFinite)) return Number.NaN
|
||||
return hours * 3600 + minutes * 60 + seconds
|
||||
}
|
||||
|
||||
function parseCueSettings(tokens: string[]): SubtitleCueSettings {
|
||||
const settings: SubtitleCueSettings = {}
|
||||
for (const token of tokens) {
|
||||
const colon = token.indexOf(':')
|
||||
if (colon <= 0) continue
|
||||
const key = token.slice(0, colon).toLowerCase()
|
||||
const value = token.slice(colon + 1)
|
||||
if (key === 'align') {
|
||||
if (['start', 'center', 'end', 'left', 'right'].includes(value)) {
|
||||
settings.align = value as SubtitleCueAlignment
|
||||
}
|
||||
} else if (key === 'line') {
|
||||
settings.line = value
|
||||
} else if (key === 'linealign') {
|
||||
if (['start', 'center', 'end'].includes(value)) {
|
||||
settings.lineAlign = value as SubtitleCueAlignment
|
||||
}
|
||||
} else if (key === 'position') {
|
||||
const position = Number.parseFloat(value)
|
||||
if (Number.isFinite(position)) settings.position = Math.min(100, Math.max(0, position))
|
||||
} else if (key === 'positionalign') {
|
||||
if (['start', 'center', 'end', 'line-left', 'line-right', 'auto'].includes(value)) {
|
||||
settings.positionAlign = value as SubtitleCuePositionAlignment
|
||||
}
|
||||
} else if (key === 'size') {
|
||||
const size = Number.parseFloat(value)
|
||||
if (Number.isFinite(size)) settings.size = Math.min(100, Math.max(0, size))
|
||||
} else if (key === 'vertical') {
|
||||
if (value === 'rl' || value === 'lr') settings.vertical = value
|
||||
}
|
||||
}
|
||||
return settings
|
||||
}
|
||||
|
||||
export function parseWebVTTCues(body: string): SubtitleCue[] {
|
||||
const blocks = body
|
||||
.replace(/^\uFEFF/, '')
|
||||
.replace(/\r\n?/g, '\n')
|
||||
.split(/\n{2,}/)
|
||||
const cues: SubtitleCue[] = []
|
||||
|
||||
for (const block of blocks) {
|
||||
const lines = block.split('\n')
|
||||
const timingIndex = lines.findIndex((line) => line.includes('-->'))
|
||||
if (timingIndex < 0) continue
|
||||
|
||||
const [rawStart, rawRest] = lines[timingIndex].split('-->', 2)
|
||||
const rest = rawRest.trim().split(/\s+/)
|
||||
const startTime = parseVTTTimestamp(rawStart)
|
||||
const endTime = parseVTTTimestamp(rest[0] || '')
|
||||
const text = lines.slice(timingIndex + 1).join('\n').trim()
|
||||
if (Number.isFinite(startTime) && Number.isFinite(endTime) && endTime >= startTime && text) {
|
||||
cues.push({
|
||||
startTime,
|
||||
endTime,
|
||||
text,
|
||||
settings: parseCueSettings(rest.slice(1)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return cues
|
||||
}
|
||||
Reference in New Issue
Block a user