jassub 2.2.7 → 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
@@ -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!)
83
+ this._loadInitialFonts(data.fonts)
92
84
 
93
- for (const font of data.fonts) this._asyncWrite(font)
85
+ this._wasm.createTrackMem(data.subContent ?? await fetchtext(data.subUrl!))
94
86
 
95
- this._wasm.createTrackMem(subContent)
96
- this._processAvailableFonts(subContent)
97
-
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,52 +173,111 @@ 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)))
210
209
  }
211
210
 
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)
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
217
+ }
218
+
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]!)
267
+ async _asyncWrite (font: string) {
268
+ const res = await _fetch(font)
269
+ this._allocFonts([new Uint8Array(await res.arrayBuffer())])
270
+ }
271
+
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)
241
279
  }
280
+ this._wasm.reloadFonts()
242
281
  }
243
282
 
244
283
  _resizeCanvas (width: number, height: number, videoWidth: number, videoHeight: number) {
@@ -260,7 +299,7 @@ export class ASSRenderer {
260
299
  _draw (time: number, repaint = false) {
261
300
  if (!this._offCanvas || !this._gpurender) return
262
301
 
263
- const result: ASSImage = this._wasm.rawRender(time, Number(repaint))!
302
+ const result = this._wasm.rawRender(time, Number(repaint))!
264
303
  if (this._wasm.changed === 0 && !repaint) return
265
304
 
266
305
  const bitmaps: ASSImage[] = []