@xmaxai/web-sdk 1.0.4 → 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.
@@ -12,9 +12,16 @@ var API_BASE_URLS = {
12
12
  ["china" /* china */]: "https://cloud.xmax.22duck.cn/open/api/v1",
13
13
  ["global" /* global */]: "https://api.xmax.cloud/open/api/v1"
14
14
  };
15
+ var NON_REALTIME_API_BASE_URLS = {
16
+ ["china" /* china */]: "https://api.xmaxai.com/open/api/v1",
17
+ ["global" /* global */]: "https://api.xmax.ai/open/api/v1"
18
+ };
15
19
  function apiBaseURL(environment) {
16
20
  return API_BASE_URLS[environment];
17
21
  }
22
+ function nonRealtimeApiBaseURL(environment) {
23
+ return NON_REALTIME_API_BASE_URLS[environment];
24
+ }
18
25
 
19
26
  // src/Foundation/Logging/XmaxLogger.ts
20
27
  var XmaxLoggerOption = /* @__PURE__ */ ((XmaxLoggerOption2) => {
@@ -198,7 +205,7 @@ var XmaxError = class _XmaxError extends Error {
198
205
  };
199
206
 
200
207
  // src/Foundation/Runtime/RuntimeInfo.ts
201
- var XMAX_SDK_VERSION = "1.0.4";
208
+ var XMAX_SDK_VERSION = "1.0.6";
202
209
  var _RuntimeInfo = class _RuntimeInfo {
203
210
  /**
204
211
  * 序列化为接口传输使用的 snake_case 字段。
@@ -268,8 +275,24 @@ var _ApiService = class _ApiService {
268
275
  */
269
276
  async request(method, path, body, options) {
270
277
  this.validateConfiguration();
278
+ if (options?.signal?.aborted) {
279
+ throw new XmaxError("CANCELLED" /* cancelled */, "API request was cancelled");
280
+ }
271
281
  const request = this.makeRequest(method, path, body, options);
272
282
  const startedAt = Date.now();
283
+ const timeoutSignal = request.init.signal;
284
+ const controller = new AbortController();
285
+ const abortTimeout = () => controller.abort(timeoutSignal.reason);
286
+ const abortCaller = () => controller.abort(new DOMException("Cancelled", "AbortError"));
287
+ timeoutSignal.addEventListener("abort", abortTimeout, { once: true });
288
+ options?.signal?.addEventListener("abort", abortCaller, { once: true });
289
+ request.init.signal = controller.signal;
290
+ if (timeoutSignal.aborted) {
291
+ abortTimeout();
292
+ }
293
+ if (options?.signal?.aborted) {
294
+ abortCaller();
295
+ }
273
296
  let status;
274
297
  let text;
275
298
  try {
@@ -280,6 +303,9 @@ var _ApiService = class _ApiService {
280
303
  const mapped = _ApiService.transportError(error);
281
304
  this.logFailure(method, path, mapped, startedAt);
282
305
  throw mapped;
306
+ } finally {
307
+ timeoutSignal.removeEventListener("abort", abortTimeout);
308
+ options?.signal?.removeEventListener("abort", abortCaller);
283
309
  }
284
310
  try {
285
311
  const value = _ApiService.parseResponse(text, status);
@@ -297,8 +323,8 @@ var _ApiService = class _ApiService {
297
323
  /**
298
324
  * 发送 GET 请求。
299
325
  */
300
- get(path) {
301
- return this.request("GET" /* get */, path);
326
+ get(path, options) {
327
+ return this.request("GET" /* get */, path, void 0, options);
302
328
  }
303
329
  /**
304
330
  * 发送 POST 请求。
@@ -405,7 +431,8 @@ var _ApiService = class _ApiService {
405
431
  });
406
432
  }
407
433
  const ok = status >= 200 && status < 300;
408
- if (!ok || envelope.success !== true) {
434
+ const succeeded = envelope.success === true || envelope.success === void 0 && envelope.code === 200;
435
+ if (!ok || !succeeded) {
409
436
  const message = envelope.message?.trim();
410
437
  throw new XmaxError(
411
438
  "API_ERROR" /* apiError */,
@@ -561,50 +588,68 @@ var RealtimeVideoFormat = class _RealtimeVideoFormat {
561
588
  }
562
589
  };
563
590
 
591
+ // src/Foundation/RTC/RtcProvider.ts
592
+ var RtcProvider = /* @__PURE__ */ ((RtcProvider2) => {
593
+ RtcProvider2["trtc"] = "trtc";
594
+ RtcProvider2["agora"] = "agora";
595
+ RtcProvider2["vertc"] = "vertc";
596
+ return RtcProvider2;
597
+ })(RtcProvider || {});
598
+
564
599
  // src/Service/Realtime/RealtimeModel.ts
565
600
  var RealtimeModel = /* @__PURE__ */ ((RealtimeModel2) => {
566
601
  RealtimeModel2["x2_0"] = "x2.0";
567
602
  RealtimeModel2["x2_0_trtc"] = "x2.0-trtc";
568
- RealtimeModel2["x2_0_agora"] = "x2.0-agora";
569
603
  RealtimeModel2["x2_1_preview"] = "x2.1-preview";
570
604
  return RealtimeModel2;
571
605
  })(RealtimeModel || {});
606
+ function isKnownModel(model) {
607
+ if (typeof model !== "string" || !model.trim()) {
608
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Realtime model must be a non-empty string");
609
+ }
610
+ return Object.values(RealtimeModel).includes(model);
611
+ }
612
+ function modelDefaults(model) {
613
+ return isKnownModel(model) ? model : "x2.1-preview" /* x2_1_preview */;
614
+ }
615
+ var MODEL_DISPLAY_NAMES = {
616
+ ["x2.0" /* x2_0 */]: "X2.0",
617
+ ["x2.0-trtc" /* x2_0_trtc */]: "X2.1-preview-trtc",
618
+ ["x2.1-preview" /* x2_1_preview */]: "X2.1-preview-agora"
619
+ };
572
620
  var MODEL_BASE_URLS = {
573
- ["x2.0-agora" /* x2_0_agora */]: "https://dev.xmaxai.com/open/api/v1",
574
- ["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"
575
622
  };
576
623
  var RESOLUTION_BUCKETS = {
577
- ["x2.1-preview" /* x2_1_preview */]: [
578
- { width: 1024, height: 1920 },
579
- { width: 1920, height: 1024 }
580
- ],
581
- ["x2.0-agora" /* x2_0_agora */]: [
624
+ ["x2.0" /* x2_0 */]: [],
625
+ ["x2.0-trtc" /* x2_0_trtc */]: [
582
626
  { width: 1024, height: 1920 },
583
627
  { width: 1920, height: 1024 }
584
628
  ],
585
- ["x2.0" /* x2_0 */]: [],
586
- ["x2.0-trtc" /* x2_0_trtc */]: [
629
+ ["x2.1-preview" /* x2_1_preview */]: [
587
630
  { width: 1024, height: 1920 },
588
631
  { width: 1920, height: 1024 }
589
632
  ]
590
633
  };
591
634
  var MAXIMUM_INPUT_PIXELS = {
592
- ["x2.1-preview" /* x2_1_preview */]: 21e5,
593
- ["x2.0-agora" /* x2_0_agora */]: 21e5,
594
635
  ["x2.0" /* x2_0 */]: 128e4,
595
- ["x2.0-trtc" /* x2_0_trtc */]: 21e5
636
+ ["x2.0-trtc" /* x2_0_trtc */]: 21e5,
637
+ ["x2.1-preview" /* x2_1_preview */]: 21e5
596
638
  };
639
+ function modelDisplayName(model) {
640
+ return isKnownModel(model) ? MODEL_DISPLAY_NAMES[model] : model;
641
+ }
597
642
  function modelBaseURL(model) {
598
- return MODEL_BASE_URLS[model];
643
+ return MODEL_BASE_URLS[modelDefaults(model)];
599
644
  }
600
645
  function resolutionBuckets(model) {
601
- return RESOLUTION_BUCKETS[model];
646
+ return RESOLUTION_BUCKETS[modelDefaults(model)];
602
647
  }
603
648
  function minimumInputPixels(_model) {
604
649
  return 6e5;
605
650
  }
606
651
  function maximumInputPixels(model) {
607
- return MAXIMUM_INPUT_PIXELS[model];
652
+ return MAXIMUM_INPUT_PIXELS[modelDefaults(model)];
608
653
  }
609
654
  function inputSizeAlignment(_model) {
610
655
  return 32;
@@ -613,11 +658,23 @@ function defaultFrameRate(_model) {
613
658
  return 30;
614
659
  }
615
660
  function defaultCameraVideoFormat(model) {
616
- if (model === "x2.0" /* x2_0 */) {
661
+ if (modelDefaults(model) === "x2.0" /* x2_0 */) {
617
662
  return new RealtimeVideoFormat({ width: 832, height: 1472, fps: 30 });
618
663
  }
619
664
  return new RealtimeVideoFormat({ width: 1024, height: 1920, fps: 30 });
620
665
  }
666
+ var MODEL_RTC_PROVIDERS = {
667
+ ["x2.0" /* x2_0 */]: Object.freeze(["vertc" /* vertc */]),
668
+ ["x2.0-trtc" /* x2_0_trtc */]: Object.freeze(["trtc" /* trtc */]),
669
+ ["x2.1-preview" /* x2_1_preview */]: Object.freeze(["agora" /* agora */])
670
+ };
671
+ function supportedRtcProviders(model) {
672
+ return isKnownModel(model) ? MODEL_RTC_PROVIDERS[model] : CUSTOM_MODEL_RTC_PROVIDERS;
673
+ }
674
+ var CUSTOM_MODEL_RTC_PROVIDERS = Object.freeze([
675
+ ...MODEL_RTC_PROVIDERS["x2.1-preview" /* x2_1_preview */],
676
+ ...Object.values(RtcProvider).filter((provider) => !MODEL_RTC_PROVIDERS["x2.1-preview" /* x2_1_preview */].includes(provider))
677
+ ]);
621
678
 
622
679
  // src/Foundation/Media/Video/FrameInterpolationSupport.ts
623
680
  async function frameInterpolationAdapter(size) {
@@ -802,7 +859,14 @@ var CONTENT_TYPES = {
802
859
  gif: "image/gif",
803
860
  bmp: "image/bmp",
804
861
  heic: "image/heic",
805
- heif: "image/heif"
862
+ heif: "image/heif",
863
+ mp4: "video/mp4",
864
+ m4v: "video/mp4",
865
+ mov: "video/quicktime",
866
+ webm: "video/webm",
867
+ avi: "video/x-msvideo",
868
+ mkv: "video/x-matroska",
869
+ ogv: "video/ogg"
806
870
  };
807
871
  var StorageService = class _StorageService {
808
872
  /**
@@ -836,6 +900,81 @@ var StorageService = class _StorageService {
836
900
  * 上传图片并返回访问地址。
837
901
  */
838
902
  async uploadImage(options) {
903
+ return this.upload(options, "Image");
904
+ }
905
+ /**
906
+ * 普通上传原始视频并返回访问地址,不进行转码、压缩或分片。
907
+ */
908
+ async uploadVideo(options) {
909
+ return this.upload(options, "Video");
910
+ }
911
+ /**
912
+ * 下载原始视频文件并交给浏览器保存,支持取消网络下载。
913
+ */
914
+ async downloadVideo(options) {
915
+ const fileName = _StorageService.validateFileName(options.fileName);
916
+ let url;
917
+ try {
918
+ url = new URL(options.url.trim());
919
+ } catch {
920
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Invalid video download URL");
921
+ }
922
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
923
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video download requires an HTTP(S) URL without embedded credentials");
924
+ }
925
+ if (typeof document === "undefined" || !document.body) {
926
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video saving requires a browser document");
927
+ }
928
+ let objectURL;
929
+ let link;
930
+ let saveRequested = false;
931
+ try {
932
+ if (options.signal?.aborted) {
933
+ throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
934
+ }
935
+ const response = await fetch(options.url.trim(), { credentials: "omit", signal: options.signal });
936
+ if (!response.ok) {
937
+ throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, "Video download failed", { httpStatus: response.status });
938
+ }
939
+ const data = await response.blob();
940
+ if (options.signal?.aborted) {
941
+ throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
942
+ }
943
+ if (data.size === 0) {
944
+ throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, "Downloaded video is empty");
945
+ }
946
+ objectURL = URL.createObjectURL(data);
947
+ link = document.createElement("a");
948
+ link.href = objectURL;
949
+ link.download = fileName;
950
+ link.hidden = true;
951
+ document.body.appendChild(link);
952
+ link.click();
953
+ saveRequested = true;
954
+ } catch (error) {
955
+ if (options.signal?.aborted) {
956
+ throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
957
+ }
958
+ if (error instanceof XmaxError) {
959
+ throw error;
960
+ }
961
+ throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, error instanceof Error ? error.message : String(error));
962
+ } finally {
963
+ link?.remove();
964
+ if (objectURL) {
965
+ const resourceURL = objectURL;
966
+ if (saveRequested) {
967
+ setTimeout(() => URL.revokeObjectURL(resourceURL), 6e4);
968
+ } else {
969
+ URL.revokeObjectURL(resourceURL);
970
+ }
971
+ }
972
+ }
973
+ }
974
+ /**
975
+ * 获取临时凭证并通过 COS putObject 上传文件,统一处理进度、结果和错误。
976
+ */
977
+ async upload(options, mediaType) {
839
978
  const startedAt = Date.now();
840
979
  try {
841
980
  const safeName = _StorageService.validateFileName(options.fileName);
@@ -844,7 +983,7 @@ var StorageService = class _StorageService {
844
983
  if (body.size === 0) {
845
984
  throw new XmaxError(
846
985
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
847
- "Image data cannot be empty"
986
+ `${mediaType} data cannot be empty`
848
987
  );
849
988
  }
850
989
  const temporary = await this.fetchStorageConfiguration();
@@ -982,7 +1121,7 @@ var StorageService = class _StorageService {
982
1121
  if (!safeName || safeName.includes("/") || safeName.includes("\\")) {
983
1122
  throw new XmaxError(
984
1123
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
985
- "Invalid file name for storage upload"
1124
+ "Invalid file name for storage operation"
986
1125
  );
987
1126
  }
988
1127
  return safeName;
@@ -1121,6 +1260,11 @@ ${JSON.stringify({
1121
1260
  receivedBytes: valid(rtc.RecvBytes),
1122
1261
  localVideo,
1123
1262
  remoteVideo: remote,
1263
+ capture: {
1264
+ width: valid(local.captureResolutionWidth, 1),
1265
+ height: valid(local.captureResolutionHeight, 1),
1266
+ frameRate: valid(local.captureFrameRate)
1267
+ },
1124
1268
  localAudio: client.getLocalAudioStats(),
1125
1269
  remoteAudio: client.getRemoteAudioStats()
1126
1270
  }, null, 2)}`, 2 /* performance */);
@@ -1168,6 +1312,59 @@ function rtcOrientedVideoSize(size, environment) {
1168
1312
  return shouldTransposeRtcVideoSize(environment) ? { width: size.height, height: size.width } : size;
1169
1313
  }
1170
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
+
1171
1368
  // src/Foundation/RTC/Agora/AgoraRtcManager.ts
1172
1369
  var _AgoraRtcManager = class _AgoraRtcManager {
1173
1370
  /**
@@ -1185,6 +1382,12 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1185
1382
  __publicField(this, "lifecycle", 0);
1186
1383
  __publicField(this, "camera");
1187
1384
  __publicField(this, "microphone");
1385
+ __publicField(this, "externalVideo");
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");
1188
1391
  __publicField(this, "connection");
1189
1392
  __publicField(this, "roomVersion", 0);
1190
1393
  /**
@@ -1201,6 +1404,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1201
1404
  */
1202
1405
  __publicField(this, "listener");
1203
1406
  __publicField(this, "statisticsTimer");
1407
+ __publicField(this, "networkQualitySuspended", false);
1204
1408
  __publicField(this, "audioVolumes", /* @__PURE__ */ new Map());
1205
1409
  __publicField(this, "requestedAudio", /* @__PURE__ */ new Set());
1206
1410
  /**
@@ -1267,12 +1471,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1267
1471
  async destroy() {
1268
1472
  this.lifecycle++;
1269
1473
  const client = this.client;
1270
- this.listener = void 0;
1271
1474
  this.clearRoom();
1272
1475
  this.client = void 0;
1273
1476
  client?.removeAllListeners();
1477
+ this.releaseCameraSender();
1274
1478
  this.camera?.close();
1275
1479
  this.microphone?.close();
1480
+ this.externalVideo?.close();
1481
+ this.externalAudio?.close();
1482
+ this.externalVideo = void 0;
1483
+ this.externalAudio = void 0;
1276
1484
  this.camera = void 0;
1277
1485
  this.microphone = void 0;
1278
1486
  const sdk = this.sdk;
@@ -1291,7 +1499,9 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1291
1499
  */
1292
1500
  async startCameraCapture(options) {
1293
1501
  const client = this.requireClient();
1294
- if (this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is already running");
1502
+ if (this.camera || this.externalVideo) {
1503
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "A local video source is already active");
1504
+ }
1295
1505
  try {
1296
1506
  const captureSize = rtcOrientedVideoSize(options);
1297
1507
  const camera = await this.sdk.createCameraVideoTrack({
@@ -1303,6 +1513,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1303
1513
  throw this.cancelled();
1304
1514
  }
1305
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);
1306
1526
  return camera.getMediaStreamTrack();
1307
1527
  } catch (error) {
1308
1528
  throw this.mapError(error, "CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */);
@@ -1315,6 +1535,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1315
1535
  const camera = this.requireCamera();
1316
1536
  await this.run(() => camera.setDevice({ facingMode: to === "front" /* front */ ? "user" : "environment" }));
1317
1537
  if (this.camera !== camera) throw this.cancelled();
1538
+ if (this.videoPublishRequested) await this.restoreCameraSender(camera);
1318
1539
  return camera.getMediaStreamTrack();
1319
1540
  }
1320
1541
  /**
@@ -1322,15 +1543,62 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1322
1543
  */
1323
1544
  async stopCameraCapture() {
1324
1545
  const camera = this.camera;
1546
+ this.releaseCameraSender();
1325
1547
  this.camera = void 0;
1326
1548
  camera?.close();
1327
1549
  }
1328
1550
  /**
1329
- * 应用公共编码配置,补偿移动端竖屏的宽高转置。
1551
+ * 将外部原生轨包装为 Agora 本地轨,不申请设备权限;失败时释放本次克隆。
1552
+ */
1553
+ async setExternalMediaTracks(tracks) {
1554
+ this.requireClient();
1555
+ if (this.camera || this.microphone || this.externalVideo) {
1556
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
1557
+ }
1558
+ if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
1559
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media requires live video and optional audio tracks");
1560
+ }
1561
+ const video = tracks.videoTrack.clone();
1562
+ let audio;
1563
+ let wrappedVideo;
1564
+ let wrappedAudio;
1565
+ try {
1566
+ audio = tracks.audioTrack?.clone();
1567
+ wrappedVideo = this.sdk.createCustomVideoTrack({ mediaStreamTrack: video });
1568
+ if (audio) {
1569
+ wrappedAudio = this.sdk.createCustomAudioTrack({ mediaStreamTrack: audio, encoderConfig: "music_standard" });
1570
+ }
1571
+ this.externalVideo = wrappedVideo;
1572
+ this.externalAudio = wrappedAudio;
1573
+ } catch (error) {
1574
+ wrappedVideo?.close();
1575
+ wrappedAudio?.close();
1576
+ video.stop();
1577
+ audio?.stop();
1578
+ throw this.mapError(error);
1579
+ }
1580
+ }
1581
+ /**
1582
+ * 摄像头实验走原生 sender(发布前暂存);外部视频仍走 Agora 编码配置。
1330
1583
  */
1331
1584
  async configureVideoEncoding(config) {
1332
- const camera = this.requireCamera();
1333
- const encodingSize = rtcOrientedVideoSize(config);
1585
+ const camera = this.requireLocalVideo();
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;
1334
1602
  await this.run(async () => {
1335
1603
  await camera.setEncoderConfiguration({
1336
1604
  width: encodingSize.width,
@@ -1339,9 +1607,37 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1339
1607
  bitrateMin: config.minimumBitrate,
1340
1608
  bitrateMax: config.maximumBitrate
1341
1609
  });
1342
- 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
+ }
1614
+ });
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);
1343
1634
  });
1344
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
+ }
1345
1641
  /**
1346
1642
  * 使用会话下发的字符串频道与 UID 入房;不采信后端区域覆盖。
1347
1643
  */
@@ -1349,7 +1645,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1349
1645
  await this.rejoinTask?.catch(() => {
1350
1646
  });
1351
1647
  const client = this.requireClient();
1352
- if (configuration.provider !== "agora" || this.connection) {
1648
+ if (configuration.provider !== "agora" /* agora */ || this.connection) {
1353
1649
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Agora credentials and an idle room are required");
1354
1650
  }
1355
1651
  const version = ++this.roomVersion;
@@ -1385,7 +1681,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1385
1681
  if (signal?.aborted) throw this.cancelled();
1386
1682
  const current = this.connection;
1387
1683
  const client = this.requireClient();
1388
- if (!current || config.provider !== "agora" || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
1684
+ if (!current || config.provider !== "agora" /* agora */ || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
1389
1685
  throw new XmaxError("SESSION_ERROR" /* sessionError */, "Agora session binding changed");
1390
1686
  }
1391
1687
  const version = this.roomVersion;
@@ -1454,12 +1750,15 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1454
1750
  await this.run(() => client.leave());
1455
1751
  throw this.cancelled();
1456
1752
  }
1457
- if (this.videoPublishRequested && this.camera) {
1458
- await this.run(() => client.publish(this.camera));
1753
+ const video = this.externalVideo ?? this.camera;
1754
+ const audio = this.externalVideo ? this.externalAudio : this.microphone;
1755
+ if (this.videoPublishRequested && video) {
1756
+ await this.run(() => client.publish(video));
1459
1757
  this.ensureRoomCurrent(client, version);
1758
+ if (video === this.camera) await this.restoreCameraSender(this.camera);
1460
1759
  }
1461
- if (this.audioPublishRequested && this.microphone) {
1462
- await this.run(() => client.publish(this.microphone));
1760
+ if (this.audioPublishRequested && audio) {
1761
+ await this.run(() => client.publish(audio));
1463
1762
  this.ensureRoomCurrent(client, version);
1464
1763
  }
1465
1764
  for (const user of client.remoteUsers) {
@@ -1480,12 +1779,13 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1480
1779
  */
1481
1780
  async publishLocalVideo() {
1482
1781
  const client = this.requireClient();
1483
- const camera = this.requireCamera();
1782
+ const camera = this.requireLocalVideo();
1484
1783
  this.videoPublishRequested = true;
1485
1784
  await this.rejoinOperation;
1486
1785
  if (client !== this.client || !this.videoPublishRequested) throw this.cancelled();
1487
1786
  if (client.localTracks.includes(camera)) return;
1488
1787
  await this.run(() => client.publish(camera));
1788
+ if (camera === this.camera) await this.run(() => this.restoreCameraSender(this.camera));
1489
1789
  }
1490
1790
  /**
1491
1791
  * 停止视频发布但保留采集与预览。
@@ -1493,10 +1793,13 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1493
1793
  async unpublishLocalVideo() {
1494
1794
  this.videoPublishRequested = false;
1495
1795
  if (this.rejoinTask || this.tokenExpired) return;
1496
- if (this.client && this.camera) await this.run(() => this.client.unpublish(this.camera));
1796
+ const video = this.externalVideo ?? this.camera;
1797
+ if (this.client && video) {
1798
+ await this.run(() => this.client.unpublish(video));
1799
+ }
1497
1800
  }
1498
1801
  /**
1499
- * 首次发布时申请麦克风;取消后的迟到采集立即释放。
1802
+ * 外部源只发布提供的音轨;相机源首次发布时申请麦克风,迟到采集立即释放。
1500
1803
  */
1501
1804
  async publishLocalAudio() {
1502
1805
  const client = this.requireClient();
@@ -1504,6 +1807,13 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1504
1807
  await this.rejoinOperation;
1505
1808
  const version = this.roomVersion;
1506
1809
  if (client !== this.client || !this.audioPublishRequested) throw this.cancelled();
1810
+ if (this.externalVideo) {
1811
+ const audio = this.externalAudio;
1812
+ if (audio && !client.localTracks.includes(audio)) {
1813
+ await this.run(() => client.publish(audio));
1814
+ }
1815
+ return;
1816
+ }
1507
1817
  try {
1508
1818
  if (!this.microphone) {
1509
1819
  const track = await this.sdk.createMicrophoneAudioTrack();
@@ -1524,7 +1834,10 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1524
1834
  async unpublishLocalAudio() {
1525
1835
  this.audioPublishRequested = false;
1526
1836
  if (this.rejoinTask || this.tokenExpired) return;
1527
- if (this.client && this.microphone) await this.run(() => this.client.unpublish(this.microphone));
1837
+ const audio = this.externalVideo ? this.externalAudio : this.microphone;
1838
+ if (this.client && audio) {
1839
+ await this.run(() => this.client.unpublish(audio));
1840
+ }
1528
1841
  }
1529
1842
  /**
1530
1843
  * 订阅远端主视频并返回原生轨道,不创建额外播放视图。
@@ -1670,6 +1983,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1670
1983
  const handleExpired = () => {
1671
1984
  if (!active() || this.tokenExpired) return;
1672
1985
  this.tokenExpired = true;
1986
+ this.listener?.onNetworkStatistics?.(void 0);
1673
1987
  this.listener?.onTokenExpired?.();
1674
1988
  };
1675
1989
  client.on("user-published", (user, kind) => {
@@ -1693,13 +2007,15 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1693
2007
  if (mediaActive()) this.listener?.onCustomMessageReceived(String(uid), typeof data === "string" ? data : new TextDecoder().decode(data));
1694
2008
  });
1695
2009
  client.on("network-quality", (quality) => {
1696
- if (mediaActive()) AgoraRtcStatistics.network(quality, this.listener);
2010
+ if (mediaActive() && !this.networkQualitySuspended) AgoraRtcStatistics.network(quality, this.listener);
1697
2011
  });
1698
2012
  client.on("token-privilege-will-expire", () => {
1699
2013
  if (active() && !this.tokenExpired) this.listener?.onTokenWillExpire?.();
1700
2014
  });
1701
2015
  client.on("token-privilege-did-expire", handleExpired);
1702
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);
1703
2019
  if (state2 === "DISCONNECTED" && reason === "TOKEN_EXPIRE") {
1704
2020
  handleExpired();
1705
2021
  return;
@@ -1717,6 +2033,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1717
2033
  this.cancelRejoin?.();
1718
2034
  this.connection = void 0;
1719
2035
  this.tokenExpired = false;
2036
+ this.networkQualitySuspended = false;
1720
2037
  this.videoPublishRequested = false;
1721
2038
  this.audioPublishRequested = false;
1722
2039
  clearInterval(this.statisticsTimer);
@@ -1755,6 +2072,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1755
2072
  if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
1756
2073
  return this.camera;
1757
2074
  }
2075
+ /**
2076
+ * 获取当前可编码、发布的视频轨,不要求来源为摄像头。
2077
+ */
2078
+ requireLocalVideo() {
2079
+ const video = this.externalVideo ?? this.camera;
2080
+ if (!video) {
2081
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video source is not prepared");
2082
+ }
2083
+ return video;
2084
+ }
1758
2085
  /**
1759
2086
  * 执行声网操作并统一错误类型。
1760
2087
  */
@@ -1787,14 +2114,6 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1787
2114
  __publicField(_AgoraRtcManager, "regions", /* @__PURE__ */ new Map());
1788
2115
  var AgoraRtcManager = _AgoraRtcManager;
1789
2116
 
1790
- // src/Foundation/RTC/RtcProvider.ts
1791
- var RtcProvider = /* @__PURE__ */ ((RtcProvider2) => {
1792
- RtcProvider2["trtc"] = "trtc";
1793
- RtcProvider2["agora"] = "agora";
1794
- RtcProvider2["vertc"] = "vertc";
1795
- return RtcProvider2;
1796
- })(RtcProvider || {});
1797
-
1798
2117
  // src/Foundation/RTC/TRTC/RtcEngineManager.ts
1799
2118
  var RtcEngineLease = class {
1800
2119
  /**
@@ -2072,6 +2391,11 @@ var TrtcRtcManager = class {
2072
2391
  */
2073
2392
  __publicField(this, "isCapturing", false);
2074
2393
  __publicField(this, "isAudioCapturing", false);
2394
+ /**
2395
+ * 外部媒体的克隆轨;原始轨道由媒体源管理。
2396
+ */
2397
+ __publicField(this, "externalVideo");
2398
+ __publicField(this, "externalAudio");
2075
2399
  /**
2076
2400
  * 房间状态
2077
2401
  */
@@ -2106,13 +2430,18 @@ var TrtcRtcManager = class {
2106
2430
  async destroy() {
2107
2431
  const lease = this.lease;
2108
2432
  this.lease = void 0;
2109
- this.eventListener = void 0;
2433
+ const externalVideo = this.externalVideo;
2434
+ const externalAudio = this.externalAudio;
2435
+ this.externalVideo = void 0;
2436
+ this.externalAudio = void 0;
2110
2437
  this.isCapturing = false;
2111
2438
  this.isAudioCapturing = false;
2112
2439
  this.isInRoom = false;
2113
2440
  this.removeStatsListeners?.();
2114
2441
  this.removeStatsListeners = void 0;
2115
2442
  if (!lease) {
2443
+ externalVideo?.stop();
2444
+ externalAudio?.stop();
2116
2445
  return;
2117
2446
  }
2118
2447
  try {
@@ -2123,8 +2452,56 @@ var TrtcRtcManager = class {
2123
2452
  await lease.engine.stopLocalVideo();
2124
2453
  } catch {
2125
2454
  }
2455
+ externalVideo?.stop();
2456
+ externalAudio?.stop();
2126
2457
  this.engineManager.release(lease);
2127
2458
  }
2459
+ /**
2460
+ * 准备外部音视频轨但不发布;失败时释放克隆轨,不影响原始媒体源。
2461
+ */
2462
+ async setExternalMediaTracks(tracks) {
2463
+ const engine = this.requireEngine();
2464
+ if (this.isCapturing || this.isAudioCapturing || this.externalVideo) {
2465
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
2466
+ }
2467
+ if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
2468
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media tracks must be live video/audio tracks");
2469
+ }
2470
+ const video = tracks.videoTrack.clone();
2471
+ this.externalVideo = video;
2472
+ let audio;
2473
+ try {
2474
+ audio = tracks.audioTrack?.clone();
2475
+ this.externalAudio = audio;
2476
+ await engine.startLocalVideo({ publish: false, option: { videoTrack: video } });
2477
+ if (this.lease?.engine !== engine) {
2478
+ throw new XmaxError("CANCELLED" /* cancelled */, "External media preparation was cancelled");
2479
+ }
2480
+ if (audio) {
2481
+ await engine.startLocalAudio({
2482
+ publish: false,
2483
+ option: { audioTrack: audio, echoCancellation: false, autoGainControl: false, noiseSuppression: false }
2484
+ });
2485
+ if (this.lease?.engine !== engine) {
2486
+ throw new XmaxError("CANCELLED" /* cancelled */, "External media preparation was cancelled");
2487
+ }
2488
+ }
2489
+ } catch (error) {
2490
+ if (this.lease?.engine === engine) {
2491
+ await engine.stopLocalAudio().catch(() => {
2492
+ });
2493
+ await engine.stopLocalVideo().catch(() => {
2494
+ });
2495
+ }
2496
+ if (this.externalVideo === video) {
2497
+ this.externalVideo = void 0;
2498
+ this.externalAudio = void 0;
2499
+ }
2500
+ video.stop();
2501
+ audio?.stop();
2502
+ throw this.mapError(error);
2503
+ }
2504
+ }
2128
2505
  /**
2129
2506
  * 启动摄像头采集(不发布)。
2130
2507
  *
@@ -2133,7 +2510,7 @@ var TrtcRtcManager = class {
2133
2510
  */
2134
2511
  async startCameraCapture(options) {
2135
2512
  const engine = this.requireEngine();
2136
- if (this.isCapturing) {
2513
+ if (this.isCapturing || this.externalVideo) {
2137
2514
  throw new XmaxError(
2138
2515
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
2139
2516
  "Camera capture is already running"
@@ -2203,6 +2580,9 @@ var TrtcRtcManager = class {
2203
2580
  * 停止摄像头采集;引擎未初始化或采集未启动时不产生效果。
2204
2581
  */
2205
2582
  async stopCameraCapture() {
2583
+ if (this.externalVideo) {
2584
+ return;
2585
+ }
2206
2586
  const engine = this.lease?.engine;
2207
2587
  this.isCapturing = false;
2208
2588
  if (!engine) {
@@ -2220,7 +2600,7 @@ var TrtcRtcManager = class {
2220
2600
  * @throws 引擎未初始化、进房参数无效或进房失败时抛出错误。
2221
2601
  */
2222
2602
  async joinRoom(configuration) {
2223
- if (configuration.provider !== "trtc") {
2603
+ if (configuration.provider !== "trtc" /* trtc */) {
2224
2604
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "TRTC credentials required");
2225
2605
  }
2226
2606
  const engine = this.requireEngine();
@@ -2276,18 +2656,18 @@ var TrtcRtcManager = class {
2276
2656
  * TRTC 只接受单一目标码率,以 `minimumBitrate`(流畅优先档)作为目标码率;
2277
2657
  * 编码策略偏好映射为 TRTC 弱网偏好。
2278
2658
  *
2279
- * @throws 摄像头采集未启动或编码参数配置失败时抛出错误。
2659
+ * @throws 本地视频未准备或编码参数配置失败时抛出错误。
2280
2660
  */
2281
2661
  async configureVideoEncoding(configuration) {
2282
2662
  const engine = this.requireEngine();
2283
- if (!this.isCapturing) {
2663
+ if (!this.isCapturing && !this.externalVideo) {
2284
2664
  throw new XmaxError(
2285
2665
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
2286
- "Camera capture is not running"
2666
+ "Local video is not prepared"
2287
2667
  );
2288
2668
  }
2289
2669
  try {
2290
- const encodingSize = rtcOrientedVideoSize({
2670
+ const encodingSize = this.externalVideo ? configuration : rtcOrientedVideoSize({
2291
2671
  width: configuration.width,
2292
2672
  height: configuration.height
2293
2673
  });
@@ -2309,14 +2689,14 @@ var TrtcRtcManager = class {
2309
2689
  /**
2310
2690
  * 发布本地视频流。
2311
2691
  *
2312
- * @throws 摄像头采集未启动或发布失败时抛出错误。
2692
+ * @throws 本地视频未准备或发布失败时抛出错误。
2313
2693
  */
2314
2694
  async publishLocalVideo() {
2315
2695
  const engine = this.requireEngine();
2316
- if (!this.isCapturing) {
2696
+ if (!this.isCapturing && !this.externalVideo) {
2317
2697
  throw new XmaxError(
2318
2698
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
2319
- "Camera capture is not running"
2699
+ "Local video is not prepared"
2320
2700
  );
2321
2701
  }
2322
2702
  try {
@@ -2330,7 +2710,7 @@ var TrtcRtcManager = class {
2330
2710
  */
2331
2711
  async unpublishLocalVideo() {
2332
2712
  const engine = this.lease?.engine;
2333
- if (!engine || !this.isCapturing) {
2713
+ if (!engine || !this.isCapturing && !this.externalVideo) {
2334
2714
  return;
2335
2715
  }
2336
2716
  try {
@@ -2340,14 +2720,18 @@ var TrtcRtcManager = class {
2340
2720
  }
2341
2721
  }
2342
2722
  /**
2343
- * 发布本地音频流;首次调用时启动麦克风采集。
2723
+ * 发布文件音频或麦克风;无文件音轨时不启动麦克风。
2344
2724
  *
2345
2725
  * @throws 引擎未初始化、麦克风权限被拒绝或发布失败时抛出错误。
2346
2726
  */
2347
2727
  async publishLocalAudio() {
2348
2728
  const engine = this.requireEngine();
2349
2729
  try {
2350
- if (!this.isAudioCapturing) {
2730
+ if (this.externalVideo) {
2731
+ if (this.externalAudio) {
2732
+ await engine.updateLocalAudio({ publish: true });
2733
+ }
2734
+ } else if (!this.isAudioCapturing) {
2351
2735
  await engine.startLocalAudio();
2352
2736
  this.isAudioCapturing = true;
2353
2737
  } else {
@@ -2362,7 +2746,7 @@ var TrtcRtcManager = class {
2362
2746
  */
2363
2747
  async unpublishLocalAudio() {
2364
2748
  const engine = this.lease?.engine;
2365
- if (!engine || !this.isAudioCapturing) {
2749
+ if (!engine || !this.isAudioCapturing && !this.externalAudio) {
2366
2750
  return;
2367
2751
  }
2368
2752
  try {
@@ -2540,19 +2924,19 @@ var TrtcRtcManager = class {
2540
2924
  source.off(RTC_EVENT.networkQuality, onNetworkQuality);
2541
2925
  };
2542
2926
  source.on(RTC_EVENT.remoteVideoAvailable, (event) => {
2543
- if (event.streamType !== STREAM_TYPE_MAIN) {
2927
+ if (this.lease?.engine !== engine || event.streamType !== STREAM_TYPE_MAIN) {
2544
2928
  return;
2545
2929
  }
2546
2930
  this.eventListener?.onRemoteVideoPublished(event.userId, true);
2547
2931
  });
2548
2932
  source.on(RTC_EVENT.remoteVideoUnavailable, (event) => {
2549
- if (event.streamType !== STREAM_TYPE_MAIN) {
2933
+ if (this.lease?.engine !== engine || event.streamType !== STREAM_TYPE_MAIN) {
2550
2934
  return;
2551
2935
  }
2552
2936
  this.eventListener?.onRemoteVideoPublished(event.userId, false);
2553
2937
  });
2554
2938
  source.on(RTC_EVENT.customMessage, (event) => {
2555
- if (event.cmdId !== CUSTOM_MESSAGE_CMD_ID) {
2939
+ if (this.lease?.engine !== engine || event.cmdId !== CUSTOM_MESSAGE_CMD_ID) {
2556
2940
  return;
2557
2941
  }
2558
2942
  let message;
@@ -2645,6 +3029,11 @@ var VeRtcManager = class {
2645
3029
  __publicField(this, "captureVersion", 0);
2646
3030
  __publicField(this, "camera");
2647
3031
  __publicField(this, "microphone", false);
3032
+ /**
3033
+ * 外部媒体的克隆轨;原始轨道由媒体源管理。
3034
+ */
3035
+ __publicField(this, "externalVideo");
3036
+ __publicField(this, "externalAudio");
2648
3037
  __publicField(this, "connection");
2649
3038
  __publicField(this, "joined", false);
2650
3039
  __publicField(this, "roomVersion", 0);
@@ -2700,18 +3089,76 @@ var VeRtcManager = class {
2700
3089
  this.captureVersion++;
2701
3090
  const engine = this.engine;
2702
3091
  const sdk = this.sdk;
2703
- this.listener = void 0;
2704
3092
  this.clearRoom();
2705
3093
  this.engine = void 0;
2706
3094
  this.sdk = void 0;
2707
3095
  this.camera = void 0;
2708
3096
  this.microphone = false;
2709
3097
  engine?.removeAllListeners();
2710
- if (engine && sdk) sdk.default.destroyEngine(engine);
3098
+ try {
3099
+ if (engine && sdk) sdk.default.destroyEngine(engine);
3100
+ } finally {
3101
+ this.externalVideo?.stop();
3102
+ this.externalAudio?.stop();
3103
+ this.externalVideo = void 0;
3104
+ this.externalAudio = void 0;
3105
+ }
3106
+ }
3107
+ /**
3108
+ * 将克隆的文件音视频轨绑定到主流,不采集设备、不自动发布。
3109
+ */
3110
+ async setExternalMediaTracks(tracks) {
3111
+ const engine = this.requireEngine();
3112
+ const sdk = this.sdk;
3113
+ if (this.camera || this.microphone || this.externalVideo) {
3114
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
3115
+ }
3116
+ if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
3117
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media tracks must be live video/audio tracks");
3118
+ }
3119
+ const video = tracks.videoTrack.clone();
3120
+ this.externalVideo = video;
3121
+ let audio;
3122
+ const index = sdk.StreamIndex.STREAM_INDEX_MAIN;
3123
+ const ensureCurrent = () => {
3124
+ if (this.engine !== engine || this.externalVideo !== video) {
3125
+ throw this.cancelled();
3126
+ }
3127
+ };
3128
+ try {
3129
+ audio = tracks.audioTrack?.clone();
3130
+ this.externalAudio = audio;
3131
+ await engine.setVideoSourceType(index, sdk.VideoSourceType.VIDEO_SOURCE_TYPE_EXTERNAL);
3132
+ ensureCurrent();
3133
+ await engine.setExternalVideoTrack(index, video);
3134
+ ensureCurrent();
3135
+ if (audio) {
3136
+ await engine.setAudioSourceType(index, sdk.AudioSourceType.AUDIO_SOURCE_TYPE_EXTERNAL);
3137
+ ensureCurrent();
3138
+ await engine.setExternalAudioTrack(index, audio);
3139
+ ensureCurrent();
3140
+ }
3141
+ } catch (error) {
3142
+ if (this.engine === engine) {
3143
+ await engine.setAudioSourceType(index, sdk.AudioSourceType.AUDIO_SOURCE_TYPE_INTERNAL).catch(() => {
3144
+ });
3145
+ await engine.setVideoSourceType(index, sdk.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL).catch(() => {
3146
+ });
3147
+ }
3148
+ if (this.externalVideo === video) {
3149
+ this.externalVideo = void 0;
3150
+ this.externalAudio = void 0;
3151
+ }
3152
+ video.stop();
3153
+ audio?.stop();
3154
+ throw this.mapError(error);
3155
+ }
2711
3156
  }
2712
3157
  async startCameraCapture(options) {
2713
3158
  const engine = this.requireEngine();
2714
- if (this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is already running");
3159
+ if (this.camera || this.externalVideo) {
3160
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video is already prepared");
3161
+ }
2715
3162
  const version = ++this.captureVersion;
2716
3163
  await this.run(() => engine.setVideoEncoderConfig({
2717
3164
  ...rtcOrientedVideoSize({ width: options.width, height: options.height }),
@@ -2751,15 +3198,20 @@ var VeRtcManager = class {
2751
3198
  return track;
2752
3199
  }
2753
3200
  async stopCameraCapture() {
3201
+ if (this.externalVideo) {
3202
+ return;
3203
+ }
2754
3204
  this.captureVersion++;
2755
3205
  this.camera = void 0;
2756
3206
  if (this.engine) await this.run(() => this.engine.stopVideoCapture());
2757
3207
  }
2758
3208
  async configureVideoEncoding(config) {
2759
3209
  const engine = this.requireEngine();
2760
- this.requireCamera();
3210
+ this.requireLocalVideo();
3211
+ const size = this.externalVideo ? config : rtcOrientedVideoSize(config);
2761
3212
  await this.run(() => engine.setVideoEncoderConfig({
2762
- ...rtcOrientedVideoSize({ width: config.width, height: config.height }),
3213
+ width: size.width,
3214
+ height: size.height,
2763
3215
  frameRate: config.frameRate,
2764
3216
  maxKbps: config.maximumBitrate,
2765
3217
  contentHint: config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail",
@@ -2772,7 +3224,7 @@ var VeRtcManager = class {
2772
3224
  await this.recovery?.catch(() => {
2773
3225
  });
2774
3226
  const engine = this.requireEngine();
2775
- if (config.provider !== "vertc" || config.appID !== VERTC_APP_ID || this.connection) {
3227
+ if (config.provider !== "vertc" /* vertc */ || config.appID !== VERTC_APP_ID || this.connection) {
2776
3228
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "VeRTC credentials for the configured AppID and an idle room are required");
2777
3229
  }
2778
3230
  this.clearRoom();
@@ -2819,7 +3271,7 @@ var VeRtcManager = class {
2819
3271
  const engine = this.requireEngine();
2820
3272
  const current = this.connection;
2821
3273
  if (signal?.aborted) throw this.cancelled();
2822
- if (!current || config.provider !== "vertc" || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
3274
+ if (!current || config.provider !== "vertc" /* vertc */ || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
2823
3275
  throw new XmaxError("SESSION_ERROR" /* sessionError */, "VeRTC session binding changed");
2824
3276
  }
2825
3277
  const version = this.roomVersion;
@@ -2861,7 +3313,7 @@ var VeRtcManager = class {
2861
3313
  throw this.cancelled();
2862
3314
  }
2863
3315
  }
2864
- if ((this.tokenExpired || this.publishPrivilegeExpired) && this.videoRequested && this.camera) {
3316
+ if ((this.tokenExpired || this.publishPrivilegeExpired) && this.videoRequested && (this.camera || this.externalVideo)) {
2865
3317
  await this.run(() => engine.publishStream(this.sdk.MediaType.VIDEO));
2866
3318
  this.ensureCurrent(engine, version);
2867
3319
  if (!this.videoRequested) {
@@ -2869,7 +3321,7 @@ var VeRtcManager = class {
2869
3321
  this.ensureCurrent(engine, version);
2870
3322
  }
2871
3323
  }
2872
- if ((this.tokenExpired || this.publishPrivilegeExpired) && this.audioRequested && this.microphone) {
3324
+ if ((this.tokenExpired || this.publishPrivilegeExpired) && this.audioRequested && (this.microphone || this.externalAudio)) {
2873
3325
  await this.run(() => engine.publishStream(this.sdk.MediaType.AUDIO));
2874
3326
  this.ensureCurrent(engine, version);
2875
3327
  if (!this.audioRequested) {
@@ -2893,7 +3345,7 @@ var VeRtcManager = class {
2893
3345
  }
2894
3346
  async publishLocalVideo() {
2895
3347
  const engine = this.requireEngine();
2896
- this.requireCamera();
3348
+ this.requireLocalVideo();
2897
3349
  this.videoRequested = true;
2898
3350
  const version = this.roomVersion;
2899
3351
  await this.recovery;
@@ -2907,12 +3359,15 @@ var VeRtcManager = class {
2907
3359
  }
2908
3360
  async publishLocalAudio() {
2909
3361
  const engine = this.requireEngine();
3362
+ if (this.externalVideo && !this.externalAudio) {
3363
+ return;
3364
+ }
2910
3365
  this.audioRequested = true;
2911
3366
  const version = this.roomVersion;
2912
3367
  await this.recovery;
2913
3368
  this.ensureCurrent(engine, version);
2914
3369
  try {
2915
- if (!this.microphone) {
3370
+ if (!this.externalVideo && !this.microphone) {
2916
3371
  await engine.startAudioCapture();
2917
3372
  if (!this.isCurrent(engine, version)) {
2918
3373
  await engine.stopAudioCapture().catch(() => {
@@ -2924,7 +3379,7 @@ var VeRtcManager = class {
2924
3379
  if (!this.audioRequested) throw this.cancelled();
2925
3380
  await engine.publishStream(this.sdk.MediaType.AUDIO);
2926
3381
  } catch (error) {
2927
- throw this.mapError(error, "MICROPHONE_PERMISSION_DENIED" /* microphonePermissionDenied */);
3382
+ throw this.mapError(error, this.externalVideo ? "RTC_ERROR" /* rtcError */ : "MICROPHONE_PERMISSION_DENIED" /* microphonePermissionDenied */);
2928
3383
  }
2929
3384
  }
2930
3385
  async unpublishLocalAudio() {
@@ -3189,6 +3644,15 @@ var VeRtcManager = class {
3189
3644
  if (!this.engine) throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC is not initialized");
3190
3645
  return this.engine;
3191
3646
  }
3647
+ /**
3648
+ * 获取已准备的文件或摄像头视频轨。
3649
+ */
3650
+ requireLocalVideo() {
3651
+ if (this.externalVideo) {
3652
+ return this.externalVideo;
3653
+ }
3654
+ return this.requireCamera();
3655
+ }
3192
3656
  requireCamera() {
3193
3657
  if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
3194
3658
  return this.camera;
@@ -3215,12 +3679,14 @@ var VeRtcManager = class {
3215
3679
  // src/Foundation/RTC/RtcFactory.ts
3216
3680
  function createRtcManager(provider, environment) {
3217
3681
  switch (provider) {
3682
+ case "trtc" /* trtc */:
3683
+ return new TrtcRtcManager();
3218
3684
  case "agora" /* agora */:
3219
3685
  return new AgoraRtcManager({ environment });
3220
3686
  case "vertc" /* vertc */:
3221
3687
  return new VeRtcManager();
3222
3688
  default:
3223
- return new TrtcRtcManager();
3689
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported RTC provider");
3224
3690
  }
3225
3691
  }
3226
3692
 
@@ -3715,91 +4181,548 @@ var _CameraController = class _CameraController {
3715
4181
  __publicField(_CameraController, "localVideoTrackID", "video0");
3716
4182
  var CameraController = _CameraController;
3717
4183
 
3718
- // src/Service/Realtime/RealtimeSession.ts
3719
- var RealtimeSession = class {
3720
- /**
3721
- * 创建实时会话。
3722
- *
3723
- * @param init.id 会话标识(sessionUid)。
3724
- * @param init.userID 业务用户标识。
3725
- * @param init.status 会话状态。
3726
- * @param init.connection RTC 连接参数。
3727
- * @param init.closeReason 会话关闭原因。
3728
- */
3729
- constructor(init) {
3730
- /**
3731
- * 会话信息
3732
- */
3733
- /**
3734
- * 会话标识(sessionUid)。
3735
- */
3736
- __publicField(this, "id");
3737
- /**
3738
- * 业务用户标识。
3739
- */
3740
- __publicField(this, "userID");
3741
- /**
3742
- * 运行状态
3743
- */
3744
- /**
3745
- * 会话状态(如 ACTIVE);响应未携带时为空。
3746
- */
3747
- __publicField(this, "status");
3748
- /**
3749
- * 会话关闭原因。
3750
- */
3751
- __publicField(this, "closeReason");
4184
+ // src/Service/Realtime/RealtimeReferenceVideo.ts
4185
+ var RealtimeVideoSampleMethod = /* @__PURE__ */ ((RealtimeVideoSampleMethod2) => {
4186
+ RealtimeVideoSampleMethod2["time"] = "time";
4187
+ RealtimeVideoSampleMethod2["fps"] = "fps";
4188
+ return RealtimeVideoSampleMethod2;
4189
+ })(RealtimeVideoSampleMethod || {});
4190
+
4191
+ // src/Media/Video/NetworkVideoController.ts
4192
+ var NetworkVideoController = class {
4193
+ constructor() {
3752
4194
  /**
3753
- * RTC 连接
4195
+ * 当前视频源与完成通知
3754
4196
  */
4197
+ __publicField(this, "currentTrack");
4198
+ __publicField(this, "reference");
4199
+ __publicField(this, "onFinish");
3755
4200
  /**
3756
- * RTC 连接参数;心跳等响应未携带完整参数时为空。
4201
+ * 预览进度与视图资源
3757
4202
  */
3758
- __publicField(this, "connection");
3759
- this.id = init.id;
3760
- this.userID = init.userID;
3761
- this.status = init.status;
3762
- this.connection = init.connection;
3763
- this.closeReason = init.closeReason;
4203
+ __publicField(this, "position", 0);
4204
+ __publicField(this, "ended", false);
4205
+ __publicField(this, "bindings", /* @__PURE__ */ new Map());
3764
4206
  }
3765
- };
3766
-
3767
- // src/Foundation/RTC/RoomJoinConfiguration.ts
3768
- var RoomJoinConfiguration = class {
3769
4207
  /**
3770
- * 创建房间加入参数。
3771
- *
3772
- * @param init.roomID TRTC 房间号。
3773
- * @param init.userID TRTC 登录身份。
3774
- * @param init.sdkAppID TRTC SDKAppID(字符串形式)。
3775
- * @param init.userSig TRTC 登录签名。
3776
- * @param init.privateMapKey TRTC 进房鉴权字段。
4208
+ * 创建网络视频源,校验地址、采样方式和模型支持的尺寸;不请求视频下载。
3777
4209
  */
3778
- constructor(init) {
3779
- /**
3780
- * RTC 提供方。
3781
- */
3782
- __publicField(this, "provider", "trtc");
3783
- /**
3784
- * 进房参数
3785
- */
3786
- /**
3787
- * TRTC 房间号(字符串房间号)。
3788
- */
3789
- __publicField(this, "roomID");
3790
- /**
3791
- * TRTC 登录身份(`rtc_user_id`)。
3792
- */
3793
- __publicField(this, "userID");
3794
- /**
3795
- * TRTC SDKAppID(字符串形式,进房时转换为数值)。
3796
- */
3797
- __publicField(this, "sdkAppID");
3798
- /**
3799
- * TRTC 登录签名。
3800
- */
3801
- __publicField(this, "userSig");
3802
- /**
4210
+ create(options, mediaService) {
4211
+ let url;
4212
+ try {
4213
+ url = new URL(options.url);
4214
+ } catch {
4215
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an absolute HTTP or HTTPS URL");
4216
+ }
4217
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password) {
4218
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an HTTP or HTTPS URL without embedded credentials");
4219
+ }
4220
+ const sampleMethod = options.sampleMethod ?? "time" /* time */;
4221
+ if (!Object.values(RealtimeVideoSampleMethod).includes(sampleMethod)) {
4222
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported video sample method");
4223
+ }
4224
+ if (this.currentTrack) {
4225
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current network video stream before creating another one");
4226
+ }
4227
+ options.videoFormat.validate();
4228
+ const size = mediaService.resolveModelInputSize(options.videoFormat);
4229
+ const track = new RealtimeVideoTrack({
4230
+ id: "network-video",
4231
+ videoFormat: options.videoFormat.resized(size.width, size.height)
4232
+ });
4233
+ this.currentTrack = track;
4234
+ this.reference = Object.freeze({ path: options.url.trim(), sampleMethod });
4235
+ this.onFinish = options.onFinish;
4236
+ VideoRenderRegistry.register(track, {
4237
+ attachHandler: (view) => this.attach(view),
4238
+ detachHandler: (view) => this.detach(view)
4239
+ });
4240
+ return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
4241
+ }
4242
+ /**
4243
+ * 释放全部预览绑定及源信息,不影响其他媒体源。
4244
+ */
4245
+ stop() {
4246
+ for (const view of this.bindings.keys()) {
4247
+ this.detach(view);
4248
+ }
4249
+ if (this.currentTrack) {
4250
+ VideoRenderRegistry.unregister(this.currentTrack);
4251
+ }
4252
+ this.currentTrack = void 0;
4253
+ this.reference = void 0;
4254
+ this.onFinish = void 0;
4255
+ this.position = 0;
4256
+ this.ended = false;
4257
+ }
4258
+ /**
4259
+ * 绑定播放器,恢复预览位置;本地播放错误仅记录,不中断服务端生成。
4260
+ */
4261
+ attach(view) {
4262
+ if (!this.reference || !view.videoElement || !view.setVideoURL) {
4263
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "This render target does not support network video preview");
4264
+ }
4265
+ this.detach(view);
4266
+ const video = view.videoElement;
4267
+ video.autoplay = !this.ended;
4268
+ const restore = () => {
4269
+ if (this.position > 0) {
4270
+ video.currentTime = Math.min(this.position, Math.max(0, video.duration - 1e-3) || this.position);
4271
+ }
4272
+ if (this.ended) {
4273
+ video.pause();
4274
+ }
4275
+ };
4276
+ const fail = () => {
4277
+ XmaxLogger.render.warning(() => "\u7F51\u7EDC\u89C6\u9891\u9884\u89C8\u5931\u8D25 (Network Video Preview Failed)");
4278
+ };
4279
+ video.addEventListener("loadedmetadata", restore);
4280
+ video.addEventListener("error", fail);
4281
+ this.bindings.set(view, () => {
4282
+ this.position = video.currentTime;
4283
+ this.ended = video.ended || this.ended;
4284
+ video.removeEventListener("loadedmetadata", restore);
4285
+ video.removeEventListener("error", fail);
4286
+ view.setVideoURL?.(null);
4287
+ });
4288
+ view.isMirrored = false;
4289
+ view.setVideoURL(this.reference.path);
4290
+ }
4291
+ /**
4292
+ * 保存预览进度并释放该视图的地址和事件监听。
4293
+ */
4294
+ detach(view) {
4295
+ this.bindings.get(view)?.();
4296
+ this.bindings.delete(view);
4297
+ }
4298
+ };
4299
+
4300
+ // src/Media/Video/LocalVideoController.ts
4301
+ var LocalVideoController = class {
4302
+ /**
4303
+ * 保存媒体错误处理器,不创建浏览器资源。
4304
+ */
4305
+ constructor(onError = () => {
4306
+ }) {
4307
+ /**
4308
+ * 当前文件资源与运行期错误通知
4309
+ */
4310
+ __publicField(this, "source");
4311
+ __publicField(this, "onError");
4312
+ this.onError = onError;
4313
+ }
4314
+ /**
4315
+ * 是否持有文件资源,包含尚未完成准备的播放器。
4316
+ */
4317
+ get isActive() {
4318
+ return this.source !== void 0;
4319
+ }
4320
+ /**
4321
+ * 准备完成的本地视频轨。
4322
+ */
4323
+ get currentTrack() {
4324
+ return this.source?.track;
4325
+ }
4326
+ /**
4327
+ * 文件音频输出;禁用音频时为空,无音轨文件的 Web Audio 输出为静音。
4328
+ */
4329
+ get audioTrack() {
4330
+ return this.source?.audioDestination?.stream.getAudioTracks()[0];
4331
+ }
4332
+ /**
4333
+ * 校验文件和格式,准备首帧与音频输出;应从用户点击事件直接调用以解锁播放。
4334
+ */
4335
+ async create(options, mediaService, signal) {
4336
+ if (this.source) {
4337
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local video before creating another one");
4338
+ }
4339
+ if (!(options.file instanceof Blob) || options.file.size === 0) {
4340
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video requires a non-empty File or Blob");
4341
+ }
4342
+ const format = this.resolveVideoFormat(options.videoFormat, mediaService);
4343
+ if (signal.aborted) {
4344
+ throw this.cancelled();
4345
+ }
4346
+ if (typeof document === "undefined") {
4347
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Local video requires a browser");
4348
+ }
4349
+ const video = document.createElement("video");
4350
+ const canvas = document.createElement("canvas");
4351
+ const context = canvas.getContext("2d");
4352
+ if (!context || typeof canvas.captureStream !== "function") {
4353
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas video capture is unavailable");
4354
+ }
4355
+ canvas.width = format.width;
4356
+ canvas.height = format.height;
4357
+ const source = {
4358
+ video,
4359
+ canvas,
4360
+ context,
4361
+ url: URL.createObjectURL(options.file),
4362
+ abort: new AbortController(),
4363
+ targets: /* @__PURE__ */ new Set()
4364
+ };
4365
+ this.source = source;
4366
+ try {
4367
+ video.playsInline = true;
4368
+ video.loop = options.loop ?? true;
4369
+ video.preload = "auto";
4370
+ video.volume = 1;
4371
+ video.muted = options.includeAudio === false;
4372
+ if (options.includeAudio !== false) {
4373
+ if (typeof AudioContext === "undefined") {
4374
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Web Audio is unavailable; disable file audio or use a supported browser");
4375
+ }
4376
+ const audio = new AudioContext();
4377
+ source.audioContext = audio;
4378
+ source.audioSource = audio.createMediaElementSource(video);
4379
+ source.audioDestination = audio.createMediaStreamDestination();
4380
+ source.audioSource.connect(source.audioDestination);
4381
+ }
4382
+ video.src = source.url;
4383
+ const resume = source.audioContext?.resume();
4384
+ const playing = this.play(source, signal);
4385
+ await this.wait(source, Promise.all([resume, playing]), signal);
4386
+ video.pause();
4387
+ if (!video.videoWidth || !video.videoHeight || !Number.isFinite(video.duration) || video.duration <= 0) {
4388
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "The file has no playable finite video stream");
4389
+ }
4390
+ await this.rewind(source, signal);
4391
+ this.draw(source);
4392
+ source.stream = canvas.captureStream(format.fps);
4393
+ const nativeTrack = source.stream.getVideoTracks()[0];
4394
+ if (!nativeTrack) {
4395
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas did not provide a video track");
4396
+ }
4397
+ const track = new RealtimeVideoTrack({ id: "local-video", videoFormat: format });
4398
+ track.mediaStreamTrack = nativeTrack;
4399
+ source.track = track;
4400
+ this.draw(source);
4401
+ source.drawTimer = setInterval(() => {
4402
+ if (this.source !== source) {
4403
+ return;
4404
+ }
4405
+ try {
4406
+ this.draw(source);
4407
+ } catch {
4408
+ this.handleError(source);
4409
+ }
4410
+ }, 1e3 / format.fps);
4411
+ source.onError = () => this.handleError(source);
4412
+ video.addEventListener("error", source.onError);
4413
+ VideoRenderRegistry.register(track, {
4414
+ attachHandler: (view) => {
4415
+ view.isMirrored = false;
4416
+ view.setMediaStream(source.stream);
4417
+ source.targets.add(view);
4418
+ },
4419
+ detachHandler: (view) => {
4420
+ source.targets.delete(view);
4421
+ view.setMediaStream(null);
4422
+ }
4423
+ });
4424
+ return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
4425
+ } catch (error) {
4426
+ await this.stop();
4427
+ throw XmaxError.from(error);
4428
+ }
4429
+ }
4430
+ /**
4431
+ * 开始信令发送成功后,从文件开头同步启动音视频;不等待远端首帧。
4432
+ */
4433
+ async start(signal) {
4434
+ const source = this.source;
4435
+ if (!source?.track) {
4436
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video is not prepared");
4437
+ }
4438
+ if (signal.aborted) {
4439
+ throw this.cancelled();
4440
+ }
4441
+ try {
4442
+ await this.rewind(source, signal);
4443
+ await this.wait(source, Promise.all([source.audioContext?.resume(), this.play(source, signal)]), signal);
4444
+ } catch (error) {
4445
+ source.video.pause();
4446
+ throw error;
4447
+ }
4448
+ }
4449
+ /**
4450
+ * 暂停文件音视频,保留当前预览;重新开始生成时回到文件开头。
4451
+ */
4452
+ pause() {
4453
+ this.source?.video.pause();
4454
+ }
4455
+ /**
4456
+ * 释放播放器、预览、定时器、轨道、音频上下文和对象 URL。
4457
+ */
4458
+ async stop() {
4459
+ const source = this.source;
4460
+ this.source = void 0;
4461
+ if (!source) {
4462
+ return;
4463
+ }
4464
+ source.abort.abort();
4465
+ clearInterval(source.drawTimer);
4466
+ if (source.onError) {
4467
+ source.video.removeEventListener("error", source.onError);
4468
+ }
4469
+ if (source.track) {
4470
+ VideoRenderRegistry.unregister(source.track);
4471
+ source.track.mediaStreamTrack = void 0;
4472
+ }
4473
+ for (const view of source.targets) {
4474
+ try {
4475
+ view.setMediaStream(null);
4476
+ } catch {
4477
+ }
4478
+ }
4479
+ source.targets.clear();
4480
+ source.video.pause();
4481
+ source.video.removeAttribute("src");
4482
+ source.video.load();
4483
+ source.stream?.getTracks().forEach((track) => track.stop());
4484
+ source.audioDestination?.stream.getTracks().forEach((track) => track.stop());
4485
+ source.audioSource?.disconnect();
4486
+ source.audioDestination?.disconnect();
4487
+ URL.revokeObjectURL(source.url);
4488
+ await source.audioContext?.close().catch(() => {
4489
+ });
4490
+ }
4491
+ /**
4492
+ * 播放被浏览器拒绝时给出可操作错误;关闭后的迟到播放立即暂停。
4493
+ */
4494
+ play(source, signal) {
4495
+ return source.video.play().then(() => {
4496
+ if (this.source !== source || source.abort.signal.aborted || signal.aborted) {
4497
+ source.video.pause();
4498
+ throw this.cancelled();
4499
+ }
4500
+ }).catch((error) => {
4501
+ if (error instanceof XmaxError) {
4502
+ throw error;
4503
+ }
4504
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Cannot play local video; call from a user click and use a browser-supported video codec");
4505
+ });
4506
+ }
4507
+ /**
4508
+ * 等待 seek 完成,确保重新生成从首帧开始,而非使用旧缓存画面。
4509
+ */
4510
+ async rewind(source, signal) {
4511
+ if (source.video.currentTime === 0) {
4512
+ return;
4513
+ }
4514
+ let seeked;
4515
+ const ready = new Promise((resolve) => {
4516
+ seeked = resolve;
4517
+ });
4518
+ source.video.addEventListener("seeked", seeked, { once: true });
4519
+ try {
4520
+ source.video.currentTime = 0;
4521
+ await this.wait(source, ready, signal);
4522
+ } finally {
4523
+ source.video.removeEventListener("seeked", seeked);
4524
+ }
4525
+ }
4526
+ /**
4527
+ * 给浏览器准备和播放操作设置 10 秒上限,同时响应关闭、取消和解码错误。
4528
+ */
4529
+ wait(source, operation, signal) {
4530
+ return new Promise((resolve, reject) => {
4531
+ const finish = (error) => {
4532
+ clearTimeout(timer);
4533
+ signal.removeEventListener("abort", abort);
4534
+ source.abort.signal.removeEventListener("abort", abort);
4535
+ source.video.removeEventListener("error", failed);
4536
+ if (error) {
4537
+ reject(error);
4538
+ } else {
4539
+ resolve();
4540
+ }
4541
+ };
4542
+ const abort = () => finish(this.cancelled());
4543
+ const failed = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "The browser cannot decode this video file"));
4544
+ const timer = setTimeout(() => finish(new XmaxError("TIMEOUT" /* timeout */, "Local video preparation timed out; start from a user click and check the video codec")), 1e4);
4545
+ signal.addEventListener("abort", abort, { once: true });
4546
+ source.abort.signal.addEventListener("abort", abort, { once: true });
4547
+ source.video.addEventListener("error", failed, { once: true });
4548
+ operation.then(() => finish(), finish);
4549
+ if (signal.aborted || source.abort.signal.aborted) {
4550
+ abort();
4551
+ }
4552
+ });
4553
+ }
4554
+ /**
4555
+ * 等比缩放并居中补黑边,保留完整画面,不按设备方向转置文件尺寸。
4556
+ */
4557
+ draw(source) {
4558
+ const { video, canvas, context } = source;
4559
+ if (video.readyState < 2) {
4560
+ return;
4561
+ }
4562
+ const scale = Math.min(canvas.width / video.videoWidth, canvas.height / video.videoHeight);
4563
+ const width = video.videoWidth * scale;
4564
+ const height = video.videoHeight * scale;
4565
+ context.fillStyle = "black";
4566
+ context.fillRect(0, 0, canvas.width, canvas.height);
4567
+ context.drawImage(video, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
4568
+ }
4569
+ /**
4570
+ * 仅为本地文件选择最接近宽高比的模型档位;比例距离相同时优先横屏。
4571
+ * 无固定档位时保留公共尺寸解析规则,帧率和显式编码配置不变。
4572
+ */
4573
+ resolveVideoFormat(format, mediaService) {
4574
+ format.validate();
4575
+ const buckets = resolutionBuckets(mediaService.model);
4576
+ let target = buckets.find((size2) => size2.width === format.width && size2.height === format.height);
4577
+ if (!target && buckets.length > 0) {
4578
+ const ratio = format.width / format.height;
4579
+ let bestDistance = Number.POSITIVE_INFINITY;
4580
+ for (const bucket of buckets) {
4581
+ const distance = Math.abs(Math.log(bucket.width / bucket.height / ratio));
4582
+ const tied = Math.abs(distance - bestDistance) < 1e-9;
4583
+ if (distance < bestDistance - 1e-9 || tied && bucket.width >= bucket.height && target && target.width < target.height) {
4584
+ target = bucket;
4585
+ bestDistance = distance;
4586
+ }
4587
+ }
4588
+ }
4589
+ const size = mediaService.resolveModelInputSize(target ?? format);
4590
+ return format.resized(size.width, size.height);
4591
+ }
4592
+ /**
4593
+ * 解码或绘制失败时停止媒体推进,并报告一次运行期错误。
4594
+ */
4595
+ handleError(source) {
4596
+ if (this.source !== source || source.drawTimer === void 0) {
4597
+ return;
4598
+ }
4599
+ clearInterval(source.drawTimer);
4600
+ source.drawTimer = void 0;
4601
+ source.video.pause();
4602
+ XmaxLogger.realtime.warning(() => "\u672C\u5730\u89C6\u9891\u89E3\u7801\u5931\u8D25 (Local Video Decode Failed)");
4603
+ this.onError(new XmaxError("MEDIA_ERROR" /* mediaError */, "Local video decoding failed"));
4604
+ }
4605
+ /**
4606
+ * 构造统一的媒体操作取消错误。
4607
+ */
4608
+ cancelled() {
4609
+ return new XmaxError("CANCELLED" /* cancelled */, "Local video operation was cancelled");
4610
+ }
4611
+ };
4612
+
4613
+ // src/Service/Realtime/RealtimeContext.ts
4614
+ var RealtimeContext = class {
4615
+ /**
4616
+ * 创建实时生成条件并规范化文本和参考路径。
4617
+ */
4618
+ constructor(init) {
4619
+ /**
4620
+ * 服务端直接读取的网络视频。
4621
+ */
4622
+ __publicField(this, "referenceVideo");
4623
+ /**
4624
+ * 生成条件
4625
+ */
4626
+ /**
4627
+ * 实时生成使用的文本条件。
4628
+ */
4629
+ __publicField(this, "prompt");
4630
+ /**
4631
+ * 已上传参考图片的可选远端路径。
4632
+ */
4633
+ __publicField(this, "referencePath");
4634
+ this.referenceVideo = init.referenceVideo && Object.freeze({ ...init.referenceVideo });
4635
+ this.prompt = init.prompt.trim();
4636
+ const normalizedReferencePath = init.referencePath?.trim();
4637
+ this.referencePath = normalizedReferencePath || void 0;
4638
+ }
4639
+ };
4640
+
4641
+ // src/Service/Realtime/RealtimeSession.ts
4642
+ var RealtimeSession = class {
4643
+ /**
4644
+ * 创建实时会话。
4645
+ *
4646
+ * @param init.id 会话标识(sessionUid)。
4647
+ * @param init.userID 业务用户标识。
4648
+ * @param init.status 会话状态。
4649
+ * @param init.connection RTC 连接参数。
4650
+ * @param init.closeReason 会话关闭原因。
4651
+ */
4652
+ constructor(init) {
4653
+ /**
4654
+ * 会话信息
4655
+ */
4656
+ /**
4657
+ * 会话标识(sessionUid)。
4658
+ */
4659
+ __publicField(this, "id");
4660
+ /**
4661
+ * 业务用户标识。
4662
+ */
4663
+ __publicField(this, "userID");
4664
+ /**
4665
+ * 运行状态
4666
+ */
4667
+ /**
4668
+ * 会话状态(如 ACTIVE);响应未携带时为空。
4669
+ */
4670
+ __publicField(this, "status");
4671
+ /**
4672
+ * 会话关闭原因。
4673
+ */
4674
+ __publicField(this, "closeReason");
4675
+ /**
4676
+ * RTC 连接
4677
+ */
4678
+ /**
4679
+ * RTC 连接参数;心跳等响应未携带完整参数时为空。
4680
+ */
4681
+ __publicField(this, "connection");
4682
+ this.id = init.id;
4683
+ this.userID = init.userID;
4684
+ this.status = init.status;
4685
+ this.connection = init.connection;
4686
+ this.closeReason = init.closeReason;
4687
+ }
4688
+ };
4689
+
4690
+ // src/Foundation/RTC/RoomJoinConfiguration.ts
4691
+ var RoomJoinConfiguration = class {
4692
+ /**
4693
+ * 创建房间加入参数。
4694
+ *
4695
+ * @param init.roomID TRTC 房间号。
4696
+ * @param init.userID TRTC 登录身份。
4697
+ * @param init.sdkAppID TRTC SDKAppID(字符串形式)。
4698
+ * @param init.userSig TRTC 登录签名。
4699
+ * @param init.privateMapKey TRTC 进房鉴权字段。
4700
+ */
4701
+ constructor(init) {
4702
+ /**
4703
+ * RTC 提供方。
4704
+ */
4705
+ __publicField(this, "provider", "trtc" /* trtc */);
4706
+ /**
4707
+ * 进房参数
4708
+ */
4709
+ /**
4710
+ * TRTC 房间号(字符串房间号)。
4711
+ */
4712
+ __publicField(this, "roomID");
4713
+ /**
4714
+ * TRTC 登录身份(`rtc_user_id`)。
4715
+ */
4716
+ __publicField(this, "userID");
4717
+ /**
4718
+ * TRTC SDKAppID(字符串形式,进房时转换为数值)。
4719
+ */
4720
+ __publicField(this, "sdkAppID");
4721
+ /**
4722
+ * TRTC 登录签名。
4723
+ */
4724
+ __publicField(this, "userSig");
4725
+ /**
3803
4726
  * TRTC 进房鉴权字段(字符串房间号对应的 privateMapKey)。
3804
4727
  */
3805
4728
  __publicField(this, "privateMapKey");
@@ -3872,10 +4795,10 @@ var RealtimeSessionConnection = class {
3872
4795
  }
3873
4796
  };
3874
4797
  function toRoomJoinConfiguration(connection) {
3875
- return connection.provider === "trtc" ? new RoomJoinConfiguration(connection) : connection;
4798
+ return connection.provider === "trtc" /* trtc */ ? new RoomJoinConfiguration(connection) : connection;
3876
4799
  }
3877
4800
  function connectionAppID(connection) {
3878
- return connection.provider === "trtc" ? connection.sdkAppID : connection.appID;
4801
+ return connection.provider === "trtc" /* trtc */ ? connection.sdkAppID : connection.appID;
3879
4802
  }
3880
4803
 
3881
4804
  // src/Service/Realtime/RealtimeSessionService.ts
@@ -4090,7 +5013,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
4090
5013
  return void 0;
4091
5014
  }
4092
5015
  return new RealtimeSessionConnection({
4093
- provider: "trtc",
5016
+ provider: "trtc" /* trtc */,
4094
5017
  roomID,
4095
5018
  sdkAppID,
4096
5019
  userID,
@@ -4234,9 +5157,21 @@ var RoomEvent = class _RoomEvent {
4234
5157
  static start(options) {
4235
5158
  return _RoomEvent.encode({
4236
5159
  event: "start",
4237
- params: _RoomEvent.generationParameters(options),
5160
+ params: {
5161
+ ..._RoomEvent.generationParameters(options),
5162
+ mirror: options.mirror ?? false,
5163
+ ...options.context.referenceVideo && {
5164
+ ref_video_path: options.context.referenceVideo.path,
5165
+ sample_method: options.context.referenceVideo.sampleMethod,
5166
+ target_size: [
5167
+ options.targetSize?.width ?? options.videoFormat.width,
5168
+ options.targetSize?.height ?? options.videoFormat.height
5169
+ ]
5170
+ }
5171
+ },
4238
5172
  user_id: options.userID,
4239
- uid: options.taskID
5173
+ uid: options.taskID,
5174
+ ...options.context.referenceVideo && { session_uid: options.taskID }
4240
5175
  });
4241
5176
  }
4242
5177
  /**
@@ -4452,20 +5387,20 @@ var _RoomMessageCodec = class _RoomMessageCodec {
4452
5387
  return parsed;
4453
5388
  }
4454
5389
  const key = `${senderUserID}:${chunk.eventId}`;
4455
- let record = this.records.get(key);
4456
- if (!record) {
4457
- record = { count: chunk.count, parts: /* @__PURE__ */ new Map() };
4458
- this.records.set(key, record);
5390
+ let record2 = this.records.get(key);
5391
+ if (!record2) {
5392
+ record2 = { count: chunk.count, parts: /* @__PURE__ */ new Map() };
5393
+ this.records.set(key, record2);
4459
5394
  this.evictOverflow();
4460
5395
  }
4461
- record.parts.set(chunk.index, chunk.data);
4462
- if (record.parts.size < record.count) {
5396
+ record2.parts.set(chunk.index, chunk.data);
5397
+ if (record2.parts.size < record2.count) {
4463
5398
  return void 0;
4464
5399
  }
4465
5400
  this.records.delete(key);
4466
5401
  const parts = [];
4467
- for (let index = 0; index < record.count; index += 1) {
4468
- const part = record.parts.get(index);
5402
+ for (let index = 0; index < record2.count; index += 1) {
5403
+ const part = record2.parts.get(index);
4469
5404
  if (part === void 0) {
4470
5405
  return void 0;
4471
5406
  }
@@ -4495,16 +5430,16 @@ var _RoomMessageCodec = class _RoomMessageCodec {
4495
5430
  if (this.chunkEvent === null || typeof parsed !== "object" || parsed === null) {
4496
5431
  return void 0;
4497
5432
  }
4498
- const record = parsed;
4499
- if (record.event !== this.chunkEvent || typeof record.eventId !== "string" || record.eventId.length === 0 || !Number.isInteger(record.index) || !Number.isInteger(record.count) || typeof record.data !== "string") {
5433
+ const record2 = parsed;
5434
+ if (record2.event !== this.chunkEvent || typeof record2.eventId !== "string" || record2.eventId.length === 0 || !Number.isInteger(record2.index) || !Number.isInteger(record2.count) || typeof record2.data !== "string") {
4500
5435
  return void 0;
4501
5436
  }
4502
- const index = record.index;
4503
- const count = record.count;
5437
+ const index = record2.index;
5438
+ const count = record2.count;
4504
5439
  if (count < 1 || index < 0 || index >= count) {
4505
5440
  return void 0;
4506
5441
  }
4507
- return { eventId: record.eventId, index, count, data: record.data };
5442
+ return { eventId: record2.eventId, index, count, data: record2.data };
4508
5443
  }
4509
5444
  /**
4510
5445
  * 淘汰最旧的未完成记录,保持缓存不超过上限。
@@ -4616,9 +5551,9 @@ var RoomController = class {
4616
5551
  *
4617
5552
  * @throws 已有房间未离开、操作失效或进房失败时抛出错误。
4618
5553
  */
4619
- async join(connection, ensureActive) {
5554
+ async join(connection, ensureActive2) {
4620
5555
  try {
4621
- ensureActive();
5556
+ ensureActive2();
4622
5557
  } catch (error) {
4623
5558
  throw XmaxError.from(error);
4624
5559
  }
@@ -4633,10 +5568,10 @@ var RoomController = class {
4633
5568
  this.heartbeat.stop();
4634
5569
  try {
4635
5570
  if (!this.customCodec) this.codec = new RoomMessageCodec(
4636
- connection.provider === "vertc" ? null : connection.provider === "agora" ? "__agora_chunk__" : "__trtc_chunk__"
5571
+ connection.provider === "vertc" /* vertc */ ? null : connection.provider === "agora" /* agora */ ? "__agora_chunk__" : "__trtc_chunk__"
4637
5572
  );
4638
5573
  await this.rtcManager.joinRoom(toRoomJoinConfiguration(connection));
4639
- ensureActive();
5574
+ ensureActive2();
4640
5575
  if (this.state.kind !== "joining" || this.state.operationID !== operationID || this.leaveOperation) {
4641
5576
  throw new XmaxError("CANCELLED" /* cancelled */, "RTC room join was cancelled");
4642
5577
  }
@@ -4677,6 +5612,7 @@ var RoomController = class {
4677
5612
  startGeneration(options) {
4678
5613
  return this.send(
4679
5614
  RoomEvent.start({
5615
+ mirror: options.mirror ?? false,
4680
5616
  userID: this.requireUserID(),
4681
5617
  taskID: options.taskID,
4682
5618
  videoFormat: options.videoFormat,
@@ -4828,6 +5764,7 @@ var _EncodingController = class _EncodingController {
4828
5764
  * 基础层组件
4829
5765
  */
4830
5766
  __publicField(this, "rtcManager");
5767
+ __publicField(this, "lastAppliedConfiguration");
4831
5768
  this.rtcManager = options.rtcManager;
4832
5769
  }
4833
5770
  /**
@@ -4863,14 +5800,38 @@ var _EncodingController = class _EncodingController {
4863
5800
  "Minimum bitrate must not exceed maximum bitrate after applying SDK defaults"
4864
5801
  );
4865
5802
  }
4866
- await this.rtcManager.configureVideoEncoding({
5803
+ const configuration = {
4867
5804
  width: videoFormat.width,
4868
5805
  height: videoFormat.height,
4869
5806
  frameRate: videoFormat.fps,
4870
5807
  minimumBitrate: minimum,
4871
5808
  maximumBitrate: maximum,
4872
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 */
4873
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;
4874
5835
  }
4875
5836
  /**
4876
5837
  * 按像素面积与帧率计算码率区间(kbps):流畅优先为最低,画质优先为最高。
@@ -5019,6 +5980,7 @@ var StreamController = class {
5019
5980
  * 运行状态
5020
5981
  */
5021
5982
  __publicField(this, "state", makeInitialState());
5983
+ __publicField(this, "roomListener");
5022
5984
  this.rtcManager = options.rtcManager;
5023
5985
  this.roomController = options.roomController ?? new RoomController({ rtcManager: options.rtcManager });
5024
5986
  this.encodingController = options.encodingController ?? new EncodingController({ rtcManager: options.rtcManager });
@@ -5027,13 +5989,19 @@ var StreamController = class {
5027
5989
  this.remoteStreamListener = options.remoteStreamListener ?? (() => {
5028
5990
  });
5029
5991
  this.generationTimeoutMs = options.generationTimeoutMs ?? 3e4;
5992
+ this.roomController.setListener({
5993
+ onRoomMessage: (sender, message) => {
5994
+ this.handleNetworkVideoCompletion(sender, message);
5995
+ this.roomListener?.onRoomMessage(sender, message);
5996
+ }
5997
+ });
5030
5998
  this.rtcManager.setEventListener({
5031
5999
  onTokenWillExpire: options.onCredentialsRequired,
5032
6000
  onTokenExpired: options.onCredentialsRequired,
5033
6001
  onRoomRejoining: () => this.handleRoomRejoining(),
5034
6002
  onError: (error) => this.errorListener(error),
5035
6003
  onNetworkStatistics: (statistics) => {
5036
- if (this.state.localVideoPublished) {
6004
+ if (this.state.roomID) {
5037
6005
  this.networkStatisticsListener?.(statistics);
5038
6006
  }
5039
6007
  },
@@ -5101,25 +6069,31 @@ var StreamController = class {
5101
6069
  *
5102
6070
  * @throws 格式无效、码率区间无效或 RTC 配置失败时抛出错误。
5103
6071
  */
5104
- async setVideoEncoderConfig(videoFormat) {
6072
+ async updateUplinkVideoFormat(videoFormat) {
5105
6073
  await this.encodingController.configure(videoFormat);
5106
6074
  }
6075
+ /** 不根据新帧率重新推算码率区间。 */
6076
+ async updateUplinkFrameRate(fps) {
6077
+ return this.encodingController.updateFrameRate(fps);
6078
+ }
5107
6079
  /**
5108
6080
  * 加入 RTC 房间并发布本地媒体流。
5109
6081
  *
5110
6082
  * @throws 连接已取消,或 RTC 进房、房间配置与本地流发布失败时抛出错误。
5111
6083
  */
5112
- async connect(connection, includeLocalAudio, ensureActive, beforePublish) {
6084
+ async connect(connection, includeLocalAudio, ensureActive2, beforePublish, publishLocalMedia = true) {
5113
6085
  this.configureRoom({ roomID: connection.roomID, botID: connection.botID });
5114
- await this.roomController.join(connection, ensureActive);
5115
- ensureActive();
6086
+ await this.roomController.join(connection, ensureActive2);
6087
+ ensureActive2();
5116
6088
  XmaxLogger.stream.info(
5117
6089
  () => `RTC \u623F\u95F4\u5DF2\u914D\u7F6E (RTC Room Configured)
5118
6090
  \u2514\u2500 roomID: ${connection.roomID}, botID: ${connection.botID ?? "(\u672A\u8BBE\u7F6E)"}`
5119
6091
  );
5120
- await beforePublish?.();
5121
- ensureActive();
5122
- await this.publishLocalStream(includeLocalAudio);
6092
+ if (publishLocalMedia) {
6093
+ await beforePublish?.();
6094
+ ensureActive2();
6095
+ await this.publishLocalStream(includeLocalAudio);
6096
+ }
5123
6097
  }
5124
6098
  /**
5125
6099
  * 清理生成状态、本地发布和远端订阅,并离开当前 RTC 房间。
@@ -5180,18 +6154,22 @@ var StreamController = class {
5180
6154
  }, this.generationTimeoutMs)
5181
6155
  };
5182
6156
  this.state.generationTaskID = taskID;
6157
+ this.state.networkVideoCompletion = options.context.referenceVideo ? { taskID, finished: false, scheduled: false } : void 0;
5183
6158
  this.state.generationWaiter = waiter;
5184
6159
  void confirmation.catch(() => {
5185
6160
  });
5186
6161
  try {
5187
6162
  const sending = this.roomController.startGeneration({
5188
6163
  taskID,
6164
+ mirror: options.mirror ?? false,
5189
6165
  videoFormat: options.videoFormat,
5190
6166
  targetSize: options.targetSize,
5191
6167
  context: options.context
5192
6168
  });
5193
6169
  const state2 = this.state;
5194
- const sent = Promise.resolve(sending).then(() => {
6170
+ const sent = Promise.resolve(sending).then(async () => {
6171
+ if (this.state !== state2 || state2.generationTaskID !== taskID) return;
6172
+ await options.onStartSent?.();
5195
6173
  if (this.state !== state2 || state2.generationTaskID !== taskID) return;
5196
6174
  for (const userID of state2.publishedRemoteUserIDs) {
5197
6175
  void this.handleRemoteVideoPublished(userID, true);
@@ -5257,9 +6235,9 @@ var StreamController = class {
5257
6235
  *
5258
6236
  * @throws 操作已取消、房间未就绪或信令发送失败时抛出错误。
5259
6237
  */
5260
- async changeTargetSize(taskID, targetSize, ensureActive) {
6238
+ async updateDownlinkVideoResolution(taskID, targetSize, ensureActive2) {
5261
6239
  try {
5262
- await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive });
6240
+ await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive: ensureActive2 });
5263
6241
  } catch (error) {
5264
6242
  throw XmaxError.from(error);
5265
6243
  }
@@ -5296,7 +6274,50 @@ var StreamController = class {
5296
6274
  * 设置房间业务消息监听器,传入空值时清除监听器。
5297
6275
  */
5298
6276
  setRoomListener(listener) {
5299
- this.roomController.setListener(listener);
6277
+ this.roomListener = listener;
6278
+ }
6279
+ /**
6280
+ * 生成状态已提交后登记完成通知,避免快速任务在启动返回前交付完成事件。
6281
+ */
6282
+ activateNetworkVideoCompletion(onFinish) {
6283
+ const completion = this.state.networkVideoCompletion;
6284
+ if (completion) {
6285
+ completion.onFinish = onFinish;
6286
+ this.deliverNetworkVideoCompletion();
6287
+ }
6288
+ }
6289
+ /**
6290
+ * 仅接受当前任务和目标机器人的完成消息;不因完成事件立即退房。
6291
+ */
6292
+ handleNetworkVideoCompletion(sender, message) {
6293
+ const state2 = this.state;
6294
+ const completion = state2.networkVideoCompletion;
6295
+ if (!state2.roomID || !completion || completion.taskID !== state2.generationTaskID || message.event !== "video_stopped" || message.uid !== completion.taskID || !sender || state2.botID && sender !== state2.botID) {
6296
+ return;
6297
+ }
6298
+ completion.finished = true;
6299
+ this.deliverNetworkVideoCompletion();
6300
+ }
6301
+ /**
6302
+ * 异步交付一次完成通知;执行前复核任务,隔离用户回调异常。
6303
+ */
6304
+ deliverNetworkVideoCompletion() {
6305
+ const state2 = this.state;
6306
+ const completion = state2.networkVideoCompletion;
6307
+ if (!completion?.finished || !completion.onFinish || completion.scheduled) {
6308
+ return;
6309
+ }
6310
+ completion.scheduled = true;
6311
+ queueMicrotask(() => {
6312
+ if (this.state !== state2 || state2.generationTaskID !== completion.taskID) {
6313
+ return;
6314
+ }
6315
+ try {
6316
+ void Promise.resolve(completion.onFinish?.()).catch(() => {
6317
+ });
6318
+ } catch {
6319
+ }
6320
+ });
5300
6321
  }
5301
6322
  /**
5302
6323
  * 配置当前房间;已有发布、订阅或生成任务时拒绝重新配置。
@@ -5527,6 +6548,7 @@ var StreamController = class {
5527
6548
  const remoteAudioUserIDs = [...this.state.subscribedRemoteAudioUserIDs];
5528
6549
  this.state.generationTaskID = void 0;
5529
6550
  this.state.generationWaiter = void 0;
6551
+ this.state.networkVideoCompletion = void 0;
5530
6552
  this.state.activeRemoteStream = void 0;
5531
6553
  this.state.subscribedRemoteAudioUserIDs.clear();
5532
6554
  if (waiter) {
@@ -5635,6 +6657,195 @@ var StreamController = class {
5635
6657
  }
5636
6658
  };
5637
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
+
5638
6849
  // src/Core/Realtime/RealtimeCoordinator.ts
5639
6850
  function scopeAffects(scope, kind) {
5640
6851
  if (scope === 1 /* all */) {
@@ -5737,6 +6948,10 @@ var RealtimeCoordinator = class _RealtimeCoordinator {
5737
6948
  get currentState() {
5738
6949
  return this.state;
5739
6950
  }
6951
+ /** 后台自动调档仅在没有前台操作或清理时申请操作租约。 */
6952
+ get isBusy() {
6953
+ return this.activeOperation !== void 0 || this.termination !== void 0;
6954
+ }
5740
6955
  /**
5741
6956
  * 设置实时状态监听器;设置后立即回放当前状态。
5742
6957
  *
@@ -6078,13 +7293,18 @@ var RealtimeLaunchTimer = class {
6078
7293
  * 开始新的启动周期并清空旧快照,返回记录相机创建完成的一次性回调。
6079
7294
  */
6080
7295
  startCamera() {
7296
+ this.start();
7297
+ return this.measure("cameraMs", this.startedAt);
7298
+ }
7299
+ /**
7300
+ * 开始新的启动周期;非摄像头源不产生相机和发布耗时。
7301
+ */
7302
+ start() {
6081
7303
  this.sequence += 1;
6082
7304
  this.active = true;
6083
7305
  this.startedAt = performance.now();
6084
7306
  this.snapshot = Object.freeze({});
6085
- const complete = this.measure("cameraMs", this.startedAt);
6086
7307
  this.notify();
6087
- return complete;
6088
7308
  }
6089
7309
  /**
6090
7310
  * 开始等待用户授权系统媒体权限,返回授权决定时调用的记录函数。
@@ -6208,14 +7428,19 @@ var XmaxRealtimeConnectionManager = class {
6208
7428
  }
6209
7429
  const refreshVersion = ++this.refreshVersion;
6210
7430
  this.credentialController = new AbortController();
6211
- const videoFormat = options.localTrack.videoFormat;
7431
+ const videoFormat = options.localVideoFormat ?? options.localTrack.videoFormat;
6212
7432
  if (!videoFormat) {
6213
7433
  throw new XmaxError(
6214
7434
  "INTERNAL_ERROR" /* internalError */,
6215
7435
  "Local video stream has no video format"
6216
7436
  );
6217
7437
  }
6218
- await streamController.setVideoEncoderConfig(videoFormat);
7438
+ const publishLocalMedia = options.publishLocalMedia ?? true;
7439
+ if (publishLocalMedia) {
7440
+ await streamController.updateUplinkVideoFormat(videoFormat);
7441
+ options.ensureCurrent();
7442
+ options.onVideoEncoderConfigured?.();
7443
+ }
6219
7444
  options.ensureCurrent();
6220
7445
  let completePublish;
6221
7446
  await streamController.connect(connection, options.includeLocalAudio, () => {
@@ -6225,8 +7450,9 @@ var XmaxRealtimeConnectionManager = class {
6225
7450
  await options.beforePublish?.();
6226
7451
  completeConnection();
6227
7452
  completePublish = this.dependencies.timing.startPublish();
6228
- });
7453
+ }, publishLocalMedia);
6229
7454
  options.ensureCurrent();
7455
+ completeConnection();
6230
7456
  completePublish?.();
6231
7457
  options.onPublished();
6232
7458
  sessionService.startHeartbeat(session.id, {
@@ -6275,7 +7501,7 @@ var XmaxRealtimeConnectionManager = class {
6275
7501
  async refreshCredentials() {
6276
7502
  if (this.credentialRefresh) return this.credentialRefresh;
6277
7503
  const current = this.activeSession;
6278
- if (!current?.connection || current.connection.provider === "trtc") return;
7504
+ if (!current?.connection || current.connection.provider === "trtc" /* trtc */) return;
6279
7505
  const version = this.refreshVersion;
6280
7506
  const operation = (async () => {
6281
7507
  try {
@@ -6360,7 +7586,7 @@ var XmaxRealtimeConnectionManager = class {
6360
7586
  },
6361
7587
  attachHandler: (view) => {
6362
7588
  this.remoteTarget?.setFrameInterpolation?.(void 0);
6363
- view.isMirrored = this.dependencies.isMirrored();
7589
+ view.isMirrored = false;
6364
7590
  this.remoteTarget = view;
6365
7591
  this.dependencies.onRenderAttached();
6366
7592
  const mediaTrack = track.mediaStreamTrack;
@@ -6376,14 +7602,6 @@ var XmaxRealtimeConnectionManager = class {
6376
7602
  }
6377
7603
  });
6378
7604
  }
6379
- /**
6380
- * 同步远端结果画面的镜像状态:与当前本地摄像头位置保持一致。
6381
- */
6382
- updateRemoteMirror() {
6383
- if (this.remoteTarget) {
6384
- this.remoteTarget.isMirrored = this.dependencies.isMirrored();
6385
- }
6386
- }
6387
7605
  /**
6388
7606
  * 构造当前远端生成结果媒体流。
6389
7607
  */
@@ -6428,7 +7646,7 @@ var XmaxRealtimeConnectionManager = class {
6428
7646
  );
6429
7647
  return;
6430
7648
  }
6431
- if (next.provider !== "trtc" && (force || !previous || previous.provider === "trtc" || next.roomToken !== previous.roomToken)) {
7649
+ if (next.provider !== "trtc" /* trtc */ && (force || !previous || previous.provider === "trtc" /* trtc */ || next.roomToken !== previous.roomToken)) {
6432
7650
  await this.dependencies.streamController.updateCredentials(next, this.credentialController?.signal);
6433
7651
  if (version !== this.refreshVersion || this.activeSession !== current) return;
6434
7652
  }
@@ -6526,7 +7744,10 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
6526
7744
  const confirmation = this.streamController.beginGeneration({
6527
7745
  taskID,
6528
7746
  videoFormat: options.videoFormat,
6529
- context
7747
+ context,
7748
+ ...options.targetSize && { targetSize: options.targetSize },
7749
+ mirror: options.mirror ?? false,
7750
+ ...options.onStartSent && { onStartSent: options.onStartSent }
6530
7751
  });
6531
7752
  await this.awaitConfirmation(confirmation, options.signal);
6532
7753
  options.ensureCurrent();
@@ -6552,7 +7773,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
6552
7773
  await this.streamController.updateGeneration({
6553
7774
  taskID,
6554
7775
  videoFormat: options.videoFormat,
6555
- context
7776
+ context,
7777
+ ...options.targetSize && { targetSize: options.targetSize }
6556
7778
  });
6557
7779
  if (this.activeTaskID !== taskID) throw RealtimeCoordinator.cancelledError();
6558
7780
  this.currentContext = context;
@@ -6659,6 +7881,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6659
7881
  __publicField(this, "coordinator");
6660
7882
  __publicField(this, "errorHandler");
6661
7883
  __publicField(this, "cameraController");
7884
+ __publicField(this, "networkVideoController", new NetworkVideoController());
7885
+ __publicField(this, "localVideoController", new LocalVideoController((error) => {
7886
+ void this.coordinator.terminateWithError(error, 0 /* connection */);
7887
+ }));
7888
+ __publicField(this, "rtcManager");
6662
7889
  /**
6663
7890
  * 启动计时与首帧通知
6664
7891
  */
@@ -6684,6 +7911,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6684
7911
  __publicField(this, "connectionManager");
6685
7912
  __publicField(this, "generationManager");
6686
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);
6687
7922
  /**
6688
7923
  * 音量配置
6689
7924
  */
@@ -6695,6 +7930,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6695
7930
  this.supportsInterpolation = dependencies?.supportsFrameInterpolation ?? (async (size) => await frameInterpolationAdapter(size) !== null);
6696
7931
  this.errorHandler = new RealtimeErrorHandler();
6697
7932
  const rtcManager = dependencies?.rtcManager ?? createRtcManager(options.provider, dependencies?.environment ?? "china" /* china */);
7933
+ this.rtcManager = rtcManager;
6698
7934
  this.cameraController = dependencies?.cameraController ?? new CameraController({
6699
7935
  rtcManager,
6700
7936
  mediaService: this.mediaService,
@@ -6721,7 +7957,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6721
7957
  sessionService,
6722
7958
  streamController: this.streamController,
6723
7959
  timing: this.launchTimer,
6724
- isMirrored: () => this.cameraController.currentTrack?.position === "front" /* front */,
6725
7960
  remoteAudioVolume: () => this.storedRemoteAudioVolume,
6726
7961
  onHeartbeatFailure: (sessionID, error) => {
6727
7962
  void this.handleHeartbeatFailure(sessionID, error);
@@ -6737,6 +7972,31 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6737
7972
  errorHandler: this.errorHandler,
6738
7973
  cleanup: (scope, taskID) => this.performCleanup(scope, taskID)
6739
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
+ }
6740
8000
  this.streamController.setLocalVideoStatisticsListener((statistics) => {
6741
8001
  if (this.acceptsVideoStatistics) {
6742
8002
  this.localVideoStatistics = statistics ? Object.freeze({ ...statistics }) : void 0;
@@ -6746,6 +8006,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6746
8006
  this.streamController.setNetworkStatisticsListener((statistics) => {
6747
8007
  if (this.acceptsVideoStatistics) {
6748
8008
  this.networkStatistics = statistics ? Object.freeze({ ...statistics }) : void 0;
8009
+ this.handleAdaptiveQuality(statistics);
6749
8010
  this.notifyNetworkStatistics();
6750
8011
  }
6751
8012
  });
@@ -6773,7 +8034,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6773
8034
  */
6774
8035
  async setFrameInterpolationEnabled(enabled) {
6775
8036
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
6776
- const format = this.generationVideoFormat ?? this.cameraController.currentTrack?.videoFormat;
8037
+ const format = this.downlinkQualityState?.size ?? this.generationVideoFormat ?? this.currentLocalTrack?.videoFormat;
6777
8038
  const size = format && enabled ? this.mediaService.resolveFrameInterpolationSize(format) : void 0;
6778
8039
  if (enabled && !await this.checkInterpolationSupport(size, token)) {
6779
8040
  token.ensureCurrent();
@@ -6785,6 +8046,43 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6785
8046
  this.applyRemoteInterpolation();
6786
8047
  });
6787
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
+ }
6788
8086
  /**
6789
8087
  * 根据当前视频尺寸准备插帧,能力不可用时降级为原视频,并在应用前校验租约。
6790
8088
  */
@@ -6838,7 +8136,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6838
8136
  const size = this.interpolationSize;
6839
8137
  this.connectionManager.setFrameInterpolation(size ? {
6840
8138
  size,
6841
- targetFrameRate: this.options.frameInterpolation.targetFrameRate,
6842
8139
  onFailure: (error) => {
6843
8140
  if (revision === this.interpolationRevision) this.handleInterpolationFailure(error);
6844
8141
  }
@@ -6945,6 +8242,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6945
8242
  * 停止接收媒体统计并清空各类快照,仅对曾有数据的统计发出清空通知。
6946
8243
  */
6947
8244
  clearVideoStatistics() {
8245
+ this.qualityController?.stop();
8246
+ this.downlinkQualityController?.stop();
8247
+ this.downlinkQualityState = void 0;
8248
+ this.adaptiveQualityRevision++;
6948
8249
  this.acceptsVideoStatistics = false;
6949
8250
  const hadNetworkStatistics = this.networkStatistics !== void 0;
6950
8251
  this.networkStatistics = void 0;
@@ -6999,6 +8300,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6999
8300
  "media" /* media */,
7000
8301
  void 0,
7001
8302
  async (token) => {
8303
+ this.ensureNoLocalSource();
7002
8304
  await this.coordinator.commit(
7003
8305
  new RealtimeState({
7004
8306
  connectionState: "Preparing" /* preparing */
@@ -7035,6 +8337,63 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7035
8337
  }
7036
8338
  );
7037
8339
  }
8340
+ /**
8341
+ * 创建网络视频源并初始化接收端 RTC;预览是否加载成功不影响服务端读取视频。
8342
+ */
8343
+ async createNetworkVideoStream(options) {
8344
+ return this.coordinator.run("media" /* media */, void 0, async (token) => {
8345
+ this.ensureNoLocalSource();
8346
+ const stream = this.networkVideoController.create(options, this.mediaService);
8347
+ token.setFailureScope(1 /* all */);
8348
+ this.launchTimer.start();
8349
+ this.clearVideoStatistics();
8350
+ await this.rtcManager.initialize();
8351
+ token.ensureCurrent();
8352
+ this.generationVideoFormat = stream.videoTrack?.videoFormat;
8353
+ await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
8354
+ return stream;
8355
+ });
8356
+ }
8357
+ /**
8358
+ * 创建文件音视频源并准备外部轨道;准备失败时统一释放本次媒体和 RTC 资源。
8359
+ */
8360
+ async createLocalVideoStream(options) {
8361
+ return this.coordinator.run("media" /* media */, void 0, async (token) => {
8362
+ this.ensureNoLocalSource();
8363
+ if (!this.rtcManager.setExternalMediaTracks) {
8364
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "The RTC provider does not support local video streams");
8365
+ }
8366
+ token.setFailureScope(1 /* all */);
8367
+ this.launchTimer.start();
8368
+ this.clearVideoStatistics();
8369
+ const stream = await this.localVideoController.create(options, this.mediaService, token.signal);
8370
+ token.ensureCurrent();
8371
+ await this.rtcManager.initialize();
8372
+ token.ensureCurrent();
8373
+ await this.rtcManager.setExternalMediaTracks({
8374
+ videoTrack: stream.videoTrack.mediaStreamTrack,
8375
+ audioTrack: this.localVideoController.audioTrack
8376
+ });
8377
+ token.ensureCurrent();
8378
+ this.generationVideoFormat = stream.videoTrack?.videoFormat;
8379
+ await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
8380
+ return stream;
8381
+ });
8382
+ }
8383
+ /**
8384
+ * 当前 Manager 拥有的媒体源轨道。
8385
+ */
8386
+ get currentLocalTrack() {
8387
+ return this.localVideoController.currentTrack ?? this.networkVideoController.currentTrack ?? this.cameraController.currentTrack;
8388
+ }
8389
+ /**
8390
+ * 创建前拒绝覆盖活动源,避免旧预览或连接资源被静默遗留。
8391
+ */
8392
+ ensureNoLocalSource() {
8393
+ if (this.currentLocalTrack || this.localVideoController.isActive) {
8394
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local stream before creating another one");
8395
+ }
8396
+ }
7038
8397
  /**
7039
8398
  * 移除媒体权限弹窗观察;授权决定已记录或不再需要观察时调用。
7040
8399
  */
@@ -7046,26 +8405,105 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7046
8405
  * 应用完整的上行视频格式,成功后更新本地轨道元数据,不改变生成格式或生命周期。
7047
8406
  * 复用配置操作准入,避免与建连、切换摄像头或其他配置更新并发。
7048
8407
  *
7049
- * @throws 无本地相机流、参数无效、操作冲突或底层编码更新失败时抛错。
8408
+ * @throws 无本地相机或文件视频流、参数无效、操作冲突或底层编码更新失败时抛错。
7050
8409
  */
7051
- async updateVideoFormat(videoFormat) {
8410
+ async updateUplinkVideoFormat(videoFormat) {
7052
8411
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
7053
- const track = this.cameraController.currentTrack;
8412
+ const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
7054
8413
  const previousFormat = track?.videoFormat;
7055
8414
  if (!track || !previousFormat) {
7056
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera stream is not started");
8415
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera or file video stream is not started");
7057
8416
  }
7058
8417
  videoFormat.validate();
7059
- await this.streamController.setVideoEncoderConfig(videoFormat);
8418
+ this.qualityController?.stop();
8419
+ await this.streamController.updateUplinkVideoFormat(videoFormat);
7060
8420
  token.ensureCurrent();
7061
8421
  this.generationVideoFormat ?? (this.generationVideoFormat = previousFormat);
7062
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;
7063
8498
  });
7064
8499
  }
7065
8500
  /**
7066
8501
  * 停止本地相机流并释放本地预览与 RTC 资源。
7067
8502
  */
7068
8503
  async stopLocalCameraStream() {
8504
+ if (this.networkVideoController.currentTrack || this.localVideoController.isActive) {
8505
+ return;
8506
+ }
7069
8507
  await this.coordinator.run(
7070
8508
  "media" /* media */,
7071
8509
  void 0,
@@ -7096,9 +8534,9 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7096
8534
  "cameraSwitch" /* cameraSwitch */,
7097
8535
  void 0,
7098
8536
  async (token) => {
8537
+ this.qualityController?.stop();
7099
8538
  const stream = await this.cameraController.switchCamera();
7100
8539
  token.ensureCurrent();
7101
- this.connectionManager.updateRemoteMirror();
7102
8540
  return stream;
7103
8541
  }
7104
8542
  );
@@ -7106,11 +8544,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7106
8544
  /**
7107
8545
  * 使用当前 Manager 创建的本地流建立实时连接。
7108
8546
  *
7109
- * 创建实时会话、加入 RTC 房间并发布本地流,成功后启动会话心跳。
8547
+ * 创建实时会话、加入 RTC 房间并启动会话心跳;摄像头源发布本地流,网络视频源仅接收。
7110
8548
  * 启用帧检测时,发布前并行等待相机预热(固定 200ms),避免把黑帧推给 RTC。
7111
8549
  * 返回的远端媒体流在生成开始后承载远端生成画面。
7112
8550
  *
7113
- * @param localStream 由 `createLocalCameraStream` 创建的本地媒体流。
8551
+ * @param localStream 由当前 Manager 创建的摄像头、本地文件或网络视频源。
7114
8552
  * @returns 远端生成结果占位的媒体流。
7115
8553
  * @throws 本地流不属于当前 Manager、已有活动连接、会话创建或进房
7116
8554
  * 发布失败时抛出错误;失败时自动释放连接资源并恢复本地预览。
@@ -7137,7 +8575,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7137
8575
  * 尚未连接时先建立实时连接;已在生成时仅更新生成条件,不重启生成。
7138
8576
  * 首次生成必须提供条件上下文,之后缺省时复用最近一次缓存的上下文。
7139
8577
  *
7140
- * @param options.localStream 由 `createLocalCameraStream` 创建的本地媒体流。
8578
+ * @param options.localStream 由当前 Manager 创建的摄像头、本地文件或网络视频源。
7141
8579
  * @param options.context 本次生成使用的条件上下文;缺省时复用缓存。
7142
8580
  * @returns 承载远端生成画面的媒体流。
7143
8581
  * @throws 本地流不属于当前 Manager、缺少可用的条件上下文、信令发送
@@ -7149,7 +8587,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7149
8587
  void 0,
7150
8588
  async (token) => {
7151
8589
  const localTrack = options.localStream.videoTrack;
7152
- if (!localTrack || localTrack !== this.cameraController.currentTrack) {
8590
+ if (!localTrack || localTrack !== this.currentLocalTrack) {
7153
8591
  throw new XmaxError(
7154
8592
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
7155
8593
  "The local stream must be created and started by this realtime manager"
@@ -7166,12 +8604,18 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7166
8604
  if (currentState.connectionState === "Generating" /* generating */) {
7167
8605
  await this.generationManager.update(currentState.taskID, {
7168
8606
  videoFormat,
8607
+ targetSize: this.downlinkQualityController ? this.downlinkQualityState?.size : void 0,
7169
8608
  context: options.context
7170
8609
  });
7171
8610
  token.ensureCurrent();
7172
8611
  return this.connectionManager.makeRemoteStream();
7173
8612
  }
7174
- const context = this.generationManager.validateContext(options.context);
8613
+ const supplied = this.generationManager.validateContext(options.context);
8614
+ const referenceVideo = this.networkVideoController.reference;
8615
+ if (!referenceVideo && supplied.referenceVideo) {
8616
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Create a network video stream before using a reference video");
8617
+ }
8618
+ const context = referenceVideo ? new RealtimeContext({ ...supplied, referenceVideo }) : supplied;
7175
8619
  if (currentState.connectionState !== "Connected" /* connected */) {
7176
8620
  await this.performConnect(options.localStream, token);
7177
8621
  }
@@ -7183,6 +8627,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7183
8627
  * 断开实时连接并保留当前本地媒体预览。
7184
8628
  */
7185
8629
  async disconnect() {
8630
+ this.localVideoController.pause();
7186
8631
  this.launchTimer.cancel();
7187
8632
  this.clearVideoStatistics();
7188
8633
  await this.coordinator.disconnect();
@@ -7192,10 +8637,20 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7192
8637
  * 关闭期间重复调用会等待同一个释放任务;关闭完成后仍可重新创建本地流。
7193
8638
  */
7194
8639
  async close() {
8640
+ this.localVideoController.pause();
7195
8641
  this.launchTimer.cancel();
7196
8642
  this.clearVideoStatistics();
7197
8643
  await this.coordinator.terminate(1 /* all */);
7198
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
+ }
7199
8654
  /**
7200
8655
  * 建立实时连接:创建会话、进房发布、启动心跳并准备远端渲染轨道。
7201
8656
  *
@@ -7205,7 +8660,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7205
8660
  */
7206
8661
  async performConnect(localStream, token) {
7207
8662
  const localTrack = localStream.videoTrack;
7208
- if (!localTrack || localTrack !== this.cameraController.currentTrack) {
8663
+ if (!localTrack || localTrack !== this.currentLocalTrack) {
7209
8664
  throw new XmaxError(
7210
8665
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
7211
8666
  "The local stream must be created and started by this realtime manager"
@@ -7223,7 +8678,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7223
8678
  if (token.signal.aborted) abortWarmup();
7224
8679
  try {
7225
8680
  let beforePublish;
7226
- if (this.cameraController.isFrameValidationEnabled) {
8681
+ if (!this.localVideoController.isActive && !this.networkVideoController.currentTrack && this.cameraController.isFrameValidationEnabled) {
7227
8682
  const cameraReady = this.cameraController.waitForValidCameraFrame(warmupController.signal).then(() => {
7228
8683
  token.ensureCurrent();
7229
8684
  });
@@ -7231,11 +8686,22 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7231
8686
  });
7232
8687
  beforePublish = () => cameraReady;
7233
8688
  }
8689
+ const initialQuality = this.resolveInitialUplinkQuality(localTrack);
8690
+ const initialFormat = initialQuality ? qualityVideoFormat(initialQuality.baseline, initialQuality.level) : void 0;
7234
8691
  const remote = await this.connectionManager.connect({
7235
8692
  localTrack,
7236
- 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
+ },
7237
8702
  model: this.options.model,
7238
- includeLocalAudio: this.cameraController.useMicrophone,
8703
+ includeLocalAudio: this.localVideoController.isActive ? this.localVideoController.audioTrack !== void 0 : !this.networkVideoController.currentTrack && this.cameraController.useMicrophone,
8704
+ publishLocalMedia: !this.networkVideoController.currentTrack,
7239
8705
  ensureCurrent: () => token.ensureCurrent(),
7240
8706
  onPublished: () => {
7241
8707
  this.acceptsVideoStatistics = true;
@@ -7260,16 +8726,25 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7260
8726
  */
7261
8727
  async performStartGeneration(token, videoFormat, context) {
7262
8728
  token.setFailureScope(0 /* connection */);
7263
- 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);
7264
8732
  const completeFirstFrame = this.launchTimer.startFirstFrame();
7265
8733
  this.remoteFrameDisplayHandler = () => {
7266
8734
  if (!token.signal.aborted) completeFirstFrame();
7267
8735
  };
7268
8736
  const taskID = await this.generationManager.start({
7269
8737
  videoFormat,
8738
+ targetSize,
7270
8739
  context,
8740
+ mirror: this.currentLocalTrack === this.cameraController.currentTrack && this.cameraController.currentTrack?.position === "front" /* front */,
7271
8741
  signal: token.signal,
7272
8742
  ensureCurrent: () => token.ensureCurrent(),
8743
+ onStartSent: this.localVideoController.isActive ? async () => {
8744
+ token.ensureCurrent();
8745
+ await this.localVideoController.start(token.signal);
8746
+ token.ensureCurrent();
8747
+ } : void 0,
7273
8748
  waitUntilRemoteReady: () => this.connectionManager.waitUntilRemoteTrackReady(
7274
8749
  REMOTE_FIRST_FRAME_TIMEOUT_MS,
7275
8750
  token.signal
@@ -7279,10 +8754,26 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7279
8754
  if (!sessionID) {
7280
8755
  throw new XmaxError("SESSION_ERROR" /* sessionError */, "Realtime session is unavailable");
7281
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
+ };
7282
8764
  await this.coordinator.commit(
7283
8765
  new RealtimeState({ connectionState: "Generating" /* generating */, sessionID, taskID }),
7284
8766
  token
7285
8767
  );
8768
+ this.qualityHistory?.write("downlink", initialLevel);
8769
+ const onFinish = this.networkVideoController.onFinish;
8770
+ if (this.networkVideoController.currentTrack) {
8771
+ this.streamController.activateNetworkVideoCompletion(onFinish && (() => {
8772
+ if (this.currentState.taskID === taskID && this.currentState.connectionState === "Generating" /* generating */) {
8773
+ return onFinish();
8774
+ }
8775
+ }));
8776
+ }
7286
8777
  return this.connectionManager.makeRemoteStream();
7287
8778
  }
7288
8779
  /**
@@ -7299,6 +8790,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7299
8790
  * 统一清理顺序:停止心跳与生成,再断开连接;all 额外释放本地媒体。
7300
8791
  */
7301
8792
  async performCleanup(scope, taskID) {
8793
+ this.localVideoController.pause();
7302
8794
  this.launchTimer.cancel();
7303
8795
  this.remoteFrameDisplayHandler = void 0;
7304
8796
  this.clearVideoStatistics();
@@ -7309,20 +8801,30 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7309
8801
  await this.generationManager.reset(taskID);
7310
8802
  const sessionID = await this.connectionManager.disconnect();
7311
8803
  if (scope === 1 /* all */) {
8804
+ const hadFileSource = this.networkVideoController.currentTrack !== void 0 || this.localVideoController.isActive;
8805
+ this.networkVideoController.stop();
7312
8806
  this.generationVideoFormat = void 0;
7313
8807
  this.disposePermissionWatch();
7314
8808
  try {
7315
- await this.cameraController.stopLocalCameraStream();
8809
+ if (hadFileSource) {
8810
+ await this.rtcManager.destroy();
8811
+ } else {
8812
+ await this.cameraController.stopLocalCameraStream();
8813
+ }
7316
8814
  } catch (error) {
7317
8815
  XmaxLogger.realtime.error(
7318
- () => `\u505C\u6B62\u672C\u5730\u76F8\u673A\u6D41\u5931\u8D25 (Failed to Stop Local Camera Stream)
8816
+ () => `\u91CA\u653E\u672C\u5730\u5A92\u4F53\u5931\u8D25 (Failed to Release Local Media)
7319
8817
  \u2514\u2500 ${XmaxLogger.localized("\u539F\u56E0\uFF1A", "Reason: ")}${XmaxError.from(error).message}`
7320
8818
  );
8819
+ } finally {
8820
+ if (this.localVideoController.isActive) {
8821
+ await this.localVideoController.stop();
8822
+ }
7321
8823
  }
7322
8824
  }
7323
8825
  return {
7324
8826
  sessionID,
7325
- hasLocalMedia: this.cameraController.currentTrack !== void 0
8827
+ hasLocalMedia: this.currentLocalTrack !== void 0
7326
8828
  };
7327
8829
  }
7328
8830
  /**
@@ -7338,51 +8840,450 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7338
8840
  }
7339
8841
  };
7340
8842
 
7341
- // src/Core/XmaxConfiguration.ts
7342
- var XmaxConfiguration = class {
8843
+ // src/Core/XmaxConfiguration.ts
8844
+ var XmaxConfiguration = class {
8845
+ /**
8846
+ * 创建 SDK 全局配置。
8847
+ *
8848
+ * @param init.apiKey 调用 Xmax 服务使用的 API Key。
8849
+ * @param init.environment SDK 连接的服务环境;默认为国内环境。
8850
+ * @param init.loggerOptions SDK 输出的日志类型;默认为空。
8851
+ */
8852
+ constructor(init) {
8853
+ /**
8854
+ * 服务身份与环境
8855
+ */
8856
+ /**
8857
+ * 调用 Xmax 服务使用的 API Key。
8858
+ */
8859
+ __publicField(this, "apiKey");
8860
+ /**
8861
+ * SDK 连接的服务环境,同时决定日志细项语言:国内为中文,海外为英文。
8862
+ */
8863
+ __publicField(this, "environment");
8864
+ /**
8865
+ * 日志配置
8866
+ */
8867
+ /**
8868
+ * SDK 输出的日志类型;默认为不输出日志。
8869
+ */
8870
+ __publicField(this, "loggerOptions");
8871
+ this.apiKey = init.apiKey.trim();
8872
+ this.environment = init.environment ?? "china" /* china */;
8873
+ this.loggerOptions = init.loggerOptions ?? 0 /* none */;
8874
+ }
8875
+ /**
8876
+ * 校验全局配置。
8877
+ * @throws API Key 为空时抛出 `XmaxError`。
8878
+ */
8879
+ validate() {
8880
+ if (this.apiKey.length === 0) {
8881
+ throw new XmaxError(
8882
+ "INVALID_API_KEY" /* invalidAPIKey */,
8883
+ "API key cannot be empty"
8884
+ );
8885
+ }
8886
+ }
8887
+ };
8888
+
8889
+ // src/Service/NonRealtime/NonRealtimeTask.ts
8890
+ var NonRealtimeQuality = /* @__PURE__ */ ((NonRealtimeQuality2) => {
8891
+ NonRealtimeQuality2["sd"] = "sd";
8892
+ NonRealtimeQuality2["hd"] = "hd";
8893
+ return NonRealtimeQuality2;
8894
+ })(NonRealtimeQuality || {});
8895
+ var NonRealtimeTaskStatus = /* @__PURE__ */ ((NonRealtimeTaskStatus2) => {
8896
+ NonRealtimeTaskStatus2["submitted"] = "submitted";
8897
+ NonRealtimeTaskStatus2["processing"] = "processing";
8898
+ NonRealtimeTaskStatus2["completed"] = "completed";
8899
+ NonRealtimeTaskStatus2["error"] = "error";
8900
+ return NonRealtimeTaskStatus2;
8901
+ })(NonRealtimeTaskStatus || {});
8902
+
8903
+ // src/Service/NonRealtime/NonRealtimeTaskService.ts
8904
+ var SUPPORTED_FPS = /* @__PURE__ */ new Set([8, 10, 12, 15, 16, 18, 20, 22, 24, 25, 30, 45, 48, 50, 60, 72, 90, 100, 120]);
8905
+ var NonRealtimeTaskService = class {
8906
+ /**
8907
+ * 注入非实时任务使用的 API 服务。
8908
+ */
8909
+ constructor(api) {
8910
+ /**
8911
+ * 网络依赖
8912
+ */
8913
+ __publicField(this, "api");
8914
+ this.api = api;
8915
+ }
8916
+ /**
8917
+ * 提交一次任务;请求结果不确定时也不自动重试,以免重复扣费。
8918
+ */
8919
+ async submitTask(options) {
8920
+ const model = options.model?.trim();
8921
+ if (!model) {
8922
+ invalid("Model must not be empty");
8923
+ }
8924
+ const prompt = options.prompt.trim();
8925
+ if (!prompt || [...prompt].length > 1024) {
8926
+ invalid("Prompt must contain 1\u20131024 characters");
8927
+ }
8928
+ validateURL(options.videoPath);
8929
+ if (options.referencePath !== void 0) {
8930
+ validateURL(options.referencePath);
8931
+ }
8932
+ if (!Object.values(NonRealtimeQuality).includes(options.quality)) {
8933
+ invalid("Quality must be sd or hd");
8934
+ }
8935
+ if (options.fps !== void 0 && !SUPPORTED_FPS.has(options.fps)) {
8936
+ invalid("Unsupported processing frame rate");
8937
+ }
8938
+ const payload = await this.api.post("/offline-task", {
8939
+ model,
8940
+ prompt,
8941
+ refVideoPath: options.videoPath.trim(),
8942
+ ...options.referencePath === void 0 ? {} : { refImagePath: options.referencePath.trim() },
8943
+ quality: options.quality,
8944
+ ...options.fps === void 0 ? {} : { fps: options.fps }
8945
+ });
8946
+ return parseTask(payload);
8947
+ }
8948
+ /**
8949
+ * 查询任务,校验返回 ID;signal 只取消本次 HTTP 查询,不取消服务端任务。
8950
+ */
8951
+ async getTask(taskUid, signal) {
8952
+ const uid = validateTaskUid(taskUid);
8953
+ const task = parseTask(await this.api.get(`/offline-task/${encodeURIComponent(uid)}`, { signal }));
8954
+ if (task.uid !== uid) {
8955
+ malformed("Task UID does not match the request");
8956
+ }
8957
+ return task;
8958
+ }
8959
+ /**
8960
+ * 查询最多 100 个 ID,按首次出现顺序去重;任一任务不可用时整体失败。
8961
+ */
8962
+ async getTasks(taskUids) {
8963
+ if (taskUids.length < 1 || taskUids.length > 100) {
8964
+ invalid("Provide 1\u2013100 task UIDs");
8965
+ }
8966
+ const uids = [...new Set(taskUids.map(validateTaskUid))];
8967
+ const payload = record(await this.api.post("/offline-task/batch-query", { taskUids: uids }));
8968
+ const tasks = parseList(payload.list);
8969
+ if (tasks.length !== uids.length || tasks.some((task, index) => task.uid !== uids[index])) {
8970
+ malformed("Batch response does not match the requested task UIDs");
8971
+ }
8972
+ return tasks;
8973
+ }
8974
+ /**
8975
+ * 分页查询任务,不隐式加载其他页。
8976
+ */
8977
+ async listTasks(options = {}) {
8978
+ const pageNumber = options.pageNumber ?? 1;
8979
+ const pageSize = options.pageSize ?? 10;
8980
+ if (!Number.isSafeInteger(pageNumber) || pageNumber < 1) {
8981
+ invalid("Page number must be a positive integer");
8982
+ }
8983
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
8984
+ invalid("Page size must be between 1 and 100");
8985
+ }
8986
+ if (options.status !== void 0 && !Object.values(NonRealtimeTaskStatus).includes(options.status)) {
8987
+ invalid("Invalid task status");
8988
+ }
8989
+ const query = new URLSearchParams({ pageNumber: String(pageNumber), pageSize: String(pageSize) });
8990
+ if (options.status !== void 0) {
8991
+ query.set("status", options.status);
8992
+ }
8993
+ const payload = record(await this.api.get(`/offline-task/page?${query}`));
8994
+ return Object.freeze({
8995
+ pageNumber: pageInteger(payload, "pageNumber", 1),
8996
+ pageSize: pageInteger(payload, "pageSize", 1),
8997
+ total: pageInteger(payload, "total", 0),
8998
+ list: parseList(payload.list)
8999
+ });
9000
+ }
9001
+ };
9002
+ function validateTaskUid(value) {
9003
+ const uid = value.trim();
9004
+ if (!uid || uid === "." || uid === "..") {
9005
+ invalid("Task UID must not be empty or a relative path segment");
9006
+ }
9007
+ return uid;
9008
+ }
9009
+ function validateURL(value) {
9010
+ let url;
9011
+ try {
9012
+ url = new URL(value.trim());
9013
+ } catch {
9014
+ return invalid("Resource must be an uploaded HTTP(S) URL");
9015
+ }
9016
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password) {
9017
+ invalid("Resource must be an uploaded HTTP(S) URL");
9018
+ }
9019
+ }
9020
+ function invalid(message) {
9021
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, message);
9022
+ }
9023
+ function malformed(message = "Invalid non-realtime response data") {
9024
+ throw new XmaxError("API_ERROR" /* apiError */, message);
9025
+ }
9026
+ function record(value) {
9027
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
9028
+ malformed();
9029
+ }
9030
+ return value;
9031
+ }
9032
+ function field(data, key, type) {
9033
+ const value = data[key];
9034
+ if (value === void 0 || value === null) {
9035
+ return void 0;
9036
+ }
9037
+ if (typeof value !== type || typeof value === "number" && (!Number.isFinite(value) || value < 0)) {
9038
+ malformed(`Invalid field: ${key}`);
9039
+ }
9040
+ return value;
9041
+ }
9042
+ function pageInteger(data, key, minimum) {
9043
+ const value = field(data, key, "number");
9044
+ if (value === void 0 || !Number.isSafeInteger(value) || value < minimum) {
9045
+ malformed(`Invalid field: ${key}`);
9046
+ }
9047
+ return value;
9048
+ }
9049
+ function parseList(value) {
9050
+ if (!Array.isArray(value)) {
9051
+ malformed("Missing task list");
9052
+ }
9053
+ return Object.freeze(value.map(parseTask));
9054
+ }
9055
+ function parseTask(value) {
9056
+ const data = record(value);
9057
+ const uid = field(data, "uid", "string");
9058
+ const status = field(data, "status", "string");
9059
+ const quality = field(data, "quality", "string");
9060
+ if (!uid?.trim() || !Object.values(NonRealtimeTaskStatus).includes(status)) {
9061
+ malformed("Missing task UID or invalid status");
9062
+ }
9063
+ if (quality !== void 0 && !Object.values(NonRealtimeQuality).includes(quality)) {
9064
+ malformed("Invalid task quality");
9065
+ }
9066
+ return Object.freeze({
9067
+ uid,
9068
+ status,
9069
+ quality,
9070
+ userUid: field(data, "userUid", "string"),
9071
+ prompt: field(data, "prompt", "string"),
9072
+ videoPath: field(data, "refVideoPath", "string"),
9073
+ referencePath: field(data, "refImagePath", "string"),
9074
+ processorId: field(data, "processorId", "string"),
9075
+ requestedFps: field(data, "requestedFps", "number"),
9076
+ resolvedFps: field(data, "resolvedFps", "number"),
9077
+ retryCount: field(data, "retryCount", "number"),
9078
+ videoDurationSeconds: field(data, "videoDurationSeconds", "number"),
9079
+ billableDurationSeconds: field(data, "billableDurationSeconds", "number"),
9080
+ chargePoints: field(data, "chargePoints", "number"),
9081
+ result: data.result == null ? void 0 : parseResult(data.result),
9082
+ processStartTime: field(data, "processStartTime", "string"),
9083
+ createTimestamp: field(data, "createTimestamp", "string"),
9084
+ updateTimestamp: field(data, "updateTimestamp", "string")
9085
+ });
9086
+ }
9087
+ function parseResult(value) {
9088
+ const data = record(value);
9089
+ return Object.freeze({
9090
+ url: field(data, "result_url", "string"),
9091
+ uploadError: field(data, "upload_error", "string"),
9092
+ uploadStatus: field(data, "upload_status", "string"),
9093
+ finalizeDurationMs: field(data, "finalize_duration_ms", "number"),
9094
+ compressionLevel: field(data, "compression_level", "string"),
9095
+ frameCount: field(data, "frame_count", "number"),
9096
+ fps: field(data, "fps", "number"),
9097
+ durationMs: field(data, "result_duration_ms", "number"),
9098
+ fileSizeBytes: field(data, "file_size_bytes", "number"),
9099
+ droppedFrameCount: field(data, "dropped_frame_count", "number"),
9100
+ originalFileSizeBytes: field(data, "original_file_size_bytes", "number")
9101
+ });
9102
+ }
9103
+
9104
+ // src/Service/NonRealtime/NonRealtimeTaskError.ts
9105
+ var NonRealtimeTaskError = class extends XmaxError {
7343
9106
  /**
7344
- * 创建 SDK 全局配置。
7345
- *
7346
- * @param init.apiKey 调用 Xmax 服务使用的 API Key。
7347
- * @param init.environment SDK 连接的服务环境;默认为国内环境。
7348
- * @param init.loggerOptions SDK 输出的日志类型;默认为空。
9107
+ * 使用 API 错误分类,不虚构服务端未返回的业务错误码。
7349
9108
  */
7350
- constructor(init) {
7351
- /**
7352
- * 服务身份与环境
7353
- */
7354
- /**
7355
- * 调用 Xmax 服务使用的 API Key。
7356
- */
7357
- __publicField(this, "apiKey");
7358
- /**
7359
- * SDK 连接的服务环境,同时决定日志细项语言:国内为中文,海外为英文。
7360
- */
7361
- __publicField(this, "environment");
9109
+ constructor(task, message) {
9110
+ super("API_ERROR" /* apiError */, message);
7362
9111
  /**
7363
- * 日志配置
9112
+ * 失败上下文
7364
9113
  */
9114
+ __publicField(this, "task");
9115
+ this.name = "NonRealtimeTaskError";
9116
+ this.task = task;
9117
+ }
9118
+ };
9119
+
9120
+ // src/Core/NonRealtime/XmaxNonRealtimeManager.ts
9121
+ var XmaxNonRealtimeManager = class {
9122
+ /**
9123
+ * 注入非实时任务服务。
9124
+ */
9125
+ constructor(tasks) {
7365
9126
  /**
7366
- * SDK 输出的日志类型;默认为不输出日志。
9127
+ * 服务层组件
7367
9128
  */
7368
- __publicField(this, "loggerOptions");
7369
- this.apiKey = init.apiKey.trim();
7370
- this.environment = init.environment ?? "china" /* china */;
7371
- this.loggerOptions = init.loggerOptions ?? 0 /* none */;
9129
+ __publicField(this, "tasks");
9130
+ this.tasks = tasks;
7372
9131
  }
7373
9132
  /**
7374
- * 校验全局配置。
7375
- * @throws API Key 为空时抛出 `XmaxError`。
9133
+ * 单次提交任务,不自动重试可能已扣费的请求。
7376
9134
  */
7377
- validate() {
7378
- if (this.apiKey.length === 0) {
9135
+ submitTask(options) {
9136
+ return this.tasks.submitTask(options);
9137
+ }
9138
+ /**
9139
+ * 取得任务快照,不把服务端失败终态当成查询失败。
9140
+ */
9141
+ getTask(taskUid, options) {
9142
+ return this.tasks.getTask(taskUid, options?.signal);
9143
+ }
9144
+ /**
9145
+ * 批量取得任务快照。
9146
+ */
9147
+ getTasks(taskUids) {
9148
+ return this.tasks.getTasks(taskUids);
9149
+ }
9150
+ /**
9151
+ * 分页取得任务列表。
9152
+ */
9153
+ listTasks(options) {
9154
+ return this.tasks.listTasks(options);
9155
+ }
9156
+ /**
9157
+ * 等待任务完成;查询串行执行,瞬时错误有限退避,取消与总超时覆盖请求及等待阶段。
9158
+ */
9159
+ async waitForCompletion(taskUid, options = {}) {
9160
+ const uid = validateTaskUid(taskUid);
9161
+ const interval = options.intervalMs ?? 2e3;
9162
+ const retries = options.maxConsecutiveRetries ?? 3;
9163
+ validateDelay(interval);
9164
+ if (options.timeoutMs !== void 0) {
9165
+ validateDelay(options.timeoutMs);
9166
+ }
9167
+ if (!Number.isSafeInteger(retries) || retries < 0) {
7379
9168
  throw new XmaxError(
7380
- "INVALID_API_KEY" /* invalidAPIKey */,
7381
- "API key cannot be empty"
9169
+ "INVALID_CONFIGURATION" /* invalidConfiguration */,
9170
+ "Retry count must be a non-negative integer"
7382
9171
  );
7383
9172
  }
9173
+ const controller = new AbortController();
9174
+ let timedOut = false;
9175
+ const abort = () => controller.abort();
9176
+ options.signal?.addEventListener("abort", abort, { once: true });
9177
+ if (options.signal?.aborted) {
9178
+ abort();
9179
+ }
9180
+ const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => {
9181
+ timedOut = true;
9182
+ abort();
9183
+ }, options.timeoutMs);
9184
+ let failures = 0;
9185
+ try {
9186
+ for (; ; ) {
9187
+ ensureActive(controller.signal);
9188
+ let task;
9189
+ try {
9190
+ task = await this.tasks.getTask(uid, controller.signal);
9191
+ } catch (error) {
9192
+ ensureActive(controller.signal);
9193
+ if (!isTransient(error) || failures >= retries) {
9194
+ throw error;
9195
+ }
9196
+ failures++;
9197
+ await delay(Math.min(interval * 2 ** (failures - 1), 3e4), controller.signal);
9198
+ continue;
9199
+ }
9200
+ ensureActive(controller.signal);
9201
+ failures = 0;
9202
+ try {
9203
+ void Promise.resolve(options.onTaskUpdated?.(task)).catch(() => {
9204
+ });
9205
+ } catch {
9206
+ }
9207
+ ensureActive(controller.signal);
9208
+ if (task.status === "error" /* error */) {
9209
+ throw new NonRealtimeTaskError(task, `Non-realtime task ${uid} failed`);
9210
+ }
9211
+ if (task.status === "completed" /* completed */) {
9212
+ if (!isResultAvailable(task)) {
9213
+ throw new NonRealtimeTaskError(
9214
+ task,
9215
+ `Non-realtime task ${uid} completed without a usable result`
9216
+ );
9217
+ }
9218
+ return task;
9219
+ }
9220
+ await delay(interval, controller.signal);
9221
+ }
9222
+ } catch (error) {
9223
+ if (timedOut) {
9224
+ throw new XmaxError(
9225
+ "TIMEOUT" /* timeout */,
9226
+ `Waiting for task ${uid} timed out; the server task was not cancelled`
9227
+ );
9228
+ }
9229
+ ensureActive(controller.signal);
9230
+ throw error;
9231
+ } finally {
9232
+ clearTimeout(timer);
9233
+ options.signal?.removeEventListener("abort", abort);
9234
+ }
7384
9235
  }
7385
9236
  };
9237
+ function validateDelay(value) {
9238
+ if (!Number.isFinite(value) || value <= 0 || value > 2147483647) {
9239
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Polling durations must be positive and within the timer range");
9240
+ }
9241
+ }
9242
+ function ensureActive(signal) {
9243
+ if (signal.aborted) {
9244
+ throw new XmaxError(
9245
+ "CANCELLED" /* cancelled */,
9246
+ "Task wait was cancelled locally; the server task was not cancelled"
9247
+ );
9248
+ }
9249
+ }
9250
+ function delay(ms, signal) {
9251
+ return new Promise((resolve, reject) => {
9252
+ const finish = () => {
9253
+ clearTimeout(timer);
9254
+ signal.removeEventListener("abort", finish);
9255
+ try {
9256
+ ensureActive(signal);
9257
+ resolve();
9258
+ } catch (error) {
9259
+ reject(error);
9260
+ }
9261
+ };
9262
+ const timer = setTimeout(finish, ms);
9263
+ signal.addEventListener("abort", finish, { once: true });
9264
+ if (signal.aborted) {
9265
+ finish();
9266
+ }
9267
+ });
9268
+ }
9269
+ function isTransient(error) {
9270
+ if (!(error instanceof XmaxError)) {
9271
+ return false;
9272
+ }
9273
+ return error.code === "NETWORK_ERROR" /* networkError */ || error.code === "TIMEOUT" /* timeout */ || error.code === "API_ERROR" /* apiError */ && (error.httpStatus ?? 0) >= 500 && (error.apiCode === void 0 || error.apiCode === error.httpStatus);
9274
+ }
9275
+ function isResultAvailable(task) {
9276
+ const result = task.result;
9277
+ if (!result?.url?.trim() || result.uploadError?.trim() || result.uploadStatus !== void 0 && result.uploadStatus !== "success") {
9278
+ return false;
9279
+ }
9280
+ try {
9281
+ const url = new URL(result.url);
9282
+ return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
9283
+ } catch {
9284
+ return false;
9285
+ }
9286
+ }
7386
9287
 
7387
9288
  // src/Core/XmaxClient.ts
7388
9289
  var XmaxClient = class {
@@ -7429,6 +9330,17 @@ var XmaxClient = class {
7429
9330
  environment: this.configuration.environment
7430
9331
  });
7431
9332
  }
9333
+ /**
9334
+ * 创建非实时视频任务 Manager,使用全局 API Key 和当前环境的非实时任务服务地址。
9335
+ * 本方法不发起网络请求或加载 RTC,任务由服务端持久化,可按 ID 恢复查询。
9336
+ */
9337
+ createNonRealtimeManager() {
9338
+ const apiService = new ApiService({
9339
+ apiKey: this.configuration.apiKey,
9340
+ baseURL: nonRealtimeApiBaseURL(this.configuration.environment)
9341
+ });
9342
+ return new XmaxNonRealtimeManager(new NonRealtimeTaskService(apiService));
9343
+ }
7432
9344
  /**
7433
9345
  * 创建媒体处理与能力查询 Service。
7434
9346
  *
@@ -7440,7 +9352,7 @@ var XmaxClient = class {
7440
9352
  /**
7441
9353
  * 创建文件存储 Service。
7442
9354
  *
7443
- * @returns 可上传图片到对象存储的存储 Service,返回地址可用作参考图路径。
9355
+ * @returns 可上传图片和视频到对象存储的存储 Service。
7444
9356
  */
7445
9357
  createStorageService() {
7446
9358
  return new StorageService({ apiService: this.apiService });
@@ -7452,45 +9364,36 @@ var RealtimeConfiguration = class {
7452
9364
  /**
7453
9365
  * 创建实时业务配置。
7454
9366
  *
7455
- * @param init.model 实时生成业务使用的模型。
7456
- * @param init.provider RTC 提供方,默认 TRTC;创建后不可切换。
7457
- * @param init.isFrameInterpolationEnabled 是否默认开启远端生成画面的插帧。
7458
- * @param init.frameInterpolation 插帧开关和显示帧率上限的详细配置。
9367
+ * @param init.model 实时生成业务使用的模型;未知名称原样透传,默认配置同 x2.1-preview。
9368
+ * @param init.provider RTC 提供方,默认按模型定义选择;必须受模型支持,创建后不可切换。
9369
+ * @param init.isFrameInterpolationEnabled 是否默认开启远端生成画面的插帧,默认 false。
7459
9370
  */
7460
9371
  constructor(init) {
7461
- /**
7462
- * RTC 配置
7463
- */
7464
- __publicField(this, "provider");
7465
- /**
7466
- * 模型配置
7467
- */
7468
9372
  /**
7469
9373
  * 实时生成业务使用的模型。
7470
9374
  */
7471
9375
  __publicField(this, "model");
7472
9376
  /**
7473
- * 远端插帧配置
9377
+ * 实时连接使用的 RTC 提供方。
7474
9378
  */
9379
+ __publicField(this, "provider");
7475
9380
  /**
7476
9381
  * 是否默认请求开启远端生成画面的插帧。
7477
9382
  */
7478
9383
  __publicField(this, "isFrameInterpolationEnabled");
7479
- /**
7480
- * 已补全默认值的只读插帧配置。
7481
- */
7482
- __publicField(this, "frameInterpolation");
7483
- this.provider = init.provider ?? "trtc" /* trtc */;
9384
+ const providers = supportedRtcProviders(init.model);
9385
+ this.provider = init.provider ?? providers[0];
7484
9386
  if (!Object.values(RtcProvider).includes(this.provider)) {
7485
9387
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported RTC provider");
7486
9388
  }
7487
- this.model = init.model;
7488
- const targetFrameRate = init.frameInterpolation?.targetFrameRate ?? 60;
7489
- if (!Number.isFinite(targetFrameRate) || targetFrameRate < 1 || targetFrameRate > 60) {
7490
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Interpolation targetFrameRate must be between 1 and 60");
9389
+ if (!providers.includes(this.provider)) {
9390
+ throw new XmaxError(
9391
+ "INVALID_CONFIGURATION" /* invalidConfiguration */,
9392
+ `Model ${init.model} does not support RTC provider ${this.provider}`
9393
+ );
7491
9394
  }
7492
- this.isFrameInterpolationEnabled = init.frameInterpolation?.enabled ?? init.isFrameInterpolationEnabled ?? false;
7493
- this.frameInterpolation = Object.freeze({ enabled: this.isFrameInterpolationEnabled, targetFrameRate });
9395
+ this.model = init.model;
9396
+ this.isFrameInterpolationEnabled = init.isFrameInterpolationEnabled ?? false;
7494
9397
  }
7495
9398
  };
7496
9399
 
@@ -7501,26 +9404,143 @@ var VideoContentMode = /* @__PURE__ */ ((VideoContentMode2) => {
7501
9404
  return VideoContentMode2;
7502
9405
  })(VideoContentMode || {});
7503
9406
 
7504
- // src/Service/Realtime/RealtimeContext.ts
7505
- var RealtimeContext = class {
9407
+ // src/Render/Video/RenderStatisticsController.ts
9408
+ var RenderStatisticsController = class {
7506
9409
  /**
7507
- * 创建实时生成条件并规范化文本和参考路径。
9410
+ * 绑定用于观察原视频帧的元素,调用 start 后才开始采集。
7508
9411
  */
7509
- constructor(init) {
9412
+ constructor(video) {
9413
+ __publicField(this, "video", video);
7510
9414
  /**
7511
- * 生成条件
9415
+ * 统计回调
7512
9416
  */
9417
+ __publicField(this, "handler");
7513
9418
  /**
7514
- * 实时生成使用的文本条件。
9419
+ * 帧观察与采样生命周期
7515
9420
  */
7516
- __publicField(this, "prompt");
9421
+ __publicField(this, "timer");
9422
+ __publicField(this, "callbackID");
9423
+ __publicField(this, "sequence", 0);
7517
9424
  /**
7518
- * 已上传参考图片的可选远端路径。
9425
+ * 采样窗口与输出方式
7519
9426
  */
7520
- __publicField(this, "referencePath");
7521
- this.prompt = init.prompt.trim();
7522
- const normalizedReferencePath = init.referencePath?.trim();
7523
- this.referencePath = normalizedReferencePath || void 0;
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
+ }
7524
9544
  }
7525
9545
  };
7526
9546
 
@@ -9516,11 +11536,11 @@ var _FrameInterpolationManager = class _FrameInterpolationManager {
9516
11536
  });
9517
11537
  }
9518
11538
  static async create(canvas, size, onFailure, signal) {
9519
- const ensureActive = () => {
11539
+ const ensureActive2 = () => {
9520
11540
  if (signal.aborted) throw new XmaxError("CANCELLED" /* cancelled */, "Interpolation initialization cancelled");
9521
11541
  };
9522
11542
  const adapter = await frameInterpolationAdapter(size);
9523
- ensureActive();
11543
+ ensureActive2();
9524
11544
  if (!adapter) throw new XmaxError("FRAME_INTERPOLATION_UNSUPPORTED" /* frameInterpolationUnsupported */, "WebGPU shader-f16 is unavailable");
9525
11545
  const device = await adapter.requestDevice({ requiredFeatures: ["shader-f16"] });
9526
11546
  const manager = new _FrameInterpolationManager(device, canvas, size, onFailure);
@@ -9531,9 +11551,9 @@ var _FrameInterpolationManager = class _FrameInterpolationManager {
9531
11551
  if (!manager.destroyed) onFailure(new Error(`Interpolation GPU device lost: ${info.message}`));
9532
11552
  });
9533
11553
  try {
9534
- ensureActive();
11554
+ ensureActive2();
9535
11555
  await manager.initialize();
9536
- ensureActive();
11556
+ ensureActive2();
9537
11557
  return manager;
9538
11558
  } catch (error) {
9539
11559
  manager.destroy();
@@ -9659,6 +11679,7 @@ __publicField(_FrameInterpolationManager, "originalSlots", 3);
9659
11679
  var FrameInterpolationManager = _FrameInterpolationManager;
9660
11680
 
9661
11681
  // src/Render/Video/RemoteVideoFramePipeline.ts
11682
+ var INTERPOLATION_FRAME_RATE = 60;
9662
11683
  var RemoteVideoFramePipeline = class {
9663
11684
  constructor(video, canvas, options, createProcessor = FrameInterpolationManager.create) {
9664
11685
  __publicField(this, "video", video);
@@ -9686,6 +11707,10 @@ var RemoteVideoFramePipeline = class {
9686
11707
  __publicField(this, "previousPresented");
9687
11708
  __publicField(this, "slowPairs", 0);
9688
11709
  __publicField(this, "active", false);
11710
+ /**
11711
+ * 帧时间基准:媒体时间异常后,本管线持续使用回调时钟,避免反复切换。
11712
+ */
11713
+ __publicField(this, "timeSource", "mediaTime");
9689
11714
  /** vsync 显示节拍:弹出到点的帧并显示最新的一张。 */
9690
11715
  __publicField(this, "tick", (now) => {
9691
11716
  if (this.stopped) return;
@@ -9732,8 +11757,8 @@ var RemoteVideoFramePipeline = class {
9732
11757
  if (!this.loading) void this.initialize();
9733
11758
  return;
9734
11759
  }
9735
- const time = metadata.mediaTime * 1e3;
9736
- 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;
9737
11762
  const previous = this.previousTime;
9738
11763
  const interval = previous === void 0 ? 0 : time - previous;
9739
11764
  const consecutive = this.previousPresented === void 0 || metadata.presentedFrames === this.previousPresented + 1;
@@ -9753,9 +11778,27 @@ var RemoteVideoFramePipeline = class {
9753
11778
  this.delay = interval;
9754
11779
  this.enqueue({ slot, time });
9755
11780
  this.startTick();
9756
- if (!consecutive || interval < 2e3 / this.options.targetFrameRate - 1 || previousSlot === void 0 || this.busy) return;
11781
+ if (!consecutive || interval < 2e3 / INTERPOLATION_FRAME_RATE - 1 || previousSlot === void 0 || this.busy) {
11782
+ return;
11783
+ }
9757
11784
  this.interpolate(previousSlot, slot, previous, time, interval);
9758
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
+ }
9759
11802
  /** 帧按显示时间升序入队;同一时间的内容后到先出。 */
9760
11803
  enqueue(frame) {
9761
11804
  const index = this.queue.findIndex((queued) => queued.time >= frame.time);
@@ -9782,8 +11825,10 @@ var RemoteVideoFramePipeline = class {
9782
11825
  this.fail(new Error("Interpolation exceeded the GPU frame budget for 5 consecutive pairs"));
9783
11826
  return;
9784
11827
  }
9785
- if (this.position(performance.now()) >= midpoint) return;
9786
- this.enqueue({ slot, time: midpoint });
11828
+ if (this.position(performance.now()) >= midpoint) {
11829
+ return;
11830
+ }
11831
+ this.enqueue({ slot, time: midpoint, interpolated: true });
9787
11832
  }).catch((error) => {
9788
11833
  if (!this.stopped) this.fail(error);
9789
11834
  });
@@ -9802,6 +11847,7 @@ var RemoteVideoFramePipeline = class {
9802
11847
  this.displayedTime = frame.time;
9803
11848
  this.canvas.style.visibility = "visible";
9804
11849
  this.setActive(true);
11850
+ this.options.onFramePresented?.(frame.interpolated ?? false);
9805
11851
  }
9806
11852
  async initialize() {
9807
11853
  this.loading = true;
@@ -9831,11 +11877,13 @@ var RemoteVideoFramePipeline = class {
9831
11877
  this.active = active;
9832
11878
  this.options.onActiveChange?.(active);
9833
11879
  }
9834
- resetTimeline() {
11880
+ resetTimeline(preserveGpuTimeout = false) {
9835
11881
  this.generation++;
9836
11882
  this.queue = [];
9837
- clearTimeout(this.stallTimer);
9838
- this.stallTimer = void 0;
11883
+ if (!preserveGpuTimeout) {
11884
+ clearTimeout(this.stallTimer);
11885
+ this.stallTimer = void 0;
11886
+ }
9839
11887
  if (this.raf !== void 0) cancelAnimationFrame(this.raf);
9840
11888
  this.raf = void 0;
9841
11889
  this.anchor = void 0;
@@ -9914,6 +11962,10 @@ var XmaxVideoView = class {
9914
11962
  __publicField(this, "interpolationOptions");
9915
11963
  __publicField(this, "interpolationPipeline");
9916
11964
  __publicField(this, "interpolationCanvas");
11965
+ /**
11966
+ * 呈现统计组件
11967
+ */
11968
+ __publicField(this, "statisticsController");
9917
11969
  /**
9918
11970
  * DOM 挂载状态
9919
11971
  */
@@ -9924,6 +11976,7 @@ var XmaxVideoView = class {
9924
11976
  this.element.style.overflow = "hidden";
9925
11977
  this.element.style.backgroundColor = "black";
9926
11978
  this.videoElement = document.createElement("video");
11979
+ this.statisticsController = new RenderStatisticsController(this.videoElement);
9927
11980
  this.videoElement.autoplay = true;
9928
11981
  this.videoElement.playsInline = true;
9929
11982
  this.videoElement.muted = true;
@@ -9940,6 +11993,20 @@ var XmaxVideoView = class {
9940
11993
  get track() {
9941
11994
  return this.currentTrack;
9942
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
+ }
9943
12010
  /**
9944
12011
  * 解绑旧轨道后接入新轨道,并为新内容重新启用一次性首帧通知。
9945
12012
  */
@@ -9992,14 +12059,15 @@ var XmaxVideoView = class {
9992
12059
  attach(to) {
9993
12060
  this.detached = false;
9994
12061
  to.appendChild(this.element);
9995
- if (this.videoElement.srcObject && this.videoElement.paused && typeof this.videoElement.play === "function") {
12062
+ if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && this.videoElement.autoplay && !this.videoElement.ended && this.videoElement.paused && typeof this.videoElement.play === "function") {
9996
12063
  void this.videoElement.play().catch((error) => {
9997
12064
  XmaxLogger.render.warning(() => `\u6062\u590D\u89C6\u9891\u64AD\u653E\u5931\u8D25 (Failed to Resume Video Playback)
9998
12065
  \u2514\u2500 ${String(error)}`);
9999
12066
  });
10000
12067
  }
10001
12068
  this.startInterpolation();
10002
- if (this.videoElement.srcObject && !this.hasNotifiedFrameDisplay) {
12069
+ this.startRenderStatistics();
12070
+ if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && !this.hasNotifiedFrameDisplay) {
10003
12071
  this.armFrameDisplayNotification();
10004
12072
  }
10005
12073
  }
@@ -10008,10 +12076,34 @@ var XmaxVideoView = class {
10008
12076
  */
10009
12077
  detach() {
10010
12078
  this.detached = true;
12079
+ this.statisticsController.stop();
10011
12080
  this.stopInterpolation();
10012
12081
  this.cancelFrameDisplayNotification();
12082
+ if (this.videoElement.getAttribute("src")) {
12083
+ this.videoElement.pause();
12084
+ }
10013
12085
  this.element.remove();
10014
12086
  }
12087
+ /**
12088
+ * 设置网络视频预览;静音播放一次,不捕获或发布媒体轨道。 @internal
12089
+ */
12090
+ setVideoURL(url) {
12091
+ this.statisticsController.stop();
12092
+ this.stopInterpolation();
12093
+ this.cancelFrameDisplayNotification();
12094
+ this.hasNotifiedFrameDisplay = false;
12095
+ this.videoElement.pause();
12096
+ this.videoElement.srcObject = null;
12097
+ this.videoElement.removeAttribute("src");
12098
+ this.videoElement.muted = true;
12099
+ this.videoElement.loop = false;
12100
+ if (url) {
12101
+ this.videoElement.src = url;
12102
+ this.armFrameDisplayNotification();
12103
+ }
12104
+ this.videoElement.load();
12105
+ this.startRenderStatistics();
12106
+ }
10015
12107
  /**
10016
12108
  * 直接设置渲染用的媒体流(绑定管线和直接渲染共用)。 @internal
10017
12109
  */
@@ -10019,13 +12111,16 @@ var XmaxVideoView = class {
10019
12111
  if (this.videoElement.srcObject === stream) {
10020
12112
  return;
10021
12113
  }
12114
+ this.statisticsController.stop();
10022
12115
  this.stopInterpolation();
10023
12116
  this.cancelFrameDisplayNotification();
10024
12117
  this.hasNotifiedFrameDisplay = false;
10025
12118
  this.videoElement.srcObject = stream;
10026
12119
  if (stream) {
12120
+ this.videoElement.autoplay = true;
10027
12121
  this.armFrameDisplayNotification();
10028
12122
  this.startInterpolation();
12123
+ this.startRenderStatistics();
10029
12124
  }
10030
12125
  }
10031
12126
  /**
@@ -10055,7 +12150,18 @@ var XmaxVideoView = class {
10055
12150
  this.interpolationCanvas = canvas;
10056
12151
  this.applyContentMode();
10057
12152
  this.element.appendChild(canvas);
10058
- 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
+ });
10059
12165
  this.interpolationPipeline = pipeline;
10060
12166
  pipeline.start();
10061
12167
  }
@@ -10090,6 +12196,7 @@ var XmaxVideoView = class {
10090
12196
  * 解绑当前轨道并清空渲染内容。
10091
12197
  */
10092
12198
  detachCurrentTrack() {
12199
+ this.statisticsController.stop();
10093
12200
  const track = this.currentTrack;
10094
12201
  if (track) {
10095
12202
  VideoRenderRegistry.binding(track)?.detachHandler(this);
@@ -10097,6 +12204,9 @@ var XmaxVideoView = class {
10097
12204
  this.setFrameInterpolation(void 0);
10098
12205
  this.cancelFrameDisplayNotification();
10099
12206
  this.videoElement.srcObject = null;
12207
+ if (this.videoElement.getAttribute("src")) {
12208
+ this.setVideoURL(null);
12209
+ }
10100
12210
  }
10101
12211
  /**
10102
12212
  * 首帧提交合成器时通知;旧浏览器回退到 playing 近似判断。
@@ -10152,6 +12262,12 @@ var XmaxVideoView = class {
10152
12262
  this.interpolationCanvas.style.transform = this.mirrored ? "scaleX(-1)" : "";
10153
12263
  }
10154
12264
  }
12265
+ /**
12266
+ * 视图未卸载时启动呈现统计,采样及媒体就绪检查由统计组件负责。
12267
+ */
12268
+ startRenderStatistics() {
12269
+ if (!this.detached) this.statisticsController.start();
12270
+ }
10155
12271
  };
10156
12272
 
10157
12273
  // src/Render/Video/XmaxRealtimeVideoView.ts
@@ -10324,25 +12440,31 @@ export {
10324
12440
  XmaxError,
10325
12441
  RealtimeVideoEncoderPreference,
10326
12442
  RealtimeVideoFormat,
12443
+ RtcProvider,
10327
12444
  RealtimeModel,
12445
+ modelDisplayName,
10328
12446
  defaultFrameRate,
10329
12447
  defaultCameraVideoFormat,
12448
+ supportedRtcProviders,
10330
12449
  MediaService,
10331
12450
  StoredFile,
10332
12451
  StorageService,
10333
12452
  CameraPosition,
10334
- RtcProvider,
10335
12453
  RealtimeVideoTrack,
10336
12454
  RealtimeMediaStream,
12455
+ RealtimeVideoSampleMethod,
12456
+ RealtimeContext,
10337
12457
  RealtimeConnectionState,
10338
12458
  RealtimeReason,
10339
12459
  RealtimeState,
10340
12460
  XmaxConfiguration,
12461
+ NonRealtimeQuality,
12462
+ NonRealtimeTaskStatus,
12463
+ NonRealtimeTaskError,
10341
12464
  XmaxClient,
10342
12465
  RealtimeConfiguration,
10343
12466
  VideoContentMode,
10344
- RealtimeContext,
10345
12467
  XmaxVideoView,
10346
12468
  XmaxRealtimeVideoView
10347
12469
  };
10348
- //# sourceMappingURL=chunk-FMAQXPTE.js.map
12470
+ //# sourceMappingURL=chunk-PLFWPS5A.js.map