@give-tech/ec-player 0.0.1-beta.3 → 0.0.1-beta.31
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/demuxer/fMP4Demuxer.d.ts +22 -4
- package/dist/demuxer/fMP4Demuxer.d.ts.map +1 -1
- package/dist/index.js +465 -164
- package/dist/index.js.map +1 -1
- package/dist/player/EcPlayerCore.d.ts.map +1 -1
- package/dist/player/FLVPlayer.d.ts.map +1 -1
- package/dist/player/HLSPlayer.d.ts +28 -0
- package/dist/player/HLSPlayer.d.ts.map +1 -1
- package/dist/prefetch/SegmentPrefetcher.d.ts +4 -1
- package/dist/prefetch/SegmentPrefetcher.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -118,9 +118,11 @@ class H264Decoder {
|
|
|
118
118
|
}
|
|
119
119
|
const DEFAULT_PLAYER_CONFIG = {
|
|
120
120
|
wasmPath: "/wasm/decoder-simd.js",
|
|
121
|
-
targetBufferSize:
|
|
121
|
+
targetBufferSize: 90,
|
|
122
|
+
// 90 帧(约 3.6 秒 @25fps),适应高分辨率视频
|
|
122
123
|
decodeBatchSize: 2,
|
|
123
|
-
maxQueueSize:
|
|
124
|
+
maxQueueSize: 300,
|
|
125
|
+
// 增加 sample 队列上限
|
|
124
126
|
isLive: false
|
|
125
127
|
};
|
|
126
128
|
class BasePlayer {
|
|
@@ -415,6 +417,9 @@ class fMP4Demuxer {
|
|
|
415
417
|
this.timescale = 1e3;
|
|
416
418
|
this.trackId = 1;
|
|
417
419
|
this.avcC = null;
|
|
420
|
+
this.hvcC = null;
|
|
421
|
+
this.isHevc = false;
|
|
422
|
+
this.trackHandlers = /* @__PURE__ */ new Map();
|
|
418
423
|
}
|
|
419
424
|
/**
|
|
420
425
|
* 解析初始化段 (ftyp + moov)
|
|
@@ -433,7 +438,9 @@ class fMP4Demuxer {
|
|
|
433
438
|
return {
|
|
434
439
|
trackId: this.trackId,
|
|
435
440
|
timescale: this.timescale,
|
|
436
|
-
avcC: this.avcC || void 0
|
|
441
|
+
avcC: this.avcC || void 0,
|
|
442
|
+
hvcC: this.hvcC || void 0,
|
|
443
|
+
isHevc: this.isHevc
|
|
437
444
|
};
|
|
438
445
|
}
|
|
439
446
|
/**
|
|
@@ -458,33 +465,56 @@ class fMP4Demuxer {
|
|
|
458
465
|
parseTrak(data, trakOffset, trakSize) {
|
|
459
466
|
let offset = trakOffset + 8;
|
|
460
467
|
const endOffset = trakOffset + trakSize;
|
|
468
|
+
let currentTrackId = null;
|
|
469
|
+
let handlerType = null;
|
|
470
|
+
console.log("[fMP4Demuxer] parseTrak called, size:", trakSize);
|
|
461
471
|
while (offset < endOffset - 8) {
|
|
462
472
|
const boxSize = readUint32$1(data, offset);
|
|
463
473
|
const boxType = readFourCC(data, offset + 4);
|
|
464
474
|
if (boxSize < 8) break;
|
|
465
|
-
if (boxType === "
|
|
466
|
-
|
|
475
|
+
if (boxType === "tkhd") {
|
|
476
|
+
const boxDataStart = offset + 8;
|
|
477
|
+
const version = data[boxDataStart];
|
|
478
|
+
if (version === 1) {
|
|
479
|
+
currentTrackId = readUint32$1(data, boxDataStart + 1 + 3 + 8 + 8);
|
|
480
|
+
} else {
|
|
481
|
+
currentTrackId = readUint32$1(data, boxDataStart + 1 + 3 + 4 + 4);
|
|
482
|
+
}
|
|
483
|
+
console.log("[fMP4Demuxer] tkhd: version=", version, "trackId=", currentTrackId);
|
|
484
|
+
} else if (boxType === "mdia") {
|
|
485
|
+
handlerType = this.parseMdiaAndGetHandlerType(data, offset, boxSize);
|
|
486
|
+
if (handlerType && currentTrackId !== null) {
|
|
487
|
+
this.trackHandlers.set(currentTrackId, handlerType);
|
|
488
|
+
console.log("[fMP4Demuxer] Track", currentTrackId, "handler:", handlerType);
|
|
489
|
+
if (handlerType === "vide") {
|
|
490
|
+
this.trackId = currentTrackId;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
467
493
|
}
|
|
468
494
|
offset += boxSize;
|
|
469
495
|
}
|
|
470
496
|
}
|
|
471
497
|
/**
|
|
472
|
-
* 解析 mdia box
|
|
498
|
+
* 解析 mdia box 并返回 handler type
|
|
473
499
|
*/
|
|
474
|
-
|
|
500
|
+
parseMdiaAndGetHandlerType(data, mdiaOffset, mdiaSize) {
|
|
475
501
|
let offset = mdiaOffset + 8;
|
|
476
502
|
const endOffset = mdiaOffset + mdiaSize;
|
|
503
|
+
let handlerType = null;
|
|
477
504
|
while (offset < endOffset - 8) {
|
|
478
505
|
const boxSize = readUint32$1(data, offset);
|
|
479
506
|
const boxType = readFourCC(data, offset + 4);
|
|
480
507
|
if (boxSize < 8) break;
|
|
481
508
|
if (boxType === "mdhd") {
|
|
482
509
|
this.parseMdhd(data, offset + 8);
|
|
483
|
-
} else if (boxType === "
|
|
510
|
+
} else if (boxType === "hdlr") {
|
|
511
|
+
handlerType = readFourCC(data, offset + 8 + 8);
|
|
512
|
+
} else if (boxType === "minf" && handlerType === "vide") {
|
|
484
513
|
this.parseMinf(data, offset, boxSize);
|
|
485
514
|
}
|
|
486
515
|
offset += boxSize;
|
|
487
516
|
}
|
|
517
|
+
return handlerType;
|
|
488
518
|
}
|
|
489
519
|
/**
|
|
490
520
|
* 解析 mdhd box 获取 timescale
|
|
@@ -530,7 +560,7 @@ class fMP4Demuxer {
|
|
|
530
560
|
}
|
|
531
561
|
}
|
|
532
562
|
/**
|
|
533
|
-
* 解析 stsd box 获取 avcC
|
|
563
|
+
* 解析 stsd box 获取 avcC 或 hvcC
|
|
534
564
|
*/
|
|
535
565
|
parseStsd(data, stsdOffset, stsdSize) {
|
|
536
566
|
const boxDataOffset = stsdOffset + 8;
|
|
@@ -544,6 +574,10 @@ class fMP4Demuxer {
|
|
|
544
574
|
this.parseAvcSampleEntry(data, offset, entrySize);
|
|
545
575
|
return;
|
|
546
576
|
}
|
|
577
|
+
if (entryType === "hvc1" || entryType === "hev1") {
|
|
578
|
+
this.parseHevcSampleEntry(data, offset, entrySize);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
547
581
|
offset += entrySize;
|
|
548
582
|
}
|
|
549
583
|
}
|
|
@@ -564,6 +598,25 @@ class fMP4Demuxer {
|
|
|
564
598
|
offset += boxSize;
|
|
565
599
|
}
|
|
566
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* 解析 HEVC Sample Entry
|
|
603
|
+
*/
|
|
604
|
+
parseHevcSampleEntry(data, entryOffset, entrySize) {
|
|
605
|
+
this.isHevc = true;
|
|
606
|
+
let offset = entryOffset + 8 + 78;
|
|
607
|
+
const endOffset = entryOffset + entrySize;
|
|
608
|
+
while (offset < endOffset - 8) {
|
|
609
|
+
const boxSize = readUint32$1(data, offset);
|
|
610
|
+
const boxType = readFourCC(data, offset + 4);
|
|
611
|
+
if (boxSize < 8) break;
|
|
612
|
+
if (boxType === "hvcC") {
|
|
613
|
+
this.hvcC = data.slice(offset, offset + boxSize);
|
|
614
|
+
console.log("[fMP4Demuxer] Found hvcC, size:", boxSize);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
offset += boxSize;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
567
620
|
/**
|
|
568
621
|
* 解析 moof box
|
|
569
622
|
*/
|
|
@@ -714,21 +767,51 @@ class fMP4Demuxer {
|
|
|
714
767
|
}
|
|
715
768
|
/**
|
|
716
769
|
* 从 moof + mdat 提取视频样本
|
|
770
|
+
* 只提取视频轨道(trackId 匹配)的数据
|
|
717
771
|
*/
|
|
718
|
-
extractSamples(moof, mdatData, mdatOffset) {
|
|
772
|
+
extractSamples(moof, mdatData, moofOffset, mdatOffset) {
|
|
719
773
|
const samples = [];
|
|
774
|
+
console.log(`[fMP4Demuxer] extractSamples: moof.trafs=${moof.trafs.length}, trackHandlers=${JSON.stringify(Object.fromEntries(this.trackHandlers))}`);
|
|
720
775
|
for (const traf of moof.trafs) {
|
|
721
|
-
if (!traf.tfhd || !traf.tfdt)
|
|
776
|
+
if (!traf.tfhd || !traf.tfdt) {
|
|
777
|
+
console.log(`[fMP4Demuxer] traf missing tfhd or tfdt`);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const trackId = traf.tfhd.trackId;
|
|
781
|
+
const handlerType = this.trackHandlers.get(trackId);
|
|
782
|
+
if (handlerType && handlerType !== "vide") {
|
|
783
|
+
console.log(`[fMP4Demuxer] Skipping non-video track: trackId=${trackId}, handler=${handlerType}`);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (!handlerType) {
|
|
787
|
+
console.log(`[fMP4Demuxer] Unknown trackId=${trackId}, will filter by sample size`);
|
|
788
|
+
} else {
|
|
789
|
+
console.log(`[fMP4Demuxer] Processing video track: trackId=${trackId}, truns=${traf.truns.length}`);
|
|
790
|
+
}
|
|
722
791
|
let baseDts = traf.tfdt.baseMediaDecodeTime;
|
|
792
|
+
const baseDataOffset = traf.tfhd.baseDataOffset ?? moofOffset;
|
|
723
793
|
for (const trun of traf.truns) {
|
|
724
794
|
let dataOffset = 0;
|
|
725
795
|
if (trun.dataOffset !== void 0) {
|
|
726
|
-
|
|
796
|
+
const absoluteOffset = baseDataOffset + trun.dataOffset;
|
|
797
|
+
dataOffset = absoluteOffset - mdatOffset - 8;
|
|
727
798
|
}
|
|
728
799
|
for (const sample of trun.samples) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
800
|
+
let sampleSize = sample.size;
|
|
801
|
+
if (sampleSize === void 0 || sampleSize <= 0) {
|
|
802
|
+
sampleSize = mdatData.length - dataOffset;
|
|
803
|
+
}
|
|
804
|
+
if (sampleSize <= 0) {
|
|
805
|
+
console.log(`[fMP4Demuxer] Skipping sample: no data available`);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (!handlerType && sampleSize < 1e3) {
|
|
809
|
+
dataOffset += sampleSize;
|
|
810
|
+
baseDts += sample.duration ?? 0;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const sampleData = mdatData.slice(dataOffset, dataOffset + sampleSize);
|
|
814
|
+
const isSync = !((sample.flags ?? 0) & 16777216);
|
|
732
815
|
const cto = sample.compositionTimeOffset ?? 0;
|
|
733
816
|
const duration = sample.duration ?? 0;
|
|
734
817
|
samples.push({
|
|
@@ -736,9 +819,9 @@ class fMP4Demuxer {
|
|
|
736
819
|
dts: baseDts,
|
|
737
820
|
pts: baseDts + cto,
|
|
738
821
|
duration,
|
|
739
|
-
isSync
|
|
822
|
+
isSync
|
|
740
823
|
});
|
|
741
|
-
dataOffset +=
|
|
824
|
+
dataOffset += sampleSize;
|
|
742
825
|
baseDts += duration;
|
|
743
826
|
}
|
|
744
827
|
}
|
|
@@ -757,6 +840,18 @@ class fMP4Demuxer {
|
|
|
757
840
|
getAvcC() {
|
|
758
841
|
return this.avcC;
|
|
759
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* 获取 hvcC 配置
|
|
845
|
+
*/
|
|
846
|
+
getHvcC() {
|
|
847
|
+
return this.hvcC;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* 是否为 HEVC 编码
|
|
851
|
+
*/
|
|
852
|
+
isHevcStream() {
|
|
853
|
+
return this.isHevc;
|
|
854
|
+
}
|
|
760
855
|
}
|
|
761
856
|
class NALParser {
|
|
762
857
|
/**
|
|
@@ -804,6 +899,93 @@ class NALParser {
|
|
|
804
899
|
return units;
|
|
805
900
|
}
|
|
806
901
|
}
|
|
902
|
+
const HEVC_NAL_TYPE = {
|
|
903
|
+
// Picture Parameter Set
|
|
904
|
+
IDR_W_RADL: 19
|
|
905
|
+
};
|
|
906
|
+
class HEVCDecoder {
|
|
907
|
+
constructor(wasmLoader) {
|
|
908
|
+
this.wasmModule = null;
|
|
909
|
+
this.decoderContext = null;
|
|
910
|
+
this.wasmModule = wasmLoader.getModule();
|
|
911
|
+
}
|
|
912
|
+
async init() {
|
|
913
|
+
if (!this.wasmModule) {
|
|
914
|
+
throw new Error("WASM module not loaded");
|
|
915
|
+
}
|
|
916
|
+
this.decoderContext = this.wasmModule._create_decoder(173);
|
|
917
|
+
if (this.decoderContext === 0 || this.decoderContext === null) {
|
|
918
|
+
throw new Error("Failed to create HEVC decoder context");
|
|
919
|
+
}
|
|
920
|
+
console.log("[HEVCDecoder] Initialized with codec_id=173");
|
|
921
|
+
}
|
|
922
|
+
decode(nalUnit) {
|
|
923
|
+
if (this.decoderContext === null || !this.wasmModule) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
const dataPtr = this.wasmModule._malloc(nalUnit.size);
|
|
927
|
+
this.wasmModule.HEAPU8.set(nalUnit.data, dataPtr);
|
|
928
|
+
const result = this.wasmModule._decode_video(
|
|
929
|
+
this.decoderContext,
|
|
930
|
+
dataPtr,
|
|
931
|
+
nalUnit.size
|
|
932
|
+
);
|
|
933
|
+
this.wasmModule._free(dataPtr);
|
|
934
|
+
if (result === 1) {
|
|
935
|
+
const width = this.wasmModule._get_frame_width(this.decoderContext);
|
|
936
|
+
const height = this.wasmModule._get_frame_height(this.decoderContext);
|
|
937
|
+
if (width <= 0 || height <= 0) {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
const yPtr = this.wasmModule._get_frame_data(this.decoderContext, 0);
|
|
941
|
+
const uPtr = this.wasmModule._get_frame_data(this.decoderContext, 1);
|
|
942
|
+
const vPtr = this.wasmModule._get_frame_data(this.decoderContext, 2);
|
|
943
|
+
const yLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 0);
|
|
944
|
+
const uLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 1);
|
|
945
|
+
const vLineSize = this.wasmModule._get_frame_linesize(this.decoderContext, 2);
|
|
946
|
+
const frameData = this.yuv420pToRgba(
|
|
947
|
+
yPtr,
|
|
948
|
+
uPtr,
|
|
949
|
+
vPtr,
|
|
950
|
+
width,
|
|
951
|
+
height,
|
|
952
|
+
yLineSize,
|
|
953
|
+
uLineSize,
|
|
954
|
+
vLineSize
|
|
955
|
+
);
|
|
956
|
+
return { width, height, data: frameData };
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
yuv420pToRgba(yPtr, uPtr, vPtr, width, height, yLineSize, uLineSize, vLineSize) {
|
|
961
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
962
|
+
for (let y = 0; y < height; y++) {
|
|
963
|
+
for (let x = 0; x < width; x++) {
|
|
964
|
+
const yIndex = y * yLineSize + x;
|
|
965
|
+
const uIndex = (y >> 1) * uLineSize + (x >> 1);
|
|
966
|
+
const vIndex = (y >> 1) * vLineSize + (x >> 1);
|
|
967
|
+
const yValue = this.wasmModule.HEAPU8[yPtr + yIndex];
|
|
968
|
+
const uValue = this.wasmModule.HEAPU8[uPtr + uIndex];
|
|
969
|
+
const vValue = this.wasmModule.HEAPU8[vPtr + vIndex];
|
|
970
|
+
const c = yValue - 16;
|
|
971
|
+
const d = uValue - 128;
|
|
972
|
+
const e = vValue - 128;
|
|
973
|
+
let r = 298 * c + 409 * e + 128 >> 8;
|
|
974
|
+
let g = 298 * c - 100 * d - 208 * e + 128 >> 8;
|
|
975
|
+
let b = 298 * c + 516 * d + 128 >> 8;
|
|
976
|
+
r = r < 0 ? 0 : r > 255 ? 255 : r;
|
|
977
|
+
g = g < 0 ? 0 : g > 255 ? 255 : g;
|
|
978
|
+
b = b < 0 ? 0 : b > 255 ? 255 : b;
|
|
979
|
+
const rgbaIndex = (y * width + x) * 4;
|
|
980
|
+
rgba[rgbaIndex] = r;
|
|
981
|
+
rgba[rgbaIndex + 1] = g;
|
|
982
|
+
rgba[rgbaIndex + 2] = b;
|
|
983
|
+
rgba[rgbaIndex + 3] = 255;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return rgba;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
807
989
|
class BasePrefetcher {
|
|
808
990
|
constructor(config, callbacks = {}) {
|
|
809
991
|
this.isRunning = false;
|
|
@@ -1077,11 +1259,16 @@ class SegmentPrefetcher extends BasePrefetcher {
|
|
|
1077
1259
|
/**
|
|
1078
1260
|
* 处理预取队列中的数据
|
|
1079
1261
|
*
|
|
1262
|
+
* @param maxSegments 最多处理的分片数量(默认处理所有)
|
|
1080
1263
|
* @returns 是否有数据被处理
|
|
1081
1264
|
*/
|
|
1082
|
-
processQueue() {
|
|
1265
|
+
processQueue(maxSegments) {
|
|
1083
1266
|
let processed = false;
|
|
1267
|
+
let processedCount = 0;
|
|
1084
1268
|
while (this.prefetchQueue.length > 0) {
|
|
1269
|
+
if (maxSegments !== void 0 && processedCount >= maxSegments) {
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1085
1272
|
const item = this.prefetchQueue[0];
|
|
1086
1273
|
if (item.segmentIndex !== this.currentSegmentIndex) {
|
|
1087
1274
|
break;
|
|
@@ -1097,6 +1284,7 @@ class SegmentPrefetcher extends BasePrefetcher {
|
|
|
1097
1284
|
callbacks.onSegmentParsed?.(item.segmentIndex, itemCount);
|
|
1098
1285
|
console.log(`[SegmentPrefetcher] Parsed segment #${item.segmentIndex}: ${itemCount} items, ${parseTime.toFixed(0)}ms`);
|
|
1099
1286
|
processed = true;
|
|
1287
|
+
processedCount++;
|
|
1100
1288
|
}
|
|
1101
1289
|
return processed;
|
|
1102
1290
|
}
|
|
@@ -1324,7 +1512,8 @@ const HLS_PREFETCHER_CONFIG = {
|
|
|
1324
1512
|
...DEFAULT_SEGMENT_PREFETCHER_CONFIG,
|
|
1325
1513
|
lowWaterMark: 30,
|
|
1326
1514
|
highWaterMark: 100,
|
|
1327
|
-
prefetchAhead:
|
|
1515
|
+
prefetchAhead: 6,
|
|
1516
|
+
// 预载 6 个分片(约 30 秒),适应高分辨率视频
|
|
1328
1517
|
prefetchInterval: 10,
|
|
1329
1518
|
dynamicInterval: true
|
|
1330
1519
|
};
|
|
@@ -1334,12 +1523,18 @@ const DEFAULT_HLS_CONFIG = {
|
|
|
1334
1523
|
class HLSSegmentPrefetcher extends SegmentPrefetcher {
|
|
1335
1524
|
constructor(config, callbacks, player) {
|
|
1336
1525
|
super(config, callbacks);
|
|
1526
|
+
this.currentInitSegmentUri = null;
|
|
1337
1527
|
this.player = player;
|
|
1338
1528
|
}
|
|
1339
1529
|
/**
|
|
1340
1530
|
* 获取分片数据
|
|
1341
1531
|
*/
|
|
1342
1532
|
async fetchSegment(segment, index) {
|
|
1533
|
+
if (segment.initSegmentUri && segment.initSegmentUri !== this.currentInitSegmentUri) {
|
|
1534
|
+
console.log("[HLSSegmentPrefetcher] Init segment changed:", segment.initSegmentUri);
|
|
1535
|
+
await this.player.loadNewInitSegment(segment.initSegmentUri);
|
|
1536
|
+
this.currentInitSegmentUri = segment.initSegmentUri;
|
|
1537
|
+
}
|
|
1343
1538
|
const baseUrl = this.baseUrl;
|
|
1344
1539
|
const url = segment.uri.startsWith("http") ? segment.uri : baseUrl + segment.uri;
|
|
1345
1540
|
const headers = {};
|
|
@@ -1378,19 +1573,29 @@ class HLSPlayer extends BasePlayer {
|
|
|
1378
1573
|
this.pesExtractor = new PESExtractor();
|
|
1379
1574
|
this.nalParser = new NALParser();
|
|
1380
1575
|
this.fmp4Demuxer = new fMP4Demuxer();
|
|
1576
|
+
this.hevcDecoder = null;
|
|
1381
1577
|
this.isPrefetching = false;
|
|
1382
1578
|
this.currentSegmentIndex = 0;
|
|
1383
1579
|
this.segments = [];
|
|
1384
1580
|
this.fmp4Segments = [];
|
|
1385
1581
|
this.initSegment = null;
|
|
1386
1582
|
this._isFMP4 = false;
|
|
1583
|
+
this._isHevc = false;
|
|
1387
1584
|
this.currentPlaylistUrl = "";
|
|
1388
1585
|
this._nalQueue = [];
|
|
1389
1586
|
this._sampleQueue = [];
|
|
1390
1587
|
this.prefetcher = null;
|
|
1391
1588
|
this.lastRenderTime = 0;
|
|
1589
|
+
this.playStartTime = 0;
|
|
1590
|
+
this.accumulatedMediaTime = 0;
|
|
1591
|
+
this._lastLogTime = 0;
|
|
1392
1592
|
this.renderLoop = (timestamp = 0) => {
|
|
1393
1593
|
if (!this.isPlaying) return;
|
|
1594
|
+
const now = performance.now();
|
|
1595
|
+
if (!this._lastLogTime || now - this._lastLogTime > 1e3) {
|
|
1596
|
+
this._lastLogTime = now;
|
|
1597
|
+
console.log("[renderLoop] frameBuffer=", this.frameBuffer.length, "renderer=", !!this.renderer);
|
|
1598
|
+
}
|
|
1394
1599
|
const downloaded = this.isFMP4 ? this.sampleQueue.length : this.nalQueue.length;
|
|
1395
1600
|
const segments = this.isFMP4 ? this.fmp4Segments : this.segments;
|
|
1396
1601
|
const totalSegments = segments.length;
|
|
@@ -1402,19 +1607,35 @@ class HLSPlayer extends BasePlayer {
|
|
|
1402
1607
|
totalSegments
|
|
1403
1608
|
});
|
|
1404
1609
|
if (this.frameBuffer.length > 0 && this.renderer) {
|
|
1405
|
-
const frame = this.frameBuffer
|
|
1406
|
-
this.
|
|
1407
|
-
if (this.
|
|
1408
|
-
|
|
1409
|
-
this.
|
|
1610
|
+
const frame = this.frameBuffer[0];
|
|
1611
|
+
const timescale = this.fmp4Demuxer.getTimescale();
|
|
1612
|
+
if (this.playStartTime === 0) {
|
|
1613
|
+
this.playStartTime = now;
|
|
1614
|
+
this.accumulatedMediaTime = 0;
|
|
1615
|
+
console.log("[renderLoop] Init: timescale=", timescale);
|
|
1616
|
+
}
|
|
1617
|
+
const elapsedWallTime = now - this.playStartTime;
|
|
1618
|
+
const frameDurationMs = frame.duration ? frame.duration * 1e3 / timescale : 33.33;
|
|
1619
|
+
if (Math.floor(elapsedWallTime / 1e3) !== Math.floor((elapsedWallTime - 20) / 1e3)) {
|
|
1620
|
+
console.log("[renderLoop] elapsed=", Math.floor(elapsedWallTime), "accumulated=", Math.floor(this.accumulatedMediaTime), "frameDuration=", frame.duration, "frameDurationMs=", frameDurationMs.toFixed(2), "frameBuffer=", this.frameBuffer.length);
|
|
1621
|
+
}
|
|
1622
|
+
const bufferMs = 50;
|
|
1623
|
+
if (elapsedWallTime >= this.accumulatedMediaTime - bufferMs) {
|
|
1624
|
+
this.frameBuffer.shift();
|
|
1625
|
+
this.renderer.render(frame);
|
|
1626
|
+
this.accumulatedMediaTime += frameDurationMs;
|
|
1627
|
+
this.setCurrentTime(this.accumulatedMediaTime / 1e3);
|
|
1628
|
+
this.updateState({ resolution: `${frame.width}x${frame.height}` });
|
|
1629
|
+
const fps = this.renderer.updateFps();
|
|
1630
|
+
this.updateState({ fps });
|
|
1631
|
+
this.callbacks.onFrameRender?.(frame);
|
|
1410
1632
|
}
|
|
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
1633
|
} else {
|
|
1417
|
-
this.
|
|
1634
|
+
if (this.playStartTime !== 0) {
|
|
1635
|
+
const oldPlayStartTime = this.playStartTime;
|
|
1636
|
+
this.playStartTime = now - this.accumulatedMediaTime;
|
|
1637
|
+
console.log("[renderLoop] No frames, adjusting playStartTime from", oldPlayStartTime, "to", this.playStartTime, "accumulated=", this.accumulatedMediaTime);
|
|
1638
|
+
}
|
|
1418
1639
|
}
|
|
1419
1640
|
this.frameTimer = requestAnimationFrame(this.renderLoop);
|
|
1420
1641
|
};
|
|
@@ -1423,6 +1644,9 @@ class HLSPlayer extends BasePlayer {
|
|
|
1423
1644
|
get isFMP4() {
|
|
1424
1645
|
return this._isFMP4;
|
|
1425
1646
|
}
|
|
1647
|
+
get isHevc() {
|
|
1648
|
+
return this._isHevc;
|
|
1649
|
+
}
|
|
1426
1650
|
get nalQueue() {
|
|
1427
1651
|
return this._nalQueue;
|
|
1428
1652
|
}
|
|
@@ -1489,6 +1713,8 @@ class HLSPlayer extends BasePlayer {
|
|
|
1489
1713
|
this.isPlaying = true;
|
|
1490
1714
|
this.decodeLoopAbort = false;
|
|
1491
1715
|
this.lastRenderTime = 0;
|
|
1716
|
+
this.playStartTime = 0;
|
|
1717
|
+
this.accumulatedMediaTime = 0;
|
|
1492
1718
|
this.updateState({ isPlaying: true });
|
|
1493
1719
|
if (!this.prefetcher) {
|
|
1494
1720
|
this.initPrefetcher();
|
|
@@ -1531,10 +1757,22 @@ class HLSPlayer extends BasePlayer {
|
|
|
1531
1757
|
console.log("[DecodeLoop] START, isFMP4:", this.isFMP4);
|
|
1532
1758
|
let batchCount = 0;
|
|
1533
1759
|
while (this.isPlaying && !this.decodeLoopAbort) {
|
|
1534
|
-
this.
|
|
1760
|
+
const sampleQueueSize = this.sampleQueue.length;
|
|
1761
|
+
const maxSamplesBeforePause = this.config.maxQueueSize * 3;
|
|
1762
|
+
if (sampleQueueSize < maxSamplesBeforePause && this.prefetcher) {
|
|
1763
|
+
this.prefetcher.processQueue(1);
|
|
1764
|
+
}
|
|
1765
|
+
if (!this.config.isLive && this.frameBuffer.length >= this.config.targetBufferSize) {
|
|
1766
|
+
await this.sleep(10);
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1535
1769
|
const batchSize = this.config.decodeBatchSize;
|
|
1536
1770
|
let decodedInBatch = 0;
|
|
1537
1771
|
if (this.isFMP4) {
|
|
1772
|
+
const queueSize = this.sampleQueue.length;
|
|
1773
|
+
if (queueSize > 0 || batchCount % 50 === 0) {
|
|
1774
|
+
console.log("[DecodeLoop] fMP4: sampleQueue=", queueSize, "frameBuffer=", this.frameBuffer.length, "batch=", batchCount);
|
|
1775
|
+
}
|
|
1538
1776
|
while (this.sampleQueue.length > 0 && decodedInBatch < batchSize) {
|
|
1539
1777
|
const queuedSample = this.sampleQueue.shift();
|
|
1540
1778
|
const sample = queuedSample.sample;
|
|
@@ -1542,9 +1780,11 @@ class HLSPlayer extends BasePlayer {
|
|
|
1542
1780
|
if (frame) {
|
|
1543
1781
|
this.frameBuffer.push(frame);
|
|
1544
1782
|
decodedInBatch++;
|
|
1545
|
-
|
|
1546
|
-
this.frameBuffer.
|
|
1547
|
-
|
|
1783
|
+
if (this.config.isLive) {
|
|
1784
|
+
while (this.frameBuffer.length > this.config.targetBufferSize) {
|
|
1785
|
+
this.frameBuffer.shift();
|
|
1786
|
+
this.droppedFrames++;
|
|
1787
|
+
}
|
|
1548
1788
|
}
|
|
1549
1789
|
}
|
|
1550
1790
|
}
|
|
@@ -1560,9 +1800,11 @@ class HLSPlayer extends BasePlayer {
|
|
|
1560
1800
|
if (frame) {
|
|
1561
1801
|
this.frameBuffer.push(frame);
|
|
1562
1802
|
decodedInBatch++;
|
|
1563
|
-
|
|
1564
|
-
this.frameBuffer.
|
|
1565
|
-
|
|
1803
|
+
if (this.config.isLive) {
|
|
1804
|
+
while (this.frameBuffer.length > this.config.targetBufferSize) {
|
|
1805
|
+
this.frameBuffer.shift();
|
|
1806
|
+
this.droppedFrames++;
|
|
1807
|
+
}
|
|
1566
1808
|
}
|
|
1567
1809
|
}
|
|
1568
1810
|
}
|
|
@@ -1604,7 +1846,9 @@ class HLSPlayer extends BasePlayer {
|
|
|
1604
1846
|
const segmentInfos = this.fmp4Segments.map((seg) => ({
|
|
1605
1847
|
uri: seg.uri,
|
|
1606
1848
|
duration: seg.duration,
|
|
1607
|
-
byteRange: seg.byteRange
|
|
1849
|
+
byteRange: seg.byteRange,
|
|
1850
|
+
initSegmentUri: seg.initSegmentUri
|
|
1851
|
+
// 传递初始化段 URI
|
|
1608
1852
|
}));
|
|
1609
1853
|
this.prefetcher.setSegments(segmentInfos, baseUrl);
|
|
1610
1854
|
} else {
|
|
@@ -1620,33 +1864,48 @@ class HLSPlayer extends BasePlayer {
|
|
|
1620
1864
|
* 解析 fMP4 数据
|
|
1621
1865
|
*/
|
|
1622
1866
|
parseFMP4Data(data) {
|
|
1623
|
-
let
|
|
1624
|
-
let mdatOffset = -1;
|
|
1625
|
-
let mdatSize = 0;
|
|
1867
|
+
let totalSampleCount = 0;
|
|
1626
1868
|
let offset = 0;
|
|
1627
1869
|
while (offset < data.length - 8) {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1870
|
+
let moofOffset = -1;
|
|
1871
|
+
let mdatOffset = -1;
|
|
1872
|
+
let mdatSize = 0;
|
|
1873
|
+
while (offset < data.length - 8) {
|
|
1874
|
+
const boxSize = this.readBoxSize(data, offset);
|
|
1875
|
+
const boxType = this.readBoxType(data, offset + 4);
|
|
1876
|
+
if (boxSize < 8) break;
|
|
1877
|
+
if (boxType === "moof") {
|
|
1878
|
+
moofOffset = offset;
|
|
1879
|
+
offset += boxSize;
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
offset += boxSize;
|
|
1636
1883
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1884
|
+
if (moofOffset < 0) break;
|
|
1885
|
+
while (offset < data.length - 8) {
|
|
1886
|
+
const boxSize = this.readBoxSize(data, offset);
|
|
1887
|
+
const boxType = this.readBoxType(data, offset + 4);
|
|
1888
|
+
if (boxSize < 8) break;
|
|
1889
|
+
if (boxType === "mdat") {
|
|
1890
|
+
mdatOffset = offset;
|
|
1891
|
+
mdatSize = boxSize;
|
|
1892
|
+
offset += boxSize;
|
|
1893
|
+
break;
|
|
1894
|
+
}
|
|
1895
|
+
offset += boxSize;
|
|
1896
|
+
}
|
|
1897
|
+
if (mdatOffset < 0) break;
|
|
1641
1898
|
const moof = this.fmp4Demuxer.parseMoof(data, moofOffset);
|
|
1642
1899
|
const mdatData = data.slice(mdatOffset + 8, mdatOffset + mdatSize);
|
|
1643
|
-
const samples = this.fmp4Demuxer.extractSamples(moof, mdatData, mdatOffset);
|
|
1900
|
+
const samples = this.fmp4Demuxer.extractSamples(moof, mdatData, moofOffset, mdatOffset);
|
|
1644
1901
|
for (const sample of samples) {
|
|
1645
1902
|
this._sampleQueue.push({ sample });
|
|
1646
1903
|
}
|
|
1647
|
-
|
|
1904
|
+
totalSampleCount += samples.length;
|
|
1905
|
+
console.log("[parseFMP4Data] Pushed", samples.length, "samples, totalQueue=", this._sampleQueue.length);
|
|
1648
1906
|
}
|
|
1649
|
-
|
|
1907
|
+
console.log("[parseFMP4Data] Total samples parsed:", totalSampleCount);
|
|
1908
|
+
return totalSampleCount;
|
|
1650
1909
|
}
|
|
1651
1910
|
/**
|
|
1652
1911
|
* 解析 TS 数据
|
|
@@ -1690,16 +1949,74 @@ class HLSPlayer extends BasePlayer {
|
|
|
1690
1949
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
1691
1950
|
console.log("[fMP4] Init segment data size:", data.length, "bytes");
|
|
1692
1951
|
const initInfo = this.fmp4Demuxer.parseInitSegment(data);
|
|
1693
|
-
console.log("[fMP4] Parse result:",
|
|
1694
|
-
|
|
1952
|
+
console.log("[fMP4] Parse result:", {
|
|
1953
|
+
isHevc: initInfo?.isHevc,
|
|
1954
|
+
hasAvcC: !!initInfo?.avcC,
|
|
1955
|
+
hasHvcC: !!initInfo?.hvcC,
|
|
1956
|
+
timescale: initInfo?.timescale
|
|
1957
|
+
});
|
|
1958
|
+
this._isHevc = initInfo?.isHevc ?? false;
|
|
1959
|
+
if (initInfo?.hvcC && this._isHevc) {
|
|
1960
|
+
console.log("[fMP4] hvcC size:", initInfo.hvcC.length);
|
|
1961
|
+
await this.initHevcDecoder();
|
|
1962
|
+
this.initDecoderFromHvcC(initInfo.hvcC);
|
|
1963
|
+
console.log("[fMP4] HEVC decoder initialized, timescale:", initInfo.timescale);
|
|
1964
|
+
} else if (initInfo?.avcC) {
|
|
1695
1965
|
console.log("[fMP4] avcC size:", initInfo.avcC.length);
|
|
1696
1966
|
this.initDecoderFromAvcC(initInfo.avcC);
|
|
1697
|
-
console.log("[fMP4]
|
|
1967
|
+
console.log("[fMP4] AVC decoder initialized, timescale:", initInfo.timescale);
|
|
1968
|
+
} else {
|
|
1969
|
+
console.error("[fMP4] Failed to parse init segment or no avcC/hvcC found!");
|
|
1970
|
+
throw new Error("Failed to parse fMP4 init segment: no valid codec config found");
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* 加载新的初始化段(用于不连续性流)
|
|
1975
|
+
*/
|
|
1976
|
+
async loadNewInitSegment(uri) {
|
|
1977
|
+
console.log("[fMP4] Loading new init segment for discontinuity:", uri);
|
|
1978
|
+
this._sampleQueue.length = 0;
|
|
1979
|
+
this._nalQueue.length = 0;
|
|
1980
|
+
const url = uri.startsWith("http") ? uri : this.currentPlaylistUrl.substring(0, this.currentPlaylistUrl.lastIndexOf("/") + 1) + uri;
|
|
1981
|
+
const response = await fetch(url);
|
|
1982
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
1983
|
+
console.log("[fMP4] New init segment data size:", data.length, "bytes");
|
|
1984
|
+
const initInfo = this.fmp4Demuxer.parseInitSegment(data);
|
|
1985
|
+
console.log("[fMP4] New init segment parse result:", {
|
|
1986
|
+
isHevc: initInfo?.isHevc,
|
|
1987
|
+
hasAvcC: !!initInfo?.avcC,
|
|
1988
|
+
hasHvcC: !!initInfo?.hvcC
|
|
1989
|
+
});
|
|
1990
|
+
const newIsHevc = initInfo?.isHevc ?? false;
|
|
1991
|
+
if (newIsHevc !== this._isHevc) {
|
|
1992
|
+
console.log("[fMP4] Codec type changed from", this._isHevc ? "HEVC" : "AVC", "to", newIsHevc ? "HEVC" : "AVC");
|
|
1993
|
+
this._isHevc = newIsHevc;
|
|
1994
|
+
this.decoderInitialized = false;
|
|
1995
|
+
if (newIsHevc) {
|
|
1996
|
+
await this.initHevcDecoder();
|
|
1997
|
+
}
|
|
1998
|
+
} else {
|
|
1999
|
+
this.decoderInitialized = false;
|
|
2000
|
+
}
|
|
2001
|
+
if (initInfo?.hvcC && this._isHevc) {
|
|
2002
|
+
this.initDecoderFromHvcC(initInfo.hvcC);
|
|
2003
|
+
console.log("[fMP4] HEVC decoder re-initialized for discontinuity");
|
|
2004
|
+
} else if (initInfo?.avcC) {
|
|
2005
|
+
this.initDecoderFromAvcC(initInfo.avcC);
|
|
2006
|
+
console.log("[fMP4] AVC decoder re-initialized for discontinuity");
|
|
1698
2007
|
} else {
|
|
1699
|
-
console.error("[fMP4] Failed to parse init segment
|
|
1700
|
-
throw new Error("Failed to parse fMP4 init segment");
|
|
2008
|
+
console.error("[fMP4] Failed to parse new init segment!");
|
|
1701
2009
|
}
|
|
1702
2010
|
}
|
|
2011
|
+
/**
|
|
2012
|
+
* 初始化 HEVC 解码器
|
|
2013
|
+
*/
|
|
2014
|
+
async initHevcDecoder() {
|
|
2015
|
+
if (this.hevcDecoder) return;
|
|
2016
|
+
this.hevcDecoder = new HEVCDecoder(this.wasmLoader);
|
|
2017
|
+
await this.hevcDecoder.init();
|
|
2018
|
+
console.log("[fMP4] HEVC decoder created");
|
|
2019
|
+
}
|
|
1703
2020
|
/**
|
|
1704
2021
|
* 从 avcC 初始化解码器
|
|
1705
2022
|
*/
|
|
@@ -1744,6 +2061,43 @@ class HLSPlayer extends BasePlayer {
|
|
|
1744
2061
|
this.decoderInitialized = true;
|
|
1745
2062
|
console.log("[fMP4] Decoder initialized from avcC");
|
|
1746
2063
|
}
|
|
2064
|
+
/**
|
|
2065
|
+
* 从 hvcC 初始化 HEVC 解码器
|
|
2066
|
+
*/
|
|
2067
|
+
initDecoderFromHvcC(hvcC) {
|
|
2068
|
+
if (this.decoderInitialized || !this.hevcDecoder) return;
|
|
2069
|
+
console.log("[fMP4] hvcC length:", hvcC.length);
|
|
2070
|
+
console.log("[fMP4] hvcC first 8 bytes:", Array.from(hvcC.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join(" "));
|
|
2071
|
+
console.log("[fMP4] hvcC bytes 8-24:", Array.from(hvcC.slice(8, 24)).map((b) => b.toString(16).padStart(2, "0")).join(" "));
|
|
2072
|
+
let offset = 8 + 22;
|
|
2073
|
+
const numOfArrays = hvcC[offset];
|
|
2074
|
+
console.log("[fMP4] hvcC[30] (numOfArrays):", numOfArrays, "0x" + numOfArrays.toString(16));
|
|
2075
|
+
offset += 1;
|
|
2076
|
+
for (let i = 0; i < numOfArrays && offset < hvcC.length - 3; i++) {
|
|
2077
|
+
const typeCompressed = hvcC[offset];
|
|
2078
|
+
const arrayType = typeCompressed & 63;
|
|
2079
|
+
const numNalus = hvcC[offset + 1] << 8 | hvcC[offset + 2];
|
|
2080
|
+
offset += 3;
|
|
2081
|
+
console.log(`[fMP4] HEVC array: type=${arrayType}, count=${numNalus}`);
|
|
2082
|
+
for (let j = 0; j < numNalus && offset < hvcC.length - 2; j++) {
|
|
2083
|
+
const nalUnitLength = hvcC[offset] << 8 | hvcC[offset + 1];
|
|
2084
|
+
offset += 2;
|
|
2085
|
+
if (offset + nalUnitLength > hvcC.length) break;
|
|
2086
|
+
const nalUnitData = hvcC.slice(offset, offset + nalUnitLength);
|
|
2087
|
+
offset += nalUnitLength;
|
|
2088
|
+
const nalWithStartCode = new Uint8Array(4 + nalUnitLength);
|
|
2089
|
+
nalWithStartCode.set([0, 0, 0, 1], 0);
|
|
2090
|
+
nalWithStartCode.set(nalUnitData, 4);
|
|
2091
|
+
if (arrayType === 32 || arrayType === 33 || arrayType === 34) {
|
|
2092
|
+
this.hevcDecoder.decode({ type: arrayType, data: nalWithStartCode, size: nalWithStartCode.length });
|
|
2093
|
+
const typeName = arrayType === 32 ? "VPS" : arrayType === 33 ? "SPS" : "PPS";
|
|
2094
|
+
console.log(`[fMP4] HEVC ${typeName} decoded, length=${nalUnitLength}`);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
this.decoderInitialized = true;
|
|
2099
|
+
console.log("[fMP4] HEVC decoder initialized from hvcC");
|
|
2100
|
+
}
|
|
1747
2101
|
/**
|
|
1748
2102
|
* 解析 HLS 播放列表
|
|
1749
2103
|
*/
|
|
@@ -1776,25 +2130,31 @@ class HLSPlayer extends BasePlayer {
|
|
|
1776
2130
|
}
|
|
1777
2131
|
const isFMP4 = lines.some((line) => line.includes("#EXT-X-MAP:"));
|
|
1778
2132
|
console.log("[parsePlaylist] isFMP4:", isFMP4, "url:", url);
|
|
2133
|
+
let currentInitSegment;
|
|
1779
2134
|
for (let i = 0; i < lines.length; i++) {
|
|
1780
2135
|
const line = lines[i].trim();
|
|
1781
2136
|
if (line.startsWith("#EXT-X-MAP:")) {
|
|
1782
2137
|
const mapInfo = line.substring("#EXT-X-MAP:".length);
|
|
1783
2138
|
const uriMatch = mapInfo.match(/URI="([^"]+)"/);
|
|
1784
2139
|
if (uriMatch) {
|
|
1785
|
-
|
|
2140
|
+
currentInitSegment = { uri: uriMatch[1] };
|
|
1786
2141
|
const byteRangeMatch = mapInfo.match(/BYTERANGE="(\d+)@(\d+)"/);
|
|
1787
2142
|
if (byteRangeMatch) {
|
|
1788
|
-
|
|
2143
|
+
currentInitSegment.byteRange = {
|
|
1789
2144
|
start: parseInt(byteRangeMatch[2]),
|
|
1790
2145
|
end: parseInt(byteRangeMatch[2]) + parseInt(byteRangeMatch[1]) - 1
|
|
1791
2146
|
};
|
|
1792
2147
|
}
|
|
2148
|
+
if (!initSegment) {
|
|
2149
|
+
initSegment = currentInitSegment;
|
|
2150
|
+
}
|
|
1793
2151
|
}
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
if (line === "#EXT-X-DISCONTINUITY") {
|
|
2155
|
+
console.log("[parsePlaylist] Found EXT-X-DISCONTINUITY");
|
|
2156
|
+
continue;
|
|
1794
2157
|
}
|
|
1795
|
-
}
|
|
1796
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1797
|
-
const line = lines[i].trim();
|
|
1798
2158
|
if (line.startsWith("#EXTINF:")) {
|
|
1799
2159
|
const duration = parseFloat(line.split(":")[1].split(",")[0]);
|
|
1800
2160
|
let byteRange;
|
|
@@ -1812,7 +2172,13 @@ class HLSPlayer extends BasePlayer {
|
|
|
1812
2172
|
const uri = nextLine;
|
|
1813
2173
|
if (uri && !uri.startsWith("#")) {
|
|
1814
2174
|
if (isFMP4) {
|
|
1815
|
-
fmp4Segments.push({
|
|
2175
|
+
fmp4Segments.push({
|
|
2176
|
+
uri,
|
|
2177
|
+
duration,
|
|
2178
|
+
byteRange,
|
|
2179
|
+
initSegmentUri: currentInitSegment?.uri
|
|
2180
|
+
// 关联当前初始化段
|
|
2181
|
+
});
|
|
1816
2182
|
} else {
|
|
1817
2183
|
segments.push({ uri, duration });
|
|
1818
2184
|
}
|
|
@@ -1855,7 +2221,8 @@ class HLSPlayer extends BasePlayer {
|
|
|
1855
2221
|
* 解码 fMP4 sample
|
|
1856
2222
|
*/
|
|
1857
2223
|
decodeSample(sample) {
|
|
1858
|
-
|
|
2224
|
+
const decoder = this._isHevc ? this.hevcDecoder : this.decoder;
|
|
2225
|
+
if (!decoder) {
|
|
1859
2226
|
console.warn("[fMP4] Decoder not available");
|
|
1860
2227
|
return null;
|
|
1861
2228
|
}
|
|
@@ -1864,10 +2231,18 @@ class HLSPlayer extends BasePlayer {
|
|
|
1864
2231
|
return null;
|
|
1865
2232
|
}
|
|
1866
2233
|
const data = sample.data;
|
|
1867
|
-
const
|
|
2234
|
+
const looksLikeStartCode = data.length >= 4 && data[0] === 0 && data[1] === 0 && (data[2] === 1 || data[2] === 0 && data[3] === 1);
|
|
2235
|
+
let hasStartCode = looksLikeStartCode;
|
|
2236
|
+
if (looksLikeStartCode) {
|
|
2237
|
+
const possibleAvccLength = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3];
|
|
2238
|
+
if (possibleAvccLength > 0 && possibleAvccLength + 4 === data.length) {
|
|
2239
|
+
hasStartCode = false;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
1868
2242
|
if (hasStartCode) {
|
|
1869
|
-
|
|
1870
|
-
|
|
2243
|
+
const nalType2 = this._isHevc ? sample.isSync ? HEVC_NAL_TYPE.IDR_W_RADL : 1 : sample.isSync ? 5 : 1;
|
|
2244
|
+
return decoder.decode({
|
|
2245
|
+
type: nalType2,
|
|
1871
2246
|
data,
|
|
1872
2247
|
size: data.length
|
|
1873
2248
|
});
|
|
@@ -1906,11 +2281,17 @@ class HLSPlayer extends BasePlayer {
|
|
|
1906
2281
|
writeOffset += nalLength;
|
|
1907
2282
|
offset += 4 + nalLength;
|
|
1908
2283
|
}
|
|
1909
|
-
const
|
|
1910
|
-
|
|
2284
|
+
const nalType = this._isHevc ? sample.isSync ? HEVC_NAL_TYPE.IDR_W_RADL : 1 : sample.isSync ? 5 : 1;
|
|
2285
|
+
const frame = decoder.decode({
|
|
2286
|
+
type: nalType,
|
|
1911
2287
|
data: annexBData,
|
|
1912
2288
|
size: annexBData.length
|
|
1913
2289
|
});
|
|
2290
|
+
if (frame) {
|
|
2291
|
+
frame.pts = sample.pts;
|
|
2292
|
+
frame.dts = sample.dts;
|
|
2293
|
+
frame.duration = sample.duration;
|
|
2294
|
+
}
|
|
1914
2295
|
return frame;
|
|
1915
2296
|
}
|
|
1916
2297
|
/**
|
|
@@ -2388,89 +2769,6 @@ class FLVDemuxer {
|
|
|
2388
2769
|
return tag.frameType === FRAME_TYPE_KEYFRAME;
|
|
2389
2770
|
}
|
|
2390
2771
|
}
|
|
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
2772
|
const CODEC_ID_AVC = 7;
|
|
2475
2773
|
const CODEC_ID_HEVC = 12;
|
|
2476
2774
|
const FLV_PREFETCHER_CONFIG = {
|
|
@@ -2734,8 +3032,8 @@ class FLVPlayer extends BasePlayer {
|
|
|
2734
3032
|
* 开始播放(覆盖基类方法)
|
|
2735
3033
|
*/
|
|
2736
3034
|
async play() {
|
|
2737
|
-
const MIN_BUFFER_SIZE = this.dynamicMinBufferSize;
|
|
2738
|
-
const MAX_WAIT_TIME = 1e4;
|
|
3035
|
+
const MIN_BUFFER_SIZE = this.config.isLive ? 3 : this.dynamicMinBufferSize;
|
|
3036
|
+
const MAX_WAIT_TIME = this.config.isLive ? 3e3 : 1e4;
|
|
2739
3037
|
const startTime = Date.now();
|
|
2740
3038
|
console.log(`[FLVPlayer] Waiting for buffer (target: ${MIN_BUFFER_SIZE} frames)...`);
|
|
2741
3039
|
let aggressiveDecodeCount = 0;
|
|
@@ -2766,11 +3064,11 @@ class FLVPlayer extends BasePlayer {
|
|
|
2766
3064
|
}
|
|
2767
3065
|
console.log("[FLVPlayer] Buffer ready, frames:", this._timedFrameBuffer.length, "queue:", this._videoTagQueue.length);
|
|
2768
3066
|
if (this._timedFrameBuffer.length > 0) {
|
|
2769
|
-
if (this.config.isLive && this._timedFrameBuffer.length >
|
|
2770
|
-
const droppedCount = this._timedFrameBuffer.length -
|
|
2771
|
-
this._timedFrameBuffer = this._timedFrameBuffer.slice(-
|
|
3067
|
+
if (this.config.isLive && this._timedFrameBuffer.length > 60) {
|
|
3068
|
+
const droppedCount = this._timedFrameBuffer.length - 30;
|
|
3069
|
+
this._timedFrameBuffer = this._timedFrameBuffer.slice(-30);
|
|
2772
3070
|
this.droppedFrames += droppedCount;
|
|
2773
|
-
console.log("[FLVPlayer] Live resume: dropped", droppedCount, "old frames, keeping latest
|
|
3071
|
+
console.log("[FLVPlayer] Live resume: dropped", droppedCount, "old frames, keeping latest 30");
|
|
2774
3072
|
}
|
|
2775
3073
|
this.firstFrameDts = this._timedFrameBuffer[0].dts;
|
|
2776
3074
|
this.playStartTime = performance.now();
|
|
@@ -2794,8 +3092,7 @@ class FLVPlayer extends BasePlayer {
|
|
|
2794
3092
|
this.prefetcher.stop();
|
|
2795
3093
|
}
|
|
2796
3094
|
if (this.config.isLive) {
|
|
2797
|
-
console.log("[FLVPlayer] Pausing live stream,
|
|
2798
|
-
this.stopLiveDownload();
|
|
3095
|
+
console.log("[FLVPlayer] Pausing live stream, keeping download running");
|
|
2799
3096
|
}
|
|
2800
3097
|
}
|
|
2801
3098
|
/**
|
|
@@ -3427,6 +3724,10 @@ class EcPlayerCore {
|
|
|
3427
3724
|
this.pendingRenderer = null;
|
|
3428
3725
|
}
|
|
3429
3726
|
await this.player.load(url);
|
|
3727
|
+
if (this.player instanceof HLSPlayer) {
|
|
3728
|
+
this.detectedFormat = this.player.isFMP4 ? "hls-fmp4" : "hls-ts";
|
|
3729
|
+
console.log("[EcPlayerCore] Updated format after playlist parse:", this.detectedFormat);
|
|
3730
|
+
}
|
|
3430
3731
|
await this.player.initDecoder();
|
|
3431
3732
|
this.callbacks.onStateChange?.({ isLoaded: true });
|
|
3432
3733
|
} finally {
|