@give-tech/ec-player 0.0.1-beta.4 → 0.0.1-beta.41

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/dist/index.js CHANGED
@@ -1,8 +1,43 @@
1
+ const moduleCache = /* @__PURE__ */ new Map();
2
+ const loadingPromises = /* @__PURE__ */ new Map();
1
3
  class WASMLoader {
2
4
  constructor() {
3
- this.module = null;
5
+ this.localModule = null;
6
+ this.localPath = null;
4
7
  }
8
+ /**
9
+ * 加载 WASM 模块
10
+ */
5
11
  async load(wasmPath) {
12
+ if (this.localModule && this.localPath === wasmPath) {
13
+ return this.localModule;
14
+ }
15
+ const cachedModule = moduleCache.get(wasmPath);
16
+ if (cachedModule) {
17
+ this.localModule = cachedModule;
18
+ this.localPath = wasmPath;
19
+ return cachedModule;
20
+ }
21
+ const loadingPromise = loadingPromises.get(wasmPath);
22
+ if (loadingPromise) {
23
+ const module = await loadingPromise;
24
+ this.localModule = module;
25
+ this.localPath = wasmPath;
26
+ return module;
27
+ }
28
+ const loadPromise = this.doLoad(wasmPath);
29
+ loadingPromises.set(wasmPath, loadPromise);
30
+ try {
31
+ const module = await loadPromise;
32
+ this.localModule = module;
33
+ this.localPath = wasmPath;
34
+ return module;
35
+ } catch (error) {
36
+ loadingPromises.delete(wasmPath);
37
+ throw error;
38
+ }
39
+ }
40
+ async doLoad(wasmPath) {
6
41
  return new Promise((resolve, reject) => {
7
42
  const script = document.createElement("script");
8
43
  script.src = wasmPath;
@@ -14,30 +49,75 @@ class WASMLoader {
14
49
  reject(new Error("createFFmpegDecoder not found"));
15
50
  return;
16
51
  }
17
- this.module = await createFFmpegDecoder();
18
- if (typeof this.module?._create_decoder !== "function") {
52
+ const module = await createFFmpegDecoder();
53
+ if (typeof module?._create_decoder !== "function") {
19
54
  reject(new Error("Module initialization failed"));
20
55
  return;
21
56
  }
22
- resolve(this.module);
57
+ moduleCache.set(wasmPath, module);
58
+ loadingPromises.delete(wasmPath);
59
+ resolve(module);
23
60
  } catch (e) {
61
+ loadingPromises.delete(wasmPath);
24
62
  reject(new Error(`Failed to initialize decoder: ${e.message}`));
25
63
  }
26
64
  };
27
65
  script.onerror = () => {
66
+ loadingPromises.delete(wasmPath);
28
67
  reject(new Error(`Failed to load script: ${wasmPath}`));
29
68
  };
30
69
  document.head.appendChild(script);
31
70
  });
32
71
  }
33
72
  getModule() {
34
- return this.module;
73
+ return this.localModule;
74
+ }
75
+ /**
76
+ * 预加载 WASM 模块(后台加载,不阻塞)
77
+ */
78
+ static preload(wasmPath) {
79
+ if (moduleCache.has(wasmPath) || loadingPromises.has(wasmPath)) {
80
+ return;
81
+ }
82
+ const loader = new WASMLoader();
83
+ loader.load(wasmPath).catch((err) => {
84
+ console.warn(`[WASMLoader] Preload failed for ${wasmPath}:`, err.message);
85
+ });
86
+ }
87
+ /**
88
+ * 批量预加载
89
+ */
90
+ static preloadAll(wasmPaths) {
91
+ wasmPaths.forEach((path) => this.preload(path));
92
+ }
93
+ /**
94
+ * 检查是否已加载
95
+ */
96
+ static isLoaded(wasmPath) {
97
+ if (wasmPath) {
98
+ return moduleCache.has(wasmPath);
99
+ }
100
+ return moduleCache.size > 0;
101
+ }
102
+ /**
103
+ * 获取已缓存的路径
104
+ */
105
+ static getLoadedPaths() {
106
+ return Array.from(moduleCache.keys());
107
+ }
108
+ /**
109
+ * 重置(仅用于测试)
110
+ */
111
+ static reset() {
112
+ moduleCache.clear();
113
+ loadingPromises.clear();
35
114
  }
36
115
  }
