@xmaxai/web-sdk 1.0.5 → 1.0.6

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.
@@ -205,7 +205,7 @@ var XmaxError = class _XmaxError extends Error {
205
205
  };
206
206
 
207
207
  // src/Foundation/Runtime/RuntimeInfo.ts
208
- var XMAX_SDK_VERSION = "1.0.5";
208
+ var XMAX_SDK_VERSION = "1.0.6";
209
209
  var _RuntimeInfo = class _RuntimeInfo {
210
210
  /**
211
211
  * 序列化为接口传输使用的 snake_case 字段。
@@ -600,7 +600,6 @@ var RtcProvider = /* @__PURE__ */ ((RtcProvider2) => {
600
600
  var RealtimeModel = /* @__PURE__ */ ((RealtimeModel2) => {
601
601
  RealtimeModel2["x2_0"] = "x2.0";
602
602
  RealtimeModel2["x2_0_trtc"] = "x2.0-trtc";
603
- RealtimeModel2["x2_0_agora"] = "x2.0-agora";
604
603
  RealtimeModel2["x2_1_preview"] = "x2.1-preview";
605
604
  return RealtimeModel2;
606
605
  })(RealtimeModel || {});
@@ -616,12 +615,10 @@ function modelDefaults(model) {
616
615
  var MODEL_DISPLAY_NAMES = {
617
616
  ["x2.0" /* x2_0 */]: "X2.0",
618
617
  ["x2.0-trtc" /* x2_0_trtc */]: "X2.1-preview-trtc",
619
- ["x2.0-agora" /* x2_0_agora */]: "X2.1-preview-agora",
620
- ["x2.1-preview" /* x2_1_preview */]: "X2.1-preview-vertc"
618
+ ["x2.1-preview" /* x2_1_preview */]: "X2.1-preview-agora"
621
619
  };
622
620
  var MODEL_BASE_URLS = {
623
- ["x2.0-agora" /* x2_0_agora */]: "https://dev.xmaxai.com/open/api/v1",
624
- ["x2.1-preview" /* x2_1_preview */]: "https://dev.xmaxai.com/open/api/v1"
621
+ ["x2.1-preview" /* x2_1_preview */]: "https://cloud.xmax.22duck.cn/open/api/v1"
625
622
  };
626
623
  var RESOLUTION_BUCKETS = {
627
624
  ["x2.0" /* x2_0 */]: [],
@@ -629,10 +626,6 @@ var RESOLUTION_BUCKETS = {
629
626
  { width: 1024, height: 1920 },
630
627
  { width: 1920, height: 1024 }
631
628
  ],
632
- ["x2.0-agora" /* x2_0_agora */]: [
633
- { width: 1024, height: 1920 },
634
- { width: 1920, height: 1024 }
635
- ],
636
629
  ["x2.1-preview" /* x2_1_preview */]: [
637
630
  { width: 1024, height: 1920 },
638
631
  { width: 1920, height: 1024 }
@@ -641,7 +634,6 @@ var RESOLUTION_BUCKETS = {
641
634
  var MAXIMUM_INPUT_PIXELS = {
642
635
  ["x2.0" /* x2_0 */]: 128e4,
643
636
  ["x2.0-trtc" /* x2_0_trtc */]: 21e5,
644
- ["x2.0-agora" /* x2_0_agora */]: 21e5,
645
637
  ["x2.1-preview" /* x2_1_preview */]: 21e5
646
638
  };
647
639
  function modelDisplayName(model) {
@@ -674,8 +666,7 @@ function defaultCameraVideoFormat(model) {
674
666
  var MODEL_RTC_PROVIDERS = {
675
667
  ["x2.0" /* x2_0 */]: Object.freeze(["vertc" /* vertc */]),
676
668
  ["x2.0-trtc" /* x2_0_trtc */]: Object.freeze(["trtc" /* trtc */]),
677
- ["x2.0-agora" /* x2_0_agora */]: Object.freeze(["agora" /* agora */]),
678
- ["x2.1-preview" /* x2_1_preview */]: Object.freeze(["vertc" /* vertc */])
669
+ ["x2.1-preview" /* x2_1_preview */]: Object.freeze(["agora" /* agora */])
679
670
  };
680
671
  function supportedRtcProviders(model) {
681
672
  return isKnownModel(model) ? MODEL_RTC_PROVIDERS[model] : CUSTOM_MODEL_RTC_PROVIDERS;
@@ -1269,6 +1260,11 @@ ${JSON.stringify({
1269
1260
  receivedBytes: valid(rtc.RecvBytes),
1270
1261
  localVideo,
1271
1262
  remoteVideo: remote,
1263
+ capture: {
1264
+ width: valid(local.captureResolutionWidth, 1),
1265
+ height: valid(local.captureResolutionHeight, 1),
1266
+ frameRate: valid(local.captureFrameRate)
1267
+ },
1272
1268
  localAudio: client.getLocalAudioStats(),
1273
1269
  remoteAudio: client.getRemoteAudioStats()
1274
1270
  }, null, 2)}`, 2 /* performance */);
@@ -1316,6 +1312,59 @@ function rtcOrientedVideoSize(size, environment) {
1316
1312
  return shouldTransposeRtcVideoSize(environment) ? { width: size.height, height: size.width } : size;
1317
1313
  }
1318
1314
 
1315
+ // src/Foundation/RTC/Agora/AgoraVideoSender.ts
1316
+ var AgoraVideoSender = class {
1317
+ static async apply(track, config, ensureActive2) {
1318
+ ensureActive2();
1319
+ const sender = track.getRTCRtpTransceiver()?.sender;
1320
+ if (!sender) throw new XmaxError("RTC_ERROR" /* rtcError */, "WebRTC video sender is unavailable");
1321
+ const capture = sender.track?.getSettings();
1322
+ if (!capture?.width || !capture.height) {
1323
+ throw new XmaxError("RTC_ERROR" /* rtcError */, "WebRTC capture dimensions are unavailable");
1324
+ }
1325
+ const scale = Math.max(1, capture.width / config.width, capture.height / config.height);
1326
+ const parameters = sender.getParameters();
1327
+ if (parameters.encodings?.length !== 1) {
1328
+ throw new XmaxError("RTC_ERROR" /* rtcError */, "Sender-only configuration requires one video encoding");
1329
+ }
1330
+ const encoding = parameters.encodings[0];
1331
+ const previous = { ...encoding };
1332
+ const previousPreference = parameters.degradationPreference;
1333
+ encoding.scaleResolutionDownBy = scale;
1334
+ encoding.maxFramerate = config.frameRate;
1335
+ encoding.maxBitrate = Math.round(config.maximumBitrate * 1e3);
1336
+ parameters.degradationPreference = config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "maintain-framerate" : "maintain-resolution";
1337
+ await sender.setParameters(parameters);
1338
+ ensureActive2();
1339
+ if (track.getRTCRtpTransceiver()?.sender !== sender) {
1340
+ throw new XmaxError("CANCELLED" /* cancelled */, "WebRTC video sender changed during configuration");
1341
+ }
1342
+ const applied = sender.getParameters();
1343
+ const actual = applied.encodings?.[0];
1344
+ if (!actual || Math.abs((actual.scaleResolutionDownBy ?? 1) - scale) > 1e-4 || actual.maxFramerate !== encoding.maxFramerate || actual.maxBitrate !== encoding.maxBitrate) {
1345
+ if (actual && applied.encodings.length === 1) {
1346
+ for (const key of ["scaleResolutionDownBy", "maxFramerate", "maxBitrate"]) {
1347
+ if (previous[key] === void 0) delete actual[key];
1348
+ else actual[key] = previous[key];
1349
+ }
1350
+ if (previousPreference === void 0) delete applied.degradationPreference;
1351
+ else applied.degradationPreference = previousPreference;
1352
+ await sender.setParameters(applied);
1353
+ }
1354
+ throw new XmaxError("RTC_ERROR" /* rtcError */, "Browser did not retain WebRTC video sender parameters");
1355
+ }
1356
+ XmaxLogger.rtc.info(() => `Agora \u53D1\u9001\u53C2\u6570\u5B9E\u9A8C (Agora Sender Parameters)
1357
+ ${JSON.stringify({
1358
+ capture: { width: capture.width, height: capture.height, frameRate: capture.frameRate },
1359
+ target: { width: config.width, height: config.height, frameRate: config.frameRate },
1360
+ scaleResolutionDownBy: actual.scaleResolutionDownBy,
1361
+ maxFramerate: actual.maxFramerate,
1362
+ maxBitrateKbps: actual.maxBitrate / 1e3,
1363
+ minimumBitrateApplied: false
1364
+ }, null, 2)}`, 2 /* performance */);
1365
+ }
1366
+ };
1367
+
1319
1368
  // src/Foundation/RTC/Agora/AgoraRtcManager.ts
1320
1369
  var _AgoraRtcManager = class _AgoraRtcManager {
1321
1370
  /**
@@ -1335,6 +1384,10 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1335
1384
  __publicField(this, "microphone");
1336
1385
  __publicField(this, "externalVideo");
1337
1386
  __publicField(this, "externalAudio");
1387
+ __publicField(this, "appliedOptimizationModes", /* @__PURE__ */ new WeakMap());
1388
+ __publicField(this, "cameraSenderConfiguration");
1389
+ __publicField(this, "senderUpdateTail", Promise.resolve());
1390
+ __publicField(this, "cameraTransceiverListener");
1338
1391
  __publicField(this, "connection");
1339
1392
  __publicField(this, "roomVersion", 0);
1340
1393
  /**
@@ -1351,6 +1404,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1351
1404
  */
1352
1405
  __publicField(this, "listener");
1353
1406
  __publicField(this, "statisticsTimer");
1407
+ __publicField(this, "networkQualitySuspended", false);
1354
1408
  __publicField(this, "audioVolumes", /* @__PURE__ */ new Map());
1355
1409
  __publicField(this, "requestedAudio", /* @__PURE__ */ new Set());
1356
1410
  /**
@@ -1420,6 +1474,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1420
1474
  this.clearRoom();
1421
1475
  this.client = void 0;
1422
1476
  client?.removeAllListeners();
1477
+ this.releaseCameraSender();
1423
1478
  this.camera?.close();
1424
1479
  this.microphone?.close();
1425
1480
  this.externalVideo?.close();
@@ -1458,6 +1513,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1458
1513
  throw this.cancelled();
1459
1514
  }
1460
1515
  this.camera = camera;
1516
+ this.cameraTransceiverListener = () => {
1517
+ if (this.client === client && client.localTracks.includes(camera) && this.videoPublishRequested && !this.rejoinTask) {
1518
+ void this.restoreCameraSender(camera).catch(() => {
1519
+ if (this.camera === camera && this.videoPublishRequested) {
1520
+ XmaxLogger.rtc.warning(() => "Agora \u53D1\u9001\u53C2\u6570\u6062\u590D\u5931\u8D25 (Agora Sender Parameters Restore Failed)");
1521
+ }
1522
+ });
1523
+ }
1524
+ };
1525
+ camera.on("transceiver-updated", this.cameraTransceiverListener);
1461
1526
  return camera.getMediaStreamTrack();
1462
1527
  } catch (error) {
1463
1528
  throw this.mapError(error, "CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */);
@@ -1470,6 +1535,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1470
1535
  const camera = this.requireCamera();
1471
1536
  await this.run(() => camera.setDevice({ facingMode: to === "front" /* front */ ? "user" : "environment" }));
1472
1537
  if (this.camera !== camera) throw this.cancelled();
1538
+ if (this.videoPublishRequested) await this.restoreCameraSender(camera);
1473
1539
  return camera.getMediaStreamTrack();
1474
1540
  }
1475
1541
  /**
@@ -1477,6 +1543,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1477
1543
  */
1478
1544
  async stopCameraCapture() {
1479
1545
  const camera = this.camera;
1546
+ this.releaseCameraSender();
1480
1547
  this.camera = void 0;
1481
1548
  camera?.close();
1482
1549
  }
@@ -1512,11 +1579,26 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1512
1579
  }
1513
1580
  }
1514
1581
  /**
1515
- * 应用公共编码配置;仅相机源补偿移动端转置,Canvas 外部源使用原始方向。
1582
+ * 摄像头实验走原生 sender(发布前暂存);外部视频仍走 Agora 编码配置。
1516
1583
  */
1517
1584
  async configureVideoEncoding(config) {
1518
1585
  const camera = this.requireLocalVideo();
1519
- const encodingSize = this.externalVideo ? config : rtcOrientedVideoSize(config);
1586
+ if (camera === this.camera) {
1587
+ const client = this.requireClient(), version = this.roomVersion;
1588
+ const published = this.videoPublishRequested;
1589
+ await this.run(() => this.queueSenderUpdate(async () => {
1590
+ const ensureActive2 = () => {
1591
+ if (this.camera !== camera || this.client !== client || this.roomVersion !== version || published && !this.videoPublishRequested) throw this.cancelled();
1592
+ };
1593
+ ensureActive2();
1594
+ if (this.videoPublishRequested) {
1595
+ await AgoraVideoSender.apply(camera, { ...config, ...rtcOrientedVideoSize(config) }, ensureActive2);
1596
+ }
1597
+ this.cameraSenderConfiguration = { ...config };
1598
+ }));
1599
+ return;
1600
+ }
1601
+ const encodingSize = config;
1520
1602
  await this.run(async () => {
1521
1603
  await camera.setEncoderConfiguration({
1522
1604
  width: encodingSize.width,
@@ -1525,9 +1607,37 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1525
1607
  bitrateMin: config.minimumBitrate,
1526
1608
  bitrateMax: config.maximumBitrate
1527
1609
  });
1528
- await camera.setOptimizationMode(config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail");
1610
+ if (this.appliedOptimizationModes.get(camera) !== config.encoderPreference) {
1611
+ await camera.setOptimizationMode(config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail");
1612
+ this.appliedOptimizationModes.set(camera, config.encoderPreference);
1613
+ }
1529
1614
  });
1530
1615
  }
1616
+ queueSenderUpdate(operation) {
1617
+ const next = this.senderUpdateTail.then(operation);
1618
+ this.senderUpdateTail = next.catch(() => {
1619
+ });
1620
+ return next;
1621
+ }
1622
+ /** 发布/重建 sender 后恢复最近成功的目标配置;排队时不捕获过期的参数对象。 */
1623
+ restoreCameraSender(camera) {
1624
+ const client = this.client, version = this.roomVersion;
1625
+ return this.queueSenderUpdate(async () => {
1626
+ const ensureActive2 = () => {
1627
+ if (this.camera !== camera || this.client !== client || this.roomVersion !== version || !this.videoPublishRequested) {
1628
+ throw this.cancelled();
1629
+ }
1630
+ };
1631
+ ensureActive2();
1632
+ const config = this.cameraSenderConfiguration;
1633
+ if (config) await AgoraVideoSender.apply(camera, { ...config, ...rtcOrientedVideoSize(config) }, ensureActive2);
1634
+ });
1635
+ }
1636
+ releaseCameraSender() {
1637
+ if (this.cameraTransceiverListener) this.camera?.off("transceiver-updated", this.cameraTransceiverListener);
1638
+ this.cameraTransceiverListener = void 0;
1639
+ this.cameraSenderConfiguration = void 0;
1640
+ }
1531
1641
  /**
1532
1642
  * 使用会话下发的字符串频道与 UID 入房;不采信后端区域覆盖。
1533
1643
  */
@@ -1645,6 +1755,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1645
1755
  if (this.videoPublishRequested && video) {
1646
1756
  await this.run(() => client.publish(video));
1647
1757
  this.ensureRoomCurrent(client, version);
1758
+ if (video === this.camera) await this.restoreCameraSender(this.camera);
1648
1759
  }
1649
1760
  if (this.audioPublishRequested && audio) {
1650
1761
  await this.run(() => client.publish(audio));
@@ -1674,6 +1785,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1674
1785
  if (client !== this.client || !this.videoPublishRequested) throw this.cancelled();
1675
1786
  if (client.localTracks.includes(camera)) return;
1676
1787
  await this.run(() => client.publish(camera));
1788
+ if (camera === this.camera) await this.run(() => this.restoreCameraSender(this.camera));
1677
1789
  }
1678
1790
  /**
1679
1791
  * 停止视频发布但保留采集与预览。
@@ -1871,6 +1983,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1871
1983
  const handleExpired = () => {
1872
1984
  if (!active() || this.tokenExpired) return;
1873
1985
  this.tokenExpired = true;
1986
+ this.listener?.onNetworkStatistics?.(void 0);
1874
1987
  this.listener?.onTokenExpired?.();
1875
1988
  };
1876
1989
  client.on("user-published", (user, kind) => {
@@ -1894,13 +2007,15 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1894
2007
  if (mediaActive()) this.listener?.onCustomMessageReceived(String(uid), typeof data === "string" ? data : new TextDecoder().decode(data));
1895
2008
  });
1896
2009
  client.on("network-quality", (quality) => {
1897
- if (mediaActive()) AgoraRtcStatistics.network(quality, this.listener);
2010
+ if (mediaActive() && !this.networkQualitySuspended) AgoraRtcStatistics.network(quality, this.listener);
1898
2011
  });
1899
2012
  client.on("token-privilege-will-expire", () => {
1900
2013
  if (active() && !this.tokenExpired) this.listener?.onTokenWillExpire?.();
1901
2014
  });
1902
2015
  client.on("token-privilege-did-expire", handleExpired);
1903
2016
  client.on("connection-state-change", (state2, previous, reason) => {
2017
+ if (this.client === client) this.networkQualitySuspended = state2 !== "CONNECTED";
2018
+ if (active() && state2 !== "CONNECTED") this.listener?.onNetworkStatistics?.(void 0);
1904
2019
  if (state2 === "DISCONNECTED" && reason === "TOKEN_EXPIRE") {
1905
2020
  handleExpired();
1906
2021
  return;
@@ -1918,6 +2033,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1918
2033
  this.cancelRejoin?.();
1919
2034
  this.connection = void 0;
1920
2035
  this.tokenExpired = false;
2036
+ this.networkQualitySuspended = false;
1921
2037
  this.videoPublishRequested = false;
1922
2038
  this.audioPublishRequested = false;
1923
2039
  clearInterval(this.statisticsTimer);
@@ -5043,6 +5159,7 @@ var RoomEvent = class _RoomEvent {
5043
5159
  event: "start",
5044
5160
  params: {
5045
5161
  ..._RoomEvent.generationParameters(options),
5162
+ mirror: options.mirror ?? false,
5046
5163
  ...options.context.referenceVideo && {
5047
5164
  ref_video_path: options.context.referenceVideo.path,
5048
5165
  sample_method: options.context.referenceVideo.sampleMethod,
@@ -5495,6 +5612,7 @@ var RoomController = class {
5495
5612
  startGeneration(options) {
5496
5613
  return this.send(
5497
5614
  RoomEvent.start({
5615
+ mirror: options.mirror ?? false,
5498
5616
  userID: this.requireUserID(),
5499
5617
  taskID: options.taskID,
5500
5618
  videoFormat: options.videoFormat,
@@ -5646,6 +5764,7 @@ var _EncodingController = class _EncodingController {
5646
5764
  * 基础层组件
5647
5765
  */
5648
5766
  __publicField(this, "rtcManager");
5767
+ __publicField(this, "lastAppliedConfiguration");
5649
5768
  this.rtcManager = options.rtcManager;
5650
5769
  }
5651
5770
  /**
@@ -5681,14 +5800,38 @@ var _EncodingController = class _EncodingController {
5681
5800
  "Minimum bitrate must not exceed maximum bitrate after applying SDK defaults"
5682
5801
  );
5683
5802
  }
5684
- await this.rtcManager.configureVideoEncoding({
5803
+ const configuration = {
5685
5804
  width: videoFormat.width,
5686
5805
  height: videoFormat.height,
5687
5806
  frameRate: videoFormat.fps,
5688
5807
  minimumBitrate: minimum,
5689
5808
  maximumBitrate: maximum,
5690
5809
  encoderPreference: ENCODER_PREFERENCE_MAP[videoFormat.encoderPreference]
5810
+ };
5811
+ await this.rtcManager.configureVideoEncoding(configuration);
5812
+ this.lastAppliedConfiguration = { ...configuration };
5813
+ }
5814
+ /** 只替换目标帧率,保留最近成功应用的尺寸、已解析码率区间和偏好。 */
5815
+ async updateFrameRate(fps) {
5816
+ const previous = this.lastAppliedConfiguration;
5817
+ if (!previous) {
5818
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Apply an encoder configuration before updating frame rate");
5819
+ }
5820
+ const format = new RealtimeVideoFormat({
5821
+ width: previous.width,
5822
+ height: previous.height,
5823
+ fps,
5824
+ minimumBitrate: previous.minimumBitrate,
5825
+ maximumBitrate: previous.maximumBitrate,
5826
+ encoderPreference: previous.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "maintainFramerate" /* maintainFramerate */ : "maintainQuality" /* maintainQuality */
5691
5827
  });
5828
+ format.validate();
5829
+ if (fps !== previous.frameRate) {
5830
+ const next = { ...previous, frameRate: fps };
5831
+ await this.rtcManager.configureVideoEncoding(next);
5832
+ this.lastAppliedConfiguration = next;
5833
+ }
5834
+ return format;
5692
5835
  }
5693
5836
  /**
5694
5837
  * 按像素面积与帧率计算码率区间(kbps):流畅优先为最低,画质优先为最高。
@@ -5926,9 +6069,13 @@ var StreamController = class {
5926
6069
  *
5927
6070
  * @throws 格式无效、码率区间无效或 RTC 配置失败时抛出错误。
5928
6071
  */
5929
- async setVideoEncoderConfig(videoFormat) {
6072
+ async updateUplinkVideoFormat(videoFormat) {
5930
6073
  await this.encodingController.configure(videoFormat);
5931
6074
  }
6075
+ /** 不根据新帧率重新推算码率区间。 */
6076
+ async updateUplinkFrameRate(fps) {
6077
+ return this.encodingController.updateFrameRate(fps);
6078
+ }
5932
6079
  /**
5933
6080
  * 加入 RTC 房间并发布本地媒体流。
5934
6081
  *
@@ -6014,6 +6161,7 @@ var StreamController = class {
6014
6161
  try {
6015
6162
  const sending = this.roomController.startGeneration({
6016
6163
  taskID,
6164
+ mirror: options.mirror ?? false,
6017
6165
  videoFormat: options.videoFormat,
6018
6166
  targetSize: options.targetSize,
6019
6167
  context: options.context
@@ -6087,7 +6235,7 @@ var StreamController = class {
6087
6235
  *
6088
6236
  * @throws 操作已取消、房间未就绪或信令发送失败时抛出错误。
6089
6237
  */
6090
- async changeTargetSize(taskID, targetSize, ensureActive2) {
6238
+ async updateDownlinkVideoResolution(taskID, targetSize, ensureActive2) {
6091
6239
  try {
6092
6240
  await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive: ensureActive2 });
6093
6241
  } catch (error) {
@@ -6509,6 +6657,195 @@ var StreamController = class {
6509
6657
  }
6510
6658
  };
6511
6659
 
6660
+ // src/Stream/Quality/QualityTiers.ts
6661
+ var INITIAL_QUALITY_LEVEL = 2;
6662
+ var QUALITY_TIERS = {
6663
+ L1: { width: 1024, height: 1920, frameRate: 30 },
6664
+ L2: { width: 768, height: 1440, frameRate: 24 },
6665
+ L3: { width: 640, height: 1200, frameRate: 24 },
6666
+ L4: { width: 512, height: 960, frameRate: 20 },
6667
+ L5: { width: 384, height: 720, frameRate: 16 }
6668
+ };
6669
+
6670
+ // src/Stream/Quality/QualityPolicy.ts
6671
+ var DEFAULT_POLICY = {
6672
+ poorSamples: 3,
6673
+ goodSamples: 5,
6674
+ stabilizationMs: 4e3,
6675
+ probeMs: 2e4,
6676
+ upgradeCooldownMs: 6e4,
6677
+ sampleTimeoutMs: 5e3
6678
+ };
6679
+
6680
+ // src/Stream/Quality/QualityController.ts
6681
+ var ORDERED_QUALITY_TIERS = Object.values(QUALITY_TIERS);
6682
+ var QualityController = class {
6683
+ constructor(apply, now = () => performance.now(), policy = {}) {
6684
+ __publicField(this, "apply", apply);
6685
+ __publicField(this, "now", now);
6686
+ __publicField(this, "policy");
6687
+ __publicField(this, "active", false);
6688
+ __publicField(this, "index", 0);
6689
+ __publicField(this, "good", 0);
6690
+ __publicField(this, "poor", 0);
6691
+ __publicField(this, "lastSampleAt");
6692
+ __publicField(this, "stableUntil", 0);
6693
+ __publicField(this, "upgradeAfter", 0);
6694
+ __publicField(this, "probeUntil", 0);
6695
+ __publicField(this, "switching", false);
6696
+ __publicField(this, "lifecycle", new AbortController());
6697
+ this.policy = { ...DEFAULT_POLICY, ...policy };
6698
+ for (const value of Object.values(this.policy)) {
6699
+ if (!Number.isFinite(value) || value < 0) throw new Error("Invalid quality policy");
6700
+ }
6701
+ for (const value of [this.policy.poorSamples, this.policy.goodSamples]) {
6702
+ if (!Number.isInteger(value) || value < 1) throw new Error("Sample counts must be positive integers");
6703
+ }
6704
+ }
6705
+ get snapshot() {
6706
+ return Object.freeze({
6707
+ active: this.active,
6708
+ level: this.index + 1,
6709
+ ...ORDERED_QUALITY_TIERS[this.index],
6710
+ switching: this.switching,
6711
+ upgradeAfter: this.upgradeAfter
6712
+ });
6713
+ }
6714
+ /** 传入调用方保存的档位,不在 start 中擅自改写编码配置。 */
6715
+ start(level = 1) {
6716
+ if (!Number.isInteger(level) || level < 1 || level > ORDERED_QUALITY_TIERS.length) {
6717
+ throw new Error("Unsupported quality level");
6718
+ }
6719
+ this.stop();
6720
+ this.lifecycle = new AbortController();
6721
+ this.index = level - 1;
6722
+ this.active = true;
6723
+ this.stableUntil = this.upgradeAfter = this.probeUntil = 0;
6724
+ }
6725
+ /** 取消旧会话的迟到结果;底层尚未完成时仍保持串行限制。 */
6726
+ stop() {
6727
+ this.active = false;
6728
+ this.lifecycle.abort();
6729
+ this.resetSamples();
6730
+ }
6731
+ /** 前台操作期间丢弃累计样本,但保留试升失败后的冷却时间。 */
6732
+ resetSamples() {
6733
+ this.resetCounts();
6734
+ this.lastSampleAt = void 0;
6735
+ }
6736
+ /** 只接收质量事件,不得用定时器重复提交同一份缓存统计。异常由调用方记录。 */
6737
+ async observe(quality, sampleAt = this.now()) {
6738
+ if (!this.active || this.switching || !Number.isFinite(sampleAt)) return;
6739
+ const now = this.now();
6740
+ if (sampleAt > now) return;
6741
+ if (this.lastSampleAt !== void 0) {
6742
+ if (sampleAt <= this.lastSampleAt) return;
6743
+ if (sampleAt - this.lastSampleAt > this.policy.sampleTimeoutMs) this.resetCounts();
6744
+ }
6745
+ this.lastSampleAt = sampleAt;
6746
+ if (now - sampleAt > this.policy.sampleTimeoutMs || sampleAt < this.stableUntil) {
6747
+ this.resetCounts();
6748
+ return;
6749
+ }
6750
+ if (quality === 4 || quality === 5) {
6751
+ this.good = 0;
6752
+ this.poor++;
6753
+ if (this.poor >= this.policy.poorSamples && this.index < ORDERED_QUALITY_TIERS.length - 1) {
6754
+ return this.change(this.index + 1, now < this.probeUntil ? "failedProbe" : "poorNetwork");
6755
+ }
6756
+ } else if (quality === 1 || quality === 2) {
6757
+ this.poor = 0;
6758
+ if (sampleAt < this.upgradeAfter) {
6759
+ this.good = 0;
6760
+ return;
6761
+ }
6762
+ this.good++;
6763
+ if (this.good >= this.policy.goodSamples && this.index > 0) {
6764
+ return this.change(this.index - 1, "probeUpgrade");
6765
+ }
6766
+ } else {
6767
+ this.resetCounts();
6768
+ }
6769
+ return;
6770
+ }
6771
+ resetCounts() {
6772
+ this.good = this.poor = 0;
6773
+ }
6774
+ async change(index, reason) {
6775
+ const lifecycle = this.lifecycle;
6776
+ const change = { from: this.index + 1, to: index + 1, ...ORDERED_QUALITY_TIERS[index], reason };
6777
+ this.switching = true;
6778
+ this.resetCounts();
6779
+ try {
6780
+ await this.apply(change, lifecycle.signal);
6781
+ if (lifecycle.signal.aborted) return;
6782
+ this.index = index;
6783
+ const now = this.now();
6784
+ this.stableUntil = now + this.policy.stabilizationMs;
6785
+ this.probeUntil = reason === "probeUpgrade" ? now + this.policy.probeMs : 0;
6786
+ if (reason === "failedProbe") this.upgradeAfter = now + this.policy.upgradeCooldownMs;
6787
+ return change;
6788
+ } catch (error) {
6789
+ if (!lifecycle.signal.aborted) {
6790
+ this.stableUntil = this.now() + this.policy.stabilizationMs;
6791
+ throw error;
6792
+ }
6793
+ return;
6794
+ } finally {
6795
+ this.switching = false;
6796
+ }
6797
+ }
6798
+ };
6799
+
6800
+ // src/Stream/Quality/QualityVideoFormat.ts
6801
+ function qualityVideoSize(baseline, level) {
6802
+ const tier = Object.values(QUALITY_TIERS)[level - 1];
6803
+ if (!Number.isInteger(level) || !tier) throw new Error("Unsupported quality level");
6804
+ baseline.validate();
6805
+ const scale = tier.width / QUALITY_TIERS.L1.width;
6806
+ const even = (value) => Math.max(2, Math.floor(value / 2) * 2);
6807
+ return { width: even(baseline.width * scale), height: even(baseline.height * scale) };
6808
+ }
6809
+ function qualityVideoFormat(baseline, level) {
6810
+ const size = qualityVideoSize(baseline, level);
6811
+ const tier = Object.values(QUALITY_TIERS)[level - 1];
6812
+ return new RealtimeVideoFormat({
6813
+ ...baseline,
6814
+ ...size,
6815
+ fps: Math.min(baseline.fps, tier.frameRate)
6816
+ });
6817
+ }
6818
+
6819
+ // src/Stream/Quality/QualityHistory.ts
6820
+ var QualityHistory = class {
6821
+ constructor(model) {
6822
+ __publicField(this, "model", model);
6823
+ }
6824
+ /** 无记录、损坏、SSR 或浏览器禁止存储时使用默认 L2。 */
6825
+ read(direction) {
6826
+ try {
6827
+ const level = Number(globalThis.localStorage.getItem(this.key(direction)));
6828
+ if (this.isValid(level)) return level;
6829
+ } catch {
6830
+ }
6831
+ return INITIAL_QUALITY_LEVEL;
6832
+ }
6833
+ /** 仅在应用成功后调用;下行成功表示信令已发送,不代表服务端已确认生效。 */
6834
+ write(direction, level) {
6835
+ if (!this.isValid(level)) return;
6836
+ try {
6837
+ globalThis.localStorage.setItem(this.key(direction), String(level));
6838
+ } catch {
6839
+ }
6840
+ }
6841
+ key(direction) {
6842
+ return `xmax:quality:v1:agora:${encodeURIComponent(this.model)}:${direction}`;
6843
+ }
6844
+ isValid(level) {
6845
+ return Number.isInteger(level) && level >= 1 && level <= Object.keys(QUALITY_TIERS).length;
6846
+ }
6847
+ };
6848
+
6512
6849
  // src/Core/Realtime/RealtimeCoordinator.ts
6513
6850
  function scopeAffects(scope, kind) {
6514
6851
  if (scope === 1 /* all */) {
@@ -6611,6 +6948,10 @@ var RealtimeCoordinator = class _RealtimeCoordinator {
6611
6948
  get currentState() {
6612
6949
  return this.state;
6613
6950
  }
6951
+ /** 后台自动调档仅在没有前台操作或清理时申请操作租约。 */
6952
+ get isBusy() {
6953
+ return this.activeOperation !== void 0 || this.termination !== void 0;
6954
+ }
6614
6955
  /**
6615
6956
  * 设置实时状态监听器;设置后立即回放当前状态。
6616
6957
  *
@@ -7087,7 +7428,7 @@ var XmaxRealtimeConnectionManager = class {
7087
7428
  }
7088
7429
  const refreshVersion = ++this.refreshVersion;
7089
7430
  this.credentialController = new AbortController();
7090
- const videoFormat = options.localTrack.videoFormat;
7431
+ const videoFormat = options.localVideoFormat ?? options.localTrack.videoFormat;
7091
7432
  if (!videoFormat) {
7092
7433
  throw new XmaxError(
7093
7434
  "INTERNAL_ERROR" /* internalError */,
@@ -7096,7 +7437,9 @@ var XmaxRealtimeConnectionManager = class {
7096
7437
  }
7097
7438
  const publishLocalMedia = options.publishLocalMedia ?? true;
7098
7439
  if (publishLocalMedia) {
7099
- await streamController.setVideoEncoderConfig(videoFormat);
7440
+ await streamController.updateUplinkVideoFormat(videoFormat);
7441
+ options.ensureCurrent();
7442
+ options.onVideoEncoderConfigured?.();
7100
7443
  }
7101
7444
  options.ensureCurrent();
7102
7445
  let completePublish;
@@ -7243,7 +7586,7 @@ var XmaxRealtimeConnectionManager = class {
7243
7586
  },
7244
7587
  attachHandler: (view) => {
7245
7588
  this.remoteTarget?.setFrameInterpolation?.(void 0);
7246
- view.isMirrored = this.dependencies.isMirrored();
7589
+ view.isMirrored = false;
7247
7590
  this.remoteTarget = view;
7248
7591
  this.dependencies.onRenderAttached();
7249
7592
  const mediaTrack = track.mediaStreamTrack;
@@ -7259,14 +7602,6 @@ var XmaxRealtimeConnectionManager = class {
7259
7602
  }
7260
7603
  });
7261
7604
  }
7262
- /**
7263
- * 同步远端结果画面的镜像状态:与当前本地摄像头位置保持一致。
7264
- */
7265
- updateRemoteMirror() {
7266
- if (this.remoteTarget) {
7267
- this.remoteTarget.isMirrored = this.dependencies.isMirrored();
7268
- }
7269
- }
7270
7605
  /**
7271
7606
  * 构造当前远端生成结果媒体流。
7272
7607
  */
@@ -7410,6 +7745,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
7410
7745
  taskID,
7411
7746
  videoFormat: options.videoFormat,
7412
7747
  context,
7748
+ ...options.targetSize && { targetSize: options.targetSize },
7749
+ mirror: options.mirror ?? false,
7413
7750
  ...options.onStartSent && { onStartSent: options.onStartSent }
7414
7751
  });
7415
7752
  await this.awaitConfirmation(confirmation, options.signal);
@@ -7436,7 +7773,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
7436
7773
  await this.streamController.updateGeneration({
7437
7774
  taskID,
7438
7775
  videoFormat: options.videoFormat,
7439
- context
7776
+ context,
7777
+ ...options.targetSize && { targetSize: options.targetSize }
7440
7778
  });
7441
7779
  if (this.activeTaskID !== taskID) throw RealtimeCoordinator.cancelledError();
7442
7780
  this.currentContext = context;
@@ -7573,6 +7911,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7573
7911
  __publicField(this, "connectionManager");
7574
7912
  __publicField(this, "generationManager");
7575
7913
  __publicField(this, "streamController");
7914
+ __publicField(this, "qualityController");
7915
+ __publicField(this, "qualityHistory");
7916
+ __publicField(this, "qualityStates", /* @__PURE__ */ new WeakMap());
7917
+ __publicField(this, "downlinkQualityController");
7918
+ /** 无服务端生效 ACK;level/size 表示最后成功发送的目标,而非实际接收规格。 */
7919
+ __publicField(this, "downlinkQualityState");
7920
+ __publicField(this, "adaptiveQualityPending", false);
7921
+ __publicField(this, "adaptiveQualityRevision", 0);
7576
7922
  /**
7577
7923
  * 音量配置
7578
7924
  */
@@ -7611,7 +7957,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7611
7957
  sessionService,
7612
7958
  streamController: this.streamController,
7613
7959
  timing: this.launchTimer,
7614
- isMirrored: () => this.cameraController.currentTrack?.position === "front" /* front */,
7615
7960
  remoteAudioVolume: () => this.storedRemoteAudioVolume,
7616
7961
  onHeartbeatFailure: (sessionID, error) => {
7617
7962
  void this.handleHeartbeatFailure(sessionID, error);
@@ -7627,6 +7972,31 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7627
7972
  errorHandler: this.errorHandler,
7628
7973
  cleanup: (scope, taskID) => this.performCleanup(scope, taskID)
7629
7974
  });
7975
+ if (options.provider === "agora" /* agora */) {
7976
+ this.qualityHistory = new QualityHistory(options.model);
7977
+ this.qualityController = new QualityController(async (change, signal) => {
7978
+ if (signal.aborted || this.currentState.connectionState !== "Generating" /* generating */) {
7979
+ throw new XmaxError("CANCELLED" /* cancelled */, "Adaptive video format update was cancelled");
7980
+ }
7981
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
7982
+ const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
7983
+ const state2 = track && this.qualityStates.get(track);
7984
+ if (!track || !state2) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Adaptive video baseline is unavailable");
7985
+ const format = qualityVideoFormat(state2.baseline, change.to);
7986
+ await this.streamController.updateUplinkVideoFormat(format);
7987
+ token.ensureCurrent();
7988
+ this.generationVideoFormat ?? (this.generationVideoFormat = state2.baseline);
7989
+ track.updateVideoFormat(format);
7990
+ state2.level = change.to;
7991
+ this.qualityHistory?.write("uplink", change.to);
7992
+ });
7993
+ });
7994
+ this.downlinkQualityController = new QualityController(async (change, signal) => {
7995
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
7996
+ await this.applyDownlinkQuality(change.to, token, signal);
7997
+ });
7998
+ });
7999
+ }
7630
8000
  this.streamController.setLocalVideoStatisticsListener((statistics) => {
7631
8001
  if (this.acceptsVideoStatistics) {
7632
8002
  this.localVideoStatistics = statistics ? Object.freeze({ ...statistics }) : void 0;
@@ -7636,6 +8006,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7636
8006
  this.streamController.setNetworkStatisticsListener((statistics) => {
7637
8007
  if (this.acceptsVideoStatistics) {
7638
8008
  this.networkStatistics = statistics ? Object.freeze({ ...statistics }) : void 0;
8009
+ this.handleAdaptiveQuality(statistics);
7639
8010
  this.notifyNetworkStatistics();
7640
8011
  }
7641
8012
  });
@@ -7663,7 +8034,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7663
8034
  */
7664
8035
  async setFrameInterpolationEnabled(enabled) {
7665
8036
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
7666
- const format = this.generationVideoFormat ?? this.currentLocalTrack?.videoFormat;
8037
+ const format = this.downlinkQualityState?.size ?? this.generationVideoFormat ?? this.currentLocalTrack?.videoFormat;
7667
8038
  const size = format && enabled ? this.mediaService.resolveFrameInterpolationSize(format) : void 0;
7668
8039
  if (enabled && !await this.checkInterpolationSupport(size, token)) {
7669
8040
  token.ensureCurrent();
@@ -7675,6 +8046,43 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7675
8046
  this.applyRemoteInterpolation();
7676
8047
  });
7677
8048
  }
8049
+ /** 手动调试与自动调档使用同一尺寸信令路径,不修改生成或上行配置。 */
8050
+ async adjustDownlinkQuality(direction) {
8051
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8052
+ const controller = this.downlinkQualityController;
8053
+ const state2 = this.downlinkQualityState;
8054
+ if (!controller || !state2 || this.currentState.connectionState !== "Generating" /* generating */ || direction !== "upgrade" && direction !== "downgrade") {
8055
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Downlink quality adjustment requires an active Agora generation and a valid direction");
8056
+ }
8057
+ const level = Math.max(1, Math.min(Object.keys(QUALITY_TIERS).length, state2.level + (direction === "upgrade" ? -1 : 1)));
8058
+ if (level === state2.level) return;
8059
+ this.adaptiveQualityRevision++;
8060
+ controller.resetSamples();
8061
+ await this.applyDownlinkQuality(level, token);
8062
+ controller.start(level);
8063
+ });
8064
+ }
8065
+ async applyDownlinkQuality(level, token, signal) {
8066
+ const state2 = this.downlinkQualityState;
8067
+ const ensureActive2 = () => {
8068
+ token.ensureCurrent();
8069
+ if (signal?.aborted || !state2 || state2 !== this.downlinkQualityState || this.currentState.connectionState !== "Generating" /* generating */ || this.currentState.taskID !== state2.taskID) {
8070
+ throw new XmaxError("CANCELLED" /* cancelled */, "Target size update was cancelled");
8071
+ }
8072
+ };
8073
+ ensureActive2();
8074
+ if (!state2) return;
8075
+ const size = qualityVideoSize(state2.baseline, level);
8076
+ await this.streamController.updateDownlinkVideoResolution(state2.taskID, size, ensureActive2);
8077
+ ensureActive2();
8078
+ state2.level = level;
8079
+ state2.size = size;
8080
+ this.qualityHistory?.write("downlink", level);
8081
+ if (this.interpolationRequested) {
8082
+ this.interpolationSize = size;
8083
+ this.applyRemoteInterpolation();
8084
+ }
8085
+ }
7678
8086
  /**
7679
8087
  * 根据当前视频尺寸准备插帧,能力不可用时降级为原视频,并在应用前校验租约。
7680
8088
  */
@@ -7834,6 +8242,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7834
8242
  * 停止接收媒体统计并清空各类快照,仅对曾有数据的统计发出清空通知。
7835
8243
  */
7836
8244
  clearVideoStatistics() {
8245
+ this.qualityController?.stop();
8246
+ this.downlinkQualityController?.stop();
8247
+ this.downlinkQualityState = void 0;
8248
+ this.adaptiveQualityRevision++;
7837
8249
  this.acceptsVideoStatistics = false;
7838
8250
  const hadNetworkStatistics = this.networkStatistics !== void 0;
7839
8251
  this.networkStatistics = void 0;
@@ -7995,7 +8407,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7995
8407
  *
7996
8408
  * @throws 无本地相机或文件视频流、参数无效、操作冲突或底层编码更新失败时抛错。
7997
8409
  */
7998
- async updateVideoFormat(videoFormat) {
8410
+ async updateUplinkVideoFormat(videoFormat) {
7999
8411
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8000
8412
  const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
8001
8413
  const previousFormat = track?.videoFormat;
@@ -8003,10 +8415,86 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8003
8415
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera or file video stream is not started");
8004
8416
  }
8005
8417
  videoFormat.validate();
8006
- await this.streamController.setVideoEncoderConfig(videoFormat);
8418
+ this.qualityController?.stop();
8419
+ await this.streamController.updateUplinkVideoFormat(videoFormat);
8007
8420
  token.ensureCurrent();
8008
8421
  this.generationVideoFormat ?? (this.generationVideoFormat = previousFormat);
8009
8422
  track.updateVideoFormat(videoFormat);
8423
+ this.qualityStates.set(track, { baseline: videoFormat, level: 1 });
8424
+ this.qualityHistory?.write("uplink", 1);
8425
+ });
8426
+ }
8427
+ /** 已连接时仅替换帧率;与其他配置及生命周期操作共享同一操作租约。 */
8428
+ async updateUplinkFrameRate(fps) {
8429
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8430
+ this.qualityController?.stop();
8431
+ await this.applyUplinkFrameRate(fps, token);
8432
+ const track = this.currentLocalTrack;
8433
+ if (track?.videoFormat) {
8434
+ this.qualityStates.set(track, { baseline: track.videoFormat, level: 1 });
8435
+ this.qualityHistory?.write("uplink", 1);
8436
+ }
8437
+ });
8438
+ }
8439
+ /** 手动仅调帧接口,成功后的当前格式将成为新的自适应 L1 基准。 */
8440
+ async applyUplinkFrameRate(fps, token) {
8441
+ const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
8442
+ const previous = track?.videoFormat;
8443
+ const state2 = this.currentState.connectionState;
8444
+ if (!track || !previous || state2 !== "Connected" /* connected */ && state2 !== "Generating" /* generating */) {
8445
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Frame rate updates require a connected local camera or file stream");
8446
+ }
8447
+ if (!Number.isFinite(fps) || fps <= 0) {
8448
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video frame rate must be greater than zero");
8449
+ }
8450
+ const format = await this.streamController.updateUplinkFrameRate(fps);
8451
+ token.ensureCurrent();
8452
+ this.generationVideoFormat ?? (this.generationVideoFormat = previous);
8453
+ track.updateVideoFormat(format);
8454
+ }
8455
+ /** 上下行独立判断;串行应用同一原始事件,避免同时降档争抢配置租约。 */
8456
+ handleAdaptiveQuality(statistics) {
8457
+ const revision = ++this.adaptiveQualityRevision;
8458
+ const controller = this.qualityController;
8459
+ const downlink = this.downlinkQualityController;
8460
+ if (!controller || !downlink) return;
8461
+ const taskID = this.currentState.taskID;
8462
+ if (this.currentState.connectionState !== "Generating" /* generating */ || !taskID) {
8463
+ controller.stop();
8464
+ downlink.stop();
8465
+ return;
8466
+ }
8467
+ if (!statistics || this.coordinator.isBusy || this.adaptiveQualityPending) {
8468
+ controller.resetSamples();
8469
+ downlink.resetSamples();
8470
+ return;
8471
+ }
8472
+ const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
8473
+ if (!track) {
8474
+ controller.stop();
8475
+ } else if (!controller.snapshot.active) {
8476
+ const format = track.videoFormat;
8477
+ if (format) {
8478
+ const state3 = this.qualityStates.get(track) ?? { baseline: format, level: 1 };
8479
+ this.qualityStates.set(track, state3);
8480
+ controller.start(state3.level);
8481
+ }
8482
+ }
8483
+ const state2 = this.downlinkQualityState;
8484
+ if (state2 && !downlink.snapshot.active) downlink.start(state2.level);
8485
+ const sampleAt = performance.now();
8486
+ this.adaptiveQualityPending = true;
8487
+ void (async () => {
8488
+ await controller.observe(statistics.uplinkQuality, sampleAt).catch(() => {
8489
+ });
8490
+ if (revision !== this.adaptiveQualityRevision || this.currentState.taskID !== taskID || state2 !== this.downlinkQualityState || this.currentState.connectionState !== "Generating" /* generating */ || this.coordinator.isBusy) {
8491
+ downlink.resetSamples();
8492
+ return;
8493
+ }
8494
+ await downlink.observe(statistics.downlinkQuality, sampleAt).catch(() => {
8495
+ });
8496
+ })().finally(() => {
8497
+ this.adaptiveQualityPending = false;
8010
8498
  });
8011
8499
  }
8012
8500
  /**
@@ -8046,9 +8534,9 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8046
8534
  "cameraSwitch" /* cameraSwitch */,
8047
8535
  void 0,
8048
8536
  async (token) => {
8537
+ this.qualityController?.stop();
8049
8538
  const stream = await this.cameraController.switchCamera();
8050
8539
  token.ensureCurrent();
8051
- this.connectionManager.updateRemoteMirror();
8052
8540
  return stream;
8053
8541
  }
8054
8542
  );
@@ -8116,6 +8604,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8116
8604
  if (currentState.connectionState === "Generating" /* generating */) {
8117
8605
  await this.generationManager.update(currentState.taskID, {
8118
8606
  videoFormat,
8607
+ targetSize: this.downlinkQualityController ? this.downlinkQualityState?.size : void 0,
8119
8608
  context: options.context
8120
8609
  });
8121
8610
  token.ensureCurrent();
@@ -8153,6 +8642,15 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8153
8642
  this.clearVideoStatistics();
8154
8643
  await this.coordinator.terminate(1 /* all */);
8155
8644
  }
8645
+ /** 仅为尚无档位记录的本地上行选择初始档位;不应用配置或覆盖重连状态。 */
8646
+ resolveInitialUplinkQuality(track) {
8647
+ if (!this.qualityController) return void 0;
8648
+ if (this.networkVideoController.currentTrack) return void 0;
8649
+ if (this.qualityStates.has(track)) return void 0;
8650
+ const baseline = track.videoFormat;
8651
+ if (!baseline) return void 0;
8652
+ return { baseline, level: this.qualityHistory.read("uplink") };
8653
+ }
8156
8654
  /**
8157
8655
  * 建立实时连接:创建会话、进房发布、启动心跳并准备远端渲染轨道。
8158
8656
  *
@@ -8188,9 +8686,19 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8188
8686
  });
8189
8687
  beforePublish = () => cameraReady;
8190
8688
  }
8689
+ const initialQuality = this.resolveInitialUplinkQuality(localTrack);
8690
+ const initialFormat = initialQuality ? qualityVideoFormat(initialQuality.baseline, initialQuality.level) : void 0;
8191
8691
  const remote = await this.connectionManager.connect({
8192
8692
  localTrack,
8193
- remoteVideoFormat: this.generationVideoFormat,
8693
+ remoteVideoFormat: this.generationVideoFormat ?? localTrack.videoFormat,
8694
+ localVideoFormat: initialFormat,
8695
+ onVideoEncoderConfigured: () => {
8696
+ if (!initialQuality || !initialFormat) return;
8697
+ this.generationVideoFormat ?? (this.generationVideoFormat = initialQuality.baseline);
8698
+ localTrack.updateVideoFormat(initialFormat);
8699
+ this.qualityStates.set(localTrack, initialQuality);
8700
+ this.qualityHistory?.write("uplink", initialQuality.level);
8701
+ },
8194
8702
  model: this.options.model,
8195
8703
  includeLocalAudio: this.localVideoController.isActive ? this.localVideoController.audioTrack !== void 0 : !this.networkVideoController.currentTrack && this.cameraController.useMicrophone,
8196
8704
  publishLocalMedia: !this.networkVideoController.currentTrack,
@@ -8218,14 +8726,18 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8218
8726
  */
8219
8727
  async performStartGeneration(token, videoFormat, context) {
8220
8728
  token.setFailureScope(0 /* connection */);
8221
- await this.prepareFrameInterpolation(videoFormat, token);
8729
+ const initialLevel = this.qualityHistory?.read("downlink") ?? 1;
8730
+ const targetSize = this.downlinkQualityController ? qualityVideoSize(videoFormat, initialLevel) : void 0;
8731
+ await this.prepareFrameInterpolation(targetSize ?? videoFormat, token);
8222
8732
  const completeFirstFrame = this.launchTimer.startFirstFrame();
8223
8733
  this.remoteFrameDisplayHandler = () => {
8224
8734
  if (!token.signal.aborted) completeFirstFrame();
8225
8735
  };
8226
8736
  const taskID = await this.generationManager.start({
8227
8737
  videoFormat,
8738
+ targetSize,
8228
8739
  context,
8740
+ mirror: this.currentLocalTrack === this.cameraController.currentTrack && this.cameraController.currentTrack?.position === "front" /* front */,
8229
8741
  signal: token.signal,
8230
8742
  ensureCurrent: () => token.ensureCurrent(),
8231
8743
  onStartSent: this.localVideoController.isActive ? async () => {
@@ -8242,10 +8754,18 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8242
8754
  if (!sessionID) {
8243
8755
  throw new XmaxError("SESSION_ERROR" /* sessionError */, "Realtime session is unavailable");
8244
8756
  }
8757
+ this.downlinkQualityController?.stop();
8758
+ this.downlinkQualityState = {
8759
+ taskID,
8760
+ baseline: videoFormat,
8761
+ level: initialLevel,
8762
+ size: targetSize ?? { width: videoFormat.width, height: videoFormat.height }
8763
+ };
8245
8764
  await this.coordinator.commit(
8246
8765
  new RealtimeState({ connectionState: "Generating" /* generating */, sessionID, taskID }),
8247
8766
  token
8248
8767
  );
8768
+ this.qualityHistory?.write("downlink", initialLevel);
8249
8769
  const onFinish = this.networkVideoController.onFinish;
8250
8770
  if (this.networkVideoController.currentTrack) {
8251
8771
  this.streamController.activateNetworkVideoCompletion(onFinish && (() => {
@@ -8884,6 +9404,146 @@ var VideoContentMode = /* @__PURE__ */ ((VideoContentMode2) => {
8884
9404
  return VideoContentMode2;
8885
9405
  })(VideoContentMode || {});
8886
9406
 
9407
+ // src/Render/Video/RenderStatisticsController.ts
9408
+ var RenderStatisticsController = class {
9409
+ /**
9410
+ * 绑定用于观察原视频帧的元素,调用 start 后才开始采集。
9411
+ */
9412
+ constructor(video) {
9413
+ __publicField(this, "video", video);
9414
+ /**
9415
+ * 统计回调
9416
+ */
9417
+ __publicField(this, "handler");
9418
+ /**
9419
+ * 帧观察与采样生命周期
9420
+ */
9421
+ __publicField(this, "timer");
9422
+ __publicField(this, "callbackID");
9423
+ __publicField(this, "sequence", 0);
9424
+ /**
9425
+ * 采样窗口与输出方式
9426
+ */
9427
+ __publicField(this, "startedAt", 0);
9428
+ __publicField(this, "originalFrames", 0);
9429
+ __publicField(this, "interpolatedFrames", 0);
9430
+ __publicField(this, "interpolationActive", false);
9431
+ __publicField(this, "previousPresentedFrames");
9432
+ }
9433
+ /**
9434
+ * 独立于统计面板可见性采集;没有监听者时不占用定时器。
9435
+ */
9436
+ start() {
9437
+ if (this.timer !== void 0 || !this.video.srcObject && !this.video.getAttribute("src") || !this.handler || typeof this.video.requestVideoFrameCallback !== "function") {
9438
+ return;
9439
+ }
9440
+ this.reset();
9441
+ const sequence = this.sequence;
9442
+ const observe = () => {
9443
+ this.callbackID = this.video.requestVideoFrameCallback((_now, metadata) => {
9444
+ if (sequence !== this.sequence) {
9445
+ return;
9446
+ }
9447
+ this.callbackID = void 0;
9448
+ if (!this.interpolationActive && !document.hidden) {
9449
+ this.recordVideoFrames(metadata.presentedFrames);
9450
+ } else {
9451
+ this.previousPresentedFrames = void 0;
9452
+ }
9453
+ observe();
9454
+ });
9455
+ };
9456
+ observe();
9457
+ this.timer = setInterval(() => this.sample(), 1e3);
9458
+ }
9459
+ /**
9460
+ * 切换输出方式并清空采样窗口,避免原视频和画布重复计数。
9461
+ */
9462
+ setInterpolationActive(active) {
9463
+ this.interpolationActive = active;
9464
+ this.reset();
9465
+ }
9466
+ /**
9467
+ * 记录实际提交到画布的原帧或插值帧,不统计未呈现的计算结果。
9468
+ */
9469
+ recordCanvasFrame(interpolated) {
9470
+ if (interpolated) {
9471
+ this.interpolatedFrames++;
9472
+ } else {
9473
+ this.originalFrames++;
9474
+ }
9475
+ }
9476
+ /**
9477
+ * 按浏览器累计呈现帧数的增量统计,包含两次回调之间已呈现但未回调的帧。
9478
+ */
9479
+ recordVideoFrames(presentedFrames) {
9480
+ if (!Number.isSafeInteger(presentedFrames) || presentedFrames < 0) {
9481
+ this.previousPresentedFrames = void 0;
9482
+ return;
9483
+ }
9484
+ const previous = this.previousPresentedFrames;
9485
+ this.previousPresentedFrames = presentedFrames;
9486
+ if (previous === void 0 || presentedFrames < previous) {
9487
+ this.originalFrames += presentedFrames > 0 ? 1 : 0;
9488
+ return;
9489
+ }
9490
+ this.originalFrames += presentedFrames - previous;
9491
+ }
9492
+ /**
9493
+ * 取消采样及帧观察,忽略已排队的旧回调并清空对外统计。
9494
+ */
9495
+ stop() {
9496
+ this.sequence++;
9497
+ clearInterval(this.timer);
9498
+ this.timer = void 0;
9499
+ if (this.callbackID !== void 0) {
9500
+ this.video.cancelVideoFrameCallback(this.callbackID);
9501
+ }
9502
+ this.callbackID = void 0;
9503
+ this.reset();
9504
+ }
9505
+ /**
9506
+ * 清空采样计数,避免换流或切换输出方式时混入旧数据。
9507
+ */
9508
+ reset() {
9509
+ this.startedAt = performance.now();
9510
+ this.originalFrames = 0;
9511
+ this.interpolatedFrames = 0;
9512
+ this.previousPresentedFrames = void 0;
9513
+ this.notify(void 0);
9514
+ }
9515
+ /**
9516
+ * 按实际经过的时间计算帧率;没有新呈现时输出 0,后台页面不报告帧率。
9517
+ */
9518
+ sample() {
9519
+ const now = performance.now();
9520
+ const seconds = (now - this.startedAt) / 1e3;
9521
+ if (document.hidden || seconds <= 0) {
9522
+ this.reset();
9523
+ return;
9524
+ }
9525
+ const statistics = Object.freeze({
9526
+ source: this.interpolationActive ? "canvas" : "video",
9527
+ frameRate: (this.originalFrames + this.interpolatedFrames) / seconds,
9528
+ originalFrameRate: this.originalFrames / seconds,
9529
+ interpolatedFrameRate: this.interpolatedFrames / seconds
9530
+ });
9531
+ this.startedAt = now;
9532
+ this.originalFrames = 0;
9533
+ this.interpolatedFrames = 0;
9534
+ this.notify(statistics);
9535
+ }
9536
+ /**
9537
+ * 隔离接入方回调异常,避免影响播放和后续采样。
9538
+ */
9539
+ notify(statistics) {
9540
+ try {
9541
+ this.handler?.(statistics);
9542
+ } catch {
9543
+ }
9544
+ }
9545
+ };
9546
+
8887
9547
  // ../../node_modules/.pnpm/framegen@1.4.0/node_modules/framegen/rt.js
8888
9548
  var WG = 8;
8889
9549
  var texBgSeq = 0;
@@ -11047,6 +11707,10 @@ var RemoteVideoFramePipeline = class {
11047
11707
  __publicField(this, "previousPresented");
11048
11708
  __publicField(this, "slowPairs", 0);
11049
11709
  __publicField(this, "active", false);
11710
+ /**
11711
+ * 帧时间基准:媒体时间异常后,本管线持续使用回调时钟,避免反复切换。
11712
+ */
11713
+ __publicField(this, "timeSource", "mediaTime");
11050
11714
  /** vsync 显示节拍:弹出到点的帧并显示最新的一张。 */
11051
11715
  __publicField(this, "tick", (now) => {
11052
11716
  if (this.stopped) return;
@@ -11093,8 +11757,8 @@ var RemoteVideoFramePipeline = class {
11093
11757
  if (!this.loading) void this.initialize();
11094
11758
  return;
11095
11759
  }
11096
- const time = metadata.mediaTime * 1e3;
11097
- if (!Number.isFinite(time) || this.previousTime !== void 0 && time <= this.previousTime) return;
11760
+ const time = this.resolveFrameTime(now, metadata);
11761
+ if (time === void 0) return;
11098
11762
  const previous = this.previousTime;
11099
11763
  const interval = previous === void 0 ? 0 : time - previous;
11100
11764
  const consecutive = this.previousPresented === void 0 || metadata.presentedFrames === this.previousPresented + 1;
@@ -11119,6 +11783,22 @@ var RemoteVideoFramePipeline = class {
11119
11783
  }
11120
11784
  this.interpolate(previousSlot, slot, previous, time, interval);
11121
11785
  }
11786
+ /**
11787
+ * 优先使用媒体时间;新帧的媒体时间异常时切换到单调回调时钟。
11788
+ */
11789
+ resolveFrameTime(now, metadata) {
11790
+ if (this.previousPresented !== void 0 && metadata.presentedFrames <= this.previousPresented) {
11791
+ return void 0;
11792
+ }
11793
+ let time = this.timeSource === "mediaTime" ? metadata.mediaTime * 1e3 : now;
11794
+ if (!Number.isFinite(time) || this.previousTime !== void 0 && time <= this.previousTime) {
11795
+ if (this.timeSource === "callback" || !Number.isFinite(now)) return void 0;
11796
+ this.timeSource = "callback";
11797
+ this.resetTimeline(true);
11798
+ time = now;
11799
+ }
11800
+ return time;
11801
+ }
11122
11802
  /** 帧按显示时间升序入队;同一时间的内容后到先出。 */
11123
11803
  enqueue(frame) {
11124
11804
  const index = this.queue.findIndex((queued) => queued.time >= frame.time);
@@ -11145,8 +11825,10 @@ var RemoteVideoFramePipeline = class {
11145
11825
  this.fail(new Error("Interpolation exceeded the GPU frame budget for 5 consecutive pairs"));
11146
11826
  return;
11147
11827
  }
11148
- if (this.position(performance.now()) >= midpoint) return;
11149
- this.enqueue({ slot, time: midpoint });
11828
+ if (this.position(performance.now()) >= midpoint) {
11829
+ return;
11830
+ }
11831
+ this.enqueue({ slot, time: midpoint, interpolated: true });
11150
11832
  }).catch((error) => {
11151
11833
  if (!this.stopped) this.fail(error);
11152
11834
  });
@@ -11165,6 +11847,7 @@ var RemoteVideoFramePipeline = class {
11165
11847
  this.displayedTime = frame.time;
11166
11848
  this.canvas.style.visibility = "visible";
11167
11849
  this.setActive(true);
11850
+ this.options.onFramePresented?.(frame.interpolated ?? false);
11168
11851
  }
11169
11852
  async initialize() {
11170
11853
  this.loading = true;
@@ -11194,11 +11877,13 @@ var RemoteVideoFramePipeline = class {
11194
11877
  this.active = active;
11195
11878
  this.options.onActiveChange?.(active);
11196
11879
  }
11197
- resetTimeline() {
11880
+ resetTimeline(preserveGpuTimeout = false) {
11198
11881
  this.generation++;
11199
11882
  this.queue = [];
11200
- clearTimeout(this.stallTimer);
11201
- this.stallTimer = void 0;
11883
+ if (!preserveGpuTimeout) {
11884
+ clearTimeout(this.stallTimer);
11885
+ this.stallTimer = void 0;
11886
+ }
11202
11887
  if (this.raf !== void 0) cancelAnimationFrame(this.raf);
11203
11888
  this.raf = void 0;
11204
11889
  this.anchor = void 0;
@@ -11277,6 +11962,10 @@ var XmaxVideoView = class {
11277
11962
  __publicField(this, "interpolationOptions");
11278
11963
  __publicField(this, "interpolationPipeline");
11279
11964
  __publicField(this, "interpolationCanvas");
11965
+ /**
11966
+ * 呈现统计组件
11967
+ */
11968
+ __publicField(this, "statisticsController");
11280
11969
  /**
11281
11970
  * DOM 挂载状态
11282
11971
  */
@@ -11287,6 +11976,7 @@ var XmaxVideoView = class {
11287
11976
  this.element.style.overflow = "hidden";
11288
11977
  this.element.style.backgroundColor = "black";
11289
11978
  this.videoElement = document.createElement("video");
11979
+ this.statisticsController = new RenderStatisticsController(this.videoElement);
11290
11980
  this.videoElement.autoplay = true;
11291
11981
  this.videoElement.playsInline = true;
11292
11982
  this.videoElement.muted = true;
@@ -11303,6 +11993,20 @@ var XmaxVideoView = class {
11303
11993
  get track() {
11304
11994
  return this.currentTrack;
11305
11995
  }
11996
+ /**
11997
+ * 每秒接收当前视图的呈现统计;换流、卸载或不可统计时返回 undefined。
11998
+ */
11999
+ get renderStatisticsHandler() {
12000
+ return this.statisticsController.handler;
12001
+ }
12002
+ /**
12003
+ * 更新统计回调,不影响播放;移除回调时停止采集。
12004
+ */
12005
+ set renderStatisticsHandler(handler) {
12006
+ this.statisticsController.handler = handler;
12007
+ this.statisticsController.stop();
12008
+ this.startRenderStatistics();
12009
+ }
11306
12010
  /**
11307
12011
  * 解绑旧轨道后接入新轨道,并为新内容重新启用一次性首帧通知。
11308
12012
  */
@@ -11362,6 +12066,7 @@ var XmaxVideoView = class {
11362
12066
  });
11363
12067
  }
11364
12068
  this.startInterpolation();
12069
+ this.startRenderStatistics();
11365
12070
  if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && !this.hasNotifiedFrameDisplay) {
11366
12071
  this.armFrameDisplayNotification();
11367
12072
  }
@@ -11371,6 +12076,7 @@ var XmaxVideoView = class {
11371
12076
  */
11372
12077
  detach() {
11373
12078
  this.detached = true;
12079
+ this.statisticsController.stop();
11374
12080
  this.stopInterpolation();
11375
12081
  this.cancelFrameDisplayNotification();
11376
12082
  if (this.videoElement.getAttribute("src")) {
@@ -11382,6 +12088,7 @@ var XmaxVideoView = class {
11382
12088
  * 设置网络视频预览;静音播放一次,不捕获或发布媒体轨道。 @internal
11383
12089
  */
11384
12090
  setVideoURL(url) {
12091
+ this.statisticsController.stop();
11385
12092
  this.stopInterpolation();
11386
12093
  this.cancelFrameDisplayNotification();
11387
12094
  this.hasNotifiedFrameDisplay = false;
@@ -11395,6 +12102,7 @@ var XmaxVideoView = class {
11395
12102
  this.armFrameDisplayNotification();
11396
12103
  }
11397
12104
  this.videoElement.load();
12105
+ this.startRenderStatistics();
11398
12106
  }
11399
12107
  /**
11400
12108
  * 直接设置渲染用的媒体流(绑定管线和直接渲染共用)。 @internal
@@ -11403,6 +12111,7 @@ var XmaxVideoView = class {
11403
12111
  if (this.videoElement.srcObject === stream) {
11404
12112
  return;
11405
12113
  }
12114
+ this.statisticsController.stop();
11406
12115
  this.stopInterpolation();
11407
12116
  this.cancelFrameDisplayNotification();
11408
12117
  this.hasNotifiedFrameDisplay = false;
@@ -11411,6 +12120,7 @@ var XmaxVideoView = class {
11411
12120
  this.videoElement.autoplay = true;
11412
12121
  this.armFrameDisplayNotification();
11413
12122
  this.startInterpolation();
12123
+ this.startRenderStatistics();
11414
12124
  }
11415
12125
  }
11416
12126
  /**
@@ -11440,7 +12150,18 @@ var XmaxVideoView = class {
11440
12150
  this.interpolationCanvas = canvas;
11441
12151
  this.applyContentMode();
11442
12152
  this.element.appendChild(canvas);
11443
- const pipeline = new RemoteVideoFramePipeline(this.videoElement, canvas, this.interpolationOptions);
12153
+ const options = this.interpolationOptions;
12154
+ const pipeline = new RemoteVideoFramePipeline(this.videoElement, canvas, {
12155
+ ...options,
12156
+ onActiveChange: (active) => {
12157
+ this.statisticsController.setInterpolationActive(active);
12158
+ options.onActiveChange?.(active);
12159
+ },
12160
+ onFramePresented: (interpolated) => {
12161
+ this.statisticsController.recordCanvasFrame(interpolated);
12162
+ options.onFramePresented?.(interpolated);
12163
+ }
12164
+ });
11444
12165
  this.interpolationPipeline = pipeline;
11445
12166
  pipeline.start();
11446
12167
  }
@@ -11475,6 +12196,7 @@ var XmaxVideoView = class {
11475
12196
  * 解绑当前轨道并清空渲染内容。
11476
12197
  */
11477
12198
  detachCurrentTrack() {
12199
+ this.statisticsController.stop();
11478
12200
  const track = this.currentTrack;
11479
12201
  if (track) {
11480
12202
  VideoRenderRegistry.binding(track)?.detachHandler(this);
@@ -11540,6 +12262,12 @@ var XmaxVideoView = class {
11540
12262
  this.interpolationCanvas.style.transform = this.mirrored ? "scaleX(-1)" : "";
11541
12263
  }
11542
12264
  }
12265
+ /**
12266
+ * 视图未卸载时启动呈现统计,采样及媒体就绪检查由统计组件负责。
12267
+ */
12268
+ startRenderStatistics() {
12269
+ if (!this.detached) this.statisticsController.start();
12270
+ }
11543
12271
  };
11544
12272
 
11545
12273
  // src/Render/Video/XmaxRealtimeVideoView.ts
@@ -11739,4 +12467,4 @@ export {
11739
12467
  XmaxVideoView,
11740
12468
  XmaxRealtimeVideoView
11741
12469
  };
11742
- //# sourceMappingURL=chunk-VMJXIVUF.js.map
12470
+ //# sourceMappingURL=chunk-PLFWPS5A.js.map