@zenvor/hls.js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +472 -0
  3. package/dist/hls-demo.js +26995 -0
  4. package/dist/hls-demo.js.map +1 -0
  5. package/dist/hls.d.mts +4204 -0
  6. package/dist/hls.d.ts +4204 -0
  7. package/dist/hls.js +40050 -0
  8. package/dist/hls.js.d.ts +4204 -0
  9. package/dist/hls.js.map +1 -0
  10. package/dist/hls.light.js +27145 -0
  11. package/dist/hls.light.js.map +1 -0
  12. package/dist/hls.light.min.js +2 -0
  13. package/dist/hls.light.min.js.map +1 -0
  14. package/dist/hls.light.mjs +26392 -0
  15. package/dist/hls.light.mjs.map +1 -0
  16. package/dist/hls.min.js +2 -0
  17. package/dist/hls.min.js.map +1 -0
  18. package/dist/hls.mjs +38956 -0
  19. package/dist/hls.mjs.map +1 -0
  20. package/dist/hls.worker.js +2 -0
  21. package/dist/hls.worker.js.map +1 -0
  22. package/package.json +143 -0
  23. package/src/config.ts +794 -0
  24. package/src/controller/abr-controller.ts +1019 -0
  25. package/src/controller/algo-data-controller.ts +794 -0
  26. package/src/controller/audio-stream-controller.ts +1099 -0
  27. package/src/controller/audio-track-controller.ts +454 -0
  28. package/src/controller/base-playlist-controller.ts +438 -0
  29. package/src/controller/base-stream-controller.ts +2526 -0
  30. package/src/controller/buffer-controller.ts +2015 -0
  31. package/src/controller/buffer-operation-queue.ts +159 -0
  32. package/src/controller/cap-level-controller.ts +367 -0
  33. package/src/controller/cmcd-controller.ts +422 -0
  34. package/src/controller/content-steering-controller.ts +622 -0
  35. package/src/controller/eme-controller.ts +1617 -0
  36. package/src/controller/error-controller.ts +627 -0
  37. package/src/controller/fps-controller.ts +146 -0
  38. package/src/controller/fragment-finders.ts +256 -0
  39. package/src/controller/fragment-tracker.ts +567 -0
  40. package/src/controller/gap-controller.ts +719 -0
  41. package/src/controller/id3-track-controller.ts +488 -0
  42. package/src/controller/interstitial-player.ts +302 -0
  43. package/src/controller/interstitials-controller.ts +2895 -0
  44. package/src/controller/interstitials-schedule.ts +698 -0
  45. package/src/controller/latency-controller.ts +294 -0
  46. package/src/controller/level-controller.ts +776 -0
  47. package/src/controller/stream-controller.ts +1597 -0
  48. package/src/controller/subtitle-stream-controller.ts +508 -0
  49. package/src/controller/subtitle-track-controller.ts +617 -0
  50. package/src/controller/timeline-controller.ts +677 -0
  51. package/src/crypt/aes-crypto.ts +36 -0
  52. package/src/crypt/aes-decryptor.ts +339 -0
  53. package/src/crypt/decrypter-aes-mode.ts +4 -0
  54. package/src/crypt/decrypter.ts +225 -0
  55. package/src/crypt/fast-aes-key.ts +39 -0
  56. package/src/define-plugin.d.ts +17 -0
  57. package/src/demux/audio/aacdemuxer.ts +126 -0
  58. package/src/demux/audio/ac3-demuxer.ts +170 -0
  59. package/src/demux/audio/adts.ts +249 -0
  60. package/src/demux/audio/base-audio-demuxer.ts +205 -0
  61. package/src/demux/audio/dolby.ts +21 -0
  62. package/src/demux/audio/mp3demuxer.ts +85 -0
  63. package/src/demux/audio/mpegaudio.ts +177 -0
  64. package/src/demux/chunk-cache.ts +42 -0
  65. package/src/demux/dummy-demuxed-track.ts +13 -0
  66. package/src/demux/inject-worker.ts +75 -0
  67. package/src/demux/mp4demuxer.ts +234 -0
  68. package/src/demux/sample-aes.ts +198 -0
  69. package/src/demux/transmuxer-interface.ts +449 -0
  70. package/src/demux/transmuxer-worker.ts +221 -0
  71. package/src/demux/transmuxer.ts +560 -0
  72. package/src/demux/tsdemuxer.ts +1256 -0
  73. package/src/demux/video/avc-video-parser.ts +401 -0
  74. package/src/demux/video/base-video-parser.ts +198 -0
  75. package/src/demux/video/exp-golomb.ts +153 -0
  76. package/src/demux/video/hevc-video-parser.ts +736 -0
  77. package/src/empty-es.js +5 -0
  78. package/src/empty.js +3 -0
  79. package/src/errors.ts +107 -0
  80. package/src/events.ts +548 -0
  81. package/src/exports-default.ts +3 -0
  82. package/src/exports-named.ts +81 -0
  83. package/src/hls.ts +1613 -0
  84. package/src/is-supported.ts +54 -0
  85. package/src/loader/date-range.ts +207 -0
  86. package/src/loader/fragment-loader.ts +403 -0
  87. package/src/loader/fragment.ts +487 -0
  88. package/src/loader/interstitial-asset-list.ts +162 -0
  89. package/src/loader/interstitial-event.ts +337 -0
  90. package/src/loader/key-loader.ts +439 -0
  91. package/src/loader/level-details.ts +203 -0
  92. package/src/loader/level-key.ts +259 -0
  93. package/src/loader/load-stats.ts +17 -0
  94. package/src/loader/m3u8-parser.ts +1072 -0
  95. package/src/loader/playlist-loader.ts +839 -0
  96. package/src/polyfills/number.ts +15 -0
  97. package/src/remux/aac-helper.ts +81 -0
  98. package/src/remux/mp4-generator.ts +1380 -0
  99. package/src/remux/mp4-remuxer.ts +1261 -0
  100. package/src/remux/passthrough-remuxer.ts +434 -0
  101. package/src/task-loop.ts +130 -0
  102. package/src/types/algo.ts +44 -0
  103. package/src/types/buffer.ts +105 -0
  104. package/src/types/component-api.ts +20 -0
  105. package/src/types/demuxer.ts +208 -0
  106. package/src/types/events.ts +574 -0
  107. package/src/types/fragment-tracker.ts +23 -0
  108. package/src/types/level.ts +268 -0
  109. package/src/types/loader.ts +198 -0
  110. package/src/types/media-playlist.ts +92 -0
  111. package/src/types/network-details.ts +3 -0
  112. package/src/types/remuxer.ts +104 -0
  113. package/src/types/track.ts +12 -0
  114. package/src/types/transmuxer.ts +46 -0
  115. package/src/types/tuples.ts +6 -0
  116. package/src/types/vtt.ts +11 -0
  117. package/src/utils/arrays.ts +22 -0
  118. package/src/utils/attr-list.ts +192 -0
  119. package/src/utils/binary-search.ts +46 -0
  120. package/src/utils/buffer-helper.ts +173 -0
  121. package/src/utils/cea-608-parser.ts +1413 -0
  122. package/src/utils/chunker.ts +41 -0
  123. package/src/utils/codecs.ts +314 -0
  124. package/src/utils/cues.ts +96 -0
  125. package/src/utils/discontinuities.ts +174 -0
  126. package/src/utils/encryption-methods-util.ts +21 -0
  127. package/src/utils/error-helper.ts +95 -0
  128. package/src/utils/event-listener-helper.ts +16 -0
  129. package/src/utils/ewma-bandwidth-estimator.ts +97 -0
  130. package/src/utils/ewma.ts +43 -0
  131. package/src/utils/fetch-loader.ts +331 -0
  132. package/src/utils/global.ts +2 -0
  133. package/src/utils/hash.ts +10 -0
  134. package/src/utils/hdr.ts +67 -0
  135. package/src/utils/hex.ts +32 -0
  136. package/src/utils/imsc1-ttml-parser.ts +261 -0
  137. package/src/utils/keysystem-util.ts +45 -0
  138. package/src/utils/level-helper.ts +629 -0
  139. package/src/utils/logger.ts +120 -0
  140. package/src/utils/media-option-attributes.ts +49 -0
  141. package/src/utils/mediacapabilities-helper.ts +301 -0
  142. package/src/utils/mediakeys-helper.ts +210 -0
  143. package/src/utils/mediasource-helper.ts +37 -0
  144. package/src/utils/mp4-tools.ts +1473 -0
  145. package/src/utils/number.ts +3 -0
  146. package/src/utils/numeric-encoding-utils.ts +26 -0
  147. package/src/utils/output-filter.ts +46 -0
  148. package/src/utils/rendition-helper.ts +505 -0
  149. package/src/utils/safe-json-stringify.ts +22 -0
  150. package/src/utils/texttrack-utils.ts +164 -0
  151. package/src/utils/time-ranges.ts +17 -0
  152. package/src/utils/timescale-conversion.ts +46 -0
  153. package/src/utils/utf8-utils.ts +18 -0
  154. package/src/utils/variable-substitution.ts +105 -0
  155. package/src/utils/vttcue.ts +384 -0
  156. package/src/utils/vttparser.ts +497 -0
  157. package/src/utils/webvtt-parser.ts +166 -0
  158. package/src/utils/xhr-loader.ts +337 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,794 @@
