mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 08:51:20 -08:00
refactor(multiple-subtitles): [PKG-MULTI-SUB-04] type runtime modules and vendor boundaries
This commit is contained in:
1 parent
beaa93d6cc
commit
da64aae267
28 files changed
+7075
-86
No files matched your search
+10
-7
@@ -1,19 +1,21 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Option, Result, Track } from './types'
|
||||
import createLifetime from './lifetime'
|
||||
import { parseTracks, serializeTracks } from './merge'
|
||||
import createRenderer from './render'
|
||||
import loadVtt from './request'
|
||||
|
||||
export default function artplayerPluginMultipleSubtitles({ subtitles = [] }) {
|
||||
return async (art) => {
|
||||
const { unescape, getExt, srtToVtt, assToVtt } = art.constructor.utils
|
||||
export default function artplayerPluginMultipleSubtitles({ subtitles = [] }: Option) {
|
||||
return async (art: Artplayer): Promise<Result> => {
|
||||
const { unescape, getExt, srtToVtt, assToVtt } = (art.constructor as typeof Artplayer).utils
|
||||
const lifetime = createLifetime(art)
|
||||
const render = createRenderer(art, lifetime, unescape)
|
||||
let trees = []
|
||||
function setTracks(selected) {
|
||||
let trees: Track[] = []
|
||||
function setTracks(selected: readonly (Track | undefined)[]): void {
|
||||
if (!lifetime.closed)
|
||||
render(serializeTracks(selected))
|
||||
}
|
||||
const result = {
|
||||
const result: Result = {
|
||||
name: 'multipleSubtitles',
|
||||
tracks(names = []) {
|
||||
if (!lifetime.closed)
|
||||
@@ -29,7 +31,8 @@ export default function artplayerPluginMultipleSubtitles({ subtitles = [] }) {
|
||||
const vtts = await Promise.all(subtitles.map(option => loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime)))
|
||||
if (lifetime.closed)
|
||||
return result
|
||||
trees = parseTracks(vtts, subtitles)
|
||||
// A live completed request yields text; cancellation was checked above.
|
||||
trees = parseTracks(vtts as string[], subtitles)
|
||||
setTracks(trees)
|
||||
return result
|
||||
}
|
||||
+8
-6
@@ -1,11 +1,13 @@
|
||||
export default function createLifetime(art) {
|
||||
import type { Cleanup, Lifetime, LifetimeHost } from './types'
|
||||
|
||||
export default function createLifetime(art: LifetimeHost): Lifetime {
|
||||
let closed = Boolean(art.isDestroy)
|
||||
const cleanups = new Set()
|
||||
let cancel
|
||||
const cancelled = new Promise((resolve) => {
|
||||
const cleanups = new Set<Cleanup>()
|
||||
let cancel!: Cleanup
|
||||
const cancelled = new Promise<void>((resolve) => {
|
||||
cancel = resolve
|
||||
})
|
||||
function run(cleanup) {
|
||||
function run(cleanup: Cleanup): void {
|
||||
try {
|
||||
cleanup()
|
||||
}
|
||||
@@ -13,7 +15,7 @@ export default function createLifetime(art) {
|
||||
console.warn('Failed to clean up multiple subtitles:', error)
|
||||
}
|
||||
}
|
||||
const lifetime = {
|
||||
const lifetime: Lifetime = {
|
||||
get closed() { return closed },
|
||||
own(cleanup) {
|
||||
if (closed)
|
||||
@@ -1,30 +0,0 @@
|
||||
import { WebVTTParser, WebVTTSerializer } from './parser'
|
||||
|
||||
export function parseTracks(vtts, subtitles) {
|
||||
const parser = new WebVTTParser()
|
||||
return vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, 'metadata')
|
||||
tree.url = subtitles[index].url
|
||||
tree.name = subtitles[index].name
|
||||
return tree
|
||||
})
|
||||
}
|
||||
|
||||
export function serializeTracks(trees) {
|
||||
const cues = []
|
||||
for (const tree of trees) {
|
||||
for (const cue of tree.cues) {
|
||||
cues.push({
|
||||
...cue,
|
||||
tree: {
|
||||
...cue.tree,
|
||||
children: cue.tree.children.map(child => ({
|
||||
...child,
|
||||
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`,
|
||||
})),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return new WebVTTSerializer().serialize(cues)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Cue, WrappedNode } from './parser'
|
||||
import type { Track, TrackOption } from './types'
|
||||
import { WebVTTParser, WebVTTSerializer } from './parser'
|
||||
|
||||
export function parseTracks(vtts: readonly string[], subtitles: readonly TrackOption[]): Track[] {
|
||||
const parser = new WebVTTParser()
|
||||
return vtts.map((vtt, index) => {
|
||||
const tree = parser.parse(vtt, 'metadata')
|
||||
// Options and completed downloads share the same Promise.all index.
|
||||
return { ...tree, url: subtitles[index]!.url, name: subtitles[index]!.name }
|
||||
})
|
||||
}
|
||||
|
||||
export function serializeTracks(trees: readonly (Track | undefined)[]): string {
|
||||
const cues: Cue<WrappedNode>[] = []
|
||||
for (const selected of trees) {
|
||||
// Unknown names retain the historical TypeError at the property access below.
|
||||
const tree = selected!
|
||||
for (const cue of tree.cues) {
|
||||
cues.push({
|
||||
...cue,
|
||||
tree: {
|
||||
...cue.tree,
|
||||
children: cue.tree.children.map(child => ({
|
||||
...child,
|
||||
value: `<div class="art-subtitle-${tree.name}">${child.value}</div>`,
|
||||
})),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return new WebVTTSerializer().serialize(cues)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
export interface TextNode {
|
||||
type: 'text'
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface ObjectNode {
|
||||
type: 'object'
|
||||
name: string
|
||||
classes: string[]
|
||||
value?: string
|
||||
children: CueNode[]
|
||||
}
|
||||
|
||||
export interface TimestampNode {
|
||||
type: 'timestamp'
|
||||
value: number
|
||||
}
|
||||
|
||||
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 interface Cue<Node = CueNode> {
|
||||
id: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
pauseOnExit: boolean
|
||||
direction: 'horizontal' | 'rl' | 'lr'
|
||||
snapToLines: boolean
|
||||
linePosition: number | 'auto'
|
||||
lineAlign: 'start' | 'center' | 'end'
|
||||
textPosition: number | 'auto'
|
||||
positionAlign: 'auto' | 'line-left' | 'center' | 'line-right'
|
||||
size: number
|
||||
alignment: 'start' | 'center' | 'end' | 'left' | 'right'
|
||||
text: string
|
||||
tree: { children: Node[] }
|
||||
nonSerializable?: boolean
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
cues: Cue[]
|
||||
errors: { message: string, line: number, col: number | undefined }[]
|
||||
styles: string[]
|
||||
time: number
|
||||
}
|
||||
|
||||
// Private declaration for the vendored exports used by this package, not a new public API.
|
||||
export declare class WebVTTParser {
|
||||
constructor(entities?: Record<string, string>)
|
||||
parse(input: string, mode: 'metadata'): ParseResult
|
||||
}
|
||||
|
||||
export declare class WebVTTSerializer {
|
||||
serialize(cues: readonly Cue<CueNode | WrappedNode>[], styles?: readonly string[]): string
|
||||
}
|
||||
+14
-10
@@ -1,9 +1,12 @@
|
||||
export default function createRenderer(art, lifetime, unescape) {
|
||||
let current = null
|
||||
function own(url) {
|
||||
import type { Subtitle, Utils } from 'artplayer'
|
||||
import type { Lifetime, OwnedURL, RenderHost } from './types'
|
||||
|
||||
export default function createRenderer(art: RenderHost, lifetime: Lifetime, unescape: Utils['unescape']): (vtt: string) => void {
|
||||
let current: OwnedURL | null = null
|
||||
function own(url: string): OwnedURL {
|
||||
let freed = false
|
||||
let release = () => {}
|
||||
const entry = {
|
||||
let release: () => unknown = () => {}
|
||||
const entry: OwnedURL = {
|
||||
free() {
|
||||
if (freed)
|
||||
return
|
||||
@@ -24,12 +27,13 @@ export default function createRenderer(art, lifetime, unescape) {
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const previous = current
|
||||
let option
|
||||
let escape
|
||||
let option: Subtitle | undefined
|
||||
let escape: boolean | undefined
|
||||
let configured = false
|
||||
current = entry
|
||||
try {
|
||||
option = art.option.subtitle
|
||||
// ArtPlayer merges subtitle defaults; preserve errors for invalid custom hosts.
|
||||
option = art.option.subtitle!
|
||||
escape = option.escape
|
||||
if (lifetime.closed || current !== entry)
|
||||
return
|
||||
@@ -52,8 +56,8 @@ export default function createRenderer(art, lifetime, unescape) {
|
||||
catch (error) {
|
||||
if (current === entry) {
|
||||
current = previous
|
||||
if (configured && option.escape === false)
|
||||
option.escape = escape
|
||||
if (configured && option!.escape === false)
|
||||
option!.escape = escape
|
||||
}
|
||||
entry.free()
|
||||
throw error
|
||||
+10
-7
@@ -1,4 +1,6 @@
|
||||
export default async function loadVtt(option, { getExt, srtToVtt, assToVtt }, lifetime) {
|
||||
import type { Converters, Lifetime, TrackOption } from './types'
|
||||
|
||||
export default async function loadVtt(option: TrackOption, { getExt, srtToVtt, assToVtt }: Converters, lifetime: Lifetime): Promise<string | void> {
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
||||
@@ -7,16 +9,17 @@ export default async function loadVtt(option, { getExt, srtToVtt, assToVtt }, li
|
||||
const url = option.url
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const response = await lifetime.wait(controller ? fetch(url, { signal: controller.signal }) : fetch(url))
|
||||
// Keep native fetch coercion for historically optional URLs; do not add a default URL.
|
||||
const response = await lifetime.wait(controller ? fetch(url!, { signal: controller.signal }) : fetch(url!))
|
||||
if (lifetime.closed)
|
||||
return
|
||||
if (response.ok === false)
|
||||
throw new Error(`Failed to fetch multiple subtitles: HTTP ${response.status}`)
|
||||
const buffer = await lifetime.wait(response.arrayBuffer())
|
||||
if (response!.ok === false)
|
||||
throw new Error(`Failed to fetch multiple subtitles: HTTP ${response!.status}`)
|
||||
const buffer = await lifetime.wait(response!.arrayBuffer())
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const text = new TextDecoder(option.encoding || 'utf-8').decode(buffer)
|
||||
switch (option.type || getExt(option.url)) {
|
||||
const text = new TextDecoder(option.encoding || 'utf-8').decode(buffer!)
|
||||
switch (option.type || getExt(option.url!)) {
|
||||
case 'srt': return srtToVtt(text)
|
||||
case 'ass': return assToVtt(text)
|
||||
case 'vtt': return text
|
||||
@@ -0,0 +1,35 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Subtitle, Utils } from 'artplayer'
|
||||
import type legacyFactory from '../types/artplayer-plugin-multiple-subtitles'
|
||||
import type { ParseResult } from './parser'
|
||||
|
||||
export type LegacyOption = Parameters<typeof legacyFactory>[0]
|
||||
export type TrackOption = LegacyOption['subtitles'][number]
|
||||
export interface Option {
|
||||
subtitles?: TrackOption[]
|
||||
}
|
||||
export interface Result {
|
||||
name: 'multipleSubtitles'
|
||||
tracks: (names?: string[]) => void
|
||||
reset: () => void
|
||||
}
|
||||
export interface Track extends ParseResult {
|
||||
url: string | undefined
|
||||
name: string | undefined
|
||||
}
|
||||
export type Converters = Pick<Utils, 'getExt' | 'srtToVtt' | 'assToVtt'>
|
||||
export type Cleanup = () => void
|
||||
export interface Lifetime {
|
||||
readonly closed: boolean
|
||||
own: (cleanup: Cleanup) => () => boolean
|
||||
wait: <Value>(value: Value | PromiseLike<Value>) => Promise<Value | void>
|
||||
dispose: Cleanup
|
||||
}
|
||||
export type LifetimeHost = Pick<Artplayer, 'isDestroy' | 'on' | 'off'>
|
||||
export interface RenderHost {
|
||||
option: { subtitle?: Subtitle }
|
||||
subtitle: { init: (option: Subtitle) => unknown }
|
||||
}
|
||||
export interface OwnedURL {
|
||||
free: Cleanup
|
||||
}
|
||||
Reference in new issue
Block a user