jassub 2.2.6 → 2.3.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.
@@ -1,17 +1,15 @@
1
1
  /* eslint-disable camelcase */
2
2
  import { finalizer } from 'abslink'
3
3
  import { expose } from 'abslink/w3c'
4
+ import { queryRemoteFonts } from 'lfa-ponyfill'
4
5
 
5
6
  import WASM from '../wasm/jassub-worker.js'
6
7
 
7
- import { libassYCbCrMap, read_, readAsync, _applyKeys } from './util'
8
- import { WebGL2Renderer } from './webgl-renderer'
9
- // import { WebGPURenderer } from './webgpu-renderer'
10
-
11
- import type { ASSEvent, ASSImage, ASSStyle } from '../jassub'
12
- import type { JASSUB, MainModule } from '../wasm/types.js'
8
+ import { _applyKeys, _fetch, fetchtext, IS_FIREFOX, LIBASS_YCBCR_MAP, WEIGHT_MAP, type ASSEvent, type ASSImage, type ASSStyle, type WeightValue } from './util.ts'
9
+ import { WebGL2Renderer } from './webgl-renderer.ts'
13
10
 
14
- const IS_FIREFOX = navigator.userAgent.toLowerCase().includes('firefox')
11
+ import type { JASSUB, MainModule } from '../wasm/types.d.ts'
12
+ // import { WebGPURenderer } from './webgpu-renderer'
15
13
 
16
14
  declare const self: DedicatedWorkerGlobalScope &