1
+ import { decodeMulti } from '@msgpack/msgpack';
2
+ import { buildAbsoluteURL } from 'url-toolkit';
3
+ import { findFragmentByPTS } from './fragment-finders';
4
+ import { Events } from '../events';
5
+ import { PlaylistLevelType } from '../types/loader';
6
+ import {
7
+ getLoaderConfigWithoutReties,
8
+ getRetryDelay,
9
+ shouldRetry,
10
+ } from '../utils/error-helper';
11
+ import type { RetryConfig } from '../config';
12
+ import type Hls from '../hls';
13
+ import type { Fragment, MediaFragment } from '../loader/fragment';
14
+ import type { LevelDetails } from '../loader/level-details';
15
+ import type {
16
+ AipdMessage,
17
+ AlgoChunk,
18
+ AutoCameraItem,
19
+ DetItem,
20
+ FrameItem,
21
+ TrackItem,
22
+ } from '../types/algo';
23
+ import type { NetworkComponentAPI } from '../types/component-api';
24
+ import type {
25
+ AlgoDataErrorData,
26
+ AlgoDataLoadedData,
27
+ AlgoDataLoadingData,
28
+ FragChangedData,
29
+ FragLoadingData,
30
+ LevelLoadedData,
31
+ LevelUpdatedData,
32
+ } from '../types/events';
33
+ import type {
34
+ Loader,
35
+ LoaderCallbacks,
36
+ LoaderConfiguration,
37
+ LoaderContext,
38
+ LoaderResponse,
39
+ LoaderStats,
40
+ } from '../types/loader';
41
+ import type { NullableNetworkDetails } from '../types/network-details';
42
+
43
+ class AlgoDataController implements NetworkComponentAPI {
44
+ private hls: Hls | null;
45
+ private currentLevelDetails: LevelDetails | null = null;
46
+ private algoChunkCache = new Map<number, AlgoChunk>();
47
+ private algoChunkLoading = new Map<number, Loader<LoaderContext>>();
48
+ private algoChunkFailed = new Set<number>();
49
+ private algoChunkRetryCount = new Map<number, number>();
50
+ private algoChunkRetryTimer = new Map<number, number>();
51
+ private started = false;
52
+
53
+ constructor(hls: Hls) {
54
+ this.hls = hls;
55
+ this.registerListeners();
56
+ }
57
+
58
+ public startLoad() {
59
+ this.started = true;
60
+ }
61
+
62
+ public stopLoad() {
63
+ this.started = false;
64
+ this.abortAllLoads();
65
+ }
66
+
67
+ public destroy() {
68
+ this.unregisterListeners();
69
+ this.abortAllLoads();
70
+ this.resetCache();
71
+ this.hls = null;
72
+ }
73
+
74
+ public getFrameByTime(time: number): FrameItem | null {
75
+ return this.resolveFrameByTime(time);
76
+ }
77
+
78
+ public getFrameByIndex(frameIdx: number): FrameItem | null {
79
+ if (!Number.isFinite(frameIdx)) {
80
+ return null;
81
+ }
82
+ const chunk = this.findChunkByFrameIndex(frameIdx);
83
+ if (!chunk) return null;
84
+ const frameOffset = frameIdx - chunk.startFrameIndex;
85
+ if (frameOffset < 0 || frameOffset >= chunk.frames.length) {
86
+ return null;
87
+ }
88
+ return chunk.frames[frameOffset] || null;
89
+ }
90
+
91
+ public isDataReady(time: number): boolean {
92
+ return this.resolveFrameByTime(time) !== null;
93
+ }
94
+
95
+ private resolveFrameByTime(time: number): FrameItem | null {
96
+ const levelDetails = this.getLevelDetails();
97
+ if (!levelDetails || !Number.isFinite(time)) {
98
+ return null;
99
+ }
100
+ const frag = this.findFragmentByTime(levelDetails, time);
101
+ if (!frag || time < frag.start) {
102
+ return null;
103
+ }
104
+ const chunk = this.getChunkByFragment(frag);
105
+ if (!chunk || !Number.isFinite(chunk.frameRate) || chunk.frameRate <= 0) {
106
+ return null;
107
+ }
108
+ const frameOffset = Math.floor((time - frag.start) * chunk.frameRate);
109
+ if (frameOffset < 0 || frameOffset >= chunk.frames.length) {
110
+ return null;
111
+ }
112
+ return chunk.frames[frameOffset] || null;
113
+ }
114
+
115
+ public isDataReadyByIndex(frameIdx: number): boolean {
116
+ return this.getFrameByIndex(frameIdx) !== null;
117
+ }
118
+
119
+ public getAllCachedChunks(): AlgoChunk[] {
120
+ return Array.from(this.algoChunkCache.values()).sort((a, b) => {
121
+ if (a.fragSn !== b.fragSn) return a.fragSn - b.fragSn;
122
+ if (a.chunkIndex !== b.chunkIndex) return a.chunkIndex - b.chunkIndex;
123
+ return a.startFrameIndex - b.startFrameIndex;
124
+ });
125
+ }
126
+
127
+ private registerListeners() {
128
+ const hls = this.hls;
129
+ if (!hls) return;
130
+ hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
131
+ hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
132
+ hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
133
+ hls.on(Events.FRAG_CHANGED, this.onFragChanged, this);
134
+ hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
135
+ }
136
+
137
+ private unregisterListeners() {
138
+ const hls = this.hls;
139
+ if (!hls) return;
140
+ hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
141
+ hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
142
+ hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this);
143
+ hls.off(Events.FRAG_CHANGED, this.onFragChanged, this);
144
+ hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
145
+ }
146
+
147
+ private onManifestLoading() {
148
+ this.resetCache();
149
+ }
150
+
151
+ private onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
152
+ this.currentLevelDetails = data.details;
153
+ }
154
+
155
+ private onLevelUpdated(event: Events.LEVEL_UPDATED, data: LevelUpdatedData) {
156
+ this.currentLevelDetails = data.details;
157
+ }
158
+
159
+ private onFragChanged(event: Events.FRAG_CHANGED, data: FragChangedData) {
160
+ if (!this.started) return;
161
+ const frag = data.frag;
162
+ if (frag.type !== PlaylistLevelType.MAIN) return;
163
+ if (!this.currentLevelDetails) return;
164
+ this.preloadFromFragment(frag as MediaFragment);
165
+ }
166
+
167
+ /**
168
+ * 视频分片开始加载时,同步触发对应算法分片的加载。
169
+ * FRAG_CHANGED 仅在播放到新分片时触发,无法覆盖提前缓冲的分片,
170
+ * 因此需要在视频分片开始下载的同一时刻并行加载算法分片,确保一一对应。
171
+ */
172
+ private onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
173
+ if (!this.started) return;
174
+ const frag = data.frag;
175
+ if (frag.type !== PlaylistLevelType.MAIN) return;
176
+ if (!frag.algoRelurl) return;
177
+ this.loadAlgoChunk(frag as MediaFragment);
178
+ }
179
+
180
+ private preloadFromFragment(frag: MediaFragment) {
181
+ const levelDetails = this.getLevelDetails();
182
+ if (!levelDetails) return;
183
+ const startIndex = this.getFragmentIndex(levelDetails, frag);
184
+ if (startIndex < 0) return;
185
+
186
+ const preloadCount = this.getPreloadCount();
187
+ for (let offset = 0; offset <= preloadCount; offset += 1) {
188
+ const target = levelDetails.fragments[startIndex + offset];
189
+ if (!target?.algoRelurl) continue;
190
+ this.loadAlgoChunk(target as MediaFragment);
191
+ }
192
+ }
193
+
194
+ private getFragmentIndex(
195
+ levelDetails: LevelDetails,
196
+ frag: MediaFragment,
197
+ ): number {
198
+ if (typeof frag.sn === 'number') {
199
+ const index = frag.sn - levelDetails.startSN;
200
+ if (levelDetails.fragments[index]?.sn === frag.sn) {
201
+ return index;
202
+ }
203
+ }
204
+ return levelDetails.fragments.findIndex((item) => item?.sn === frag.sn);
205
+ }
206
+
207
+ private loadAlgoChunk(frag: MediaFragment) {
208
+ if (!this.hls || !frag.algoRelurl) return;
209
+
210
+ const key = this.getAlgoChunkKey(frag);
211
+ if (this.shouldSkipLoad(key)) return;
212
+
213
+ const algoUrl = this.resolveAlgoUrl(frag);
214
+ if (!algoUrl) {
215
+ this.reportAlgoError(frag, '', new Error('算法分片地址解析失败'));
216
+ return;
217
+ }
218
+
219
+ this.startAlgoLoad(frag, algoUrl, key);
220
+ }
221
+
222
+ private shouldSkipLoad(key: number): boolean {
223
+ return (
224
+ this.algoChunkCache.has(key) ||
225
+ this.algoChunkLoading.has(key) ||
226
+ this.algoChunkFailed.has(key) ||
227
+ this.algoChunkRetryTimer.has(key)
228
+ );
229
+ }
230
+
231
+ private startAlgoLoad(frag: MediaFragment, algoUrl: string, key: number) {
232
+ if (!this.started || !this.hls) return;
233
+ const hls = this.hls as Hls;
234
+ const loader = this.createLoader();
235
+ const loaderConfig = this.createLoaderConfig();
236
+ this.algoChunkLoading.set(key, loader);
237
+
238
+ const loadingData: AlgoDataLoadingData = {
239
+ frag,
240
+ url: algoUrl,
241
+ };
242
+ hls.trigger(Events.ALGO_DATA_LOADING, loadingData);
243
+
244
+ const callbacks = this.createAlgoCallbacks(frag, algoUrl, key, loader);
245
+ loader.load(
246
+ {
247
+ responseType: 'arraybuffer',
248
+ url: algoUrl,
249
+ },
250
+ loaderConfig,
251
+ callbacks,
252
+ );
253
+ }
254
+
255
+ private createAlgoCallbacks(
256
+ frag: MediaFragment,
257
+ algoUrl: string,
258
+ key: number,
259
+ loader: Loader<LoaderContext>,
260
+ ): LoaderCallbacks<LoaderContext> {
261
+ return {
262
+ onSuccess: (response, stats, context, networkDetails) => {
263
+ this.cleanupLoader(key, loader);
264
+ this.handleAlgoLoaded(
265
+ frag,
266
+ algoUrl,
267
+ response.data as ArrayBuffer,
268
+ stats,
269
+ networkDetails,
270
+ );
271
+ },
272
+ onError: (error, context, networkDetails, stats) => {
273
+ this.cleanupLoader(key, loader);
274
+ const response: LoaderResponse = {
275
+ url: context.url,
276
+ data: undefined,
277
+ code: error.code,
278
+ };
279
+ const retried = this.retryAlgoLoad(frag, algoUrl, key, false, response);
280
+ if (retried) return;
281
+ this.reportAlgoError(
282
+ frag,
283
+ algoUrl,
284
+ new Error(
285
+ `算法分片加载失败:HTTP ${error.code} ${error.text} (${context.url})`,
286
+ ),
287
+ stats,
288
+ networkDetails,
289
+ );
290
+ },
291
+ onTimeout: (stats, context, networkDetails) => {
292
+ this.cleanupLoader(key, loader);
293
+ const retried = this.retryAlgoLoad(frag, algoUrl, key, true);
294
+ if (retried) return;
295
+ this.reportAlgoError(
296
+ frag,
297
+ algoUrl,
298
+ new Error(`算法分片加载超时 (${context.url})`),
299
+ stats,
300
+ networkDetails,
301
+ );
302
+ },
303
+ };
304
+ }
305
+
306
+ private handleAlgoLoaded(
307
+ frag: MediaFragment,
308
+ algoUrl: string,
309
+ payload: ArrayBuffer,
310
+ stats: LoaderStats,
311
+ networkDetails: NullableNetworkDetails,
312
+ ) {
313
+ const hls = this.hls;
314
+ if (!hls) return;
315
+
316
+ let message: AipdMessage;
317
+ try {
318
+ message = this.parseAipdMessage(payload);
319
+ } catch (error) {
320
+ this.reportAlgoError(
321
+ frag,
322
+ algoUrl,
323
+ error as Error,
324
+ stats,
325
+ networkDetails,
326
+ );
327
+ return;
328
+ }
329
+
330
+ const chunk = this.buildAlgoChunk(frag, algoUrl, message);
331
+ const key = this.getAlgoChunkKey(frag);
332
+ this.algoChunkCache.set(key, chunk);
333
+ this.clearRetryState(key);
334
+ this.evictCache();
335
+
336
+ const loadedData: AlgoDataLoadedData = {
337
+ frag,
338
+ url: algoUrl,
339
+ chunk,
340
+ stats,
341
+ networkDetails,
342
+ };
343
+ hls.trigger(Events.ALGO_DATA_LOADED, loadedData);
344
+ }
345
+
346
+ private reportAlgoError(
347
+ frag: MediaFragment,
348
+ algoUrl: string,
349
+ error: Error,
350
+ stats?: LoaderStats,
351
+ networkDetails?: NullableNetworkDetails,
352
+ ) {
353
+ const hls = this.hls;
354
+ if (!hls) return;
355
+
356
+ const key = this.getAlgoChunkKey(frag);
357
+ this.algoChunkFailed.add(key);
358
+
359
+ const errorData: AlgoDataErrorData = {
360
+ frag,
361
+ url: algoUrl,
362
+ error,
363
+ reason: error.message,
364
+ stats,
365
+ networkDetails,
366
+ };
367
+ hls.trigger(Events.ALGO_DATA_ERROR, errorData);
368
+ }
369
+
370
+ private parseAipdMessage(payload: ArrayBuffer): AipdMessage {
371
+ const decodedItems = this.decodeMultiItems(payload);
372
+ const root = this.extractRootFields(decodedItems);
373
+
374
+ if (!Array.isArray(root.framesRaw)) {
375
+ throw new Error('算法帧数据不是数组');
376
+ }
377
+
378
+ const frames = this.parseFrames(root.framesRaw);
379
+
380
+ return {
381
+ version: Number(root.version) || 0,
382
+ chunkIndex: Number(root.chunkIndex) || 0,
383
+ frameSize: frames.length,
384
+ frames,
385
+ };
386
+ }
387
+
388
+ private extractRootFields(decodedItems: unknown[]): {
389
+ version: unknown;
390
+ chunkIndex: unknown;
391
+ framesRaw: unknown;
392
+ } {
393
+ // Format 1: 4 sequential msgpack values [version, chunkIndex, frameSize, frames]
394
+ if (decodedItems.length === 4) {
395
+ return {
396
+ version: decodedItems[0],
397
+ chunkIndex: decodedItems[1],
398
+ framesRaw: decodedItems[3],
399
+ };
400
+ }
401
+
402
+ // Format 2: single msgpack array [version, chunkIndex, frames] (no frameSize)
403
+ // decodeMulti returns 1 item which is the array
404
+ if (
405
+ decodedItems.length === 1 &&
406
+ Array.isArray(decodedItems[0]) &&
407
+ decodedItems[0].length >= 3
408
+ ) {
409
+ const arr = decodedItems[0];
410
+ return {
411
+ version: arr[0],
412
+ chunkIndex: arr[1],
413
+ framesRaw: arr.length === 3 ? arr[2] : arr[3],
414
+ };
415
+ }
416
+
417
+ throw new Error('算法数据结构不正确');
418
+ }
419
+
420
+ private parseFrames(framesRaw: unknown[]): FrameItem[] {
421
+ return framesRaw.map((raw) => this.parseFrameItem(raw));
422
+ }
423
+
424
+ private parseFrameItem(raw: unknown): FrameItem {
425
+ const [frameIdx, autoCameraRaw, tracksRaw, detectionsRaw] =
426
+ this.decodeBinAsSequence(raw, '算法帧', 4);
427
+
428
+ const autoCameras = this.parseAutoCamera(autoCameraRaw);
429
+ const tracks = this.parseTrackList(tracksRaw);
430
+ const detections = this.parseDetList(detectionsRaw);
431
+
432
+ return {
433
+ frameIdx: Number(frameIdx) || 0,
434
+ autoCameras,
435
+ tracks,
436
+ detections,
437
+ };
438
+ }
439
+
440
+ private parseAutoCamera(raw: unknown): AutoCameraItem {
441
+ // Format 2: already decoded flat array [x, y, focus, r0, r1, r2, r3]
442
+ if (Array.isArray(raw) && raw.length === 7) {
443
+ return {
444
+ x: Number(raw[0]) || 0,
445
+ y: Number(raw[1]) || 0,
446
+ focus: Number(raw[2]) || 0,
447
+ reserved: [
448
+ Number(raw[3]) || 0,
449
+ Number(raw[4]) || 0,
450
+ Number(raw[5]) || 0,
451
+ Number(raw[6]) || 0,
452
+ ],
453
+ };
454
+ }
455
+ // Format 1: binary blob or [x, y, focus, [reserved]]
456
+ const [x, y, focus, reserved] = this.decodeBinAsSequence(
457
+ raw,
458
+ '算法相机',
459
+ 4,
460
+ );
461
+ this.ensureFixedArray(reserved, 4, '算法相机 reserved');
462
+ const reservedArray = (reserved as unknown[]).map((v) => Number(v) || 0);
463
+ return {
464
+ x: Number(x) || 0,
465
+ y: Number(y) || 0,
466
+ focus: Number(focus) || 0,
467
+ reserved: reservedArray as [number, number, number, number],
468
+ };
469
+ }
470
+
471
+ private parseTrackItem(raw: unknown): TrackItem {
472
+ const [trackId, score, boxRaw, reserved] = this.decodeBinAsSequence(
473
+ raw,
474
+ 'Track',
475
+ 4,
476
+ );
477
+ const box = this.parseBox(boxRaw, 'Track box');
478
+ this.ensureFixedArray(reserved, 4, 'Track reserved');
479
+ const reservedArray = (reserved as unknown[]).map((v) => Number(v) || 0);
480
+
481
+ return {
482
+ trackId: Number(trackId) || 0,
483
+ score: Number(score) || 0,
484
+ box,
485
+ reserved: reservedArray as [number, number, number, number],
486
+ };
487
+ }
488
+
489
+ private parseDetItem(raw: unknown): DetItem {
490
+ const [classId, score, boxRaw, reserved] = this.decodeBinAsSequence(
491
+ raw,
492
+ 'Det',
493
+ 4,
494
+ );
495
+ const box = this.parseBox(boxRaw, 'Det box');
496
+ this.ensureFixedArray(reserved, 4, 'Det reserved');
497
+ const reservedArray = (reserved as unknown[]).map((v) => Number(v) || 0);
498
+
499
+ return {
500
+ classId: Number(classId) || 0,
501
+ score: Number(score) || 0,
502
+ box,
503
+ reserved: reservedArray as [number, number, number, number],
504
+ };
505
+ }
506
+
507
+ private parseBox(
508
+ raw: unknown,
509
+ name: string,
510
+ ): [number, number, number, number] {
511
+ if (!Array.isArray(raw) || raw.length !== 4) {
512
+ throw new Error(`${name} 长度不正确`);
513
+ }
514
+ return [
515
+ Number(raw[0]) || 0,
516
+ Number(raw[1]) || 0,
517
+ Number(raw[2]) || 0,
518
+ Number(raw[3]) || 0,
519
+ ];
520
+ }
521
+
522
+ private decodeMultiItems(payload: ArrayBuffer | Uint8Array): unknown[] {
523
+ try {
524
+ const input =
525
+ payload instanceof Uint8Array ? payload : new Uint8Array(payload);
526
+ const result = decodeMulti(input);
527
+ return Array.isArray(result) ? result : Array.from(result);
528
+ } catch (error) {
529
+ const reason = error instanceof Error ? error.message : String(error);
530
+ throw new Error(`算法数据解包失败: ${reason}`);
531
+ }
532
+ }
533
+
534
+ private decodeBinAsSequence(
535
+ value: unknown,
536
+ name: string,
537
+ expectedLength: number,
538
+ ): unknown[] {
539
+ // Format 2: already decoded array
540
+ if (Array.isArray(value)) {
541
+ if (value.length !== expectedLength) {
542
+ throw new Error(`${name} 结构不正确`);
543
+ }
544
+ return value;
545
+ }
546
+ // Format 1: binary blob, needs msgpack decoding
547
+ if (!(value instanceof Uint8Array)) {
548
+ throw new Error(`${name} 数据格式不支持`);
549
+ }
550
+ const items = this.decodeMultiItems(value);
551
+ if (items.length !== expectedLength) {
552
+ throw new Error(`${name} 结构不正确`);
553
+ }
554
+ return items;
555
+ }
556
+
557
+ private ensureFixedArray(
558
+ value: unknown,
559
+ length: number,
560
+ name: string,
561
+ ): asserts value is unknown[] {
562
+ if (!Array.isArray(value) || value.length !== length) {
563
+ throw new Error(`${name} 长度不正确`);
564
+ }
565
+ }
566
+
567
+ private parseTrackList(value: unknown): TrackItem[] {
568
+ if (!Array.isArray(value)) {
569
+ throw new Error('tracks_ 不是数组');
570
+ }
571
+ return value.map((item) => this.parseTrackItem(item));
572
+ }
573
+
574
+ private parseDetList(value: unknown): DetItem[] {
575
+ if (!Array.isArray(value)) {
576
+ throw new Error('detections_ 不是数组');
577
+ }
578
+ return value.map((item) => this.parseDetItem(item));
579
+ }
580
+
581
+ private buildAlgoChunk(
582
+ frag: MediaFragment,
583
+ algoUrl: string,
584
+ message: AipdMessage,
585
+ ): AlgoChunk {
586
+ const hls = this.hls;
587
+ const logger = hls?.logger;
588
+
589
+ this.checkFrameSequence(message.frames, message.chunkIndex, logger);
590
+
591
+ const frameCount = message.frames.length;
592
+ const configFrameRate = hls?.config.algoFrameRate;
593
+ const frameRate =
594
+ Number.isFinite(configFrameRate) && configFrameRate! > 0
595
+ ? (configFrameRate as number)
596
+ : frag.duration > 0
597
+ ? frameCount / frag.duration
598
+ : 0;
599
+
600
+ return {
601
+ fragSn: typeof frag.sn === 'number' ? frag.sn : -1,
602
+ algoUrl,
603
+ chunkIndex: message.chunkIndex,
604
+ frameSize: message.frameSize,
605
+ frameRate,
606
+ startFrameIndex: message.frames[0]?.frameIdx ?? 1,
607
+ frames: message.frames,
608
+ };
609
+ }
610
+
611
+ private checkFrameSequence(
612
+ frames: FrameItem[],
613
+ chunkIndex: number,
614
+ logger?: { warn: (msg: string) => void },
615
+ ) {
616
+ if (frames.length <= 1) return;
617
+ let prevIndex = frames[0]?.frameIdx ?? 0;
618
+ for (let i = 1; i < frames.length; i += 1) {
619
+ const current = frames[i]?.frameIdx ?? 0;
620
+ if (current !== prevIndex + 1) {
621
+ logger?.warn(
622
+ `[AlgoData] 帧索引不连续,chunkIndex=${chunkIndex} prev=${prevIndex} current=${current}`,
623
+ );
624
+ break;
625
+ }
626
+ prevIndex = current;
627
+ }
628
+ }
629
+
630
+ private getChunkByFragment(frag: MediaFragment): AlgoChunk | null {
631
+ const key = this.getAlgoChunkKey(frag);
632
+ return this.algoChunkCache.get(key) || null;
633
+ }
634
+
635
+ private findChunkByFrameIndex(frameIdx: number): AlgoChunk | null {
636
+ const chunks = Array.from(this.algoChunkCache.values());
637
+ for (let i = 0; i < chunks.length; i += 1) {
638
+ const chunk = chunks[i];
639
+ const frameSize =
640
+ Number.isFinite(chunk.frameSize) && chunk.frameSize > 0
641
+ ? chunk.frameSize
642
+ : chunk.frames.length;
643
+ const start = chunk.startFrameIndex ?? 1;
644
+ const end = start + frameSize - 1;
645
+ if (frameIdx >= start && frameIdx <= end) {
646
+ return chunk;
647
+ }
648
+ }
649
+ return null;
650
+ }
651
+
652
+ private findFragmentByTime(
653
+ levelDetails: LevelDetails,
654
+ time: number,
655
+ ): MediaFragment | null {
656
+ const fragments = levelDetails.fragments.filter(Boolean) as MediaFragment[];
657
+ const maxFragLookUpTolerance = this.hls?.config.maxFragLookUpTolerance ?? 0;
658
+ return findFragmentByPTS(null, fragments, time, maxFragLookUpTolerance);
659
+ }
660
+
661
+ private getAlgoChunkKey(frag: Fragment): number {
662
+ if (typeof frag.sn === 'number') {
663
+ return frag.sn;
664
+ }
665
+ return Math.round(frag.start * 1000);
666
+ }
667
+
668
+ private resolveAlgoUrl(frag: MediaFragment): string | null {
669
+ if (!frag.algoRelurl) return null;
670
+ return buildAbsoluteURL(frag.baseurl, frag.algoRelurl, {
671
+ alwaysNormalize: true,
672
+ });
673
+ }
674
+
675
+ private createLoader(): Loader<LoaderContext> {
676
+ const hls = this.hls as Hls;
677
+ const Loader = hls.config.loader;
678
+ return new Loader(hls.config) as Loader<LoaderContext>;
679
+ }
680
+
681
+ private createLoaderConfig(): LoaderConfiguration {
682
+ const hls = this.hls as Hls;
683
+ const loadPolicy = getLoaderConfigWithoutReties(
684
+ hls.config.fragLoadPolicy.default,
685
+ );
686
+ return {
687
+ loadPolicy,
688
+ timeout: loadPolicy.maxLoadTimeMs,
689
+ maxRetry: 0,
690
+ retryDelay: 0,
691
+ maxRetryDelay: 0,
692
+ };
693
+ }
694
+
695
+ private getPreloadCount(): number {
696
+ const value = this.hls?.config.algoPreloadCount ?? 0;
697
+ return Math.max(0, Math.floor(value));
698
+ }
699
+
700
+ private evictCache() {
701
+ const configSize = this.hls?.config.algoCacheSize ?? 0;
702
+ if (configSize <= 0) return;
703
+ const maxSize = Math.max(1, Math.floor(configSize));
704
+ if (this.algoChunkCache.size <= maxSize) return;
705
+ while (this.algoChunkCache.size > maxSize) {
706
+ const firstKey = this.algoChunkCache.keys().next().value;
707
+ if (firstKey === undefined) break;
708
+ this.algoChunkCache.delete(firstKey);
709
+ }
710
+ }
711
+
712
+ private abortAllLoads() {
713
+ this.algoChunkLoading.forEach((loader) => {
714
+ loader.abort();
715
+ loader.destroy();
716
+ });
717
+ this.algoChunkLoading.clear();
718
+ this.clearAllRetryTimers();
719
+ }
720
+
721
+ private cleanupLoader(key: number, loader: Loader<LoaderContext>) {
722
+ const current = this.algoChunkLoading.get(key);
723
+ if (current === loader) {
724
+ this.algoChunkLoading.delete(key);
725
+ }
726
+ loader.destroy();
727
+ }
728
+
729
+ private resetCache() {
730
+ this.abortAllLoads();
731
+ this.algoChunkCache.clear();
732
+ this.algoChunkFailed.clear();
733
+ this.algoChunkRetryCount.clear();
734
+ }
735
+
736
+ private getLevelDetails(): LevelDetails | null {
737
+ return this.currentLevelDetails || this.hls?.latestLevelDetails || null;
738
+ }
739
+
740
+ private getRetryConfig(isTimeout: boolean): RetryConfig | null {
741
+ const loadPolicy = this.hls?.config.fragLoadPolicy.default;
742
+ if (!loadPolicy) return null;
743
+ return isTimeout ? loadPolicy.timeoutRetry : loadPolicy.errorRetry;
744
+ }
745
+
746
+ private retryAlgoLoad(
747
+ frag: MediaFragment,
748
+ algoUrl: string,
749
+ key: number,
750
+ isTimeout: boolean,
751
+ response?: LoaderResponse,
752
+ ): boolean {
753
+ const retryConfig = this.getRetryConfig(isTimeout);
754
+ if (!retryConfig) return false;
755
+ const retryCount = this.algoChunkRetryCount.get(key) ?? 0;
756
+ if (!shouldRetry(retryConfig, retryCount, isTimeout, response)) {
757
+ return false;
758
+ }
759
+ const delay = getRetryDelay(retryConfig, retryCount);
760
+ this.algoChunkRetryCount.set(key, retryCount + 1);
761
+ this.clearRetryTimer(key);
762
+ const timer = self.setTimeout(() => {
763
+ this.algoChunkRetryTimer.delete(key);
764
+ this.startAlgoLoad(frag, algoUrl, key);
765
+ }, delay);
766
+ this.algoChunkRetryTimer.set(key, timer);
767
+ this.hls?.logger?.warn(
768
+ `[AlgoData] 算法分片加载失败,准备重试(${retryCount + 1}/${retryConfig.maxNumRetry}) ${delay}ms: ${algoUrl}`,
769
+ );
770
+ return true;
771
+ }
772
+
773
+ private clearRetryTimer(key: number) {
774
+ const timer = this.algoChunkRetryTimer.get(key);
775
+ if (timer === undefined) return;
776
+ self.clearTimeout(timer);
777
+ this.algoChunkRetryTimer.delete(key);
778
+ }
779
+
780
+ private clearAllRetryTimers() {
781
+ this.algoChunkRetryTimer.forEach((timer) => {
782
+ self.clearTimeout(timer);
783
+ });
784
+ this.algoChunkRetryTimer.clear();
785
+ }
786
+
787
+ private clearRetryState(key: number) {
788
+ this.algoChunkRetryCount.delete(key);
789
+ this.clearRetryTimer(key);
790
+ this.algoChunkFailed.delete(key);
791
+ }
792
+ }
793
+
794
+ export default AlgoDataController;