libbitsub 1.10.2 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +296 -49
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/hlsjs.d.ts +45 -0
- package/dist/integrations/hlsjs.d.ts.map +1 -0
- package/dist/integrations/hlsjs.js +121 -0
- package/dist/integrations/hlsjs.js.map +1 -0
- package/dist/integrations/index.d.ts +11 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +11 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/react.d.ts +55 -0
- package/dist/integrations/react.d.ts.map +1 -0
- package/dist/integrations/react.js +133 -0
- package/dist/integrations/react.js.map +1 -0
- package/dist/integrations/shaka.d.ts +38 -0
- package/dist/integrations/shaka.d.ts.map +1 -0
- package/dist/integrations/shaka.js +71 -0
- package/dist/integrations/shaka.js.map +1 -0
- package/dist/integrations/shared.d.ts +41 -0
- package/dist/integrations/shared.d.ts.map +1 -0
- package/dist/integrations/shared.js +123 -0
- package/dist/integrations/shared.js.map +1 -0
- package/dist/integrations/videojs.d.ts +85 -0
- package/dist/integrations/videojs.d.ts.map +1 -0
- package/dist/integrations/videojs.js +111 -0
- package/dist/integrations/videojs.js.map +1 -0
- package/dist/ts/capabilities.d.ts +8 -0
- package/dist/ts/capabilities.d.ts.map +1 -0
- package/dist/ts/capabilities.js +105 -0
- package/dist/ts/capabilities.js.map +1 -0
- package/dist/ts/diagnostics.d.ts.map +1 -1
- package/dist/ts/diagnostics.js.map +1 -1
- package/dist/ts/frame-export.d.ts.map +1 -1
- package/dist/ts/frame-export.js +4 -4
- package/dist/ts/frame-export.js.map +1 -1
- package/dist/ts/parsers.d.ts +46 -8
- package/dist/ts/parsers.d.ts.map +1 -1
- package/dist/ts/parsers.js +305 -14
- package/dist/ts/parsers.js.map +1 -1
- package/dist/ts/range-loader.d.ts +31 -0
- package/dist/ts/range-loader.d.ts.map +1 -0
- package/dist/ts/range-loader.js +220 -0
- package/dist/ts/range-loader.js.map +1 -0
- package/dist/ts/renderers.d.ts +140 -8
- package/dist/ts/renderers.d.ts.map +1 -1
- package/dist/ts/renderers.js +1773 -287
- package/dist/ts/renderers.js.map +1 -1
- package/dist/ts/types.d.ts +210 -7
- package/dist/ts/types.d.ts.map +1 -1
- package/dist/ts/utils.d.ts.map +1 -1
- package/dist/ts/utils.js +98 -2
- package/dist/ts/utils.js.map +1 -1
- package/dist/ts/video-frame-scheduler.d.ts +35 -0
- package/dist/ts/video-frame-scheduler.d.ts.map +1 -0
- package/dist/ts/video-frame-scheduler.js +98 -0
- package/dist/ts/video-frame-scheduler.js.map +1 -0
- package/dist/ts/wasm.d.ts +2 -0
- package/dist/ts/wasm.d.ts.map +1 -1
- package/dist/ts/wasm.js +12 -0
- package/dist/ts/wasm.js.map +1 -1
- package/dist/ts/webgl2-renderer.d.ts +9 -0
- package/dist/ts/webgl2-renderer.d.ts.map +1 -1
- package/dist/ts/webgl2-renderer.js +34 -0
- package/dist/ts/webgl2-renderer.js.map +1 -1
- package/dist/ts/webgpu-renderer.d.ts +9 -0
- package/dist/ts/webgpu-renderer.d.ts.map +1 -1
- package/dist/ts/webgpu-renderer.js +70 -2
- package/dist/ts/webgpu-renderer.js.map +1 -1
- package/dist/ts/worker.d.ts +11 -0
- package/dist/ts/worker.d.ts.map +1 -1
- package/dist/ts/worker.js +620 -158
- package/dist/ts/worker.js.map +1 -1
- package/dist/wrapper.d.ts +6 -3
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +5 -2
- package/dist/wrapper.js.map +1 -1
- package/examples/README.md +38 -0
- package/examples/hlsjs/README.md +53 -0
- package/examples/react/README.md +60 -0
- package/examples/shaka/README.md +61 -0
- package/examples/videojs/README.md +55 -0
- package/package.json +66 -8
- package/pkg/README.md +296 -49
- package/pkg/libbitsub.d.ts +65 -1
- package/pkg/libbitsub.js +228 -3
- package/pkg/libbitsub_bg.wasm +0 -0
- package/pkg/libbitsub_bg.wasm.d.ts +28 -0
- package/pkg/package.json +4 -4
- package/src/integrations/hlsjs.ts +154 -0
- package/src/integrations/index.ts +36 -0
- package/src/integrations/react.tsx +176 -0
- package/src/integrations/shaka.ts +92 -0
- package/src/integrations/shared.ts +171 -0
- package/src/integrations/videojs.ts +200 -0
- package/src/wrapper.ts +34 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional hls.js adapter for libbitsub bitmap subtitles.
|
|
3
|
+
*
|
|
4
|
+
* hls.js handles HLS media and WebVTT/IMSC text tracks; use this helper for
|
|
5
|
+
* external PGS/VobSub/MKS bitmap overlays on the same media element.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import Hls from 'hls.js'
|
|
10
|
+
* import { attachBitSubToHls } from 'libbitsub/hlsjs'
|
|
11
|
+
*
|
|
12
|
+
* const hls = new Hls()
|
|
13
|
+
* hls.loadSource(playlistUrl)
|
|
14
|
+
* hls.attachMedia(video)
|
|
15
|
+
* const bitsub = attachBitSubToHls(hls, { subUrl: '/subs/movie.sup' })
|
|
16
|
+
* // later
|
|
17
|
+
* bitsub.dispose()
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { attachBitSub, type AttachBitSubOptions, type BitSubController, type BitSubSourceOptions } from './shared'
|
|
22
|
+
|
|
23
|
+
/** Minimal hls.js surface used by the adapter. */
|
|
24
|
+
export interface HlsLike {
|
|
25
|
+
media: HTMLMediaElement | null
|
|
26
|
+
on?(event: string, listener: (...args: unknown[]) => void): void
|
|
27
|
+
off?(event: string, listener: (...args: unknown[]) => void): void
|
|
28
|
+
/** hls.js Events map — optional so callers can pass event name strings. */
|
|
29
|
+
Events?: {
|
|
30
|
+
MEDIA_ATTACHED?: string
|
|
31
|
+
MEDIA_DETACHED?: string
|
|
32
|
+
DESTROYING?: string
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type BitSubHlsOptions = AttachBitSubOptions
|
|
37
|
+
|
|
38
|
+
export interface BitSubHlsHandle extends BitSubController {
|
|
39
|
+
/** Underlying hls.js instance. */
|
|
40
|
+
readonly hls: HlsLike
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function eventName(hls: HlsLike, key: 'MEDIA_ATTACHED' | 'MEDIA_DETACHED' | 'DESTROYING', fallback: string): string {
|
|
44
|
+
return hls.Events?.[key] ?? fallback
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasSubtitleSource(source: BitSubSourceOptions): boolean {
|
|
48
|
+
return Boolean(source.subUrl || source.subContent || source.idxUrl || source.idxContent || source.fileName)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Attach libbitsub to an hls.js instance.
|
|
53
|
+
* Binds to `hls.media` and re-attaches on MEDIA_ATTACHED when event APIs exist.
|
|
54
|
+
*/
|
|
55
|
+
export function attachBitSubToHls(hls: HlsLike, options: BitSubHlsOptions = {}): BitSubHlsHandle {
|
|
56
|
+
const initialMedia = hls.media
|
|
57
|
+
if (initialMedia && !(initialMedia instanceof HTMLVideoElement)) {
|
|
58
|
+
throw new Error('libbitsub hls.js adapter requires hls.media to be an HTMLVideoElement')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Allow constructing before attachMedia(); load waits until a video is available.
|
|
62
|
+
const placeholderVideo = initialMedia instanceof HTMLVideoElement ? initialMedia : document.createElement('video')
|
|
63
|
+
const { autoLoad: initialAutoLoad = true, ...initialSource } = options
|
|
64
|
+
let lastSource: BitSubSourceOptions = { ...initialSource }
|
|
65
|
+
let autoLoad = initialAutoLoad
|
|
66
|
+
|
|
67
|
+
const controller = attachBitSub(placeholderVideo, {
|
|
68
|
+
...initialSource,
|
|
69
|
+
autoLoad: initialMedia instanceof HTMLVideoElement ? initialAutoLoad : false
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const bindMedia = (media: HTMLMediaElement | null): void => {
|
|
73
|
+
if (controller.disposed) return
|
|
74
|
+
if (media && !(media instanceof HTMLVideoElement)) {
|
|
75
|
+
throw new Error('libbitsub hls.js adapter requires hls.media to be an HTMLVideoElement')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!media) {
|
|
79
|
+
controller.setVideo(null)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// setVideo reloads the last source when present; otherwise load explicitly if autoLoad.
|
|
84
|
+
const needsExplicitLoad = autoLoad && hasSubtitleSource(lastSource) && controller.renderer === null
|
|
85
|
+
controller.setVideo(media)
|
|
86
|
+
if (needsExplicitLoad && controller.renderer === null && hasSubtitleSource(lastSource)) {
|
|
87
|
+
controller.load(lastSource)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const onMediaAttached = (): void => bindMedia(hls.media)
|
|
92
|
+
const onMediaDetached = (): void => {
|
|
93
|
+
if (!controller.disposed) controller.clear()
|
|
94
|
+
}
|
|
95
|
+
const onDestroying = (): void => dispose()
|
|
96
|
+
|
|
97
|
+
const mediaAttached = eventName(hls, 'MEDIA_ATTACHED', 'hlsMediaAttached')
|
|
98
|
+
const mediaDetached = eventName(hls, 'MEDIA_DETACHED', 'hlsMediaDetached')
|
|
99
|
+
const destroying = eventName(hls, 'DESTROYING', 'hlsDestroying')
|
|
100
|
+
|
|
101
|
+
hls.on?.(mediaAttached, onMediaAttached)
|
|
102
|
+
hls.on?.(mediaDetached, onMediaDetached)
|
|
103
|
+
hls.on?.(destroying, onDestroying)
|
|
104
|
+
|
|
105
|
+
if (initialMedia instanceof HTMLVideoElement) {
|
|
106
|
+
bindMedia(initialMedia)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dispose = (): void => {
|
|
110
|
+
hls.off?.(mediaAttached, onMediaAttached)
|
|
111
|
+
hls.off?.(mediaDetached, onMediaDetached)
|
|
112
|
+
hls.off?.(destroying, onDestroying)
|
|
113
|
+
controller.dispose()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
hls,
|
|
118
|
+
get renderer() {
|
|
119
|
+
return controller.renderer
|
|
120
|
+
},
|
|
121
|
+
get video() {
|
|
122
|
+
return controller.video
|
|
123
|
+
},
|
|
124
|
+
get disposed() {
|
|
125
|
+
return controller.disposed
|
|
126
|
+
},
|
|
127
|
+
load: (source) => {
|
|
128
|
+
lastSource = { ...source }
|
|
129
|
+
autoLoad = true
|
|
130
|
+
if (!(controller.video instanceof HTMLVideoElement) || controller.video === placeholderVideo) {
|
|
131
|
+
const media = hls.media
|
|
132
|
+
if (media instanceof HTMLVideoElement) {
|
|
133
|
+
controller.setVideo(media)
|
|
134
|
+
} else {
|
|
135
|
+
throw new Error('libbitsub hls.js adapter: call hls.attachMedia(video) before load()')
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return controller.load(source)
|
|
139
|
+
},
|
|
140
|
+
clear: () => {
|
|
141
|
+
autoLoad = false
|
|
142
|
+
controller.clear()
|
|
143
|
+
},
|
|
144
|
+
dispose,
|
|
145
|
+
getDisplaySettings: () => controller.getDisplaySettings(),
|
|
146
|
+
setDisplaySettings: (settings) => controller.setDisplaySettings(settings),
|
|
147
|
+
resetDisplaySettings: () => controller.resetDisplaySettings(),
|
|
148
|
+
getStats: () => controller.getStats(),
|
|
149
|
+
setVideo: (video) => controller.setVideo(video)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type { AttachBitSubOptions, BitSubController, BitSubSourceOptions, BitSubRenderer } from './shared'
|
|
154
|
+
export { attachBitSub, createBitSubRenderer } from './shared'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional player integrations for libbitsub.
|
|
3
|
+
* Prefer deep imports (`libbitsub/videojs`, `libbitsub/shaka`, `libbitsub/hlsjs`, `libbitsub/react`)
|
|
4
|
+
* so unused peer dependencies stay out of your bundle.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
attachBitSub,
|
|
9
|
+
createBitSubRenderer,
|
|
10
|
+
type AttachBitSubOptions,
|
|
11
|
+
type BitSubController,
|
|
12
|
+
type BitSubRenderer,
|
|
13
|
+
type BitSubSourceOptions
|
|
14
|
+
} from './shared'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
registerBitSubPlugin,
|
|
18
|
+
type BitSubVideoJsApi,
|
|
19
|
+
type BitSubVideoJsOptions,
|
|
20
|
+
type VideoJsBitSubPlayer,
|
|
21
|
+
type VideoJsLike,
|
|
22
|
+
type VideoJsPlayerLike
|
|
23
|
+
} from './videojs'
|
|
24
|
+
|
|
25
|
+
export { attachBitSubToShaka, type BitSubShakaHandle, type BitSubShakaOptions, type ShakaPlayerLike } from './shaka'
|
|
26
|
+
|
|
27
|
+
export { attachBitSubToHls, type BitSubHlsHandle, type BitSubHlsOptions, type HlsLike } from './hlsjs'
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
BitSubOverlay,
|
|
31
|
+
useBitSub,
|
|
32
|
+
type BitSubOverlayProps,
|
|
33
|
+
type BitSubReactSource,
|
|
34
|
+
type UseBitSubOptions,
|
|
35
|
+
type UseBitSubResult
|
|
36
|
+
} from './react'
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional React bindings for libbitsub bitmap subtitles.
|
|
3
|
+
*
|
|
4
|
+
* Peer dependency: react >= 18
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { useRef } from 'react'
|
|
9
|
+
* import { useBitSub } from 'libbitsub/react'
|
|
10
|
+
*
|
|
11
|
+
* function Player({ src, subUrl }: { src: string; subUrl: string }) {
|
|
12
|
+
* const videoRef = useRef<HTMLVideoElement>(null)
|
|
13
|
+
* useBitSub(videoRef, { subUrl })
|
|
14
|
+
* return (
|
|
15
|
+
* <div style={{ position: 'relative' }}>
|
|
16
|
+
* <video ref={videoRef} src={src} controls playsInline />
|
|
17
|
+
* </div>
|
|
18
|
+
* )
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Prefer either `useBitSub` *or* `BitSubOverlay` for a given video — not both.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { useEffect, useRef, useState, type CSSProperties, type ReactElement, type RefObject } from 'react'
|
|
26
|
+
import { attachBitSub, type AttachBitSubOptions, type BitSubController, type BitSubSourceOptions } from './shared'
|
|
27
|
+
import type { SubtitleDisplaySettings, SubtitleDiagnosticWarning, SubtitleRendererEvent } from '../wrapper'
|
|
28
|
+
|
|
29
|
+
export type BitSubReactSource = BitSubSourceOptions
|
|
30
|
+
|
|
31
|
+
export interface UseBitSubOptions extends BitSubReactSource {
|
|
32
|
+
/** When false, do not load even if source fields are set. Default true. */
|
|
33
|
+
enabled?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseBitSubResult {
|
|
37
|
+
controller: BitSubController | null
|
|
38
|
+
error: Error | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sourceKey(source: BitSubReactSource | null | undefined): string {
|
|
42
|
+
if (!source) return ''
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
subUrl: source.subUrl ?? null,
|
|
45
|
+
idxUrl: source.idxUrl ?? null,
|
|
46
|
+
fileName: source.fileName ?? null,
|
|
47
|
+
hasSubContent: Boolean(source.subContent),
|
|
48
|
+
hasIdxContent: Boolean(source.idxContent),
|
|
49
|
+
cacheLimit: source.cacheLimit ?? null,
|
|
50
|
+
timeOffset: source.timeOffset ?? null,
|
|
51
|
+
frameAwareSync: source.frameAwareSync ?? null,
|
|
52
|
+
backend: source.backend ?? null,
|
|
53
|
+
devicePixelRatioCap: source.devicePixelRatioCap ?? null,
|
|
54
|
+
offscreenRender: source.offscreenRender ?? null,
|
|
55
|
+
streamingLoad: source.streamingLoad ?? null,
|
|
56
|
+
rangeRequests: source.rangeRequests ?? null,
|
|
57
|
+
debug: source.debug ?? null,
|
|
58
|
+
displaySettings: source.displaySettings ?? null,
|
|
59
|
+
prefetchWindow: source.prefetchWindow ?? null
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Bind a libbitsub renderer to a video element ref for the lifetime of the source.
|
|
65
|
+
* Cleans up automatically on unmount or when the source identity changes.
|
|
66
|
+
*/
|
|
67
|
+
export function useBitSub(
|
|
68
|
+
videoRef: RefObject<HTMLVideoElement | null>,
|
|
69
|
+
options: UseBitSubOptions | null
|
|
70
|
+
): UseBitSubResult {
|
|
71
|
+
const controllerRef = useRef<BitSubController | null>(null)
|
|
72
|
+
const [controller, setController] = useState<BitSubController | null>(null)
|
|
73
|
+
const [error, setError] = useState<Error | null>(null)
|
|
74
|
+
|
|
75
|
+
const onEventRef = useRef<((event: SubtitleRendererEvent) => void) | undefined>(undefined)
|
|
76
|
+
const onWarningRef = useRef<((warning: SubtitleDiagnosticWarning) => void) | undefined>(undefined)
|
|
77
|
+
const onErrorRef = useRef<((error: Error) => void) | undefined>(undefined)
|
|
78
|
+
const onLoadingRef = useRef<(() => void) | undefined>(undefined)
|
|
79
|
+
const onLoadedRef = useRef<(() => void) | undefined>(undefined)
|
|
80
|
+
|
|
81
|
+
onEventRef.current = options?.onEvent
|
|
82
|
+
onWarningRef.current = options?.onWarning
|
|
83
|
+
onErrorRef.current = options?.onError
|
|
84
|
+
onLoadingRef.current = options?.onLoading
|
|
85
|
+
onLoadedRef.current = options?.onLoaded
|
|
86
|
+
|
|
87
|
+
const enabled = options?.enabled !== false
|
|
88
|
+
const key = sourceKey(options)
|
|
89
|
+
const displaySettings = options?.displaySettings
|
|
90
|
+
const container = options?.container
|
|
91
|
+
const canvas = options?.canvas
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
setError(null)
|
|
95
|
+
const video = videoRef.current
|
|
96
|
+
if (!video || !options || !enabled) {
|
|
97
|
+
controllerRef.current?.dispose()
|
|
98
|
+
controllerRef.current = null
|
|
99
|
+
setController(null)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const {
|
|
104
|
+
enabled: _enabled,
|
|
105
|
+
onEvent: _onEvent,
|
|
106
|
+
onWarning: _onWarning,
|
|
107
|
+
onError: _onError,
|
|
108
|
+
onLoading: _onLoading,
|
|
109
|
+
onLoaded: _onLoaded,
|
|
110
|
+
...source
|
|
111
|
+
} = options
|
|
112
|
+
|
|
113
|
+
const attachOptions: AttachBitSubOptions = {
|
|
114
|
+
...source,
|
|
115
|
+
onEvent: (event) => onEventRef.current?.(event),
|
|
116
|
+
onWarning: (warning) => onWarningRef.current?.(warning),
|
|
117
|
+
onError: (err) => {
|
|
118
|
+
setError(err)
|
|
119
|
+
onErrorRef.current?.(err)
|
|
120
|
+
},
|
|
121
|
+
onLoading: () => onLoadingRef.current?.(),
|
|
122
|
+
onLoaded: () => onLoadedRef.current?.()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const next = attachBitSub(video, attachOptions)
|
|
127
|
+
controllerRef.current = next
|
|
128
|
+
setController(next)
|
|
129
|
+
return () => {
|
|
130
|
+
next.dispose()
|
|
131
|
+
if (controllerRef.current === next) {
|
|
132
|
+
controllerRef.current = null
|
|
133
|
+
setController(null)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
const resolved = err instanceof Error ? err : new Error(String(err))
|
|
138
|
+
setError(resolved)
|
|
139
|
+
onErrorRef.current?.(resolved)
|
|
140
|
+
controllerRef.current = null
|
|
141
|
+
setController(null)
|
|
142
|
+
}
|
|
143
|
+
// key captures source identity; callback props stay fresh via refs
|
|
144
|
+
}, [videoRef, key, enabled, container, canvas])
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (displaySettings) {
|
|
148
|
+
controllerRef.current?.setDisplaySettings(displaySettings)
|
|
149
|
+
}
|
|
150
|
+
}, [displaySettings])
|
|
151
|
+
|
|
152
|
+
return { controller, error }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface BitSubOverlayProps extends UseBitSubOptions {
|
|
156
|
+
videoRef: RefObject<HTMLVideoElement | null>
|
|
157
|
+
/** Optional class name applied to a zero-size host span for debugging. */
|
|
158
|
+
className?: string
|
|
159
|
+
style?: CSSProperties
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Declarative overlay helper. Renders nothing visible itself — libbitsub attaches
|
|
164
|
+
* its canvas next to the video element. Useful when you want JSX-configured tracks.
|
|
165
|
+
*/
|
|
166
|
+
export function BitSubOverlay(props: BitSubOverlayProps): ReactElement | null {
|
|
167
|
+
const { videoRef, className, style, ...options } = props
|
|
168
|
+
useBitSub(videoRef, options)
|
|
169
|
+
|
|
170
|
+
if (!className && !style) return null
|
|
171
|
+
return <span className={className} style={{ display: 'none', ...style }} aria-hidden='true' />
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type { AttachBitSubOptions, BitSubController, BitSubSourceOptions, BitSubRenderer } from './shared'
|
|
175
|
+
export { attachBitSub, createBitSubRenderer } from './shared'
|
|
176
|
+
export type { SubtitleDisplaySettings }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional Shaka Player adapter for libbitsub bitmap subtitles.
|
|
3
|
+
*
|
|
4
|
+
* Bitmap PGS/VobSub tracks are not native Shaka text tracks — this helper
|
|
5
|
+
* overlays libbitsub on the player media element.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import shaka from 'shaka-player'
|
|
10
|
+
* import { attachBitSubToShaka } from 'libbitsub/shaka'
|
|
11
|
+
*
|
|
12
|
+
* const player = new shaka.Player(video)
|
|
13
|
+
* await player.load(manifestUri)
|
|
14
|
+
* const bitsub = attachBitSubToShaka(player, { subUrl: '/subs/movie.sup' })
|
|
15
|
+
* // later
|
|
16
|
+
* bitsub.dispose()
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { attachBitSub, type AttachBitSubOptions, type BitSubController } from './shared'
|
|
21
|
+
|
|
22
|
+
/** Minimal Shaka Player surface used by the adapter. */
|
|
23
|
+
export interface ShakaPlayerLike {
|
|
24
|
+
getMediaElement(): HTMLMediaElement | null
|
|
25
|
+
addEventListener?(type: string, listener: (...args: unknown[]) => void): void
|
|
26
|
+
removeEventListener?(type: string, listener: (...args: unknown[]) => void): void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type BitSubShakaOptions = AttachBitSubOptions
|
|
30
|
+
|
|
31
|
+
export interface BitSubShakaHandle extends BitSubController {
|
|
32
|
+
/** Underlying Shaka player reference. */
|
|
33
|
+
readonly player: ShakaPlayerLike
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Attach libbitsub to a Shaka Player instance.
|
|
38
|
+
* Rebinds automatically when Shaka swaps the media element (if event APIs exist).
|
|
39
|
+
*/
|
|
40
|
+
export function attachBitSubToShaka(player: ShakaPlayerLike, options: BitSubShakaOptions = {}): BitSubShakaHandle {
|
|
41
|
+
const media = player.getMediaElement()
|
|
42
|
+
if (!(media instanceof HTMLVideoElement)) {
|
|
43
|
+
throw new Error('libbitsub Shaka adapter requires a player with an HTMLVideoElement media element')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const controller = attachBitSub(media, options)
|
|
47
|
+
|
|
48
|
+
const onLoaded = (): void => {
|
|
49
|
+
if (controller.disposed) return
|
|
50
|
+
const next = player.getMediaElement()
|
|
51
|
+
if (next instanceof HTMLVideoElement) {
|
|
52
|
+
controller.setVideo(next)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const onUnloading = (): void => {
|
|
57
|
+
if (!controller.disposed) controller.clear()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
player.addEventListener?.('loaded', onLoaded)
|
|
61
|
+
player.addEventListener?.('unloading', onUnloading)
|
|
62
|
+
|
|
63
|
+
const dispose = (): void => {
|
|
64
|
+
player.removeEventListener?.('loaded', onLoaded)
|
|
65
|
+
player.removeEventListener?.('unloading', onUnloading)
|
|
66
|
+
controller.dispose()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
player,
|
|
71
|
+
get renderer() {
|
|
72
|
+
return controller.renderer
|
|
73
|
+
},
|
|
74
|
+
get video() {
|
|
75
|
+
return controller.video
|
|
76
|
+
},
|
|
77
|
+
get disposed() {
|
|
78
|
+
return controller.disposed
|
|
79
|
+
},
|
|
80
|
+
load: (source) => controller.load(source),
|
|
81
|
+
clear: () => controller.clear(),
|
|
82
|
+
dispose,
|
|
83
|
+
getDisplaySettings: () => controller.getDisplaySettings(),
|
|
84
|
+
setDisplaySettings: (settings) => controller.setDisplaySettings(settings),
|
|
85
|
+
resetDisplaySettings: () => controller.resetDisplaySettings(),
|
|
86
|
+
getStats: () => controller.getStats(),
|
|
87
|
+
setVideo: (video) => controller.setVideo(video)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type { AttachBitSubOptions, BitSubController, BitSubSourceOptions, BitSubRenderer } from './shared'
|
|
92
|
+
export { attachBitSub, createBitSubRenderer } from './shared'
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared player-integration helpers for libbitsub.
|
|
3
|
+
* Optional adapters build on this surface so the core package stays dependency-free.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createAutoSubtitleRenderer,
|
|
8
|
+
type AutoVideoSubtitleOptions,
|
|
9
|
+
type DvbRenderer,
|
|
10
|
+
type PgsRenderer,
|
|
11
|
+
type SubtitleDisplaySettings,
|
|
12
|
+
type SubtitleRendererStats,
|
|
13
|
+
type VobSubRenderer
|
|
14
|
+
} from '../wrapper'
|
|
15
|
+
|
|
16
|
+
/** High-level renderer produced by integrations. */
|
|
17
|
+
export type BitSubRenderer = PgsRenderer | VobSubRenderer | DvbRenderer
|
|
18
|
+
|
|
19
|
+
/** Subtitle source + renderer options without a bound video element. */
|
|
20
|
+
export type BitSubSourceOptions = Omit<AutoVideoSubtitleOptions, 'video'>
|
|
21
|
+
|
|
22
|
+
export interface BitSubController {
|
|
23
|
+
/** Active renderer, or null when no track is loaded. */
|
|
24
|
+
readonly renderer: BitSubRenderer | null
|
|
25
|
+
/** Currently bound video element. */
|
|
26
|
+
readonly video: HTMLVideoElement | null
|
|
27
|
+
/** Load or replace the active subtitle track. */
|
|
28
|
+
load(source: BitSubSourceOptions): BitSubRenderer
|
|
29
|
+
/** Remove the active track without disposing the controller. */
|
|
30
|
+
clear(): void
|
|
31
|
+
/** Tear down the renderer and release the controller. */
|
|
32
|
+
dispose(): void
|
|
33
|
+
/** Whether {@link dispose} has already been called. */
|
|
34
|
+
readonly disposed: boolean
|
|
35
|
+
getDisplaySettings(): SubtitleDisplaySettings | null
|
|
36
|
+
setDisplaySettings(settings: Partial<SubtitleDisplaySettings>): void
|
|
37
|
+
resetDisplaySettings(): void
|
|
38
|
+
getStats(): SubtitleRendererStats | null
|
|
39
|
+
/** Rebind to another video element, keeping the last source when present. */
|
|
40
|
+
setVideo(video: HTMLVideoElement | null): void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AttachBitSubOptions extends BitSubSourceOptions {
|
|
44
|
+
/** When false, skip the initial load even if source fields are present. Default true. */
|
|
45
|
+
autoLoad?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasSubtitleSource(source: BitSubSourceOptions | undefined): boolean {
|
|
49
|
+
if (!source) return false
|
|
50
|
+
return Boolean(source.subUrl || source.subContent || source.idxUrl || source.idxContent || source.fileName)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertVideo(video: HTMLVideoElement | null | undefined): HTMLVideoElement {
|
|
54
|
+
if (!video) {
|
|
55
|
+
throw new Error('libbitsub integration requires an HTMLVideoElement')
|
|
56
|
+
}
|
|
57
|
+
return video
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Attach a disposable controller to a video element.
|
|
62
|
+
* Call {@link BitSubController.load} to start a track, or pass source fields to auto-load.
|
|
63
|
+
*/
|
|
64
|
+
export function attachBitSub(video: HTMLVideoElement, options: AttachBitSubOptions = {}): BitSubController {
|
|
65
|
+
const { autoLoad = true, ...initialSource } = options
|
|
66
|
+
let disposed = false
|
|
67
|
+
let boundVideo: HTMLVideoElement | null = video
|
|
68
|
+
let renderer: BitSubRenderer | null = null
|
|
69
|
+
let lastSource: BitSubSourceOptions | null = hasSubtitleSource(initialSource) ? { ...initialSource } : null
|
|
70
|
+
|
|
71
|
+
const clearRenderer = (): void => {
|
|
72
|
+
if (!renderer) return
|
|
73
|
+
renderer.dispose()
|
|
74
|
+
renderer = null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const controller: BitSubController = {
|
|
78
|
+
get renderer() {
|
|
79
|
+
return renderer
|
|
80
|
+
},
|
|
81
|
+
get video() {
|
|
82
|
+
return boundVideo
|
|
83
|
+
},
|
|
84
|
+
get disposed() {
|
|
85
|
+
return disposed
|
|
86
|
+
},
|
|
87
|
+
load(source: BitSubSourceOptions): BitSubRenderer {
|
|
88
|
+
if (disposed) {
|
|
89
|
+
throw new Error('BitSubController has been disposed')
|
|
90
|
+
}
|
|
91
|
+
const activeVideo = assertVideo(boundVideo)
|
|
92
|
+
if (!hasSubtitleSource(source)) {
|
|
93
|
+
throw new Error('BitSubController.load requires subUrl, subContent, or a detectable fileName')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
clearRenderer()
|
|
97
|
+
lastSource = { ...source }
|
|
98
|
+
renderer = createAutoSubtitleRenderer({
|
|
99
|
+
...source,
|
|
100
|
+
video: activeVideo
|
|
101
|
+
})
|
|
102
|
+
return renderer
|
|
103
|
+
},
|
|
104
|
+
clear(): void {
|
|
105
|
+
if (disposed) return
|
|
106
|
+
clearRenderer()
|
|
107
|
+
lastSource = null
|
|
108
|
+
},
|
|
109
|
+
dispose(): void {
|
|
110
|
+
if (disposed) return
|
|
111
|
+
disposed = true
|
|
112
|
+
clearRenderer()
|
|
113
|
+
lastSource = null
|
|
114
|
+
boundVideo = null
|
|
115
|
+
},
|
|
116
|
+
getDisplaySettings(): SubtitleDisplaySettings | null {
|
|
117
|
+
return renderer?.getDisplaySettings() ?? null
|
|
118
|
+
},
|
|
119
|
+
setDisplaySettings(settings: Partial<SubtitleDisplaySettings>): void {
|
|
120
|
+
renderer?.setDisplaySettings(settings)
|
|
121
|
+
},
|
|
122
|
+
resetDisplaySettings(): void {
|
|
123
|
+
renderer?.resetDisplaySettings()
|
|
124
|
+
},
|
|
125
|
+
getStats(): SubtitleRendererStats | null {
|
|
126
|
+
return renderer?.getStats() ?? null
|
|
127
|
+
},
|
|
128
|
+
setVideo(nextVideo: HTMLVideoElement | null): void {
|
|
129
|
+
if (disposed) {
|
|
130
|
+
throw new Error('BitSubController has been disposed')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (nextVideo === boundVideo) {
|
|
134
|
+
// Same element: still materialize a pending source (e.g. delayed autoLoad).
|
|
135
|
+
if (nextVideo && !renderer && lastSource) {
|
|
136
|
+
renderer = createAutoSubtitleRenderer({
|
|
137
|
+
...lastSource,
|
|
138
|
+
video: nextVideo
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const previousSource = lastSource
|
|
145
|
+
clearRenderer()
|
|
146
|
+
boundVideo = nextVideo
|
|
147
|
+
|
|
148
|
+
if (nextVideo && previousSource) {
|
|
149
|
+
lastSource = previousSource
|
|
150
|
+
renderer = createAutoSubtitleRenderer({
|
|
151
|
+
...previousSource,
|
|
152
|
+
video: nextVideo
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (autoLoad && lastSource) {
|
|
159
|
+
controller.load(lastSource)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return controller
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Convenience one-shot helper used by thin player wrappers. */
|
|
166
|
+
export function createBitSubRenderer(video: HTMLVideoElement, source: BitSubSourceOptions): BitSubRenderer {
|
|
167
|
+
return createAutoSubtitleRenderer({
|
|
168
|
+
...source,
|
|
169
|
+
video: assertVideo(video)
|
|
170
|
+
})
|
|
171
|
+
}
|