@give-tech/ec-player 0.0.1-beta.48 → 0.0.1-beta.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  export { EcPlayerCore, type EcPlayerCoreConfig } from './player';
8
8
  export { EnvDetector, type EnvInfo, type EnvCapabilities } from './env';
9
9
  export { WASMLoader } from './decoder/WASMLoader';
10
- export type { PlayerState as EcPlayerState, PlayerCallbacks as EcPlayerCallbacks } from './types';
10
+ export type { PlayerState as EcPlayerState, PlayerCallbacks as EcPlayerCallbacks, LoadingStageInfo as EcLoadingStageInfo } from './types';
11
11
  export { LogLevel, setLogLevel } from './utils';
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAGhE,OAAO,EACL,WAAW,EACX,KAAK,OAAO,EACZ,KAAK,eAAe,EACrB,MAAM,OAAO,CAAA;AAGd,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAGjD,YAAY,EACV,WAAW,IAAI,aAAa,EAC5B,eAAe,IAAI,iBAAiB,EACrC,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAGhE,OAAO,EACL,WAAW,EACX,KAAK,OAAO,EACZ,KAAK,eAAe,EACrB,MAAM,OAAO,CAAA;AAGd,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAGjD,YAAY,EACV,WAAW,IAAI,aAAa,EAC5B,eAAe,IAAI,iBAAiB,EACpC,gBAAgB,IAAI,kBAAkB,EACvC,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA"}
package/dist/index.js CHANGED
@@ -235,6 +235,8 @@ class BasePlayer {
235
235
  isPlaying: false,
236
236
  isLoaded: false,
237
237
  isLoading: false,
238
+ isBuffering: false,
239
+ bufferingProgress: void 0,
238
240
  fps: 0,
239
241
  resolution: "-",
240
242
  decoded: 0,
@@ -283,6 +285,23 @@ class BasePlayer {
283
285
  this.decoder = new H264Decoder(this.wasmLoader);
284
286
  await this.decoder.init();
285
287
  }
288
+ /**
289
+ * 预初始化解码器(并行加载优化)
290
+ * 在数据下载的同时加载 WASM 模块
291
+ * 返回初始化结果,供 finishInitDecoder 使用
292
+ */
293
+ async preInitDecoder() {
294
+ await this.wasmLoader.load(this.config.wasmPath);
295
+ return null;
296
+ }
297
+ /**
298
+ * 完成解码器初始化
299
+ * 使用 preInitDecoder 的结果完成初始化
300
+ */
301
+ async finishInitDecoder(_initResult) {
302
+ this.decoder = new H264Decoder(this.wasmLoader);
303
+ await this.decoder.init();
304
+ }
286
305
  /**
287
306
  * 开始播放
288
307
  */
@@ -317,6 +336,8 @@ class BasePlayer {
317
336
  isPlaying: false,
318
337
  isLoaded: false,
319
338
  isLoading: false,
339
+ isBuffering: false,
340
+ bufferingProgress: void 0,
320
341
  fps: 0,
321
342
  resolution: "-",
322
343
  decoded: 0,
@@ -1202,9 +1223,9 @@ class BasePrefetcher {
1202
1223
  const queueSize = this.getQueueSize();
1203
1224
  this.updateStatus({ queueSize });
1204
1225
  if (this.shouldPrefetch()) {
1205
- this.updateStatus({ isPrefetching: true });
1206
- await this.doPrefetch();
1207
- this.updateStatus({ isPrefetching: false });
1226
+ this.doPrefetch().catch((err) => {
1227
+ console.error("[BasePrefetcher] Prefetch error:", err);
1228
+ });
1208
1229
  }
1209
1230
  const interval = this.calculateInterval(queueSize);
1210
1231
  await this.sleep(interval);
@@ -1303,7 +1324,8 @@ class SegmentPrefetcher extends BasePrefetcher {
1303
1324
  this.currentSegmentIndex = 0;
1304
1325
  this.fetchedSegmentCount = 0;
1305
1326
  this.baseUrl = "";
1306
- this.isPrefetchingSegment = false;
1327
+ this.activePrefetchCount = 0;
1328
+ this.prefetchingIndexes = /* @__PURE__ */ new Set();
1307
1329
  }
1308
1330
  /**
1309
1331
  * 创建初始状态
@@ -1356,35 +1378,46 @@ class SegmentPrefetcher extends BasePrefetcher {
1356
1378
  * 判断是否应该预取
1357
1379
  */
1358
1380
  shouldPrefetch() {
1359
- if (this.isPrefetchingSegment) return false;
1381
+ const maxConcurrent = this.config.maxConcurrentPrefetch ?? 1;
1382
+ if (this.activePrefetchCount >= maxConcurrent) return false;
1360
1383
  if (this.segments.length === 0) return false;
1361
1384
  const prefetchAhead = this.config.prefetchAhead;
1362
- if (this.prefetchQueue.length >= prefetchAhead) return false;
1363
- const nextIndex = this.currentSegmentIndex + this.prefetchQueue.length;
1385
+ const totalPending = this.prefetchQueue.length + this.activePrefetchCount;
1386
+ if (totalPending >= prefetchAhead) return false;
1387
+ const nextIndex = this.currentSegmentIndex + this.prefetchQueue.length + this.activePrefetchCount;
1364
1388
  return nextIndex < this.segments.length;
1365
1389
  }
1366
1390
  /**
1367
- * 执行预取
1391
+ * 执行预取(支持并行下载)
1368
1392
  */
1369
1393
  async doPrefetch() {
1370
- if (this.isPrefetchingSegment) return;
1371
- this.isPrefetchingSegment = true;
1394
+ const maxConcurrent = this.config.maxConcurrentPrefetch ?? 1;
1395
+ if (this.activePrefetchCount >= maxConcurrent) return;
1396
+ this.activePrefetchCount++;
1372
1397
  this.updateStatus({ isPrefetching: true });
1373
1398
  try {
1374
- const nextIndex = this.currentSegmentIndex + this.prefetchQueue.length;
1399
+ const nextIndex = this.currentSegmentIndex + this.prefetchQueue.length + this.prefetchingIndexes.size;
1375
1400
  if (nextIndex >= this.segments.length) {
1401
+ this.activePrefetchCount--;
1402
+ return;
1403
+ }
1404
+ if (this.prefetchingIndexes.has(nextIndex)) {
1405
+ this.activePrefetchCount--;
1376
1406
  return;
1377
1407
  }
1408
+ this.prefetchingIndexes.add(nextIndex);
1378
1409
  const segment = this.segments[nextIndex];
1379
1410
  const fetchStart = performance.now();
1380
1411
  const data = await this.fetchSegment(segment, nextIndex);
1381
1412
  const fetchTime = performance.now() - fetchStart;
1413
+ this.prefetchingIndexes.delete(nextIndex);
1382
1414
  this.prefetchQueue.push({
1383
1415
  data,
1384
1416
  segmentIndex: nextIndex,
1385
1417
  fetchTime,
1386
1418
  segmentDuration: segment.duration
1387
1419
  });
1420
+ this.prefetchQueue.sort((a, b) => a.segmentIndex - b.segmentIndex);
1388
1421
  this.fetchedSegmentCount++;
1389
1422
  this.updateStatus({
1390
1423
  prefetchQueueSize: this.prefetchQueue.length,
@@ -1396,13 +1429,15 @@ class SegmentPrefetcher extends BasePrefetcher {
1396
1429
  } catch (error) {
1397
1430
  if (error.name === "AbortError") {
1398
1431
  console.log(`[SegmentPrefetcher] Fetch aborted`);
1432
+ this.prefetchingIndexes.clear();
1433
+ this.activePrefetchCount = 0;
1399
1434
  return;
1400
1435
  }
1401
1436
  console.error(`[SegmentPrefetcher] Fetch failed:`, error.message);
1402
1437
  this.callbacks.onError?.(error);
1403
1438
  } finally {
1404
- this.isPrefetchingSegment = false;
1405
- this.updateStatus({ isPrefetching: false });
1439
+ this.activePrefetchCount--;
1440
+ this.updateStatus({ isPrefetching: this.activePrefetchCount > 0 });
1406
1441
  }
1407
1442
  }
1408
1443
  /**
@@ -1445,7 +1480,8 @@ class SegmentPrefetcher extends BasePrefetcher {
1445
1480
  this.segments = [];
1446
1481
  this.currentSegmentIndex = 0;
1447
1482
  this.baseUrl = "";
1448
- this.isPrefetchingSegment = false;
1483
+ this.activePrefetchCount = 0;
1484
+ this.prefetchingIndexes.clear();
1449
1485
  }
1450
1486
  /**
1451
1487
  * 获取当前分片索引
@@ -1652,7 +1688,8 @@ const DEFAULT_PREFETCHER_CONFIG = {
1652
1688
  };
1653
1689
  const DEFAULT_SEGMENT_PREFETCHER_CONFIG = {
1654
1690
  ...DEFAULT_PREFETCHER_CONFIG,
1655
- prefetchAhead: 2
1691
+ prefetchAhead: 2,
1692
+ maxConcurrentPrefetch: 2
1656
1693
  };
1657
1694
  const DEFAULT_STREAM_PREFETCHER_CONFIG = {
1658
1695
  ...DEFAULT_PREFETCHER_CONFIG,
@@ -1668,7 +1705,9 @@ const HLS_PREFETCHER_CONFIG = {
1668
1705
  prefetchAhead: 6,
1669
1706
  // 预载 6 个分片(约 30 秒),适应高分辨率视频
1670
1707
  prefetchInterval: 10,
1671
- dynamicInterval: true
1708
+ dynamicInterval: true,
1709
+ maxConcurrentPrefetch: 2
1710
+ // 并行下载 2 个分片
1672
1711
  };
1673
1712
  const DEFAULT_HLS_CONFIG = {
1674
1713
  ...DEFAULT_PLAYER_CONFIG
@@ -1677,10 +1716,12 @@ class HLSSegmentPrefetcher extends SegmentPrefetcher {
1677
1716
  constructor(config, callbacks, player) {
1678
1717
  super(config, callbacks);
1679
1718
  this.currentInitSegmentUri = null;
1719
+ this.progressDisplayIndex = -1;
1720
+ this.lastReportedPercent = -1;
1680
1721
  this.player = player;
1681
1722
  }
1682
1723
  /**
1683
- * 获取分片数据
1724
+ * 获取分片数据(支持流式下载和进度报告)
1684
1725
  */
1685
1726
  async fetchSegment(segment, index) {
1686
1727
  if (segment.initSegmentUri && segment.initSegmentUri !== this.currentInitSegmentUri) {
@@ -1697,7 +1738,61 @@ class HLSSegmentPrefetcher extends SegmentPrefetcher {
1697
1738
  }
1698
1739
  const signal = this.player.getAbortSignal();
1699
1740
  const response = await fetch(url, { headers, signal });
1700
- return new Uint8Array(await response.arrayBuffer());
1741
+ const contentLength = response.headers.get("Content-Length");
1742
+ const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
1743
+ const reader = response.body?.getReader();
1744
+ if (!reader) {
1745
+ return new Uint8Array(await response.arrayBuffer());
1746
+ }
1747
+ const chunks = [];
1748
+ let loadedBytes = 0;
1749
+ let lastProgressTime = 0;
1750
+ let lastProgressBytes = 0;
1751
+ let downloadSpeed = 0;
1752
+ const PROGRESS_INTERVAL = 200;
1753
+ if (index > this.progressDisplayIndex) {
1754
+ this.progressDisplayIndex = index;
1755
+ }
1756
+ while (true) {
1757
+ const { done, value } = await reader.read();
1758
+ if (done) break;
1759
+ chunks.push(value);
1760
+ loadedBytes += value.length;
1761
+ const now = Date.now();
1762
+ if (now - lastProgressTime >= PROGRESS_INTERVAL) {
1763
+ if (lastProgressTime > 0) {
1764
+ const timeDiff = (now - lastProgressTime) / 1e3;
1765
+ const bytesDiff = loadedBytes - lastProgressBytes;
1766
+ downloadSpeed = bytesDiff / timeDiff;
1767
+ }
1768
+ lastProgressTime = now;
1769
+ lastProgressBytes = loadedBytes;
1770
+ if (index === this.progressDisplayIndex && totalSize > 0) {
1771
+ const percent = Math.min(100, Math.round(loadedBytes / totalSize * 100));
1772
+ if (percent !== this.lastReportedPercent) {
1773
+ this.lastReportedPercent = percent;
1774
+ const callbacks = this.callbacks;
1775
+ callbacks.onDownloadProgress?.({
1776
+ segmentIndex: index,
1777
+ loaded: loadedBytes,
1778
+ total: totalSize,
1779
+ speed: downloadSpeed > 0 ? downloadSpeed : void 0
1780
+ });
1781
+ }
1782
+ }
1783
+ }
1784
+ }
1785
+ if (index === this.progressDisplayIndex) {
1786
+ this.progressDisplayIndex = -1;
1787
+ this.lastReportedPercent = -1;
1788
+ }
1789
+ const result = new Uint8Array(loadedBytes);
1790
+ let offset = 0;
1791
+ for (const chunk of chunks) {
1792
+ result.set(chunk, offset);
1793
+ offset += chunk.length;
1794
+ }
1795
+ return result;
1701
1796
  }
1702
1797
  /**
1703
1798
  * 解析分片数据
@@ -1785,12 +1880,20 @@ class HLSPlayer extends BasePlayer {
1785
1880
  totalSegments
1786
1881
  });
1787
1882
  if (this.frameBuffer.length > 0 && this.renderer) {
1883
+ const wasBuffering = this.state.isBuffering;
1884
+ if (this.state.isBuffering) {
1885
+ this.updateState({ isBuffering: false, bufferingProgress: void 0 });
1886
+ }
1788
1887
  const timescale = this.fmp4Demuxer.getTimescale();
1789
1888
  if (this.playStartTime === 0) {
1790
1889
  this.playStartTime = now;
1791
1890
  this.accumulatedMediaTime = 0;
1792
1891
  console.log("[renderLoop] Init: timescale=", timescale);
1793
1892
  }
1893
+ if (wasBuffering && this.accumulatedMediaTime > 0) {
1894
+ this.playStartTime = now - this.accumulatedMediaTime / this._playbackRate;
1895
+ console.log("[renderLoop] Buffer recovered, reset playStartTime to continue from", Math.floor(this.accumulatedMediaTime), "ms");
1896
+ }
1794
1897
  const elapsedWallTime = (now - this.playStartTime) * this._playbackRate;
1795
1898
  if (Math.floor(elapsedWallTime / 1e3) !== Math.floor((elapsedWallTime - 20 * this._playbackRate) / 1e3)) {
1796
1899
  console.log("[renderLoop] elapsed=", Math.floor(elapsedWallTime), "accumulated=", Math.floor(this.accumulatedMediaTime), "rate=", this._playbackRate, "frameBuffer=", this.frameBuffer.length);
@@ -1831,6 +1934,9 @@ class HLSPlayer extends BasePlayer {
1831
1934
  this.callbacks.onFrameRender?.(frame);
1832
1935
  }
1833
1936
  } else {
1937
+ if (this.isPlaying && !this.state.isBuffering) {
1938
+ this.updateState({ isBuffering: true });
1939
+ }
1834
1940
  if (this.playStartTime === 0) {
1835
1941
  console.log("[renderLoop] Waiting for frames...");
1836
1942
  }
@@ -1909,7 +2015,6 @@ class HLSPlayer extends BasePlayer {
1909
2015
  totalSegments: segments.length
1910
2016
  });
1911
2017
  this.initPrefetcher();
1912
- this.prefetcher.start();
1913
2018
  }
1914
2019
  /**
1915
2020
  * 初始化解码器(覆盖基类方法,添加 fMP4 支持)
@@ -1920,6 +2025,22 @@ class HLSPlayer extends BasePlayer {
1920
2025
  await this.loadInitSegment();
1921
2026
  }
1922
2027
  }
2028
+ /**
2029
+ * 预初始化解码器(并行加载优化)
2030
+ * 在数据下载的同时加载 WASM 模块
2031
+ */
2032
+ async preInitDecoder() {
2033
+ return super.preInitDecoder();
2034
+ }
2035
+ /**
2036
+ * 完成解码器初始化(使用预加载的 WASM)
2037
+ */
2038
+ async finishInitDecoder(initResult) {
2039
+ await super.finishInitDecoder(initResult);
2040
+ if (this.isFMP4 && this.initSegment) {
2041
+ await this.loadInitSegment();
2042
+ }
2043
+ }
1923
2044
  /**
1924
2045
  * 开始播放(覆盖基类方法,使用自定义渲染循环)
1925
2046
  */
@@ -2017,14 +2138,26 @@ class HLSPlayer extends BasePlayer {
2017
2138
  async decodeLoop() {
2018
2139
  console.log("[DecodeLoop] START, isFMP4:", this.isFMP4);
2019
2140
  let batchCount = 0;
2020
- const READY_TIMEOUT_MS = 5e3;
2021
- const readyTimeoutId = setTimeout(() => {
2022
- if (!this.readyFired) {
2023
- console.warn("[HLSPlayer] Ready timeout, triggering onReady anyway");
2141
+ const CHECK_INTERVAL_MS = 5e3;
2142
+ let checkCount = 0;
2143
+ const MAX_CHECK_COUNT = 24;
2144
+ const checkReadyTimeout = () => {
2145
+ if (this.readyFired || this.decodeLoopAbort) return;
2146
+ checkCount++;
2147
+ if (checkCount > MAX_CHECK_COUNT) {
2148
+ console.warn("[HLSPlayer] Max ready timeout reached, triggering onReady");
2024
2149
  this.readyFired = true;
2025
2150
  this.callbacks.onReady?.();
2151
+ return;
2026
2152
  }
2027
- }, READY_TIMEOUT_MS);
2153
+ if (this.frameBuffer.length > 0) {
2154
+ this.readyFired = true;
2155
+ this.callbacks.onReady?.();
2156
+ return;
2157
+ }
2158
+ setTimeout(checkReadyTimeout, CHECK_INTERVAL_MS);
2159
+ };
2160
+ setTimeout(checkReadyTimeout, CHECK_INTERVAL_MS);
2028
2161
  while (this.isPlaying && !this.decodeLoopAbort) {
2029
2162
  const sampleQueueSize = this.sampleQueue.length;
2030
2163
  const maxSamplesBeforePause = this.config.maxQueueSize * 3;
@@ -2131,7 +2264,6 @@ class HLSPlayer extends BasePlayer {
2131
2264
  await this.sleep(2);
2132
2265
  }
2133
2266
  }
2134
- clearTimeout(readyTimeoutId);
2135
2267
  console.log("[DecodeLoop] END, batches:", batchCount);
2136
2268
  }
2137
2269
  /**
@@ -2152,6 +2284,18 @@ class HLSPlayer extends BasePlayer {
2152
2284
  onSegmentParsed: (segmentIndex, itemCount) => {
2153
2285
  this.currentSegmentIndex = segmentIndex + 1;
2154
2286
  this.updateState({ segmentIndex: this.currentSegmentIndex });
2287
+ },
2288
+ onDownloadProgress: (progress) => {
2289
+ if (progress.total && progress.total > 0) {
2290
+ const percent = Math.round(progress.loaded / progress.total * 100);
2291
+ this.callbacks.onLoadingStageChange?.({
2292
+ stage: "loading",
2293
+ detail: `下载中 ${percent}%`
2294
+ });
2295
+ if (this.state.isBuffering) {
2296
+ this.updateState({ bufferingProgress: percent });
2297
+ }
2298
+ }
2155
2299
  }
2156
2300
  },
2157
2301
  this
@@ -2354,6 +2498,9 @@ class HLSPlayer extends BasePlayer {
2354
2498
  } else {
2355
2499
  console.error("[fMP4] Failed to parse new init segment!");
2356
2500
  }
2501
+ if (initInfo?.avcC || initInfo?.hvcC) {
2502
+ this.decoderInitialized = true;
2503
+ }
2357
2504
  }
2358
2505
  /**
2359
2506
  * 初始化 HEVC 解码器
@@ -3206,6 +3353,9 @@ class FLVPlayer extends BasePlayer {
3206
3353
  this.abortController = null;
3207
3354
  this.readyFired = false;
3208
3355
  this._currentDownloadSpeed = 0;
3356
+ this._decoderInitContext = null;
3357
+ this._totalFileSize = 0;
3358
+ this._downloadedBytes = 0;
3209
3359
  this.renderedFrames = 0;
3210
3360
  this.lastRenderLogTime = 0;
3211
3361
  this.consecutiveEmptyBuffer = 0;
@@ -3244,6 +3394,9 @@ class FLVPlayer extends BasePlayer {
3244
3394
  }
3245
3395
  this._lastRafTime = now;
3246
3396
  if (this._timedFrameBuffer.length > 0 && this.renderer) {
3397
+ if (this.state.isBuffering) {
3398
+ this.updateState({ isBuffering: false, bufferingProgress: void 0 });
3399
+ }
3247
3400
  this.consecutiveEmptyBuffer = 0;
3248
3401
  if (this.playStartTime <= 0) {
3249
3402
  this.firstFrameDts = this._timedFrameBuffer[0].dts;
@@ -3309,6 +3462,9 @@ class FLVPlayer extends BasePlayer {
3309
3462
  this.lastRenderLogTime = now;
3310
3463
  }
3311
3464
  } else {
3465
+ if (this.isPlaying && !this.state.isBuffering) {
3466
+ this.updateState({ isBuffering: true });
3467
+ }
3312
3468
  if (this.consecutiveEmptyBuffer === 0 && this.lastRenderedDts >= 0) {
3313
3469
  this.bufferEmptyStartTime = now;
3314
3470
  }
@@ -3410,6 +3566,62 @@ class FLVPlayer extends BasePlayer {
3410
3566
  console.warn("[FLVPlayer] No config found, will try to init from first frames");
3411
3567
  }
3412
3568
  }
3569
+ /**
3570
+ * 预初始化解码器(并行加载优化)
3571
+ * 在数据下载的同时加载 WASM 模块
3572
+ */
3573
+ async preInitDecoder() {
3574
+ await this.wasmLoader.load(this.config.wasmPath);
3575
+ this._decoderInitContext = {
3576
+ wasmLoaded: true,
3577
+ codecId: this._currentCodecId
3578
+ };
3579
+ console.log("[FLVPlayer] WASM pre-loaded for parallel init");
3580
+ return this._decoderInitContext;
3581
+ }
3582
+ /**
3583
+ * 完成解码器初始化(使用预加载的 WASM)
3584
+ */
3585
+ async finishInitDecoder(_initResult) {
3586
+ if (!this._decoderInitContext?.wasmLoaded) {
3587
+ return this.initDecoder();
3588
+ }
3589
+ const avcConfig = this.flvDemuxer.getAvcConfig();
3590
+ const hevcConfig = this.flvDemuxer.getHevcConfig();
3591
+ this._currentCodecId = this.flvDemuxer.getCodecId();
3592
+ if (hevcConfig) {
3593
+ console.log("[FLVPlayer] Finishing HEVC decoder init...");
3594
+ this.hevcDecoder = new HEVCDecoder(this.wasmLoader);
3595
+ await this.hevcDecoder.init();
3596
+ if (hevcConfig.vpsList.length > 0 || hevcConfig.spsList.length > 0) {
3597
+ this.initDecoderFromHEVCConfig(hevcConfig);
3598
+ this.decoderInitialized = true;
3599
+ } else {
3600
+ console.log("[FLVPlayer] HEVC config has no VPS/SPS/PPS, will init from first NALU");
3601
+ if (this._videoTagQueue.length > 0) {
3602
+ const firstTag = this._videoTagQueue[0];
3603
+ this.tryInitFromData(firstTag.annexBData);
3604
+ }
3605
+ }
3606
+ } else if (avcConfig) {
3607
+ console.log("[FLVPlayer] Finishing AVC decoder init...");
3608
+ this.h264Decoder = new H264Decoder(this.wasmLoader);
3609
+ await this.h264Decoder.init();
3610
+ if (avcConfig.spsList.length > 0) {
3611
+ this.initDecoderFromAVCConfig(avcConfig);
3612
+ this.decoderInitialized = true;
3613
+ } else {
3614
+ console.log("[FLVPlayer] AVC config has no SPS/PPS, will init from first NALU");
3615
+ if (this._videoTagQueue.length > 0) {
3616
+ const firstTag = this._videoTagQueue[0];
3617
+ this.tryInitFromData(firstTag.annexBData);
3618
+ }
3619
+ }
3620
+ } else {
3621
+ console.warn("[FLVPlayer] No config found, will try to init from first frames");
3622
+ }
3623
+ this._decoderInitContext = null;
3624
+ }
3413
3625
  /**
3414
3626
  * 根据视频分辨率动态调整缓冲参数
3415
3627
  */
@@ -3687,10 +3899,6 @@ class FLVPlayer extends BasePlayer {
3687
3899
  }
3688
3900
  console.log("[FLVPlayer] DecodeLoop END, decoded:", this._timedFrameBuffer.length + this.droppedFrames, "failed:", this.decodeFailCount, "dropped:", this.droppedFrames);
3689
3901
  }
3690
- // ==================== 私有方法 ====================
3691
- /**
3692
- * 流式加载 FLV 文件
3693
- */
3694
3902
  async loadFLV(url) {
3695
3903
  console.log("[FLVPlayer] Fetching FLV...");
3696
3904
  const signal = this.abortController?.signal;
@@ -3708,6 +3916,10 @@ class FLVPlayer extends BasePlayer {
3708
3916
  throw new Error(`Failed to fetch FLV: ${response.status}`);
3709
3917
  }
3710
3918
  console.log("[FLVPlayer] Fetch response OK, status:", response.status);
3919
+ const contentLength = response.headers.get("Content-Length");
3920
+ this._totalFileSize = contentLength ? parseInt(contentLength, 10) : 0;
3921
+ this._downloadedBytes = 0;
3922
+ console.log("[FLVPlayer] Content-Length:", this._totalFileSize, "bytes");
3711
3923
  this.isPrefetching = true;
3712
3924
  this.updateState({ isPrefetching: true });
3713
3925
  const isLive = this.config.isLive || false;
@@ -3724,12 +3936,25 @@ class FLVPlayer extends BasePlayer {
3724
3936
  const startTime = Date.now();
3725
3937
  let started = false;
3726
3938
  let lastLoggedTags = 0;
3939
+ let lastProgressUpdate = 0;
3727
3940
  this.liveReader = reader;
3728
3941
  while (true) {
3729
3942
  const { done, value } = await reader.read();
3730
3943
  if (value) {
3731
3944
  chunks.push(value);
3732
3945
  totalLength += value.length;
3946
+ this._downloadedBytes = totalLength;
3947
+ const now = Date.now();
3948
+ if (this._totalFileSize > 0 && now - lastProgressUpdate > 200) {
3949
+ lastProgressUpdate = now;
3950
+ const percent = Math.round(totalLength / this._totalFileSize * 100);
3951
+ const downloadedMB = (totalLength / 1024 / 1024).toFixed(1);
3952
+ const totalMB = (this._totalFileSize / 1024 / 1024).toFixed(1);
3953
+ this.callbacks.onLoadingStageChange?.({
3954
+ stage: "loading",
3955
+ detail: `${percent}% (${downloadedMB}/${totalMB} MB)`
3956
+ });
3957
+ }
3733
3958
  }
3734
3959
  const shouldStart = totalLength >= MIN_DATA_SIZE || Date.now() - startTime > TIMEOUT_MS;
3735
3960
  if (!started && shouldStart && totalLength > 0) {
@@ -4207,9 +4432,11 @@ class EcPlayerCore {
4207
4432
  */
4208
4433
  async load(url, isLive) {
4209
4434
  this.callbacks.onStateChange?.({ isLoading: true, isLoaded: false });
4435
+ this.callbacks.onLoadingStageChange?.({ stage: "connecting" });
4210
4436
  try {
4211
4437
  this.detectedFormat = FormatDetector.detectFromUrl(url);
4212
4438
  console.log("[EcPlayerCore] Detected format:", this.detectedFormat, "from URL:", url, "isLive:", isLive);
4439
+ this.callbacks.onLoadingStageChange?.({ stage: "detected", detail: this.detectedFormat });
4213
4440
  if (isLive !== void 0) {
4214
4441
  this.config.isLive = isLive;
4215
4442
  }
@@ -4218,13 +4445,22 @@ class EcPlayerCore {
4218
4445
  this.player.setRenderer(this.pendingRenderer);
4219
4446
  this.pendingRenderer = null;
4220
4447
  }
4221
- await this.player.load(url);
4448
+ this.callbacks.onLoadingStageChange?.({ stage: "loading" });
4449
+ const [_, initResult] = await Promise.all([
4450
+ this.player.load(url),
4451
+ this.player.preInitDecoder()
4452
+ ]);
4222
4453
  if (this.player instanceof HLSPlayer) {
4223
4454
  this.detectedFormat = this.player.isFMP4 ? "hls-fmp4" : "hls-ts";
4224
4455
  console.log("[EcPlayerCore] Updated format after playlist parse:", this.detectedFormat);
4225
4456
  }
4226
- await this.player.initDecoder();
4457
+ this.callbacks.onLoadingStageChange?.({ stage: "initializing" });
4458
+ await this.player.finishInitDecoder(initResult);
4227
4459
  this.callbacks.onStateChange?.({ isLoaded: true });
4460
+ } catch (error) {
4461
+ console.error("[EcPlayerCore] Load failed:", error);
4462
+ this.callbacks.onError?.(error);
4463
+ throw error;
4228
4464
  } finally {
4229
4465
  this.callbacks.onStateChange?.({ isLoading: false });
4230
4466
  }
@@ -4300,6 +4536,7 @@ class EcPlayerCore {
4300
4536
  isPlaying: false,
4301
4537
  isLoaded: false,
4302
4538
  isLoading: false,
4539
+ isBuffering: false,
4303
4540
  fps: 0,
4304
4541
  resolution: "-",
4305
4542
  decoded: 0,