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.
- package/README.md +52 -0
- package/dist/default.woff2 +0 -0
- package/dist/jassub.d.ts +14 -14
- package/dist/jassub.js +39 -50
- package/dist/jassub.js.map +1 -1
- package/dist/wasm/jassub-worker.js +4 -0
- package/dist/wasm/jassub-worker.js.map +1 -1
- package/dist/worker/util.d.ts +10 -4
- package/dist/worker/util.js +20 -20
- package/dist/worker/util.js.map +1 -1
- package/dist/worker/webgl-renderer.d.ts +8 -2
- package/dist/worker/webgl-renderer.js +93 -81
- package/dist/worker/webgl-renderer.js.map +1 -1
- package/dist/worker/worker.d.ts +26 -21
- package/dist/worker/worker.js +116 -75
- package/dist/worker/worker.js.map +1 -1
- package/package.json +5 -2
- package/src/jassub.ts +54 -56
- package/src/worker/util.ts +30 -22
- package/src/worker/webgl-renderer.ts +107 -97
- package/src/worker/worker.ts +120 -82
package/src/worker/worker.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
defaultFont: string
|
|
31
29
|
debug: boolean
|
|
32
30
|
libassMemoryLimit: number
|
|
33
31
|
libassGlyphLimit: number
|
|
34
|
-
|
|
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<
|
|
55
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
this._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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
176
|
+
_defaultFont
|
|
177
|
+
setDefaultFont (fontName: string) {
|
|
178
|
+
this._defaultFont = fontName.trim().toLowerCase()
|
|
179
|
+
this._wasm.setDefaultFont(this._defaultFont)
|
|
180
|
+
}
|
|
200
181
|
|
|
201
|
-
|
|
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
|
-
|
|
192
|
+
async addFonts (fontOrURLs: Array<Uint8Array | string>) {
|
|
193
|
+
if (!fontOrURLs.length) return
|
|
194
|
+
const strings: string[] = []
|
|
195
|
+
const uint8s: Uint8Array[] = []
|
|
204
196
|
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
231
|
-
if (
|
|
258
|
+
async _queryRemoteFont (fontName: string, postscriptName: string) {
|
|
259
|
+
if (this.queryFonts !== 'localandremote') return
|
|
232
260
|
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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,
|
|
299
|
+
_draw (time: number, repaint = false) {
|
|
262
300
|
if (!this._offCanvas || !this._gpurender) return
|
|
263
301
|
|
|
264
|
-
const result
|
|
265
|
-
if (this._wasm.changed === 0 && !
|
|
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
|
|