@xmaxai/web-sdk 1.0.5 → 1.0.7

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.7";
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,231 @@ 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
+
1368
+ // src/Foundation/Media/Camera/CameraRotation.ts
1369
+ function validateCameraRotation(rotation) {
1370
+ if (![0, 90, 180, 270].includes(rotation)) {
1371
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera rotation must be 0, 90, 180 or 270");
1372
+ }
1373
+ }
1374
+
1375
+ // src/Foundation/Media/Camera/CameraRotationProcessor.ts
1376
+ var CameraRotationProcessor = class {
1377
+ constructor(options) {
1378
+ __publicField(this, "options", options);
1379
+ __publicField(this, "video", document.createElement("video"));
1380
+ __publicField(this, "canvas", document.createElement("canvas"));
1381
+ __publicField(this, "context");
1382
+ __publicField(this, "stream");
1383
+ __publicField(this, "source");
1384
+ __publicField(this, "frameCallback");
1385
+ __publicField(this, "timer");
1386
+ __publicField(this, "cancelPending");
1387
+ __publicField(this, "stopped", false);
1388
+ __publicField(this, "version", 0);
1389
+ __publicField(this, "sourceEnded", () => {
1390
+ this.fail(new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera source ended during rotation"));
1391
+ });
1392
+ this.canvas.width = options.width;
1393
+ this.canvas.height = options.height;
1394
+ const context = this.canvas.getContext("2d", { alpha: false });
1395
+ if (!context || typeof this.canvas.captureStream !== "function") {
1396
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas camera rotation is not supported by this browser");
1397
+ }
1398
+ this.context = context;
1399
+ this.video.muted = true;
1400
+ this.video.autoplay = true;
1401
+ this.video.playsInline = true;
1402
+ }
1403
+ /** 等待第一帧实际绘制成功;换摄像头时复用输出轨,不重建 sender。 */
1404
+ async setSource(source) {
1405
+ if (this.stopped) throw new XmaxError("CANCELLED" /* cancelled */, "Camera rotation has stopped");
1406
+ if (source.readyState === "ended") {
1407
+ this.stop();
1408
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera rotation requires a live source");
1409
+ }
1410
+ this.cancelPending?.();
1411
+ this.cancelFrame();
1412
+ this.source?.removeEventListener("ended", this.sourceEnded);
1413
+ const version = ++this.version;
1414
+ this.source = source;
1415
+ source.addEventListener("ended", this.sourceEnded);
1416
+ this.video.srcObject = new MediaStream([source]);
1417
+ try {
1418
+ await new Promise((resolve, reject) => {
1419
+ let settled = false;
1420
+ const finish = (error) => {
1421
+ if (settled) return;
1422
+ settled = true;
1423
+ clearTimeout(timeout);
1424
+ this.cancelPending = void 0;
1425
+ if (error) reject(error);
1426
+ else resolve();
1427
+ };
1428
+ const timeout = setTimeout(() => finish(new XmaxError("TIMEOUT" /* timeout */, "Timed out waiting for a camera rotation frame")), 1e4);
1429
+ this.cancelPending = () => finish(new XmaxError("CANCELLED" /* cancelled */, "Camera rotation source changed"));
1430
+ void this.video.play().then(() => {
1431
+ if (settled || this.stopped || version !== this.version) return;
1432
+ const firstFrame = () => {
1433
+ if (settled || this.stopped || version !== this.version) return;
1434
+ try {
1435
+ if (!this.draw()) {
1436
+ this.schedule(firstFrame);
1437
+ return;
1438
+ }
1439
+ this.stream ?? (this.stream = this.canvas.captureStream(this.options.frameRate));
1440
+ if (!this.stream.getVideoTracks()[0]) throw new Error("Canvas did not produce a video track");
1441
+ this.requestFrame();
1442
+ finish();
1443
+ this.schedule(() => this.render(version));
1444
+ } catch (error) {
1445
+ finish(error);
1446
+ }
1447
+ };
1448
+ this.schedule(firstFrame);
1449
+ }, finish);
1450
+ });
1451
+ if (this.stopped || version !== this.version) {
1452
+ throw new XmaxError("CANCELLED" /* cancelled */, "Camera rotation source changed");
1453
+ }
1454
+ return this.stream.getVideoTracks()[0];
1455
+ } catch (error) {
1456
+ if (version === this.version) this.stop();
1457
+ throw XmaxError.from(error);
1458
+ }
1459
+ }
1460
+ /** 换设备前解除旧原始轨监听,保留 Canvas 最后一帧及输出轨。 */
1461
+ pauseSource() {
1462
+ ++this.version;
1463
+ this.cancelPending?.();
1464
+ this.cancelFrame();
1465
+ this.source?.removeEventListener("ended", this.sourceEnded);
1466
+ this.source = void 0;
1467
+ this.video.pause();
1468
+ this.video.srcObject = null;
1469
+ }
1470
+ stop() {
1471
+ if (this.stopped) return;
1472
+ this.stopped = true;
1473
+ ++this.version;
1474
+ this.cancelPending?.();
1475
+ this.cancelFrame();
1476
+ this.source?.removeEventListener("ended", this.sourceEnded);
1477
+ this.source = void 0;
1478
+ this.video.pause();
1479
+ this.video.srcObject = null;
1480
+ this.stream?.getTracks().forEach((track) => track.stop());
1481
+ this.stream = void 0;
1482
+ this.canvas.width = this.canvas.height = 0;
1483
+ }
1484
+ draw() {
1485
+ const { videoWidth: width, videoHeight: height } = this.video;
1486
+ if (this.video.readyState < 2 || !width || !height) return false;
1487
+ const { width: outWidth, height: outHeight, rotation } = this.options;
1488
+ const quarterTurn = rotation === 90 || rotation === 270;
1489
+ const targetWidth = quarterTurn ? outHeight : outWidth;
1490
+ const targetHeight = quarterTurn ? outWidth : outHeight;
1491
+ const scale = Math.max(targetWidth / width, targetHeight / height);
1492
+ const cropWidth = targetWidth / scale, cropHeight = targetHeight / scale;
1493
+ this.context.setTransform(1, 0, 0, 1, outWidth / 2, outHeight / 2);
1494
+ this.context.rotate(rotation * Math.PI / 180);
1495
+ this.context.drawImage(
1496
+ this.video,
1497
+ (width - cropWidth) / 2,
1498
+ (height - cropHeight) / 2,
1499
+ cropWidth,
1500
+ cropHeight,
1501
+ -targetWidth / 2,
1502
+ -targetHeight / 2,
1503
+ targetWidth,
1504
+ targetHeight
1505
+ );
1506
+ return true;
1507
+ }
1508
+ render(version) {
1509
+ if (this.stopped || version !== this.version) return;
1510
+ try {
1511
+ if (this.draw()) this.requestFrame();
1512
+ this.schedule(() => this.render(version));
1513
+ } catch (error) {
1514
+ this.fail(XmaxError.from(error));
1515
+ }
1516
+ }
1517
+ requestFrame() {
1518
+ const track = this.stream?.getVideoTracks()[0];
1519
+ track?.requestFrame?.();
1520
+ }
1521
+ schedule(callback) {
1522
+ if (typeof this.video.requestVideoFrameCallback === "function") {
1523
+ this.frameCallback = this.video.requestVideoFrameCallback(callback);
1524
+ } else {
1525
+ this.timer = setTimeout(callback, 1e3 / this.options.frameRate);
1526
+ }
1527
+ }
1528
+ cancelFrame() {
1529
+ if (this.frameCallback !== void 0) this.video.cancelVideoFrameCallback(this.frameCallback);
1530
+ clearTimeout(this.timer);
1531
+ this.frameCallback = void 0;
1532
+ this.timer = void 0;
1533
+ }
1534
+ fail(error) {
1535
+ this.stop();
1536
+ this.options.onError(error);
1537
+ }
1538
+ };
1539
+
1319
1540
  // src/Foundation/RTC/Agora/AgoraRtcManager.ts
1320
1541
  var _AgoraRtcManager = class _AgoraRtcManager {
1321
1542
  /**
@@ -1332,9 +1553,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1332
1553
  __publicField(this, "initialization");
1333
1554
  __publicField(this, "lifecycle", 0);
1334
1555
  __publicField(this, "camera");
1556
+ __publicField(this, "cameraSource");
1557
+ __publicField(this, "cameraRotation");
1558
+ __publicField(this, "captureVersion", 0);
1335
1559
  __publicField(this, "microphone");
1336
1560
  __publicField(this, "externalVideo");
1337
1561
  __publicField(this, "externalAudio");
1562
+ __publicField(this, "appliedOptimizationModes", /* @__PURE__ */ new WeakMap());
1563
+ __publicField(this, "cameraSenderConfiguration");
1564
+ __publicField(this, "senderUpdateTail", Promise.resolve());
1565
+ __publicField(this, "cameraTransceiverListener");
1338
1566
  __publicField(this, "connection");
1339
1567
  __publicField(this, "roomVersion", 0);
1340
1568
  /**
@@ -1351,6 +1579,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1351
1579
  */
1352
1580
  __publicField(this, "listener");
1353
1581
  __publicField(this, "statisticsTimer");
1582
+ __publicField(this, "networkQualitySuspended", false);
1354
1583
  __publicField(this, "audioVolumes", /* @__PURE__ */ new Map());
1355
1584
  __publicField(this, "requestedAudio", /* @__PURE__ */ new Set());
1356
1585
  /**
@@ -1420,7 +1649,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1420
1649
  this.clearRoom();
1421
1650
  this.client = void 0;
1422
1651
  client?.removeAllListeners();
1423
- this.camera?.close();
1652
+ this.releaseCameraCapture();
1424
1653
  this.microphone?.close();
1425
1654
  this.externalVideo?.close();
1426
1655
  this.externalAudio?.close();
@@ -1444,22 +1673,55 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1444
1673
  */
1445
1674
  async startCameraCapture(options) {
1446
1675
  const client = this.requireClient();
1447
- if (this.camera || this.externalVideo) {
1676
+ if (this.camera || this.cameraSource || this.externalVideo) {
1448
1677
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "A local video source is already active");
1449
1678
  }
1679
+ const rotation = options.rotation ?? 0;
1680
+ validateCameraRotation(rotation);
1681
+ const version = ++this.captureVersion;
1682
+ let source;
1450
1683
  try {
1451
- const captureSize = rtcOrientedVideoSize(options);
1452
- const camera = await this.sdk.createCameraVideoTrack({
1684
+ const captureSize = rtcOrientedVideoSize(rotation === 90 || rotation === 270 ? { width: options.height, height: options.width } : options);
1685
+ source = await this.sdk.createCameraVideoTrack({
1453
1686
  facingMode: options.position === "front" /* front */ ? "user" : "environment",
1454
1687
  encoderConfig: { width: captureSize.width, height: captureSize.height, frameRate: options.frameRate }
1455
1688
  });
1456
- if (this.client !== client) {
1457
- camera.close();
1689
+ if (this.client !== client || version !== this.captureVersion) {
1690
+ source.close();
1458
1691
  throw this.cancelled();
1459
1692
  }
1693
+ this.cameraSource = source;
1694
+ let camera = source;
1695
+ if (rotation !== 0) {
1696
+ const processor = new CameraRotationProcessor({
1697
+ ...options,
1698
+ rotation,
1699
+ onError: (error) => this.listener?.onError?.(error)
1700
+ });
1701
+ this.cameraRotation = processor;
1702
+ const output = await processor.setSource(source.getMediaStreamTrack());
1703
+ if (this.client !== client || version !== this.captureVersion) throw this.cancelled();
1704
+ camera = this.sdk.createCustomVideoTrack({
1705
+ mediaStreamTrack: output,
1706
+ width: options.width,
1707
+ height: options.height,
1708
+ frameRate: options.frameRate
1709
+ });
1710
+ }
1460
1711
  this.camera = camera;
1712
+ this.cameraTransceiverListener = () => {
1713
+ if (this.client === client && client.localTracks.includes(camera) && this.videoPublishRequested && !this.rejoinTask) {
1714
+ void this.restoreCameraSender(camera).catch(() => {
1715
+ if (this.camera === camera && this.videoPublishRequested) {
1716
+ XmaxLogger.rtc.warning(() => "Agora \u53D1\u9001\u53C2\u6570\u6062\u590D\u5931\u8D25 (Agora Sender Parameters Restore Failed)");
1717
+ }
1718
+ });
1719
+ }
1720
+ };
1721
+ camera.on("transceiver-updated", this.cameraTransceiverListener);
1461
1722
  return camera.getMediaStreamTrack();
1462
1723
  } catch (error) {
1724
+ if (version === this.captureVersion) this.releaseCameraCapture();
1463
1725
  throw this.mapError(error, "CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */);
1464
1726
  }
1465
1727
  }
@@ -1467,25 +1729,50 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1467
1729
  * 切换声网相机设备并返回切换后的原生轨道。
1468
1730
  */
1469
1731
  async switchCameraCapture(to) {
1470
- const camera = this.requireCamera();
1471
- await this.run(() => camera.setDevice({ facingMode: to === "front" /* front */ ? "user" : "environment" }));
1472
- if (this.camera !== camera) throw this.cancelled();
1732
+ const source = this.requireCamera();
1733
+ const camera = this.camera;
1734
+ this.cameraRotation?.pauseSource();
1735
+ try {
1736
+ await this.run(() => source.setDevice({ facingMode: to === "front" /* front */ ? "user" : "environment" }));
1737
+ } catch (error) {
1738
+ if (this.camera === camera && this.cameraRotation) {
1739
+ await this.cameraRotation.setSource(source.getMediaStreamTrack()).catch((restoreError) => {
1740
+ this.listener?.onError?.(XmaxError.from(restoreError));
1741
+ });
1742
+ }
1743
+ throw error;
1744
+ }
1745
+ if (this.camera !== camera || this.cameraSource !== source) throw this.cancelled();
1746
+ if (this.cameraRotation) {
1747
+ await this.cameraRotation.setSource(source.getMediaStreamTrack());
1748
+ if (this.camera !== camera) throw this.cancelled();
1749
+ }
1750
+ if (this.videoPublishRequested) await this.restoreCameraSender(camera);
1473
1751
  return camera.getMediaStreamTrack();
1474
1752
  }
1475
1753
  /**
1476
1754
  * 停止采集并释放摄像头。
1477
1755
  */
1478
1756
  async stopCameraCapture() {
1757
+ this.releaseCameraCapture();
1758
+ }
1759
+ releaseCameraCapture() {
1760
+ ++this.captureVersion;
1479
1761
  const camera = this.camera;
1762
+ this.releaseCameraSender();
1480
1763
  this.camera = void 0;
1481
1764
  camera?.close();
1765
+ this.cameraRotation?.stop();
1766
+ this.cameraRotation = void 0;
1767
+ if (this.cameraSource !== camera) this.cameraSource?.close();
1768
+ this.cameraSource = void 0;
1482
1769
  }
1483
1770
  /**
1484
1771
  * 将外部原生轨包装为 Agora 本地轨,不申请设备权限;失败时释放本次克隆。
1485
1772
  */
1486
1773
  async setExternalMediaTracks(tracks) {
1487
1774
  this.requireClient();
1488
- if (this.camera || this.microphone || this.externalVideo) {
1775
+ if (this.camera || this.cameraSource || this.microphone || this.externalVideo) {
1489
1776
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
1490
1777
  }
1491
1778
  if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
@@ -1512,11 +1799,26 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1512
1799
  }
1513
1800
  }
1514
1801
  /**
1515
- * 应用公共编码配置;仅相机源补偿移动端转置,Canvas 外部源使用原始方向。
1802
+ * 摄像头实验走原生 sender(发布前暂存);外部视频仍走 Agora 编码配置。
1516
1803
  */
1517
1804
  async configureVideoEncoding(config) {
1518
1805
  const camera = this.requireLocalVideo();
1519
- const encodingSize = this.externalVideo ? config : rtcOrientedVideoSize(config);
1806
+ if (camera === this.camera) {
1807
+ const client = this.requireClient(), version = this.roomVersion;
1808
+ const published = this.videoPublishRequested;
1809
+ await this.run(() => this.queueSenderUpdate(async () => {
1810
+ const ensureActive2 = () => {
1811
+ if (this.camera !== camera || this.client !== client || this.roomVersion !== version || published && !this.videoPublishRequested) throw this.cancelled();
1812
+ };
1813
+ ensureActive2();
1814
+ if (this.videoPublishRequested) {
1815
+ await AgoraVideoSender.apply(camera, this.cameraEncodingConfiguration(config), ensureActive2);
1816
+ }
1817
+ this.cameraSenderConfiguration = { ...config };
1818
+ }));
1819
+ return;
1820
+ }
1821
+ const encodingSize = config;
1520
1822
  await this.run(async () => {
1521
1823
  await camera.setEncoderConfiguration({
1522
1824
  width: encodingSize.width,
@@ -1525,8 +1827,39 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1525
1827
  bitrateMin: config.minimumBitrate,
1526
1828
  bitrateMax: config.maximumBitrate
1527
1829
  });
1528
- await camera.setOptimizationMode(config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail");
1830
+ if (this.appliedOptimizationModes.get(camera) !== config.encoderPreference) {
1831
+ await camera.setOptimizationMode(config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail");
1832
+ this.appliedOptimizationModes.set(camera, config.encoderPreference);
1833
+ }
1834
+ });
1835
+ }
1836
+ queueSenderUpdate(operation) {
1837
+ const next = this.senderUpdateTail.then(operation);
1838
+ this.senderUpdateTail = next.catch(() => {
1529
1839
  });
1840
+ return next;
1841
+ }
1842
+ /** 发布/重建 sender 后恢复最近成功的目标配置;排队时不捕获过期的参数对象。 */
1843
+ restoreCameraSender(camera) {
1844
+ const client = this.client, version = this.roomVersion;
1845
+ return this.queueSenderUpdate(async () => {
1846
+ const ensureActive2 = () => {
1847
+ if (this.camera !== camera || this.client !== client || this.roomVersion !== version || !this.videoPublishRequested) {
1848
+ throw this.cancelled();
1849
+ }
1850
+ };
1851
+ ensureActive2();
1852
+ const config = this.cameraSenderConfiguration;
1853
+ if (config) await AgoraVideoSender.apply(camera, this.cameraEncodingConfiguration(config), ensureActive2);
1854
+ });
1855
+ }
1856
+ cameraEncodingConfiguration(config) {
1857
+ return this.cameraRotation ? config : { ...config, ...rtcOrientedVideoSize(config) };
1858
+ }
1859
+ releaseCameraSender() {
1860
+ if (this.cameraTransceiverListener) this.camera?.off("transceiver-updated", this.cameraTransceiverListener);
1861
+ this.cameraTransceiverListener = void 0;
1862
+ this.cameraSenderConfiguration = void 0;
1530
1863
  }
1531
1864
  /**
1532
1865
  * 使用会话下发的字符串频道与 UID 入房;不采信后端区域覆盖。
@@ -1645,6 +1978,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1645
1978
  if (this.videoPublishRequested && video) {
1646
1979
  await this.run(() => client.publish(video));
1647
1980
  this.ensureRoomCurrent(client, version);
1981
+ if (video === this.camera) await this.restoreCameraSender(this.camera);
1648
1982
  }
1649
1983
  if (this.audioPublishRequested && audio) {
1650
1984
  await this.run(() => client.publish(audio));
@@ -1674,6 +2008,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1674
2008
  if (client !== this.client || !this.videoPublishRequested) throw this.cancelled();
1675
2009
  if (client.localTracks.includes(camera)) return;
1676
2010
  await this.run(() => client.publish(camera));
2011
+ if (camera === this.camera) await this.run(() => this.restoreCameraSender(this.camera));
1677
2012
  }
1678
2013
  /**
1679
2014
  * 停止视频发布但保留采集与预览。
@@ -1871,6 +2206,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1871
2206
  const handleExpired = () => {
1872
2207
  if (!active() || this.tokenExpired) return;
1873
2208
  this.tokenExpired = true;
2209
+ this.listener?.onNetworkStatistics?.(void 0);
1874
2210
  this.listener?.onTokenExpired?.();
1875
2211
  };
1876
2212
  client.on("user-published", (user, kind) => {
@@ -1894,13 +2230,15 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1894
2230
  if (mediaActive()) this.listener?.onCustomMessageReceived(String(uid), typeof data === "string" ? data : new TextDecoder().decode(data));
1895
2231
  });
1896
2232
  client.on("network-quality", (quality) => {
1897
- if (mediaActive()) AgoraRtcStatistics.network(quality, this.listener);
2233
+ if (mediaActive() && !this.networkQualitySuspended) AgoraRtcStatistics.network(quality, this.listener);
1898
2234
  });
1899
2235
  client.on("token-privilege-will-expire", () => {
1900
2236
  if (active() && !this.tokenExpired) this.listener?.onTokenWillExpire?.();
1901
2237
  });
1902
2238
  client.on("token-privilege-did-expire", handleExpired);
1903
2239
  client.on("connection-state-change", (state2, previous, reason) => {
2240
+ if (this.client === client) this.networkQualitySuspended = state2 !== "CONNECTED";
2241
+ if (active() && state2 !== "CONNECTED") this.listener?.onNetworkStatistics?.(void 0);
1904
2242
  if (state2 === "DISCONNECTED" && reason === "TOKEN_EXPIRE") {
1905
2243
  handleExpired();
1906
2244
  return;
@@ -1918,6 +2256,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1918
2256
  this.cancelRejoin?.();
1919
2257
  this.connection = void 0;
1920
2258
  this.tokenExpired = false;
2259
+ this.networkQualitySuspended = false;
1921
2260
  this.videoPublishRequested = false;
1922
2261
  this.audioPublishRequested = false;
1923
2262
  clearInterval(this.statisticsTimer);
@@ -1953,8 +2292,8 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1953
2292
  * 获取已启动的相机采集。
1954
2293
  */
1955
2294
  requireCamera() {
1956
- if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
1957
- return this.camera;
2295
+ if (!this.cameraSource || !this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
2296
+ return this.cameraSource;
1958
2297
  }
1959
2298
  /**
1960
2299
  * 获取当前可编码、发布的视频轨,不要求来源为摄像头。
@@ -2393,6 +2732,9 @@ var TrtcRtcManager = class {
2393
2732
  * @throws 引擎未初始化、已在采集、设备不可用或权限被拒绝时抛出错误。
2394
2733
  */
2395
2734
  async startCameraCapture(options) {
2735
+ if (options.rotation !== void 0 && options.rotation !== 0) {
2736
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera rotation currently requires Agora");
2737
+ }
2396
2738
  const engine = this.requireEngine();
2397
2739
  if (this.isCapturing || this.externalVideo) {
2398
2740
  throw new XmaxError(
@@ -3039,6 +3381,9 @@ var VeRtcManager = class {
3039
3381
  }
3040
3382
  }
3041
3383
  async startCameraCapture(options) {
3384
+ if (options.rotation !== void 0 && options.rotation !== 0) {
3385
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera rotation currently requires Agora");
3386
+ }
3042
3387
  const engine = this.requireEngine();
3043
3388
  if (this.camera || this.externalVideo) {
3044
3389
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video is already prepared");
@@ -3875,6 +4220,7 @@ var _CameraController = class _CameraController {
3875
4220
  *
3876
4221
  * @param options.videoFormat 期望的输出尺寸、帧率和编码配置;尺寸按模型规则调整。
3877
4222
  * @param options.position 首次启动时使用的摄像头位置。
4223
+ * @param options.rotation 输入帧顺时针旋转角度,默认 0;videoFormat 为旋转后的规格,目前仅 Agora 支持非零角度。
3878
4224
  * @param options.useMicrophone 是否申请麦克风权限并允许实时连接时启动音频采集。
3879
4225
  * @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
3880
4226
  * @returns 包含本地相机视频轨道的媒体流。
@@ -3887,6 +4233,7 @@ var _CameraController = class _CameraController {
3887
4233
  "Stop the current local camera stream before creating another one"
3888
4234
  );
3889
4235
  }
4236
+ validateCameraRotation(options.rotation ?? 0);
3890
4237
  const resolvedFormat = this.resolveVideoFormat(options.videoFormat);
3891
4238
  const track = new RealtimeVideoTrack({
3892
4239
  id: _CameraController.localVideoTrackID,
@@ -3909,7 +4256,8 @@ var _CameraController = class _CameraController {
3909
4256
  width: resolvedFormat.width,
3910
4257
  height: resolvedFormat.height,
3911
4258
  frameRate: resolvedFormat.fps,
3912
- position: options.position
4259
+ position: options.position,
4260
+ ...options.rotation !== void 0 ? { rotation: options.rotation } : {}
3913
4261
  });
3914
4262
  track.mediaStreamTrack = mediaTrack;
3915
4263
  this.previewStream = new MediaStream([mediaTrack]);
@@ -5043,6 +5391,7 @@ var RoomEvent = class _RoomEvent {
5043
5391
  event: "start",
5044
5392
  params: {
5045
5393
  ..._RoomEvent.generationParameters(options),
5394
+ mirror: options.mirror ?? false,
5046
5395
  ...options.context.referenceVideo && {
5047
5396
  ref_video_path: options.context.referenceVideo.path,
5048
5397
  sample_method: options.context.referenceVideo.sampleMethod,
@@ -5495,6 +5844,7 @@ var RoomController = class {
5495
5844
  startGeneration(options) {
5496
5845
  return this.send(
5497
5846
  RoomEvent.start({
5847
+ mirror: options.mirror ?? false,
5498
5848
  userID: this.requireUserID(),
5499
5849
  taskID: options.taskID,
5500
5850
  videoFormat: options.videoFormat,
@@ -5646,6 +5996,7 @@ var _EncodingController = class _EncodingController {
5646
5996
  * 基础层组件
5647
5997
  */
5648
5998
  __publicField(this, "rtcManager");
5999
+ __publicField(this, "lastAppliedConfiguration");
5649
6000
  this.rtcManager = options.rtcManager;
5650
6001
  }
5651
6002
  /**
@@ -5681,14 +6032,38 @@ var _EncodingController = class _EncodingController {
5681
6032
  "Minimum bitrate must not exceed maximum bitrate after applying SDK defaults"
5682
6033
  );
5683
6034
  }
5684
- await this.rtcManager.configureVideoEncoding({
6035
+ const configuration = {
5685
6036
  width: videoFormat.width,
5686
6037
  height: videoFormat.height,
5687
6038
  frameRate: videoFormat.fps,
5688
6039
  minimumBitrate: minimum,
5689
6040
  maximumBitrate: maximum,
5690
6041
  encoderPreference: ENCODER_PREFERENCE_MAP[videoFormat.encoderPreference]
6042
+ };
6043
+ await this.rtcManager.configureVideoEncoding(configuration);
6044
+ this.lastAppliedConfiguration = { ...configuration };
6045
+ }
6046
+ /** 只替换目标帧率,保留最近成功应用的尺寸、已解析码率区间和偏好。 */
6047
+ async updateFrameRate(fps) {
6048
+ const previous = this.lastAppliedConfiguration;
6049
+ if (!previous) {
6050
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Apply an encoder configuration before updating frame rate");
6051
+ }
6052
+ const format = new RealtimeVideoFormat({
6053
+ width: previous.width,
6054
+ height: previous.height,
6055
+ fps,
6056
+ minimumBitrate: previous.minimumBitrate,
6057
+ maximumBitrate: previous.maximumBitrate,
6058
+ encoderPreference: previous.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "maintainFramerate" /* maintainFramerate */ : "maintainQuality" /* maintainQuality */
5691
6059
  });
6060
+ format.validate();
6061
+ if (fps !== previous.frameRate) {
6062
+ const next = { ...previous, frameRate: fps };
6063
+ await this.rtcManager.configureVideoEncoding(next);
6064
+ this.lastAppliedConfiguration = next;
6065
+ }
6066
+ return format;
5692
6067
  }
5693
6068
  /**
5694
6069
  * 按像素面积与帧率计算码率区间(kbps):流畅优先为最低,画质优先为最高。
@@ -5926,9 +6301,13 @@ var StreamController = class {
5926
6301
  *
5927
6302
  * @throws 格式无效、码率区间无效或 RTC 配置失败时抛出错误。
5928
6303
  */
5929
- async setVideoEncoderConfig(videoFormat) {
6304
+ async updateUplinkVideoFormat(videoFormat) {
5930
6305
  await this.encodingController.configure(videoFormat);
5931
6306
  }
6307
+ /** 不根据新帧率重新推算码率区间。 */
6308
+ async updateUplinkFrameRate(fps) {
6309
+ return this.encodingController.updateFrameRate(fps);
6310
+ }
5932
6311
  /**
5933
6312
  * 加入 RTC 房间并发布本地媒体流。
5934
6313
  *
@@ -6014,6 +6393,7 @@ var StreamController = class {
6014
6393
  try {
6015
6394
  const sending = this.roomController.startGeneration({
6016
6395
  taskID,
6396
+ mirror: options.mirror ?? false,
6017
6397
  videoFormat: options.videoFormat,
6018
6398
  targetSize: options.targetSize,
6019
6399
  context: options.context
@@ -6087,7 +6467,7 @@ var StreamController = class {
6087
6467
  *
6088
6468
  * @throws 操作已取消、房间未就绪或信令发送失败时抛出错误。
6089
6469
  */
6090
- async changeTargetSize(taskID, targetSize, ensureActive2) {
6470
+ async updateDownlinkVideoResolution(taskID, targetSize, ensureActive2) {
6091
6471
  try {
6092
6472
  await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive: ensureActive2 });
6093
6473
  } catch (error) {
@@ -6509,6 +6889,195 @@ var StreamController = class {
6509
6889
  }
6510
6890
  };
6511
6891
 
6892
+ // src/Stream/Quality/QualityTiers.ts
6893
+ var INITIAL_QUALITY_LEVEL = 2;
6894
+ var QUALITY_TIERS = {
6895
+ L1: { width: 1024, height: 1920, frameRate: 30 },
6896
+ L2: { width: 768, height: 1440, frameRate: 24 },
6897
+ L3: { width: 640, height: 1200, frameRate: 24 },
6898
+ L4: { width: 512, height: 960, frameRate: 20 },
6899
+ L5: { width: 384, height: 720, frameRate: 16 }
6900
+ };
6901
+
6902
+ // src/Stream/Quality/QualityPolicy.ts
6903
+ var DEFAULT_POLICY = {
6904
+ poorSamples: 3,
6905
+ goodSamples: 5,
6906
+ stabilizationMs: 4e3,
6907
+ probeMs: 2e4,
6908
+ upgradeCooldownMs: 6e4,
6909
+ sampleTimeoutMs: 5e3
6910
+ };
6911
+
6912
+ // src/Stream/Quality/QualityController.ts
6913
+ var ORDERED_QUALITY_TIERS = Object.values(QUALITY_TIERS);
6914
+ var QualityController = class {
6915
+ constructor(apply, now = () => performance.now(), policy = {}) {
6916
+ __publicField(this, "apply", apply);
6917
+ __publicField(this, "now", now);
6918
+ __publicField(this, "policy");
6919
+ __publicField(this, "active", false);
6920
+ __publicField(this, "index", 0);
6921
+ __publicField(this, "good", 0);
6922
+ __publicField(this, "poor", 0);
6923
+ __publicField(this, "lastSampleAt");
6924
+ __publicField(this, "stableUntil", 0);
6925
+ __publicField(this, "upgradeAfter", 0);
6926
+ __publicField(this, "probeUntil", 0);
6927
+ __publicField(this, "switching", false);
6928
+ __publicField(this, "lifecycle", new AbortController());
6929
+ this.policy = { ...DEFAULT_POLICY, ...policy };
6930
+ for (const value of Object.values(this.policy)) {
6931
+ if (!Number.isFinite(value) || value < 0) throw new Error("Invalid quality policy");
6932
+ }
6933
+ for (const value of [this.policy.poorSamples, this.policy.goodSamples]) {
6934
+ if (!Number.isInteger(value) || value < 1) throw new Error("Sample counts must be positive integers");
6935
+ }
6936
+ }
6937
+ get snapshot() {
6938
+ return Object.freeze({
6939
+ active: this.active,
6940
+ level: this.index + 1,
6941
+ ...ORDERED_QUALITY_TIERS[this.index],
6942
+ switching: this.switching,
6943
+ upgradeAfter: this.upgradeAfter
6944
+ });
6945
+ }
6946
+ /** 传入调用方保存的档位,不在 start 中擅自改写编码配置。 */
6947
+ start(level = 1) {
6948
+ if (!Number.isInteger(level) || level < 1 || level > ORDERED_QUALITY_TIERS.length) {
6949
+ throw new Error("Unsupported quality level");
6950
+ }
6951
+ this.stop();
6952
+ this.lifecycle = new AbortController();
6953
+ this.index = level - 1;
6954
+ this.active = true;
6955
+ this.stableUntil = this.upgradeAfter = this.probeUntil = 0;
6956
+ }
6957
+ /** 取消旧会话的迟到结果;底层尚未完成时仍保持串行限制。 */
6958
+ stop() {
6959
+ this.active = false;
6960
+ this.lifecycle.abort();
6961
+ this.resetSamples();
6962
+ }
6963
+ /** 前台操作期间丢弃累计样本,但保留试升失败后的冷却时间。 */
6964
+ resetSamples() {
6965
+ this.resetCounts();
6966
+ this.lastSampleAt = void 0;
6967
+ }
6968
+ /** 只接收质量事件,不得用定时器重复提交同一份缓存统计。异常由调用方记录。 */
6969
+ async observe(quality, sampleAt = this.now()) {
6970
+ if (!this.active || this.switching || !Number.isFinite(sampleAt)) return;
6971
+ const now = this.now();
6972
+ if (sampleAt > now) return;
6973
+ if (this.lastSampleAt !== void 0) {
6974
+ if (sampleAt <= this.lastSampleAt) return;
6975
+ if (sampleAt - this.lastSampleAt > this.policy.sampleTimeoutMs) this.resetCounts();
6976
+ }
6977
+ this.lastSampleAt = sampleAt;
6978
+ if (now - sampleAt > this.policy.sampleTimeoutMs || sampleAt < this.stableUntil) {
6979
+ this.resetCounts();
6980
+ return;
6981
+ }
6982
+ if (quality === 4 || quality === 5) {
6983
+ this.good = 0;
6984
+ this.poor++;
6985
+ if (this.poor >= this.policy.poorSamples && this.index < ORDERED_QUALITY_TIERS.length - 1) {
6986
+ return this.change(this.index + 1, now < this.probeUntil ? "failedProbe" : "poorNetwork");
6987
+ }
6988
+ } else if (quality === 1 || quality === 2) {
6989
+ this.poor = 0;
6990
+ if (sampleAt < this.upgradeAfter) {
6991
+ this.good = 0;
6992
+ return;
6993
+ }
6994
+ this.good++;
6995
+ if (this.good >= this.policy.goodSamples && this.index > 0) {
6996
+ return this.change(this.index - 1, "probeUpgrade");
6997
+ }
6998
+ } else {
6999
+ this.resetCounts();
7000
+ }
7001
+ return;
7002
+ }
7003
+ resetCounts() {
7004
+ this.good = this.poor = 0;
7005
+ }
7006
+ async change(index, reason) {
7007
+ const lifecycle = this.lifecycle;
7008
+ const change = { from: this.index + 1, to: index + 1, ...ORDERED_QUALITY_TIERS[index], reason };
7009
+ this.switching = true;
7010
+ this.resetCounts();
7011
+ try {
7012
+ await this.apply(change, lifecycle.signal);
7013
+ if (lifecycle.signal.aborted) return;
7014
+ this.index = index;
7015
+ const now = this.now();
7016
+ this.stableUntil = now + this.policy.stabilizationMs;
7017
+ this.probeUntil = reason === "probeUpgrade" ? now + this.policy.probeMs : 0;
7018
+ if (reason === "failedProbe") this.upgradeAfter = now + this.policy.upgradeCooldownMs;
7019
+ return change;
7020
+ } catch (error) {
7021
+ if (!lifecycle.signal.aborted) {
7022
+ this.stableUntil = this.now() + this.policy.stabilizationMs;
7023
+ throw error;
7024
+ }
7025
+ return;
7026
+ } finally {
7027
+ this.switching = false;
7028
+ }
7029
+ }
7030
+ };
7031
+
7032
+ // src/Stream/Quality/QualityVideoFormat.ts
7033
+ function qualityVideoSize(baseline, level) {
7034
+ const tier = Object.values(QUALITY_TIERS)[level - 1];
7035
+ if (!Number.isInteger(level) || !tier) throw new Error("Unsupported quality level");
7036
+ baseline.validate();
7037
+ const scale = tier.width / QUALITY_TIERS.L1.width;
7038
+ const even = (value) => Math.max(2, Math.floor(value / 2) * 2);
7039
+ return { width: even(baseline.width * scale), height: even(baseline.height * scale) };
7040
+ }
7041
+ function qualityVideoFormat(baseline, level) {
7042
+ const size = qualityVideoSize(baseline, level);
7043
+ const tier = Object.values(QUALITY_TIERS)[level - 1];
7044
+ return new RealtimeVideoFormat({
7045
+ ...baseline,
7046
+ ...size,
7047
+ fps: Math.min(baseline.fps, tier.frameRate)
7048
+ });
7049
+ }
7050
+
7051
+ // src/Stream/Quality/QualityHistory.ts
7052
+ var QualityHistory = class {
7053
+ constructor(model) {
7054
+ __publicField(this, "model", model);
7055
+ }
7056
+ /** 无记录、损坏、SSR 或浏览器禁止存储时使用默认 L2。 */
7057
+ read(direction) {
7058
+ try {
7059
+ const level = Number(globalThis.localStorage.getItem(this.key(direction)));
7060
+ if (this.isValid(level)) return level;
7061
+ } catch {
7062
+ }
7063
+ return INITIAL_QUALITY_LEVEL;
7064
+ }
7065
+ /** 仅在应用成功后调用;下行成功表示信令已发送,不代表服务端已确认生效。 */
7066
+ write(direction, level) {
7067
+ if (!this.isValid(level)) return;
7068
+ try {
7069
+ globalThis.localStorage.setItem(this.key(direction), String(level));
7070
+ } catch {
7071
+ }
7072
+ }
7073
+ key(direction) {
7074
+ return `xmax:quality:v1:agora:${encodeURIComponent(this.model)}:${direction}`;
7075
+ }
7076
+ isValid(level) {
7077
+ return Number.isInteger(level) && level >= 1 && level <= Object.keys(QUALITY_TIERS).length;
7078
+ }
7079
+ };
7080
+
6512
7081
  // src/Core/Realtime/RealtimeCoordinator.ts
6513
7082
  function scopeAffects(scope, kind) {
6514
7083
  if (scope === 1 /* all */) {
@@ -6611,6 +7180,10 @@ var RealtimeCoordinator = class _RealtimeCoordinator {
6611
7180
  get currentState() {
6612
7181
  return this.state;
6613
7182
  }
7183
+ /** 后台自动调档仅在没有前台操作或清理时申请操作租约。 */
7184
+ get isBusy() {
7185
+ return this.activeOperation !== void 0 || this.termination !== void 0;
7186
+ }
6614
7187
  /**
6615
7188
  * 设置实时状态监听器;设置后立即回放当前状态。
6616
7189
  *
@@ -7087,7 +7660,7 @@ var XmaxRealtimeConnectionManager = class {
7087
7660
  }
7088
7661
  const refreshVersion = ++this.refreshVersion;
7089
7662
  this.credentialController = new AbortController();
7090
- const videoFormat = options.localTrack.videoFormat;
7663
+ const videoFormat = options.localVideoFormat ?? options.localTrack.videoFormat;
7091
7664
  if (!videoFormat) {
7092
7665
  throw new XmaxError(
7093
7666
  "INTERNAL_ERROR" /* internalError */,
@@ -7096,7 +7669,9 @@ var XmaxRealtimeConnectionManager = class {
7096
7669
  }
7097
7670
  const publishLocalMedia = options.publishLocalMedia ?? true;
7098
7671
  if (publishLocalMedia) {
7099
- await streamController.setVideoEncoderConfig(videoFormat);
7672
+ await streamController.updateUplinkVideoFormat(videoFormat);
7673
+ options.ensureCurrent();
7674
+ options.onVideoEncoderConfigured?.();
7100
7675
  }
7101
7676
  options.ensureCurrent();
7102
7677
  let completePublish;
@@ -7243,7 +7818,7 @@ var XmaxRealtimeConnectionManager = class {
7243
7818
  },
7244
7819
  attachHandler: (view) => {
7245
7820
  this.remoteTarget?.setFrameInterpolation?.(void 0);
7246
- view.isMirrored = this.dependencies.isMirrored();
7821
+ view.isMirrored = false;
7247
7822
  this.remoteTarget = view;
7248
7823
  this.dependencies.onRenderAttached();
7249
7824
  const mediaTrack = track.mediaStreamTrack;
@@ -7259,14 +7834,6 @@ var XmaxRealtimeConnectionManager = class {
7259
7834
  }
7260
7835
  });
7261
7836
  }
7262
- /**
7263
- * 同步远端结果画面的镜像状态:与当前本地摄像头位置保持一致。
7264
- */
7265
- updateRemoteMirror() {
7266
- if (this.remoteTarget) {
7267
- this.remoteTarget.isMirrored = this.dependencies.isMirrored();
7268
- }
7269
- }
7270
7837
  /**
7271
7838
  * 构造当前远端生成结果媒体流。
7272
7839
  */
@@ -7410,6 +7977,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
7410
7977
  taskID,
7411
7978
  videoFormat: options.videoFormat,
7412
7979
  context,
7980
+ ...options.targetSize && { targetSize: options.targetSize },
7981
+ mirror: options.mirror ?? false,
7413
7982
  ...options.onStartSent && { onStartSent: options.onStartSent }
7414
7983
  });
7415
7984
  await this.awaitConfirmation(confirmation, options.signal);
@@ -7436,7 +8005,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
7436
8005
  await this.streamController.updateGeneration({
7437
8006
  taskID,
7438
8007
  videoFormat: options.videoFormat,
7439
- context
8008
+ context,
8009
+ ...options.targetSize && { targetSize: options.targetSize }
7440
8010
  });
7441
8011
  if (this.activeTaskID !== taskID) throw RealtimeCoordinator.cancelledError();
7442
8012
  this.currentContext = context;
@@ -7573,6 +8143,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7573
8143
  __publicField(this, "connectionManager");
7574
8144
  __publicField(this, "generationManager");
7575
8145
  __publicField(this, "streamController");
8146
+ __publicField(this, "qualityController");
8147
+ __publicField(this, "qualityHistory");
8148
+ __publicField(this, "qualityStates", /* @__PURE__ */ new WeakMap());
8149
+ __publicField(this, "downlinkQualityController");
8150
+ /** 无服务端生效 ACK;level/size 表示最后成功发送的目标,而非实际接收规格。 */
8151
+ __publicField(this, "downlinkQualityState");
8152
+ __publicField(this, "adaptiveQualityPending", false);
8153
+ __publicField(this, "adaptiveQualityRevision", 0);
7576
8154
  /**
7577
8155
  * 音量配置
7578
8156
  */
@@ -7611,7 +8189,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7611
8189
  sessionService,
7612
8190
  streamController: this.streamController,
7613
8191
  timing: this.launchTimer,
7614
- isMirrored: () => this.cameraController.currentTrack?.position === "front" /* front */,
7615
8192
  remoteAudioVolume: () => this.storedRemoteAudioVolume,
7616
8193
  onHeartbeatFailure: (sessionID, error) => {
7617
8194
  void this.handleHeartbeatFailure(sessionID, error);
@@ -7627,6 +8204,31 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7627
8204
  errorHandler: this.errorHandler,
7628
8205
  cleanup: (scope, taskID) => this.performCleanup(scope, taskID)
7629
8206
  });
8207
+ if (options.provider === "agora" /* agora */) {
8208
+ this.qualityHistory = new QualityHistory(options.model);
8209
+ this.qualityController = new QualityController(async (change, signal) => {
8210
+ if (signal.aborted || this.currentState.connectionState !== "Generating" /* generating */) {
8211
+ throw new XmaxError("CANCELLED" /* cancelled */, "Adaptive video format update was cancelled");
8212
+ }
8213
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8214
+ const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
8215
+ const state2 = track && this.qualityStates.get(track);
8216
+ if (!track || !state2) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Adaptive video baseline is unavailable");
8217
+ const format = qualityVideoFormat(state2.baseline, change.to);
8218
+ await this.streamController.updateUplinkVideoFormat(format);
8219
+ token.ensureCurrent();
8220
+ this.generationVideoFormat ?? (this.generationVideoFormat = state2.baseline);
8221
+ track.updateVideoFormat(format);
8222
+ state2.level = change.to;
8223
+ this.qualityHistory?.write("uplink", change.to);
8224
+ });
8225
+ });
8226
+ this.downlinkQualityController = new QualityController(async (change, signal) => {
8227
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8228
+ await this.applyDownlinkQuality(change.to, token, signal);
8229
+ });
8230
+ });
8231
+ }
7630
8232
  this.streamController.setLocalVideoStatisticsListener((statistics) => {
7631
8233
  if (this.acceptsVideoStatistics) {
7632
8234
  this.localVideoStatistics = statistics ? Object.freeze({ ...statistics }) : void 0;
@@ -7636,6 +8238,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7636
8238
  this.streamController.setNetworkStatisticsListener((statistics) => {
7637
8239
  if (this.acceptsVideoStatistics) {
7638
8240
  this.networkStatistics = statistics ? Object.freeze({ ...statistics }) : void 0;
8241
+ this.handleAdaptiveQuality(statistics);
7639
8242
  this.notifyNetworkStatistics();
7640
8243
  }
7641
8244
  });
@@ -7663,7 +8266,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7663
8266
  */
7664
8267
  async setFrameInterpolationEnabled(enabled) {
7665
8268
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
7666
- const format = this.generationVideoFormat ?? this.currentLocalTrack?.videoFormat;
8269
+ const format = this.downlinkQualityState?.size ?? this.generationVideoFormat ?? this.currentLocalTrack?.videoFormat;
7667
8270
  const size = format && enabled ? this.mediaService.resolveFrameInterpolationSize(format) : void 0;
7668
8271
  if (enabled && !await this.checkInterpolationSupport(size, token)) {
7669
8272
  token.ensureCurrent();
@@ -7675,6 +8278,43 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7675
8278
  this.applyRemoteInterpolation();
7676
8279
  });
7677
8280
  }
8281
+ /** 手动调试与自动调档使用同一尺寸信令路径,不修改生成或上行配置。 */
8282
+ async adjustDownlinkQuality(direction) {
8283
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8284
+ const controller = this.downlinkQualityController;
8285
+ const state2 = this.downlinkQualityState;
8286
+ if (!controller || !state2 || this.currentState.connectionState !== "Generating" /* generating */ || direction !== "upgrade" && direction !== "downgrade") {
8287
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Downlink quality adjustment requires an active Agora generation and a valid direction");
8288
+ }
8289
+ const level = Math.max(1, Math.min(Object.keys(QUALITY_TIERS).length, state2.level + (direction === "upgrade" ? -1 : 1)));
8290
+ if (level === state2.level) return;
8291
+ this.adaptiveQualityRevision++;
8292
+ controller.resetSamples();
8293
+ await this.applyDownlinkQuality(level, token);
8294
+ controller.start(level);
8295
+ });
8296
+ }
8297
+ async applyDownlinkQuality(level, token, signal) {
8298
+ const state2 = this.downlinkQualityState;
8299
+ const ensureActive2 = () => {
8300
+ token.ensureCurrent();
8301
+ if (signal?.aborted || !state2 || state2 !== this.downlinkQualityState || this.currentState.connectionState !== "Generating" /* generating */ || this.currentState.taskID !== state2.taskID) {
8302
+ throw new XmaxError("CANCELLED" /* cancelled */, "Target size update was cancelled");
8303
+ }
8304
+ };
8305
+ ensureActive2();
8306
+ if (!state2) return;
8307
+ const size = qualityVideoSize(state2.baseline, level);
8308
+ await this.streamController.updateDownlinkVideoResolution(state2.taskID, size, ensureActive2);
8309
+ ensureActive2();
8310
+ state2.level = level;
8311
+ state2.size = size;
8312
+ this.qualityHistory?.write("downlink", level);
8313
+ if (this.interpolationRequested) {
8314
+ this.interpolationSize = size;
8315
+ this.applyRemoteInterpolation();
8316
+ }
8317
+ }
7678
8318
  /**
7679
8319
  * 根据当前视频尺寸准备插帧,能力不可用时降级为原视频,并在应用前校验租约。
7680
8320
  */
@@ -7834,6 +8474,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7834
8474
  * 停止接收媒体统计并清空各类快照,仅对曾有数据的统计发出清空通知。
7835
8475
  */
7836
8476
  clearVideoStatistics() {
8477
+ this.qualityController?.stop();
8478
+ this.downlinkQualityController?.stop();
8479
+ this.downlinkQualityState = void 0;
8480
+ this.adaptiveQualityRevision++;
7837
8481
  this.acceptsVideoStatistics = false;
7838
8482
  const hadNetworkStatistics = this.networkStatistics !== void 0;
7839
8483
  this.networkStatistics = void 0;
@@ -7880,6 +8524,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7880
8524
  * 将返回的轨道绑定到预览视图;收到有效帧且视图已绑定后进入 `ready`。
7881
8525
  *
7882
8526
  * @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
8527
+ * @param options.rotation 输入帧顺时针旋转角度,默认 0;videoFormat 为旋转后的规格,目前仅 Agora 支持非零角度。
7883
8528
  * @returns 包含本地相机视频轨道的媒体流。
7884
8529
  * @throws 模型不支持相机输入、配置无效、权限或采集启动失败时抛出错误。
7885
8530
  */
@@ -7995,7 +8640,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7995
8640
  *
7996
8641
  * @throws 无本地相机或文件视频流、参数无效、操作冲突或底层编码更新失败时抛错。
7997
8642
  */
7998
- async updateVideoFormat(videoFormat) {
8643
+ async updateUplinkVideoFormat(videoFormat) {
7999
8644
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8000
8645
  const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
8001
8646
  const previousFormat = track?.videoFormat;
@@ -8003,10 +8648,86 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8003
8648
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera or file video stream is not started");
8004
8649
  }
8005
8650
  videoFormat.validate();
8006
- await this.streamController.setVideoEncoderConfig(videoFormat);
8651
+ this.qualityController?.stop();
8652
+ await this.streamController.updateUplinkVideoFormat(videoFormat);
8007
8653
  token.ensureCurrent();
8008
8654
  this.generationVideoFormat ?? (this.generationVideoFormat = previousFormat);
8009
8655
  track.updateVideoFormat(videoFormat);
8656
+ this.qualityStates.set(track, { baseline: videoFormat, level: 1 });
8657
+ this.qualityHistory?.write("uplink", 1);
8658
+ });
8659
+ }
8660
+ /** 已连接时仅替换帧率;与其他配置及生命周期操作共享同一操作租约。 */
8661
+ async updateUplinkFrameRate(fps) {
8662
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8663
+ this.qualityController?.stop();
8664
+ await this.applyUplinkFrameRate(fps, token);
8665
+ const track = this.currentLocalTrack;
8666
+ if (track?.videoFormat) {
8667
+ this.qualityStates.set(track, { baseline: track.videoFormat, level: 1 });
8668
+ this.qualityHistory?.write("uplink", 1);
8669
+ }
8670
+ });
8671
+ }
8672
+ /** 手动仅调帧接口,成功后的当前格式将成为新的自适应 L1 基准。 */
8673
+ async applyUplinkFrameRate(fps, token) {
8674
+ const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
8675
+ const previous = track?.videoFormat;
8676
+ const state2 = this.currentState.connectionState;
8677
+ if (!track || !previous || state2 !== "Connected" /* connected */ && state2 !== "Generating" /* generating */) {
8678
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Frame rate updates require a connected local camera or file stream");
8679
+ }
8680
+ if (!Number.isFinite(fps) || fps <= 0) {
8681
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video frame rate must be greater than zero");
8682
+ }
8683
+ const format = await this.streamController.updateUplinkFrameRate(fps);
8684
+ token.ensureCurrent();
8685
+ this.generationVideoFormat ?? (this.generationVideoFormat = previous);
8686
+ track.updateVideoFormat(format);
8687
+ }
8688
+ /** 上下行独立判断;串行应用同一原始事件,避免同时降档争抢配置租约。 */
8689
+ handleAdaptiveQuality(statistics) {
8690
+ const revision = ++this.adaptiveQualityRevision;
8691
+ const controller = this.qualityController;
8692
+ const downlink = this.downlinkQualityController;
8693
+ if (!controller || !downlink) return;
8694
+ const taskID = this.currentState.taskID;
8695
+ if (this.currentState.connectionState !== "Generating" /* generating */ || !taskID) {
8696
+ controller.stop();
8697
+ downlink.stop();
8698
+ return;
8699
+ }
8700
+ if (!statistics || this.coordinator.isBusy || this.adaptiveQualityPending) {
8701
+ controller.resetSamples();
8702
+ downlink.resetSamples();
8703
+ return;
8704
+ }
8705
+ const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
8706
+ if (!track) {
8707
+ controller.stop();
8708
+ } else if (!controller.snapshot.active) {
8709
+ const format = track.videoFormat;
8710
+ if (format) {
8711
+ const state3 = this.qualityStates.get(track) ?? { baseline: format, level: 1 };
8712
+ this.qualityStates.set(track, state3);
8713
+ controller.start(state3.level);
8714
+ }
8715
+ }
8716
+ const state2 = this.downlinkQualityState;
8717
+ if (state2 && !downlink.snapshot.active) downlink.start(state2.level);
8718
+ const sampleAt = performance.now();
8719
+ this.adaptiveQualityPending = true;
8720
+ void (async () => {
8721
+ await controller.observe(statistics.uplinkQuality, sampleAt).catch(() => {
8722
+ });
8723
+ if (revision !== this.adaptiveQualityRevision || this.currentState.taskID !== taskID || state2 !== this.downlinkQualityState || this.currentState.connectionState !== "Generating" /* generating */ || this.coordinator.isBusy) {
8724
+ downlink.resetSamples();
8725
+ return;
8726
+ }
8727
+ await downlink.observe(statistics.downlinkQuality, sampleAt).catch(() => {
8728
+ });
8729
+ })().finally(() => {
8730
+ this.adaptiveQualityPending = false;
8010
8731
  });
8011
8732
  }
8012
8733
  /**
@@ -8046,9 +8767,9 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8046
8767
  "cameraSwitch" /* cameraSwitch */,
8047
8768
  void 0,
8048
8769
  async (token) => {
8770
+ this.qualityController?.stop();
8049
8771
  const stream = await this.cameraController.switchCamera();
8050
8772
  token.ensureCurrent();
8051
- this.connectionManager.updateRemoteMirror();
8052
8773
  return stream;
8053
8774
  }
8054
8775
  );
@@ -8116,6 +8837,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8116
8837
  if (currentState.connectionState === "Generating" /* generating */) {
8117
8838
  await this.generationManager.update(currentState.taskID, {
8118
8839
  videoFormat,
8840
+ targetSize: this.downlinkQualityController ? this.downlinkQualityState?.size : void 0,
8119
8841
  context: options.context
8120
8842
  });
8121
8843
  token.ensureCurrent();
@@ -8153,6 +8875,15 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8153
8875
  this.clearVideoStatistics();
8154
8876
  await this.coordinator.terminate(1 /* all */);
8155
8877
  }
8878
+ /** 仅为尚无档位记录的本地上行选择初始档位;不应用配置或覆盖重连状态。 */
8879
+ resolveInitialUplinkQuality(track) {
8880
+ if (!this.qualityController) return void 0;
8881
+ if (this.networkVideoController.currentTrack) return void 0;
8882
+ if (this.qualityStates.has(track)) return void 0;
8883
+ const baseline = track.videoFormat;
8884
+ if (!baseline) return void 0;
8885
+ return { baseline, level: this.qualityHistory.read("uplink") };
8886
+ }
8156
8887
  /**
8157
8888
  * 建立实时连接:创建会话、进房发布、启动心跳并准备远端渲染轨道。
8158
8889
  *
@@ -8188,9 +8919,19 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8188
8919
  });
8189
8920
  beforePublish = () => cameraReady;
8190
8921
  }
8922
+ const initialQuality = this.resolveInitialUplinkQuality(localTrack);
8923
+ const initialFormat = initialQuality ? qualityVideoFormat(initialQuality.baseline, initialQuality.level) : void 0;
8191
8924
  const remote = await this.connectionManager.connect({
8192
8925
  localTrack,
8193
- remoteVideoFormat: this.generationVideoFormat,
8926
+ remoteVideoFormat: this.generationVideoFormat ?? localTrack.videoFormat,
8927
+ localVideoFormat: initialFormat,
8928
+ onVideoEncoderConfigured: () => {
8929
+ if (!initialQuality || !initialFormat) return;
8930
+ this.generationVideoFormat ?? (this.generationVideoFormat = initialQuality.baseline);
8931
+ localTrack.updateVideoFormat(initialFormat);
8932
+ this.qualityStates.set(localTrack, initialQuality);
8933
+ this.qualityHistory?.write("uplink", initialQuality.level);
8934
+ },
8194
8935
  model: this.options.model,
8195
8936
  includeLocalAudio: this.localVideoController.isActive ? this.localVideoController.audioTrack !== void 0 : !this.networkVideoController.currentTrack && this.cameraController.useMicrophone,
8196
8937
  publishLocalMedia: !this.networkVideoController.currentTrack,
@@ -8218,14 +8959,18 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8218
8959
  */
8219
8960
  async performStartGeneration(token, videoFormat, context) {
8220
8961
  token.setFailureScope(0 /* connection */);
8221
- await this.prepareFrameInterpolation(videoFormat, token);
8962
+ const initialLevel = this.qualityHistory?.read("downlink") ?? 1;
8963
+ const targetSize = this.downlinkQualityController ? qualityVideoSize(videoFormat, initialLevel) : void 0;
8964
+ await this.prepareFrameInterpolation(targetSize ?? videoFormat, token);
8222
8965
  const completeFirstFrame = this.launchTimer.startFirstFrame();
8223
8966
  this.remoteFrameDisplayHandler = () => {
8224
8967
  if (!token.signal.aborted) completeFirstFrame();
8225
8968
  };
8226
8969
  const taskID = await this.generationManager.start({
8227
8970
  videoFormat,
8971
+ targetSize,
8228
8972
  context,
8973
+ mirror: this.currentLocalTrack === this.cameraController.currentTrack && this.cameraController.currentTrack?.position === "front" /* front */,
8229
8974
  signal: token.signal,
8230
8975
  ensureCurrent: () => token.ensureCurrent(),
8231
8976
  onStartSent: this.localVideoController.isActive ? async () => {
@@ -8242,10 +8987,18 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8242
8987
  if (!sessionID) {
8243
8988
  throw new XmaxError("SESSION_ERROR" /* sessionError */, "Realtime session is unavailable");
8244
8989
  }
8990
+ this.downlinkQualityController?.stop();
8991
+ this.downlinkQualityState = {
8992
+ taskID,
8993
+ baseline: videoFormat,
8994
+ level: initialLevel,
8995
+ size: targetSize ?? { width: videoFormat.width, height: videoFormat.height }
8996
+ };
8245
8997
  await this.coordinator.commit(
8246
8998
  new RealtimeState({ connectionState: "Generating" /* generating */, sessionID, taskID }),
8247
8999
  token
8248
9000
  );
9001
+ this.qualityHistory?.write("downlink", initialLevel);
8249
9002
  const onFinish = this.networkVideoController.onFinish;
8250
9003
  if (this.networkVideoController.currentTrack) {
8251
9004
  this.streamController.activateNetworkVideoCompletion(onFinish && (() => {
@@ -8884,6 +9637,146 @@ var VideoContentMode = /* @__PURE__ */ ((VideoContentMode2) => {
8884
9637
  return VideoContentMode2;
8885
9638
  })(VideoContentMode || {});
8886
9639
 
9640
+ // src/Render/Video/RenderStatisticsController.ts
9641
+ var RenderStatisticsController = class {
9642
+ /**
9643
+ * 绑定用于观察原视频帧的元素,调用 start 后才开始采集。
9644
+ */
9645
+ constructor(video) {
9646
+ __publicField(this, "video", video);
9647
+ /**
9648
+ * 统计回调
9649
+ */
9650
+ __publicField(this, "handler");
9651
+ /**
9652
+ * 帧观察与采样生命周期
9653
+ */
9654
+ __publicField(this, "timer");
9655
+ __publicField(this, "callbackID");
9656
+ __publicField(this, "sequence", 0);
9657
+ /**
9658
+ * 采样窗口与输出方式
9659
+ */
9660
+ __publicField(this, "startedAt", 0);
9661
+ __publicField(this, "originalFrames", 0);
9662
+ __publicField(this, "interpolatedFrames", 0);
9663
+ __publicField(this, "interpolationActive", false);
9664
+ __publicField(this, "previousPresentedFrames");
9665
+ }
9666
+ /**
9667
+ * 独立于统计面板可见性采集;没有监听者时不占用定时器。
9668
+ */
9669
+ start() {
9670
+ if (this.timer !== void 0 || !this.video.srcObject && !this.video.getAttribute("src") || !this.handler || typeof this.video.requestVideoFrameCallback !== "function") {
9671
+ return;
9672
+ }
9673
+ this.reset();
9674
+ const sequence = this.sequence;
9675
+ const observe = () => {
9676
+ this.callbackID = this.video.requestVideoFrameCallback((_now, metadata) => {
9677
+ if (sequence !== this.sequence) {
9678
+ return;
9679
+ }
9680
+ this.callbackID = void 0;
9681
+ if (!this.interpolationActive && !document.hidden) {
9682
+ this.recordVideoFrames(metadata.presentedFrames);
9683
+ } else {
9684
+ this.previousPresentedFrames = void 0;
9685
+ }
9686
+ observe();
9687
+ });
9688
+ };
9689
+ observe();
9690
+ this.timer = setInterval(() => this.sample(), 1e3);
9691
+ }
9692
+ /**
9693
+ * 切换输出方式并清空采样窗口,避免原视频和画布重复计数。
9694
+ */
9695
+ setInterpolationActive(active) {
9696
+ this.interpolationActive = active;
9697
+ this.reset();
9698
+ }
9699
+ /**
9700
+ * 记录实际提交到画布的原帧或插值帧,不统计未呈现的计算结果。
9701
+ */
9702
+ recordCanvasFrame(interpolated) {
9703
+ if (interpolated) {
9704
+ this.interpolatedFrames++;
9705
+ } else {
9706
+ this.originalFrames++;
9707
+ }
9708
+ }
9709
+ /**
9710
+ * 按浏览器累计呈现帧数的增量统计,包含两次回调之间已呈现但未回调的帧。
9711
+ */
9712
+ recordVideoFrames(presentedFrames) {
9713
+ if (!Number.isSafeInteger(presentedFrames) || presentedFrames < 0) {
9714
+ this.previousPresentedFrames = void 0;
9715
+ return;
9716
+ }
9717
+ const previous = this.previousPresentedFrames;
9718
+ this.previousPresentedFrames = presentedFrames;
9719
+ if (previous === void 0 || presentedFrames < previous) {
9720
+ this.originalFrames += presentedFrames > 0 ? 1 : 0;
9721
+ return;
9722
+ }
9723
+ this.originalFrames += presentedFrames - previous;
9724
+ }
9725
+ /**
9726
+ * 取消采样及帧观察,忽略已排队的旧回调并清空对外统计。
9727
+ */
9728
+ stop() {
9729
+ this.sequence++;
9730
+ clearInterval(this.timer);
9731
+ this.timer = void 0;
9732
+ if (this.callbackID !== void 0) {
9733
+ this.video.cancelVideoFrameCallback(this.callbackID);
9734
+ }
9735
+ this.callbackID = void 0;
9736
+ this.reset();
9737
+ }
9738
+ /**
9739
+ * 清空采样计数,避免换流或切换输出方式时混入旧数据。
9740
+ */
9741
+ reset() {
9742
+ this.startedAt = performance.now();
9743
+ this.originalFrames = 0;
9744
+ this.interpolatedFrames = 0;
9745
+ this.previousPresentedFrames = void 0;
9746
+ this.notify(void 0);
9747
+ }
9748
+ /**
9749
+ * 按实际经过的时间计算帧率;没有新呈现时输出 0,后台页面不报告帧率。
9750
+ */
9751
+ sample() {
9752
+ const now = performance.now();
9753
+ const seconds = (now - this.startedAt) / 1e3;
9754
+ if (document.hidden || seconds <= 0) {
9755
+ this.reset();
9756
+ return;
9757
+ }
9758
+ const statistics = Object.freeze({
9759
+ source: this.interpolationActive ? "canvas" : "video",
9760
+ frameRate: (this.originalFrames + this.interpolatedFrames) / seconds,
9761
+ originalFrameRate: this.originalFrames / seconds,
9762
+ interpolatedFrameRate: this.interpolatedFrames / seconds
9763
+ });
9764
+ this.startedAt = now;
9765
+ this.originalFrames = 0;
9766
+ this.interpolatedFrames = 0;
9767
+ this.notify(statistics);
9768
+ }
9769
+ /**
9770
+ * 隔离接入方回调异常,避免影响播放和后续采样。
9771
+ */
9772
+ notify(statistics) {
9773
+ try {
9774
+ this.handler?.(statistics);
9775
+ } catch {
9776
+ }
9777
+ }
9778
+ };
9779
+
8887
9780
  // ../../node_modules/.pnpm/framegen@1.4.0/node_modules/framegen/rt.js
8888
9781
  var WG = 8;
8889
9782
  var texBgSeq = 0;
@@ -11047,6 +11940,10 @@ var RemoteVideoFramePipeline = class {
11047
11940
  __publicField(this, "previousPresented");
11048
11941
  __publicField(this, "slowPairs", 0);
11049
11942
  __publicField(this, "active", false);
11943
+ /**
11944
+ * 帧时间基准:媒体时间异常后,本管线持续使用回调时钟,避免反复切换。
11945
+ */
11946
+ __publicField(this, "timeSource", "mediaTime");
11050
11947
  /** vsync 显示节拍:弹出到点的帧并显示最新的一张。 */
11051
11948
  __publicField(this, "tick", (now) => {
11052
11949
  if (this.stopped) return;
@@ -11093,8 +11990,8 @@ var RemoteVideoFramePipeline = class {
11093
11990
  if (!this.loading) void this.initialize();
11094
11991
  return;
11095
11992
  }
11096
- const time = metadata.mediaTime * 1e3;
11097
- if (!Number.isFinite(time) || this.previousTime !== void 0 && time <= this.previousTime) return;
11993
+ const time = this.resolveFrameTime(now, metadata);
11994
+ if (time === void 0) return;
11098
11995
  const previous = this.previousTime;
11099
11996
  const interval = previous === void 0 ? 0 : time - previous;
11100
11997
  const consecutive = this.previousPresented === void 0 || metadata.presentedFrames === this.previousPresented + 1;
@@ -11119,6 +12016,22 @@ var RemoteVideoFramePipeline = class {
11119
12016
  }
11120
12017
  this.interpolate(previousSlot, slot, previous, time, interval);
11121
12018
  }
12019
+ /**
12020
+ * 优先使用媒体时间;新帧的媒体时间异常时切换到单调回调时钟。
12021
+ */
12022
+ resolveFrameTime(now, metadata) {
12023
+ if (this.previousPresented !== void 0 && metadata.presentedFrames <= this.previousPresented) {
12024
+ return void 0;
12025
+ }
12026
+ let time = this.timeSource === "mediaTime" ? metadata.mediaTime * 1e3 : now;
12027
+ if (!Number.isFinite(time) || this.previousTime !== void 0 && time <= this.previousTime) {
12028
+ if (this.timeSource === "callback" || !Number.isFinite(now)) return void 0;
12029
+ this.timeSource = "callback";
12030
+ this.resetTimeline(true);
12031
+ time = now;
12032
+ }
12033
+ return time;
12034
+ }
11122
12035
  /** 帧按显示时间升序入队;同一时间的内容后到先出。 */
11123
12036
  enqueue(frame) {
11124
12037
  const index = this.queue.findIndex((queued) => queued.time >= frame.time);
@@ -11145,8 +12058,10 @@ var RemoteVideoFramePipeline = class {
11145
12058
  this.fail(new Error("Interpolation exceeded the GPU frame budget for 5 consecutive pairs"));
11146
12059
  return;
11147
12060
  }
11148
- if (this.position(performance.now()) >= midpoint) return;
11149
- this.enqueue({ slot, time: midpoint });
12061
+ if (this.position(performance.now()) >= midpoint) {
12062
+ return;
12063
+ }
12064
+ this.enqueue({ slot, time: midpoint, interpolated: true });
11150
12065
  }).catch((error) => {
11151
12066
  if (!this.stopped) this.fail(error);
11152
12067
  });
@@ -11165,6 +12080,7 @@ var RemoteVideoFramePipeline = class {
11165
12080
  this.displayedTime = frame.time;
11166
12081
  this.canvas.style.visibility = "visible";
11167
12082
  this.setActive(true);
12083
+ this.options.onFramePresented?.(frame.interpolated ?? false);
11168
12084
  }
11169
12085
  async initialize() {
11170
12086
  this.loading = true;
@@ -11194,11 +12110,13 @@ var RemoteVideoFramePipeline = class {
11194
12110
  this.active = active;
11195
12111
  this.options.onActiveChange?.(active);
11196
12112
  }
11197
- resetTimeline() {
12113
+ resetTimeline(preserveGpuTimeout = false) {
11198
12114
  this.generation++;
11199
12115
  this.queue = [];
11200
- clearTimeout(this.stallTimer);
11201
- this.stallTimer = void 0;
12116
+ if (!preserveGpuTimeout) {
12117
+ clearTimeout(this.stallTimer);
12118
+ this.stallTimer = void 0;
12119
+ }
11202
12120
  if (this.raf !== void 0) cancelAnimationFrame(this.raf);
11203
12121
  this.raf = void 0;
11204
12122
  this.anchor = void 0;
@@ -11277,6 +12195,10 @@ var XmaxVideoView = class {
11277
12195
  __publicField(this, "interpolationOptions");
11278
12196
  __publicField(this, "interpolationPipeline");
11279
12197
  __publicField(this, "interpolationCanvas");
12198
+ /**
12199
+ * 呈现统计组件
12200
+ */
12201
+ __publicField(this, "statisticsController");
11280
12202
  /**
11281
12203
  * DOM 挂载状态
11282
12204
  */
@@ -11287,6 +12209,7 @@ var XmaxVideoView = class {
11287
12209
  this.element.style.overflow = "hidden";
11288
12210
  this.element.style.backgroundColor = "black";
11289
12211
  this.videoElement = document.createElement("video");
12212
+ this.statisticsController = new RenderStatisticsController(this.videoElement);
11290
12213
  this.videoElement.autoplay = true;
11291
12214
  this.videoElement.playsInline = true;
11292
12215
  this.videoElement.muted = true;
@@ -11303,6 +12226,20 @@ var XmaxVideoView = class {
11303
12226
  get track() {
11304
12227
  return this.currentTrack;
11305
12228
  }
12229
+ /**
12230
+ * 每秒接收当前视图的呈现统计;换流、卸载或不可统计时返回 undefined。
12231
+ */
12232
+ get renderStatisticsHandler() {
12233
+ return this.statisticsController.handler;
12234
+ }
12235
+ /**
12236
+ * 更新统计回调,不影响播放;移除回调时停止采集。
12237
+ */
12238
+ set renderStatisticsHandler(handler) {
12239
+ this.statisticsController.handler = handler;
12240
+ this.statisticsController.stop();
12241
+ this.startRenderStatistics();
12242
+ }
11306
12243
  /**
11307
12244
  * 解绑旧轨道后接入新轨道,并为新内容重新启用一次性首帧通知。
11308
12245
  */
@@ -11362,6 +12299,7 @@ var XmaxVideoView = class {
11362
12299
  });
11363
12300
  }
11364
12301
  this.startInterpolation();
12302
+ this.startRenderStatistics();
11365
12303
  if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && !this.hasNotifiedFrameDisplay) {
11366
12304
  this.armFrameDisplayNotification();
11367
12305
  }
@@ -11371,6 +12309,7 @@ var XmaxVideoView = class {
11371
12309
  */
11372
12310
  detach() {
11373
12311
  this.detached = true;
12312
+ this.statisticsController.stop();
11374
12313
  this.stopInterpolation();
11375
12314
  this.cancelFrameDisplayNotification();
11376
12315
  if (this.videoElement.getAttribute("src")) {
@@ -11382,6 +12321,7 @@ var XmaxVideoView = class {
11382
12321
  * 设置网络视频预览;静音播放一次,不捕获或发布媒体轨道。 @internal
11383
12322
  */
11384
12323
  setVideoURL(url) {
12324
+ this.statisticsController.stop();
11385
12325
  this.stopInterpolation();
11386
12326
  this.cancelFrameDisplayNotification();
11387
12327
  this.hasNotifiedFrameDisplay = false;
@@ -11395,6 +12335,7 @@ var XmaxVideoView = class {
11395
12335
  this.armFrameDisplayNotification();
11396
12336
  }
11397
12337
  this.videoElement.load();
12338
+ this.startRenderStatistics();
11398
12339
  }
11399
12340
  /**
11400
12341
  * 直接设置渲染用的媒体流(绑定管线和直接渲染共用)。 @internal
@@ -11403,6 +12344,7 @@ var XmaxVideoView = class {
11403
12344
  if (this.videoElement.srcObject === stream) {
11404
12345
  return;
11405
12346
  }
12347
+ this.statisticsController.stop();
11406
12348
  this.stopInterpolation();
11407
12349
  this.cancelFrameDisplayNotification();
11408
12350
  this.hasNotifiedFrameDisplay = false;
@@ -11411,6 +12353,7 @@ var XmaxVideoView = class {
11411
12353
  this.videoElement.autoplay = true;
11412
12354
  this.armFrameDisplayNotification();
11413
12355
  this.startInterpolation();
12356
+ this.startRenderStatistics();
11414
12357
  }
11415
12358
  }
11416
12359
  /**
@@ -11440,7 +12383,18 @@ var XmaxVideoView = class {
11440
12383
  this.interpolationCanvas = canvas;
11441
12384
  this.applyContentMode();
11442
12385
  this.element.appendChild(canvas);
11443
- const pipeline = new RemoteVideoFramePipeline(this.videoElement, canvas, this.interpolationOptions);
12386
+ const options = this.interpolationOptions;
12387
+ const pipeline = new RemoteVideoFramePipeline(this.videoElement, canvas, {
12388
+ ...options,
12389
+ onActiveChange: (active) => {
12390
+ this.statisticsController.setInterpolationActive(active);
12391
+ options.onActiveChange?.(active);
12392
+ },
12393
+ onFramePresented: (interpolated) => {
12394
+ this.statisticsController.recordCanvasFrame(interpolated);
12395
+ options.onFramePresented?.(interpolated);
12396
+ }
12397
+ });
11444
12398
  this.interpolationPipeline = pipeline;
11445
12399
  pipeline.start();
11446
12400
  }
@@ -11475,6 +12429,7 @@ var XmaxVideoView = class {
11475
12429
  * 解绑当前轨道并清空渲染内容。
11476
12430
  */
11477
12431
  detachCurrentTrack() {
12432
+ this.statisticsController.stop();
11478
12433
  const track = this.currentTrack;
11479
12434
  if (track) {
11480
12435
  VideoRenderRegistry.binding(track)?.detachHandler(this);
@@ -11540,6 +12495,12 @@ var XmaxVideoView = class {
11540
12495
  this.interpolationCanvas.style.transform = this.mirrored ? "scaleX(-1)" : "";
11541
12496
  }
11542
12497
  }
12498
+ /**
12499
+ * 视图未卸载时启动呈现统计,采样及媒体就绪检查由统计组件负责。
12500
+ */
12501
+ startRenderStatistics() {
12502
+ if (!this.detached) this.statisticsController.start();
12503
+ }
11543
12504
  };
11544
12505
 
11545
12506
  // src/Render/Video/XmaxRealtimeVideoView.ts
@@ -11739,4 +12700,4 @@ export {
11739
12700
  XmaxVideoView,
11740
12701
  XmaxRealtimeVideoView
11741
12702
  };
11742
- //# sourceMappingURL=chunk-VMJXIVUF.js.map
12703
+ //# sourceMappingURL=chunk-4O5BSMUX.js.map