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.
Files changed (46) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +56 -54
  3. package/THIRD_PARTY_NOTICES.md +1590 -0
  4. package/dist/COPYRIGHT +1588 -949
  5. package/dist/akarisub-worker.js +2 -2
  6. package/dist/akarisub-worker.wasm +0 -0
  7. package/dist/akarisub.umd.js +3 -3
  8. package/dist/index.js +3 -3
  9. package/dist/ts/index.d.ts +14 -0
  10. package/dist/ts/index.d.ts.map +1 -0
  11. package/dist/ts/index.js +17 -0
  12. package/dist/ts/index.js.map +1 -0
  13. package/dist/ts/ts/akarisub.d.ts +225 -0
  14. package/dist/ts/ts/akarisub.d.ts.map +1 -0
  15. package/dist/ts/ts/akarisub.js +1073 -0
  16. package/dist/ts/ts/akarisub.js.map +1 -0
  17. package/dist/ts/ts/types.d.ts +424 -0
  18. package/dist/ts/ts/types.d.ts.map +1 -0
  19. package/dist/ts/ts/types.js +5 -0
  20. package/dist/ts/ts/types.js.map +1 -0
  21. package/dist/ts/ts/utils.d.ts +78 -0
  22. package/dist/ts/ts/utils.d.ts.map +1 -0
  23. package/dist/ts/ts/utils.js +395 -0
  24. package/dist/ts/ts/utils.js.map +1 -0
  25. package/dist/ts/ts/webgl2-renderer.d.ts +52 -0
  26. package/dist/ts/ts/webgl2-renderer.d.ts.map +1 -0
  27. package/dist/ts/ts/webgl2-renderer.js +391 -0
  28. package/dist/ts/ts/webgl2-renderer.js.map +1 -0
  29. package/dist/ts/ts/webgpu-renderer.d.ts +62 -0
  30. package/dist/ts/ts/webgpu-renderer.d.ts.map +1 -0
  31. package/dist/ts/ts/webgpu-renderer.js +577 -0
  32. package/dist/ts/ts/webgpu-renderer.js.map +1 -0
  33. package/dist/ts/ts/worker.d.ts +6 -0
  34. package/dist/ts/ts/worker.d.ts.map +1 -0
  35. package/dist/ts/ts/worker.js +1762 -0
  36. package/dist/ts/ts/worker.js.map +1 -0
  37. package/dist/ts/wrapper.d.ts +8 -0
  38. package/dist/ts/wrapper.d.ts.map +1 -0
  39. package/dist/ts/wrapper.js +9 -0
  40. package/dist/ts/wrapper.js.map +1 -0
  41. package/package.json +9 -6
  42. package/src/ts/akarisub.ts +48 -12
  43. package/src/ts/types.ts +22 -7
  44. package/src/ts/webgl2-renderer.ts +19 -13
  45. package/src/ts/webgpu-renderer.ts +26 -71
  46. 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[] = new Array(MAX_POOLED_IMAGES)
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
- if (index < MAX_POOLED_IMAGES) {
202
- return imagePool[index]
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 { w: 0, h: 0, x: 0, y: 0, image: 0 }
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 getFirstEventStartTime = (): number | null => {
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 count = api.getEventCount(handle)
259
- if (count <= 0) return null
288
+ const outPtr = _Module._malloc(2 * Int32Array.BYTES_PER_ELEMENT)
289
+ if (!outPtr) return null
260
290
 
261
- let firstStart = Number.POSITIVE_INFINITY
291
+ try {
292
+ if (!api.getEventTimeRange(handle, outPtr)) return null
262
293
 
263
- for (let i = 0; i < count; i++) {
264
- const start = api.eventGetInt(handle, i, EVENT_INT_FIELDS.Start) / ASS_TIME_SCALE
265
- if (Number.isFinite(start) && start < firstStart) {
266
- firstStart = start
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 getTrackEventTimeRange = (): { start: number; end: number } | null => {
275
- if (!akariSubHandle) return null
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
- let start = Number.POSITIVE_INFINITY
283
- let end = 0
307
+ let emptyWindowFrom = -1
308
+ let emptyWindowUntil = -1
284
309
 
285
- for (let i = 0; i < count; i++) {
286
- const eventStart = api.eventGetInt(handle, i, EVENT_INT_FIELDS.Start) / ASS_TIME_SCALE
287
- const eventDuration = Math.max(0, api.eventGetInt(handle, i, EVENT_INT_FIELDS.Duration) / ASS_TIME_SCALE)
310
+ const invalidateEmptyWindow = (): void => {
311
+ emptyWindowFrom = -1
312
+ emptyWindowUntil = -1
313
+ }
288
314
 
289
- if (!Number.isFinite(eventStart)) continue
315
+ const computeEmptyWindow = (time: number): void => {
316
+ if (!akariSubHandle || !_Module) return
290
317
 
291
- const eventEnd = eventStart + eventDuration
292
- if (eventStart < start) start = eventStart
293
- if (eventEnd > end) end = eventEnd
294
- }
318
+ const outPtr = _Module._malloc(Int32Array.BYTES_PER_ELEMENT)
319
+ if (!outPtr) return
295
320
 
296
- if (!Number.isFinite(start)) return null
297
- if (end < start) end = start
321
+ try {
322
+ if (!requireApi().getEmptyWindow(requireHandle(), time, outPtr)) return
298
323
 
299
- return {
300
- start: Math.max(0, start),
301
- end: Math.max(0, end)
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
- self.setTrack = ({ content }: { content: string }): void => {
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
- const api = requireApi()
746
- const handle = requireHandle()
747
- withCString(content, (contentPtr) => {
748
- api.createTrackMem(handle, contentPtr)
749
- })
750
- syncTotalEventsMetric()
751
- firstTrackEventStartTime = getFirstEventStartTime()
752
- subtitleColorSpace = libassYCbCrMap[api.getTrackColorSpace(handle)]
753
- forceNextDemandRender = true
754
- postMessage({ target: 'verifyColorSpace', subtitleColorSpace })
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
- const headerView = new Int32Array(self.wasmMemory.buffer, rrcBufPtr, RRC_HEADER_INTS)
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
- const imageCount = headerView[1]
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 imgDataOffset = rrcBufPtr + RRC_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT
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
- const rawData = new Uint8ClampedArray(self.wasmMemory.buffer, pointer, byteLength)
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 (!offCanvasCtx) {
1187
+ if (copyTarget) {
1010
1188
  const imagePtr = item.image as number
1011
1189
  const byteLength = item.w * item.h * 4
1012
- const copiedData = new Uint8ClampedArray(byteLength)
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
- if (!subContent) subContent = read_(data.subUrl) as string
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.length > 500000
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
- processAvailableFonts(subContent)
1434
- if (clampPos) subContent = fixPlayRes(subContent)
1435
- if (dropAllBlur) subContent = dropBlur(subContent)
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
- withCString(subContent, (subPtr) => {
1495
- requireApi().createTrackMem(requireHandle(), subPtr)
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
- events.push({ ...readEvent(i), _index: i })
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
  }