akarisub 0.2.2 → 1.0.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 (76) hide show
  1. package/README.md +198 -167
  2. package/THIRD_PARTY_NOTICES.md +55 -163
  3. package/dist/COPYRIGHT +56 -164
  4. package/dist/index.d.ts +12 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +11 -147
  7. package/dist/index.js.map +1 -0
  8. package/dist/ts/{ts/akarisub.d.ts → akarisub.d.ts} +80 -99
  9. package/dist/ts/akarisub.d.ts.map +1 -0
  10. package/dist/ts/akarisub.js +2086 -0
  11. package/dist/ts/akarisub.js.map +1 -0
  12. package/dist/ts/timing.d.ts +63 -0
  13. package/dist/ts/timing.d.ts.map +1 -0
  14. package/dist/ts/timing.js +219 -0
  15. package/dist/ts/timing.js.map +1 -0
  16. package/dist/ts/{ts/types.d.ts → types.d.ts} +103 -78
  17. package/dist/ts/types.d.ts.map +1 -0
  18. package/dist/ts/types.js +2 -0
  19. package/dist/ts/{ts/types.js.map → types.js.map} +1 -1
  20. package/dist/ts/{ts/utils.d.ts → utils.d.ts} +0 -31
  21. package/dist/ts/utils.d.ts.map +1 -0
  22. package/dist/ts/{ts/utils.js → utils.js} +0 -56
  23. package/dist/ts/utils.js.map +1 -0
  24. package/dist/ts/wasm.d.ts +12 -0
  25. package/dist/ts/wasm.d.ts.map +1 -0
  26. package/dist/ts/wasm.js +46 -0
  27. package/dist/ts/wasm.js.map +1 -0
  28. package/dist/ts/{ts/webgl2-renderer.d.ts → webgl2-renderer.d.ts} +0 -12
  29. package/dist/ts/webgl2-renderer.d.ts.map +1 -0
  30. package/dist/ts/{ts/webgl2-renderer.js → webgl2-renderer.js} +0 -27
  31. package/dist/ts/webgl2-renderer.js.map +1 -0
  32. package/dist/ts/{ts/webgpu-renderer.d.ts → webgpu-renderer.d.ts} +17 -16
  33. package/dist/ts/webgpu-renderer.d.ts.map +1 -0
  34. package/dist/ts/{ts/webgpu-renderer.js → webgpu-renderer.js} +148 -71
  35. package/dist/ts/webgpu-renderer.js.map +1 -0
  36. package/dist/ts/worker.d.ts +2 -0
  37. package/dist/ts/{ts/worker.d.ts.map → worker.d.ts.map} +1 -1
  38. package/dist/ts/{ts/worker.js → worker.js} +1064 -264
  39. package/dist/ts/worker.js.map +1 -0
  40. package/dist/wrapper.d.ts +8 -0
  41. package/dist/wrapper.d.ts.map +1 -0
  42. package/dist/{ts/index.js → wrapper.js} +4 -14
  43. package/dist/wrapper.js.map +1 -0
  44. package/package.json +24 -22
  45. package/pkg/akarisub.js +53 -0
  46. package/{dist/akarisub-worker.wasm → pkg/akarisub.wasm} +0 -0
  47. package/src/wrapper.ts +46 -0
  48. package/dist/akarisub-worker.js +0 -39
  49. package/dist/akarisub.umd.js +0 -159
  50. package/dist/ts/index.d.ts +0 -14
  51. package/dist/ts/index.d.ts.map +0 -1
  52. package/dist/ts/index.js.map +0 -1
  53. package/dist/ts/ts/akarisub.d.ts.map +0 -1
  54. package/dist/ts/ts/akarisub.js +0 -1073
  55. package/dist/ts/ts/akarisub.js.map +0 -1
  56. package/dist/ts/ts/types.d.ts.map +0 -1
  57. package/dist/ts/ts/types.js +0 -5
  58. package/dist/ts/ts/utils.d.ts.map +0 -1
  59. package/dist/ts/ts/utils.js.map +0 -1
  60. package/dist/ts/ts/webgl2-renderer.d.ts.map +0 -1
  61. package/dist/ts/ts/webgl2-renderer.js.map +0 -1
  62. package/dist/ts/ts/webgpu-renderer.d.ts.map +0 -1
  63. package/dist/ts/ts/webgpu-renderer.js.map +0 -1
  64. package/dist/ts/ts/worker.d.ts +0 -6
  65. package/dist/ts/ts/worker.js.map +0 -1
  66. package/dist/ts/wrapper.d.ts +0 -8
  67. package/dist/ts/wrapper.d.ts.map +0 -1
  68. package/dist/ts/wrapper.js +0 -9
  69. package/dist/ts/wrapper.js.map +0 -1
  70. package/src/ts/akarisub.ts +0 -1272
  71. package/src/ts/types.ts +0 -397
  72. package/src/ts/utils.ts +0 -512
  73. package/src/ts/webgl2-renderer.ts +0 -426
  74. package/src/ts/webgpu-renderer.ts +0 -691
  75. package/src/ts/worker.ts +0 -2052
  76. /package/{dist → assets}/default.woff2 +0 -0
