akarisub 0.2.0 → 0.2.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.
- package/README.md +3 -3
- package/dist/akarisub-worker.js +2 -2
- package/dist/akarisub-worker.wasm +0 -0
- package/dist/akarisub.umd.js +3 -3
- package/dist/index.js +3 -3
- package/dist/ts/index.d.ts +14 -0
- package/dist/ts/index.d.ts.map +1 -0
- package/dist/ts/index.js +17 -0
- package/dist/ts/index.js.map +1 -0
- package/dist/ts/ts/akarisub.d.ts +229 -0
- package/dist/ts/ts/akarisub.d.ts.map +1 -0
- package/dist/ts/ts/akarisub.js +1079 -0
- package/dist/ts/ts/akarisub.js.map +1 -0
- package/dist/ts/ts/types.d.ts +424 -0
- package/dist/ts/ts/types.d.ts.map +1 -0
- package/dist/ts/ts/types.js +5 -0
- package/dist/ts/ts/types.js.map +1 -0
- package/dist/ts/ts/utils.d.ts +78 -0
- package/dist/ts/ts/utils.d.ts.map +1 -0
- package/dist/ts/ts/utils.js +395 -0
- package/dist/ts/ts/utils.js.map +1 -0
- package/dist/ts/ts/webgl2-renderer.d.ts +51 -0
- package/dist/ts/ts/webgl2-renderer.d.ts.map +1 -0
- package/dist/ts/ts/webgl2-renderer.js +388 -0
- package/dist/ts/ts/webgl2-renderer.js.map +1 -0
- package/dist/ts/ts/webgpu-renderer.d.ts +64 -0
- package/dist/ts/ts/webgpu-renderer.d.ts.map +1 -0
- package/dist/ts/ts/webgpu-renderer.js +610 -0
- package/dist/ts/ts/webgpu-renderer.js.map +1 -0
- package/dist/ts/ts/worker.d.ts +6 -0
- package/dist/ts/ts/worker.d.ts.map +1 -0
- package/dist/ts/ts/worker.js +1695 -0
- package/dist/ts/ts/worker.js.map +1 -0
- package/dist/ts/wrapper.d.ts +8 -0
- package/dist/ts/wrapper.d.ts.map +1 -0
- package/dist/ts/wrapper.js +9 -0
- package/dist/ts/wrapper.js.map +1 -0
- package/package.json +7 -6
- package/src/ts/akarisub.ts +46 -4
- package/src/ts/types.ts +18 -4
- package/src/ts/worker.ts +177 -23
|
@@ -0,0 +1,1695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AkariSub Worker - TypeScript implementation.
|
|
3
|
+
* Runs in a Web Worker to offload subtitle rendering from the main thread.
|
|
4
|
+
*/
|
|
5
|
+
/// <reference lib="webworker" />
|
|
6
|
+
// @ts-ignore - WASM module is aliased during build
|
|
7
|
+
import WASM from 'wasm';
|
|
8
|
+
import { parseAss, dropBlur, fixPlayRes, libassYCbCrMap } from './utils';
|
|
9
|
+
let lastCurrentTime = 0;
|
|
10
|
+
let rate = 1;
|
|
11
|
+
let rafId = null;
|
|
12
|
+
let nextIsRaf = false;
|
|
13
|
+
const nowMs = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
14
|
+
let lastCurrentTimeReceivedAt = nowMs();
|
|
15
|
+
let targetFps = 24;
|
|
16
|
+
let onDemandRenderMode = false;
|
|
17
|
+
let useLocalFonts = false;
|
|
18
|
+
let blendMode = 'wasm';
|
|
19
|
+
let availableFonts = {};
|
|
20
|
+
const fontMap_ = {};
|
|
21
|
+
let attachedFontId = 0; // For attached/preloaded fonts (higher priority)
|
|
22
|
+
let fallbackFontId = 0; // For fallback fonts (lower priority)
|
|
23
|
+
const pendingFallbackFonts = [];
|
|
24
|
+
let debug = false;
|
|
25
|
+
let clampPos = false;
|
|
26
|
+
let renderInFlight = false;
|
|
27
|
+
const MAX_QUEUED_RENDERS = 3;
|
|
28
|
+
const queuedRenders = [];
|
|
29
|
+
self.width = 0;
|
|
30
|
+
self.height = 0;
|
|
31
|
+
// Performance metrics
|
|
32
|
+
const metrics = {
|
|
33
|
+
framesRendered: 0,
|
|
34
|
+
framesDropped: 0,
|
|
35
|
+
totalRenderTime: 0,
|
|
36
|
+
maxRenderTime: 0,
|
|
37
|
+
minRenderTime: Infinity,
|
|
38
|
+
lastRenderTime: 0,
|
|
39
|
+
renderStartTime: 0,
|
|
40
|
+
pendingRenders: 0,
|
|
41
|
+
totalEvents: 0,
|
|
42
|
+
currentEventIndex: 0,
|
|
43
|
+
cacheHits: 0,
|
|
44
|
+
cacheMisses: 0
|
|
45
|
+
};
|
|
46
|
+
const resetMetrics = () => {
|
|
47
|
+
metrics.framesRendered = 0;
|
|
48
|
+
metrics.framesDropped = 0;
|
|
49
|
+
metrics.totalRenderTime = 0;
|
|
50
|
+
metrics.maxRenderTime = 0;
|
|
51
|
+
metrics.minRenderTime = Infinity;
|
|
52
|
+
metrics.lastRenderTime = 0;
|
|
53
|
+
metrics.cacheHits = 0;
|
|
54
|
+
metrics.cacheMisses = 0;
|
|
55
|
+
};
|
|
56
|
+
let asyncRender = false;
|
|
57
|
+
let asyncRenderOptions = true;
|
|
58
|
+
let offCanvas = null;
|
|
59
|
+
let offCanvasCtx = null;
|
|
60
|
+
let offscreenRender = false;
|
|
61
|
+
let bufferCanvas = null;
|
|
62
|
+
let bufferCtx = null;
|
|
63
|
+
let akariSubHandle = 0;
|
|
64
|
+
let subtitleColorSpace = null;
|
|
65
|
+
let dropAllBlur = false;
|
|
66
|
+
let fullTrackWarmupEnabled = false;
|
|
67
|
+
let hasBitmapBug = false;
|
|
68
|
+
let _Module = null;
|
|
69
|
+
let forceNextDemandRender = false;
|
|
70
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
71
|
+
const TEXT_DECODER = new TextDecoder();
|
|
72
|
+
let akariSubApi = null;
|
|
73
|
+
// Pre-allocated object pool for render results
|
|
74
|
+
const MAX_POOLED_IMAGES = 128;
|
|
75
|
+
const RENDER_COLLECT_MAX_IMAGES = Math.max(MAX_POOLED_IMAGES, 4096);
|
|
76
|
+
const PREWARM_MAX_IMAGES = RENDER_COLLECT_MAX_IMAGES;
|
|
77
|
+
const WARMUP_AHEAD_SECONDS = 30;
|
|
78
|
+
const WARMUP_STEP_SECONDS = 0.5;
|
|
79
|
+
const WARMUP_TICK_MS = 40;
|
|
80
|
+
const ENABLE_RUNTIME_WARMUP = false;
|
|
81
|
+
const FULL_WARMUP_CAP_SECONDS = 30;
|
|
82
|
+
const FULL_WARMUP_STEP_SECONDS = 1;
|
|
83
|
+
const FULL_WARMUP_YIELD_EVERY = 24;
|
|
84
|
+
const ASS_TIME_SCALE = 1000;
|
|
85
|
+
const imagePool = new Array(MAX_POOLED_IMAGES);
|
|
86
|
+
let poolInitialized = false;
|
|
87
|
+
// Batch render-collect buffer: 3 header ints (changed, count, time) + 5 ints per image (x, y, w, h, image_ptr)
|
|
88
|
+
const RRC_HEADER_INTS = 3;
|
|
89
|
+
const RRC_IMG_STRIDE = 5;
|
|
90
|
+
// Pre-allocated buffer for batch render-collect calls
|
|
91
|
+
let rrcBufPtr = 0;
|
|
92
|
+
let rrcBufCapacity = 0;
|
|
93
|
+
const frameImages = [];
|
|
94
|
+
const frameArrayBuffers = [];
|
|
95
|
+
const frameBitmapPromises = [];
|
|
96
|
+
let warmupTimer = null;
|
|
97
|
+
let warmupCursorTime = 0;
|
|
98
|
+
let warmupEndTime = 0;
|
|
99
|
+
let warmupEnabled = false;
|
|
100
|
+
let firstTrackEventStartTime = null;
|
|
101
|
+
let fullTrackWarmupPromise = null;
|
|
102
|
+
let protectedTrackContent = false;
|
|
103
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
104
|
+
const initPool = () => {
|
|
105
|
+
if (poolInitialized)
|
|
106
|
+
return;
|
|
107
|
+
for (let i = 0; i < MAX_POOLED_IMAGES; i++) {
|
|
108
|
+
imagePool[i] = { w: 0, h: 0, x: 0, y: 0, image: 0 };
|
|
109
|
+
}
|
|
110
|
+
poolInitialized = true;
|
|
111
|
+
};
|
|
112
|
+
const getPooledItem = (index) => {
|
|
113
|
+
if (index < MAX_POOLED_IMAGES) {
|
|
114
|
+
return imagePool[index];
|
|
115
|
+
}
|
|
116
|
+
return { w: 0, h: 0, x: 0, y: 0, image: 0 };
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Ensure the batch render-collect buffer is large enough.
|
|
120
|
+
* Layout: [changed, count, time, (x, y, w, h, image_ptr) * N]
|
|
121
|
+
* = 3 + 5*N ints
|
|
122
|
+
*/
|
|
123
|
+
const ensureRenderCollectBuffer = (maxImages) => {
|
|
124
|
+
if (!_Module || maxImages <= 0)
|
|
125
|
+
return;
|
|
126
|
+
const totalInts = RRC_HEADER_INTS + RRC_IMG_STRIDE * maxImages;
|
|
127
|
+
if (rrcBufCapacity >= totalInts && rrcBufPtr)
|
|
128
|
+
return;
|
|
129
|
+
const nextCapacity = Math.max(totalInts, (rrcBufCapacity || 64) * 2);
|
|
130
|
+
const nextSizeBytes = nextCapacity * Int32Array.BYTES_PER_ELEMENT;
|
|
131
|
+
if (rrcBufPtr) {
|
|
132
|
+
_Module._free(rrcBufPtr);
|
|
133
|
+
rrcBufPtr = 0;
|
|
134
|
+
rrcBufCapacity = 0;
|
|
135
|
+
}
|
|
136
|
+
rrcBufPtr = _Module._malloc(nextSizeBytes);
|
|
137
|
+
if (!rrcBufPtr) {
|
|
138
|
+
rrcBufCapacity = 0;
|
|
139
|
+
throw new Error('Failed to allocate render-collect buffer');
|
|
140
|
+
}
|
|
141
|
+
rrcBufCapacity = nextCapacity;
|
|
142
|
+
};
|
|
143
|
+
const prewarmRenderer = (time) => {
|
|
144
|
+
if (!akariSubHandle)
|
|
145
|
+
return;
|
|
146
|
+
const api = requireApi();
|
|
147
|
+
const handle = requireHandle();
|
|
148
|
+
ensureRenderCollectBuffer(PREWARM_MAX_IMAGES);
|
|
149
|
+
if (blendMode === 'wasm') {
|
|
150
|
+
api.renderBlendCollect(handle, time, 0, rrcBufPtr, rrcBufCapacity);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
api.renderImageCollect(handle, time, 0, rrcBufPtr, rrcBufCapacity);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const syncTotalEventsMetric = () => {
|
|
157
|
+
metrics.totalEvents = akariSubHandle ? requireApi().getEventCount(akariSubHandle) : 0;
|
|
158
|
+
};
|
|
159
|
+
const getFirstEventStartTime = () => {
|
|
160
|
+
if (!akariSubHandle)
|
|
161
|
+
return null;
|
|
162
|
+
const api = requireApi();
|
|
163
|
+
const handle = requireHandle();
|
|
164
|
+
const count = api.getEventCount(handle);
|
|
165
|
+
if (count <= 0)
|
|
166
|
+
return null;
|
|
167
|
+
let firstStart = Number.POSITIVE_INFINITY;
|
|
168
|
+
for (let i = 0; i < count; i++) {
|
|
169
|
+
const start = api.eventGetInt(handle, i, EVENT_INT_FIELDS.Start) / ASS_TIME_SCALE;
|
|
170
|
+
if (Number.isFinite(start) && start < firstStart) {
|
|
171
|
+
firstStart = start;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isFinite(firstStart))
|
|
175
|
+
return null;
|
|
176
|
+
return Math.max(0, firstStart);
|
|
177
|
+
};
|
|
178
|
+
const getTrackEventTimeRange = () => {
|
|
179
|
+
if (!akariSubHandle)
|
|
180
|
+
return null;
|
|
181
|
+
const api = requireApi();
|
|
182
|
+
const handle = requireHandle();
|
|
183
|
+
const count = api.getEventCount(handle);
|
|
184
|
+
if (count <= 0)
|
|
185
|
+
return null;
|
|
186
|
+
let start = Number.POSITIVE_INFINITY;
|
|
187
|
+
let end = 0;
|
|
188
|
+
for (let i = 0; i < count; i++) {
|
|
189
|
+
const eventStart = api.eventGetInt(handle, i, EVENT_INT_FIELDS.Start) / ASS_TIME_SCALE;
|
|
190
|
+
const eventDuration = Math.max(0, api.eventGetInt(handle, i, EVENT_INT_FIELDS.Duration) / ASS_TIME_SCALE);
|
|
191
|
+
if (!Number.isFinite(eventStart))
|
|
192
|
+
continue;
|
|
193
|
+
const eventEnd = eventStart + eventDuration;
|
|
194
|
+
if (eventStart < start)
|
|
195
|
+
start = eventStart;
|
|
196
|
+
if (eventEnd > end)
|
|
197
|
+
end = eventEnd;
|
|
198
|
+
}
|
|
199
|
+
if (!Number.isFinite(start))
|
|
200
|
+
return null;
|
|
201
|
+
if (end < start)
|
|
202
|
+
end = start;
|
|
203
|
+
return {
|
|
204
|
+
start: Math.max(0, start),
|
|
205
|
+
end: Math.max(0, end)
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
const prewarmEntireTrack = async () => {
|
|
209
|
+
if (!akariSubHandle)
|
|
210
|
+
return;
|
|
211
|
+
const range = getTrackEventTimeRange();
|
|
212
|
+
if (!range)
|
|
213
|
+
return;
|
|
214
|
+
const cappedEnd = Math.min(range.end, range.start + FULL_WARMUP_CAP_SECONDS);
|
|
215
|
+
let ticks = 0;
|
|
216
|
+
for (let time = range.start; time <= cappedEnd; time += FULL_WARMUP_STEP_SECONDS) {
|
|
217
|
+
if (!akariSubHandle)
|
|
218
|
+
return;
|
|
219
|
+
if (onDemandRenderMode && (renderInFlight || queuedRenders.length > 0 || metrics.pendingRenders > 0)) {
|
|
220
|
+
await sleep(0);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
prewarmRenderer(time);
|
|
224
|
+
ticks++;
|
|
225
|
+
if (onDemandRenderMode || ticks % FULL_WARMUP_YIELD_EVERY === 0) {
|
|
226
|
+
await sleep(0);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
prewarmRenderer(cappedEnd);
|
|
230
|
+
};
|
|
231
|
+
const getWarmupAnchorTime = (fallbackTime) => {
|
|
232
|
+
if (firstTrackEventStartTime == null)
|
|
233
|
+
return fallbackTime;
|
|
234
|
+
if (fallbackTime < firstTrackEventStartTime)
|
|
235
|
+
return firstTrackEventStartTime;
|
|
236
|
+
return fallbackTime;
|
|
237
|
+
};
|
|
238
|
+
const stopWarmup = () => {
|
|
239
|
+
warmupEnabled = false;
|
|
240
|
+
if (warmupTimer) {
|
|
241
|
+
clearTimeout(warmupTimer);
|
|
242
|
+
warmupTimer = null;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const scheduleFullTrackWarmup = () => {
|
|
246
|
+
if (!fullTrackWarmupEnabled || fullTrackWarmupPromise || !akariSubHandle)
|
|
247
|
+
return;
|
|
248
|
+
fullTrackWarmupPromise = (async () => {
|
|
249
|
+
await sleep(0);
|
|
250
|
+
try {
|
|
251
|
+
await prewarmEntireTrack();
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
if (debug)
|
|
255
|
+
console.warn('[AkariSub] Full track warmup failed, continuing:', e);
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
if (akariSubHandle) {
|
|
259
|
+
prewarmRenderer(getCurrentTime());
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
if (debug)
|
|
264
|
+
console.warn('[AkariSub] Post-warmup re-prime failed, continuing:', e);
|
|
265
|
+
}
|
|
266
|
+
})().finally(() => {
|
|
267
|
+
fullTrackWarmupPromise = null;
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
const scheduleWarmupTick = () => {
|
|
271
|
+
if (!warmupEnabled || warmupTimer)
|
|
272
|
+
return;
|
|
273
|
+
warmupTimer = setTimeout(runWarmupTick, WARMUP_TICK_MS);
|
|
274
|
+
};
|
|
275
|
+
const startWarmupWindow = (fromTime) => {
|
|
276
|
+
if (!ENABLE_RUNTIME_WARMUP)
|
|
277
|
+
return;
|
|
278
|
+
if (!akariSubHandle || !Number.isFinite(fromTime))
|
|
279
|
+
return;
|
|
280
|
+
warmupCursorTime = fromTime;
|
|
281
|
+
warmupEndTime = fromTime + WARMUP_AHEAD_SECONDS;
|
|
282
|
+
warmupEnabled = true;
|
|
283
|
+
scheduleWarmupTick();
|
|
284
|
+
};
|
|
285
|
+
const runWarmupTick = () => {
|
|
286
|
+
warmupTimer = null;
|
|
287
|
+
if (!warmupEnabled || !akariSubHandle) {
|
|
288
|
+
warmupEnabled = false;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (warmupCursorTime >= warmupEndTime) {
|
|
292
|
+
warmupEnabled = false;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (renderInFlight || queuedRenders.length > 0 || metrics.pendingRenders > 0) {
|
|
296
|
+
scheduleWarmupTick();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const now = getCurrentTime();
|
|
301
|
+
if (warmupCursorTime < now) {
|
|
302
|
+
warmupCursorTime = now;
|
|
303
|
+
}
|
|
304
|
+
prewarmRenderer(warmupCursorTime);
|
|
305
|
+
warmupCursorTime += WARMUP_STEP_SECONDS;
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
if (debug)
|
|
309
|
+
console.warn('[AkariSub] Warmup tick failed, continuing:', e);
|
|
310
|
+
warmupCursorTime += WARMUP_STEP_SECONDS;
|
|
311
|
+
}
|
|
312
|
+
scheduleWarmupTick();
|
|
313
|
+
};
|
|
314
|
+
const EVENT_INT_FIELDS = {
|
|
315
|
+
Start: 0,
|
|
316
|
+
Duration: 1,
|
|
317
|
+
ReadOrder: 2,
|
|
318
|
+
Layer: 3,
|
|
319
|
+
Style: 4,
|
|
320
|
+
MarginL: 5,
|
|
321
|
+
MarginR: 6,
|
|
322
|
+
MarginV: 7
|
|
323
|
+
};
|
|
324
|
+
const EVENT_STR_FIELDS = {
|
|
325
|
+
Name: 0,
|
|
326
|
+
Effect: 1,
|
|
327
|
+
Text: 2
|
|
328
|
+
};
|
|
329
|
+
const STYLE_NUM_FIELDS = {
|
|
330
|
+
FontSize: 0,
|
|
331
|
+
PrimaryColour: 1,
|
|
332
|
+
SecondaryColour: 2,
|
|
333
|
+
OutlineColour: 3,
|
|
334
|
+
BackColour: 4,
|
|
335
|
+
Bold: 5,
|
|
336
|
+
Italic: 6,
|
|
337
|
+
Underline: 7,
|
|
338
|
+
StrikeOut: 8,
|
|
339
|
+
ScaleX: 9,
|
|
340
|
+
ScaleY: 10,
|
|
341
|
+
Spacing: 11,
|
|
342
|
+
Angle: 12,
|
|
343
|
+
BorderStyle: 13,
|
|
344
|
+
Outline: 14,
|
|
345
|
+
Shadow: 15,
|
|
346
|
+
Alignment: 16,
|
|
347
|
+
MarginL: 17,
|
|
348
|
+
MarginR: 18,
|
|
349
|
+
MarginV: 19,
|
|
350
|
+
Encoding: 20,
|
|
351
|
+
treat_fontname_as_pattern: 21,
|
|
352
|
+
Blur: 22,
|
|
353
|
+
Justify: 23
|
|
354
|
+
};
|
|
355
|
+
const STYLE_STR_FIELDS = {
|
|
356
|
+
Name: 0,
|
|
357
|
+
FontName: 1
|
|
358
|
+
};
|
|
359
|
+
const encodeString = (input) => {
|
|
360
|
+
return TEXT_ENCODER.encode(input);
|
|
361
|
+
};
|
|
362
|
+
const allocString = (input) => {
|
|
363
|
+
if (!_Module)
|
|
364
|
+
return 0;
|
|
365
|
+
const bytes = encodeString(input);
|
|
366
|
+
const ptr = _Module._malloc(bytes.length + 1);
|
|
367
|
+
if (!ptr)
|
|
368
|
+
return 0;
|
|
369
|
+
self.HEAPU8.set(bytes, ptr);
|
|
370
|
+
self.HEAPU8[ptr + bytes.length] = 0;
|
|
371
|
+
return ptr;
|
|
372
|
+
};
|
|
373
|
+
const readCString = (ptr) => {
|
|
374
|
+
if (!ptr)
|
|
375
|
+
return '';
|
|
376
|
+
let end = ptr;
|
|
377
|
+
const heap = self.HEAPU8;
|
|
378
|
+
while (heap[end] !== 0)
|
|
379
|
+
end++;
|
|
380
|
+
return TEXT_DECODER.decode(heap.subarray(ptr, end));
|
|
381
|
+
};
|
|
382
|
+
const withCString = (input, callback) => {
|
|
383
|
+
const ptr = allocString(input);
|
|
384
|
+
try {
|
|
385
|
+
return callback(ptr);
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
if (ptr && _Module)
|
|
389
|
+
_Module._free(ptr);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const toUint8Array = (content) => {
|
|
393
|
+
return content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
394
|
+
};
|
|
395
|
+
const isBinaryContent = (content) => {
|
|
396
|
+
return content instanceof Uint8Array || content instanceof ArrayBuffer;
|
|
397
|
+
};
|
|
398
|
+
const withCBytes = (input, callback) => {
|
|
399
|
+
if (!_Module)
|
|
400
|
+
throw new Error('AkariSub module is not initialized');
|
|
401
|
+
const ptr = _Module._malloc(input.length + 1);
|
|
402
|
+
if (!ptr)
|
|
403
|
+
throw new Error('Failed to allocate subtitle content');
|
|
404
|
+
try {
|
|
405
|
+
self.HEAPU8.set(input, ptr);
|
|
406
|
+
self.HEAPU8[ptr + input.length] = 0;
|
|
407
|
+
return callback(ptr);
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
self.HEAPU8.fill(0, ptr, ptr + input.length + 1);
|
|
411
|
+
_Module._free(ptr);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const decryptV2Payload = async (encrypted, contentKey) => {
|
|
415
|
+
const data = new Uint8Array(encrypted);
|
|
416
|
+
const keyIdSize = 8;
|
|
417
|
+
const nonceSize = 12;
|
|
418
|
+
const headerSize = 1 + keyIdSize + nonceSize;
|
|
419
|
+
if (data.length < headerSize + 16) {
|
|
420
|
+
throw new Error('Ciphertext too short for v2 subtitle payload');
|
|
421
|
+
}
|
|
422
|
+
if (data[0] !== 2) {
|
|
423
|
+
throw new Error('Unsupported encrypted subtitle protocol version');
|
|
424
|
+
}
|
|
425
|
+
const header = data.subarray(0, 1 + keyIdSize);
|
|
426
|
+
const nonce = data.subarray(1 + keyIdSize, headerSize);
|
|
427
|
+
const ciphertext = data.subarray(headerSize);
|
|
428
|
+
const decrypted = await crypto.subtle.decrypt({
|
|
429
|
+
name: 'AES-GCM',
|
|
430
|
+
iv: nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength),
|
|
431
|
+
additionalData: header.buffer.slice(header.byteOffset, header.byteOffset + header.byteLength),
|
|
432
|
+
tagLength: 128
|
|
433
|
+
}, contentKey, ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength));
|
|
434
|
+
return new Uint8Array(decrypted);
|
|
435
|
+
};
|
|
436
|
+
const decryptSubtitleContent = async (content) => {
|
|
437
|
+
if (content.encrypted) {
|
|
438
|
+
return decryptV2Payload(content.encrypted, content.contentKey);
|
|
439
|
+
}
|
|
440
|
+
const chunks = content.encryptedChunks || [];
|
|
441
|
+
if (chunks.length === 0) {
|
|
442
|
+
throw new Error('Encrypted subtitle content is empty');
|
|
443
|
+
}
|
|
444
|
+
const decryptedChunks = await Promise.all(chunks.map((chunk) => decryptV2Payload(chunk, content.contentKey)));
|
|
445
|
+
const totalLength = decryptedChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
446
|
+
const result = new Uint8Array(totalLength);
|
|
447
|
+
let offset = 0;
|
|
448
|
+
for (const chunk of decryptedChunks) {
|
|
449
|
+
result.set(chunk, offset);
|
|
450
|
+
chunk.fill(0);
|
|
451
|
+
offset += chunk.length;
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
};
|
|
455
|
+
const createTrackFromBytes = (content) => {
|
|
456
|
+
const api = requireApi();
|
|
457
|
+
const handle = requireHandle();
|
|
458
|
+
withCBytes(content, (contentPtr) => {
|
|
459
|
+
api.createTrackMem(handle, contentPtr);
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
const createTrackFromString = (content) => {
|
|
463
|
+
const api = requireApi();
|
|
464
|
+
const handle = requireHandle();
|
|
465
|
+
withCString(content, (contentPtr) => {
|
|
466
|
+
api.createTrackMem(handle, contentPtr);
|
|
467
|
+
});
|
|
468
|
+
};
|
|
469
|
+
const requireApi = () => {
|
|
470
|
+
if (!akariSubApi)
|
|
471
|
+
throw new Error('AkariSub API is not initialized');
|
|
472
|
+
return akariSubApi;
|
|
473
|
+
};
|
|
474
|
+
const requireHandle = () => {
|
|
475
|
+
if (!akariSubHandle)
|
|
476
|
+
throw new Error('AkariSub instance is not initialized');
|
|
477
|
+
return akariSubHandle;
|
|
478
|
+
};
|
|
479
|
+
// =============================================================================
|
|
480
|
+
// Font Management
|
|
481
|
+
// =============================================================================
|
|
482
|
+
// Fonts added via addFont are explicitly requested, so they should be attached (high priority)
|
|
483
|
+
self.addFont = ({ font }) => asyncWrite(font, false);
|
|
484
|
+
const findAvailableFonts = (font) => {
|
|
485
|
+
font = font.trim().toLowerCase();
|
|
486
|
+
if (font.startsWith('@'))
|
|
487
|
+
font = font.substring(1);
|
|
488
|
+
if (fontMap_[font])
|
|
489
|
+
return;
|
|
490
|
+
fontMap_[font] = true;
|
|
491
|
+
if (!availableFonts[font]) {
|
|
492
|
+
if (useLocalFonts)
|
|
493
|
+
postMessage({ target: 'getLocalFont', font });
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
asyncWrite(availableFonts[font]);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const asyncWrite = (font, isFallback = true) => {
|
|
500
|
+
if (typeof font === 'string') {
|
|
501
|
+
readAsync(font, (fontData) => {
|
|
502
|
+
writeFontToFS(new Uint8Array(fontData), isFallback);
|
|
503
|
+
}, console.error);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
writeFontToFS(font, isFallback);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
// Synchronous font loading for critical fonts (fallback fonts)
|
|
510
|
+
const syncWrite = (font, isFallback = true) => {
|
|
511
|
+
if (typeof font === 'string') {
|
|
512
|
+
const fontData = read_(font, true);
|
|
513
|
+
if (fontData) {
|
|
514
|
+
writeFontToFSImmediate(new Uint8Array(fontData), isFallback);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
writeFontToFSImmediate(font, isFallback);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
// Debounced font reload
|
|
522
|
+
let pendingFontReload = null;
|
|
523
|
+
const scheduleReloadFonts = () => {
|
|
524
|
+
if (pendingFontReload)
|
|
525
|
+
return;
|
|
526
|
+
pendingFontReload = setTimeout(() => {
|
|
527
|
+
pendingFontReload = null;
|
|
528
|
+
if (akariSubHandle) {
|
|
529
|
+
const api = requireApi();
|
|
530
|
+
api.reloadFonts(akariSubHandle);
|
|
531
|
+
}
|
|
532
|
+
}, 16);
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* Add a font as an embedded font via ass_add_font.
|
|
536
|
+
* Embedded fonts have higher priority than fontconfig fonts in libass.
|
|
537
|
+
*/
|
|
538
|
+
const addFontAsEmbedded = (uint8, name) => {
|
|
539
|
+
if (!_Module || !akariSubHandle) {
|
|
540
|
+
if (debug)
|
|
541
|
+
console.warn('[AkariSub] Cannot add embedded font, module or AkariSub not ready:', name);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const api = requireApi();
|
|
546
|
+
// Allocate memory in WASM heap and copy font data
|
|
547
|
+
const ptr = _Module._malloc(uint8.length);
|
|
548
|
+
if (!ptr) {
|
|
549
|
+
console.warn('[AkariSub] Failed to allocate memory for embedded font:', name);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Copy font data to WASM heap
|
|
553
|
+
self.HEAPU8.set(uint8, ptr);
|
|
554
|
+
withCString(name, (namePtr) => {
|
|
555
|
+
api.addFont(akariSubHandle, namePtr, ptr, uint8.length);
|
|
556
|
+
});
|
|
557
|
+
if (debug)
|
|
558
|
+
console.log('[AkariSub] Added embedded font:', name, 'size:', uint8.length);
|
|
559
|
+
}
|
|
560
|
+
catch (e) {
|
|
561
|
+
console.warn('[AkariSub] Failed to add embedded font:', name, e);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
/**
|
|
565
|
+
* Write a font to the virtual filesystem so fontconfig can index it.
|
|
566
|
+
* Fonts are written to separate directories based on priority:
|
|
567
|
+
* - /fonts/attached: For attached/preloaded fonts (highest priority)
|
|
568
|
+
* - /fonts/fallback: For fallback fonts
|
|
569
|
+
*/
|
|
570
|
+
const writeFontToFS = (uint8, isFallback = true) => {
|
|
571
|
+
const fontDir = isFallback ? '/fonts/fallback' : '/fonts/attached';
|
|
572
|
+
const fontFileName = isFallback ? 'fallback-' + fallbackFontId++ : 'attached-' + attachedFontId++;
|
|
573
|
+
if (_Module) {
|
|
574
|
+
try {
|
|
575
|
+
_Module.FS_createDataFile(fontDir, fontFileName, uint8, true, true, true);
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
console.warn('Failed to write font to filesystem:', fontDir + '/' + fontFileName, e);
|
|
579
|
+
}
|
|
580
|
+
if (!isFallback) {
|
|
581
|
+
addFontAsEmbedded(uint8, fontFileName);
|
|
582
|
+
}
|
|
583
|
+
else if (akariSubHandle) {
|
|
584
|
+
addFontAsEmbedded(uint8, fontFileName);
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
pendingFallbackFonts.push({ data: uint8, name: fontFileName });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
scheduleReloadFonts();
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* Immediate font write without debounced reload (for synchronous loading).
|
|
594
|
+
*/
|
|
595
|
+
const writeFontToFSImmediate = (uint8, isFallback = true) => {
|
|
596
|
+
const fontDir = isFallback ? '/fonts/fallback' : '/fonts/attached';
|
|
597
|
+
const fontFileName = isFallback ? 'fallback-' + fallbackFontId++ : 'attached-' + attachedFontId++;
|
|
598
|
+
if (_Module) {
|
|
599
|
+
try {
|
|
600
|
+
_Module.FS_createDataFile(fontDir, fontFileName, uint8, true, true, true);
|
|
601
|
+
if (debug)
|
|
602
|
+
console.log('[AkariSub] Wrote font to FS:', fontDir + '/' + fontFileName, 'size:', uint8.length);
|
|
603
|
+
}
|
|
604
|
+
catch (e) {
|
|
605
|
+
console.warn('Failed to write font to filesystem:', fontDir + '/' + fontFileName, e);
|
|
606
|
+
}
|
|
607
|
+
if (!isFallback) {
|
|
608
|
+
addFontAsEmbedded(uint8, fontFileName);
|
|
609
|
+
}
|
|
610
|
+
else if (akariSubHandle) {
|
|
611
|
+
addFontAsEmbedded(uint8, fontFileName);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
pendingFallbackFonts.push({ data: uint8, name: fontFileName });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
const processAvailableFonts = (content) => {
|
|
619
|
+
if (!availableFonts)
|
|
620
|
+
return;
|
|
621
|
+
const isLargeFile = content.length > 500000;
|
|
622
|
+
if (isLargeFile) {
|
|
623
|
+
// Extract only the styles section for large files
|
|
624
|
+
const stylesMatch = content.match(/\[V4\+?\s*Styles?\][^\[]*(?=\[|$)/i);
|
|
625
|
+
if (stylesMatch) {
|
|
626
|
+
const stylesSection = stylesMatch[0];
|
|
627
|
+
// Parse only the styles section
|
|
628
|
+
const styleFontMatches = stylesSection.matchAll(/^Style:[^,]*,([^,]+)/gm);
|
|
629
|
+
for (const match of styleFontMatches) {
|
|
630
|
+
findAvailableFonts(match[1].trim());
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// For Events section in large files, limit to first 1000 \fn tags
|
|
634
|
+
const eventsMatch = content.match(/\[Events\][\s\S]*/i);
|
|
635
|
+
if (eventsMatch) {
|
|
636
|
+
const eventsContent = eventsMatch[0];
|
|
637
|
+
const fnMatches = eventsContent.matchAll(/\\fn([^\\}]*?)[\\}]/g);
|
|
638
|
+
let count = 0;
|
|
639
|
+
for (const match of fnMatches) {
|
|
640
|
+
findAvailableFonts(match[1]);
|
|
641
|
+
if (++count >= 1000)
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// Original behavior for small files
|
|
648
|
+
const sections = parseAss(content, true);
|
|
649
|
+
for (let i = 0; i < sections.length; i++) {
|
|
650
|
+
for (let j = 0; j < sections[i].body.length; j++) {
|
|
651
|
+
const entry = sections[i].body[j];
|
|
652
|
+
if (entry.key === 'Style' && typeof entry.value === 'object' && !Array.isArray(entry.value)) {
|
|
653
|
+
findAvailableFonts(entry.value.Fontname);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Use matchAll for Events section
|
|
658
|
+
const eventsMatch = content.match(/\[Events\][\s\S]*/i);
|
|
659
|
+
if (eventsMatch) {
|
|
660
|
+
const eventsContent = eventsMatch[0];
|
|
661
|
+
const fnMatches = eventsContent.matchAll(/\\fn([^\\}]*?)[\\}]/g);
|
|
662
|
+
for (const match of fnMatches) {
|
|
663
|
+
findAvailableFonts(match[1]);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
// =============================================================================
|
|
669
|
+
// Network Utilities
|
|
670
|
+
// =============================================================================
|
|
671
|
+
const read_ = (url, ab) => {
|
|
672
|
+
const xhr = new XMLHttpRequest();
|
|
673
|
+
xhr.open('GET', url, false);
|
|
674
|
+
xhr.responseType = ab ? 'arraybuffer' : 'text';
|
|
675
|
+
xhr.send(null);
|
|
676
|
+
return xhr.response;
|
|
677
|
+
};
|
|
678
|
+
const readAsync = (url, load, err) => {
|
|
679
|
+
const xhr = new XMLHttpRequest();
|
|
680
|
+
xhr.open('GET', url, true);
|
|
681
|
+
xhr.responseType = 'arraybuffer';
|
|
682
|
+
xhr.onload = () => {
|
|
683
|
+
if ((xhr.status === 200 || xhr.status === 0) && xhr.response) {
|
|
684
|
+
return load(xhr.response);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
xhr.onerror = err;
|
|
688
|
+
xhr.send(null);
|
|
689
|
+
};
|
|
690
|
+
// =============================================================================
|
|
691
|
+
// Track Management
|
|
692
|
+
// =============================================================================
|
|
693
|
+
const finishTrackLoad = () => {
|
|
694
|
+
const api = requireApi();
|
|
695
|
+
const handle = requireHandle();
|
|
696
|
+
syncTotalEventsMetric();
|
|
697
|
+
firstTrackEventStartTime = getFirstEventStartTime();
|
|
698
|
+
subtitleColorSpace = libassYCbCrMap[api.getTrackColorSpace(handle)];
|
|
699
|
+
forceNextDemandRender = true;
|
|
700
|
+
postMessage({ target: 'verifyColorSpace', subtitleColorSpace });
|
|
701
|
+
postMessage({ target: 'trackReady' });
|
|
702
|
+
};
|
|
703
|
+
self.setTrack = ({ content }) => {
|
|
704
|
+
stopWarmup();
|
|
705
|
+
fullTrackWarmupPromise = null;
|
|
706
|
+
protectedTrackContent = false;
|
|
707
|
+
if (isBinaryContent(content)) {
|
|
708
|
+
createTrackFromBytes(toUint8Array(content));
|
|
709
|
+
finishTrackLoad();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
processAvailableFonts(content);
|
|
713
|
+
if (clampPos)
|
|
714
|
+
content = fixPlayRes(content);
|
|
715
|
+
if (dropAllBlur)
|
|
716
|
+
content = dropBlur(content);
|
|
717
|
+
createTrackFromString(content);
|
|
718
|
+
finishTrackLoad();
|
|
719
|
+
};
|
|
720
|
+
self.setEncryptedTrack = async ({ content }) => {
|
|
721
|
+
stopWarmup();
|
|
722
|
+
fullTrackWarmupPromise = null;
|
|
723
|
+
protectedTrackContent = true;
|
|
724
|
+
const decrypted = await decryptSubtitleContent(content);
|
|
725
|
+
try {
|
|
726
|
+
createTrackFromBytes(decrypted);
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
decrypted.fill(0);
|
|
730
|
+
}
|
|
731
|
+
finishTrackLoad();
|
|
732
|
+
};
|
|
733
|
+
self.getColorSpace = () => {
|
|
734
|
+
postMessage({ target: 'verifyColorSpace', subtitleColorSpace });
|
|
735
|
+
};
|
|
736
|
+
self.freeTrack = () => {
|
|
737
|
+
stopWarmup();
|
|
738
|
+
fullTrackWarmupPromise = null;
|
|
739
|
+
firstTrackEventStartTime = null;
|
|
740
|
+
protectedTrackContent = false;
|
|
741
|
+
const api = requireApi();
|
|
742
|
+
const handle = requireHandle();
|
|
743
|
+
api.removeTrack(handle);
|
|
744
|
+
syncTotalEventsMetric();
|
|
745
|
+
};
|
|
746
|
+
self.setTrackByUrl = ({ url }) => {
|
|
747
|
+
self.setTrack({ content: read_(url) });
|
|
748
|
+
};
|
|
749
|
+
// =============================================================================
|
|
750
|
+
// Time Management
|
|
751
|
+
// =============================================================================
|
|
752
|
+
let _isPaused = true;
|
|
753
|
+
const getCurrentTime = () => {
|
|
754
|
+
const diff = (nowMs() - lastCurrentTimeReceivedAt) / 1000;
|
|
755
|
+
if (_isPaused) {
|
|
756
|
+
return lastCurrentTime;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
if (diff > 5) {
|
|
760
|
+
console.error("Didn't receive currentTime > 5 seconds. Assuming video was paused.");
|
|
761
|
+
setIsPaused(true);
|
|
762
|
+
}
|
|
763
|
+
return lastCurrentTime + diff * rate;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
const setCurrentTime = (currentTime) => {
|
|
767
|
+
lastCurrentTime = currentTime;
|
|
768
|
+
lastCurrentTimeReceivedAt = nowMs();
|
|
769
|
+
if (onDemandRenderMode) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (!rafId) {
|
|
773
|
+
if (nextIsRaf) {
|
|
774
|
+
rafId = requestAnimationFrame(renderLoop);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
renderLoop();
|
|
778
|
+
nextIsRaf = true;
|
|
779
|
+
setTimeout(() => {
|
|
780
|
+
nextIsRaf = false;
|
|
781
|
+
}, 20);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
const setIsPaused = (isPaused) => {
|
|
786
|
+
if (onDemandRenderMode) {
|
|
787
|
+
_isPaused = isPaused;
|
|
788
|
+
if (rafId) {
|
|
789
|
+
cancelAnimationFrame(rafId);
|
|
790
|
+
rafId = null;
|
|
791
|
+
}
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (isPaused !== _isPaused) {
|
|
795
|
+
_isPaused = isPaused;
|
|
796
|
+
if (isPaused) {
|
|
797
|
+
if (rafId) {
|
|
798
|
+
cancelAnimationFrame(rafId);
|
|
799
|
+
rafId = null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
lastCurrentTimeReceivedAt = nowMs();
|
|
804
|
+
rafId = requestAnimationFrame(renderLoop);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const flushQueuedRender = () => {
|
|
809
|
+
if (renderInFlight || queuedRenders.length === 0)
|
|
810
|
+
return;
|
|
811
|
+
if (queuedRenders.length > 1) {
|
|
812
|
+
const dropped = queuedRenders.length - 1;
|
|
813
|
+
metrics.framesDropped += dropped;
|
|
814
|
+
const latest = queuedRenders[queuedRenders.length - 1];
|
|
815
|
+
queuedRenders.length = 0;
|
|
816
|
+
queuedRenders.push(latest);
|
|
817
|
+
}
|
|
818
|
+
const next = queuedRenders.shift();
|
|
819
|
+
if (!next)
|
|
820
|
+
return;
|
|
821
|
+
render(next.time, next.force);
|
|
822
|
+
};
|
|
823
|
+
const completeRenderCycle = () => {
|
|
824
|
+
renderInFlight = false;
|
|
825
|
+
flushQueuedRender();
|
|
826
|
+
};
|
|
827
|
+
const render = (time, force) => {
|
|
828
|
+
if (renderInFlight) {
|
|
829
|
+
const queuedItem = { time, force: force ? 1 : 0 };
|
|
830
|
+
if (queuedItem.force) {
|
|
831
|
+
queuedRenders.length = 0;
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
const lastQueued = queuedRenders[queuedRenders.length - 1];
|
|
835
|
+
if (lastQueued && Math.abs(lastQueued.time - queuedItem.time) > 0.25) {
|
|
836
|
+
queuedRenders.length = 0;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (queuedRenders.length >= MAX_QUEUED_RENDERS) {
|
|
840
|
+
queuedRenders.shift();
|
|
841
|
+
metrics.framesDropped++;
|
|
842
|
+
}
|
|
843
|
+
queuedRenders.push(queuedItem);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
renderInFlight = true;
|
|
847
|
+
initPool(); // Ensure pool is ready
|
|
848
|
+
const times = {};
|
|
849
|
+
const renderStartTime = performance.now();
|
|
850
|
+
metrics.renderStartTime = renderStartTime;
|
|
851
|
+
metrics.pendingRenders++;
|
|
852
|
+
const api = requireApi();
|
|
853
|
+
const handle = requireHandle();
|
|
854
|
+
const forceInt = force ? 1 : 0;
|
|
855
|
+
// Use the batch render-collect API: single WASM call does render + metadata + image data extraction.
|
|
856
|
+
ensureRenderCollectBuffer(RENDER_COLLECT_MAX_IMAGES);
|
|
857
|
+
const written = blendMode === 'wasm'
|
|
858
|
+
? api.renderBlendCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity)
|
|
859
|
+
: api.renderImageCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity);
|
|
860
|
+
const headerView = new Int32Array(self.wasmMemory.buffer, rrcBufPtr, RRC_HEADER_INTS);
|
|
861
|
+
const changed = headerView[0];
|
|
862
|
+
const imageCount = headerView[1];
|
|
863
|
+
// Update metrics
|
|
864
|
+
const renderEndTime = performance.now();
|
|
865
|
+
const renderDuration = renderEndTime - renderStartTime;
|
|
866
|
+
metrics.lastRenderTime = renderDuration;
|
|
867
|
+
metrics.totalRenderTime += renderDuration;
|
|
868
|
+
metrics.maxRenderTime = Math.max(metrics.maxRenderTime, renderDuration);
|
|
869
|
+
if (renderDuration > 0) {
|
|
870
|
+
metrics.minRenderTime = Math.min(metrics.minRenderTime, renderDuration);
|
|
871
|
+
}
|
|
872
|
+
if (changed !== 0 || force) {
|
|
873
|
+
metrics.framesRendered++;
|
|
874
|
+
metrics.cacheMisses++;
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
metrics.cacheHits++;
|
|
878
|
+
}
|
|
879
|
+
if (debug) {
|
|
880
|
+
const decodeEndTime = performance.now();
|
|
881
|
+
const renderEndTimeWasm = headerView[2];
|
|
882
|
+
times.WASMRenderTime = renderEndTimeWasm - renderStartTime;
|
|
883
|
+
times.WASMBitmapDecodeTime = decodeEndTime - renderEndTimeWasm;
|
|
884
|
+
times.JSRenderTime = Date.now();
|
|
885
|
+
}
|
|
886
|
+
if (changed !== 0 || force) {
|
|
887
|
+
const images = frameImages;
|
|
888
|
+
const buffers = frameArrayBuffers;
|
|
889
|
+
images.length = 0;
|
|
890
|
+
buffers.length = 0;
|
|
891
|
+
if (written === 0)
|
|
892
|
+
return paintImages({ images, buffers, times });
|
|
893
|
+
const imgDataOffset = rrcBufPtr + RRC_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
|
|
894
|
+
const meta = new Int32Array(self.wasmMemory.buffer, imgDataOffset, written * RRC_IMG_STRIDE);
|
|
895
|
+
const useAsyncBitmapPath = asyncRender && offscreenRender !== true;
|
|
896
|
+
if (useAsyncBitmapPath) {
|
|
897
|
+
const promises = frameBitmapPromises;
|
|
898
|
+
promises.length = written;
|
|
899
|
+
for (let i = 0; i < written; ++i) {
|
|
900
|
+
const metaOffset = i * RRC_IMG_STRIDE;
|
|
901
|
+
const item = getPooledItem(i);
|
|
902
|
+
item.x = meta[metaOffset];
|
|
903
|
+
item.y = meta[metaOffset + 1];
|
|
904
|
+
item.w = meta[metaOffset + 2];
|
|
905
|
+
item.h = meta[metaOffset + 3];
|
|
906
|
+
item.image = 0;
|
|
907
|
+
const pointer = meta[metaOffset + 4];
|
|
908
|
+
const byteLength = item.w * item.h * 4;
|
|
909
|
+
const rawData = new Uint8ClampedArray(self.wasmMemory.buffer, pointer, byteLength);
|
|
910
|
+
const imageData = new ImageData(rawData, item.w, item.h);
|
|
911
|
+
promises[i] = asyncRenderOptions
|
|
912
|
+
? createImageBitmap(imageData, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' })
|
|
913
|
+
: createImageBitmap(imageData);
|
|
914
|
+
images[i] = item;
|
|
915
|
+
}
|
|
916
|
+
Promise.all(promises).then((bitmaps) => {
|
|
917
|
+
for (let i = 0; i < written; i++) {
|
|
918
|
+
images[i].image = bitmaps[i];
|
|
919
|
+
}
|
|
920
|
+
if (debug)
|
|
921
|
+
times.JSBitmapGenerationTime = Date.now() - (times.JSRenderTime || 0);
|
|
922
|
+
paintImages({ images, buffers: bitmaps, times });
|
|
923
|
+
}).catch(() => {
|
|
924
|
+
if (asyncRenderOptions) {
|
|
925
|
+
asyncRenderOptions = false;
|
|
926
|
+
console.warn('[AkariSub] createImageBitmap options not supported, disabling');
|
|
927
|
+
metrics.pendingRenders--;
|
|
928
|
+
completeRenderCycle();
|
|
929
|
+
render(time, force);
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
metrics.pendingRenders--;
|
|
933
|
+
postMessage({ target: 'unbusy' });
|
|
934
|
+
completeRenderCycle();
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
for (let i = 0; i < written; ++i) {
|
|
940
|
+
const metaOffset = i * RRC_IMG_STRIDE;
|
|
941
|
+
const item = getPooledItem(i);
|
|
942
|
+
item.x = meta[metaOffset];
|
|
943
|
+
item.y = meta[metaOffset + 1];
|
|
944
|
+
item.w = meta[metaOffset + 2];
|
|
945
|
+
item.h = meta[metaOffset + 3];
|
|
946
|
+
item.image = meta[metaOffset + 4];
|
|
947
|
+
if (!offCanvasCtx) {
|
|
948
|
+
const imagePtr = item.image;
|
|
949
|
+
const byteLength = item.w * item.h * 4;
|
|
950
|
+
const copiedData = new Uint8ClampedArray(byteLength);
|
|
951
|
+
copiedData.set(self.HEAPU8C.subarray(imagePtr, imagePtr + byteLength));
|
|
952
|
+
buffers.push(copiedData.buffer);
|
|
953
|
+
item.image = copiedData;
|
|
954
|
+
}
|
|
955
|
+
images[i] = item;
|
|
956
|
+
}
|
|
957
|
+
paintImages({ images, buffers, times });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
metrics.pendingRenders--;
|
|
962
|
+
postMessage({ target: 'unbusy' });
|
|
963
|
+
completeRenderCycle();
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
self.demand = ({ time }) => {
|
|
967
|
+
lastCurrentTime = time;
|
|
968
|
+
lastCurrentTimeReceivedAt = nowMs();
|
|
969
|
+
const force = forceNextDemandRender ? 1 : 0;
|
|
970
|
+
forceNextDemandRender = false;
|
|
971
|
+
render(time, force);
|
|
972
|
+
};
|
|
973
|
+
const renderLoop = (force) => {
|
|
974
|
+
rafId = null;
|
|
975
|
+
render(getCurrentTime(), force);
|
|
976
|
+
if (!_isPaused) {
|
|
977
|
+
rafId = requestAnimationFrame(renderLoop);
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
const paintImages = ({ times, images, buffers }) => {
|
|
981
|
+
metrics.pendingRenders--;
|
|
982
|
+
const width = self.width;
|
|
983
|
+
const height = self.height;
|
|
984
|
+
const imageCount = images.length;
|
|
985
|
+
const resultObject = {
|
|
986
|
+
target: 'render',
|
|
987
|
+
asyncRender,
|
|
988
|
+
images,
|
|
989
|
+
times,
|
|
990
|
+
width,
|
|
991
|
+
height,
|
|
992
|
+
colorSpace: subtitleColorSpace
|
|
993
|
+
};
|
|
994
|
+
if (offscreenRender) {
|
|
995
|
+
// Only resize canvas when dimensions actually change
|
|
996
|
+
if (offCanvas.height !== height || offCanvas.width !== width) {
|
|
997
|
+
offCanvas.width = width;
|
|
998
|
+
offCanvas.height = height;
|
|
999
|
+
}
|
|
1000
|
+
offCanvasCtx.clearRect(0, 0, width, height);
|
|
1001
|
+
if (asyncRender) {
|
|
1002
|
+
// Batch draw all images
|
|
1003
|
+
for (let i = 0; i < imageCount; i++) {
|
|
1004
|
+
const img = images[i];
|
|
1005
|
+
if (img.image) {
|
|
1006
|
+
offCanvasCtx.drawImage(img.image, img.x, img.y);
|
|
1007
|
+
img.image.close();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
// Non-async path with buffer canvas
|
|
1013
|
+
for (let i = 0; i < imageCount; i++) {
|
|
1014
|
+
const img = images[i];
|
|
1015
|
+
if (img.image) {
|
|
1016
|
+
const imgW = img.w;
|
|
1017
|
+
const imgH = img.h;
|
|
1018
|
+
// Only resize buffer canvas when needed
|
|
1019
|
+
if (bufferCanvas.width !== imgW || bufferCanvas.height !== imgH) {
|
|
1020
|
+
bufferCanvas.width = imgW;
|
|
1021
|
+
bufferCanvas.height = imgH;
|
|
1022
|
+
}
|
|
1023
|
+
const pointer = img.image;
|
|
1024
|
+
const byteLength = imgW * imgH * 4;
|
|
1025
|
+
const rawData = self.HEAPU8C.subarray(pointer, pointer + byteLength);
|
|
1026
|
+
bufferCtx.putImageData(new ImageData(rawData, imgW, imgH), 0, 0);
|
|
1027
|
+
offCanvasCtx.drawImage(bufferCanvas, img.x, img.y);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (offscreenRender === 'hybrid') {
|
|
1032
|
+
if (!imageCount) {
|
|
1033
|
+
postMessage(resultObject);
|
|
1034
|
+
completeRenderCycle();
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (debug)
|
|
1038
|
+
times.bitmaps = imageCount;
|
|
1039
|
+
try {
|
|
1040
|
+
const bitmap = offCanvas.transferToImageBitmap();
|
|
1041
|
+
const result = {
|
|
1042
|
+
...resultObject,
|
|
1043
|
+
images: [{ image: bitmap, x: 0, y: 0 }],
|
|
1044
|
+
asyncRender: true
|
|
1045
|
+
};
|
|
1046
|
+
postMessage(result, [bitmap]);
|
|
1047
|
+
completeRenderCycle();
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
postMessage({ target: 'unbusy' });
|
|
1051
|
+
completeRenderCycle();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
if (debug) {
|
|
1056
|
+
times.JSRenderTime = Date.now() - (times.JSRenderTime || 0) - (times.JSBitmapGenerationTime || 0);
|
|
1057
|
+
let total = 0;
|
|
1058
|
+
for (const key in times)
|
|
1059
|
+
total += times[key] || 0;
|
|
1060
|
+
console.log('Bitmaps: ' + imageCount + ' Total: ' + (total | 0) + 'ms', times);
|
|
1061
|
+
}
|
|
1062
|
+
postMessage({ target: 'unbusy' });
|
|
1063
|
+
completeRenderCycle();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
postMessage(resultObject, buffers);
|
|
1068
|
+
completeRenderCycle();
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
// Custom requestAnimationFrame for worker
|
|
1072
|
+
const requestAnimationFrame = self.requestAnimationFrame ? self.requestAnimationFrame.bind(self) : (() => {
|
|
1073
|
+
let nextRAF = 0;
|
|
1074
|
+
return (func) => {
|
|
1075
|
+
const now = nowMs();
|
|
1076
|
+
if (nextRAF === 0) {
|
|
1077
|
+
nextRAF = now + 1000 / targetFps;
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
while (now + 2 >= nextRAF) {
|
|
1081
|
+
nextRAF += 1000 / targetFps;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
const delay = Math.max(nextRAF - now, 0);
|
|
1085
|
+
return setTimeout(func, delay);
|
|
1086
|
+
};
|
|
1087
|
+
})();
|
|
1088
|
+
const cancelAnimationFrame = self.cancelAnimationFrame ? self.cancelAnimationFrame.bind(self) : clearTimeout;
|
|
1089
|
+
// =============================================================================
|
|
1090
|
+
// WASM Initialization
|
|
1091
|
+
// =============================================================================
|
|
1092
|
+
self.init = async (data) => {
|
|
1093
|
+
hasBitmapBug = data.hasBitmapBug;
|
|
1094
|
+
fullTrackWarmupEnabled = !!data.fullTrackWarmup;
|
|
1095
|
+
if (typeof data.initialTime === 'number' && Number.isFinite(data.initialTime)) {
|
|
1096
|
+
lastCurrentTime = data.initialTime;
|
|
1097
|
+
}
|
|
1098
|
+
const _fetch = self.fetch;
|
|
1099
|
+
const setWasmUrl = (wasmUrl) => {
|
|
1100
|
+
if (WebAssembly.instantiateStreaming) {
|
|
1101
|
+
self.fetch = (_) => _fetch(wasmUrl);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
const restoreFetch = () => {
|
|
1105
|
+
self.fetch = _fetch;
|
|
1106
|
+
};
|
|
1107
|
+
const loadWasm = (wasmUrl) => {
|
|
1108
|
+
setWasmUrl(wasmUrl);
|
|
1109
|
+
return WASM({
|
|
1110
|
+
wasm: !WebAssembly.instantiateStreaming ? read_(wasmUrl, true) : undefined
|
|
1111
|
+
}).finally(restoreFetch);
|
|
1112
|
+
};
|
|
1113
|
+
const onWasmLoaded = async (Module) => {
|
|
1114
|
+
_Module = Module; // Store module reference for FS access
|
|
1115
|
+
akariSubApi = {
|
|
1116
|
+
create: Module._akarisub_create,
|
|
1117
|
+
destroy: Module._akarisub_destroy,
|
|
1118
|
+
setDropAnimations: Module._akarisub_set_drop_animations,
|
|
1119
|
+
createTrackMem: Module._akarisub_create_track_mem,
|
|
1120
|
+
removeTrack: Module._akarisub_remove_track,
|
|
1121
|
+
resizeCanvas: Module._akarisub_resize_canvas,
|
|
1122
|
+
addFont: Module._akarisub_add_font,
|
|
1123
|
+
reloadFonts: Module._akarisub_reload_fonts,
|
|
1124
|
+
setDefaultFont: Module._akarisub_set_default_font,
|
|
1125
|
+
setFallbackFonts: Module._akarisub_set_fallback_fonts,
|
|
1126
|
+
setMemoryLimits: Module._akarisub_set_memory_limits,
|
|
1127
|
+
getEventCount: Module._akarisub_get_event_count,
|
|
1128
|
+
allocEvent: Module._akarisub_alloc_event,
|
|
1129
|
+
removeEvent: Module._akarisub_remove_event,
|
|
1130
|
+
getStyleCount: Module._akarisub_get_style_count,
|
|
1131
|
+
allocStyle: Module._akarisub_alloc_style,
|
|
1132
|
+
removeStyle: Module._akarisub_remove_style,
|
|
1133
|
+
styleOverrideIndex: Module._akarisub_style_override_index,
|
|
1134
|
+
disableStyleOverride: Module._akarisub_disable_style_override,
|
|
1135
|
+
getTrackColorSpace: Module._akarisub_get_track_color_space,
|
|
1136
|
+
eventGetInt: Module._akarisub_event_get_int,
|
|
1137
|
+
eventSetInt: Module._akarisub_event_set_int,
|
|
1138
|
+
eventGetStr: Module._akarisub_event_get_str,
|
|
1139
|
+
eventSetStr: Module._akarisub_event_set_str,
|
|
1140
|
+
styleGetNum: Module._akarisub_style_get_num,
|
|
1141
|
+
styleSetNum: Module._akarisub_style_set_num,
|
|
1142
|
+
styleGetStr: Module._akarisub_style_get_str,
|
|
1143
|
+
styleSetStr: Module._akarisub_style_set_str,
|
|
1144
|
+
renderBlendCollect: Module._akarisub_render_blend_collect,
|
|
1145
|
+
renderImageCollect: Module._akarisub_render_image_collect
|
|
1146
|
+
};
|
|
1147
|
+
// Normalize fallback fonts and deduplicate
|
|
1148
|
+
const fallbackFonts = [];
|
|
1149
|
+
const fallbackFontKeys = new Set();
|
|
1150
|
+
if (data.fallbackFonts && data.fallbackFonts.length > 0) {
|
|
1151
|
+
for (const font of data.fallbackFonts) {
|
|
1152
|
+
const originalFont = font.trim();
|
|
1153
|
+
const key = originalFont.toLowerCase();
|
|
1154
|
+
if (key && !fallbackFontKeys.has(key)) {
|
|
1155
|
+
fallbackFontKeys.add(key);
|
|
1156
|
+
fallbackFonts.push(originalFont);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
try {
|
|
1161
|
+
Module.FS_createPath('/', 'fonts', true, true);
|
|
1162
|
+
Module.FS_createPath('/fonts', 'attached', true, true);
|
|
1163
|
+
Module.FS_createPath('/fonts', 'fallback', true, true);
|
|
1164
|
+
Module.FS_createPath('/', 'fontconfig', true, true);
|
|
1165
|
+
Module.FS_createPath('/', 'assets', true, true);
|
|
1166
|
+
Module.FS_createPath('/', 'etc', true, true);
|
|
1167
|
+
Module.FS_createPath('/etc', 'fonts', true, true);
|
|
1168
|
+
const fontsConf = `<?xml version="1.0"?>
|
|
1169
|
+
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
|
1170
|
+
<fontconfig>
|
|
1171
|
+
<!-- Font directories listed in priority order -->
|
|
1172
|
+
<dir>/fonts/attached</dir>
|
|
1173
|
+
<dir>/fonts</dir>
|
|
1174
|
+
<dir>/fonts/fallback</dir>
|
|
1175
|
+
<match target="pattern">
|
|
1176
|
+
<test qual="any" name="family">
|
|
1177
|
+
<string>mono</string>
|
|
1178
|
+
</test>
|
|
1179
|
+
<edit name="family" mode="assign" binding="same">
|
|
1180
|
+
<string>monospace</string>
|
|
1181
|
+
</edit>
|
|
1182
|
+
</match>
|
|
1183
|
+
<match target="pattern">
|
|
1184
|
+
<test qual="any" name="family">
|
|
1185
|
+
<string>sans serif</string>
|
|
1186
|
+
</test>
|
|
1187
|
+
<edit name="family" mode="assign" binding="same">
|
|
1188
|
+
<string>sans-serif</string>
|
|
1189
|
+
</edit>
|
|
1190
|
+
</match>
|
|
1191
|
+
<match target="pattern">
|
|
1192
|
+
<test qual="any" name="family">
|
|
1193
|
+
<string>sans</string>
|
|
1194
|
+
</test>
|
|
1195
|
+
<edit name="family" mode="assign" binding="same">
|
|
1196
|
+
<string>sans-serif</string>
|
|
1197
|
+
</edit>
|
|
1198
|
+
</match>
|
|
1199
|
+
<cachedir>/fontconfig</cachedir>
|
|
1200
|
+
<config>
|
|
1201
|
+
<rescan>
|
|
1202
|
+
<int>0</int>
|
|
1203
|
+
</rescan>
|
|
1204
|
+
</config>
|
|
1205
|
+
</fontconfig>
|
|
1206
|
+
`;
|
|
1207
|
+
const fontsConfData = TEXT_ENCODER.encode(fontsConf);
|
|
1208
|
+
Module.FS_createDataFile('/assets', 'fonts.conf', fontsConfData, true, false, false);
|
|
1209
|
+
Module.FS_createDataFile('/etc/fonts', 'fonts.conf', fontsConfData, true, false, false);
|
|
1210
|
+
}
|
|
1211
|
+
catch (e) {
|
|
1212
|
+
console.warn('Failed to create font directories or fonts.conf:', e);
|
|
1213
|
+
}
|
|
1214
|
+
self.width = data.width;
|
|
1215
|
+
self.height = data.height;
|
|
1216
|
+
onDemandRenderMode = !!data.onDemandRender;
|
|
1217
|
+
blendMode = data.blendMode;
|
|
1218
|
+
asyncRender = data.asyncRender;
|
|
1219
|
+
if (asyncRender && typeof createImageBitmap === 'undefined') {
|
|
1220
|
+
asyncRender = false;
|
|
1221
|
+
console.error("'createImageBitmap' needed for 'asyncRender' unsupported!");
|
|
1222
|
+
}
|
|
1223
|
+
if (asyncRender) {
|
|
1224
|
+
try {
|
|
1225
|
+
const testCanvas = new OffscreenCanvas(1, 1);
|
|
1226
|
+
const testCtx = testCanvas.getContext('2d');
|
|
1227
|
+
if (testCtx) {
|
|
1228
|
+
const testData = testCtx.getImageData(0, 0, 1, 1);
|
|
1229
|
+
await createImageBitmap(testData, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' })
|
|
1230
|
+
.catch(() => {
|
|
1231
|
+
asyncRenderOptions = false;
|
|
1232
|
+
console.warn('[AkariSub] createImageBitmap options not supported (Safari?), rendering without options');
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
asyncRenderOptions = false;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
availableFonts = data.availableFonts;
|
|
1241
|
+
debug = data.debug;
|
|
1242
|
+
targetFps = data.targetFps || targetFps;
|
|
1243
|
+
useLocalFonts = data.useLocalFonts;
|
|
1244
|
+
dropAllBlur = data.dropAllBlur;
|
|
1245
|
+
clampPos = data.clampPos;
|
|
1246
|
+
// Load fallback fonts asynchronously to avoid blocking worker thread
|
|
1247
|
+
// This is critical for mobile devices where sync XHR can cause timeouts
|
|
1248
|
+
const loadFallbackFontsAsync = async () => {
|
|
1249
|
+
const fontPromises = [];
|
|
1250
|
+
for (const font of fallbackFonts) {
|
|
1251
|
+
const fontLower = font.trim().toLowerCase();
|
|
1252
|
+
const fontKey = fontLower.startsWith('@') ? fontLower.substring(1) : fontLower;
|
|
1253
|
+
if (availableFonts && availableFonts[fontKey]) {
|
|
1254
|
+
const fontUrl = availableFonts[fontKey];
|
|
1255
|
+
if (typeof fontUrl === 'string') {
|
|
1256
|
+
// Async fetch for URL-based fonts
|
|
1257
|
+
const promise = new Promise((resolve) => {
|
|
1258
|
+
readAsync(fontUrl, (fontData) => {
|
|
1259
|
+
writeFontToFSImmediate(new Uint8Array(fontData), true);
|
|
1260
|
+
fontMap_[fontKey] = true;
|
|
1261
|
+
if (debug)
|
|
1262
|
+
console.log('[AkariSub] Loaded fallback font async:', fontKey);
|
|
1263
|
+
resolve();
|
|
1264
|
+
}, (e) => {
|
|
1265
|
+
console.error('Failed to load fallback font:', fontKey, e);
|
|
1266
|
+
resolve(); // Don't fail initialization if a single font fails
|
|
1267
|
+
});
|
|
1268
|
+
});
|
|
1269
|
+
fontPromises.push(promise);
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
// Font data directly provided - synchronous write is OK here
|
|
1273
|
+
writeFontToFSImmediate(fontUrl, true);
|
|
1274
|
+
fontMap_[fontKey] = true;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// Wait for all fonts to load (with 30s timeout to prevent blocking forever)
|
|
1279
|
+
if (fontPromises.length > 0) {
|
|
1280
|
+
let timeoutId = null;
|
|
1281
|
+
let timedOut = false;
|
|
1282
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1283
|
+
timeoutId = setTimeout(() => {
|
|
1284
|
+
timedOut = true;
|
|
1285
|
+
console.warn('[AkariSub] Fallback font loading timeout, continuing with available fonts');
|
|
1286
|
+
resolve();
|
|
1287
|
+
}, 30000);
|
|
1288
|
+
});
|
|
1289
|
+
await Promise.race([
|
|
1290
|
+
Promise.all(fontPromises).then(() => {
|
|
1291
|
+
if (timeoutId !== null)
|
|
1292
|
+
clearTimeout(timeoutId);
|
|
1293
|
+
}),
|
|
1294
|
+
timeoutPromise
|
|
1295
|
+
]);
|
|
1296
|
+
if (!timedOut && debug) {
|
|
1297
|
+
console.log('[AkariSub] All fallback fonts loaded successfully');
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
await loadFallbackFontsAsync();
|
|
1302
|
+
const primaryFallback = fallbackFonts.length > 0 ? fallbackFonts[0] : null;
|
|
1303
|
+
akariSubHandle = withCString(primaryFallback || '', (fontPtr) => {
|
|
1304
|
+
return requireApi().create(self.width, self.height, fontPtr, debug ? 1 : 0);
|
|
1305
|
+
});
|
|
1306
|
+
if (pendingFallbackFonts.length > 0) {
|
|
1307
|
+
for (const { data: fontData, name: fontName } of pendingFallbackFonts) {
|
|
1308
|
+
addFontAsEmbedded(fontData, fontName);
|
|
1309
|
+
}
|
|
1310
|
+
pendingFallbackFonts.length = 0;
|
|
1311
|
+
requireApi().reloadFonts(akariSubHandle);
|
|
1312
|
+
}
|
|
1313
|
+
if (fallbackFonts.length > 0) {
|
|
1314
|
+
withCString(fallbackFonts.join(','), (fontsPtr) => {
|
|
1315
|
+
requireApi().setFallbackFonts(requireHandle(), fontsPtr);
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
let subContent = data.subContent;
|
|
1319
|
+
let decryptedSubContent = null;
|
|
1320
|
+
if (data.encryptedSubContent) {
|
|
1321
|
+
protectedTrackContent = true;
|
|
1322
|
+
decryptedSubContent = await decryptSubtitleContent(data.encryptedSubContent);
|
|
1323
|
+
subContent = decryptedSubContent;
|
|
1324
|
+
}
|
|
1325
|
+
else {
|
|
1326
|
+
protectedTrackContent = false;
|
|
1327
|
+
if (!subContent)
|
|
1328
|
+
subContent = read_(data.subUrl);
|
|
1329
|
+
}
|
|
1330
|
+
// For large files, emit partial_ready early to allow playback to start
|
|
1331
|
+
// while font loading and track parsing continues in the background
|
|
1332
|
+
const isLargeSubtitle = typeof subContent === 'string'
|
|
1333
|
+
? subContent.length > 500000
|
|
1334
|
+
: toUint8Array(subContent).byteLength > 500000;
|
|
1335
|
+
if (isLargeSubtitle) {
|
|
1336
|
+
postMessage({ target: 'partial_ready' });
|
|
1337
|
+
if (debug)
|
|
1338
|
+
console.log('[AkariSub] Large subtitle detected, emitting partial_ready early');
|
|
1339
|
+
}
|
|
1340
|
+
if (typeof subContent === 'string') {
|
|
1341
|
+
processAvailableFonts(subContent);
|
|
1342
|
+
if (clampPos)
|
|
1343
|
+
subContent = fixPlayRes(subContent);
|
|
1344
|
+
if (dropAllBlur)
|
|
1345
|
+
subContent = dropBlur(subContent);
|
|
1346
|
+
}
|
|
1347
|
+
else if (debug && (clampPos || dropAllBlur)) {
|
|
1348
|
+
console.warn('[AkariSub] Text rewrite options are skipped for protected binary subtitle content');
|
|
1349
|
+
}
|
|
1350
|
+
// Load attached/preloaded fonts before ready to avoid runtime font churn during first playback.
|
|
1351
|
+
let hasAttachedFonts = false;
|
|
1352
|
+
const attachedFontPromises = [];
|
|
1353
|
+
for (const font of data.fonts || []) {
|
|
1354
|
+
if (typeof font === 'string') {
|
|
1355
|
+
const promise = new Promise((resolve) => {
|
|
1356
|
+
readAsync(font, (fontData) => {
|
|
1357
|
+
writeFontToFSImmediate(new Uint8Array(fontData), false);
|
|
1358
|
+
hasAttachedFonts = true;
|
|
1359
|
+
if (debug)
|
|
1360
|
+
console.log('[AkariSub] Loaded attached font async:', font);
|
|
1361
|
+
resolve();
|
|
1362
|
+
}, (e) => {
|
|
1363
|
+
console.error('Failed to load attached font:', font, e);
|
|
1364
|
+
resolve();
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
attachedFontPromises.push(promise);
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
writeFontToFSImmediate(font, false);
|
|
1371
|
+
hasAttachedFonts = true;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (attachedFontPromises.length > 0) {
|
|
1375
|
+
let attachedTimeoutId = null;
|
|
1376
|
+
let attachedTimedOut = false;
|
|
1377
|
+
const attachedTimeoutPromise = new Promise((resolve) => {
|
|
1378
|
+
attachedTimeoutId = setTimeout(() => {
|
|
1379
|
+
attachedTimedOut = true;
|
|
1380
|
+
console.warn('[AkariSub] Attached font loading timeout, continuing with available fonts');
|
|
1381
|
+
resolve();
|
|
1382
|
+
}, 30000);
|
|
1383
|
+
});
|
|
1384
|
+
await Promise.race([
|
|
1385
|
+
Promise.all(attachedFontPromises).then(() => {
|
|
1386
|
+
if (attachedTimeoutId !== null)
|
|
1387
|
+
clearTimeout(attachedTimeoutId);
|
|
1388
|
+
}),
|
|
1389
|
+
attachedTimeoutPromise
|
|
1390
|
+
]);
|
|
1391
|
+
if (!attachedTimedOut && debug) {
|
|
1392
|
+
console.log('[AkariSub] Attached font loading complete');
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (hasAttachedFonts) {
|
|
1396
|
+
if (debug)
|
|
1397
|
+
console.log('[AkariSub] Reloading fonts after writing attached fonts to FS');
|
|
1398
|
+
requireApi().reloadFonts(requireHandle());
|
|
1399
|
+
if (debug)
|
|
1400
|
+
console.log('[AkariSub] Font reload complete');
|
|
1401
|
+
}
|
|
1402
|
+
if (typeof subContent === 'string') {
|
|
1403
|
+
createTrackFromString(subContent);
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
try {
|
|
1407
|
+
createTrackFromBytes(toUint8Array(subContent));
|
|
1408
|
+
}
|
|
1409
|
+
finally {
|
|
1410
|
+
decryptedSubContent?.fill(0);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
syncTotalEventsMetric();
|
|
1414
|
+
firstTrackEventStartTime = getFirstEventStartTime();
|
|
1415
|
+
subtitleColorSpace = libassYCbCrMap[requireApi().getTrackColorSpace(requireHandle())];
|
|
1416
|
+
requireApi().setDropAnimations(requireHandle(), data.dropAllAnimations || 0);
|
|
1417
|
+
if (data.libassMemoryLimit > 0 || data.libassGlyphLimit > 0) {
|
|
1418
|
+
requireApi().setMemoryLimits(requireHandle(), data.libassGlyphLimit || 0, data.libassMemoryLimit || 0);
|
|
1419
|
+
}
|
|
1420
|
+
initPool();
|
|
1421
|
+
ensureRenderCollectBuffer(PREWARM_MAX_IMAGES);
|
|
1422
|
+
try {
|
|
1423
|
+
prewarmRenderer(lastCurrentTime);
|
|
1424
|
+
}
|
|
1425
|
+
catch (e) {
|
|
1426
|
+
if (debug)
|
|
1427
|
+
console.warn('[AkariSub] Prewarm render failed, continuing:', e);
|
|
1428
|
+
}
|
|
1429
|
+
forceNextDemandRender = true;
|
|
1430
|
+
postMessage({ target: 'ready' });
|
|
1431
|
+
postMessage({ target: 'verifyColorSpace', subtitleColorSpace });
|
|
1432
|
+
scheduleFullTrackWarmup();
|
|
1433
|
+
};
|
|
1434
|
+
loadWasm(data.wasmUrl).then(onWasmLoaded).catch((e) => {
|
|
1435
|
+
console.error('[AkariSub] WASM loading failed:', e);
|
|
1436
|
+
postMessage({ target: 'error', error: 'WASM loading failed: ' + (e && e.message ? e.message : String(e)) });
|
|
1437
|
+
});
|
|
1438
|
+
};
|
|
1439
|
+
// =============================================================================
|
|
1440
|
+
// Canvas Management
|
|
1441
|
+
// =============================================================================
|
|
1442
|
+
self.offscreenCanvas = ({ transferable }) => {
|
|
1443
|
+
offCanvas = transferable[0];
|
|
1444
|
+
offCanvasCtx = offCanvas.getContext('2d', { desynchronized: true });
|
|
1445
|
+
if (!asyncRender) {
|
|
1446
|
+
bufferCanvas = new OffscreenCanvas(self.width, self.height);
|
|
1447
|
+
bufferCtx = bufferCanvas.getContext('2d', { desynchronized: true });
|
|
1448
|
+
}
|
|
1449
|
+
offscreenRender = true;
|
|
1450
|
+
};
|
|
1451
|
+
self.detachOffscreen = () => {
|
|
1452
|
+
offCanvas = new OffscreenCanvas(self.width, self.height);
|
|
1453
|
+
offCanvasCtx = offCanvas.getContext('2d', { desynchronized: true });
|
|
1454
|
+
offscreenRender = 'hybrid';
|
|
1455
|
+
};
|
|
1456
|
+
self.canvas = ({ width, height, videoWidth, videoHeight, force }) => {
|
|
1457
|
+
if (width == null)
|
|
1458
|
+
throw new Error('Invalid canvas size specified');
|
|
1459
|
+
self.width = width;
|
|
1460
|
+
self.height = height;
|
|
1461
|
+
if (akariSubHandle)
|
|
1462
|
+
requireApi().resizeCanvas(akariSubHandle, width, height, videoWidth, videoHeight);
|
|
1463
|
+
if (force)
|
|
1464
|
+
render(lastCurrentTime, true);
|
|
1465
|
+
};
|
|
1466
|
+
self.video = ({ currentTime, isPaused, rate: newRate }) => {
|
|
1467
|
+
if (currentTime != null)
|
|
1468
|
+
setCurrentTime(currentTime);
|
|
1469
|
+
if (isPaused != null)
|
|
1470
|
+
setIsPaused(isPaused);
|
|
1471
|
+
if (newRate != null)
|
|
1472
|
+
rate = newRate;
|
|
1473
|
+
};
|
|
1474
|
+
self.destroy = () => {
|
|
1475
|
+
stopWarmup();
|
|
1476
|
+
fullTrackWarmupPromise = null;
|
|
1477
|
+
firstTrackEventStartTime = null;
|
|
1478
|
+
if (_Module) {
|
|
1479
|
+
if (rrcBufPtr) {
|
|
1480
|
+
_Module._free(rrcBufPtr);
|
|
1481
|
+
rrcBufPtr = 0;
|
|
1482
|
+
rrcBufCapacity = 0;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (akariSubHandle) {
|
|
1486
|
+
requireApi().destroy(akariSubHandle);
|
|
1487
|
+
akariSubHandle = 0;
|
|
1488
|
+
}
|
|
1489
|
+
metrics.totalEvents = 0;
|
|
1490
|
+
};
|
|
1491
|
+
self.setAsyncRender = ({ value }) => {
|
|
1492
|
+
asyncRender = value && typeof createImageBitmap !== 'undefined';
|
|
1493
|
+
};
|
|
1494
|
+
// =============================================================================
|
|
1495
|
+
// Event Management
|
|
1496
|
+
// =============================================================================
|
|
1497
|
+
const applyEventFields = (index, event) => {
|
|
1498
|
+
const api = requireApi();
|
|
1499
|
+
const handle = requireHandle();
|
|
1500
|
+
for (const key of Object.keys(event)) {
|
|
1501
|
+
const value = event[key];
|
|
1502
|
+
if (value == null || key === '_index')
|
|
1503
|
+
continue;
|
|
1504
|
+
if (key in EVENT_INT_FIELDS) {
|
|
1505
|
+
api.eventSetInt(handle, index, EVENT_INT_FIELDS[key], Number(value));
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
if (key in EVENT_STR_FIELDS) {
|
|
1509
|
+
withCString(String(value), (ptr) => {
|
|
1510
|
+
api.eventSetStr(handle, index, EVENT_STR_FIELDS[key], ptr);
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
const readEvent = (index) => {
|
|
1516
|
+
const api = requireApi();
|
|
1517
|
+
const handle = requireHandle();
|
|
1518
|
+
return {
|
|
1519
|
+
Start: api.eventGetInt(handle, index, EVENT_INT_FIELDS.Start),
|
|
1520
|
+
Duration: api.eventGetInt(handle, index, EVENT_INT_FIELDS.Duration),
|
|
1521
|
+
ReadOrder: api.eventGetInt(handle, index, EVENT_INT_FIELDS.ReadOrder),
|
|
1522
|
+
Layer: api.eventGetInt(handle, index, EVENT_INT_FIELDS.Layer),
|
|
1523
|
+
Style: String(api.eventGetInt(handle, index, EVENT_INT_FIELDS.Style)),
|
|
1524
|
+
MarginL: api.eventGetInt(handle, index, EVENT_INT_FIELDS.MarginL),
|
|
1525
|
+
MarginR: api.eventGetInt(handle, index, EVENT_INT_FIELDS.MarginR),
|
|
1526
|
+
MarginV: api.eventGetInt(handle, index, EVENT_INT_FIELDS.MarginV),
|
|
1527
|
+
Name: readCString(api.eventGetStr(handle, index, EVENT_STR_FIELDS.Name)),
|
|
1528
|
+
Text: readCString(api.eventGetStr(handle, index, EVENT_STR_FIELDS.Text)),
|
|
1529
|
+
Effect: readCString(api.eventGetStr(handle, index, EVENT_STR_FIELDS.Effect))
|
|
1530
|
+
};
|
|
1531
|
+
};
|
|
1532
|
+
const applyStyleFields = (index, style) => {
|
|
1533
|
+
const api = requireApi();
|
|
1534
|
+
const handle = requireHandle();
|
|
1535
|
+
for (const key of Object.keys(style)) {
|
|
1536
|
+
const value = style[key];
|
|
1537
|
+
if (value == null)
|
|
1538
|
+
continue;
|
|
1539
|
+
if (key in STYLE_NUM_FIELDS) {
|
|
1540
|
+
api.styleSetNum(handle, index, STYLE_NUM_FIELDS[key], Number(value));
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
if (key in STYLE_STR_FIELDS) {
|
|
1544
|
+
withCString(String(value), (ptr) => {
|
|
1545
|
+
api.styleSetStr(handle, index, STYLE_STR_FIELDS[key], ptr);
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
const readStyle = (index) => {
|
|
1551
|
+
const api = requireApi();
|
|
1552
|
+
const handle = requireHandle();
|
|
1553
|
+
return {
|
|
1554
|
+
Name: readCString(api.styleGetStr(handle, index, STYLE_STR_FIELDS.Name)),
|
|
1555
|
+
FontName: readCString(api.styleGetStr(handle, index, STYLE_STR_FIELDS.FontName)),
|
|
1556
|
+
FontSize: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.FontSize),
|
|
1557
|
+
PrimaryColour: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.PrimaryColour),
|
|
1558
|
+
SecondaryColour: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.SecondaryColour),
|
|
1559
|
+
OutlineColour: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.OutlineColour),
|
|
1560
|
+
BackColour: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.BackColour),
|
|
1561
|
+
Bold: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Bold),
|
|
1562
|
+
Italic: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Italic),
|
|
1563
|
+
Underline: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Underline),
|
|
1564
|
+
StrikeOut: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.StrikeOut),
|
|
1565
|
+
ScaleX: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.ScaleX),
|
|
1566
|
+
ScaleY: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.ScaleY),
|
|
1567
|
+
Spacing: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Spacing),
|
|
1568
|
+
Angle: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Angle),
|
|
1569
|
+
BorderStyle: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.BorderStyle),
|
|
1570
|
+
Outline: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Outline),
|
|
1571
|
+
Shadow: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Shadow),
|
|
1572
|
+
Alignment: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Alignment),
|
|
1573
|
+
MarginL: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.MarginL),
|
|
1574
|
+
MarginR: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.MarginR),
|
|
1575
|
+
MarginV: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.MarginV),
|
|
1576
|
+
Encoding: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Encoding),
|
|
1577
|
+
treat_fontname_as_pattern: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.treat_fontname_as_pattern),
|
|
1578
|
+
Blur: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Blur),
|
|
1579
|
+
Justify: api.styleGetNum(handle, index, STYLE_NUM_FIELDS.Justify)
|
|
1580
|
+
};
|
|
1581
|
+
};
|
|
1582
|
+
self.createEvent = ({ event }) => {
|
|
1583
|
+
const index = requireApi().allocEvent(requireHandle());
|
|
1584
|
+
if (index >= 0)
|
|
1585
|
+
applyEventFields(index, event);
|
|
1586
|
+
syncTotalEventsMetric();
|
|
1587
|
+
};
|
|
1588
|
+
self.getEvents = () => {
|
|
1589
|
+
const events = [];
|
|
1590
|
+
const api = requireApi();
|
|
1591
|
+
const count = api.getEventCount(requireHandle());
|
|
1592
|
+
for (let i = 0; i < count; i++) {
|
|
1593
|
+
const event = { ...readEvent(i), _index: i };
|
|
1594
|
+
if (protectedTrackContent) {
|
|
1595
|
+
event.Name = '';
|
|
1596
|
+
event.Effect = '';
|
|
1597
|
+
event.Text = '';
|
|
1598
|
+
}
|
|
1599
|
+
events.push(event);
|
|
1600
|
+
}
|
|
1601
|
+
postMessage({ target: 'getEvents', events });
|
|
1602
|
+
};
|
|
1603
|
+
self.setEvent = ({ event, index }) => {
|
|
1604
|
+
applyEventFields(index, event);
|
|
1605
|
+
};
|
|
1606
|
+
self.removeEvent = ({ index }) => {
|
|
1607
|
+
requireApi().removeEvent(requireHandle(), index);
|
|
1608
|
+
syncTotalEventsMetric();
|
|
1609
|
+
};
|
|
1610
|
+
// =============================================================================
|
|
1611
|
+
// Style Management
|
|
1612
|
+
// =============================================================================
|
|
1613
|
+
self.createStyle = ({ style }) => {
|
|
1614
|
+
const index = requireApi().allocStyle(requireHandle());
|
|
1615
|
+
if (index >= 0)
|
|
1616
|
+
applyStyleFields(index, style);
|
|
1617
|
+
return index;
|
|
1618
|
+
};
|
|
1619
|
+
self.getStyles = () => {
|
|
1620
|
+
const styles = [];
|
|
1621
|
+
const api = requireApi();
|
|
1622
|
+
const count = api.getStyleCount(requireHandle());
|
|
1623
|
+
for (let i = 0; i < count; i++) {
|
|
1624
|
+
styles.push(readStyle(i));
|
|
1625
|
+
}
|
|
1626
|
+
postMessage({ target: 'getStyles', time: Date.now(), styles });
|
|
1627
|
+
};
|
|
1628
|
+
self.setStyle = ({ style, index }) => {
|
|
1629
|
+
applyStyleFields(index, style);
|
|
1630
|
+
};
|
|
1631
|
+
self.removeStyle = ({ index }) => {
|
|
1632
|
+
requireApi().removeStyle(requireHandle(), index);
|
|
1633
|
+
};
|
|
1634
|
+
self.styleOverride = (data) => {
|
|
1635
|
+
const index = self.createStyle(data);
|
|
1636
|
+
if (typeof index === 'number' && index >= 0) {
|
|
1637
|
+
requireApi().styleOverrideIndex(requireHandle(), index);
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
self.disableStyleOverride = () => {
|
|
1641
|
+
requireApi().disableStyleOverride(requireHandle());
|
|
1642
|
+
};
|
|
1643
|
+
self.defaultFont = ({ font }) => {
|
|
1644
|
+
withCString(font, (fontPtr) => {
|
|
1645
|
+
requireApi().setDefaultFont(requireHandle(), fontPtr);
|
|
1646
|
+
});
|
|
1647
|
+
};
|
|
1648
|
+
// =============================================================================
|
|
1649
|
+
// Performance Metrics
|
|
1650
|
+
// =============================================================================
|
|
1651
|
+
self.getStats = () => {
|
|
1652
|
+
const avgRenderTime = metrics.framesRendered > 0 ? metrics.totalRenderTime / metrics.framesRendered : 0;
|
|
1653
|
+
postMessage({
|
|
1654
|
+
target: 'getStats',
|
|
1655
|
+
stats: {
|
|
1656
|
+
framesRendered: metrics.framesRendered,
|
|
1657
|
+
framesDropped: metrics.framesDropped,
|
|
1658
|
+
avgRenderTime: Math.round(avgRenderTime * 100) / 100,
|
|
1659
|
+
maxRenderTime: Math.round(metrics.maxRenderTime * 100) / 100,
|
|
1660
|
+
minRenderTime: metrics.minRenderTime === Infinity ? 0 : Math.round(metrics.minRenderTime * 100) / 100,
|
|
1661
|
+
lastRenderTime: Math.round(metrics.lastRenderTime * 100) / 100,
|
|
1662
|
+
pendingRenders: Math.max(0, metrics.pendingRenders),
|
|
1663
|
+
totalEvents: metrics.totalEvents,
|
|
1664
|
+
cacheHits: metrics.cacheHits,
|
|
1665
|
+
cacheMisses: metrics.cacheMisses
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
};
|
|
1669
|
+
self.resetStats = () => {
|
|
1670
|
+
resetMetrics();
|
|
1671
|
+
postMessage({ target: 'resetStats', success: true });
|
|
1672
|
+
};
|
|
1673
|
+
self.getEventCount = () => {
|
|
1674
|
+
const count = akariSubHandle ? requireApi().getEventCount(akariSubHandle) : 0;
|
|
1675
|
+
postMessage({ target: 'getEventCount', count });
|
|
1676
|
+
};
|
|
1677
|
+
self.getStyleCount = () => {
|
|
1678
|
+
const count = akariSubHandle ? requireApi().getStyleCount(akariSubHandle) : 0;
|
|
1679
|
+
postMessage({ target: 'getStyleCount', count });
|
|
1680
|
+
};
|
|
1681
|
+
// =============================================================================
|
|
1682
|
+
// Message Handler
|
|
1683
|
+
// =============================================================================
|
|
1684
|
+
onmessage = ({ data }) => {
|
|
1685
|
+
if (!self[data.target]) {
|
|
1686
|
+
throw new Error('Unknown event target ' + data.target);
|
|
1687
|
+
}
|
|
1688
|
+
Promise.resolve(self[data.target](data)).catch((error) => {
|
|
1689
|
+
postMessage({
|
|
1690
|
+
target: 'error',
|
|
1691
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1692
|
+
});
|
|
1693
|
+
});
|
|
1694
|
+
};
|
|
1695
|
+
//# sourceMappingURL=worker.js.map
|