jassub 2.2.7 → 2.3.1

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,109 @@ 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
+ async _log (log: string) {
183
+ console.debug(log)
184
+ const match = log.match(/JASSUB: fontselect:[^(]+: \(([^,]+), (\d{1,4}), \d\)/)
185
+ if (match && !await this._findAvailableFont(match[1]!.trim().toLowerCase(), WEIGHT_MAP[parseInt(match[2]!, 10) / 100 - 1])) {
186
+ await this._findAvailableFont(this._defaultFont)
187
+ }
188
+ }
202
189
 
203
- this._fontMap[font] = true
190
+ async addFonts (fontOrURLs: Array<Uint8Array | string>) {
191
+ if (!fontOrURLs.length) return
192
+ const strings: string[] = []
193
+ const uint8s: Uint8Array[] = []
204
194
 
205
- if (!this._availableFonts[font]) {
206
- if (this.useLocalFonts) this._getFont(font)
207
- } else {
208
- this._asyncWrite(this._availableFonts[font]!)
195
+ for (const fontOrURL of fontOrURLs) {
196
+ if (typeof fontOrURL === 'string') {
197
+ strings.push(fontOrURL)
198
+ } else {
199
+ uint8s.push(fontOrURL)
200
+ }
209
201
  }
202
+ if (uint8s.length) this._allocFonts(uint8s)
203
+
204
+ // this isn't batched like uint8s because software like jellyfin exists, which loads 50+ fonts over the network which takes time...
205
+ // is connection exhaustion a concern here?
206
+ return await Promise.allSettled(strings.map(url => this._asyncWrite(url)))
210
207
  }
211
208
 
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)
209
+ // we don't want to run _findAvailableFont before initial fonts are loaded
210
+ // because it could duplicate fonts
211
+ _loadedInitialFonts = false
212
+ async _loadInitialFonts (fontOrURLs: Array<Uint8Array | string>) {
213
+ await this.addFonts(fontOrURLs)
214
+ this._loadedInitialFonts = true
215
+ }
216
+
217
+ _getFont
218
+ _availableFonts: Record<string, Uint8Array | string> = {}
219
+ _checkedFonts = new Set<string>()
220
+ async _findAvailableFont (fontName: string, weight?: WeightValue) {
221
+ if (!this._loadedInitialFonts) return
222
+
223
+ // Roboto Medium, null -> Roboto, Medium
224
+ // Roboto Medium, Medium -> Roboto, Medium
225
+ // Roboto, null -> Roboto, Regular
226
+ // italic is not handled I guess
227
+ for (const _weight of WEIGHT_MAP) {
228
+ // check if fontname has this weight name in it, if yes remove it
229
+ if (fontName.includes(_weight)) {
230
+ fontName = fontName.replace(_weight, '').trim()
231
+ weight ??= _weight
232
+ break
233
+ }
234
+ }
235
+
236
+ weight ??= 'regular'
237
+
238
+ const key = fontName + ' ' + weight
239
+ if (this._checkedFonts.has(key)) return
240
+ this._checkedFonts.add(key)
241
+
242
+ try {
243
+ const font = this._availableFonts[key] ?? this._availableFonts[fontName] ?? await this._queryLocalFont(fontName, weight) ?? await this._queryRemoteFont(fontName, key)
244
+ if (font) return await this.addFonts([font])
245
+ } catch (e) {
246
+ console.warn('Error querying font', fontName, weight, e)
219
247
  }
220
248
  }
221
249
 
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()
250
+ queryFonts
251
+ async _queryLocalFont (fontName: string, weight: WeightValue) {
252
+ if (!this.queryFonts) return
253
+ return await this._getFont(fontName, weight)
228
254
  }
229
255
 
230
- _processAvailableFonts (content: string) {
231
- if (!this._availableFonts) return
256
+ async _queryRemoteFont (fontName: string, postscriptName: string) {
257
+ if (this.queryFonts !== 'localandremote') return
232
258
 
233
- for (const { FontName } of this.getStyles()) {
234
- this._findAvailableFonts(FontName)
235
- }
259
+ const fontData = await queryRemoteFonts({ postscriptNames: [postscriptName, fontName] })
260
+ if (!fontData.length) return
261
+ const blob = await fontData[0]!.blob()
262
+ return new Uint8Array(await blob.arrayBuffer())
263
+ }
236
264
 
237
- const regex = /\\fn([^\\}]*?)[\\}]/g
238
- let matches
239
- while ((matches = regex.exec(content)) !== null) {
240
- this._findAvailableFonts(matches[1]!)
265
+ async _asyncWrite (font: string) {
266
+ const res = await _fetch(font)
267
+ this._allocFonts([new Uint8Array(await res.arrayBuffer())])
268
+ }
269
+
270
+ _fontId = 0
271
+ _allocFonts (uint8s: Uint8Array[]) {
272
+ // TODO: this should re-draw last frame!
273
+ for (const uint8 of uint8s) {
274
+ const ptr = this._malloc(uint8.byteLength)
275
+ self.HEAPU8RAW.set(uint8, ptr)
276
+ this._wasm.addFont('font-' + (this._fontId++), ptr, uint8.byteLength)
241
277
  }
278
+ this._wasm.reloadFonts()
242
279
  }
243
280
 
244
281
  _resizeCanvas (width: number, height: number, videoWidth: number, videoHeight: number) {
@@ -260,7 +297,7 @@ export class ASSRenderer {
260
297
  _draw (time: number, repaint = false) {
261
298
  if (!this._offCanvas || !this._gpurender) return
262
299
 
263
- const result: ASSImage = this._wasm.rawRender(time, Number(repaint))!
300
+ const result = this._wasm.rawRender(time, Number(repaint))!
264
301
  if (this._wasm.changed === 0 && !repaint) return
265
302
 
266
303
  const bitmaps: ASSImage[] = []