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.
Files changed (98) hide show
  1. package/README.md +296 -49
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/integrations/hlsjs.d.ts +45 -0
  7. package/dist/integrations/hlsjs.d.ts.map +1 -0
  8. package/dist/integrations/hlsjs.js +121 -0
  9. package/dist/integrations/hlsjs.js.map +1 -0
  10. package/dist/integrations/index.d.ts +11 -0
  11. package/dist/integrations/index.d.ts.map +1 -0
  12. package/dist/integrations/index.js +11 -0
  13. package/dist/integrations/index.js.map +1 -0
  14. package/dist/integrations/react.d.ts +55 -0
  15. package/dist/integrations/react.d.ts.map +1 -0
  16. package/dist/integrations/react.js +133 -0
  17. package/dist/integrations/react.js.map +1 -0
  18. package/dist/integrations/shaka.d.ts +38 -0
  19. package/dist/integrations/shaka.d.ts.map +1 -0
  20. package/dist/integrations/shaka.js +71 -0
  21. package/dist/integrations/shaka.js.map +1 -0
  22. package/dist/integrations/shared.d.ts +41 -0
  23. package/dist/integrations/shared.d.ts.map +1 -0
  24. package/dist/integrations/shared.js +123 -0
  25. package/dist/integrations/shared.js.map +1 -0
  26. package/dist/integrations/videojs.d.ts +85 -0
  27. package/dist/integrations/videojs.d.ts.map +1 -0
  28. package/dist/integrations/videojs.js +111 -0
  29. package/dist/integrations/videojs.js.map +1 -0
  30. package/dist/ts/capabilities.d.ts +8 -0
  31. package/dist/ts/capabilities.d.ts.map +1 -0
  32. package/dist/ts/capabilities.js +105 -0
  33. package/dist/ts/capabilities.js.map +1 -0
  34. package/dist/ts/diagnostics.d.ts.map +1 -1
  35. package/dist/ts/diagnostics.js.map +1 -1
  36. package/dist/ts/frame-export.d.ts.map +1 -1
  37. package/dist/ts/frame-export.js +4 -4
  38. package/dist/ts/frame-export.js.map +1 -1
  39. package/dist/ts/parsers.d.ts +46 -8
  40. package/dist/ts/parsers.d.ts.map +1 -1
  41. package/dist/ts/parsers.js +305 -14
  42. package/dist/ts/parsers.js.map +1 -1
  43. package/dist/ts/range-loader.d.ts +31 -0
  44. package/dist/ts/range-loader.d.ts.map +1 -0
  45. package/dist/ts/range-loader.js +220 -0
  46. package/dist/ts/range-loader.js.map +1 -0
  47. package/dist/ts/renderers.d.ts +140 -8
  48. package/dist/ts/renderers.d.ts.map +1 -1
  49. package/dist/ts/renderers.js +1773 -287
  50. package/dist/ts/renderers.js.map +1 -1
  51. package/dist/ts/types.d.ts +210 -7
  52. package/dist/ts/types.d.ts.map +1 -1
  53. package/dist/ts/utils.d.ts.map +1 -1
  54. package/dist/ts/utils.js +98 -2
  55. package/dist/ts/utils.js.map +1 -1
  56. package/dist/ts/video-frame-scheduler.d.ts +35 -0
  57. package/dist/ts/video-frame-scheduler.d.ts.map +1 -0
  58. package/dist/ts/video-frame-scheduler.js +98 -0
  59. package/dist/ts/video-frame-scheduler.js.map +1 -0
  60. package/dist/ts/wasm.d.ts +2 -0
  61. package/dist/ts/wasm.d.ts.map +1 -1
  62. package/dist/ts/wasm.js +12 -0
  63. package/dist/ts/wasm.js.map +1 -1
  64. package/dist/ts/webgl2-renderer.d.ts +9 -0
  65. package/dist/ts/webgl2-renderer.d.ts.map +1 -1
  66. package/dist/ts/webgl2-renderer.js +34 -0
  67. package/dist/ts/webgl2-renderer.js.map +1 -1
  68. package/dist/ts/webgpu-renderer.d.ts +9 -0
  69. package/dist/ts/webgpu-renderer.d.ts.map +1 -1
  70. package/dist/ts/webgpu-renderer.js +70 -2
  71. package/dist/ts/webgpu-renderer.js.map +1 -1
  72. package/dist/ts/worker.d.ts +11 -0
  73. package/dist/ts/worker.d.ts.map +1 -1
  74. package/dist/ts/worker.js +620 -158
  75. package/dist/ts/worker.js.map +1 -1
  76. package/dist/wrapper.d.ts +6 -3
  77. package/dist/wrapper.d.ts.map +1 -1
  78. package/dist/wrapper.js +5 -2
  79. package/dist/wrapper.js.map +1 -1
  80. package/examples/README.md +38 -0
  81. package/examples/hlsjs/README.md +53 -0
  82. package/examples/react/README.md +60 -0
  83. package/examples/shaka/README.md +61 -0
  84. package/examples/videojs/README.md +55 -0
  85. package/package.json +66 -8
  86. package/pkg/README.md +296 -49
  87. package/pkg/libbitsub.d.ts +65 -1
  88. package/pkg/libbitsub.js +228 -3
  89. package/pkg/libbitsub_bg.wasm +0 -0
  90. package/pkg/libbitsub_bg.wasm.d.ts +28 -0
  91. package/pkg/package.json +4 -4
  92. package/src/integrations/hlsjs.ts +154 -0
  93. package/src/integrations/index.ts +36 -0
  94. package/src/integrations/react.tsx +176 -0
  95. package/src/integrations/shaka.ts +92 -0
  96. package/src/integrations/shared.ts +171 -0
  97. package/src/integrations/videojs.ts +200 -0
  98. 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
+ }