17
15
  typeof globalThis & {
@@ -27,11 +25,11 @@ interface opts {
27
25
  subContent: string | null
28
26
  fonts: Array<string | Uint8Array>
29
27
  availableFonts: Record<string, Uint8Array | string>
30
- fallbackFont: string
28
+ defaultFont: string
31
29
  debug: boolean
32
30
  libassMemoryLimit: number
33
31
  libassGlyphLimit: number
34
- useLocalFonts: boolean
32
+ queryFonts: 'local' | 'localandremote' | false
35
33
  }
36
34
 
37
35
  export class ASSRenderer {
@@ -43,19 +41,16 @@ export class ASSRenderer {
43
41
  _gpurender = new WebGL2Renderer()
44
42
 
45
43
  debug = false
46
- useLocalFonts = false
47
- _availableFonts: Record<string, Uint8Array | string> = {}
48
- _fontMap: Record<string, boolean> = {}
49
- _fontId = 0
50
44
 
51
45
  _ready
52
- _getFont
53
46
 
54
- constructor (data: opts, getFont: (font: string) => Promise<void>) {
55
- this._availableFonts = data.availableFonts
47
+ constructor (data: opts, getFont: (font: string, weight: WeightValue) => Promise<Uint8Array<ArrayBuffer> | undefined>) {
48
+ // remove case sensitivity
49
+ this._availableFonts = Object.fromEntries(Object.entries(data.availableFonts).map(([k, v]) => [k.trim().toLowerCase(), v]))
56
50
  this.debug = data.debug
57
- this.useLocalFonts = data.useLocalFonts
51
+ this.queryFonts = data.queryFonts
58
52
  this._getFont = getFont
53
+ this._defaultFont = data.defaultFont.trim().toLowerCase()
59
54
 
60
55
  // hack, we want custom WASM URLs
61
56
  const _fetch = globalThis.fetch
@@ -66,7 +61,7 @@ export class ASSRenderer {
66
61
  if (data.name === 'offscreenCanvas') {
67
62
  // await this._ready // needed for webGPU
68
63
  this._offCanvas = data.ctrl
69
- this._gpurender.setCanvas(this._offCanvas!, this._offCanvas!.width, this._offCanvas!.height)
64
+ this._gpurender.setCanvas(this._offCanvas!)
70
65
  removeEventListener('message', handleMessage)
71
66
  }
72
67
  }
@@ -76,26 +71,20 @@ export class ASSRenderer {
76
71
  // powerPreference: 'high-performance'
77
72
  // }).then(adapter => adapter?.requestDevice())
78
73
 
79
- this._ready = (WASM({ __url: data.wasmUrl }) as Promise<MainModule>).then(async Module => {
80
- // eslint-disable-next-line @typescript-eslint/unbound-method
81
- this._malloc = Module._malloc
74
+ // eslint-disable-next-line @typescript-eslint/unbound-method
75
+ this._ready = (WASM({ __url: data.wasmUrl, __out: (log: string) => this._log(log) }) as Promise<MainModule>).then(async ({ _malloc, JASSUB }) => {
76
+ this._malloc = _malloc
82
77
 
83
- const fallbackFont = data.fallbackFont.toLowerCase()
84
- this._wasm = new Module.JASSUB(data.width, data.height, fallbackFont)
78
+ this._wasm = new JASSUB(data.width, data.height, this._defaultFont)
85
79
  // Firefox seems to have issues with multithreading in workers
86
- // a worker inside a worker does not recive messages properly
80
+ // a worker inside a worker does not recieve messages properly
87
81
  this._wasm.setThreads(!IS_FIREFOX && self.crossOriginIsolated ? Math.min(Math.max(1, navigator.hardwareConcurrency - 2), 8) : 1)
88
82
 
89
- if (fallbackFont) this._findAvailableFonts(fallbackFont)
90
-
91
- const subContent = data.subContent ?? read_(data.subUrl!)
92
-
93
- for (const font of data.fonts) this._asyncWrite(font)
83
+ this._loadInitialFonts(data.fonts)
94
84
 
95
- this._wasm.createTrackMem(subContent)
96
- this._processAvailableFonts(subContent)
85
+ this._wasm.createTrackMem(data.subContent ?? await fetchtext(data.subUrl!))
97
86
 
98
- this._subtitleColorSpace = libassYCbCrMap[this._wasm.trackColorSpace]
87
+ this._subtitleColorSpace = LIBASS_YCBCR_MAP[this._wasm.trackColorSpace]
99
88
 
100
89
  if (data.libassMemoryLimit > 0 || data.libassGlyphLimit > 0) {
101
90
  this._wasm.setMemoryLimits(data.libassGlyphLimit || 0, data.libassMemoryLimit || 0)
@@ -111,10 +100,6 @@ export class ASSRenderer {
111
100
  return this._ready
112
101
  }
113
102
 
114
- addFont (fontOrURL: Uint8Array | string) {
115
- this._asyncWrite(fontOrURL)
116
- }
117
-
118
103
  createEvent (event: ASSEvent) {
119
104
  _applyKeys(event, this._wasm.getEvent(this._wasm.allocEvent())!)
120
105
  }
@@ -169,23 +154,18 @@ export class ASSRenderer {
169
154
  this._wasm.disableStyleOverride()
170
155
  }
171
156
 
172
- setDefaultFont (fontName: string) {
173
- this._wasm.setDefaultFont(fontName)
174
- }
175
-
176
157
  setTrack (content: string) {
177
158
  this._wasm.createTrackMem(content)
178
- this._processAvailableFonts(content)
179
159
 
180
- this._subtitleColorSpace = libassYCbCrMap[this._wasm.trackColorSpace]!
160
+ this._subtitleColorSpace = LIBASS_YCBCR_MAP[this._wasm.trackColorSpace]!
181
161
  }
182
162
 
183
163
  freeTrack () {
184
164
  this._wasm.removeTrack()
185
165
  }
186
166
 
187
- setTrackByUrl (url: string) {
188
- this.setTrack(read_(url))
167
+ async setTrackByUrl (url: string) {
168
+ this.setTrack(await fetchtext(url))
189
169
  }
190
170
 
191
171
  _checkColorSpace () {
@@ -193,58 +173,116 @@ export class ASSRenderer {
193
173
  this._gpurender.setColorMatrix(this._subtitleColorSpace, this._videoColorSpace)
194
174
  }
195
175
 
196
- _findAvailableFonts (font: string) {
197
- font = font.trim().toLowerCase()
198
-
199
- if (font[0] === '@') font = font.substring(1)
176
+ _defaultFont
177
+ setDefaultFont (fontName: string) {
178
+ this._defaultFont = fontName.trim().toLowerCase()
179
+ this._wasm.setDefaultFont(this._defaultFont)
180
+ }
200
181
 
201
- if (this._fontMap[font]) return
182
+ _log (log: string) {
183
+ console.debug(log)
184
+ const match = log.match(/JASSUB: fontselect: Using default font family: \(([^,]+), (\d{1,4}), \d\)/)
185
+ if (match) {
186
+ this._findAvailableFont(match[1]!.trim().toLowerCase(), WEIGHT_MAP[parseInt(match[2]!, 10) / 100 - 1])
187
+ } else if (log.startsWith('JASSUB: fontselect: failed to find any fallback with glyph 0x0 for font:')) {
188
+ this._findAvailableFont(this._defaultFont)
189
+ }
190
+ }
202
191
 
203
- this._fontMap[font] = true
192
+ async addFonts (fontOrURLs: Array<Uint8Array | string>) {
193
+ if (!fontOrURLs.length) return
194
+ const strings: string[] = []
195
+ const uint8s: Uint8Array[] = []
204
196
 
205
- if (!this._availableFonts[font]) {
206
- if (this.useLocalFonts) this._getFont(font)
207
- } else {
208
- this._asyncWrite(this._availableFonts[font]!)
197
+ for (const fontOrURL of fontOrURLs) {
198
+ if (typeof fontOrURL === 'string') {
199
+ strings.push(fontOrURL)
200
+ } else {
201
+ uint8s.push(fontOrURL)
202
+ }
209
203
  }
204
+ if (uint8s.length) this._allocFonts(uint8s)
205
+
206
+ // this isn't batched like uint8s because software like jellyfin exists, which loads 50+ fonts over the network which takes time...
207
+ // is connection exhaustion a concern here?
208
+ await Promise.allSettled(strings.map(url => this._asyncWrite(url)))
209
+ }
210
+
211
+ // we don't want to run _findAvailableFont before initial fonts are loaded
212
+ // because it could duplicate fonts
213
+ _loadedInitialFonts = false
214
+ async _loadInitialFonts (fontOrURLs: Array<Uint8Array | string>) {
215
+ await this.addFonts(fontOrURLs)
216
+ this._loadedInitialFonts = true
210
217
  }
211
218
 
212
- _asyncWrite (font: Uint8Array | string) {
213
- if (typeof font === 'string') {
214
- readAsync(font, fontData => {
215
- this._allocFont(new Uint8Array(fontData))
216
- }, console.error)
217
- } else {
218
- this._allocFont(font)
219
+ _getFont
220
+ _availableFonts: Record<string, Uint8Array | string> = {}
221
+ _checkedFonts = new Set<string>()
222
+ async _findAvailableFont (fontName: string, weight?: WeightValue) {
223
+ if (!this._loadedInitialFonts) return
224
+
225
+ // Roboto Medium, null -> Roboto, Medium
226
+ // Roboto Medium, Medium -> Roboto, Medium
227
+ // Roboto, null -> Roboto, Regular
228
+ // italic is not handled I guess
229
+ for (const _weight of WEIGHT_MAP) {
230
+ // check if fontname has this weight name in it, if yes remove it
231
+ if (fontName.includes(_weight)) {
232
+ fontName = fontName.replace(_weight, '').trim()
233
+ weight ??= _weight
234
+ break
235
+ }
236
+ }
237
+
238
+ weight ??= 'regular'
239
+
240
+ const key = fontName + ' ' + weight
241
+ if (this._checkedFonts.has(key)) return
242
+ this._checkedFonts.add(key)
243
+
244
+ try {
245
+ const font = this._availableFonts[key] ?? this._availableFonts[fontName] ?? await this._queryLocalFont(fontName, weight) ?? await this._queryRemoteFont(fontName, key)
246
+ if (font) return await this.addFonts([font])
247
+ } catch (e) {
248
+ console.warn('Error querying font', fontName, weight, e)
219
249
  }
220
250
  }
221
251
 
222
- // TODO: this should re-draw last frame!
223
- _allocFont (uint8: Uint8Array) {
224
- const ptr = this._malloc(uint8.byteLength)
225
- self.HEAPU8RAW.set(uint8, ptr)
226
- this._wasm.addFont('font-' + (this._fontId++), ptr, uint8.byteLength)
227
- this._wasm.reloadFonts()
252
+ queryFonts
253
+ async _queryLocalFont (fontName: string, weight: WeightValue) {
254
+ if (!this.queryFonts) return
255
+ return await this._getFont(fontName, weight)
228
256
  }
229
257
 
230
- _processAvailableFonts (content: string) {
231
- if (!this._availableFonts) return
258
+ async _queryRemoteFont (fontName: string, postscriptName: string) {
259
+ if (this.queryFonts !== 'localandremote') return
232
260
 
233
- for (const { FontName } of this.getStyles()) {
234
- this._findAvailableFonts(FontName)
235
- }
261
+ const fontData = await queryRemoteFonts({ postscriptNames: [postscriptName, fontName] })
262
+ if (!fontData.length) return
263
+ const blob = await fontData[0]!.blob()
264
+ return new Uint8Array(await blob.arrayBuffer())
265
+ }
236
266
 
237
- const regex = /\\fn([^\\}]*?)[\\}]/g
238
- let matches
239
- while ((matches = regex.exec(content)) !== null) {
240
- this._findAvailableFonts(matches[1]!)
241
- }
267
+ async _asyncWrite (font: string) {
268
+ const res = await _fetch(font)
269
+ this._allocFonts([new Uint8Array(await res.arrayBuffer())])
242
270
  }
243
271
 
244
- _canvas (width: number, height: number, videoWidth: number, videoHeight: number) {
245
- if (this._offCanvas && this._gpurender) this._gpurender.setCanvas(this._offCanvas, width, height)
272
+ _fontId = 0
273
+ _allocFonts (uint8s: Uint8Array[]) {
274
+ // TODO: this should re-draw last frame!
275
+ for (const uint8 of uint8s) {
276
+ const ptr = this._malloc(uint8.byteLength)
277
+ self.HEAPU8RAW.set(uint8, ptr)
278
+ this._wasm.addFont('font-' + (this._fontId++), ptr, uint8.byteLength)
279
+ }
280
+ this._wasm.reloadFonts()
281
+ }
246
282
 
283
+ _resizeCanvas (width: number, height: number, videoWidth: number, videoHeight: number) {
247
284
  this._wasm.resizeCanvas(width, height, videoWidth, videoHeight)
285
+ this._gpurender.resizeCanvas(width, height)
248
286
  }
249
287
 
250
288
  async [finalizer] () {
@@ -258,11 +296,11 @@ export class ASSRenderer {
258
296
  this._availableFonts = {}
259
297
  }
260
298
 
261
- _draw (time: number, force = false) {
299
+ _draw (time: number, repaint = false) {
262
300
  if (!this._offCanvas || !this._gpurender) return
263
301
 
264
- const result: ASSImage = this._wasm.rawRender(time, Number(force))!
265
- if (this._wasm.changed === 0 && !force) return
302
+ const result = this._wasm.rawRender(time, Number(repaint))!
303
+ if (this._wasm.changed === 0 && !repaint) return
266
304
 
267
305
  const bitmaps: ASSImage[] = []
268
306