@@ -1,1272 +0,0 @@
1
- /**
2
- * Main AkariSub class - TypeScript implementation.
3
- * High-level ASS/SSA subtitle renderer for web browsers using libass.
4
- */
5
-
6
- import type {
7
- AkariSubOptions,
8
- ASSEvent,
9
- ASSStyle,
10
- PerformanceStats,
11
- RenderImage,
12
- RenderTimes,
13
- VideoFrameCallbackMetadata,
14
- SubtitleColorSpace,
15
- WebYCbCrColorSpace
16
- } from './types'
17
- import type { EncryptedSubtitleContent } from './types'
18
- import {
19
- webYCbCrMap,
20
- colorMatrixConversionMap,
21
- computeCanvasSize,
22
- getVideoPosition,
23
- fixAlpha,
24
- getAlphaBug,
25
- getBitmapBug
26
- } from './utils'
27
- import { WebGPURenderer, isWebGPUSupported } from './webgpu-renderer'
28
- import { WebGL2Renderer, isWebGL2Supported } from './webgl2-renderer'
29
-
30
- type AnyGPURenderer = WebGPURenderer | WebGL2Renderer
31
-
32
- const DEFAULT_RENDER_AHEAD = 0.008
33
- const DEFAULT_PIPELINE_LATENCY_MS = DEFAULT_RENDER_AHEAD * 1000
34
- const DEFAULT_WEBKIT_PIPELINE_LATENCY_MS = 16
35
-
36
- const isLikelyWebKit = (): boolean => {
37
- if (typeof navigator === 'undefined') return false
38
-
39
- const userAgent = navigator.userAgent || ''
40
- const vendor = navigator.vendor || ''
41
- const isIOSWebKit = /\b(iPhone|iPad|iPod)\b/i.test(userAgent)
42
-
43
- if (!/AppleWebKit/i.test(userAgent)) return false
44
- if (isIOSWebKit) return true
45
-
46
- if (/\b(Chrome|Chromium|Edg|OPR|SamsungBrowser|Firefox)\b/i.test(userAgent)) {
47
- return false
48
- }
49
-
50
- return vendor.includes('Apple')
51
- }
52
-
53
- /**
54
- * AkariSub - JavaScript ASS/SSA Subtitle Renderer
55
- *
56
- * Renders ASS/SSA subtitles on an HTML5 video element using libass compiled to WebAssembly.
57
- *
58
- * @example
59
- * ```typescript
60
- * const renderer = new AkariSub({
61
- * video: document.querySelector('video'),
62
- * subUrl: '/subtitles/example.ass',
63
- * workerUrl: '/akarisub-worker.js'
64
- * });
65
- *
66
- * // Later, cleanup
67
- * renderer.destroy();
68
- * ```
69
- */
70
- export default class AkariSub extends EventTarget {
71
- private static readonly MAX_PENDING_DEMANDS = 3
72
-
73
- // Feature detection cache (static)
74
- private static _hasAlphaBug: boolean | null = null
75
- private static _hasBitmapBug: boolean | null = null
76
-
77
- // Instance properties
78
- private _loaded: Promise<void>
79
- private _init!: () => void
80
- private _onDemandRender: boolean
81
- private _offscreenRender: boolean
82
- private _video?: HTMLVideoElement
83
- private _videoWidth: number = 0
84
- private _videoHeight: number = 0
85
- private _videoColorSpace: WebYCbCrColorSpace | null = null
86
- private _canvas!: HTMLCanvasElement
87
- private _canvasParent?: HTMLDivElement
88
- private _bufferCanvas: HTMLCanvasElement
89
- private _bufferCtx: CanvasRenderingContext2D
90
- private _canvasctrl!: HTMLCanvasElement | OffscreenCanvas
91
- private _ctx: CanvasRenderingContext2D | false | null = null
92
- private _lastRenderTime: number = 0
93
- private _playstate: boolean = true
94
- private _destroyed: boolean = false
95
- private _workerReady: boolean = false
96
- private _ro?: ResizeObserver
97
- private _worker: Worker
98
- private _pendingDemandTimes: Array<{ mediaTime: number; width: number; height: number }> = []
99
- private readonly _isLikelyWebKit: boolean
100
- private _activeDemandStartedAt: number = 0
101
- private _smoothedDemandLatencyMs: number
102
-
103
- // Bound methods for event listeners
104
- private _boundResize: () => void
105
- private _boundTimeUpdate: (e: Event) => void
106
- private _boundSetRate: () => void
107
- private _boundUpdateColorSpace: () => void
108
- private _boundHandleRVFC: (now: number, metadata: VideoFrameCallbackMetadata) => void
109
-
110
- // GPU renderer (WebGPU or WebGL2 – whichever initialises first in the fallback chain)
111
- private _gpuRenderer: AnyGPURenderer | null = null
112
- private _rendererType: 'webgpu' | 'webgl2' | 'canvas2d' = 'canvas2d'
113
- private _onCanvasFallback?: () => void
114
-
115
- // Cached render data to reduce allocations
116
- private _lastRenderWidth: number = 0
117
- private _lastRenderHeight: number = 0
118
- private _gpuBitmapImages: Array<{ image: ImageBitmap; x: number; y: number }> = []
119
-
120
- // Public properties
121
- public timeOffset: number
122
- public debug: boolean
123
- public prescaleFactor: number
124
- public prescaleHeightLimit: number
125
- public maxRenderHeight: number
126
- public busy: boolean = false
127
- public renderAhead: number
128
-
129
- constructor(options: AkariSubOptions) {
130
- super()
131
-
132
- if (!globalThis.Worker) {
133
- throw this.destroy(new Error('Worker not supported'))
134
- }
135
- if (!options) {
136
- throw this.destroy(new Error('No options provided'))
137
- }
138
-
139
- this._loaded = new Promise((resolve) => {
140
- this._init = resolve
141
- })
142
-
143
- this._isLikelyWebKit = isLikelyWebKit()
144
- this._smoothedDemandLatencyMs = this._isLikelyWebKit
145
- ? DEFAULT_WEBKIT_PIPELINE_LATENCY_MS
146
- : DEFAULT_PIPELINE_LATENCY_MS
147
-
148
- // Run feature tests
149
- const test = AkariSub._test()
150
-
151
- this._onDemandRender = 'requestVideoFrameCallback' in HTMLVideoElement.prototype && (options.onDemandRender ?? true)
152
-
153
- this._onCanvasFallback = options.onCanvasFallback
154
-
155
- const canUseGPURenderer = !this._isLikelyWebKit && !options.canvas && (isWebGPUSupported() || isWebGL2Supported())
156
- const shouldUseAsyncRender =
157
- typeof createImageBitmap !== 'undefined' && (options.asyncRender ?? (!this._isLikelyWebKit && !canUseGPURenderer))
158
-
159
- // Don't support offscreen rendering on custom canvases
160
- this._offscreenRender =
161
- 'transferControlToOffscreen' in HTMLCanvasElement.prototype &&
162
- !options.canvas &&
163
- !canUseGPURenderer &&
164
- (options.offscreenRender ?? true)
165
-
166
- this.timeOffset = options.timeOffset || 0
167
- this._video = options.video
168
- this._canvas = options.canvas!
169
-
170
- if (this._video && !this._canvas) {
171
- this._canvasParent = document.createElement('div')
172
- this._canvasParent.className = 'AkariSub'
173
- this._canvasParent.style.position = 'relative'
174
- this._canvas = this._createCanvas()
175
- this._video.insertAdjacentElement('afterend', this._canvasParent)
176
- } else if (!this._canvas) {
177
- throw this.destroy(new Error("Don't know where to render: you should give video or canvas in options."))
178
- }
179
-
180
- this._bufferCanvas = document.createElement('canvas')
181
- const bufferCtx = this._bufferCanvas.getContext('2d')
182
- if (!bufferCtx) throw this.destroy(new Error('Canvas rendering not supported'))
183
- this._bufferCtx = bufferCtx
184
-
185
- // Try GPU renderers first (WebGPU → WebGL2 → Canvas2D)
186
- if (canUseGPURenderer) {
187
- this._initGPURenderer()
188
- } else if (!this._offscreenRender) {
189
- this._ctx = this._canvas.getContext('2d', { alpha: true, desynchronized: true })
190
- }
191
-
192
- this._canvasctrl = this._offscreenRender
193
- ? (
194
- this._canvas as HTMLCanvasElement & { transferControlToOffscreen(): OffscreenCanvas }
195
- ).transferControlToOffscreen()
196
- : this._canvas
197
-
198
- this._lastRenderTime = 0
199
- this.debug = !!options.debug
200
- this.prescaleFactor = options.prescaleFactor || 1.0
201
- this.prescaleHeightLimit = options.prescaleHeightLimit || 1080
202
- this.maxRenderHeight = options.maxRenderHeight || 0
203
- this.renderAhead = options.renderAhead ?? DEFAULT_RENDER_AHEAD
204
-
205
- // Bind methods
206
- this._boundResize = this.resize.bind(this)
207
- this._boundTimeUpdate = this._timeupdate.bind(this)
208
- this._boundSetRate = () => this.setRate((this._video as HTMLVideoElement).playbackRate)
209
- this._boundUpdateColorSpace = this._updateColorSpace.bind(this)
210
- this._boundHandleRVFC = this._handleRVFC.bind(this)
211
-
212
- if (this._video) {
213
- this.setVideo(this._video)
214
- }
215
-
216
- if (this._onDemandRender) {
217
- this.busy = false
218
- this._pendingDemandTimes.length = 0
219
- }
220
-
221
- // Create worker
222
- this._worker = new Worker(options.workerUrl || 'akarisub-worker.js')
223
- this._worker.onmessage = (e) => this._onmessage(e)
224
- this._worker.onerror = (e) => this._error(e)
225
-
226
- // Initialize worker after feature tests complete
227
- test.then(() => {
228
- const initMessage = {
229
- target: 'init',
230
- wasmUrl: options.wasmUrl ?? 'akarisub-worker.wasm',
231
- asyncRender: shouldUseAsyncRender,
232
- fullTrackWarmup: options.fullTrackWarmup ?? false,
233
- onDemandRender: this._onDemandRender,
234
- initialTime: (this._video?.currentTime ?? 0) + this.timeOffset,
235
- width: this._canvasctrl.width || 0,
236
- height: this._canvasctrl.height || 0,
237
- blendMode: options.blendMode ?? 'wasm',
238
- subUrl: options.subUrl,
239
- subContent: options.subContent || null,
240
- encryptedSubContent: options.encryptedSubContent || null,
241
- fonts: options.fonts || [],
242
- availableFonts: options.availableFonts || { 'liberation sans': './default.woff2' },
243
- fallbackFonts: options.fallbackFonts || ['liberation sans'],
244
- debug: this.debug,
245
- targetFps: options.targetFps || 24,
246
- dropAllAnimations: options.dropAllAnimations,
247
- dropAllBlur: options.dropAllBlur,
248
- clampPos: options.clampPos,
249
- libassMemoryLimit: options.libassMemoryLimit ?? 128,
250
- libassGlyphLimit: options.libassGlyphLimit ?? 2048,
251
- useLocalFonts: typeof (globalThis as any).queryLocalFonts !== 'undefined' && (options.useLocalFonts ?? true),
252
- hasBitmapBug: AkariSub._hasBitmapBug
253
- }
254
-
255
- this._worker.postMessage(initMessage, AkariSub._getSubtitleTransfers(options.subContent, options.encryptedSubContent))
256
-
257
- if (this._offscreenRender) {
258
- this.sendMessage('offscreenCanvas', {}, [this._canvasctrl as OffscreenCanvas])
259
- }
260
- })
261
- }
262
-
263
- // ==========================================================================
264
- // Static Methods
265
- // ==========================================================================
266
-
267
- private static async _testImageBugs(): Promise<void> {
268
- if (AkariSub._hasBitmapBug !== null) return
269
-
270
- const canvas1 = document.createElement('canvas')
271
- const ctx1 = canvas1.getContext('2d', { willReadFrequently: true })
272
- if (!ctx1) throw new Error('Canvas rendering not supported')
273
-
274
- // Test ImageData constructor
275
- if (typeof ImageData.prototype.constructor === 'function') {
276
- try {
277
- new ImageData(new Uint8ClampedArray([0, 0, 0, 0]), 1, 1)
278
- } catch {
279
- console.log('Detected that ImageData is not constructable despite browser saying so')
280
- }
281
- }
282
-
283
- const canvas2 = document.createElement('canvas')
284
- const ctx2 = canvas2.getContext('2d', { willReadFrequently: true })
285
- if (!ctx2) throw new Error('Canvas rendering not supported')
286
-
287
- canvas1.width = canvas2.width = 1
288
- canvas1.height = canvas2.height = 1
289
- ctx1.clearRect(0, 0, 1, 1)
290
- ctx2.clearRect(0, 0, 1, 1)
291
-
292
- const prePut = ctx2.getImageData(0, 0, 1, 1).data
293
- ctx1.putImageData(new ImageData(new Uint8ClampedArray([0, 255, 0, 0]), 1, 1), 0, 0)
294
- ctx2.drawImage(canvas1, 0, 0)
295
- const postPut = ctx2.getImageData(0, 0, 1, 1).data
296
-
297
- AkariSub._hasAlphaBug = prePut[1] !== postPut[1]
298
- if (AkariSub._hasAlphaBug) {
299
- console.log('Detected a browser having issue with transparent pixels, applying workaround')
300
- }
301
-
302
- if (typeof createImageBitmap !== 'undefined') {
303
- const subarray = new Uint8ClampedArray([255, 0, 255, 0, 255]).subarray(1, 5)
304
- ctx2.drawImage(await createImageBitmap(new ImageData(subarray, 1)), 0, 0)
305
- const { data } = ctx2.getImageData(0, 0, 1, 1)
306
- AkariSub._hasBitmapBug = false
307
-
308
- for (let i = 0; i < data.length; i++) {
309
- if (Math.abs(subarray[i] - data[i]) > 15) {
310
- AkariSub._hasBitmapBug = true
311
- console.log('Detected a browser having issue with partial bitmaps, applying workaround')
312
- break
313
- }
314
- }
315
- } else {
316
- AkariSub._hasBitmapBug = false
317
- }
318
-
319
- canvas1.remove()
320
- canvas2.remove()
321
- }
322
-
323
- private static async _test(): Promise<void> {
324
- await AkariSub._testImageBugs()
325
- }
326
-
327
- private static _getSubtitleTransfers(
328
- subContent?: string | Uint8Array | ArrayBuffer,
329
- encryptedSubContent?: EncryptedSubtitleContent
330
- ): Transferable[] {
331
- const transfers: Transferable[] = []
332
-
333
- if (subContent instanceof ArrayBuffer) {
334
- transfers.push(subContent)
335
- } else if (subContent instanceof Uint8Array) {
336
- transfers.push(subContent.buffer)
337
- }
338
-
339
- if (encryptedSubContent?.encrypted) {
340
- transfers.push(encryptedSubContent.encrypted)
341
- }
342
-
343
- for (const chunk of encryptedSubContent?.encryptedChunks || []) {
344
- transfers.push(chunk)
345
- }
346
-
347
- return transfers
348
- }
349
-
350
- // ==========================================================================
351
- // GPU Renderer Management (WebGPU → WebGL2 → Canvas2D fallback)
352
- // ==========================================================================
353
-
354
- /**
355
- * Attempt to initialise the best available GPU renderer.
356
- */
357
- private async _initGPURenderer(): Promise<void> {
358
- if (isWebGPUSupported()) {
359
- try {
360
- const renderer = new WebGPURenderer()
361
- await renderer.init()
362
- if (!this._canvas) return
363
- await renderer.setCanvas(this._canvas, Math.max(1, this._canvas.width || 1), Math.max(1, this._canvas.height || 1))
364
- this._gpuRenderer = renderer
365
- this._rendererType = 'webgpu'
366
- console.log('[AkariSub] Using WebGPU renderer')
367
- return
368
- } catch (error) {
369
- console.warn('[AkariSub] WebGPU init failed, trying WebGL2:', error)
370
- }
371
- }
372
-
373
- if (isWebGL2Supported()) {
374
- try {
375
- const renderer = new WebGL2Renderer()
376
- await renderer.init()
377
- if (!this._canvas) return
378
- await renderer.setCanvas(this._canvas, Math.max(1, this._canvas.width || 1), Math.max(1, this._canvas.height || 1))
379
- this._gpuRenderer = renderer
380
- this._rendererType = 'webgl2'
381
- console.log('[AkariSub] Using WebGL2 renderer')
382
- return
383
- } catch (error) {
384
- console.warn('[AkariSub] WebGL2 init failed, falling back to Canvas2D:', error)
385
- }
386
- }
387
-
388
- this._rendererType = 'canvas2d'
389
- if (!this._offscreenRender && !this._ctx) {
390
- this._ctx = this._canvas.getContext('2d', { alpha: true, desynchronized: true })
391
- }
392
- this.sendMessage('setAsyncRender', { value: false })
393
- this._onCanvasFallback?.()
394
- }
395
-
396
- /** Returns which renderer backend is currently active. */
397
- get rendererType(): 'webgpu' | 'webgl2' | 'canvas2d' {
398
- return this._rendererType
399
- }
400
-
401
- /** @deprecated Use rendererType === 'webgpu' */
402
- get isUsingWebGPU(): boolean {
403
- return this._rendererType === 'webgpu'
404
- }
405
-
406
- /** Returns true when a hardware-accelerated GPU renderer is active. */
407
- get isUsingGPURenderer(): boolean {
408
- return this._gpuRenderer !== null
409
- }
410
-
411
- // ==========================================================================
412
- // Canvas Management
413
- // ==========================================================================
414
-
415
- private _createCanvas(): HTMLCanvasElement {
416
- this._canvas = document.createElement('canvas')
417
- this._canvas.style.display = 'block'
418
- this._canvas.style.position = 'absolute'
419
- this._canvas.style.pointerEvents = 'none'
420
- this._canvasParent!.appendChild(this._canvas)
421
- return this._canvas
422
- }
423
-
424
- /**
425
- * Resize the canvas to given parameters. Auto-generated if values are omitted.
426
- */
427
- resize(
428
- width: number = 0,
429
- height: number = 0,
430
- top: number = 0,
431
- left: number = 0,
432
- force: boolean = this._video?.paused ?? false
433
- ): void {
434
- if ((!width || !height) && this._video) {
435
- const videoSize = getVideoPosition(this._video)
436
- let renderSize: { width: number; height: number }
437
-
438
- if (this._videoWidth) {
439
- const widthRatio = this._video.videoWidth / this._videoWidth
440
- const heightRatio = this._video.videoHeight / this._videoHeight
441
- renderSize = computeCanvasSize(
442
- (videoSize.width || 0) / widthRatio,
443
- (videoSize.height || 0) / heightRatio,
444
- this.prescaleFactor,
445
- this.prescaleHeightLimit,
446
- this.maxRenderHeight
447
- )
448
- } else {
449
- renderSize = computeCanvasSize(
450
- videoSize.width || 0,
451
- videoSize.height || 0,
452
- this.prescaleFactor,
453
- this.prescaleHeightLimit,
454
- this.maxRenderHeight
455
- )
456
- }
457
-
458
- width = renderSize.width
459
- height = renderSize.height
460
-
461
- if (this._canvasParent) {
462
- top = videoSize.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top)
463
- left = videoSize.x
464
- }
465
-
466
- this._canvas.style.width = videoSize.width + 'px'
467
- this._canvas.style.height = videoSize.height + 'px'
468
- }
469
-
470
- this._canvas.style.top = top + 'px'
471
- this._canvas.style.left = left + 'px'
472
-
473
- if (width > 0 && height > 0) {
474
- this._canvasctrl.width = width
475
- this._canvasctrl.height = height
476
- }
477
-
478
- // Update GPU renderer size if using a GPU renderer
479
- if (this._gpuRenderer && width > 0 && height > 0) {
480
- this._gpuRenderer.updateSize(width, height)
481
- }
482
-
483
- if (force && this.busy === false) {
484
- this.busy = true
485
- } else {
486
- force = false
487
- }
488
-
489
- this.sendMessage('canvas', {
490
- width,
491
- height,
492
- videoWidth: this._videoWidth || this._video?.videoWidth || 0,
493
- videoHeight: this._videoHeight || this._video?.videoHeight || 0,
494
- force
495
- })
496
- }
497
-
498
- // ==========================================================================
499
- // Video Management
500
- // ==========================================================================
501
-
502
- private _timeupdate(event: Event): void {
503
- const eventmap: Record<string, boolean> = {
504
- seeking: true,
505
- waiting: true,
506
- playing: false
507
- }
508
- const playing = eventmap[event.type]
509
- if (playing != null) this._playstate = playing
510
- this.setCurrentTime(this._video!.paused || this._playstate, this._video!.currentTime + this.timeOffset)
511
- }
512
-
513
- /**
514
- * Change the video to use as target for event listeners.
515
- */
516
- setVideo(video: HTMLVideoElement): void {
517
- if (video instanceof HTMLVideoElement) {
518
- this._removeListeners()
519
- this._video = video
520
-
521
- if (this._onDemandRender) {
522
- if (!this._destroyed && this._video === video) {
523
- ;(video as any).requestVideoFrameCallback(this._boundHandleRVFC)
524
- }
525
- } else {
526
- this._playstate = video.paused
527
-
528
- video.addEventListener('timeupdate', this._boundTimeUpdate, false)
529
- video.addEventListener('progress', this._boundTimeUpdate, false)
530
- video.addEventListener('waiting', this._boundTimeUpdate, false)
531
- video.addEventListener('seeking', this._boundTimeUpdate, false)
532
- video.addEventListener('playing', this._boundTimeUpdate, false)
533
- video.addEventListener('ratechange', this._boundSetRate, false)
534
- video.addEventListener('resize', this._boundResize, false)
535
- }
536
-
537
- if ('VideoFrame' in window) {
538
- video.addEventListener('loadedmetadata', this._boundUpdateColorSpace, false)
539
- if (video.readyState > 2) this._updateColorSpace()
540
- }
541
-
542
- if (video.videoWidth > 0) this.resize()
543
-
544
- if (typeof ResizeObserver !== 'undefined') {
545
- if (!this._ro) this._ro = new ResizeObserver(() => this.resize())
546
- this._ro.observe(video)
547
- }
548
- } else {
549
- this._error(new Error('Video element invalid!'))
550
- }
551
- }
552
-
553
- // ==========================================================================
554
- // Track Management
555
- // ==========================================================================
556
-
557
- /**
558
- * Overwrites the current subtitle content by URL.
559
- */
560
- setTrackByUrl(url: string): void {
561
- this.sendMessage('setTrackByUrl', { url })
562
- this._reAttachOffscreen()
563
- if (this._ctx) this._ctx.filter = 'none'
564
- }
565
-
566
- /**
567
- * Overwrites the current subtitle content.
568
- */
569
- setTrack(content: string | Uint8Array | ArrayBuffer): void {
570
- this.sendMessage('setTrack', { content }, AkariSub._getSubtitleTransfers(content))
571
- this._reAttachOffscreen()
572
- if (this._ctx) this._ctx.filter = 'none'
573
- }
574
-
575
- /**
576
- * Overwrites the current subtitle content with encrypted v2 payloads.
577
- * Decryption happens inside the AkariSub worker so plaintext ASS text is not
578
- * materialized in the main thread.
579
- */
580
- setEncryptedTrack(content: EncryptedSubtitleContent): void {
581
- this.sendMessage('setEncryptedTrack', { content }, AkariSub._getSubtitleTransfers(undefined, content))
582
- this._reAttachOffscreen()
583
- if (this._ctx) this._ctx.filter = 'none'
584
- }
585
-
586
- /**
587
- * Free currently used subtitle track.
588
- */
589
- freeTrack(): void {
590
- this.sendMessage('freeTrack')
591
- }
592
-
593
- // ==========================================================================
594
- // Playback Control
595
- // ==========================================================================
596
-
597
- /**
598
- * Sets the playback state of the media.
599
- */
600
- setIsPaused(isPaused: boolean): void {
601
- this.sendMessage('video', { isPaused })
602
- }
603
-
604
- /**
605
- * Sets the playback rate of the media.
606
- */
607
- setRate(rate: number): void {
608
- this.sendMessage('video', { rate })
609
- }
610
-
611
- /**
612
- * Sets the current time, playback state and rate of the subtitles.
613
- */
614
- setCurrentTime(isPaused?: boolean, currentTime?: number, rate?: number): void {
615
- this.sendMessage('video', {
616
- isPaused,
617
- currentTime,
618
- rate,
619
- colorSpace: this._videoColorSpace
620
- })
621
- }
622
-
623
- // ==========================================================================
624
- // Event Management
625
- // ==========================================================================
626
-
627
- /**
628
- * Create a new ASS event directly.
629
- */
630
- createEvent(event: Partial<ASSEvent>): void {
631
- this.sendMessage('createEvent', { event })
632
- }
633
-
634
- /**
635
- * Overwrite the data of the event with the specified index.
636
- */
637
- setEvent(event: Partial<ASSEvent>, index: number): void {
638
- this.sendMessage('setEvent', { event, index })
639
- }
640
-
641
- /**
642
- * Remove the event with the specified index.
643
- */
644
- removeEvent(index: number): void {
645
- this.sendMessage('removeEvent', { index })
646
- }
647
-
648
- /**
649
- * Get all ASS events.
650
- */
651
- async getEvents(): Promise<ASSEvent[]> {
652
- const data = await this._fetchFromWorker<{ events: ASSEvent[] }>({ target: 'getEvents' })
653
- return data.events ?? []
654
- }
655
-
656
- // ==========================================================================
657
- // Style Management
658
- // ==========================================================================
659
-
660
- /**
661
- * Set a style override.
662
- */
663
- styleOverride(style: Partial<ASSStyle>): void {
664
- this.sendMessage('styleOverride', { style })
665
- }
666
-
667
- /**
668
- * Disable style override.
669
- */
670
- disableStyleOverride(): void {
671
- this.sendMessage('disableStyleOverride')
672
- }
673
-
674
- /**
675
- * Create a new ASS style directly.
676
- */
677
- createStyle(style: Partial<ASSStyle>): void {
678
- this.sendMessage('createStyle', { style })
679
- }
680
-
681
- /**
682
- * Overwrite the data of the style with the specified index.
683
- */
684
- setStyle(style: Partial<ASSStyle>, index: number): void {
685
- this.sendMessage('setStyle', { style, index })
686
- }
687
-
688
- /**
689
- * Remove the style with the specified index.
690
- */
691
- removeStyle(index: number): void {
692
- this.sendMessage('removeStyle', { index })
693
- }
694
-
695
- /**
696
- * Get all ASS styles.
697
- */
698
- async getStyles(): Promise<ASSStyle[]> {
699
- const data = await this._fetchFromWorker<{ styles: ASSStyle[] }>({ target: 'getStyles' })
700
- return data.styles ?? []
701
- }
702
-
703
- // ==========================================================================
704
- // Font Management
705
- // ==========================================================================
706
-
707
- /**
708
- * Adds a font to the renderer.
709
- */
710
- addFont(font: string | Uint8Array): void {
711
- this.sendMessage('addFont', { font })
712
- }
713
-
714
- /**
715
- * Changes the font family of the default font.
716
- */
717
- setDefaultFont(font: string): void {
718
- this.sendMessage('defaultFont', { font })
719
- }
720
-
721
- // ==========================================================================
722
- // Performance Stats
723
- // ==========================================================================
724
-
725
- /**
726
- * Get real-time performance statistics.
727
- */
728
- async getStats(): Promise<PerformanceStats> {
729
- const data = await this._fetchFromWorker<{ stats: Partial<PerformanceStats> }>({ target: 'getStats' })
730
- const stats = data.stats
731
- return {
732
- framesRendered: stats.framesRendered ?? 0,
733
- framesDropped: stats.framesDropped ?? 0,
734
- avgRenderTime: stats.avgRenderTime ?? 0,
735
- maxRenderTime: stats.maxRenderTime ?? 0,
736
- minRenderTime: stats.minRenderTime ?? 0,
737
- lastRenderTime: stats.lastRenderTime ?? 0,
738
- pendingRenders: stats.pendingRenders ?? 0,
739
- totalEvents: stats.totalEvents ?? 0,
740
- cacheHits: stats.cacheHits ?? 0,
741
- cacheMisses: stats.cacheMisses ?? 0,
742
- renderFps: stats.avgRenderTime && stats.avgRenderTime > 0 ? Math.round(1000 / stats.avgRenderTime) : 0,
743
- usingWorker: true,
744
- offscreenRender: this._offscreenRender,
745
- onDemandRender: this._onDemandRender
746
- }
747
- }
748
-
749
- /**
750
- * Reset performance statistics counters.
751
- */
752
- async resetStats(): Promise<void> {
753
- await this._fetchFromWorker({ target: 'resetStats' })
754
- }
755
-
756
- /**
757
- * Get event count
758
- */
759
- async getEventCount(): Promise<number> {
760
- const data = await this._fetchFromWorker<{ count: number }>({ target: 'getEventCount' })
761
- return data.count
762
- }
763
-
764
- /**
765
- * Get style count
766
- */
767
- async getStyleCount(): Promise<number> {
768
- const data = await this._fetchFromWorker<{ count: number }>({ target: 'getStyleCount' })
769
- return data.count
770
- }
771
-
772
- // ==========================================================================
773
- // Private Methods
774
- // ==========================================================================
775
-
776
- private _sendLocalFont(name: string): void {
777
- try {
778
- ; (globalThis as any).queryLocalFonts().then((fontData: any[]) => {
779
- const font = fontData?.find((obj: any) => obj.fullName.toLowerCase() === name)
780
- if (font) {
781
- font.blob().then((blob: Blob) => {
782
- blob.arrayBuffer().then((buffer: ArrayBuffer) => {
783
- this.addFont(new Uint8Array(buffer))
784
- })
785
- })
786
- }
787
- })
788
- } catch (e) {
789
- console.warn('Local fonts API:', e)
790
- }
791
- }
792
-
793
- private _getLocalFont(data: { font: string }): void {
794
- try {
795
- if (navigator?.permissions?.query) {
796
- ; (navigator.permissions.query as any)({ name: 'local-fonts' }).then((permission: any) => {
797
- if (permission.state === 'granted') {
798
- this._sendLocalFont(data.font)
799
- }
800
- })
801
- } else {
802
- this._sendLocalFont(data.font)
803
- }
804
- } catch (e) {
805
- console.warn('Local fonts API:', e)
806
- }
807
- }
808
-
809
- private _unbusy(): void {
810
- this._observeDemandCompletion()
811
-
812
- if (this._pendingDemandTimes.length > 0) {
813
- if (this._pendingDemandTimes.length > 1) {
814
- const latestDemand = this._pendingDemandTimes[this._pendingDemandTimes.length - 1]
815
- this._pendingDemandTimes.length = 0
816
- this._pendingDemandTimes.push(latestDemand)
817
- }
818
-
819
- const nextDemand = this._pendingDemandTimes.shift()
820
- if (nextDemand) {
821
- this._demandRender(nextDemand)
822
- return
823
- }
824
- }
825
-
826
- this.busy = false
827
- }
828
-
829
- private _markDemandDispatched(): void {
830
- if (!this._onDemandRender) return
831
- this._activeDemandStartedAt = performance.now()
832
- }
833
-
834
- private _observeDemandCompletion(): void {
835
- if (!this._onDemandRender || this._activeDemandStartedAt === 0) return
836
-
837
- const elapsed = performance.now() - this._activeDemandStartedAt
838
- this._activeDemandStartedAt = 0
839
-
840
- if (!Number.isFinite(elapsed) || elapsed <= 0) return
841
-
842
- this._smoothedDemandLatencyMs = this._smoothedDemandLatencyMs <= 0
843
- ? elapsed
844
- : this._smoothedDemandLatencyMs * 0.75 + elapsed * 0.25
845
- }
846
-
847
- private _getDemandPipelineLeadSeconds(now: number, metadata: VideoFrameCallbackMetadata): number {
848
- const expectedDisplayTime = metadata.expectedDisplayTime ?? metadata.presentationTime ?? now
849
- const displayLeadSeconds = Math.max(0, expectedDisplayTime - now) / 1000
850
-
851
- return Math.max(0, this._smoothedDemandLatencyMs / 1000 - displayLeadSeconds)
852
- }
853
-
854
- private _enqueueDemand(metadata: { mediaTime: number; width: number; height: number }): void {
855
- const queue = this._pendingDemandTimes
856
-
857
- if (queue.length > 0) {
858
- const lastQueued = queue[queue.length - 1]
859
- if (Math.abs(lastQueued.mediaTime - metadata.mediaTime) > 0.25) {
860
- queue.length = 0
861
- }
862
- }
863
-
864
- if (queue.length >= AkariSub.MAX_PENDING_DEMANDS) {
865
- queue.shift()
866
- }
867
-
868
- queue.push(metadata)
869
- }
870
-
871
- private _handleRVFC(now: number, metadata: VideoFrameCallbackMetadata): void {
872
- if (this._destroyed) return
873
-
874
- // Get the video's playback rate to correctly scale time offsets
875
- const playbackRate = this._video?.playbackRate ?? 1
876
- const pipelineLeadSeconds = this._getDemandPipelineLeadSeconds(now, metadata)
877
- const renderTime = metadata.mediaTime + (pipelineLeadSeconds + this.renderAhead) * playbackRate
878
-
879
- const demandData = {
880
- mediaTime: renderTime,
881
- width: metadata.width,
882
- height: metadata.height
883
- }
884
-
885
- if (!this._workerReady) {
886
- this._enqueueDemand(demandData)
887
- ; (this._video as any).requestVideoFrameCallback(this._boundHandleRVFC)
888
- return
889
- }
890
-
891
- if (this.busy) {
892
- this._enqueueDemand(demandData)
893
- } else {
894
- this.busy = true
895
- this._demandRender(demandData)
896
- }
897
-
898
- ; (this._video as any).requestVideoFrameCallback(this._boundHandleRVFC)
899
- }
900
-
901
- private _demandRender(metadata: { mediaTime: number; width: number; height: number }): void {
902
- if (metadata.width !== this._videoWidth || metadata.height !== this._videoHeight) {
903
- this._videoWidth = metadata.width
904
- this._videoHeight = metadata.height
905
- this.resize()
906
- }
907
-
908
- this._markDemandDispatched()
909
- this.sendMessage('demand', { time: metadata.mediaTime + this.timeOffset })
910
- }
911
-
912
- private _detachOffscreen(): void {
913
- if (!this._offscreenRender || this._ctx) return
914
-
915
- this._canvas.remove()
916
- this._createCanvas()
917
- this._canvasctrl = this._canvas
918
- this._ctx = this._canvasctrl.getContext('2d', { alpha: true, desynchronized: true })
919
- this.sendMessage('detachOffscreen')
920
- this.busy = false
921
- this._activeDemandStartedAt = 0
922
- this._pendingDemandTimes.length = 0
923
- this.resize(0, 0, 0, 0, true)
924
- }
925
-
926
- private _reAttachOffscreen(): void {
927
- if (!this._offscreenRender || !this._ctx) return
928
-
929
- this._canvas.remove()
930
- this._createCanvas()
931
- this._canvasctrl = (this._canvas as any).transferControlToOffscreen()
932
- this._ctx = false
933
- this.sendMessage('offscreenCanvas', {}, [this._canvasctrl as OffscreenCanvas])
934
- this.resize(0, 0, 0, 0, true)
935
- }
936
-
937
- private _updateColorSpace(): void {
938
- ; (this._video as any).requestVideoFrameCallback(() => {
939
- try {
940
- const frame = new (globalThis as any).VideoFrame(this._video)
941
- this._videoColorSpace = webYCbCrMap[frame.colorSpace.matrix] ?? null
942
- frame.close()
943
- this.sendMessage('getColorSpace')
944
- } catch (e) {
945
- console.warn(e)
946
- }
947
- })
948
- }
949
-
950
- private _verifyColorSpace(data: {
951
- subtitleColorSpace: SubtitleColorSpace
952
- videoColorSpace?: WebYCbCrColorSpace | null
953
- }): void {
954
- const { subtitleColorSpace, videoColorSpace = this._videoColorSpace } = data
955
-
956
- if (!subtitleColorSpace || !videoColorSpace) return
957
- if (subtitleColorSpace === videoColorSpace) return
958
-
959
- this._detachOffscreen()
960
-
961
- const matrix = colorMatrixConversionMap[subtitleColorSpace]?.[videoColorSpace]
962
- if (matrix && this._ctx) {
963
- this._ctx.filter = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='f'><feColorMatrix type='matrix' values='${matrix} 0 0 0 0 0 1 0'/></filter></svg>#f")`
964
- }
965
- }
966
-
967
- private _render(data: {
968
- images: RenderImage[]
969
- asyncRender: boolean
970
- times: RenderTimes
971
- width: number
972
- height: number
973
- colorSpace: SubtitleColorSpace
974
- }): void {
975
- try {
976
- const dataWidth = data.width
977
- const dataHeight = data.height
978
-
979
- if (this.debug) {
980
- data.times.IPCTime = Date.now() - (data.times.JSRenderTime || 0)
981
- }
982
-
983
- // Check if canvas size changed
984
- const sizeChanged = this._canvasctrl.width !== dataWidth || this._canvasctrl.height !== dataHeight
985
- if (sizeChanged) {
986
- this._canvasctrl.width = dataWidth
987
- this._canvasctrl.height = dataHeight
988
- this._lastRenderWidth = dataWidth
989
- this._lastRenderHeight = dataHeight
990
-
991
- // Update GPU renderer size if canvas size changed
992
- if (this._gpuRenderer) {
993
- this._gpuRenderer.updateSize(dataWidth, dataHeight)
994
- }
995
-
996
- this._verifyColorSpace({ subtitleColorSpace: data.colorSpace })
997
- }
998
-
999
- // Use GPU renderer (WebGPU or WebGL2) if available
1000
- if (this._gpuRenderer) {
1001
- this._renderGPU(data)
1002
- return
1003
- }
1004
-
1005
- if (!this._ctx) return
1006
-
1007
- const ctx = this._ctx
1008
- const images = data.images
1009
- const imageCount = images.length
1010
-
1011
- ctx.clearRect(0, 0, dataWidth, dataHeight)
1012
-
1013
- if (data.asyncRender) {
1014
- for (let i = 0; i < imageCount; i++) {
1015
- const image = images[i]
1016
- if (image.image) {
1017
- ctx.drawImage(image.image as ImageBitmap, image.x, image.y)
1018
- ;(image.image as ImageBitmap).close()
1019
- }
1020
- }
1021
- } else {
1022
- const hasAlphaBug = AkariSub._hasAlphaBug ?? false
1023
-
1024
- for (let i = 0; i < imageCount; i++) {
1025
- const image = images[i]
1026
- if (image.image) {
1027
- const imgW = image.w
1028
- const imgH = image.h
1029
-
1030
- const rawImage = image.image
1031
- const rawData = rawImage instanceof Uint8ClampedArray
1032
- ? rawImage
1033
- : rawImage instanceof Uint8Array
1034
- ? new Uint8ClampedArray(rawImage.buffer, rawImage.byteOffset, rawImage.byteLength)
1035
- : new Uint8ClampedArray(rawImage as ArrayBuffer)
1036
- const fixedData = fixAlpha(rawData, hasAlphaBug)
1037
- ctx.putImageData(new ImageData(fixedData as Uint8ClampedArray<ArrayBuffer>, imgW, imgH), image.x, image.y)
1038
- }
1039
- }
1040
- }
1041
-
1042
- if (this.debug) {
1043
- data.times.JSRenderTime = Date.now() - (data.times.JSRenderTime || 0) - (data.times.IPCTime || 0)
1044
- let total = 0
1045
- const count = data.times.bitmaps || imageCount
1046
- delete data.times.bitmaps
1047
-
1048
- for (const key in data.times) {
1049
- total += (data.times as any)[key] || 0
1050
- }
1051
-
1052
- console.log('Bitmaps: ' + count + ' Total: ' + (total | 0) + 'ms', data.times)
1053
- }
1054
- } finally {
1055
- this._unbusy()
1056
- }
1057
- }
1058
-
1059
- private _renderGPU(data: { images: RenderImage[]; asyncRender: boolean; times: RenderTimes }): void {
1060
- const renderer = this._gpuRenderer
1061
- if (!renderer) return
1062
-
1063
- if (data.images.length === 0) {
1064
- renderer.clear()
1065
- return
1066
- }
1067
-
1068
- if (data.asyncRender) {
1069
- // For async render mode with ImageBitmaps
1070
- const bitmapImages = this._gpuBitmapImages
1071
- let bitmapCount = 0
1072
-
1073
- for (let i = 0; i < data.images.length; i++) {
1074
- const img = data.images[i]
1075
- if (!(img.image instanceof ImageBitmap)) continue
1076
-
1077
- const target = bitmapImages[bitmapCount] || (bitmapImages[bitmapCount] = { image: img.image, x: 0, y: 0 })
1078
- target.image = img.image
1079
- target.x = img.x
1080
- target.y = img.y
1081
- bitmapCount++
1082
- }
1083
-
1084
- bitmapImages.length = bitmapCount
1085
-
1086
- renderer.renderBitmaps(bitmapImages, this._canvasctrl.width, this._canvasctrl.height)
1087
-
1088
- // Close ImageBitmaps after rendering
1089
- for (const img of data.images) {
1090
- if (img.image instanceof ImageBitmap) {
1091
- img.image.close()
1092
- }
1093
- }
1094
- } else {
1095
- // For non-async render mode with ArrayBuffer data
1096
- renderer.render(data.images, this._canvasctrl.width, this._canvasctrl.height)
1097
- }
1098
-
1099
- if (this.debug) {
1100
- data.times.JSRenderTime = Date.now() - (data.times.JSRenderTime || 0) - (data.times.IPCTime || 0)
1101
- let total = 0
1102
- const count = (data.times as any).bitmaps || data.images.length
1103
- delete (data.times as any).bitmaps
1104
-
1105
- for (const key in data.times) {
1106
- total += (data.times as any)[key] || 0
1107
- }
1108
-
1109
- console.log(`[${this._rendererType.toUpperCase()}] Bitmaps: ` + count + ' Total: ' + (total | 0) + 'ms', data.times)
1110
- }
1111
- }
1112
-
1113
- private _ready(): void {
1114
- this._workerReady = true
1115
- this._init()
1116
-
1117
- if (this._onDemandRender && this._video) {
1118
- this.setCurrentTime(this._video.paused, this._video.currentTime + this.timeOffset, this._video.playbackRate)
1119
-
1120
- const pending = this._pendingDemandTimes.length > 0
1121
- ? this._pendingDemandTimes[this._pendingDemandTimes.length - 1]
1122
- : {
1123
- mediaTime: this._video.currentTime + this.renderAhead * (this._video.playbackRate || 1),
1124
- width: this._video.videoWidth,
1125
- height: this._video.videoHeight
1126
- }
1127
-
1128
- this._pendingDemandTimes.length = 0
1129
- this.busy = true
1130
- this._demandRender(pending)
1131
- }
1132
-
1133
- this.dispatchEvent(new CustomEvent('ready'))
1134
- }
1135
-
1136
- /**
1137
- * Handler for partial_ready message from worker.
1138
- * Emitted early for large subtitle files to allow playback to start
1139
- * while font loading and track parsing continues.
1140
- */
1141
- private _partial_ready(): void {
1142
- this.dispatchEvent(new CustomEvent('partial_ready'))
1143
- }
1144
-
1145
- private _trackReady(): void {
1146
- this.dispatchEvent(new CustomEvent('trackReady'))
1147
- }
1148
-
1149
- /**
1150
- * Send data and execute function in the worker.
1151
- */
1152
- async sendMessage(target: string, data: Record<string, any> = {}, transferable?: Transferable[]): Promise<void> {
1153
- await this._loaded
1154
-
1155
- if (transferable) {
1156
- this._worker.postMessage({ target, transferable, ...data }, [...transferable])
1157
- } else {
1158
- this._worker.postMessage({ target, ...data })
1159
- }
1160
- }
1161
-
1162
- private _fetchFromWorker<T = any>(workerOptions: { target: string }): Promise<T> {
1163
- return new Promise((resolve, reject) => {
1164
- try {
1165
- const target = workerOptions.target
1166
-
1167
- const timeout = setTimeout(() => {
1168
- cleanup()
1169
- reject(new Error('Error: Timeout while trying to fetch ' + target))
1170
- }, 5000)
1171
-
1172
- const handleMessage = (event: MessageEvent) => {
1173
- if (event.data.target === target) {
1174
- cleanup()
1175
- resolve(event.data as T)
1176
- }
1177
- }
1178
-
1179
- const handleError = (event: ErrorEvent | Error) => {
1180
- cleanup()
1181
- reject(event instanceof Error ? event : event.error || new Error('Worker error'))
1182
- }
1183
-
1184
- const cleanup = () => {
1185
- this._worker.removeEventListener('message', handleMessage)
1186
- this._worker.removeEventListener('error', handleError as any)
1187
- clearTimeout(timeout)
1188
- }
1189
-
1190
- this._worker.addEventListener('message', handleMessage)
1191
- this._worker.addEventListener('error', handleError as any)
1192
-
1193
- this._worker.postMessage(workerOptions)
1194
- } catch (error) {
1195
- reject(error)
1196
- }
1197
- })
1198
- }
1199
-
1200
- private _console(data: { content: string; command: string }): void {
1201
- ; (console as any)[data.command].apply(console, JSON.parse(data.content))
1202
- }
1203
-
1204
- private _onmessage(event: MessageEvent): void {
1205
- const target = event.data.target
1206
- if (target === 'error') {
1207
- this._error(event.data.error || 'Unknown worker error')
1208
- return
1209
- }
1210
- const handler = (this as any)['_' + target]
1211
- if (handler) {
1212
- handler.call(this, event.data)
1213
- }
1214
- }
1215
-
1216
- private _error(err: Error | ErrorEvent | string): Error {
1217
- const error =
1218
- err instanceof Error
1219
- ? err
1220
- : err instanceof ErrorEvent
1221
- ? err.error || new Error(err.message)
1222
- : new Error(String(err))
1223
-
1224
- const event = err instanceof Event ? new ErrorEvent(err.type, err) : new ErrorEvent('error', { error })
1225
-
1226
- this.dispatchEvent(event)
1227
- console.error(error)
1228
-
1229
- return error
1230
- }
1231
-
1232
- private _removeListeners(): void {
1233
- if (this._video) {
1234
- if (this._ro) this._ro.unobserve(this._video)
1235
- if (this._ctx) this._ctx.filter = 'none'
1236
-
1237
- this._video.removeEventListener('timeupdate', this._boundTimeUpdate)
1238
- this._video.removeEventListener('progress', this._boundTimeUpdate)
1239
- this._video.removeEventListener('waiting', this._boundTimeUpdate)
1240
- this._video.removeEventListener('seeking', this._boundTimeUpdate)
1241
- this._video.removeEventListener('playing', this._boundTimeUpdate)
1242
- this._video.removeEventListener('ratechange', this._boundSetRate)
1243
- this._video.removeEventListener('resize', this._boundResize)
1244
- this._video.removeEventListener('loadedmetadata', this._boundUpdateColorSpace)
1245
- }
1246
- }
1247
-
1248
- /**
1249
- * Destroy the object, worker, listeners and all data.
1250
- */
1251
- destroy(err?: Error | string): Error | undefined {
1252
- const error = err ? this._error(err) : undefined
1253
-
1254
- if (this._video && this._canvasParent) {
1255
- this._video.parentNode?.removeChild(this._canvasParent)
1256
- }
1257
-
1258
- // Clean up GPU renderer
1259
- if (this._gpuRenderer) {
1260
- this._gpuRenderer.destroy()
1261
- this._gpuRenderer = null
1262
- this._rendererType = 'canvas2d'
1263
- }
1264
-
1265
- this._destroyed = true
1266
- this._removeListeners()
1267
- this.sendMessage('destroy')
1268
- this._worker?.terminate()
1269
-
1270
- return error
1271
- }
1272
- }