37
116
  class H264Decoder {
38
117
  constructor(wasmLoader) {
39
118
  this.wasmModule = null;
40
119
  this.decoderContext = null;
120
+ this.destroyed = false;
41
121
  this.wasmModule = wasmLoader.getModule();
42
122
  }
43
123
  async init() {
@@ -50,7 +130,7 @@ class H264Decoder {
50
130
  }
51
131
  }
52
132
  decode(nalUnit) {
53
- if (this.decoderContext === null || !this.wasmModule) {
133
+ if (this.destroyed || this.decoderContext === null || !this.wasmModule) {
54
134
  return null;
55
135
  }
56
136
  const dataPtr = this.wasmModule._malloc(nalUnit.size);
@@ -87,6 +167,18 @@ class H264Decoder {
87
167
  }
88
168
  return null;
89
169
  }
170
+ /**
171
+ * 销毁解码器,释放 WASM 资源
172
+ */
173
+ destroy() {
174
+ if (this.destroyed) return;
175
+ this.destroyed = true;
176
+ if (this.decoderContext !== null && this.wasmModule?._destroy_decoder) {
177
+ this.wasmModule._destroy_decoder(this.decoderContext);
178
+ }
179
+ this.decoderContext = null;
180
+ this.wasmModule = null;
181
+ }
90
182
  yuv420pToRgba(yPtr, uPtr, vPtr, width, height, yLineSize, uLineSize, vLineSize) {
91
183
  const rgba = new Uint8Array(width * height * 4);
92
184
  for (let y = 0; y < height; y++) {
@@ -118,9 +210,11 @@ class H264Decoder {
118
210
  }
119
211
  const DEFAULT_PLAYER_CONFIG = {
120
212
  wasmPath: "/wasm/decoder-simd.js",
121
- targetBufferSize: 60,
213
+ targetBufferSize: 90,
214
+ // 90 帧(约 3.6 秒 @25fps),适应高分辨率视频
122
215
  decodeBatchSize: 2,
123
- maxQueueSize: 200,
216
+ maxQueueSize: 300,
217
+ // 增加 sample 队列上限
124
218
  isLive: false
125
219
  };
126
220
  class BasePlayer {
@@ -415,6 +509,10 @@ class fMP4Demuxer {
415
509
  this.timescale = 1e3;
416
510
  this.trackId = 1;
417
511
  this.avcC = null;
512
+ this.hvcC = null;
513
+ this.isHevc = false;
514
+ this.lengthSizeMinusOne = 3;
515
+ this.trackHandlers = /* @__PURE__ */ new Map();
418
516
  }
419
517
  /**
420
518
  * 解析初始化段 (ftyp + moov)
@@ -433,7 +531,10 @@ class fMP4Demuxer {
433
531
  return {
434
532
  trackId: this.trackId,
435
533
  timescale: this.timescale,
436
- avcC: this.avcC || void 0
534
+ avcC: this.avcC || void 0,
535
+ hvcC: this.hvcC || void 0,
536
+ isHevc: this.isHevc,
537
+ lengthSizeMinusOne: this.lengthSizeMinusOne
437
538
  };
438
539
  }
439
540
  /**
@@ -458,33 +559,56 @@ class fMP4Demuxer {
458
559
  parseTrak(data, trakOffset, trakSize) {
459
560
  let offset = trakOffset + 8;
460
561
  const endOffset = trakOffset + trakSize;
562
+ let currentTrackId = null;
563
+ let handlerType = null;
564
+ console.log("[fMP4Demuxer] parseTrak called, size:", trakSize);
461
565
  while (offset < endOffset - 8) {
462
566
  const boxSize = readUint32$1(data, offset);
463
567
  const boxType = readFourCC(data, offset + 4);
464
568
  if (boxSize < 8) break;
465
- if (boxType === "mdia") {
466
- this.parseMdia(data, offset, boxSize);
569
+ if (boxType === "tkhd") {
570
+ const boxDataStart = offset + 8;
571
+ const version = data[boxDataStart];
572
+ if (version === 1) {
573
+ currentTrackId = readUint32$1(data, boxDataStart + 1 + 3 + 8 + 8);
574
+ } else {
575
+ currentTrackId = readUint32$1(data, boxDataStart + 1 + 3 + 4 + 4);
576
+ }
577
+ console.log("[fMP4Demuxer] tkhd: version=", version, "trackId=", currentTrackId);
578
+ } else if (boxType === "mdia") {
579
+ handlerType = this.parseMdiaAndGetHandlerType(data, offset, boxSize);
580
+ if (handlerType && currentTrackId !== null) {
581
+ this.trackHandlers.set(currentTrackId, handlerType);
582
+ console.log("[fMP4Demuxer] Track", currentTrackId, "handler:", handlerType);
583
+ if (handlerType === "vide") {
584
+ this.trackId = currentTrackId;
585
+ }
586
+ }
467
587
  }
468
588
  offset += boxSize;
469
589
  }
470
590
  }
471
591
  /**
472
- * 解析 mdia box
592
+ * 解析 mdia box 并返回 handler type
473
593
  */
474
- parseMdia(data, mdiaOffset, mdiaSize) {
594
+ parseMdiaAndGetHandlerType(data, mdiaOffset, mdiaSize) {
475
595
  let offset = mdiaOffset + 8;
476
596
  const endOffset = mdiaOffset + mdiaSize;
597
+ let handlerType = null;
477
598
  while (offset < endOffset - 8) {
478
599
  const boxSize = readUint32$1(data, offset);
479
600
  const boxType = readFourCC(data, offset + 4);
480
601
  if (boxSize < 8) break;
481
602
  if (boxType === "mdhd") {
482
603
  this.parseMdhd(data, offset + 8);
483
- } else if (boxType === "minf") {
604
+ } else if (boxType === "hdlr") {
605
+ handlerType = readFourCC(data, offset + 8 + 8);
606
+ } else if (boxType === "minf" && handlerType === "vide") {
484
607
  this.parseMinf(data, offset, boxSize);
485
608
  }
486
609
  offset += boxSize;
487
610
  }
611
+ return handlerType;
488
612
  }
489
613
  /**
490
614
  * 解析 mdhd box 获取 timescale
@@ -530,7 +654,7 @@ class fMP4Demuxer {
530
654
  }
531
655
  }
532
656
  /**
533
- * 解析 stsd box 获取 avcC
657
+ * 解析 stsd box 获取 avcC 或 hvcC
534
658
  */
535
659
  parseStsd(data, stsdOffset, stsdSize) {
536
660
  const boxDataOffset = stsdOffset + 8;
@@ -544,6 +668,10 @@ class fMP4Demuxer {
544
668
  this.parseAvcSampleEntry(data, offset, entrySize);
545
669
  return;
546
670
  }
671
+ if (entryType === "hvc1" || entryType === "hev1") {
672
+ this.parseHevcSampleEntry(data, offset, entrySize);
673
+ return;
674
+ }
547
675
  offset += entrySize;
548
676
  }
549
677
  }
@@ -559,6 +687,33 @@ class fMP4Demuxer {
559
687
  if (boxSize < 8) break;
560
688
  if (boxType === "avcC") {
561
689
  this.avcC = data.slice(offset, offset + boxSize);
690
+ if (boxSize > 12) {
691
+ this.lengthSizeMinusOne = data[offset + 8 + 4] & 3;
692
+ console.log("[fMP4Demuxer] avcC lengthSizeMinusOne:", this.lengthSizeMinusOne, "(NAL length size:", this.lengthSizeMinusOne + 1, "bytes)");
693
+ }
694
+ return;
695
+ }
696
+ offset += boxSize;
697
+ }
698
+ }
699
+ /**
700
+ * 解析 HEVC Sample Entry
701
+ */
702
+ parseHevcSampleEntry(data, entryOffset, entrySize) {
703
+ this.isHevc = true;
704
+ let offset = entryOffset + 8 + 78;
705
+ const endOffset = entryOffset + entrySize;
706
+ while (offset < endOffset - 8) {
707
+ const boxSize = readUint32$1(data, offset);
708
+ const boxType = readFourCC(data, offset + 4);
709
+ if (boxSize < 8) break;
710
+ if (boxType === "hvcC") {
711
+ this.hvcC = data.slice(offset, offset + boxSize);
712
+ if (boxSize > 12) {
713
+ this.lengthSizeMinusOne = data[offset + 8 + 21] & 3;
714
+ console.log("[fMP4Demuxer] hvcC lengthSizeMinusOne:", this.lengthSizeMinusOne, "(NAL length size:", this.lengthSizeMinusOne + 1, "bytes)");
715
+ }
716
+ console.log("[fMP4Demuxer] Found hvcC, size:", boxSize);
562
717
  return;
563
718
  }
564
719
  offset += boxSize;
@@ -714,21 +869,51 @@ class fMP4Demuxer {
714
869
  }
715
870
  /**
716
871
  * 从 moof + mdat 提取视频样本
872
+ * 只提取视频轨道(trackId 匹配)的数据
717
873
  */
718
- extractSamples(moof, mdatData, mdatOffset) {
874
+ extractSamples(moof, mdatData, moofOffset, mdatOffset) {
719
875
  const samples = [];
876
+ console.log(`[fMP4Demuxer] extractSamples: moof.trafs=${moof.trafs.length}, trackHandlers=${JSON.stringify(Object.fromEntries(this.trackHandlers))}`);
720
877
  for (const traf of moof.trafs) {
721
- if (!traf.tfhd || !traf.tfdt) continue;
878
+ if (!traf.tfhd || !traf.tfdt) {
879
+ console.log(`[fMP4Demuxer] traf missing tfhd or tfdt`);
880
+ continue;
881
+ }
882
+ const trackId = traf.tfhd.trackId;
883
+ const handlerType = this.trackHandlers.get(trackId);
884
+ if (handlerType && handlerType !== "vide") {
885
+ console.log(`[fMP4Demuxer] Skipping non-video track: trackId=${trackId}, handler=${handlerType}`);
886
+ continue;
887
+ }
888
+ if (!handlerType) {
889
+ console.log(`[fMP4Demuxer] Unknown trackId=${trackId}, will filter by sample size`);
890
+ } else {
891
+ console.log(`[fMP4Demuxer] Processing video track: trackId=${trackId}, truns=${traf.truns.length}`);
892
+ }
722
893
  let baseDts = traf.tfdt.baseMediaDecodeTime;
894
+ const baseDataOffset = traf.tfhd.baseDataOffset ?? moofOffset;
723
895
  for (const trun of traf.truns) {
724
896
  let dataOffset = 0;
725
897
  if (trun.dataOffset !== void 0) {
726
- dataOffset = trun.dataOffset - mdatOffset - 8;
898
+ const absoluteOffset = baseDataOffset + trun.dataOffset;
899
+ dataOffset = absoluteOffset - mdatOffset - 8;
727
900
  }
728
901
  for (const sample of trun.samples) {
729
- if (sample.size === void 0 || sample.size <= 0) continue;
730
- const sampleData = mdatData.slice(dataOffset, dataOffset + sample.size);
731
- const isSync = (sample.flags ?? 0) & 16777216;
902
+ let sampleSize = sample.size;
903
+ if (sampleSize === void 0 || sampleSize <= 0) {
904
+ sampleSize = mdatData.length - dataOffset;
905
+ }
906
+ if (sampleSize <= 0) {
907
+ console.log(`[fMP4Demuxer] Skipping sample: no data available`);
908
+ continue;
909
+ }
910
+ if (!handlerType && sampleSize < 1e3) {
911
+ dataOffset += sampleSize;
912
+ baseDts += sample.duration ?? 0;
913
+ continue;
914
+ }
915
+ const sampleData = mdatData.slice(dataOffset, dataOffset + sampleSize);
916
+ const isSync = !((sample.flags ?? 0) & 16777216);
732
917
  const cto = sample.compositionTimeOffset ?? 0;
733
918
  const duration = sample.duration ?? 0;
734
919
  samples.push({
@@ -736,9 +921,9 @@ class fMP4Demuxer {
736
921
  dts: baseDts,
737
922
  pts: baseDts + cto,
738
923
  duration,
739
- isSync: isSync !== 0
924
+ isSync
740
925
  });
741
- dataOffset += sample.size;
926
+ dataOffset += sampleSize;
742
927
  baseDts += duration;
743
928
  }
744
929
  }
@@ -757,6 +942,24 @@ class fMP4Demuxer {
757
942
  getAvcC() {
758
943
  return this.avcC;
759
944
  }
945
+ /**
946
+ * 获取 hvcC 配置
947
+ */
948
+ getHvcC() {
949
+ return this.hvcC;
950
+ }
951
+ /**
952
+ * 是否为 HEVC 编码
953
+ */
954
+ isHevcStream() {
955
+ return this.isHevc;
956
+ }
957
+ /**
958
+ * 获取 NAL 长度字段大小 - 1
959
+ */
960
+ getLengthSizeMinusOne() {
961
+ return this.lengthSizeMinusOne;
962
+ }
760
963
  }
761
964
  class NALParser {
762
965
  /**
@@ -804,6 +1007,106 @@ class NALParser {
804
1007
  return units;
805
1008
  }
806
1009
  }
1010
+ const HEVC_NAL_TYPE = {
1011
+ // Picture Parameter Set
1012
+ IDR_W_RADL: 19
1013
+ };
1014
+ class HEVCDecoder {
1015
+ constructor(wasmLoader) {
1016
+ this.wasmModule = null;
1017
+ this.decoderContext = null;
1018
+ this.destroyed = false;
1019
+ this.wasmModule = wasmLoader.getModule();
1020
+ }
1021
+ async init() {
1022
+ if (!this.wasmModule) {
1023
+ throw new Error("WASM module not loaded");
1024
+ }
1025
+ this.decoderContext = this.wasmModule._create_decoder(173);
1026
+ if (this.decoderContext === 0 || this.decoderContext === null) {
1027
+ throw new Error("Failed to create HEVC decoder context");
1028
+ }
1029
+ console.log("[HEVCDecoder] Initialized with codec_id=173");
1030
+ }
1031
+ decode(nalUnit) {
1032
+ if (this.destroyed || this.decoderContext === null || !this.wasmModule) {
1033
+ return null;
1034
+ }
1035
+ const dataPtr = this.wasmModule._malloc(nalUnit.size);
1036
+ this.wasmModule.HEAPU8.set(nalUnit.data, dataPtr);
1037
+ const result = this.wasmModule._decode_video(
1038
+ this.decoderContext,
1039
+ dataPtr,
1040
+ nalUnit.size
1041
+ );
1042
+ this.wasmModule._free(dataPtr);
1043
+ if (result === 1) {
1044
+ const width = this.wasmModule._get_frame_width(this.decoderContext);
1045
+ const height = this.wasmModule._get_frame_height(this.decoderContext);
1046
+ if (width <= 0 || height <= 0) {
1047
+ return null;
1048
+ }
1049
+ const yPtr = this.wasmModule._get_frame_data(this.decoderContext, 0);
1050
+ const uPtr = this.wasmModule._get_frame_data(this.decoderContext, 1);
1051
+ const vPtr = this.wasmModule._get_frame_data(this.decoderContext, 2);
1052
+ const yLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 0);
1053
+ const uLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 1);
1054
+ const vLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 2);
1055
+ const frameData = this.yuv420pToRgba(
1056
+ yPtr,
1057
+ uPtr,
1058
+ vPtr,
1059
+ width,
1060
+ height,
1061
+ yLineSize,
1062
+ uLineSize,
1063
+ vLineSize
1064
+ );
1065
+ return { width, height, data: frameData };
1066
+ }
1067
+ return null;
1068
+ }
1069
+ /**
1070
+ * 销毁解码器,释放 WASM 资源
1071
+ */
1072
+ destroy() {
1073
+ if (this.destroyed) return;
1074
+ this.destroyed = true;
1075
+ if (this.decoderContext !== null && this.wasmModule?._destroy_decoder) {
1076
+ this.wasmModule._destroy_decoder(this.decoderContext);
1077
+ }
1078
+ this.decoderContext = null;
1079
+ this.wasmModule = null;
1080
+ }
1081
+ yuv420pToRgba(yPtr, uPtr, vPtr, width, height, yLineSize, uLineSize, vLineSize) {
1082
+ const rgba = new Uint8Array(width * height * 4);
1083
+ for (let y = 0; y < height; y++) {
1084
+ for (let x = 0; x < width; x++) {
1085
+ const yIndex = y * yLineSize + x;
1086
+ const uIndex = (y >> 1) * uLineSize + (x >> 1);
1087
+ const vIndex = (y >> 1) * vLineSize + (x >> 1);
1088
+ const yValue = this.wasmModule.HEAPU8[yPtr + yIndex];
1089
+ const uValue = this.wasmModule.HEAPU8[uPtr + uIndex];
1090
+ const vValue = this.wasmModule.HEAPU8[vPtr + vIndex];
1091
+ const c = yValue - 16;
1092
+ const d = uValue - 128;
1093
+ const e = vValue - 128;
1094
+ let r = 298 * c + 409 * e + 128 >> 8;
1095
+ let g = 298 * c - 100 * d - 208 * e + 128 >> 8;
1096
+ let b = 298 * c + 516 * d + 128 >> 8;
1097
+ r = r < 0 ? 0 : r > 255 ? 255 : r;
1098
+ g = g < 0 ? 0 : g > 255 ? 255 : g;
1099
+ b = b < 0 ? 0 : b > 255 ? 255 : b;
1100
+ const rgbaIndex = (y * width + x) * 4;
1101
+ rgba[rgbaIndex] = r;
1102
+ rgba[rgbaIndex + 1] = g;
1103
+ rgba[rgbaIndex + 2] = b;
1104
+ rgba[rgbaIndex + 3] = 255;
1105
+ }
1106
+ }
1107
+ return rgba;
1108
+ }
1109
+ }
807
1110
  class BasePrefetcher {
808
1111
  constructor(config, callbacks = {}) {
809
1112
  this.isRunning = false;
@@ -1056,7 +1359,8 @@ class SegmentPrefetcher extends BasePrefetcher {
1056
1359
  this.prefetchQueue.push({
1057
1360
  data,
1058
1361
  segmentIndex: nextIndex,
1059
- fetchTime
1362
+ fetchTime,
1363
+ segmentDuration: segment.duration
1060
1364
  });
1061
1365
  this.fetchedSegmentCount++;
1062
1366
  this.updateStatus({
@@ -1067,6 +1371,10 @@ class SegmentPrefetcher extends BasePrefetcher {
1067
1371
  callbacks.onSegmentFetched?.(nextIndex, data);
1068
1372
  console.log(`[SegmentPrefetcher] Fetched segment #${nextIndex}: ${data.length} bytes, ${fetchTime.toFixed(0)}ms`);
1069
1373
  } catch (error) {
1374
+ if (error.name === "AbortError") {
1375
+ console.log(`[SegmentPrefetcher] Fetch aborted`);
1376
+ return;
1377
+ }
1070
1378
  console.error(`[SegmentPrefetcher] Fetch failed:`, error.message);
1071
1379
  this.callbacks.onError?.(error);
1072
1380
  } finally {
@@ -1077,11 +1385,16 @@ class SegmentPrefetcher extends BasePrefetcher {
1077
1385
  /**
1078
1386
  * 处理预取队列中的数据
1079
1387
  *
1388
+ * @param maxSegments 最多处理的分片数量(默认处理所有)
1080
1389
  * @returns 是否有数据被处理
1081
1390
  */
1082
- processQueue() {
1391
+ processQueue(maxSegments) {
1083
1392
  let processed = false;
1393
+ let processedCount = 0;
1084
1394
  while (this.prefetchQueue.length > 0) {
1395
+ if (maxSegments !== void 0 && processedCount >= maxSegments) {
1396
+ break;
1397
+ }
1085
1398
  const item = this.prefetchQueue[0];
1086
1399
  if (item.segmentIndex !== this.currentSegmentIndex) {
1087
1400
  break;
@@ -1089,7 +1402,7 @@ class SegmentPrefetcher extends BasePrefetcher {
1089
1402
  this.prefetchQueue.shift();
1090
1403
  this.updateStatus({ prefetchQueueSize: this.prefetchQueue.length });
1091
1404
  const parseStart = performance.now();
1092
- const itemCount = this.parseSegment(item.data, item.segmentIndex);
1405
+ const itemCount = this.parseSegment(item.data, item.segmentIndex, item.segmentDuration);
1093
1406
  const parseTime = performance.now() - parseStart;
1094
1407
  this.currentSegmentIndex++;
1095
1408
  this.updateStatus({ currentSegmentIndex: this.currentSegmentIndex });
@@ -1097,6 +1410,7 @@ class SegmentPrefetcher extends BasePrefetcher {
1097
1410
  callbacks.onSegmentParsed?.(item.segmentIndex, itemCount);
1098
1411
  console.log(`[SegmentPrefetcher] Parsed segment #${item.segmentIndex}: ${itemCount} items, ${parseTime.toFixed(0)}ms`);
1099
1412
  processed = true;
1413
+ processedCount++;
1100
1414
  }
1101
1415
  return processed;
1102
1416
  }
@@ -1234,6 +1548,10 @@ class StreamPrefetcher extends BasePrefetcher {
1234
1548
  console.log(`[StreamPrefetcher] Download complete: ${this.totalDownloadedBytes} bytes`);
1235
1549
  }
1236
1550
  } catch (error) {
1551
+ if (error.name === "AbortError") {
1552
+ console.log(`[StreamPrefetcher] Read aborted`);
1553
+ return;
1554
+ }
1237
1555
  console.error(`[StreamPrefetcher] Read error:`, error.message);
1238
1556
  this.callbacks.onError?.(error);
1239
1557
  }
@@ -1324,7 +1642,8 @@ const HLS_PREFETCHER_CONFIG = {
1324
1642
  ...DEFAULT_SEGMENT_PREFETCHER_CONFIG,
1325
1643
  lowWaterMark: 30,
1326
1644
  highWaterMark: 100,
1327
- prefetchAhead: 2,
1645
+ prefetchAhead: 6,
1646
+ // 预载 6 个分片(约 30 秒),适应高分辨率视频
1328
1647
  prefetchInterval: 10,
1329
1648
  dynamicInterval: true
1330
1649
  };
@@ -1334,12 +1653,18 @@ const DEFAULT_HLS_CONFIG = {
1334
1653
  class HLSSegmentPrefetcher extends SegmentPrefetcher {
1335
1654
  constructor(config, callbacks, player) {
1336
1655
  super(config, callbacks);
1656
+ this.currentInitSegmentUri = null;
1337
1657
  this.player = player;
1338
1658
  }
1339
1659
  /**
1340
1660
  * 获取分片数据
1341
1661
  */
1342
1662
  async fetchSegment(segment, index) {
1663
+ if (segment.initSegmentUri && segment.initSegmentUri !== this.currentInitSegmentUri) {
1664
+ console.log("[HLSSegmentPrefetcher] Init segment changed:", segment.initSegmentUri);
1665
+ await this.player.loadNewInitSegment(segment.initSegmentUri);
1666
+ this.currentInitSegmentUri = segment.initSegmentUri;
1667
+ }
1343
1668
  const baseUrl = this.baseUrl;
1344
1669
  const url = segment.uri.startsWith("http") ? segment.uri : baseUrl + segment.uri;
1345
1670
  const headers = {};
@@ -1347,17 +1672,18 @@ class HLSSegmentPrefetcher extends SegmentPrefetcher {
1347
1672
  const { start, end } = segment.byteRange;
1348
1673
  headers["Range"] = `bytes=${start}-${end}`;
1349
1674
  }
1350
- const response = await fetch(url, { headers });
1675
+ const signal = this.player.getAbortSignal();
1676
+ const response = await fetch(url, { headers, signal });
1351
1677
  return new Uint8Array(await response.arrayBuffer());
1352
1678
  }
1353
1679
  /**
1354
1680
  * 解析分片数据
1355
1681
  */
1356
- parseSegment(data, index) {
1682
+ parseSegment(data, index, segmentDuration) {
1357
1683
  if (this.player.isFMP4) {
1358
1684
  return this.player.parseFMP4Data(data);
1359
1685
  } else {
1360
- return this.player.parseTSData(data);
1686
+ return this.player.parseTSData(data, segmentDuration);
1361
1687
  }
1362
1688
  }
1363
1689
  /**
@@ -1378,19 +1704,53 @@ class HLSPlayer extends BasePlayer {
1378
1704
  this.pesExtractor = new PESExtractor();
1379
1705
  this.nalParser = new NALParser();
1380
1706
  this.fmp4Demuxer = new fMP4Demuxer();
1707
+ this.hevcDecoder = null;
1381
1708
  this.isPrefetching = false;
1382
1709
  this.currentSegmentIndex = 0;
1383
1710
  this.segments = [];
1384
1711
  this.fmp4Segments = [];
1385
1712
  this.initSegment = null;
1386
1713
  this._isFMP4 = false;
1714
+ this._isHevc = false;
1387
1715
  this.currentPlaylistUrl = "";
1388
1716
  this._nalQueue = [];
1389
1717
  this._sampleQueue = [];
1718
+ this._currentSegmentDuration = 0;
1390
1719
  this.prefetcher = null;
1720
+ this.abortController = null;
1721
+ this.readyFired = false;
1722
+ this._nalCountInCurrentSegment = 0;
1391
1723
  this.lastRenderTime = 0;
1724
+ this.playStartTime = 0;
1725
+ this.accumulatedMediaTime = 0;
1726
+ this._lastLogTime = 0;
1727
+ this._lastRafTime = 0;
1392
1728
  this.renderLoop = (timestamp = 0) => {
1393
1729
  if (!this.isPlaying) return;
1730
+ const now = performance.now();
1731
+ const BACKGROUND_PAUSE_THRESHOLD = 500;
1732
+ if (this._lastRafTime > 0 && now - this._lastRafTime > BACKGROUND_PAUSE_THRESHOLD) {
1733
+ const pauseDuration = now - this._lastRafTime;
1734
+ console.log("[renderLoop] Background pause detected, duration:", pauseDuration.toFixed(0), "ms");
1735
+ if (this.config.isLive) {
1736
+ const KEEP_FRAMES = 5;
1737
+ if (this.frameBuffer.length > KEEP_FRAMES) {
1738
+ const droppedCount = this.frameBuffer.length - KEEP_FRAMES;
1739
+ this.frameBuffer = this.frameBuffer.slice(-KEEP_FRAMES);
1740
+ this.droppedFrames += droppedCount;
1741
+ console.log("[renderLoop] Live resume: dropped", droppedCount, "old frames, keeping latest", KEEP_FRAMES);
1742
+ }
1743
+ this.playStartTime = now;
1744
+ this.accumulatedMediaTime = 0;
1745
+ } else {
1746
+ this.playStartTime = now - this.accumulatedMediaTime;
1747
+ }
1748
+ }
1749
+ this._lastRafTime = now;
1750
+ if (!this._lastLogTime || now - this._lastLogTime > 1e3) {
1751
+ this._lastLogTime = now;
1752
+ console.log("[renderLoop] frameBuffer=", this.frameBuffer.length, "renderer=", !!this.renderer);
1753
+ }
1394
1754
  const downloaded = this.isFMP4 ? this.sampleQueue.length : this.nalQueue.length;
1395
1755
  const segments = this.isFMP4 ? this.fmp4Segments : this.segments;
1396
1756
  const totalSegments = segments.length;
@@ -1402,19 +1762,35 @@ class HLSPlayer extends BasePlayer {
1402
1762
  totalSegments
1403
1763
  });
1404
1764
  if (this.frameBuffer.length > 0 && this.renderer) {
1405
- const frame = this.frameBuffer.shift();
1406
- this.renderer.render(frame);
1407
- if (this.lastRenderTime > 0) {
1408
- const elapsed = timestamp - this.lastRenderTime;
1409
- this.setCurrentTime(this._currentTime + elapsed);
1765
+ const frame = this.frameBuffer[0];
1766
+ const timescale = this.fmp4Demuxer.getTimescale();
1767
+ if (this.playStartTime === 0) {
1768
+ this.playStartTime = now;
1769
+ this.accumulatedMediaTime = 0;
1770
+ console.log("[renderLoop] Init: timescale=", timescale);
1771
+ }
1772
+ const elapsedWallTime = now - this.playStartTime;
1773
+ const frameDurationMs = frame.duration ? frame.duration * 1e3 / timescale : 33.33;
1774
+ if (Math.floor(elapsedWallTime / 1e3) !== Math.floor((elapsedWallTime - 20) / 1e3)) {
1775
+ console.log("[renderLoop] elapsed=", Math.floor(elapsedWallTime), "accumulated=", Math.floor(this.accumulatedMediaTime), "frameDuration=", frame.duration, "frameDurationMs=", frameDurationMs.toFixed(2), "frameBuffer=", this.frameBuffer.length);
1776
+ }
1777
+ const bufferMs = 50;
1778
+ if (elapsedWallTime >= this.accumulatedMediaTime - bufferMs) {
1779
+ this.frameBuffer.shift();
1780
+ this.renderer.render(frame);
1781
+ this.accumulatedMediaTime += frameDurationMs;
1782
+ this.setCurrentTime(this.accumulatedMediaTime);
1783
+ this.updateState({ resolution: `${frame.width}x${frame.height}` });
1784
+ const fps = this.renderer.updateFps();
1785
+ this.updateState({ fps });
1786
+ this.callbacks.onFrameRender?.(frame);
1410
1787
  }
1411
- this.lastRenderTime = timestamp;
1412
- this.updateState({ resolution: `${frame.width}x${frame.height}` });
1413
- const fps = this.renderer.updateFps();
1414
- this.updateState({ fps });
1415
- this.callbacks.onFrameRender?.(frame);
1416
1788
  } else {
1417
- this.lastRenderTime = 0;
1789
+ if (this.playStartTime !== 0) {
1790
+ const oldPlayStartTime = this.playStartTime;
1791
+ this.playStartTime = now - this.accumulatedMediaTime;
1792
+ console.log("[renderLoop] No frames, adjusting playStartTime from", oldPlayStartTime, "to", this.playStartTime, "accumulated=", this.accumulatedMediaTime);
1793
+ }
1418
1794
  }
1419
1795
  this.frameTimer = requestAnimationFrame(this.renderLoop);
1420
1796
  };
@@ -1423,23 +1799,40 @@ class HLSPlayer extends BasePlayer {
1423
1799
  get isFMP4() {
1424
1800
  return this._isFMP4;
1425
1801
  }
1802
+ get isHevc() {
1803
+ return this._isHevc;
1804
+ }
1426
1805
  get nalQueue() {
1427
1806
  return this._nalQueue;
1428
1807
  }
1429
1808
  get sampleQueue() {
1430
1809
  return this._sampleQueue;
1431
1810
  }
1811
+ /** 获取 AbortSignal(供预取器使用) */
1812
+ getAbortSignal() {
1813
+ if (!this.abortController) {
1814
+ const controller = new AbortController();
1815
+ controller.abort();
1816
+ return controller.signal;
1817
+ }
1818
+ return this.abortController.signal;
1819
+ }
1432
1820
  /**
1433
1821
  * 加载 HLS 播放列表
1434
1822
  */
1435
1823
  async load(url) {
1436
1824
  this.currentPlaylistUrl = url;
1825
+ if (this.abortController) {
1826
+ this.abortController.abort();
1827
+ }
1828
+ this.abortController = new AbortController();
1437
1829
  this.segments = [];
1438
1830
  this.fmp4Segments = [];
1439
1831
  this.initSegment = null;
1440
1832
  this._nalQueue.length = 0;
1441
1833
  this._sampleQueue.length = 0;
1442
1834
  this.currentSegmentIndex = 0;
1835
+ this.readyFired = false;
1443
1836
  this.resetState();
1444
1837
  if (this.prefetcher) {
1445
1838
  this.prefetcher.reset();
@@ -1472,6 +1865,8 @@ class HLSPlayer extends BasePlayer {
1472
1865
  isLoaded: true,
1473
1866
  totalSegments: segments.length
1474
1867
  });
1868
+ this.initPrefetcher();
1869
+ this.prefetcher.start();
1475
1870
  }
1476
1871
  /**
1477
1872
  * 初始化解码器(覆盖基类方法,添加 fMP4 支持)
@@ -1489,6 +1884,8 @@ class HLSPlayer extends BasePlayer {
1489
1884
  this.isPlaying = true;
1490
1885
  this.decodeLoopAbort = false;
1491
1886
  this.lastRenderTime = 0;
1887
+ this.playStartTime = 0;
1888
+ this.accumulatedMediaTime = 0;
1492
1889
  this.updateState({ isPlaying: true });
1493
1890
  if (!this.prefetcher) {
1494
1891
  this.initPrefetcher();
@@ -1497,6 +1894,24 @@ class HLSPlayer extends BasePlayer {
1497
1894
  this.decodeLoop();
1498
1895
  this.frameTimer = requestAnimationFrame(this.renderLoop);
1499
1896
  }
1897
+ /**
1898
+ * 销毁播放器,释放所有资源
1899
+ */
1900
+ destroy() {
1901
+ if (this.abortController) {
1902
+ this.abortController.abort();
1903
+ this.abortController = null;
1904
+ }
1905
+ if (this.prefetcher) {
1906
+ this.prefetcher.reset();
1907
+ this.prefetcher = null;
1908
+ }
1909
+ if (this.hevcDecoder) {
1910
+ this.hevcDecoder.destroy();
1911
+ this.hevcDecoder = null;
1912
+ }
1913
+ super.destroy();
1914
+ }
1500
1915
  /**
1501
1916
  * 跳转到指定时间
1502
1917
  */
@@ -1531,10 +1946,22 @@ class HLSPlayer extends BasePlayer {
1531
1946
  console.log("[DecodeLoop] START, isFMP4:", this.isFMP4);
1532
1947
  let batchCount = 0;
1533
1948
  while (this.isPlaying && !this.decodeLoopAbort) {
1534
- this.prefetcher?.processQueue();
1949
+ const sampleQueueSize = this.sampleQueue.length;
1950
+ const maxSamplesBeforePause = this.config.maxQueueSize * 3;
1951
+ if (sampleQueueSize < maxSamplesBeforePause && this.prefetcher) {
1952
+ this.prefetcher.processQueue(1);
1953
+ }
1954
+ if (!this.config.isLive && this.frameBuffer.length >= this.config.targetBufferSize) {
1955
+ await this.sleep(10);
1956
+ continue;
1957
+ }
1535
1958
  const batchSize = this.config.decodeBatchSize;
1536
1959
  let decodedInBatch = 0;
1537
1960
  if (this.isFMP4) {
1961
+ const queueSize = this.sampleQueue.length;
1962
+ if (queueSize > 0 || batchCount % 50 === 0) {
1963
+ console.log("[DecodeLoop] fMP4: sampleQueue=", queueSize, "frameBuffer=", this.frameBuffer.length, "batch=", batchCount, "decoderInit=", this.decoderInitialized);
1964
+ }
1538
1965
  while (this.sampleQueue.length > 0 && decodedInBatch < batchSize) {
1539
1966
  const queuedSample = this.sampleQueue.shift();
1540
1967
  const sample = queuedSample.sample;
@@ -1542,27 +1969,42 @@ class HLSPlayer extends BasePlayer {
1542
1969
  if (frame) {
1543
1970
  this.frameBuffer.push(frame);
1544
1971
  decodedInBatch++;
1545
- while (this.frameBuffer.length > this.config.targetBufferSize) {
1546
- this.frameBuffer.shift();
1547
- this.droppedFrames++;
1972
+ if (!this.readyFired) {
1973
+ this.readyFired = true;
1974
+ this.callbacks.onReady?.();
1975
+ }
1976
+ if (this.config.isLive) {
1977
+ while (this.frameBuffer.length > this.config.targetBufferSize) {
1978
+ this.frameBuffer.shift();
1979
+ this.droppedFrames++;
1980
+ }
1548
1981
  }
1982
+ } else {
1983
+ console.warn("[DecodeLoop] decodeSample returned null, remaining samples=", this.sampleQueue.length);
1549
1984
  }
1550
1985
  }
1551
1986
  } else {
1552
1987
  while (this.nalQueue.length > 0 && decodedInBatch < batchSize) {
1553
1988
  const queuedNal = this.nalQueue.shift();
1554
1989
  const nalUnit = queuedNal.nalUnit;
1990
+ const segmentDuration = queuedNal.segmentDuration;
1555
1991
  if (!this.decoderInitialized && (nalUnit.type === 7 || nalUnit.type === 8)) {
1556
1992
  this.initDecoderParamsSync([nalUnit]);
1557
1993
  }
1558
1994
  if (nalUnit.type === 5 || nalUnit.type === 1) {
1559
- const frame = this.decodeNAL(nalUnit);
1995
+ const frame = this.decodeNAL(nalUnit, segmentDuration);
1560
1996
  if (frame) {
1561
1997
  this.frameBuffer.push(frame);
1562
1998
  decodedInBatch++;
1563
- while (this.frameBuffer.length > this.config.targetBufferSize) {
1564
- this.frameBuffer.shift();
1565
- this.droppedFrames++;
1999
+ if (!this.readyFired) {
2000
+ this.readyFired = true;
2001
+ this.callbacks.onReady?.();
2002
+ }
2003
+ if (this.config.isLive) {
2004
+ while (this.frameBuffer.length > this.config.targetBufferSize) {
2005
+ this.frameBuffer.shift();
2006
+ this.droppedFrames++;
2007
+ }
1566
2008
  }
1567
2009
  }
1568
2010
  }
@@ -1604,7 +2046,9 @@ class HLSPlayer extends BasePlayer {
1604
2046
  const segmentInfos = this.fmp4Segments.map((seg) => ({
1605
2047
  uri: seg.uri,
1606
2048
  duration: seg.duration,
1607
- byteRange: seg.byteRange
2049
+ byteRange: seg.byteRange,
2050
+ initSegmentUri: seg.initSegmentUri
2051
+ // 传递初始化段 URI
1608
2052
  }));
1609
2053
  this.prefetcher.setSegments(segmentInfos, baseUrl);
1610
2054
  } else {
@@ -1620,38 +2064,53 @@ class HLSPlayer extends BasePlayer {
1620
2064
  * 解析 fMP4 数据
1621
2065
  */
1622
2066
  parseFMP4Data(data) {
1623
- let moofOffset = -1;
1624
- let mdatOffset = -1;
1625
- let mdatSize = 0;
2067
+ let totalSampleCount = 0;
1626
2068
  let offset = 0;
1627
2069
  while (offset < data.length - 8) {
1628
- const boxSize = this.readBoxSize(data, offset);
1629
- const boxType = this.readBoxType(data, offset + 4);
1630
- if (boxType === "moof") {
1631
- moofOffset = offset;
1632
- } else if (boxType === "mdat") {
1633
- mdatOffset = offset;
1634
- mdatSize = boxSize;
1635
- break;
2070
+ let moofOffset = -1;
2071
+ let mdatOffset = -1;
2072
+ let mdatSize = 0;
2073
+ while (offset < data.length - 8) {
2074
+ const boxSize = this.readBoxSize(data, offset);
2075
+ const boxType = this.readBoxType(data, offset + 4);
2076
+ if (boxSize < 8) break;
2077
+ if (boxType === "moof") {
2078
+ moofOffset = offset;
2079
+ offset += boxSize;
2080
+ break;
2081
+ }
2082
+ offset += boxSize;
1636
2083
  }
1637
- offset += boxSize;
1638
- }
1639
- let sampleCount = 0;
1640
- if (moofOffset >= 0 && mdatOffset >= 0) {
2084
+ if (moofOffset < 0) break;
2085
+ while (offset < data.length - 8) {
2086
+ const boxSize = this.readBoxSize(data, offset);
2087
+ const boxType = this.readBoxType(data, offset + 4);
2088
+ if (boxSize < 8) break;
2089
+ if (boxType === "mdat") {
2090
+ mdatOffset = offset;
2091
+ mdatSize = boxSize;
2092
+ offset += boxSize;
2093
+ break;
2094
+ }
2095
+ offset += boxSize;
2096
+ }
2097
+ if (mdatOffset < 0) break;
1641
2098
  const moof = this.fmp4Demuxer.parseMoof(data, moofOffset);
1642
2099
  const mdatData = data.slice(mdatOffset + 8, mdatOffset + mdatSize);
1643
- const samples = this.fmp4Demuxer.extractSamples(moof, mdatData, mdatOffset);
2100
+ const samples = this.fmp4Demuxer.extractSamples(moof, mdatData, moofOffset, mdatOffset);
1644
2101
  for (const sample of samples) {
1645
2102
  this._sampleQueue.push({ sample });
1646
2103
  }
1647
- sampleCount = samples.length;
2104
+ totalSampleCount += samples.length;
2105
+ console.log("[parseFMP4Data] Pushed", samples.length, "samples, totalQueue=", this._sampleQueue.length);
1648
2106
  }
1649
- return sampleCount;
2107
+ console.log("[parseFMP4Data] Total samples parsed:", totalSampleCount);
2108
+ return totalSampleCount;
1650
2109
  }
1651
2110
  /**
1652
2111
  * 解析 TS 数据
1653
2112
  */
1654
- parseTSData(tsData) {
2113
+ parseTSData(tsData, segmentDuration) {
1655
2114
  const packets = this.tsDemuxer.parse(tsData);
1656
2115
  let nalCount = 0;
1657
2116
  if (packets.video.length > 0) {
@@ -1661,8 +2120,15 @@ class HLSPlayer extends BasePlayer {
1661
2120
  if (!this.decoderInitialized) {
1662
2121
  this.initDecoderParamsSync(nalUnits);
1663
2122
  }
2123
+ if (segmentDuration !== void 0) {
2124
+ this._currentSegmentDuration = segmentDuration;
2125
+ }
1664
2126
  for (const nalUnit of nalUnits) {
1665
- this._nalQueue.push({ nalUnit, pts: 0 });
2127
+ this._nalQueue.push({
2128
+ nalUnit,
2129
+ pts: 0,
2130
+ segmentDuration: this._currentSegmentDuration
2131
+ });
1666
2132
  }
1667
2133
  nalCount = nalUnits.length;
1668
2134
  }
@@ -1686,20 +2152,103 @@ class HLSPlayer extends BasePlayer {
1686
2152
  const { start, end } = this.initSegment.byteRange;
1687
2153
  headers["Range"] = `bytes=${start}-${end}`;
1688
2154
  }
1689
- const response = await fetch(url, { headers });
2155
+ let response;
2156
+ try {
2157
+ response = await fetch(url, { headers, signal: this.abortController?.signal });
2158
+ } catch (error) {
2159
+ if (error.name === "AbortError") {
2160
+ console.log("[fMP4] Load init segment aborted");
2161
+ return;
2162
+ }
2163
+ throw error;
2164
+ }
1690
2165
  const data = new Uint8Array(await response.arrayBuffer());
1691
2166
  console.log("[fMP4] Init segment data size:", data.length, "bytes");
1692
2167
  const initInfo = this.fmp4Demuxer.parseInitSegment(data);
1693
- console.log("[fMP4] Parse result:", initInfo);
1694
- if (initInfo?.avcC) {
2168
+ console.log("[fMP4] Parse result:", {
2169
+ isHevc: initInfo?.isHevc,
2170
+ hasAvcC: !!initInfo?.avcC,
2171
+ hasHvcC: !!initInfo?.hvcC,
2172
+ timescale: initInfo?.timescale,
2173
+ lengthSizeMinusOne: initInfo?.lengthSizeMinusOne
2174
+ });
2175
+ this._isHevc = initInfo?.isHevc ?? false;
2176
+ if (initInfo?.hvcC && this._isHevc) {
2177
+ console.log("[fMP4] hvcC size:", initInfo.hvcC.length);
2178
+ await this.initHevcDecoder();
2179
+ this.initDecoderFromHvcC(initInfo.hvcC);
2180
+ console.log("[fMP4] HEVC decoder initialized, timescale:", initInfo.timescale);
2181
+ } else if (initInfo?.avcC) {
1695
2182
  console.log("[fMP4] avcC size:", initInfo.avcC.length);
1696
2183
  this.initDecoderFromAvcC(initInfo.avcC);
1697
- console.log("[fMP4] Init segment loaded, timescale:", initInfo.timescale);
2184
+ console.log("[fMP4] AVC decoder initialized, timescale:", initInfo.timescale);
1698
2185
  } else {
1699
- console.error("[fMP4] Failed to parse init segment or no avcC found!");
1700
- throw new Error("Failed to parse fMP4 init segment");
2186
+ console.error("[fMP4] Failed to parse init segment or no avcC/hvcC found!");
2187
+ throw new Error("Failed to parse fMP4 init segment: no valid codec config found");
1701
2188
  }
1702
2189
  }
2190
+ /**
2191
+ * 加载新的初始化段(用于不连续性流)
2192
+ */
2193
+ async loadNewInitSegment(uri) {
2194
+ console.log("[fMP4] Loading new init segment for discontinuity:", uri);
2195
+ this._sampleQueue.length = 0;
2196
+ this._nalQueue.length = 0;
2197
+ const url = uri.startsWith("http") ? uri : this.currentPlaylistUrl.substring(0, this.currentPlaylistUrl.lastIndexOf("/") + 1) + uri;
2198
+ const headers = {};
2199
+ if (this.initSegment?.uri === uri && this.initSegment.byteRange) {
2200
+ const { start, end } = this.initSegment.byteRange;
2201
+ headers["Range"] = `bytes=${start}-${end}`;
2202
+ console.log("[fMP4] Using byte range for init segment:", { start, end });
2203
+ }
2204
+ let response;
2205
+ try {
2206
+ response = await fetch(url, { headers, signal: this.abortController?.signal });
2207
+ } catch (error) {
2208
+ if (error.name === "AbortError") {
2209
+ console.log("[fMP4] Load new init segment aborted");
2210
+ return;
2211
+ }
2212
+ throw error;
2213
+ }
2214
+ const data = new Uint8Array(await response.arrayBuffer());
2215
+ console.log("[fMP4] New init segment data size:", data.length, "bytes");
2216
+ const initInfo = this.fmp4Demuxer.parseInitSegment(data);
2217
+ console.log("[fMP4] New init segment parse result:", {
2218
+ isHevc: initInfo?.isHevc,
2219
+ hasAvcC: !!initInfo?.avcC,
2220
+ hasHvcC: !!initInfo?.hvcC
2221
+ });
2222
+ const newIsHevc = initInfo?.isHevc ?? false;
2223
+ if (newIsHevc !== this._isHevc) {
2224
+ console.log("[fMP4] Codec type changed from", this._isHevc ? "HEVC" : "AVC", "to", newIsHevc ? "HEVC" : "AVC");
2225
+ this._isHevc = newIsHevc;
2226
+ this.decoderInitialized = false;
2227
+ if (newIsHevc) {
2228
+ await this.initHevcDecoder();
2229
+ }
2230
+ } else {
2231
+ this.decoderInitialized = false;
2232
+ }
2233
+ if (initInfo?.hvcC && this._isHevc) {
2234
+ this.initDecoderFromHvcC(initInfo.hvcC);
2235
+ console.log("[fMP4] HEVC decoder re-initialized for discontinuity");
2236
+ } else if (initInfo?.avcC) {
2237
+ this.initDecoderFromAvcC(initInfo.avcC);
2238
+ console.log("[fMP4] AVC decoder re-initialized for discontinuity");
2239
+ } else {
2240
+ console.error("[fMP4] Failed to parse new init segment!");
2241
+ }
2242
+ }
2243
+ /**
2244
+ * 初始化 HEVC 解码器
2245
+ */
2246
+ async initHevcDecoder() {
2247
+ if (this.hevcDecoder) return;
2248
+ this.hevcDecoder = new HEVCDecoder(this.wasmLoader);
2249
+ await this.hevcDecoder.init();
2250
+ console.log("[fMP4] HEVC decoder created");
2251
+ }
1703
2252
  /**
1704
2253
  * 从 avcC 初始化解码器
1705
2254
  */
@@ -1744,11 +2293,57 @@ class HLSPlayer extends BasePlayer {
1744
2293
  this.decoderInitialized = true;
1745
2294
  console.log("[fMP4] Decoder initialized from avcC");
1746
2295
  }
2296
+ /**
2297
+ * 从 hvcC 初始化 HEVC 解码器
2298
+ */
2299
+ initDecoderFromHvcC(hvcC) {
2300
+ if (this.decoderInitialized || !this.hevcDecoder) return;
2301
+ console.log("[fMP4] hvcC length:", hvcC.length);
2302
+ console.log("[fMP4] hvcC first 8 bytes:", Array.from(hvcC.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join(" "));
2303
+ console.log("[fMP4] hvcC bytes 8-24:", Array.from(hvcC.slice(8, 24)).map((b) => b.toString(16).padStart(2, "0")).join(" "));
2304
+ let offset = 8 + 22;
2305
+ const numOfArrays = hvcC[offset];
2306
+ console.log("[fMP4] hvcC[30] (numOfArrays):", numOfArrays, "0x" + numOfArrays.toString(16));
2307
+ offset += 1;
2308
+ for (let i = 0; i < numOfArrays && offset < hvcC.length - 3; i++) {
2309
+ const typeCompressed = hvcC[offset];
2310
+ const arrayType = typeCompressed & 63;
2311
+ const numNalus = hvcC[offset + 1] << 8 | hvcC[offset + 2];
2312
+ offset += 3;
2313
+ console.log(`[fMP4] HEVC array: type=${arrayType}, count=${numNalus}`);
2314
+ for (let j = 0; j < numNalus && offset < hvcC.length - 2; j++) {
2315
+ const nalUnitLength = hvcC[offset] << 8 | hvcC[offset + 1];
2316
+ offset += 2;
2317
+ if (offset + nalUnitLength > hvcC.length) break;
2318
+ const nalUnitData = hvcC.slice(offset, offset + nalUnitLength);
2319
+ offset += nalUnitLength;
2320
+ const nalWithStartCode = new Uint8Array(4 + nalUnitLength);
2321
+ nalWithStartCode.set([0, 0, 0, 1], 0);
2322
+ nalWithStartCode.set(nalUnitData, 4);
2323
+ if (arrayType === 32 || arrayType === 33 || arrayType === 34) {
2324
+ this.hevcDecoder.decode({ type: arrayType, data: nalWithStartCode, size: nalWithStartCode.length });
2325
+ const typeName = arrayType === 32 ? "VPS" : arrayType === 33 ? "SPS" : "PPS";
2326
+ console.log(`[fMP4] HEVC ${typeName} decoded, length=${nalUnitLength}`);
2327
+ }
2328
+ }
2329
+ }
2330
+ this.decoderInitialized = true;
2331
+ console.log("[fMP4] HEVC decoder initialized from hvcC");
2332
+ }
1747
2333
  /**
1748
2334
  * 解析 HLS 播放列表
1749
2335
  */
1750
2336
  async parsePlaylist(url) {
1751
- const response = await fetch(url);
2337
+ let response;
2338
+ try {
2339
+ response = await fetch(url, { signal: this.abortController?.signal });
2340
+ } catch (error) {
2341
+ if (error.name === "AbortError") {
2342
+ console.log("[HLSPlayer] Parse playlist aborted");
2343
+ return { isMaster: true, isFMP4: false, segments: [], fmp4Segments: [], variants: 0 };
2344
+ }
2345
+ throw error;
2346
+ }
1752
2347
  const content = await response.text();
1753
2348
  const lines = content.split("\n");
1754
2349
  const segments = [];
@@ -1776,25 +2371,31 @@ class HLSPlayer extends BasePlayer {
1776
2371
  }
1777
2372
  const isFMP4 = lines.some((line) => line.includes("#EXT-X-MAP:"));
1778
2373
  console.log("[parsePlaylist] isFMP4:", isFMP4, "url:", url);
2374
+ let currentInitSegment;
1779
2375
  for (let i = 0; i < lines.length; i++) {
1780
2376
  const line = lines[i].trim();
1781
2377
  if (line.startsWith("#EXT-X-MAP:")) {
1782
2378
  const mapInfo = line.substring("#EXT-X-MAP:".length);
1783
2379
  const uriMatch = mapInfo.match(/URI="([^"]+)"/);
1784
2380
  if (uriMatch) {
1785
- initSegment = { uri: uriMatch[1] };
2381
+ currentInitSegment = { uri: uriMatch[1] };
1786
2382
  const byteRangeMatch = mapInfo.match(/BYTERANGE="(\d+)@(\d+)"/);
1787
2383
  if (byteRangeMatch) {
1788
- initSegment.byteRange = {
2384
+ currentInitSegment.byteRange = {
1789
2385
  start: parseInt(byteRangeMatch[2]),
1790
2386
  end: parseInt(byteRangeMatch[2]) + parseInt(byteRangeMatch[1]) - 1
1791
2387
  };
1792
2388
  }
2389
+ if (!initSegment) {
2390
+ initSegment = currentInitSegment;
2391
+ }
1793
2392
  }
2393
+ continue;
2394
+ }
2395
+ if (line === "#EXT-X-DISCONTINUITY") {
2396
+ console.log("[parsePlaylist] Found EXT-X-DISCONTINUITY");
2397
+ continue;
1794
2398
  }
1795
- }
1796
- for (let i = 0; i < lines.length; i++) {
1797
- const line = lines[i].trim();
1798
2399
  if (line.startsWith("#EXTINF:")) {
1799
2400
  const duration = parseFloat(line.split(":")[1].split(",")[0]);
1800
2401
  let byteRange;
@@ -1812,7 +2413,13 @@ class HLSPlayer extends BasePlayer {
1812
2413
  const uri = nextLine;
1813
2414
  if (uri && !uri.startsWith("#")) {
1814
2415
  if (isFMP4) {
1815
- fmp4Segments.push({ uri, duration, byteRange });
2416
+ fmp4Segments.push({
2417
+ uri,
2418
+ duration,
2419
+ byteRange,
2420
+ initSegmentUri: currentInitSegment?.uri
2421
+ // 关联当前初始化段
2422
+ });
1816
2423
  } else {
1817
2424
  segments.push({ uri, duration });
1818
2425
  }
@@ -1855,46 +2462,63 @@ class HLSPlayer extends BasePlayer {
1855
2462
  * 解码 fMP4 sample
1856
2463
  */
1857
2464
  decodeSample(sample) {
1858
- if (!this.decoder) {
1859
- console.warn("[fMP4] Decoder not available");
2465
+ const decoder = this._isHevc ? this.hevcDecoder : this.decoder;
2466
+ if (!decoder) {
2467
+ console.warn("[fMP4] Decoder not available, isHevc=", this._isHevc);
1860
2468
  return null;
1861
2469
  }
1862
2470
  if (!this.decoderInitialized) {
1863
- console.warn("[fMP4] Decoder not initialized, skipping sample");
2471
+ console.warn("[fMP4] Decoder not initialized, isHevc=", this._isHevc, ", sample size=", sample.data.length);
1864
2472
  return null;
1865
2473
  }
1866
2474
  const data = sample.data;
1867
- const hasStartCode = data.length >= 4 && data[0] === 0 && data[1] === 0 && (data[2] === 1 || data[2] === 0 && data[3] === 1);
1868
- if (hasStartCode) {
1869
- return this.decoder.decode({
1870
- type: sample.isSync ? 5 : 1,
1871
- data,
1872
- size: data.length
1873
- });
1874
- }
2475
+ const lengthSize = this.fmp4Demuxer.getLengthSizeMinusOne() + 1;
2476
+ const readNalLength = (data2, offset2, size) => {
2477
+ switch (size) {
2478
+ case 1:
2479
+ return data2[offset2];
2480
+ case 2:
2481
+ return data2[offset2] << 8 | data2[offset2 + 1];
2482
+ case 4:
2483
+ default:
2484
+ return data2[offset2] << 24 | data2[offset2 + 1] << 16 | data2[offset2 + 2] << 8 | data2[offset2 + 3];
2485
+ }
2486
+ };
2487
+ const hasAnnexBStartCode = data.length >= 4 && data[0] === 0 && data[1] === 0 && (data[2] === 0 && data[3] === 1 || data[2] === 1);
1875
2488
  let nalCount = 0;
1876
2489
  let totalSize = 0;
1877
2490
  let offset = 0;
1878
- while (offset + 4 <= data.length) {
1879
- const nalLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
1880
- if (nalLength <= 0 || offset + 4 + nalLength > data.length) {
1881
- console.warn(`[fMP4] Invalid NAL length: ${nalLength} at offset ${offset}`);
2491
+ let avccParseError = false;
2492
+ while (offset + lengthSize <= data.length) {
2493
+ const nalLength = readNalLength(data, offset, lengthSize);
2494
+ if (nalLength <= 0 || nalLength > 1e7 || offset + lengthSize + nalLength > data.length) {
2495
+ avccParseError = true;
1882
2496
  break;
1883
2497
  }
1884
2498
  totalSize += 4 + nalLength;
1885
- offset += 4 + nalLength;
2499
+ offset += lengthSize + nalLength;
1886
2500
  nalCount++;
1887
2501
  }
2502
+ if (avccParseError && hasAnnexBStartCode) {
2503
+ const nalType2 = this._isHevc ? sample.isSync ? HEVC_NAL_TYPE.IDR_W_RADL : 1 : sample.isSync ? 5 : 1;
2504
+ console.log("[fMP4] Sample appears to be Annex B format, isSync=", sample.isSync, "size=", data.length);
2505
+ return decoder.decode({
2506
+ type: nalType2,
2507
+ data,
2508
+ size: data.length
2509
+ });
2510
+ }
1888
2511
  if (nalCount === 0) {
1889
- console.warn("[fMP4] No valid NAL units found in sample");
2512
+ console.warn("[fMP4] No valid NAL units found in sample, data.length=", data.length, "lengthSize=", lengthSize, "firstBytes=", Array.from(data.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join(" "));
1890
2513
  return null;
1891
2514
  }
2515
+ console.log("[fMP4] AVCC sample: nalCount=", nalCount, "annexBSize=", totalSize, "isSync=", sample.isSync, "lengthSize=", lengthSize);
1892
2516
  const annexBData = new Uint8Array(totalSize);
1893
2517
  let writeOffset = 0;
1894
2518
  offset = 0;
1895
- while (offset + 4 <= data.length) {
1896
- const nalLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
1897
- if (nalLength <= 0 || offset + 4 + nalLength > data.length) {
2519
+ while (offset + lengthSize <= data.length) {
2520
+ const nalLength = readNalLength(data, offset, lengthSize);
2521
+ if (nalLength <= 0 || offset + lengthSize + nalLength > data.length) {
1898
2522
  break;
1899
2523
  }
1900
2524
  annexBData[writeOffset] = 0;
@@ -1902,30 +2526,42 @@ class HLSPlayer extends BasePlayer {
1902
2526
  annexBData[writeOffset + 2] = 0;
1903
2527
  annexBData[writeOffset + 3] = 1;
1904
2528
  writeOffset += 4;
1905
- annexBData.set(data.slice(offset + 4, offset + 4 + nalLength), writeOffset);
2529
+ annexBData.set(data.slice(offset + lengthSize, offset + lengthSize + nalLength), writeOffset);
1906
2530
  writeOffset += nalLength;
1907
- offset += 4 + nalLength;
2531
+ offset += lengthSize + nalLength;
1908
2532
  }
1909
- const frame = this.decoder.decode({
1910
- type: sample.isSync ? 5 : 1,
2533
+ const nalType = this._isHevc ? sample.isSync ? HEVC_NAL_TYPE.IDR_W_RADL : 1 : sample.isSync ? 5 : 1;
2534
+ const frame = decoder.decode({
2535
+ type: nalType,
1911
2536
  data: annexBData,
1912
2537
  size: annexBData.length
1913
2538
  });
2539
+ if (frame) {
2540
+ frame.pts = sample.pts;
2541
+ frame.dts = sample.dts;
2542
+ frame.duration = sample.duration;
2543
+ }
1914
2544
  return frame;
1915
2545
  }
1916
2546
  /**
1917
2547
  * 解码单个 NAL 单元
1918
2548
  */
1919
- decodeNAL(nalUnit) {
2549
+ decodeNAL(nalUnit, segmentDuration) {
1920
2550
  if (!this.decoder) return null;
1921
2551
  const nalWithStartCode = new Uint8Array(nalUnit.size + 4);
1922
2552
  nalWithStartCode.set([0, 0, 0, 1], 0);
1923
2553
  nalWithStartCode.set(nalUnit.data, 4);
1924
- return this.decoder.decode({
2554
+ const frame = this.decoder.decode({
1925
2555
  type: nalUnit.type,
1926
2556
  data: nalWithStartCode,
1927
2557
  size: nalWithStartCode.length
1928
2558
  });
2559
+ if (frame && segmentDuration && segmentDuration > 0) {
2560
+ const timescale = 1e3;
2561
+ const frameDuration = timescale / 60;
2562
+ frame.duration = frameDuration;
2563
+ }
2564
+ return frame;
1929
2565
  }
1930
2566
  }
1931
2567
  const FLV_SIGNATURE = [70, 76, 86];
@@ -2388,89 +3024,6 @@ class FLVDemuxer {
2388
3024
  return tag.frameType === FRAME_TYPE_KEYFRAME;
2389
3025
  }
2390
3026
  }
2391
- class HEVCDecoder {
2392
- constructor(wasmLoader) {
2393
- this.wasmModule = null;
2394
- this.decoderContext = null;
2395
- this.wasmModule = wasmLoader.getModule();
2396
- }
2397
- async init() {
2398
- if (!this.wasmModule) {
2399
- throw new Error("WASM module not loaded");
2400
- }
2401
- this.decoderContext = this.wasmModule._create_decoder(173);
2402
- if (this.decoderContext === 0 || this.decoderContext === null) {
2403
- throw new Error("Failed to create HEVC decoder context");
2404
- }
2405
- console.log("[HEVCDecoder] Initialized with codec_id=173");
2406
- }
2407
- decode(nalUnit) {
2408
- if (this.decoderContext === null || !this.wasmModule) {
2409
- return null;
2410
- }
2411
- const dataPtr = this.wasmModule._malloc(nalUnit.size);
2412
- this.wasmModule.HEAPU8.set(nalUnit.data, dataPtr);
2413
- const result = this.wasmModule._decode_video(
2414
- this.decoderContext,
2415
- dataPtr,
2416
- nalUnit.size
2417
- );
2418
- this.wasmModule._free(dataPtr);
2419
- if (result === 1) {
2420
- const width = this.wasmModule._get_frame_width(this.decoderContext);
2421
- const height = this.wasmModule._get_frame_height(this.decoderContext);
2422
- if (width <= 0 || height <= 0) {
2423
- return null;
2424
- }
2425
- const yPtr = this.wasmModule._get_frame_data(this.decoderContext, 0);
2426
- const uPtr = this.wasmModule._get_frame_data(this.decoderContext, 1);
2427
- const vPtr = this.wasmModule._get_frame_data(this.decoderContext, 2);
2428
- const yLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 0);
2429
- const uLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 1);
2430
- const vLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 2);
2431
- const frameData = this.yuv420pToRgba(
2432
- yPtr,
2433
- uPtr,
2434
- vPtr,
2435
- width,
2436
- height,
2437
- yLineSize,
2438
- uLineSize,
2439
- vLineSize
2440
- );
2441
- return { width, height, data: frameData };
2442
- }
2443
- return null;
2444
- }
2445
- yuv420pToRgba(yPtr, uPtr, vPtr, width, height, yLineSize, uLineSize, vLineSize) {
2446
- const rgba = new Uint8Array(width * height * 4);
2447
- for (let y = 0; y < height; y++) {
2448
- for (let x = 0; x < width; x++) {
2449
- const yIndex = y * yLineSize + x;
2450
- const uIndex = (y >> 1) * uLineSize + (x >> 1);
2451
- const vIndex = (y >> 1) * vLineSize + (x >> 1);
2452
- const yValue = this.wasmModule.HEAPU8[yPtr + yIndex];
2453
- const uValue = this.wasmModule.HEAPU8[uPtr + uIndex];
2454
- const vValue = this.wasmModule.HEAPU8[vPtr + vIndex];
2455
- const c = yValue - 16;
2456
- const d = uValue - 128;
2457
- const e = vValue - 128;
2458
- let r = 298 * c + 409 * e + 128 >> 8;
2459
- let g = 298 * c - 100 * d - 208 * e + 128 >> 8;
2460
- let b = 298 * c + 516 * d + 128 >> 8;
2461
- r = r < 0 ? 0 : r > 255 ? 255 : r;
2462
- g = g < 0 ? 0 : g > 255 ? 255 : g;
2463
- b = b < 0 ? 0 : b > 255 ? 255 : b;
2464
- const rgbaIndex = (y * width + x) * 4;
2465
- rgba[rgbaIndex] = r;
2466
- rgba[rgbaIndex + 1] = g;
2467
- rgba[rgbaIndex + 2] = b;
2468
- rgba[rgbaIndex + 3] = 255;
2469
- }
2470
- }
2471
- return rgba;
2472
- }
2473
- }
2474
3027
  const CODEC_ID_AVC = 7;
2475
3028
  const CODEC_ID_HEVC = 12;
2476
3029
  const FLV_PREFETCHER_CONFIG = {
@@ -2535,6 +3088,8 @@ class FLVPlayer extends BasePlayer {
2535
3088
  this.liveReader = null;
2536
3089
  this.liveDownloadAbort = false;
2537
3090
  this._lastQueuedTimestamp = -1;
3091
+ this.abortController = null;
3092
+ this.readyFired = false;
2538
3093
  this._currentDownloadSpeed = 0;
2539
3094
  this.renderedFrames = 0;
2540
3095
  this.lastRenderLogTime = 0;
@@ -2544,6 +3099,7 @@ class FLVPlayer extends BasePlayer {
2544
3099
  this.bufferEmptyStartTime = 0;
2545
3100
  this.playStartTimeOffset = 0;
2546
3101
  this.resyncCount = 0;
3102
+ this._lastRafTime = 0;
2547
3103
  this.renderLoop = () => {
2548
3104
  if (!this.isPlaying) return;
2549
3105
  this.updateState({
@@ -2553,27 +3109,56 @@ class FLVPlayer extends BasePlayer {
2553
3109
  });
2554
3110
  const now = performance.now();
2555
3111
  const isLive = this.config.isLive;
3112
+ const BACKGROUND_PAUSE_THRESHOLD = 500;
3113
+ if (this._lastRafTime > 0 && now - this._lastRafTime > BACKGROUND_PAUSE_THRESHOLD) {
3114
+ const pauseDuration = now - this._lastRafTime;
3115
+ console.log("[FLVPlayer] Background pause detected, duration:", pauseDuration.toFixed(0), "ms, isLive:", isLive, "queue:", this._videoTagQueue.length, "buffer:", this._timedFrameBuffer.length);
3116
+ if (isLive) {
3117
+ const droppedDecoded = this._timedFrameBuffer.length;
3118
+ const droppedQueue = this._videoTagQueue.length;
3119
+ this._timedFrameBuffer = [];
3120
+ this._videoTagQueue = [];
3121
+ this.droppedFrames += droppedDecoded;
3122
+ this.playStartTime = 0;
3123
+ this.firstFrameDts = -1;
3124
+ this.pausedTime = 0;
3125
+ console.log("[FLVPlayer] Live resume: cleared all buffers (decoded:", droppedDecoded, ", queue:", droppedQueue, "), waiting for new data");
3126
+ } else {
3127
+ this.playStartTimeOffset += pauseDuration;
3128
+ }
3129
+ }
3130
+ this._lastRafTime = now;
2556
3131
  if (this._timedFrameBuffer.length > 0 && this.renderer) {
2557
3132
  this.consecutiveEmptyBuffer = 0;
2558
3133
  if (this.playStartTime <= 0) {
2559
3134
  this.firstFrameDts = this._timedFrameBuffer[0].dts;
2560
3135
  this.playStartTime = now;
2561
3136
  this.playStartTimeOffset = 0;
3137
+ this.pausedTime = 0;
2562
3138
  console.log("[FLVPlayer] RenderLoop initialized, firstFrameDts:", this.firstFrameDts, "isLive:", isLive);
2563
3139
  }
2564
3140
  if (isLive) {
2565
- const elapsed = now - this.playStartTime - this.pausedTime;
2566
- const currentTargetDts = this.firstFrameDts + elapsed;
2567
- this.setCurrentTime(Math.floor(currentTargetDts));
2568
- let frameToRender = null;
2569
- while (this._timedFrameBuffer.length > 0 && this._timedFrameBuffer[0].dts <= currentTargetDts) {
3141
+ if (this.lastRenderedDts < 0 && this._timedFrameBuffer.length > 0) {
3142
+ const firstFrame = this._timedFrameBuffer.shift();
3143
+ this.renderFrame(firstFrame, now);
3144
+ this.firstFrameDts = firstFrame.dts;
3145
+ this.playStartTime = now;
3146
+ this.pausedTime = 0;
3147
+ console.log("[FLVPlayer] Live first frame rendered immediately, dts:", firstFrame.dts);
3148
+ } else {
3149
+ const elapsed = now - this.playStartTime - this.pausedTime;
3150
+ const currentTargetDts = this.firstFrameDts + elapsed;
3151
+ this.setCurrentTime(Math.floor(currentTargetDts));
3152
+ let frameToRender = null;
3153
+ while (this._timedFrameBuffer.length > 0 && this._timedFrameBuffer[0].dts <= currentTargetDts) {
3154
+ if (frameToRender) {
3155
+ this.droppedFrames++;
3156
+ }
3157
+ frameToRender = this._timedFrameBuffer.shift();
3158
+ }
2570
3159
  if (frameToRender) {
2571
- this.droppedFrames++;
3160
+ this.renderFrame(frameToRender, now);
2572
3161
  }
2573
- frameToRender = this._timedFrameBuffer.shift();
2574
- }
2575
- if (frameToRender) {
2576
- this.renderFrame(frameToRender, now);
2577
3162
  }
2578
3163
  } else {
2579
3164
  const nextFrame = this._timedFrameBuffer[0];
@@ -2644,9 +3229,11 @@ class FLVPlayer extends BasePlayer {
2644
3229
  console.log("[FLVPlayer] Loading URL:", url);
2645
3230
  this.currentUrl = url;
2646
3231
  this.stopLiveDownload();
3232
+ this.abortController = new AbortController();
2647
3233
  this._timedFrameBuffer = [];
2648
3234
  this._videoTagQueue = [];
2649
3235
  this.liveDownloadAbort = false;
3236
+ this.readyFired = false;
2650
3237
  this.resetState();
2651
3238
  this.playStartTime = 0;
2652
3239
  this.firstFrameDts = -1;
@@ -2734,8 +3321,8 @@ class FLVPlayer extends BasePlayer {
2734
3321
  * 开始播放(覆盖基类方法)
2735
3322
  */
2736
3323
  async play() {
2737
- const MIN_BUFFER_SIZE = this.dynamicMinBufferSize;
2738
- const MAX_WAIT_TIME = 1e4;
3324
+ const MIN_BUFFER_SIZE = this.config.isLive ? 3 : this.dynamicMinBufferSize;
3325
+ const MAX_WAIT_TIME = this.config.isLive ? 3e3 : 1e4;
2739
3326
  const startTime = Date.now();
2740
3327
  console.log(`[FLVPlayer] Waiting for buffer (target: ${MIN_BUFFER_SIZE} frames)...`);
2741
3328
  let aggressiveDecodeCount = 0;
@@ -2753,6 +3340,10 @@ class FLVPlayer extends BasePlayer {
2753
3340
  const dts = queuedTag.tag.timestamp;
2754
3341
  const pts = dts + queuedTag.tag.compositionTimeOffset;
2755
3342
  this._timedFrameBuffer.push({ frame, dts, pts });
3343
+ if (!this.readyFired) {
3344
+ this.readyFired = true;
3345
+ this.callbacks.onReady?.();
3346
+ }
2756
3347
  if (this.videoWidth === 0 || this.videoHeight === 0) {
2757
3348
  this.adjustBufferForResolution(frame.width, frame.height);
2758
3349
  }
@@ -2797,6 +3388,25 @@ class FLVPlayer extends BasePlayer {
2797
3388
  console.log("[FLVPlayer] Pausing live stream, keeping download running");
2798
3389
  }
2799
3390
  }
3391
+ /**
3392
+ * 销毁播放器,释放所有资源
3393
+ */
3394
+ destroy() {
3395
+ this.stopLiveDownload();
3396
+ if (this.h264Decoder) {
3397
+ this.h264Decoder.destroy();
3398
+ this.h264Decoder = null;
3399
+ }
3400
+ if (this.hevcDecoder) {
3401
+ this.hevcDecoder.destroy();
3402
+ this.hevcDecoder = null;
3403
+ }
3404
+ if (this.prefetcher) {
3405
+ this.prefetcher.reset();
3406
+ this.prefetcher = null;
3407
+ }
3408
+ super.destroy();
3409
+ }
2800
3410
  /**
2801
3411
  * 处理预取缓冲区数据(供预取器调用)
2802
3412
  */
@@ -2879,6 +3489,10 @@ class FLVPlayer extends BasePlayer {
2879
3489
  const pts = dts + queuedTag.tag.compositionTimeOffset;
2880
3490
  this._timedFrameBuffer.push({ frame, dts, pts });
2881
3491
  decodedInBatch++;
3492
+ if (!this.readyFired) {
3493
+ this.readyFired = true;
3494
+ this.callbacks.onReady?.();
3495
+ }
2882
3496
  if (this.videoWidth === 0 || this.videoHeight === 0) {
2883
3497
  this.adjustBufferForResolution(frame.width, frame.height);
2884
3498
  }
@@ -2925,7 +3539,17 @@ class FLVPlayer extends BasePlayer {
2925
3539
  */
2926
3540
  async loadFLV(url) {
2927
3541
  console.log("[FLVPlayer] Fetching FLV...");
2928
- const response = await fetch(url);
3542
+ const signal = this.abortController?.signal;
3543
+ let response;
3544
+ try {
3545
+ response = await fetch(url, { signal });
3546
+ } catch (error) {
3547
+ if (error.name === "AbortError") {
3548
+ console.log("[FLVPlayer] Fetch aborted");
3549
+ return;
3550
+ }
3551
+ throw error;
3552
+ }
2929
3553
  if (!response.ok) {
2930
3554
  throw new Error(`Failed to fetch FLV: ${response.status}`);
2931
3555
  }
@@ -3000,6 +3624,10 @@ class FLVPlayer extends BasePlayer {
3000
3624
  }
3001
3625
  }
3002
3626
  } catch (error) {
3627
+ if (error.name === "AbortError" || this.liveDownloadAbort) {
3628
+ console.log("[FLVPlayer] Loading aborted");
3629
+ return;
3630
+ }
3003
3631
  console.error("[FLVPlayer] Error loading FLV:", error);
3004
3632
  throw error;
3005
3633
  } finally {
@@ -3044,6 +3672,10 @@ class FLVPlayer extends BasePlayer {
3044
3672
  */
3045
3673
  stopLiveDownload() {
3046
3674
  this.liveDownloadAbort = true;
3675
+ if (this.abortController) {
3676
+ this.abortController.abort();
3677
+ this.abortController = null;
3678
+ }
3047
3679
  if (this.liveReader) {
3048
3680
  this.liveReader.cancel();
3049
3681
  this.liveReader = null;
@@ -3367,6 +3999,13 @@ class Canvas2DRenderer {
3367
3999
  if (!this.ctx) {
3368
4000
  throw new Error("Invalid canvas element");
3369
4001
  }
4002
+ this.clear();
4003
+ }
4004
+ /**
4005
+ * 清除 canvas 内容
4006
+ */
4007
+ clear() {
4008
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3370
4009
  }
3371
4010
  render(frame) {
3372
4011
  const { width, height, data } = frame;
@@ -3426,6 +4065,10 @@ class EcPlayerCore {
3426
4065
  this.pendingRenderer = null;
3427
4066
  }
3428
4067
  await this.player.load(url);
4068
+ if (this.player instanceof HLSPlayer) {
4069
+ this.detectedFormat = this.player.isFMP4 ? "hls-fmp4" : "hls-ts";
4070
+ console.log("[EcPlayerCore] Updated format after playlist parse:", this.detectedFormat);
4071
+ }
3429
4072
  await this.player.initDecoder();
3430
4073
  this.callbacks.onStateChange?.({ isLoaded: true });
3431
4074
  } finally {
@@ -3886,6 +4529,7 @@ export {
3886
4529
  EcPlayerCore,
3887
4530
  EnvDetector,
3888
4531
  LogLevel,
4532
+ WASMLoader,
3889
4533
  setLogLevel
3890
4534
  };
3891
4535
  //# sourceMappingURL=index.js.map