@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/decoder/H264Decoder.d.ts +5 -0
- package/dist/decoder/H264Decoder.d.ts.map +1 -1
- package/dist/decoder/HEVCDecoder.d.ts +5 -0
- package/dist/decoder/HEVCDecoder.d.ts.map +1 -1
- package/dist/decoder/WASMLoader.d.ts +31 -2
- package/dist/decoder/WASMLoader.d.ts.map +1 -1
- package/dist/demuxer/fMP4Demuxer.d.ts +28 -4
- package/dist/demuxer/fMP4Demuxer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +849 -205
- package/dist/index.js.map +1 -1
- package/dist/player/EcPlayerCore.d.ts.map +1 -1
- package/dist/player/FLVPlayer.d.ts +7 -0
- package/dist/player/FLVPlayer.d.ts.map +1 -1
- package/dist/player/HLSPlayer.d.ts +41 -1
- package/dist/player/HLSPlayer.d.ts.map +1 -1
- package/dist/prefetch/SegmentPrefetcher.d.ts +6 -2
- package/dist/prefetch/SegmentPrefetcher.d.ts.map +1 -1
- package/dist/prefetch/StreamPrefetcher.d.ts.map +1 -1
- package/dist/prefetch/types.d.ts +2 -0
- package/dist/prefetch/types.d.ts.map +1 -1
- package/dist/renderer/Canvas2DRenderer.d.ts +4 -0
- package/dist/renderer/Canvas2DRenderer.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/wasm/decoder.js +19 -0
- package/wasm/decoder.wasm +0 -0
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.
|
|
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
|
-
|
|
18
|
-
if (typeof
|
|
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
|
-
|
|
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.
|
|
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:
|
|
213
|
+
targetBufferSize: 90,
|
|
214
|
+
// 90 帧(约 3.6 秒 @25fps),适应高分辨率视频
|
|
122
215
|
decodeBatchSize: 2,
|
|
123
|
-
maxQueueSize:
|
|
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 === "
|
|
466
|
-
|
|
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
|
-
|
|
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 === "
|
|
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)
|
|
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
|
-
|
|
898
|
+
const absoluteOffset = baseDataOffset + trun.dataOffset;
|
|
899
|
+
dataOffset = absoluteOffset - mdatOffset - 8;
|
|
727
900
|
}
|
|
728
901
|
for (const sample of trun.samples) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
|
924
|
+
isSync
|
|
740
925
|
});
|
|
741
|
-
dataOffset +=
|
|
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:
|
|
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
|
|
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
|
|
1406
|
-
this.
|
|
1407
|
-
if (this.
|
|
1408
|
-
|
|
1409
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1546
|
-
this.
|
|
1547
|
-
this.
|
|
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
|
-
|
|
1564
|
-
this.
|
|
1565
|
-
this.
|
|
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
|
|
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
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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
|
-
|
|
2104
|
+
totalSampleCount += samples.length;
|
|
2105
|
+
console.log("[parseFMP4Data] Pushed", samples.length, "samples, totalQueue=", this._sampleQueue.length);
|
|
1648
2106
|
}
|
|
1649
|
-
|
|
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({
|
|
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
|
-
|
|
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:",
|
|
1694
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
2381
|
+
currentInitSegment = { uri: uriMatch[1] };
|
|
1786
2382
|
const byteRangeMatch = mapInfo.match(/BYTERANGE="(\d+)@(\d+)"/);
|
|
1787
2383
|
if (byteRangeMatch) {
|
|
1788
|
-
|
|
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({
|
|
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
|
-
|
|
1859
|
-
|
|
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,
|
|
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
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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 +=
|
|
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 +
|
|
1896
|
-
const nalLength = data
|
|
1897
|
-
if (nalLength <= 0 || offset +
|
|
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 +
|
|
2529
|
+
annexBData.set(data.slice(offset + lengthSize, offset + lengthSize + nalLength), writeOffset);
|
|
1906
2530
|
writeOffset += nalLength;
|
|
1907
|
-
offset +=
|
|
2531
|
+
offset += lengthSize + nalLength;
|
|
1908
2532
|
}
|
|
1909
|
-
const
|
|
1910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
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.
|
|
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
|
|
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
|