akarisub 0.2.0 → 0.2.2
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/LICENSE +3 -0
- package/README.md +56 -54
- package/THIRD_PARTY_NOTICES.md +1590 -0
- package/dist/COPYRIGHT +1588 -949
- 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 +225 -0
- package/dist/ts/ts/akarisub.d.ts.map +1 -0
- package/dist/ts/ts/akarisub.js +1073 -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 +52 -0
- package/dist/ts/ts/webgl2-renderer.d.ts.map +1 -0
- package/dist/ts/ts/webgl2-renderer.js +391 -0
- package/dist/ts/ts/webgl2-renderer.js.map +1 -0
- package/dist/ts/ts/webgpu-renderer.d.ts +62 -0
- package/dist/ts/ts/webgpu-renderer.d.ts.map +1 -0
- package/dist/ts/ts/webgpu-renderer.js +577 -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 +1762 -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 +9 -6
- package/src/ts/akarisub.ts +48 -12
- package/src/ts/types.ts +22 -7
- package/src/ts/webgl2-renderer.ts +19 -13
- package/src/ts/webgpu-renderer.ts +26 -71
- package/src/ts/worker.ts +303 -70
package/src/ts/worker.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ASSEvent,
|
|
13
13
|
ASSStyle,
|
|
14
14
|
AkariSubModule,
|
|
15
|
+
EncryptedSubtitleContent,
|
|
15
16
|
SubtitleColorSpace,
|
|
16
17
|
WorkerInboundMessage
|
|
17
18
|
} from './types'
|
|
@@ -143,6 +144,8 @@ interface AkariSubApi {
|
|
|
143
144
|
styleSetNum: (handle: number, index: number, field: number, value: number) => void
|
|
144
145
|
styleGetStr: (handle: number, index: number, field: number) => number
|
|
145
146
|
styleSetStr: (handle: number, index: number, field: number, valuePtr: number) => void
|
|
147
|
+
getEventTimeRange: (handle: number, outPtr: number) => number
|
|
148
|
+
getEmptyWindow: (handle: number, tm: number, outPtr: number) => number
|
|
146
149
|
renderBlendCollect: (handle: number, time: number, force: number, outPtr: number, maxItems: number) => number
|
|
147
150
|
renderImageCollect: (handle: number, time: number, force: number, outPtr: number, maxItems: number) => number
|
|
148
151
|
}
|
|
@@ -161,7 +164,7 @@ const FULL_WARMUP_CAP_SECONDS = 30
|
|
|
161
164
|
const FULL_WARMUP_STEP_SECONDS = 1
|
|
162
165
|
const FULL_WARMUP_YIELD_EVERY = 24
|
|
163
166
|
const ASS_TIME_SCALE = 1000
|
|
164
|
-
const imagePool: RenderResultItem[] =
|
|
167
|
+
const imagePool: RenderResultItem[] = []
|
|
165
168
|
let poolInitialized = false
|
|
166
169
|
// Batch render-collect buffer: 3 header ints (changed, count, time) + 5 ints per image (x, y, w, h, image_ptr)
|
|
167
170
|
const RRC_HEADER_INTS = 3
|
|
@@ -169,6 +172,13 @@ const RRC_IMG_STRIDE = 5
|
|
|
169
172
|
// Pre-allocated buffer for batch render-collect calls
|
|
170
173
|
let rrcBufPtr = 0
|
|
171
174
|
let rrcBufCapacity = 0
|
|
175
|
+
// Cached views over the render-collect buffer; refreshed when the wasm memory
|
|
176
|
+
// grows (buffer identity changes) or the buffer is reallocated/resized.
|
|
177
|
+
let rrcHeaderView: Int32Array | null = null
|
|
178
|
+
let rrcMetaView: Int32Array | null = null
|
|
179
|
+
let rrcViewsBuffer: ArrayBufferLike | null = null
|
|
180
|
+
let rrcViewsPtr = 0
|
|
181
|
+
let rrcViewsCapacity = 0
|
|
172
182
|
const frameImages: RenderResultItem[] = []
|
|
173
183
|
const frameArrayBuffers: ArrayBuffer[] = []
|
|
174
184
|
const frameBitmapPromises: Promise<ImageBitmap>[] = []
|
|
@@ -178,6 +188,7 @@ let warmupEndTime = 0
|
|
|
178
188
|
let warmupEnabled = false
|
|
179
189
|
let firstTrackEventStartTime: number | null = null
|
|
180
190
|
let fullTrackWarmupPromise: Promise<void> | null = null
|
|
191
|
+
let protectedTrackContent = false
|
|
181
192
|
|
|
182
193
|
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
|
183
194
|
|
|
@@ -198,10 +209,13 @@ const initPool = (): void => {
|
|
|
198
209
|
}
|
|
199
210
|
|
|
200
211
|
const getPooledItem = (index: number): RenderResultItem => {
|
|
201
|
-
|
|
202
|
-
|
|
212
|
+
let item = imagePool[index]
|
|
213
|
+
if (!item) {
|
|
214
|
+
// Grow the pool on demand so frames with many images reuse items too
|
|
215
|
+
item = { w: 0, h: 0, x: 0, y: 0, image: 0 }
|
|
216
|
+
imagePool[index] = item
|
|
203
217
|
}
|
|
204
|
-
return
|
|
218
|
+
return item
|
|
205
219
|
}
|
|
206
220
|
|
|
207
221
|
/**
|
|
@@ -232,6 +246,22 @@ const ensureRenderCollectBuffer = (maxImages: number): void => {
|
|
|
232
246
|
rrcBufCapacity = nextCapacity
|
|
233
247
|
}
|
|
234
248
|
|
|
249
|
+
const refreshRrcViews = (): void => {
|
|
250
|
+
const buffer = self.wasmMemory.buffer
|
|
251
|
+
if (rrcHeaderView && rrcViewsBuffer === buffer && rrcViewsPtr === rrcBufPtr && rrcViewsCapacity === rrcBufCapacity) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
rrcHeaderView = new Int32Array(buffer, rrcBufPtr, RRC_HEADER_INTS)
|
|
255
|
+
rrcMetaView = new Int32Array(
|
|
256
|
+
buffer,
|
|
257
|
+
rrcBufPtr + RRC_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT,
|
|
258
|
+
rrcBufCapacity - RRC_HEADER_INTS
|
|
259
|
+
)
|
|
260
|
+
rrcViewsBuffer = buffer
|
|
261
|
+
rrcViewsPtr = rrcBufPtr
|
|
262
|
+
rrcViewsCapacity = rrcBufCapacity
|
|
263
|
+
}
|
|
264
|
+
|
|
235
265
|
const prewarmRenderer = (time: number): void => {
|
|
236
266
|
if (!akariSubHandle) return
|
|
237
267
|
|
|
@@ -250,55 +280,52 @@ const syncTotalEventsMetric = (): void => {
|
|
|
250
280
|
metrics.totalEvents = akariSubHandle ? requireApi().getEventCount(akariSubHandle) : 0
|
|
251
281
|
}
|
|
252
282
|
|
|
253
|
-
const
|
|
254
|
-
if (!akariSubHandle) return null
|
|
283
|
+
const getTrackEventTimeRange = (): { start: number; end: number } | null => {
|
|
284
|
+
if (!akariSubHandle || !_Module) return null
|
|
255
285
|
|
|
256
286
|
const api = requireApi()
|
|
257
287
|
const handle = requireHandle()
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
288
|
+
const outPtr = _Module._malloc(2 * Int32Array.BYTES_PER_ELEMENT)
|
|
289
|
+
if (!outPtr) return null
|
|
260
290
|
|
|
261
|
-
|
|
291
|
+
try {
|
|
292
|
+
if (!api.getEventTimeRange(handle, outPtr)) return null
|
|
262
293
|
|
|
263
|
-
|
|
264
|
-
const start =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
294
|
+
const view = new Int32Array(self.wasmMemory.buffer, outPtr, 2)
|
|
295
|
+
const start = Math.max(0, view[0] / ASS_TIME_SCALE)
|
|
296
|
+
const end = Math.max(start, view[1] / ASS_TIME_SCALE)
|
|
297
|
+
return { start, end }
|
|
298
|
+
} finally {
|
|
299
|
+
_Module._free(outPtr)
|
|
268
300
|
}
|
|
269
|
-
|
|
270
|
-
if (!Number.isFinite(firstStart)) return null
|
|
271
|
-
return Math.max(0, firstStart)
|
|
272
301
|
}
|
|
273
302
|
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const api = requireApi()
|
|
278
|
-
const handle = requireHandle()
|
|
279
|
-
const count = api.getEventCount(handle)
|
|
280
|
-
if (count <= 0) return null
|
|
303
|
+
const getFirstEventStartTime = (): number | null => {
|
|
304
|
+
return getTrackEventTimeRange()?.start ?? null
|
|
305
|
+
}
|
|
281
306
|
|
|
282
|
-
|
|
283
|
-
|
|
307
|
+
let emptyWindowFrom = -1
|
|
308
|
+
let emptyWindowUntil = -1
|
|
284
309
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
310
|
+
const invalidateEmptyWindow = (): void => {
|
|
311
|
+
emptyWindowFrom = -1
|
|
312
|
+
emptyWindowUntil = -1
|
|
313
|
+
}
|
|
288
314
|
|
|
289
|
-
|
|
315
|
+
const computeEmptyWindow = (time: number): void => {
|
|
316
|
+
if (!akariSubHandle || !_Module) return
|
|
290
317
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (eventEnd > end) end = eventEnd
|
|
294
|
-
}
|
|
318
|
+
const outPtr = _Module._malloc(Int32Array.BYTES_PER_ELEMENT)
|
|
319
|
+
if (!outPtr) return
|
|
295
320
|
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
try {
|
|
322
|
+
if (!requireApi().getEmptyWindow(requireHandle(), time, outPtr)) return
|
|
298
323
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
324
|
+
const nextStart = new Int32Array(self.wasmMemory.buffer, outPtr, 1)[0]
|
|
325
|
+
emptyWindowFrom = time
|
|
326
|
+
emptyWindowUntil = nextStart < 0 ? Number.POSITIVE_INFINITY : nextStart / ASS_TIME_SCALE
|
|
327
|
+
} finally {
|
|
328
|
+
_Module._free(outPtr)
|
|
302
329
|
}
|
|
303
330
|
}
|
|
304
331
|
|
|
@@ -497,6 +524,101 @@ const withCString = <T>(input: string, callback: (ptr: number) => T): T => {
|
|
|
497
524
|
}
|
|
498
525
|
}
|
|
499
526
|
|
|
527
|
+
const toUint8Array = (content: Uint8Array | ArrayBuffer): Uint8Array => {
|
|
528
|
+
return content instanceof Uint8Array ? content : new Uint8Array(content)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const isBinaryContent = (content: string | Uint8Array | ArrayBuffer): content is Uint8Array | ArrayBuffer => {
|
|
532
|
+
return content instanceof Uint8Array || content instanceof ArrayBuffer
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const withCBytes = <T>(input: Uint8Array, callback: (ptr: number) => T): T => {
|
|
536
|
+
if (!_Module) throw new Error('AkariSub module is not initialized')
|
|
537
|
+
|
|
538
|
+
const ptr = _Module._malloc(input.length + 1)
|
|
539
|
+
if (!ptr) throw new Error('Failed to allocate subtitle content')
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
self.HEAPU8.set(input, ptr)
|
|
543
|
+
self.HEAPU8[ptr + input.length] = 0
|
|
544
|
+
return callback(ptr)
|
|
545
|
+
} finally {
|
|
546
|
+
self.HEAPU8.fill(0, ptr, ptr + input.length + 1)
|
|
547
|
+
_Module._free(ptr)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const decryptV2Payload = async (encrypted: ArrayBuffer, contentKey: CryptoKey): Promise<Uint8Array> => {
|
|
552
|
+
const data = new Uint8Array(encrypted)
|
|
553
|
+
const keyIdSize = 8
|
|
554
|
+
const nonceSize = 12
|
|
555
|
+
const headerSize = 1 + keyIdSize + nonceSize
|
|
556
|
+
|
|
557
|
+
if (data.length < headerSize + 16) {
|
|
558
|
+
throw new Error('Ciphertext too short for v2 subtitle payload')
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (data[0] !== 2) {
|
|
562
|
+
throw new Error('Unsupported encrypted subtitle protocol version')
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const header = data.subarray(0, 1 + keyIdSize)
|
|
566
|
+
const nonce = data.subarray(1 + keyIdSize, headerSize)
|
|
567
|
+
const ciphertext = data.subarray(headerSize)
|
|
568
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
569
|
+
{
|
|
570
|
+
name: 'AES-GCM',
|
|
571
|
+
iv: nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength),
|
|
572
|
+
additionalData: header.buffer.slice(header.byteOffset, header.byteOffset + header.byteLength),
|
|
573
|
+
tagLength: 128
|
|
574
|
+
},
|
|
575
|
+
contentKey,
|
|
576
|
+
ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength)
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
return new Uint8Array(decrypted)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const decryptSubtitleContent = async (content: EncryptedSubtitleContent): Promise<Uint8Array> => {
|
|
583
|
+
if (content.encrypted) {
|
|
584
|
+
return decryptV2Payload(content.encrypted, content.contentKey)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const chunks = content.encryptedChunks || []
|
|
588
|
+
if (chunks.length === 0) {
|
|
589
|
+
throw new Error('Encrypted subtitle content is empty')
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const decryptedChunks = await Promise.all(chunks.map((chunk) => decryptV2Payload(chunk, content.contentKey)))
|
|
593
|
+
const totalLength = decryptedChunks.reduce((sum, chunk) => sum + chunk.length, 0)
|
|
594
|
+
const result = new Uint8Array(totalLength)
|
|
595
|
+
let offset = 0
|
|
596
|
+
|
|
597
|
+
for (const chunk of decryptedChunks) {
|
|
598
|
+
result.set(chunk, offset)
|
|
599
|
+
chunk.fill(0)
|
|
600
|
+
offset += chunk.length
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return result
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const createTrackFromBytes = (content: Uint8Array): void => {
|
|
607
|
+
const api = requireApi()
|
|
608
|
+
const handle = requireHandle()
|
|
609
|
+
withCBytes(content, (contentPtr) => {
|
|
610
|
+
api.createTrackMem(handle, contentPtr)
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const createTrackFromString = (content: string): void => {
|
|
615
|
+
const api = requireApi()
|
|
616
|
+
const handle = requireHandle()
|
|
617
|
+
withCString(content, (contentPtr) => {
|
|
618
|
+
api.createTrackMem(handle, contentPtr)
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
|
|
500
622
|
const requireApi = (): AkariSubApi => {
|
|
501
623
|
if (!akariSubApi) throw new Error('AkariSub API is not initialized')
|
|
502
624
|
return akariSubApi
|
|
@@ -733,25 +855,49 @@ const readAsync = (url: string, load: (data: ArrayBuffer) => void, err: (e: any)
|
|
|
733
855
|
// Track Management
|
|
734
856
|
// =============================================================================
|
|
735
857
|
|
|
736
|
-
|
|
858
|
+
const finishTrackLoad = (): void => {
|
|
859
|
+
const api = requireApi()
|
|
860
|
+
const handle = requireHandle()
|
|
861
|
+
syncTotalEventsMetric()
|
|
862
|
+
firstTrackEventStartTime = getFirstEventStartTime()
|
|
863
|
+
subtitleColorSpace = libassYCbCrMap[api.getTrackColorSpace(handle)]
|
|
864
|
+
forceNextDemandRender = true
|
|
865
|
+
postMessage({ target: 'verifyColorSpace', subtitleColorSpace })
|
|
866
|
+
postMessage({ target: 'trackReady' })
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
self.setTrack = ({ content }: { content: string | Uint8Array | ArrayBuffer }): void => {
|
|
737
870
|
stopWarmup()
|
|
738
871
|
fullTrackWarmupPromise = null
|
|
872
|
+
protectedTrackContent = false
|
|
873
|
+
|
|
874
|
+
if (isBinaryContent(content)) {
|
|
875
|
+
createTrackFromBytes(toUint8Array(content))
|
|
876
|
+
finishTrackLoad()
|
|
877
|
+
return
|
|
878
|
+
}
|
|
739
879
|
|
|
740
880
|
processAvailableFonts(content)
|
|
741
881
|
|
|
742
882
|
if (clampPos) content = fixPlayRes(content)
|
|
743
883
|
if (dropAllBlur) content = dropBlur(content)
|
|
744
884
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
885
|
+
createTrackFromString(content)
|
|
886
|
+
finishTrackLoad()
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
self.setEncryptedTrack = async ({ content }: { content: EncryptedSubtitleContent }): Promise<void> => {
|
|
890
|
+
stopWarmup()
|
|
891
|
+
fullTrackWarmupPromise = null
|
|
892
|
+
protectedTrackContent = true
|
|
893
|
+
|
|
894
|
+
const decrypted = await decryptSubtitleContent(content)
|
|
895
|
+
try {
|
|
896
|
+
createTrackFromBytes(decrypted)
|
|
897
|
+
} finally {
|
|
898
|
+
decrypted.fill(0)
|
|
899
|
+
}
|
|
900
|
+
finishTrackLoad()
|
|
755
901
|
}
|
|
756
902
|
|
|
757
903
|
self.getColorSpace = (): void => {
|
|
@@ -762,6 +908,7 @@ self.freeTrack = (): void => {
|
|
|
762
908
|
stopWarmup()
|
|
763
909
|
fullTrackWarmupPromise = null
|
|
764
910
|
firstTrackEventStartTime = null
|
|
911
|
+
protectedTrackContent = false
|
|
765
912
|
const api = requireApi()
|
|
766
913
|
const handle = requireHandle()
|
|
767
914
|
api.removeTrack(handle)
|
|
@@ -890,6 +1037,13 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
890
1037
|
return
|
|
891
1038
|
}
|
|
892
1039
|
|
|
1040
|
+
// Inside a known-empty window the output cannot change: skip the WASM call
|
|
1041
|
+
if (!force && emptyWindowFrom >= 0 && time >= emptyWindowFrom && time < emptyWindowUntil) {
|
|
1042
|
+
metrics.cacheHits++
|
|
1043
|
+
postMessage({ target: 'unbusy' })
|
|
1044
|
+
return
|
|
1045
|
+
}
|
|
1046
|
+
|
|
893
1047
|
renderInFlight = true
|
|
894
1048
|
initPool() // Ensure pool is ready
|
|
895
1049
|
|
|
@@ -910,9 +1064,15 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
910
1064
|
? api.renderBlendCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity)
|
|
911
1065
|
: api.renderImageCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity)
|
|
912
1066
|
|
|
913
|
-
|
|
1067
|
+
// Refresh after the WASM call: rendering may have grown the memory
|
|
1068
|
+
refreshRrcViews()
|
|
1069
|
+
const headerView = rrcHeaderView!
|
|
914
1070
|
const changed = headerView[0]
|
|
915
|
-
|
|
1071
|
+
|
|
1072
|
+
// Frame produced no images: ask how long renders can be skipped.
|
|
1073
|
+
if (headerView[1] === 0 && (emptyWindowFrom < 0 || time < emptyWindowFrom || time >= emptyWindowUntil)) {
|
|
1074
|
+
computeEmptyWindow(time)
|
|
1075
|
+
}
|
|
916
1076
|
|
|
917
1077
|
// Update metrics
|
|
918
1078
|
const renderEndTime = performance.now()
|
|
@@ -947,8 +1107,7 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
947
1107
|
|
|
948
1108
|
if (written === 0) return paintImages({ images, buffers, times })
|
|
949
1109
|
|
|
950
|
-
const
|
|
951
|
-
const meta = new Int32Array(self.wasmMemory.buffer, imgDataOffset, written * RRC_IMG_STRIDE)
|
|
1110
|
+
const meta = rrcMetaView!
|
|
952
1111
|
|
|
953
1112
|
const useAsyncBitmapPath = asyncRender && offscreenRender !== true
|
|
954
1113
|
|
|
@@ -967,7 +1126,12 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
967
1126
|
|
|
968
1127
|
const pointer = meta[metaOffset + 4]
|
|
969
1128
|
const byteLength = item.w * item.h * 4
|
|
970
|
-
|
|
1129
|
+
let rawData = new Uint8ClampedArray(self.wasmMemory.buffer, pointer, byteLength)
|
|
1130
|
+
if (hasBitmapBug) {
|
|
1131
|
+
// Browsers with the partial-bitmap bug mis-render ImageData backed
|
|
1132
|
+
// by a view into a larger buffer; give them a standalone copy.
|
|
1133
|
+
rawData = rawData.slice()
|
|
1134
|
+
}
|
|
971
1135
|
|
|
972
1136
|
const imageData = new ImageData(rawData as Uint8ClampedArray<ArrayBuffer>, item.w, item.h)
|
|
973
1137
|
|
|
@@ -997,6 +1161,20 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
997
1161
|
}
|
|
998
1162
|
})
|
|
999
1163
|
} else {
|
|
1164
|
+
// When posting to the main thread, copy all image pixels into one
|
|
1165
|
+
// transferable buffer instead of allocating/transferring one per image.
|
|
1166
|
+
let copyTarget: Uint8ClampedArray<ArrayBuffer> | null = null
|
|
1167
|
+
let copyOffset = 0
|
|
1168
|
+
if (!offCanvasCtx) {
|
|
1169
|
+
let totalBytes = 0
|
|
1170
|
+
for (let i = 0; i < written; ++i) {
|
|
1171
|
+
const metaOffset = i * RRC_IMG_STRIDE
|
|
1172
|
+
totalBytes += meta[metaOffset + 2] * meta[metaOffset + 3] * 4
|
|
1173
|
+
}
|
|
1174
|
+
copyTarget = new Uint8ClampedArray(totalBytes)
|
|
1175
|
+
buffers.push(copyTarget.buffer)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1000
1178
|
for (let i = 0; i < written; ++i) {
|
|
1001
1179
|
const metaOffset = i * RRC_IMG_STRIDE
|
|
1002
1180
|
const item = getPooledItem(i)
|
|
@@ -1006,13 +1184,13 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
1006
1184
|
item.h = meta[metaOffset + 3]
|
|
1007
1185
|
item.image = meta[metaOffset + 4]
|
|
1008
1186
|
|
|
1009
|
-
if (
|
|
1187
|
+
if (copyTarget) {
|
|
1010
1188
|
const imagePtr = item.image as number
|
|
1011
1189
|
const byteLength = item.w * item.h * 4
|
|
1012
|
-
const copiedData =
|
|
1190
|
+
const copiedData = copyTarget.subarray(copyOffset, copyOffset + byteLength)
|
|
1013
1191
|
copiedData.set(self.HEAPU8C.subarray(imagePtr, imagePtr + byteLength))
|
|
1014
|
-
buffers.push(copiedData.buffer)
|
|
1015
1192
|
item.image = copiedData
|
|
1193
|
+
copyOffset += byteLength
|
|
1016
1194
|
}
|
|
1017
1195
|
images[i] = item
|
|
1018
1196
|
}
|
|
@@ -1231,6 +1409,8 @@ self.init = async (data: any): Promise<void> => {
|
|
|
1231
1409
|
styleSetNum: Module._akarisub_style_set_num,
|
|
1232
1410
|
styleGetStr: Module._akarisub_style_get_str,
|
|
1233
1411
|
styleSetStr: Module._akarisub_style_set_str,
|
|
1412
|
+
getEventTimeRange: Module._akarisub_get_event_time_range,
|
|
1413
|
+
getEmptyWindow: Module._akarisub_get_empty_window,
|
|
1234
1414
|
renderBlendCollect: Module._akarisub_render_blend_collect,
|
|
1235
1415
|
renderImageCollect: Module._akarisub_render_image_collect
|
|
1236
1416
|
}
|
|
@@ -1420,19 +1600,34 @@ self.init = async (data: any): Promise<void> => {
|
|
|
1420
1600
|
}
|
|
1421
1601
|
|
|
1422
1602
|
let subContent = data.subContent
|
|
1423
|
-
|
|
1603
|
+
let decryptedSubContent: Uint8Array | null = null
|
|
1604
|
+
|
|
1605
|
+
if (data.encryptedSubContent) {
|
|
1606
|
+
protectedTrackContent = true
|
|
1607
|
+
decryptedSubContent = await decryptSubtitleContent(data.encryptedSubContent)
|
|
1608
|
+
subContent = decryptedSubContent
|
|
1609
|
+
} else {
|
|
1610
|
+
protectedTrackContent = false
|
|
1611
|
+
if (!subContent) subContent = read_(data.subUrl) as string
|
|
1612
|
+
}
|
|
1424
1613
|
|
|
1425
1614
|
// For large files, emit partial_ready early to allow playback to start
|
|
1426
1615
|
// while font loading and track parsing continues in the background
|
|
1427
|
-
const isLargeSubtitle = subContent
|
|
1616
|
+
const isLargeSubtitle = typeof subContent === 'string'
|
|
1617
|
+
? subContent.length > 500000
|
|
1618
|
+
: toUint8Array(subContent).byteLength > 500000
|
|
1428
1619
|
if (isLargeSubtitle) {
|
|
1429
1620
|
postMessage({ target: 'partial_ready' })
|
|
1430
1621
|
if (debug) console.log('[AkariSub] Large subtitle detected, emitting partial_ready early')
|
|
1431
1622
|
}
|
|
1432
1623
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1624
|
+
if (typeof subContent === 'string') {
|
|
1625
|
+
processAvailableFonts(subContent)
|
|
1626
|
+
if (clampPos) subContent = fixPlayRes(subContent)
|
|
1627
|
+
if (dropAllBlur) subContent = dropBlur(subContent)
|
|
1628
|
+
} else if (debug && (clampPos || dropAllBlur)) {
|
|
1629
|
+
console.warn('[AkariSub] Text rewrite options are skipped for protected binary subtitle content')
|
|
1630
|
+
}
|
|
1436
1631
|
|
|
1437
1632
|
// Load attached/preloaded fonts before ready to avoid runtime font churn during first playback.
|
|
1438
1633
|
let hasAttachedFonts = false
|
|
@@ -1491,9 +1686,15 @@ self.init = async (data: any): Promise<void> => {
|
|
|
1491
1686
|
if (debug) console.log('[AkariSub] Font reload complete')
|
|
1492
1687
|
}
|
|
1493
1688
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1689
|
+
if (typeof subContent === 'string') {
|
|
1690
|
+
createTrackFromString(subContent)
|
|
1691
|
+
} else {
|
|
1692
|
+
try {
|
|
1693
|
+
createTrackFromBytes(toUint8Array(subContent))
|
|
1694
|
+
} finally {
|
|
1695
|
+
decryptedSubContent?.fill(0)
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1497
1698
|
syncTotalEventsMetric()
|
|
1498
1699
|
firstTrackEventStartTime = getFirstEventStartTime()
|
|
1499
1700
|
subtitleColorSpace = libassYCbCrMap[requireApi().getTrackColorSpace(requireHandle())]
|
|
@@ -1589,6 +1790,11 @@ self.destroy = (): void => {
|
|
|
1589
1790
|
_Module._free(rrcBufPtr)
|
|
1590
1791
|
rrcBufPtr = 0
|
|
1591
1792
|
rrcBufCapacity = 0
|
|
1793
|
+
rrcHeaderView = null
|
|
1794
|
+
rrcMetaView = null
|
|
1795
|
+
rrcViewsBuffer = null
|
|
1796
|
+
rrcViewsPtr = 0
|
|
1797
|
+
rrcViewsCapacity = 0
|
|
1592
1798
|
}
|
|
1593
1799
|
}
|
|
1594
1800
|
if (akariSubHandle) {
|
|
@@ -1708,7 +1914,13 @@ self.getEvents = (): void => {
|
|
|
1708
1914
|
const api = requireApi()
|
|
1709
1915
|
const count = api.getEventCount(requireHandle())
|
|
1710
1916
|
for (let i = 0; i < count; i++) {
|
|
1711
|
-
|
|
1917
|
+
const event = { ...readEvent(i), _index: i }
|
|
1918
|
+
if (protectedTrackContent) {
|
|
1919
|
+
event.Name = ''
|
|
1920
|
+
event.Effect = ''
|
|
1921
|
+
event.Text = ''
|
|
1922
|
+
}
|
|
1923
|
+
events.push(event)
|
|
1712
1924
|
}
|
|
1713
1925
|
postMessage({ target: 'getEvents', events })
|
|
1714
1926
|
}
|
|
@@ -1810,10 +2022,31 @@ self.getStyleCount = (): void => {
|
|
|
1810
2022
|
// Message Handler
|
|
1811
2023
|
// =============================================================================
|
|
1812
2024
|
|
|
2025
|
+
const RENDER_SAFE_TARGETS = new Set([
|
|
2026
|
+
'demand',
|
|
2027
|
+
'video',
|
|
2028
|
+
'getEvents',
|
|
2029
|
+
'getStyles',
|
|
2030
|
+
'getStats',
|
|
2031
|
+
'resetStats',
|
|
2032
|
+
'getEventCount',
|
|
2033
|
+
'getStyleCount',
|
|
2034
|
+
'getColorSpace'
|
|
2035
|
+
])
|
|
2036
|
+
|
|
1813
2037
|
onmessage = ({ data }: MessageEvent): void => {
|
|
1814
|
-
if (self[data.target]) {
|
|
1815
|
-
self[data.target](data)
|
|
1816
|
-
} else {
|
|
2038
|
+
if (!self[data.target]) {
|
|
1817
2039
|
throw new Error('Unknown event target ' + data.target)
|
|
1818
2040
|
}
|
|
2041
|
+
|
|
2042
|
+
if (!RENDER_SAFE_TARGETS.has(data.target)) {
|
|
2043
|
+
invalidateEmptyWindow()
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
Promise.resolve(self[data.target](data)).catch((error) => {
|
|
2047
|
+
postMessage({
|
|
2048
|
+
target: 'error',
|
|
2049
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2050
|
+
})
|
|
2051
|
+
})
|
|
1819
2052
|
}
|