mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 09:36:16 -08:00
98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
import type { Rectangle, Thumbnail } from './types'
|
|
|
|
function invalid(line: number, message: string): never {
|
|
throw new TypeError(`Invalid VTT thumbnail at line ${line}: ${message}`)
|
|
}
|
|
|
|
function timestamp(value: string, line: number): number {
|
|
if (!/^\d{2,}(?::\d{2}){0,2}(?:\.\d{3})?$/.test(value))
|
|
invalid(line, 'invalid timestamp')
|
|
const [whole, fraction = '0'] = value.split('.')
|
|
const seconds = whole!.split(':').reduce((total, part) => total * 60 + Number(part), 0) + Number(fraction) / 1000
|
|
if (!Number.isSafeInteger(Math.floor(seconds)))
|
|
invalid(line, 'timestamp exceeds the supported numeric range')
|
|
return seconds
|
|
}
|
|
|
|
function rectangle(text: string, vttUrl: string, line: number): Rectangle & { url: string } {
|
|
const match = text.match(/^(.+)#([xywh]{4})=(.*)$/)
|
|
if (!match || new Set(match[2]!).size !== 4)
|
|
invalid(line, 'expected an image URL and four distinct xywh keys')
|
|
const values = match[3]!.split(',').map(value => value.trim())
|
|
if (values.length !== 4)
|
|
invalid(line, 'expected four rectangle coordinates')
|
|
const rect = {} as Rectangle
|
|
for (let index = 0; index < 4; index++) {
|
|
const key = match[2]![index] as keyof Rectangle
|
|
const value = values[index]!
|
|
if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(value) || !Number.isFinite(Number(value)))
|
|
invalid(line, 'rectangle coordinates must be finite non-negative decimals')
|
|
if ((key === 'w' || key === 'h') && Number(value) === 0)
|
|
invalid(line, 'rectangle width and height must be positive')
|
|
rect[key] = value
|
|
}
|
|
let url = match[1]!
|
|
if (!/^\/|(?:https?|ftp|file):\/\//i.test(url)) {
|
|
const segments = vttUrl.split('/')
|
|
segments.pop()
|
|
segments.push(url)
|
|
url = segments.join('/')
|
|
}
|
|
return { url, ...rect }
|
|
}
|
|
|
|
export function findThumbnail(thumbnails: readonly Thumbnail[], second: number): Thumbnail | undefined {
|
|
return thumbnails.find(item => second >= item.start && second <= item.end)
|
|
}
|
|
|
|
export default function parseVtt(text: unknown, vttUrl = ''): Thumbnail[] {
|
|
if (typeof text !== 'string')
|
|
invalid(1, 'expected text')
|
|
const lines = text.replace(/^\uFEFF/, '').split(/\r\n|\r|\n/)
|
|
let index = 0
|
|
function skipEmpty() {
|
|
while (index < lines.length && !lines[index]!.trim())
|
|
index++
|
|
}
|
|
skipEmpty()
|
|
if (index === lines.length)
|
|
return []
|
|
if (!/^WEBVTT(?:[\t ].*)?$/.test(lines[index]!.trim()) || lines[index]!.includes('-->'))
|
|
invalid(index + 1, 'expected WEBVTT header')
|
|
index++
|
|
const thumbnails: Thumbnail[] = []
|
|
while (index < lines.length) {
|
|
skipEmpty()
|
|
if (index === lines.length)
|
|
break
|
|
const first = lines[index]!.trim()
|
|
if (/^NOTE(?:[\t ]|$)/.test(first) || first === 'STYLE' || first === 'REGION') {
|
|
while (index < lines.length && lines[index]!.trim())
|
|
index++
|
|
continue
|
|
}
|
|
const identifierLine = index + 1
|
|
if (!first.includes('-->')) {
|
|
index++
|
|
if (index === lines.length || !lines[index]!.trim())
|
|
invalid(identifierLine, 'cue identifier must be followed by timing')
|
|
}
|
|
const timingLine = index + 1
|
|
const timing = lines[index]!.trim().match(/^([\d:.]+)[\t ]*-->[\t ]*([\d:.]+)(?:[\t ].*)?$/)
|
|
if (!timing)
|
|
invalid(timingLine, 'invalid cue timing')
|
|
const start = timestamp(timing[1]!, timingLine)
|
|
const end = timestamp(timing[2]!, timingLine)
|
|
if (end < start)
|
|
invalid(timingLine, 'cue end precedes its start')
|
|
index++
|
|
skipEmpty()
|
|
if (index === lines.length)
|
|
invalid(timingLine, 'missing sprite image')
|
|
const image = rectangle(lines[index]!.trim(), vttUrl, index + 1)
|
|
thumbnails.push({ start: Math.floor(start), end: Math.floor(end), ...image })
|
|
index++
|
|
}
|
|
return thumbnails
|
|
}
|