@xmaxai/web-sdk 1.0.4 → 1.0.5

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.5";
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,6 +588,14 @@ 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";
@@ -569,12 +604,28 @@ var RealtimeModel = /* @__PURE__ */ ((RealtimeModel2) => {
569
604
  RealtimeModel2["x2_1_preview"] = "x2.1-preview";
570
605
  return RealtimeModel2;
571
606
  })(RealtimeModel || {});
607
+ function isKnownModel(model) {
608
+ if (typeof model !== "string" || !model.trim()) {
609
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Realtime model must be a non-empty string");
610
+ }
611
+ return Object.values(RealtimeModel).includes(model);
612
+ }
613
+ function modelDefaults(model) {
614
+ return isKnownModel(model) ? model : "x2.1-preview" /* x2_1_preview */;
615
+ }
616
+ var MODEL_DISPLAY_NAMES = {
617
+ ["x2.0" /* x2_0 */]: "X2.0",
618
+ ["x2.0-trtc" /* x2_0_trtc */]: "X2.1-preview-trtc",
619
+ ["x2.0-agora" /* x2_0_agora */]: "X2.1-preview-agora",
620
+ ["x2.1-preview" /* x2_1_preview */]: "X2.1-preview-vertc"
621
+ };
572
622
  var MODEL_BASE_URLS = {
573
623
  ["x2.0-agora" /* x2_0_agora */]: "https://dev.xmaxai.com/open/api/v1",
574
624
  ["x2.1-preview" /* x2_1_preview */]: "https://dev.xmaxai.com/open/api/v1"
575
625
  };
576
626
  var RESOLUTION_BUCKETS = {
577
- ["x2.1-preview" /* x2_1_preview */]: [
627
+ ["x2.0" /* x2_0 */]: [],
628
+ ["x2.0-trtc" /* x2_0_trtc */]: [
578
629
  { width: 1024, height: 1920 },
579
630
  { width: 1920, height: 1024 }
580
631
  ],
@@ -582,29 +633,31 @@ var RESOLUTION_BUCKETS = {
582
633
  { width: 1024, height: 1920 },
583
634
  { width: 1920, height: 1024 }
584
635
  ],
585
- ["x2.0" /* x2_0 */]: [],
586
- ["x2.0-trtc" /* x2_0_trtc */]: [
636
+ ["x2.1-preview" /* x2_1_preview */]: [
587
637
  { width: 1024, height: 1920 },
588
638
  { width: 1920, height: 1024 }
589
639
  ]
590
640
  };
591
641
  var MAXIMUM_INPUT_PIXELS = {
592
- ["x2.1-preview" /* x2_1_preview */]: 21e5,
593
- ["x2.0-agora" /* x2_0_agora */]: 21e5,
594
642
  ["x2.0" /* x2_0 */]: 128e4,
595
- ["x2.0-trtc" /* x2_0_trtc */]: 21e5
643
+ ["x2.0-trtc" /* x2_0_trtc */]: 21e5,
644
+ ["x2.0-agora" /* x2_0_agora */]: 21e5,
645
+ ["x2.1-preview" /* x2_1_preview */]: 21e5
596
646
  };
647
+ function modelDisplayName(model) {
648
+ return isKnownModel(model) ? MODEL_DISPLAY_NAMES[model] : model;
649
+ }
597
650
  function modelBaseURL(model) {
598
- return MODEL_BASE_URLS[model];
651
+ return MODEL_BASE_URLS[modelDefaults(model)];
599
652
  }
600
653
  function resolutionBuckets(model) {
601
- return RESOLUTION_BUCKETS[model];
654
+ return RESOLUTION_BUCKETS[modelDefaults(model)];
602
655
  }
603
656
  function minimumInputPixels(_model) {
604
657
  return 6e5;
605
658
  }
606
659
  function maximumInputPixels(model) {
607
- return MAXIMUM_INPUT_PIXELS[model];
660
+ return MAXIMUM_INPUT_PIXELS[modelDefaults(model)];
608
661
  }
609
662
  function inputSizeAlignment(_model) {
610
663
  return 32;
@@ -613,11 +666,24 @@ function defaultFrameRate(_model) {
613
666
  return 30;
614
667
  }
615
668
  function defaultCameraVideoFormat(model) {
616
- if (model === "x2.0" /* x2_0 */) {
669
+ if (modelDefaults(model) === "x2.0" /* x2_0 */) {
617
670
  return new RealtimeVideoFormat({ width: 832, height: 1472, fps: 30 });
618
671
  }
619
672
  return new RealtimeVideoFormat({ width: 1024, height: 1920, fps: 30 });
620
673
  }
674
+ var MODEL_RTC_PROVIDERS = {
675
+ ["x2.0" /* x2_0 */]: Object.freeze(["vertc" /* vertc */]),
676
+ ["x2.0-trtc" /* x2_0_trtc */]: Object.freeze(["trtc" /* trtc */]),
677
+ ["x2.0-agora" /* x2_0_agora */]: Object.freeze(["agora" /* agora */]),
678
+ ["x2.1-preview" /* x2_1_preview */]: Object.freeze(["vertc" /* vertc */])
679
+ };
680
+ function supportedRtcProviders(model) {
681
+ return isKnownModel(model) ? MODEL_RTC_PROVIDERS[model] : CUSTOM_MODEL_RTC_PROVIDERS;
682
+ }
683
+ var CUSTOM_MODEL_RTC_PROVIDERS = Object.freeze([
684
+ ...MODEL_RTC_PROVIDERS["x2.1-preview" /* x2_1_preview */],
685
+ ...Object.values(RtcProvider).filter((provider) => !MODEL_RTC_PROVIDERS["x2.1-preview" /* x2_1_preview */].includes(provider))
686
+ ]);
621
687
 
622
688
  // src/Foundation/Media/Video/FrameInterpolationSupport.ts
623
689
  async function frameInterpolationAdapter(size) {
@@ -802,7 +868,14 @@ var CONTENT_TYPES = {
802
868
  gif: "image/gif",
803
869
  bmp: "image/bmp",
804
870
  heic: "image/heic",
805
- heif: "image/heif"
871
+ heif: "image/heif",
872
+ mp4: "video/mp4",
873
+ m4v: "video/mp4",
874
+ mov: "video/quicktime",
875
+ webm: "video/webm",
876
+ avi: "video/x-msvideo",
877
+ mkv: "video/x-matroska",
878
+ ogv: "video/ogg"
806
879
  };
807
880
  var StorageService = class _StorageService {
808
881
  /**
@@ -836,6 +909,81 @@ var StorageService = class _StorageService {
836
909
  * 上传图片并返回访问地址。
837
910
  */
838
911
  async uploadImage(options) {
912
+ return this.upload(options, "Image");
913
+ }
914
+ /**
915
+ * 普通上传原始视频并返回访问地址,不进行转码、压缩或分片。
916
+ */
917
+ async uploadVideo(options) {
918
+ return this.upload(options, "Video");
919
+ }
920
+ /**
921
+ * 下载原始视频文件并交给浏览器保存,支持取消网络下载。
922
+ */
923
+ async downloadVideo(options) {
924
+ const fileName = _StorageService.validateFileName(options.fileName);
925
+ let url;
926
+ try {
927
+ url = new URL(options.url.trim());
928
+ } catch {
929
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Invalid video download URL");
930
+ }
931
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
932
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video download requires an HTTP(S) URL without embedded credentials");
933
+ }
934
+ if (typeof document === "undefined" || !document.body) {
935
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video saving requires a browser document");
936
+ }
937
+ let objectURL;
938
+ let link;
939
+ let saveRequested = false;
940
+ try {
941
+ if (options.signal?.aborted) {
942
+ throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
943
+ }
944
+ const response = await fetch(options.url.trim(), { credentials: "omit", signal: options.signal });
945
+ if (!response.ok) {
946
+ throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, "Video download failed", { httpStatus: response.status });
947
+ }
948
+ const data = await response.blob();
949
+ if (options.signal?.aborted) {
950
+ throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
951
+ }
952
+ if (data.size === 0) {
953
+ throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, "Downloaded video is empty");
954
+ }
955
+ objectURL = URL.createObjectURL(data);
956
+ link = document.createElement("a");
957
+ link.href = objectURL;
958
+ link.download = fileName;
959
+ link.hidden = true;
960
+ document.body.appendChild(link);
961
+ link.click();
962
+ saveRequested = true;
963
+ } catch (error) {
964
+ if (options.signal?.aborted) {
965
+ throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
966
+ }
967
+ if (error instanceof XmaxError) {
968
+ throw error;
969
+ }
970
+ throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, error instanceof Error ? error.message : String(error));
971
+ } finally {
972
+ link?.remove();
973
+ if (objectURL) {
974
+ const resourceURL = objectURL;
975
+ if (saveRequested) {
976
+ setTimeout(() => URL.revokeObjectURL(resourceURL), 6e4);
977
+ } else {
978
+ URL.revokeObjectURL(resourceURL);
979
+ }
980
+ }
981
+ }
982
+ }
983
+ /**
984
+ * 获取临时凭证并通过 COS putObject 上传文件,统一处理进度、结果和错误。
985
+ */
986
+ async upload(options, mediaType) {
839
987
  const startedAt = Date.now();
840
988
  try {
841
989
  const safeName = _StorageService.validateFileName(options.fileName);
@@ -844,7 +992,7 @@ var StorageService = class _StorageService {
844
992
  if (body.size === 0) {
845
993
  throw new XmaxError(
846
994
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
847
- "Image data cannot be empty"
995
+ `${mediaType} data cannot be empty`
848
996
  );
849
997
  }
850
998
  const temporary = await this.fetchStorageConfiguration();
@@ -982,7 +1130,7 @@ var StorageService = class _StorageService {
982
1130
  if (!safeName || safeName.includes("/") || safeName.includes("\\")) {
983
1131
  throw new XmaxError(
984
1132
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
985
- "Invalid file name for storage upload"
1133
+ "Invalid file name for storage operation"
986
1134
  );
987
1135
  }
988
1136
  return safeName;
@@ -1185,6 +1333,8 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1185
1333
  __publicField(this, "lifecycle", 0);
1186
1334
  __publicField(this, "camera");
1187
1335
  __publicField(this, "microphone");
1336
+ __publicField(this, "externalVideo");
1337
+ __publicField(this, "externalAudio");
1188
1338
  __publicField(this, "connection");
1189
1339
  __publicField(this, "roomVersion", 0);
1190
1340
  /**
@@ -1267,12 +1417,15 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1267
1417
  async destroy() {
1268
1418
  this.lifecycle++;
1269
1419
  const client = this.client;
1270
- this.listener = void 0;
1271
1420
  this.clearRoom();
1272
1421
  this.client = void 0;
1273
1422
  client?.removeAllListeners();
1274
1423
  this.camera?.close();
1275
1424
  this.microphone?.close();
1425
+ this.externalVideo?.close();
1426
+ this.externalAudio?.close();
1427
+ this.externalVideo = void 0;
1428
+ this.externalAudio = void 0;
1276
1429
  this.camera = void 0;
1277
1430
  this.microphone = void 0;
1278
1431
  const sdk = this.sdk;
@@ -1291,7 +1444,9 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1291
1444
  */
1292
1445
  async startCameraCapture(options) {
1293
1446
  const client = this.requireClient();
1294
- if (this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is already running");
1447
+ if (this.camera || this.externalVideo) {
1448
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "A local video source is already active");
1449
+ }
1295
1450
  try {
1296
1451
  const captureSize = rtcOrientedVideoSize(options);
1297
1452
  const camera = await this.sdk.createCameraVideoTrack({
@@ -1326,11 +1481,42 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1326
1481
  camera?.close();
1327
1482
  }
1328
1483
  /**
1329
- * 应用公共编码配置,补偿移动端竖屏的宽高转置。
1484
+ * 将外部原生轨包装为 Agora 本地轨,不申请设备权限;失败时释放本次克隆。
1485
+ */
1486
+ async setExternalMediaTracks(tracks) {
1487
+ this.requireClient();
1488
+ if (this.camera || this.microphone || this.externalVideo) {
1489
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
1490
+ }
1491
+ if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
1492
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media requires live video and optional audio tracks");
1493
+ }
1494
+ const video = tracks.videoTrack.clone();
1495
+ let audio;
1496
+ let wrappedVideo;
1497
+ let wrappedAudio;
1498
+ try {
1499
+ audio = tracks.audioTrack?.clone();
1500
+ wrappedVideo = this.sdk.createCustomVideoTrack({ mediaStreamTrack: video });
1501
+ if (audio) {
1502
+ wrappedAudio = this.sdk.createCustomAudioTrack({ mediaStreamTrack: audio, encoderConfig: "music_standard" });
1503
+ }
1504
+ this.externalVideo = wrappedVideo;
1505
+ this.externalAudio = wrappedAudio;
1506
+ } catch (error) {
1507
+ wrappedVideo?.close();
1508
+ wrappedAudio?.close();
1509
+ video.stop();
1510
+ audio?.stop();
1511
+ throw this.mapError(error);
1512
+ }
1513
+ }
1514
+ /**
1515
+ * 应用公共编码配置;仅相机源补偿移动端转置,Canvas 外部源使用原始方向。
1330
1516
  */
1331
1517
  async configureVideoEncoding(config) {
1332
- const camera = this.requireCamera();
1333
- const encodingSize = rtcOrientedVideoSize(config);
1518
+ const camera = this.requireLocalVideo();
1519
+ const encodingSize = this.externalVideo ? config : rtcOrientedVideoSize(config);
1334
1520
  await this.run(async () => {
1335
1521
  await camera.setEncoderConfiguration({
1336
1522
  width: encodingSize.width,
@@ -1349,7 +1535,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1349
1535
  await this.rejoinTask?.catch(() => {
1350
1536
  });
1351
1537
  const client = this.requireClient();
1352
- if (configuration.provider !== "agora" || this.connection) {
1538
+ if (configuration.provider !== "agora" /* agora */ || this.connection) {
1353
1539
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Agora credentials and an idle room are required");
1354
1540
  }
1355
1541
  const version = ++this.roomVersion;
@@ -1385,7 +1571,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1385
1571
  if (signal?.aborted) throw this.cancelled();
1386
1572
  const current = this.connection;
1387
1573
  const client = this.requireClient();
1388
- if (!current || config.provider !== "agora" || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
1574
+ if (!current || config.provider !== "agora" /* agora */ || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
1389
1575
  throw new XmaxError("SESSION_ERROR" /* sessionError */, "Agora session binding changed");
1390
1576
  }
1391
1577
  const version = this.roomVersion;
@@ -1454,12 +1640,14 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1454
1640
  await this.run(() => client.leave());
1455
1641
  throw this.cancelled();
1456
1642
  }
1457
- if (this.videoPublishRequested && this.camera) {
1458
- await this.run(() => client.publish(this.camera));
1643
+ const video = this.externalVideo ?? this.camera;
1644
+ const audio = this.externalVideo ? this.externalAudio : this.microphone;
1645
+ if (this.videoPublishRequested && video) {
1646
+ await this.run(() => client.publish(video));
1459
1647
  this.ensureRoomCurrent(client, version);
1460
1648
  }
1461
- if (this.audioPublishRequested && this.microphone) {
1462
- await this.run(() => client.publish(this.microphone));
1649
+ if (this.audioPublishRequested && audio) {
1650
+ await this.run(() => client.publish(audio));
1463
1651
  this.ensureRoomCurrent(client, version);
1464
1652
  }
1465
1653
  for (const user of client.remoteUsers) {
@@ -1480,7 +1668,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1480
1668
  */
1481
1669
  async publishLocalVideo() {
1482
1670
  const client = this.requireClient();
1483
- const camera = this.requireCamera();
1671
+ const camera = this.requireLocalVideo();
1484
1672
  this.videoPublishRequested = true;
1485
1673
  await this.rejoinOperation;
1486
1674
  if (client !== this.client || !this.videoPublishRequested) throw this.cancelled();
@@ -1493,10 +1681,13 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1493
1681
  async unpublishLocalVideo() {
1494
1682
  this.videoPublishRequested = false;
1495
1683
  if (this.rejoinTask || this.tokenExpired) return;
1496
- if (this.client && this.camera) await this.run(() => this.client.unpublish(this.camera));
1684
+ const video = this.externalVideo ?? this.camera;
1685
+ if (this.client && video) {
1686
+ await this.run(() => this.client.unpublish(video));
1687
+ }
1497
1688
  }
1498
1689
  /**
1499
- * 首次发布时申请麦克风;取消后的迟到采集立即释放。
1690
+ * 外部源只发布提供的音轨;相机源首次发布时申请麦克风,迟到采集立即释放。
1500
1691
  */
1501
1692
  async publishLocalAudio() {
1502
1693
  const client = this.requireClient();
@@ -1504,6 +1695,13 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1504
1695
  await this.rejoinOperation;
1505
1696
  const version = this.roomVersion;
1506
1697
  if (client !== this.client || !this.audioPublishRequested) throw this.cancelled();
1698
+ if (this.externalVideo) {
1699
+ const audio = this.externalAudio;
1700
+ if (audio && !client.localTracks.includes(audio)) {
1701
+ await this.run(() => client.publish(audio));
1702
+ }
1703
+ return;
1704
+ }
1507
1705
  try {
1508
1706
  if (!this.microphone) {
1509
1707
  const track = await this.sdk.createMicrophoneAudioTrack();
@@ -1524,7 +1722,10 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1524
1722
  async unpublishLocalAudio() {
1525
1723
  this.audioPublishRequested = false;
1526
1724
  if (this.rejoinTask || this.tokenExpired) return;
1527
- if (this.client && this.microphone) await this.run(() => this.client.unpublish(this.microphone));
1725
+ const audio = this.externalVideo ? this.externalAudio : this.microphone;
1726
+ if (this.client && audio) {
1727
+ await this.run(() => this.client.unpublish(audio));
1728
+ }
1528
1729
  }
1529
1730
  /**
1530
1731
  * 订阅远端主视频并返回原生轨道,不创建额外播放视图。
@@ -1755,6 +1956,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1755
1956
  if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
1756
1957
  return this.camera;
1757
1958
  }
1959
+ /**
1960
+ * 获取当前可编码、发布的视频轨,不要求来源为摄像头。
1961
+ */
1962
+ requireLocalVideo() {
1963
+ const video = this.externalVideo ?? this.camera;
1964
+ if (!video) {
1965
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video source is not prepared");
1966
+ }
1967
+ return video;
1968
+ }
1758
1969
  /**
1759
1970
  * 执行声网操作并统一错误类型。
1760
1971
  */
@@ -1787,14 +1998,6 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1787
1998
  __publicField(_AgoraRtcManager, "regions", /* @__PURE__ */ new Map());
1788
1999
  var AgoraRtcManager = _AgoraRtcManager;
1789
2000
 
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
2001
  // src/Foundation/RTC/TRTC/RtcEngineManager.ts
1799
2002
  var RtcEngineLease = class {
1800
2003
  /**
@@ -2072,6 +2275,11 @@ var TrtcRtcManager = class {
2072
2275
  */
2073
2276
  __publicField(this, "isCapturing", false);
2074
2277
  __publicField(this, "isAudioCapturing", false);
2278
+ /**
2279
+ * 外部媒体的克隆轨;原始轨道由媒体源管理。
2280
+ */
2281
+ __publicField(this, "externalVideo");
2282
+ __publicField(this, "externalAudio");
2075
2283
  /**
2076
2284
  * 房间状态
2077
2285
  */
@@ -2106,13 +2314,18 @@ var TrtcRtcManager = class {
2106
2314
  async destroy() {
2107
2315
  const lease = this.lease;
2108
2316
  this.lease = void 0;
2109
- this.eventListener = void 0;
2317
+ const externalVideo = this.externalVideo;
2318
+ const externalAudio = this.externalAudio;
2319
+ this.externalVideo = void 0;
2320
+ this.externalAudio = void 0;
2110
2321
  this.isCapturing = false;
2111
2322
  this.isAudioCapturing = false;
2112
2323
  this.isInRoom = false;
2113
2324
  this.removeStatsListeners?.();
2114
2325
  this.removeStatsListeners = void 0;
2115
2326
  if (!lease) {
2327
+ externalVideo?.stop();
2328
+ externalAudio?.stop();
2116
2329
  return;
2117
2330
  }
2118
2331
  try {
@@ -2123,8 +2336,56 @@ var TrtcRtcManager = class {
2123
2336
  await lease.engine.stopLocalVideo();
2124
2337
  } catch {
2125
2338
  }
2339
+ externalVideo?.stop();
2340
+ externalAudio?.stop();
2126
2341
  this.engineManager.release(lease);
2127
2342
  }
2343
+ /**
2344
+ * 准备外部音视频轨但不发布;失败时释放克隆轨,不影响原始媒体源。
2345
+ */
2346
+ async setExternalMediaTracks(tracks) {
2347
+ const engine = this.requireEngine();
2348
+ if (this.isCapturing || this.isAudioCapturing || this.externalVideo) {
2349
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
2350
+ }
2351
+ if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
2352
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media tracks must be live video/audio tracks");
2353
+ }
2354
+ const video = tracks.videoTrack.clone();
2355
+ this.externalVideo = video;
2356
+ let audio;
2357
+ try {
2358
+ audio = tracks.audioTrack?.clone();
2359
+ this.externalAudio = audio;
2360
+ await engine.startLocalVideo({ publish: false, option: { videoTrack: video } });
2361
+ if (this.lease?.engine !== engine) {
2362
+ throw new XmaxError("CANCELLED" /* cancelled */, "External media preparation was cancelled");
2363
+ }
2364
+ if (audio) {
2365
+ await engine.startLocalAudio({
2366
+ publish: false,
2367
+ option: { audioTrack: audio, echoCancellation: false, autoGainControl: false, noiseSuppression: false }
2368
+ });
2369
+ if (this.lease?.engine !== engine) {
2370
+ throw new XmaxError("CANCELLED" /* cancelled */, "External media preparation was cancelled");
2371
+ }
2372
+ }
2373
+ } catch (error) {
2374
+ if (this.lease?.engine === engine) {
2375
+ await engine.stopLocalAudio().catch(() => {
2376
+ });
2377
+ await engine.stopLocalVideo().catch(() => {
2378
+ });
2379
+ }
2380
+ if (this.externalVideo === video) {
2381
+ this.externalVideo = void 0;
2382
+ this.externalAudio = void 0;
2383
+ }
2384
+ video.stop();
2385
+ audio?.stop();
2386
+ throw this.mapError(error);
2387
+ }
2388
+ }
2128
2389
  /**
2129
2390
  * 启动摄像头采集(不发布)。
2130
2391
  *
@@ -2133,7 +2394,7 @@ var TrtcRtcManager = class {
2133
2394
  */
2134
2395
  async startCameraCapture(options) {
2135
2396
  const engine = this.requireEngine();
2136
- if (this.isCapturing) {
2397
+ if (this.isCapturing || this.externalVideo) {
2137
2398
  throw new XmaxError(
2138
2399
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
2139
2400
  "Camera capture is already running"
@@ -2203,6 +2464,9 @@ var TrtcRtcManager = class {
2203
2464
  * 停止摄像头采集;引擎未初始化或采集未启动时不产生效果。
2204
2465
  */
2205
2466
  async stopCameraCapture() {
2467
+ if (this.externalVideo) {
2468
+ return;
2469
+ }
2206
2470
  const engine = this.lease?.engine;
2207
2471
  this.isCapturing = false;
2208
2472
  if (!engine) {
@@ -2220,7 +2484,7 @@ var TrtcRtcManager = class {
2220
2484
  * @throws 引擎未初始化、进房参数无效或进房失败时抛出错误。
2221
2485
  */
2222
2486
  async joinRoom(configuration) {
2223
- if (configuration.provider !== "trtc") {
2487
+ if (configuration.provider !== "trtc" /* trtc */) {
2224
2488
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "TRTC credentials required");
2225
2489
  }
2226
2490
  const engine = this.requireEngine();
@@ -2276,18 +2540,18 @@ var TrtcRtcManager = class {
2276
2540
  * TRTC 只接受单一目标码率,以 `minimumBitrate`(流畅优先档)作为目标码率;
2277
2541
  * 编码策略偏好映射为 TRTC 弱网偏好。
2278
2542
  *
2279
- * @throws 摄像头采集未启动或编码参数配置失败时抛出错误。
2543
+ * @throws 本地视频未准备或编码参数配置失败时抛出错误。
2280
2544
  */
2281
2545
  async configureVideoEncoding(configuration) {
2282
2546
  const engine = this.requireEngine();
2283
- if (!this.isCapturing) {
2547
+ if (!this.isCapturing && !this.externalVideo) {
2284
2548
  throw new XmaxError(
2285
2549
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
2286
- "Camera capture is not running"
2550
+ "Local video is not prepared"
2287
2551
  );
2288
2552
  }
2289
2553
  try {
2290
- const encodingSize = rtcOrientedVideoSize({
2554
+ const encodingSize = this.externalVideo ? configuration : rtcOrientedVideoSize({
2291
2555
  width: configuration.width,
2292
2556
  height: configuration.height
2293
2557
  });
@@ -2309,14 +2573,14 @@ var TrtcRtcManager = class {
2309
2573
  /**
2310
2574
  * 发布本地视频流。
2311
2575
  *
2312
- * @throws 摄像头采集未启动或发布失败时抛出错误。
2576
+ * @throws 本地视频未准备或发布失败时抛出错误。
2313
2577
  */
2314
2578
  async publishLocalVideo() {
2315
2579
  const engine = this.requireEngine();
2316
- if (!this.isCapturing) {
2580
+ if (!this.isCapturing && !this.externalVideo) {
2317
2581
  throw new XmaxError(
2318
2582
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
2319
- "Camera capture is not running"
2583
+ "Local video is not prepared"
2320
2584
  );
2321
2585
  }
2322
2586
  try {
@@ -2330,7 +2594,7 @@ var TrtcRtcManager = class {
2330
2594
  */
2331
2595
  async unpublishLocalVideo() {
2332
2596
  const engine = this.lease?.engine;
2333
- if (!engine || !this.isCapturing) {
2597
+ if (!engine || !this.isCapturing && !this.externalVideo) {
2334
2598
  return;
2335
2599
  }
2336
2600
  try {
@@ -2340,14 +2604,18 @@ var TrtcRtcManager = class {
2340
2604
  }
2341
2605
  }
2342
2606
  /**
2343
- * 发布本地音频流;首次调用时启动麦克风采集。
2607
+ * 发布文件音频或麦克风;无文件音轨时不启动麦克风。
2344
2608
  *
2345
2609
  * @throws 引擎未初始化、麦克风权限被拒绝或发布失败时抛出错误。
2346
2610
  */
2347
2611
  async publishLocalAudio() {
2348
2612
  const engine = this.requireEngine();
2349
2613
  try {
2350
- if (!this.isAudioCapturing) {
2614
+ if (this.externalVideo) {
2615
+ if (this.externalAudio) {
2616
+ await engine.updateLocalAudio({ publish: true });
2617
+ }
2618
+ } else if (!this.isAudioCapturing) {
2351
2619
  await engine.startLocalAudio();
2352
2620
  this.isAudioCapturing = true;
2353
2621
  } else {
@@ -2362,7 +2630,7 @@ var TrtcRtcManager = class {
2362
2630
  */
2363
2631
  async unpublishLocalAudio() {
2364
2632
  const engine = this.lease?.engine;
2365
- if (!engine || !this.isAudioCapturing) {
2633
+ if (!engine || !this.isAudioCapturing && !this.externalAudio) {
2366
2634
  return;
2367
2635
  }
2368
2636
  try {
@@ -2540,19 +2808,19 @@ var TrtcRtcManager = class {
2540
2808
  source.off(RTC_EVENT.networkQuality, onNetworkQuality);
2541
2809
  };
2542
2810
  source.on(RTC_EVENT.remoteVideoAvailable, (event) => {
2543
- if (event.streamType !== STREAM_TYPE_MAIN) {
2811
+ if (this.lease?.engine !== engine || event.streamType !== STREAM_TYPE_MAIN) {
2544
2812
  return;
2545
2813
  }
2546
2814
  this.eventListener?.onRemoteVideoPublished(event.userId, true);
2547
2815
  });
2548
2816
  source.on(RTC_EVENT.remoteVideoUnavailable, (event) => {
2549
- if (event.streamType !== STREAM_TYPE_MAIN) {
2817
+ if (this.lease?.engine !== engine || event.streamType !== STREAM_TYPE_MAIN) {
2550
2818
  return;
2551
2819
  }
2552
2820
  this.eventListener?.onRemoteVideoPublished(event.userId, false);
2553
2821
  });
2554
2822
  source.on(RTC_EVENT.customMessage, (event) => {
2555
- if (event.cmdId !== CUSTOM_MESSAGE_CMD_ID) {
2823
+ if (this.lease?.engine !== engine || event.cmdId !== CUSTOM_MESSAGE_CMD_ID) {
2556
2824
  return;
2557
2825
  }
2558
2826
  let message;
@@ -2645,6 +2913,11 @@ var VeRtcManager = class {
2645
2913
  __publicField(this, "captureVersion", 0);
2646
2914
  __publicField(this, "camera");
2647
2915
  __publicField(this, "microphone", false);
2916
+ /**
2917
+ * 外部媒体的克隆轨;原始轨道由媒体源管理。
2918
+ */
2919
+ __publicField(this, "externalVideo");
2920
+ __publicField(this, "externalAudio");
2648
2921
  __publicField(this, "connection");
2649
2922
  __publicField(this, "joined", false);
2650
2923
  __publicField(this, "roomVersion", 0);
@@ -2700,18 +2973,76 @@ var VeRtcManager = class {
2700
2973
  this.captureVersion++;
2701
2974
  const engine = this.engine;
2702
2975
  const sdk = this.sdk;
2703
- this.listener = void 0;
2704
2976
  this.clearRoom();
2705
2977
  this.engine = void 0;
2706
2978
  this.sdk = void 0;
2707
2979
  this.camera = void 0;
2708
2980
  this.microphone = false;
2709
2981
  engine?.removeAllListeners();
2710
- if (engine && sdk) sdk.default.destroyEngine(engine);
2982
+ try {
2983
+ if (engine && sdk) sdk.default.destroyEngine(engine);
2984
+ } finally {
2985
+ this.externalVideo?.stop();
2986
+ this.externalAudio?.stop();
2987
+ this.externalVideo = void 0;
2988
+ this.externalAudio = void 0;
2989
+ }
2990
+ }
2991
+ /**
2992
+ * 将克隆的文件音视频轨绑定到主流,不采集设备、不自动发布。
2993
+ */
2994
+ async setExternalMediaTracks(tracks) {
2995
+ const engine = this.requireEngine();
2996
+ const sdk = this.sdk;
2997
+ if (this.camera || this.microphone || this.externalVideo) {
2998
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
2999
+ }
3000
+ if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
3001
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media tracks must be live video/audio tracks");
3002
+ }
3003
+ const video = tracks.videoTrack.clone();
3004
+ this.externalVideo = video;
3005
+ let audio;
3006
+ const index = sdk.StreamIndex.STREAM_INDEX_MAIN;
3007
+ const ensureCurrent = () => {
3008
+ if (this.engine !== engine || this.externalVideo !== video) {
3009
+ throw this.cancelled();
3010
+ }
3011
+ };
3012
+ try {
3013
+ audio = tracks.audioTrack?.clone();
3014
+ this.externalAudio = audio;
3015
+ await engine.setVideoSourceType(index, sdk.VideoSourceType.VIDEO_SOURCE_TYPE_EXTERNAL);
3016
+ ensureCurrent();
3017
+ await engine.setExternalVideoTrack(index, video);
3018
+ ensureCurrent();
3019
+ if (audio) {
3020
+ await engine.setAudioSourceType(index, sdk.AudioSourceType.AUDIO_SOURCE_TYPE_EXTERNAL);
3021
+ ensureCurrent();
3022
+ await engine.setExternalAudioTrack(index, audio);
3023
+ ensureCurrent();
3024
+ }
3025
+ } catch (error) {
3026
+ if (this.engine === engine) {
3027
+ await engine.setAudioSourceType(index, sdk.AudioSourceType.AUDIO_SOURCE_TYPE_INTERNAL).catch(() => {
3028
+ });
3029
+ await engine.setVideoSourceType(index, sdk.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL).catch(() => {
3030
+ });
3031
+ }
3032
+ if (this.externalVideo === video) {
3033
+ this.externalVideo = void 0;
3034
+ this.externalAudio = void 0;
3035
+ }
3036
+ video.stop();
3037
+ audio?.stop();
3038
+ throw this.mapError(error);
3039
+ }
2711
3040
  }
2712
3041
  async startCameraCapture(options) {
2713
3042
  const engine = this.requireEngine();
2714
- if (this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is already running");
3043
+ if (this.camera || this.externalVideo) {
3044
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video is already prepared");
3045
+ }
2715
3046
  const version = ++this.captureVersion;
2716
3047
  await this.run(() => engine.setVideoEncoderConfig({
2717
3048
  ...rtcOrientedVideoSize({ width: options.width, height: options.height }),
@@ -2751,15 +3082,20 @@ var VeRtcManager = class {
2751
3082
  return track;
2752
3083
  }
2753
3084
  async stopCameraCapture() {
3085
+ if (this.externalVideo) {
3086
+ return;
3087
+ }
2754
3088
  this.captureVersion++;
2755
3089
  this.camera = void 0;
2756
3090
  if (this.engine) await this.run(() => this.engine.stopVideoCapture());
2757
3091
  }
2758
3092
  async configureVideoEncoding(config) {
2759
3093
  const engine = this.requireEngine();
2760
- this.requireCamera();
3094
+ this.requireLocalVideo();
3095
+ const size = this.externalVideo ? config : rtcOrientedVideoSize(config);
2761
3096
  await this.run(() => engine.setVideoEncoderConfig({
2762
- ...rtcOrientedVideoSize({ width: config.width, height: config.height }),
3097
+ width: size.width,
3098
+ height: size.height,
2763
3099
  frameRate: config.frameRate,
2764
3100
  maxKbps: config.maximumBitrate,
2765
3101
  contentHint: config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail",
@@ -2772,7 +3108,7 @@ var VeRtcManager = class {
2772
3108
  await this.recovery?.catch(() => {
2773
3109
  });
2774
3110
  const engine = this.requireEngine();
2775
- if (config.provider !== "vertc" || config.appID !== VERTC_APP_ID || this.connection) {
3111
+ if (config.provider !== "vertc" /* vertc */ || config.appID !== VERTC_APP_ID || this.connection) {
2776
3112
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "VeRTC credentials for the configured AppID and an idle room are required");
2777
3113
  }
2778
3114
  this.clearRoom();
@@ -2819,7 +3155,7 @@ var VeRtcManager = class {
2819
3155
  const engine = this.requireEngine();
2820
3156
  const current = this.connection;
2821
3157
  if (signal?.aborted) throw this.cancelled();
2822
- if (!current || config.provider !== "vertc" || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
3158
+ if (!current || config.provider !== "vertc" /* vertc */ || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
2823
3159
  throw new XmaxError("SESSION_ERROR" /* sessionError */, "VeRTC session binding changed");
2824
3160
  }
2825
3161
  const version = this.roomVersion;
@@ -2861,7 +3197,7 @@ var VeRtcManager = class {
2861
3197
  throw this.cancelled();
2862
3198
  }
2863
3199
  }
2864
- if ((this.tokenExpired || this.publishPrivilegeExpired) && this.videoRequested && this.camera) {
3200
+ if ((this.tokenExpired || this.publishPrivilegeExpired) && this.videoRequested && (this.camera || this.externalVideo)) {
2865
3201
  await this.run(() => engine.publishStream(this.sdk.MediaType.VIDEO));
2866
3202
  this.ensureCurrent(engine, version);
2867
3203
  if (!this.videoRequested) {
@@ -2869,7 +3205,7 @@ var VeRtcManager = class {
2869
3205
  this.ensureCurrent(engine, version);
2870
3206
  }
2871
3207
  }
2872
- if ((this.tokenExpired || this.publishPrivilegeExpired) && this.audioRequested && this.microphone) {
3208
+ if ((this.tokenExpired || this.publishPrivilegeExpired) && this.audioRequested && (this.microphone || this.externalAudio)) {
2873
3209
  await this.run(() => engine.publishStream(this.sdk.MediaType.AUDIO));
2874
3210
  this.ensureCurrent(engine, version);
2875
3211
  if (!this.audioRequested) {
@@ -2893,7 +3229,7 @@ var VeRtcManager = class {
2893
3229
  }
2894
3230
  async publishLocalVideo() {
2895
3231
  const engine = this.requireEngine();
2896
- this.requireCamera();
3232
+ this.requireLocalVideo();
2897
3233
  this.videoRequested = true;
2898
3234
  const version = this.roomVersion;
2899
3235
  await this.recovery;
@@ -2907,12 +3243,15 @@ var VeRtcManager = class {
2907
3243
  }
2908
3244
  async publishLocalAudio() {
2909
3245
  const engine = this.requireEngine();
3246
+ if (this.externalVideo && !this.externalAudio) {
3247
+ return;
3248
+ }
2910
3249
  this.audioRequested = true;
2911
3250
  const version = this.roomVersion;
2912
3251
  await this.recovery;
2913
3252
  this.ensureCurrent(engine, version);
2914
3253
  try {
2915
- if (!this.microphone) {
3254
+ if (!this.externalVideo && !this.microphone) {
2916
3255
  await engine.startAudioCapture();
2917
3256
  if (!this.isCurrent(engine, version)) {
2918
3257
  await engine.stopAudioCapture().catch(() => {
@@ -2924,7 +3263,7 @@ var VeRtcManager = class {
2924
3263
  if (!this.audioRequested) throw this.cancelled();
2925
3264
  await engine.publishStream(this.sdk.MediaType.AUDIO);
2926
3265
  } catch (error) {
2927
- throw this.mapError(error, "MICROPHONE_PERMISSION_DENIED" /* microphonePermissionDenied */);
3266
+ throw this.mapError(error, this.externalVideo ? "RTC_ERROR" /* rtcError */ : "MICROPHONE_PERMISSION_DENIED" /* microphonePermissionDenied */);
2928
3267
  }
2929
3268
  }
2930
3269
  async unpublishLocalAudio() {
@@ -3189,6 +3528,15 @@ var VeRtcManager = class {
3189
3528
  if (!this.engine) throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC is not initialized");
3190
3529
  return this.engine;
3191
3530
  }
3531
+ /**
3532
+ * 获取已准备的文件或摄像头视频轨。
3533
+ */
3534
+ requireLocalVideo() {
3535
+ if (this.externalVideo) {
3536
+ return this.externalVideo;
3537
+ }
3538
+ return this.requireCamera();
3539
+ }
3192
3540
  requireCamera() {
3193
3541
  if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
3194
3542
  return this.camera;
@@ -3215,12 +3563,14 @@ var VeRtcManager = class {
3215
3563
  // src/Foundation/RTC/RtcFactory.ts
3216
3564
  function createRtcManager(provider, environment) {
3217
3565
  switch (provider) {
3566
+ case "trtc" /* trtc */:
3567
+ return new TrtcRtcManager();
3218
3568
  case "agora" /* agora */:
3219
3569
  return new AgoraRtcManager({ environment });
3220
3570
  case "vertc" /* vertc */:
3221
3571
  return new VeRtcManager();
3222
3572
  default:
3223
- return new TrtcRtcManager();
3573
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported RTC provider");
3224
3574
  }
3225
3575
  }
3226
3576
 
@@ -3715,6 +4065,463 @@ var _CameraController = class _CameraController {
3715
4065
  __publicField(_CameraController, "localVideoTrackID", "video0");
3716
4066
  var CameraController = _CameraController;
3717
4067
 
4068
+ // src/Service/Realtime/RealtimeReferenceVideo.ts
4069
+ var RealtimeVideoSampleMethod = /* @__PURE__ */ ((RealtimeVideoSampleMethod2) => {
4070
+ RealtimeVideoSampleMethod2["time"] = "time";
4071
+ RealtimeVideoSampleMethod2["fps"] = "fps";
4072
+ return RealtimeVideoSampleMethod2;
4073
+ })(RealtimeVideoSampleMethod || {});
4074
+
4075
+ // src/Media/Video/NetworkVideoController.ts
4076
+ var NetworkVideoController = class {
4077
+ constructor() {
4078
+ /**
4079
+ * 当前视频源与完成通知
4080
+ */
4081
+ __publicField(this, "currentTrack");
4082
+ __publicField(this, "reference");
4083
+ __publicField(this, "onFinish");
4084
+ /**
4085
+ * 预览进度与视图资源
4086
+ */
4087
+ __publicField(this, "position", 0);
4088
+ __publicField(this, "ended", false);
4089
+ __publicField(this, "bindings", /* @__PURE__ */ new Map());
4090
+ }
4091
+ /**
4092
+ * 创建网络视频源,校验地址、采样方式和模型支持的尺寸;不请求视频下载。
4093
+ */
4094
+ create(options, mediaService) {
4095
+ let url;
4096
+ try {
4097
+ url = new URL(options.url);
4098
+ } catch {
4099
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an absolute HTTP or HTTPS URL");
4100
+ }
4101
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password) {
4102
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an HTTP or HTTPS URL without embedded credentials");
4103
+ }
4104
+ const sampleMethod = options.sampleMethod ?? "time" /* time */;
4105
+ if (!Object.values(RealtimeVideoSampleMethod).includes(sampleMethod)) {
4106
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported video sample method");
4107
+ }
4108
+ if (this.currentTrack) {
4109
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current network video stream before creating another one");
4110
+ }
4111
+ options.videoFormat.validate();
4112
+ const size = mediaService.resolveModelInputSize(options.videoFormat);
4113
+ const track = new RealtimeVideoTrack({
4114
+ id: "network-video",
4115
+ videoFormat: options.videoFormat.resized(size.width, size.height)
4116
+ });
4117
+ this.currentTrack = track;
4118
+ this.reference = Object.freeze({ path: options.url.trim(), sampleMethod });
4119
+ this.onFinish = options.onFinish;
4120
+ VideoRenderRegistry.register(track, {
4121
+ attachHandler: (view) => this.attach(view),
4122
+ detachHandler: (view) => this.detach(view)
4123
+ });
4124
+ return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
4125
+ }
4126
+ /**
4127
+ * 释放全部预览绑定及源信息,不影响其他媒体源。
4128
+ */
4129
+ stop() {
4130
+ for (const view of this.bindings.keys()) {
4131
+ this.detach(view);
4132
+ }
4133
+ if (this.currentTrack) {
4134
+ VideoRenderRegistry.unregister(this.currentTrack);
4135
+ }
4136
+ this.currentTrack = void 0;
4137
+ this.reference = void 0;
4138
+ this.onFinish = void 0;
4139
+ this.position = 0;
4140
+ this.ended = false;
4141
+ }
4142
+ /**
4143
+ * 绑定播放器,恢复预览位置;本地播放错误仅记录,不中断服务端生成。
4144
+ */
4145
+ attach(view) {
4146
+ if (!this.reference || !view.videoElement || !view.setVideoURL) {
4147
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "This render target does not support network video preview");
4148
+ }
4149
+ this.detach(view);
4150
+ const video = view.videoElement;
4151
+ video.autoplay = !this.ended;
4152
+ const restore = () => {
4153
+ if (this.position > 0) {
4154
+ video.currentTime = Math.min(this.position, Math.max(0, video.duration - 1e-3) || this.position);
4155
+ }
4156
+ if (this.ended) {
4157
+ video.pause();
4158
+ }
4159
+ };
4160
+ const fail = () => {
4161
+ XmaxLogger.render.warning(() => "\u7F51\u7EDC\u89C6\u9891\u9884\u89C8\u5931\u8D25 (Network Video Preview Failed)");
4162
+ };
4163
+ video.addEventListener("loadedmetadata", restore);
4164
+ video.addEventListener("error", fail);
4165
+ this.bindings.set(view, () => {
4166
+ this.position = video.currentTime;
4167
+ this.ended = video.ended || this.ended;
4168
+ video.removeEventListener("loadedmetadata", restore);
4169
+ video.removeEventListener("error", fail);
4170
+ view.setVideoURL?.(null);
4171
+ });
4172
+ view.isMirrored = false;
4173
+ view.setVideoURL(this.reference.path);
4174
+ }
4175
+ /**
4176
+ * 保存预览进度并释放该视图的地址和事件监听。
4177
+ */
4178
+ detach(view) {
4179
+ this.bindings.get(view)?.();
4180
+ this.bindings.delete(view);
4181
+ }
4182
+ };
4183
+
4184
+ // src/Media/Video/LocalVideoController.ts
4185
+ var LocalVideoController = class {
4186
+ /**
4187
+ * 保存媒体错误处理器,不创建浏览器资源。
4188
+ */
4189
+ constructor(onError = () => {
4190
+ }) {
4191
+ /**
4192
+ * 当前文件资源与运行期错误通知
4193
+ */
4194
+ __publicField(this, "source");
4195
+ __publicField(this, "onError");
4196
+ this.onError = onError;
4197
+ }
4198
+ /**
4199
+ * 是否持有文件资源,包含尚未完成准备的播放器。
4200
+ */
4201
+ get isActive() {
4202
+ return this.source !== void 0;
4203
+ }
4204
+ /**
4205
+ * 准备完成的本地视频轨。
4206
+ */
4207
+ get currentTrack() {
4208
+ return this.source?.track;
4209
+ }
4210
+ /**
4211
+ * 文件音频输出;禁用音频时为空,无音轨文件的 Web Audio 输出为静音。
4212
+ */
4213
+ get audioTrack() {
4214
+ return this.source?.audioDestination?.stream.getAudioTracks()[0];
4215
+ }
4216
+ /**
4217
+ * 校验文件和格式,准备首帧与音频输出;应从用户点击事件直接调用以解锁播放。
4218
+ */
4219
+ async create(options, mediaService, signal) {
4220
+ if (this.source) {
4221
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local video before creating another one");
4222
+ }
4223
+ if (!(options.file instanceof Blob) || options.file.size === 0) {
4224
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video requires a non-empty File or Blob");
4225
+ }
4226
+ const format = this.resolveVideoFormat(options.videoFormat, mediaService);
4227
+ if (signal.aborted) {
4228
+ throw this.cancelled();
4229
+ }
4230
+ if (typeof document === "undefined") {
4231
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Local video requires a browser");
4232
+ }
4233
+ const video = document.createElement("video");
4234
+ const canvas = document.createElement("canvas");
4235
+ const context = canvas.getContext("2d");
4236
+ if (!context || typeof canvas.captureStream !== "function") {
4237
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas video capture is unavailable");
4238
+ }
4239
+ canvas.width = format.width;
4240
+ canvas.height = format.height;
4241
+ const source = {
4242
+ video,
4243
+ canvas,
4244
+ context,
4245
+ url: URL.createObjectURL(options.file),
4246
+ abort: new AbortController(),
4247
+ targets: /* @__PURE__ */ new Set()
4248
+ };
4249
+ this.source = source;
4250
+ try {
4251
+ video.playsInline = true;
4252
+ video.loop = options.loop ?? true;
4253
+ video.preload = "auto";
4254
+ video.volume = 1;
4255
+ video.muted = options.includeAudio === false;
4256
+ if (options.includeAudio !== false) {
4257
+ if (typeof AudioContext === "undefined") {
4258
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Web Audio is unavailable; disable file audio or use a supported browser");
4259
+ }
4260
+ const audio = new AudioContext();
4261
+ source.audioContext = audio;
4262
+ source.audioSource = audio.createMediaElementSource(video);
4263
+ source.audioDestination = audio.createMediaStreamDestination();
4264
+ source.audioSource.connect(source.audioDestination);
4265
+ }
4266
+ video.src = source.url;
4267
+ const resume = source.audioContext?.resume();
4268
+ const playing = this.play(source, signal);
4269
+ await this.wait(source, Promise.all([resume, playing]), signal);
4270
+ video.pause();
4271
+ if (!video.videoWidth || !video.videoHeight || !Number.isFinite(video.duration) || video.duration <= 0) {
4272
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "The file has no playable finite video stream");
4273
+ }
4274
+ await this.rewind(source, signal);
4275
+ this.draw(source);
4276
+ source.stream = canvas.captureStream(format.fps);
4277
+ const nativeTrack = source.stream.getVideoTracks()[0];
4278
+ if (!nativeTrack) {
4279
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas did not provide a video track");
4280
+ }
4281
+ const track = new RealtimeVideoTrack({ id: "local-video", videoFormat: format });
4282
+ track.mediaStreamTrack = nativeTrack;
4283
+ source.track = track;
4284
+ this.draw(source);
4285
+ source.drawTimer = setInterval(() => {
4286
+ if (this.source !== source) {
4287
+ return;
4288
+ }
4289
+ try {
4290
+ this.draw(source);
4291
+ } catch {
4292
+ this.handleError(source);
4293
+ }
4294
+ }, 1e3 / format.fps);
4295
+ source.onError = () => this.handleError(source);
4296
+ video.addEventListener("error", source.onError);
4297
+ VideoRenderRegistry.register(track, {
4298
+ attachHandler: (view) => {
4299
+ view.isMirrored = false;
4300
+ view.setMediaStream(source.stream);
4301
+ source.targets.add(view);
4302
+ },
4303
+ detachHandler: (view) => {
4304
+ source.targets.delete(view);
4305
+ view.setMediaStream(null);
4306
+ }
4307
+ });
4308
+ return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
4309
+ } catch (error) {
4310
+ await this.stop();
4311
+ throw XmaxError.from(error);
4312
+ }
4313
+ }
4314
+ /**
4315
+ * 开始信令发送成功后,从文件开头同步启动音视频;不等待远端首帧。
4316
+ */
4317
+ async start(signal) {
4318
+ const source = this.source;
4319
+ if (!source?.track) {
4320
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video is not prepared");
4321
+ }
4322
+ if (signal.aborted) {
4323
+ throw this.cancelled();
4324
+ }
4325
+ try {
4326
+ await this.rewind(source, signal);
4327
+ await this.wait(source, Promise.all([source.audioContext?.resume(), this.play(source, signal)]), signal);
4328
+ } catch (error) {
4329
+ source.video.pause();
4330
+ throw error;
4331
+ }
4332
+ }
4333
+ /**
4334
+ * 暂停文件音视频,保留当前预览;重新开始生成时回到文件开头。
4335
+ */
4336
+ pause() {
4337
+ this.source?.video.pause();
4338
+ }
4339
+ /**
4340
+ * 释放播放器、预览、定时器、轨道、音频上下文和对象 URL。
4341
+ */
4342
+ async stop() {
4343
+ const source = this.source;
4344
+ this.source = void 0;
4345
+ if (!source) {
4346
+ return;
4347
+ }
4348
+ source.abort.abort();
4349
+ clearInterval(source.drawTimer);
4350
+ if (source.onError) {
4351
+ source.video.removeEventListener("error", source.onError);
4352
+ }
4353
+ if (source.track) {
4354
+ VideoRenderRegistry.unregister(source.track);
4355
+ source.track.mediaStreamTrack = void 0;
4356
+ }
4357
+ for (const view of source.targets) {
4358
+ try {
4359
+ view.setMediaStream(null);
4360
+ } catch {
4361
+ }
4362
+ }
4363
+ source.targets.clear();
4364
+ source.video.pause();
4365
+ source.video.removeAttribute("src");
4366
+ source.video.load();
4367
+ source.stream?.getTracks().forEach((track) => track.stop());
4368
+ source.audioDestination?.stream.getTracks().forEach((track) => track.stop());
4369
+ source.audioSource?.disconnect();
4370
+ source.audioDestination?.disconnect();
4371
+ URL.revokeObjectURL(source.url);
4372
+ await source.audioContext?.close().catch(() => {
4373
+ });
4374
+ }
4375
+ /**
4376
+ * 播放被浏览器拒绝时给出可操作错误;关闭后的迟到播放立即暂停。
4377
+ */
4378
+ play(source, signal) {
4379
+ return source.video.play().then(() => {
4380
+ if (this.source !== source || source.abort.signal.aborted || signal.aborted) {
4381
+ source.video.pause();
4382
+ throw this.cancelled();
4383
+ }
4384
+ }).catch((error) => {
4385
+ if (error instanceof XmaxError) {
4386
+ throw error;
4387
+ }
4388
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Cannot play local video; call from a user click and use a browser-supported video codec");
4389
+ });
4390
+ }
4391
+ /**
4392
+ * 等待 seek 完成,确保重新生成从首帧开始,而非使用旧缓存画面。
4393
+ */
4394
+ async rewind(source, signal) {
4395
+ if (source.video.currentTime === 0) {
4396
+ return;
4397
+ }
4398
+ let seeked;
4399
+ const ready = new Promise((resolve) => {
4400
+ seeked = resolve;
4401
+ });
4402
+ source.video.addEventListener("seeked", seeked, { once: true });
4403
+ try {
4404
+ source.video.currentTime = 0;
4405
+ await this.wait(source, ready, signal);
4406
+ } finally {
4407
+ source.video.removeEventListener("seeked", seeked);
4408
+ }
4409
+ }
4410
+ /**
4411
+ * 给浏览器准备和播放操作设置 10 秒上限,同时响应关闭、取消和解码错误。
4412
+ */
4413
+ wait(source, operation, signal) {
4414
+ return new Promise((resolve, reject) => {
4415
+ const finish = (error) => {
4416
+ clearTimeout(timer);
4417
+ signal.removeEventListener("abort", abort);
4418
+ source.abort.signal.removeEventListener("abort", abort);
4419
+ source.video.removeEventListener("error", failed);
4420
+ if (error) {
4421
+ reject(error);
4422
+ } else {
4423
+ resolve();
4424
+ }
4425
+ };
4426
+ const abort = () => finish(this.cancelled());
4427
+ const failed = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "The browser cannot decode this video file"));
4428
+ const timer = setTimeout(() => finish(new XmaxError("TIMEOUT" /* timeout */, "Local video preparation timed out; start from a user click and check the video codec")), 1e4);
4429
+ signal.addEventListener("abort", abort, { once: true });
4430
+ source.abort.signal.addEventListener("abort", abort, { once: true });
4431
+ source.video.addEventListener("error", failed, { once: true });
4432
+ operation.then(() => finish(), finish);
4433
+ if (signal.aborted || source.abort.signal.aborted) {
4434
+ abort();
4435
+ }
4436
+ });
4437
+ }
4438
+ /**
4439
+ * 等比缩放并居中补黑边,保留完整画面,不按设备方向转置文件尺寸。
4440
+ */
4441
+ draw(source) {
4442
+ const { video, canvas, context } = source;
4443
+ if (video.readyState < 2) {
4444
+ return;
4445
+ }
4446
+ const scale = Math.min(canvas.width / video.videoWidth, canvas.height / video.videoHeight);
4447
+ const width = video.videoWidth * scale;
4448
+ const height = video.videoHeight * scale;
4449
+ context.fillStyle = "black";
4450
+ context.fillRect(0, 0, canvas.width, canvas.height);
4451
+ context.drawImage(video, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
4452
+ }
4453
+ /**
4454
+ * 仅为本地文件选择最接近宽高比的模型档位;比例距离相同时优先横屏。
4455
+ * 无固定档位时保留公共尺寸解析规则,帧率和显式编码配置不变。
4456
+ */
4457
+ resolveVideoFormat(format, mediaService) {
4458
+ format.validate();
4459
+ const buckets = resolutionBuckets(mediaService.model);
4460
+ let target = buckets.find((size2) => size2.width === format.width && size2.height === format.height);
4461
+ if (!target && buckets.length > 0) {
4462
+ const ratio = format.width / format.height;
4463
+ let bestDistance = Number.POSITIVE_INFINITY;
4464
+ for (const bucket of buckets) {
4465
+ const distance = Math.abs(Math.log(bucket.width / bucket.height / ratio));
4466
+ const tied = Math.abs(distance - bestDistance) < 1e-9;
4467
+ if (distance < bestDistance - 1e-9 || tied && bucket.width >= bucket.height && target && target.width < target.height) {
4468
+ target = bucket;
4469
+ bestDistance = distance;
4470
+ }
4471
+ }
4472
+ }
4473
+ const size = mediaService.resolveModelInputSize(target ?? format);
4474
+ return format.resized(size.width, size.height);
4475
+ }
4476
+ /**
4477
+ * 解码或绘制失败时停止媒体推进,并报告一次运行期错误。
4478
+ */
4479
+ handleError(source) {
4480
+ if (this.source !== source || source.drawTimer === void 0) {
4481
+ return;
4482
+ }
4483
+ clearInterval(source.drawTimer);
4484
+ source.drawTimer = void 0;
4485
+ source.video.pause();
4486
+ XmaxLogger.realtime.warning(() => "\u672C\u5730\u89C6\u9891\u89E3\u7801\u5931\u8D25 (Local Video Decode Failed)");
4487
+ this.onError(new XmaxError("MEDIA_ERROR" /* mediaError */, "Local video decoding failed"));
4488
+ }
4489
+ /**
4490
+ * 构造统一的媒体操作取消错误。
4491
+ */
4492
+ cancelled() {
4493
+ return new XmaxError("CANCELLED" /* cancelled */, "Local video operation was cancelled");
4494
+ }
4495
+ };
4496
+
4497
+ // src/Service/Realtime/RealtimeContext.ts
4498
+ var RealtimeContext = class {
4499
+ /**
4500
+ * 创建实时生成条件并规范化文本和参考路径。
4501
+ */
4502
+ constructor(init) {
4503
+ /**
4504
+ * 服务端直接读取的网络视频。
4505
+ */
4506
+ __publicField(this, "referenceVideo");
4507
+ /**
4508
+ * 生成条件
4509
+ */
4510
+ /**
4511
+ * 实时生成使用的文本条件。
4512
+ */
4513
+ __publicField(this, "prompt");
4514
+ /**
4515
+ * 已上传参考图片的可选远端路径。
4516
+ */
4517
+ __publicField(this, "referencePath");
4518
+ this.referenceVideo = init.referenceVideo && Object.freeze({ ...init.referenceVideo });
4519
+ this.prompt = init.prompt.trim();
4520
+ const normalizedReferencePath = init.referencePath?.trim();
4521
+ this.referencePath = normalizedReferencePath || void 0;
4522
+ }
4523
+ };
4524
+
3718
4525
  // src/Service/Realtime/RealtimeSession.ts
3719
4526
  var RealtimeSession = class {
3720
4527
  /**
@@ -3779,7 +4586,7 @@ var RoomJoinConfiguration = class {
3779
4586
  /**
3780
4587
  * RTC 提供方。
3781
4588
  */
3782
- __publicField(this, "provider", "trtc");
4589
+ __publicField(this, "provider", "trtc" /* trtc */);
3783
4590
  /**
3784
4591
  * 进房参数
3785
4592
  */
@@ -3872,10 +4679,10 @@ var RealtimeSessionConnection = class {
3872
4679
  }
3873
4680
  };
3874
4681
  function toRoomJoinConfiguration(connection) {
3875
- return connection.provider === "trtc" ? new RoomJoinConfiguration(connection) : connection;
4682
+ return connection.provider === "trtc" /* trtc */ ? new RoomJoinConfiguration(connection) : connection;
3876
4683
  }
3877
4684
  function connectionAppID(connection) {
3878
- return connection.provider === "trtc" ? connection.sdkAppID : connection.appID;
4685
+ return connection.provider === "trtc" /* trtc */ ? connection.sdkAppID : connection.appID;
3879
4686
  }
3880
4687
 
3881
4688
  // src/Service/Realtime/RealtimeSessionService.ts
@@ -4090,7 +4897,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
4090
4897
  return void 0;
4091
4898
  }
4092
4899
  return new RealtimeSessionConnection({
4093
- provider: "trtc",
4900
+ provider: "trtc" /* trtc */,
4094
4901
  roomID,
4095
4902
  sdkAppID,
4096
4903
  userID,
@@ -4234,9 +5041,20 @@ var RoomEvent = class _RoomEvent {
4234
5041
  static start(options) {
4235
5042
  return _RoomEvent.encode({
4236
5043
  event: "start",
4237
- params: _RoomEvent.generationParameters(options),
5044
+ params: {
5045
+ ..._RoomEvent.generationParameters(options),
5046
+ ...options.context.referenceVideo && {
5047
+ ref_video_path: options.context.referenceVideo.path,
5048
+ sample_method: options.context.referenceVideo.sampleMethod,
5049
+ target_size: [
5050
+ options.targetSize?.width ?? options.videoFormat.width,
5051
+ options.targetSize?.height ?? options.videoFormat.height
5052
+ ]
5053
+ }
5054
+ },
4238
5055
  user_id: options.userID,
4239
- uid: options.taskID
5056
+ uid: options.taskID,
5057
+ ...options.context.referenceVideo && { session_uid: options.taskID }
4240
5058
  });
4241
5059
  }
4242
5060
  /**
@@ -4452,20 +5270,20 @@ var _RoomMessageCodec = class _RoomMessageCodec {
4452
5270
  return parsed;
4453
5271
  }
4454
5272
  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);
5273
+ let record2 = this.records.get(key);
5274
+ if (!record2) {
5275
+ record2 = { count: chunk.count, parts: /* @__PURE__ */ new Map() };
5276
+ this.records.set(key, record2);
4459
5277
  this.evictOverflow();
4460
5278
  }
4461
- record.parts.set(chunk.index, chunk.data);
4462
- if (record.parts.size < record.count) {
5279
+ record2.parts.set(chunk.index, chunk.data);
5280
+ if (record2.parts.size < record2.count) {
4463
5281
  return void 0;
4464
5282
  }
4465
5283
  this.records.delete(key);
4466
5284
  const parts = [];
4467
- for (let index = 0; index < record.count; index += 1) {
4468
- const part = record.parts.get(index);
5285
+ for (let index = 0; index < record2.count; index += 1) {
5286
+ const part = record2.parts.get(index);
4469
5287
  if (part === void 0) {
4470
5288
  return void 0;
4471
5289
  }
@@ -4495,16 +5313,16 @@ var _RoomMessageCodec = class _RoomMessageCodec {
4495
5313
  if (this.chunkEvent === null || typeof parsed !== "object" || parsed === null) {
4496
5314
  return void 0;
4497
5315
  }
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") {
5316
+ const record2 = parsed;
5317
+ 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
5318
  return void 0;
4501
5319
  }
4502
- const index = record.index;
4503
- const count = record.count;
5320
+ const index = record2.index;
5321
+ const count = record2.count;
4504
5322
  if (count < 1 || index < 0 || index >= count) {
4505
5323
  return void 0;
4506
5324
  }
4507
- return { eventId: record.eventId, index, count, data: record.data };
5325
+ return { eventId: record2.eventId, index, count, data: record2.data };
4508
5326
  }
4509
5327
  /**
4510
5328
  * 淘汰最旧的未完成记录,保持缓存不超过上限。
@@ -4616,9 +5434,9 @@ var RoomController = class {
4616
5434
  *
4617
5435
  * @throws 已有房间未离开、操作失效或进房失败时抛出错误。
4618
5436
  */
4619
- async join(connection, ensureActive) {
5437
+ async join(connection, ensureActive2) {
4620
5438
  try {
4621
- ensureActive();
5439
+ ensureActive2();
4622
5440
  } catch (error) {
4623
5441
  throw XmaxError.from(error);
4624
5442
  }
@@ -4633,10 +5451,10 @@ var RoomController = class {
4633
5451
  this.heartbeat.stop();
4634
5452
  try {
4635
5453
  if (!this.customCodec) this.codec = new RoomMessageCodec(
4636
- connection.provider === "vertc" ? null : connection.provider === "agora" ? "__agora_chunk__" : "__trtc_chunk__"
5454
+ connection.provider === "vertc" /* vertc */ ? null : connection.provider === "agora" /* agora */ ? "__agora_chunk__" : "__trtc_chunk__"
4637
5455
  );
4638
5456
  await this.rtcManager.joinRoom(toRoomJoinConfiguration(connection));
4639
- ensureActive();
5457
+ ensureActive2();
4640
5458
  if (this.state.kind !== "joining" || this.state.operationID !== operationID || this.leaveOperation) {
4641
5459
  throw new XmaxError("CANCELLED" /* cancelled */, "RTC room join was cancelled");
4642
5460
  }
@@ -5019,6 +5837,7 @@ var StreamController = class {
5019
5837
  * 运行状态
5020
5838
  */
5021
5839
  __publicField(this, "state", makeInitialState());
5840
+ __publicField(this, "roomListener");
5022
5841
  this.rtcManager = options.rtcManager;
5023
5842
  this.roomController = options.roomController ?? new RoomController({ rtcManager: options.rtcManager });
5024
5843
  this.encodingController = options.encodingController ?? new EncodingController({ rtcManager: options.rtcManager });
@@ -5027,13 +5846,19 @@ var StreamController = class {
5027
5846
  this.remoteStreamListener = options.remoteStreamListener ?? (() => {
5028
5847
  });
5029
5848
  this.generationTimeoutMs = options.generationTimeoutMs ?? 3e4;
5849
+ this.roomController.setListener({
5850
+ onRoomMessage: (sender, message) => {
5851
+ this.handleNetworkVideoCompletion(sender, message);
5852
+ this.roomListener?.onRoomMessage(sender, message);
5853
+ }
5854
+ });
5030
5855
  this.rtcManager.setEventListener({
5031
5856
  onTokenWillExpire: options.onCredentialsRequired,
5032
5857
  onTokenExpired: options.onCredentialsRequired,
5033
5858
  onRoomRejoining: () => this.handleRoomRejoining(),
5034
5859
  onError: (error) => this.errorListener(error),
5035
5860
  onNetworkStatistics: (statistics) => {
5036
- if (this.state.localVideoPublished) {
5861
+ if (this.state.roomID) {
5037
5862
  this.networkStatisticsListener?.(statistics);
5038
5863
  }
5039
5864
  },
@@ -5109,17 +5934,19 @@ var StreamController = class {
5109
5934
  *
5110
5935
  * @throws 连接已取消,或 RTC 进房、房间配置与本地流发布失败时抛出错误。
5111
5936
  */
5112
- async connect(connection, includeLocalAudio, ensureActive, beforePublish) {
5937
+ async connect(connection, includeLocalAudio, ensureActive2, beforePublish, publishLocalMedia = true) {
5113
5938
  this.configureRoom({ roomID: connection.roomID, botID: connection.botID });
5114
- await this.roomController.join(connection, ensureActive);
5115
- ensureActive();
5939
+ await this.roomController.join(connection, ensureActive2);
5940
+ ensureActive2();
5116
5941
  XmaxLogger.stream.info(
5117
5942
  () => `RTC \u623F\u95F4\u5DF2\u914D\u7F6E (RTC Room Configured)
5118
5943
  \u2514\u2500 roomID: ${connection.roomID}, botID: ${connection.botID ?? "(\u672A\u8BBE\u7F6E)"}`
5119
5944
  );
5120
- await beforePublish?.();
5121
- ensureActive();
5122
- await this.publishLocalStream(includeLocalAudio);
5945
+ if (publishLocalMedia) {
5946
+ await beforePublish?.();
5947
+ ensureActive2();
5948
+ await this.publishLocalStream(includeLocalAudio);
5949
+ }
5123
5950
  }
5124
5951
  /**
5125
5952
  * 清理生成状态、本地发布和远端订阅,并离开当前 RTC 房间。
@@ -5180,6 +6007,7 @@ var StreamController = class {
5180
6007
  }, this.generationTimeoutMs)
5181
6008
  };
5182
6009
  this.state.generationTaskID = taskID;
6010
+ this.state.networkVideoCompletion = options.context.referenceVideo ? { taskID, finished: false, scheduled: false } : void 0;
5183
6011
  this.state.generationWaiter = waiter;
5184
6012
  void confirmation.catch(() => {
5185
6013
  });
@@ -5191,7 +6019,9 @@ var StreamController = class {
5191
6019
  context: options.context
5192
6020
  });
5193
6021
  const state2 = this.state;
5194
- const sent = Promise.resolve(sending).then(() => {
6022
+ const sent = Promise.resolve(sending).then(async () => {
6023
+ if (this.state !== state2 || state2.generationTaskID !== taskID) return;
6024
+ await options.onStartSent?.();
5195
6025
  if (this.state !== state2 || state2.generationTaskID !== taskID) return;
5196
6026
  for (const userID of state2.publishedRemoteUserIDs) {
5197
6027
  void this.handleRemoteVideoPublished(userID, true);
@@ -5257,9 +6087,9 @@ var StreamController = class {
5257
6087
  *
5258
6088
  * @throws 操作已取消、房间未就绪或信令发送失败时抛出错误。
5259
6089
  */
5260
- async changeTargetSize(taskID, targetSize, ensureActive) {
6090
+ async changeTargetSize(taskID, targetSize, ensureActive2) {
5261
6091
  try {
5262
- await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive });
6092
+ await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive: ensureActive2 });
5263
6093
  } catch (error) {
5264
6094
  throw XmaxError.from(error);
5265
6095
  }
@@ -5296,7 +6126,50 @@ var StreamController = class {
5296
6126
  * 设置房间业务消息监听器,传入空值时清除监听器。
5297
6127
  */
5298
6128
  setRoomListener(listener) {
5299
- this.roomController.setListener(listener);
6129
+ this.roomListener = listener;
6130
+ }
6131
+ /**
6132
+ * 生成状态已提交后登记完成通知,避免快速任务在启动返回前交付完成事件。
6133
+ */
6134
+ activateNetworkVideoCompletion(onFinish) {
6135
+ const completion = this.state.networkVideoCompletion;
6136
+ if (completion) {
6137
+ completion.onFinish = onFinish;
6138
+ this.deliverNetworkVideoCompletion();
6139
+ }
6140
+ }
6141
+ /**
6142
+ * 仅接受当前任务和目标机器人的完成消息;不因完成事件立即退房。
6143
+ */
6144
+ handleNetworkVideoCompletion(sender, message) {
6145
+ const state2 = this.state;
6146
+ const completion = state2.networkVideoCompletion;
6147
+ if (!state2.roomID || !completion || completion.taskID !== state2.generationTaskID || message.event !== "video_stopped" || message.uid !== completion.taskID || !sender || state2.botID && sender !== state2.botID) {
6148
+ return;
6149
+ }
6150
+ completion.finished = true;
6151
+ this.deliverNetworkVideoCompletion();
6152
+ }
6153
+ /**
6154
+ * 异步交付一次完成通知;执行前复核任务,隔离用户回调异常。
6155
+ */
6156
+ deliverNetworkVideoCompletion() {
6157
+ const state2 = this.state;
6158
+ const completion = state2.networkVideoCompletion;
6159
+ if (!completion?.finished || !completion.onFinish || completion.scheduled) {
6160
+ return;
6161
+ }
6162
+ completion.scheduled = true;
6163
+ queueMicrotask(() => {
6164
+ if (this.state !== state2 || state2.generationTaskID !== completion.taskID) {
6165
+ return;
6166
+ }
6167
+ try {
6168
+ void Promise.resolve(completion.onFinish?.()).catch(() => {
6169
+ });
6170
+ } catch {
6171
+ }
6172
+ });
5300
6173
  }
5301
6174
  /**
5302
6175
  * 配置当前房间;已有发布、订阅或生成任务时拒绝重新配置。
@@ -5527,6 +6400,7 @@ var StreamController = class {
5527
6400
  const remoteAudioUserIDs = [...this.state.subscribedRemoteAudioUserIDs];
5528
6401
  this.state.generationTaskID = void 0;
5529
6402
  this.state.generationWaiter = void 0;
6403
+ this.state.networkVideoCompletion = void 0;
5530
6404
  this.state.activeRemoteStream = void 0;
5531
6405
  this.state.subscribedRemoteAudioUserIDs.clear();
5532
6406
  if (waiter) {
@@ -6078,13 +6952,18 @@ var RealtimeLaunchTimer = class {
6078
6952
  * 开始新的启动周期并清空旧快照,返回记录相机创建完成的一次性回调。
6079
6953
  */
6080
6954
  startCamera() {
6955
+ this.start();
6956
+ return this.measure("cameraMs", this.startedAt);
6957
+ }
6958
+ /**
6959
+ * 开始新的启动周期;非摄像头源不产生相机和发布耗时。
6960
+ */
6961
+ start() {
6081
6962
  this.sequence += 1;
6082
6963
  this.active = true;
6083
6964
  this.startedAt = performance.now();
6084
6965
  this.snapshot = Object.freeze({});
6085
- const complete = this.measure("cameraMs", this.startedAt);
6086
6966
  this.notify();
6087
- return complete;
6088
6967
  }
6089
6968
  /**
6090
6969
  * 开始等待用户授权系统媒体权限,返回授权决定时调用的记录函数。
@@ -6215,7 +7094,10 @@ var XmaxRealtimeConnectionManager = class {
6215
7094
  "Local video stream has no video format"
6216
7095
  );
6217
7096
  }
6218
- await streamController.setVideoEncoderConfig(videoFormat);
7097
+ const publishLocalMedia = options.publishLocalMedia ?? true;
7098
+ if (publishLocalMedia) {
7099
+ await streamController.setVideoEncoderConfig(videoFormat);
7100
+ }
6219
7101
  options.ensureCurrent();
6220
7102
  let completePublish;
6221
7103
  await streamController.connect(connection, options.includeLocalAudio, () => {
@@ -6225,8 +7107,9 @@ var XmaxRealtimeConnectionManager = class {
6225
7107
  await options.beforePublish?.();
6226
7108
  completeConnection();
6227
7109
  completePublish = this.dependencies.timing.startPublish();
6228
- });
7110
+ }, publishLocalMedia);
6229
7111
  options.ensureCurrent();
7112
+ completeConnection();
6230
7113
  completePublish?.();
6231
7114
  options.onPublished();
6232
7115
  sessionService.startHeartbeat(session.id, {
@@ -6275,7 +7158,7 @@ var XmaxRealtimeConnectionManager = class {
6275
7158
  async refreshCredentials() {
6276
7159
  if (this.credentialRefresh) return this.credentialRefresh;
6277
7160
  const current = this.activeSession;
6278
- if (!current?.connection || current.connection.provider === "trtc") return;
7161
+ if (!current?.connection || current.connection.provider === "trtc" /* trtc */) return;
6279
7162
  const version = this.refreshVersion;
6280
7163
  const operation = (async () => {
6281
7164
  try {
@@ -6428,7 +7311,7 @@ var XmaxRealtimeConnectionManager = class {
6428
7311
  );
6429
7312
  return;
6430
7313
  }
6431
- if (next.provider !== "trtc" && (force || !previous || previous.provider === "trtc" || next.roomToken !== previous.roomToken)) {
7314
+ if (next.provider !== "trtc" /* trtc */ && (force || !previous || previous.provider === "trtc" /* trtc */ || next.roomToken !== previous.roomToken)) {
6432
7315
  await this.dependencies.streamController.updateCredentials(next, this.credentialController?.signal);
6433
7316
  if (version !== this.refreshVersion || this.activeSession !== current) return;
6434
7317
  }
@@ -6526,7 +7409,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
6526
7409
  const confirmation = this.streamController.beginGeneration({
6527
7410
  taskID,
6528
7411
  videoFormat: options.videoFormat,
6529
- context
7412
+ context,
7413
+ ...options.onStartSent && { onStartSent: options.onStartSent }
6530
7414
  });
6531
7415
  await this.awaitConfirmation(confirmation, options.signal);
6532
7416
  options.ensureCurrent();
@@ -6659,6 +7543,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6659
7543
  __publicField(this, "coordinator");
6660
7544
  __publicField(this, "errorHandler");
6661
7545
  __publicField(this, "cameraController");
7546
+ __publicField(this, "networkVideoController", new NetworkVideoController());
7547
+ __publicField(this, "localVideoController", new LocalVideoController((error) => {
7548
+ void this.coordinator.terminateWithError(error, 0 /* connection */);
7549
+ }));
7550
+ __publicField(this, "rtcManager");
6662
7551
  /**
6663
7552
  * 启动计时与首帧通知
6664
7553
  */
@@ -6695,6 +7584,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6695
7584
  this.supportsInterpolation = dependencies?.supportsFrameInterpolation ?? (async (size) => await frameInterpolationAdapter(size) !== null);
6696
7585
  this.errorHandler = new RealtimeErrorHandler();
6697
7586
  const rtcManager = dependencies?.rtcManager ?? createRtcManager(options.provider, dependencies?.environment ?? "china" /* china */);
7587
+ this.rtcManager = rtcManager;
6698
7588
  this.cameraController = dependencies?.cameraController ?? new CameraController({
6699
7589
  rtcManager,
6700
7590
  mediaService: this.mediaService,
@@ -6773,7 +7663,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6773
7663
  */
6774
7664
  async setFrameInterpolationEnabled(enabled) {
6775
7665
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
6776
- const format = this.generationVideoFormat ?? this.cameraController.currentTrack?.videoFormat;
7666
+ const format = this.generationVideoFormat ?? this.currentLocalTrack?.videoFormat;
6777
7667
  const size = format && enabled ? this.mediaService.resolveFrameInterpolationSize(format) : void 0;
6778
7668
  if (enabled && !await this.checkInterpolationSupport(size, token)) {
6779
7669
  token.ensureCurrent();
@@ -6838,7 +7728,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6838
7728
  const size = this.interpolationSize;
6839
7729
  this.connectionManager.setFrameInterpolation(size ? {
6840
7730
  size,
6841
- targetFrameRate: this.options.frameInterpolation.targetFrameRate,
6842
7731
  onFailure: (error) => {
6843
7732
  if (revision === this.interpolationRevision) this.handleInterpolationFailure(error);
6844
7733
  }
@@ -6999,6 +7888,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
6999
7888
  "media" /* media */,
7000
7889
  void 0,
7001
7890
  async (token) => {
7891
+ this.ensureNoLocalSource();
7002
7892
  await this.coordinator.commit(
7003
7893
  new RealtimeState({
7004
7894
  connectionState: "Preparing" /* preparing */
@@ -7035,6 +7925,63 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7035
7925
  }
7036
7926
  );
7037
7927
  }
7928
+ /**
7929
+ * 创建网络视频源并初始化接收端 RTC;预览是否加载成功不影响服务端读取视频。
7930
+ */
7931
+ async createNetworkVideoStream(options) {
7932
+ return this.coordinator.run("media" /* media */, void 0, async (token) => {
7933
+ this.ensureNoLocalSource();
7934
+ const stream = this.networkVideoController.create(options, this.mediaService);
7935
+ token.setFailureScope(1 /* all */);
7936
+ this.launchTimer.start();
7937
+ this.clearVideoStatistics();
7938
+ await this.rtcManager.initialize();
7939
+ token.ensureCurrent();
7940
+ this.generationVideoFormat = stream.videoTrack?.videoFormat;
7941
+ await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
7942
+ return stream;
7943
+ });
7944
+ }
7945
+ /**
7946
+ * 创建文件音视频源并准备外部轨道;准备失败时统一释放本次媒体和 RTC 资源。
7947
+ */
7948
+ async createLocalVideoStream(options) {
7949
+ return this.coordinator.run("media" /* media */, void 0, async (token) => {
7950
+ this.ensureNoLocalSource();
7951
+ if (!this.rtcManager.setExternalMediaTracks) {
7952
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "The RTC provider does not support local video streams");
7953
+ }
7954
+ token.setFailureScope(1 /* all */);
7955
+ this.launchTimer.start();
7956
+ this.clearVideoStatistics();
7957
+ const stream = await this.localVideoController.create(options, this.mediaService, token.signal);
7958
+ token.ensureCurrent();
7959
+ await this.rtcManager.initialize();
7960
+ token.ensureCurrent();
7961
+ await this.rtcManager.setExternalMediaTracks({
7962
+ videoTrack: stream.videoTrack.mediaStreamTrack,
7963
+ audioTrack: this.localVideoController.audioTrack
7964
+ });
7965
+ token.ensureCurrent();
7966
+ this.generationVideoFormat = stream.videoTrack?.videoFormat;
7967
+ await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
7968
+ return stream;
7969
+ });
7970
+ }
7971
+ /**
7972
+ * 当前 Manager 拥有的媒体源轨道。
7973
+ */
7974
+ get currentLocalTrack() {
7975
+ return this.localVideoController.currentTrack ?? this.networkVideoController.currentTrack ?? this.cameraController.currentTrack;
7976
+ }
7977
+ /**
7978
+ * 创建前拒绝覆盖活动源,避免旧预览或连接资源被静默遗留。
7979
+ */
7980
+ ensureNoLocalSource() {
7981
+ if (this.currentLocalTrack || this.localVideoController.isActive) {
7982
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local stream before creating another one");
7983
+ }
7984
+ }
7038
7985
  /**
7039
7986
  * 移除媒体权限弹窗观察;授权决定已记录或不再需要观察时调用。
7040
7987
  */
@@ -7046,14 +7993,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7046
7993
  * 应用完整的上行视频格式,成功后更新本地轨道元数据,不改变生成格式或生命周期。
7047
7994
  * 复用配置操作准入,避免与建连、切换摄像头或其他配置更新并发。
7048
7995
  *
7049
- * @throws 无本地相机流、参数无效、操作冲突或底层编码更新失败时抛错。
7996
+ * @throws 无本地相机或文件视频流、参数无效、操作冲突或底层编码更新失败时抛错。
7050
7997
  */
7051
7998
  async updateVideoFormat(videoFormat) {
7052
7999
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
7053
- const track = this.cameraController.currentTrack;
8000
+ const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
7054
8001
  const previousFormat = track?.videoFormat;
7055
8002
  if (!track || !previousFormat) {
7056
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera stream is not started");
8003
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera or file video stream is not started");
7057
8004
  }
7058
8005
  videoFormat.validate();
7059
8006
  await this.streamController.setVideoEncoderConfig(videoFormat);
@@ -7066,6 +8013,9 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7066
8013
  * 停止本地相机流并释放本地预览与 RTC 资源。
7067
8014
  */
7068
8015
  async stopLocalCameraStream() {
8016
+ if (this.networkVideoController.currentTrack || this.localVideoController.isActive) {
8017
+ return;
8018
+ }
7069
8019
  await this.coordinator.run(
7070
8020
  "media" /* media */,
7071
8021
  void 0,
@@ -7106,11 +8056,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7106
8056
  /**
7107
8057
  * 使用当前 Manager 创建的本地流建立实时连接。
7108
8058
  *
7109
- * 创建实时会话、加入 RTC 房间并发布本地流,成功后启动会话心跳。
8059
+ * 创建实时会话、加入 RTC 房间并启动会话心跳;摄像头源发布本地流,网络视频源仅接收。
7110
8060
  * 启用帧检测时,发布前并行等待相机预热(固定 200ms),避免把黑帧推给 RTC。
7111
8061
  * 返回的远端媒体流在生成开始后承载远端生成画面。
7112
8062
  *
7113
- * @param localStream 由 `createLocalCameraStream` 创建的本地媒体流。
8063
+ * @param localStream 由当前 Manager 创建的摄像头、本地文件或网络视频源。
7114
8064
  * @returns 远端生成结果占位的媒体流。
7115
8065
  * @throws 本地流不属于当前 Manager、已有活动连接、会话创建或进房
7116
8066
  * 发布失败时抛出错误;失败时自动释放连接资源并恢复本地预览。
@@ -7137,7 +8087,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7137
8087
  * 尚未连接时先建立实时连接;已在生成时仅更新生成条件,不重启生成。
7138
8088
  * 首次生成必须提供条件上下文,之后缺省时复用最近一次缓存的上下文。
7139
8089
  *
7140
- * @param options.localStream 由 `createLocalCameraStream` 创建的本地媒体流。
8090
+ * @param options.localStream 由当前 Manager 创建的摄像头、本地文件或网络视频源。
7141
8091
  * @param options.context 本次生成使用的条件上下文;缺省时复用缓存。
7142
8092
  * @returns 承载远端生成画面的媒体流。
7143
8093
  * @throws 本地流不属于当前 Manager、缺少可用的条件上下文、信令发送
@@ -7149,7 +8099,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7149
8099
  void 0,
7150
8100
  async (token) => {
7151
8101
  const localTrack = options.localStream.videoTrack;
7152
- if (!localTrack || localTrack !== this.cameraController.currentTrack) {
8102
+ if (!localTrack || localTrack !== this.currentLocalTrack) {
7153
8103
  throw new XmaxError(
7154
8104
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
7155
8105
  "The local stream must be created and started by this realtime manager"
@@ -7171,7 +8121,12 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7171
8121
  token.ensureCurrent();
7172
8122
  return this.connectionManager.makeRemoteStream();
7173
8123
  }
7174
- const context = this.generationManager.validateContext(options.context);
8124
+ const supplied = this.generationManager.validateContext(options.context);
8125
+ const referenceVideo = this.networkVideoController.reference;
8126
+ if (!referenceVideo && supplied.referenceVideo) {
8127
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Create a network video stream before using a reference video");
8128
+ }
8129
+ const context = referenceVideo ? new RealtimeContext({ ...supplied, referenceVideo }) : supplied;
7175
8130
  if (currentState.connectionState !== "Connected" /* connected */) {
7176
8131
  await this.performConnect(options.localStream, token);
7177
8132
  }
@@ -7183,6 +8138,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7183
8138
  * 断开实时连接并保留当前本地媒体预览。
7184
8139
  */
7185
8140
  async disconnect() {
8141
+ this.localVideoController.pause();
7186
8142
  this.launchTimer.cancel();
7187
8143
  this.clearVideoStatistics();
7188
8144
  await this.coordinator.disconnect();
@@ -7192,6 +8148,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7192
8148
  * 关闭期间重复调用会等待同一个释放任务;关闭完成后仍可重新创建本地流。
7193
8149
  */
7194
8150
  async close() {
8151
+ this.localVideoController.pause();
7195
8152
  this.launchTimer.cancel();
7196
8153
  this.clearVideoStatistics();
7197
8154
  await this.coordinator.terminate(1 /* all */);
@@ -7205,7 +8162,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7205
8162
  */
7206
8163
  async performConnect(localStream, token) {
7207
8164
  const localTrack = localStream.videoTrack;
7208
- if (!localTrack || localTrack !== this.cameraController.currentTrack) {
8165
+ if (!localTrack || localTrack !== this.currentLocalTrack) {
7209
8166
  throw new XmaxError(
7210
8167
  "INVALID_CONFIGURATION" /* invalidConfiguration */,
7211
8168
  "The local stream must be created and started by this realtime manager"
@@ -7223,7 +8180,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7223
8180
  if (token.signal.aborted) abortWarmup();
7224
8181
  try {
7225
8182
  let beforePublish;
7226
- if (this.cameraController.isFrameValidationEnabled) {
8183
+ if (!this.localVideoController.isActive && !this.networkVideoController.currentTrack && this.cameraController.isFrameValidationEnabled) {
7227
8184
  const cameraReady = this.cameraController.waitForValidCameraFrame(warmupController.signal).then(() => {
7228
8185
  token.ensureCurrent();
7229
8186
  });
@@ -7235,7 +8192,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7235
8192
  localTrack,
7236
8193
  remoteVideoFormat: this.generationVideoFormat,
7237
8194
  model: this.options.model,
7238
- includeLocalAudio: this.cameraController.useMicrophone,
8195
+ includeLocalAudio: this.localVideoController.isActive ? this.localVideoController.audioTrack !== void 0 : !this.networkVideoController.currentTrack && this.cameraController.useMicrophone,
8196
+ publishLocalMedia: !this.networkVideoController.currentTrack,
7239
8197
  ensureCurrent: () => token.ensureCurrent(),
7240
8198
  onPublished: () => {
7241
8199
  this.acceptsVideoStatistics = true;
@@ -7270,6 +8228,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7270
8228
  context,
7271
8229
  signal: token.signal,
7272
8230
  ensureCurrent: () => token.ensureCurrent(),
8231
+ onStartSent: this.localVideoController.isActive ? async () => {
8232
+ token.ensureCurrent();
8233
+ await this.localVideoController.start(token.signal);
8234
+ token.ensureCurrent();
8235
+ } : void 0,
7273
8236
  waitUntilRemoteReady: () => this.connectionManager.waitUntilRemoteTrackReady(
7274
8237
  REMOTE_FIRST_FRAME_TIMEOUT_MS,
7275
8238
  token.signal
@@ -7283,6 +8246,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7283
8246
  new RealtimeState({ connectionState: "Generating" /* generating */, sessionID, taskID }),
7284
8247
  token
7285
8248
  );
8249
+ const onFinish = this.networkVideoController.onFinish;
8250
+ if (this.networkVideoController.currentTrack) {
8251
+ this.streamController.activateNetworkVideoCompletion(onFinish && (() => {
8252
+ if (this.currentState.taskID === taskID && this.currentState.connectionState === "Generating" /* generating */) {
8253
+ return onFinish();
8254
+ }
8255
+ }));
8256
+ }
7286
8257
  return this.connectionManager.makeRemoteStream();
7287
8258
  }
7288
8259
  /**
@@ -7299,6 +8270,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7299
8270
  * 统一清理顺序:停止心跳与生成,再断开连接;all 额外释放本地媒体。
7300
8271
  */
7301
8272
  async performCleanup(scope, taskID) {
8273
+ this.localVideoController.pause();
7302
8274
  this.launchTimer.cancel();
7303
8275
  this.remoteFrameDisplayHandler = void 0;
7304
8276
  this.clearVideoStatistics();
@@ -7309,20 +8281,30 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
7309
8281
  await this.generationManager.reset(taskID);
7310
8282
  const sessionID = await this.connectionManager.disconnect();
7311
8283
  if (scope === 1 /* all */) {
8284
+ const hadFileSource = this.networkVideoController.currentTrack !== void 0 || this.localVideoController.isActive;
8285
+ this.networkVideoController.stop();
7312
8286
  this.generationVideoFormat = void 0;
7313
8287
  this.disposePermissionWatch();
7314
8288
  try {
7315
- await this.cameraController.stopLocalCameraStream();
8289
+ if (hadFileSource) {
8290
+ await this.rtcManager.destroy();
8291
+ } else {
8292
+ await this.cameraController.stopLocalCameraStream();
8293
+ }
7316
8294
  } catch (error) {
7317
8295
  XmaxLogger.realtime.error(
7318
- () => `\u505C\u6B62\u672C\u5730\u76F8\u673A\u6D41\u5931\u8D25 (Failed to Stop Local Camera Stream)
8296
+ () => `\u91CA\u653E\u672C\u5730\u5A92\u4F53\u5931\u8D25 (Failed to Release Local Media)
7319
8297
  \u2514\u2500 ${XmaxLogger.localized("\u539F\u56E0\uFF1A", "Reason: ")}${XmaxError.from(error).message}`
7320
8298
  );
8299
+ } finally {
8300
+ if (this.localVideoController.isActive) {
8301
+ await this.localVideoController.stop();
8302
+ }
7321
8303
  }
7322
8304
  }
7323
8305
  return {
7324
8306
  sessionID,
7325
- hasLocalMedia: this.cameraController.currentTrack !== void 0
8307
+ hasLocalMedia: this.currentLocalTrack !== void 0
7326
8308
  };
7327
8309
  }
7328
8310
  /**
@@ -7384,6 +8366,405 @@ var XmaxConfiguration = class {
7384
8366
  }
7385
8367
  };
7386
8368
 
8369
+ // src/Service/NonRealtime/NonRealtimeTask.ts
8370
+ var NonRealtimeQuality = /* @__PURE__ */ ((NonRealtimeQuality2) => {
8371
+ NonRealtimeQuality2["sd"] = "sd";
8372
+ NonRealtimeQuality2["hd"] = "hd";
8373
+ return NonRealtimeQuality2;
8374
+ })(NonRealtimeQuality || {});
8375
+ var NonRealtimeTaskStatus = /* @__PURE__ */ ((NonRealtimeTaskStatus2) => {
8376
+ NonRealtimeTaskStatus2["submitted"] = "submitted";
8377
+ NonRealtimeTaskStatus2["processing"] = "processing";
8378
+ NonRealtimeTaskStatus2["completed"] = "completed";
8379
+ NonRealtimeTaskStatus2["error"] = "error";
8380
+ return NonRealtimeTaskStatus2;
8381
+ })(NonRealtimeTaskStatus || {});
8382
+
8383
+ // src/Service/NonRealtime/NonRealtimeTaskService.ts
8384
+ 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]);
8385
+ var NonRealtimeTaskService = class {
8386
+ /**
8387
+ * 注入非实时任务使用的 API 服务。
8388
+ */
8389
+ constructor(api) {
8390
+ /**
8391
+ * 网络依赖
8392
+ */
8393
+ __publicField(this, "api");
8394
+ this.api = api;
8395
+ }
8396
+ /**
8397
+ * 提交一次任务;请求结果不确定时也不自动重试,以免重复扣费。
8398
+ */
8399
+ async submitTask(options) {
8400
+ const model = options.model?.trim();
8401
+ if (!model) {
8402
+ invalid("Model must not be empty");
8403
+ }
8404
+ const prompt = options.prompt.trim();
8405
+ if (!prompt || [...prompt].length > 1024) {
8406
+ invalid("Prompt must contain 1\u20131024 characters");
8407
+ }
8408
+ validateURL(options.videoPath);
8409
+ if (options.referencePath !== void 0) {
8410
+ validateURL(options.referencePath);
8411
+ }
8412
+ if (!Object.values(NonRealtimeQuality).includes(options.quality)) {
8413
+ invalid("Quality must be sd or hd");
8414
+ }
8415
+ if (options.fps !== void 0 && !SUPPORTED_FPS.has(options.fps)) {
8416
+ invalid("Unsupported processing frame rate");
8417
+ }
8418
+ const payload = await this.api.post("/offline-task", {
8419
+ model,
8420
+ prompt,
8421
+ refVideoPath: options.videoPath.trim(),
8422
+ ...options.referencePath === void 0 ? {} : { refImagePath: options.referencePath.trim() },
8423
+ quality: options.quality,
8424
+ ...options.fps === void 0 ? {} : { fps: options.fps }
8425
+ });
8426
+ return parseTask(payload);
8427
+ }
8428
+ /**
8429
+ * 查询任务,校验返回 ID;signal 只取消本次 HTTP 查询,不取消服务端任务。
8430
+ */
8431
+ async getTask(taskUid, signal) {
8432
+ const uid = validateTaskUid(taskUid);
8433
+ const task = parseTask(await this.api.get(`/offline-task/${encodeURIComponent(uid)}`, { signal }));
8434
+ if (task.uid !== uid) {
8435
+ malformed("Task UID does not match the request");
8436
+ }
8437
+ return task;
8438
+ }
8439
+ /**
8440
+ * 查询最多 100 个 ID,按首次出现顺序去重;任一任务不可用时整体失败。
8441
+ */
8442
+ async getTasks(taskUids) {
8443
+ if (taskUids.length < 1 || taskUids.length > 100) {
8444
+ invalid("Provide 1\u2013100 task UIDs");
8445
+ }
8446
+ const uids = [...new Set(taskUids.map(validateTaskUid))];
8447
+ const payload = record(await this.api.post("/offline-task/batch-query", { taskUids: uids }));
8448
+ const tasks = parseList(payload.list);
8449
+ if (tasks.length !== uids.length || tasks.some((task, index) => task.uid !== uids[index])) {
8450
+ malformed("Batch response does not match the requested task UIDs");
8451
+ }
8452
+ return tasks;
8453
+ }
8454
+ /**
8455
+ * 分页查询任务,不隐式加载其他页。
8456
+ */
8457
+ async listTasks(options = {}) {
8458
+ const pageNumber = options.pageNumber ?? 1;
8459
+ const pageSize = options.pageSize ?? 10;
8460
+ if (!Number.isSafeInteger(pageNumber) || pageNumber < 1) {
8461
+ invalid("Page number must be a positive integer");
8462
+ }
8463
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
8464
+ invalid("Page size must be between 1 and 100");
8465
+ }
8466
+ if (options.status !== void 0 && !Object.values(NonRealtimeTaskStatus).includes(options.status)) {
8467
+ invalid("Invalid task status");
8468
+ }
8469
+ const query = new URLSearchParams({ pageNumber: String(pageNumber), pageSize: String(pageSize) });
8470
+ if (options.status !== void 0) {
8471
+ query.set("status", options.status);
8472
+ }
8473
+ const payload = record(await this.api.get(`/offline-task/page?${query}`));
8474
+ return Object.freeze({
8475
+ pageNumber: pageInteger(payload, "pageNumber", 1),
8476
+ pageSize: pageInteger(payload, "pageSize", 1),
8477
+ total: pageInteger(payload, "total", 0),
8478
+ list: parseList(payload.list)
8479
+ });
8480
+ }
8481
+ };
8482
+ function validateTaskUid(value) {
8483
+ const uid = value.trim();
8484
+ if (!uid || uid === "." || uid === "..") {
8485
+ invalid("Task UID must not be empty or a relative path segment");
8486
+ }
8487
+ return uid;
8488
+ }
8489
+ function validateURL(value) {
8490
+ let url;
8491
+ try {
8492
+ url = new URL(value.trim());
8493
+ } catch {
8494
+ return invalid("Resource must be an uploaded HTTP(S) URL");
8495
+ }
8496
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password) {
8497
+ invalid("Resource must be an uploaded HTTP(S) URL");
8498
+ }
8499
+ }
8500
+ function invalid(message) {
8501
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, message);
8502
+ }
8503
+ function malformed(message = "Invalid non-realtime response data") {
8504
+ throw new XmaxError("API_ERROR" /* apiError */, message);
8505
+ }
8506
+ function record(value) {
8507
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8508
+ malformed();
8509
+ }
8510
+ return value;
8511
+ }
8512
+ function field(data, key, type) {
8513
+ const value = data[key];
8514
+ if (value === void 0 || value === null) {
8515
+ return void 0;
8516
+ }
8517
+ if (typeof value !== type || typeof value === "number" && (!Number.isFinite(value) || value < 0)) {
8518
+ malformed(`Invalid field: ${key}`);
8519
+ }
8520
+ return value;
8521
+ }
8522
+ function pageInteger(data, key, minimum) {
8523
+ const value = field(data, key, "number");
8524
+ if (value === void 0 || !Number.isSafeInteger(value) || value < minimum) {
8525
+ malformed(`Invalid field: ${key}`);
8526
+ }
8527
+ return value;
8528
+ }
8529
+ function parseList(value) {
8530
+ if (!Array.isArray(value)) {
8531
+ malformed("Missing task list");
8532
+ }
8533
+ return Object.freeze(value.map(parseTask));
8534
+ }
8535
+ function parseTask(value) {
8536
+ const data = record(value);
8537
+ const uid = field(data, "uid", "string");
8538
+ const status = field(data, "status", "string");
8539
+ const quality = field(data, "quality", "string");
8540
+ if (!uid?.trim() || !Object.values(NonRealtimeTaskStatus).includes(status)) {
8541
+ malformed("Missing task UID or invalid status");
8542
+ }
8543
+ if (quality !== void 0 && !Object.values(NonRealtimeQuality).includes(quality)) {
8544
+ malformed("Invalid task quality");
8545
+ }
8546
+ return Object.freeze({
8547
+ uid,
8548
+ status,
8549
+ quality,
8550
+ userUid: field(data, "userUid", "string"),
8551
+ prompt: field(data, "prompt", "string"),
8552
+ videoPath: field(data, "refVideoPath", "string"),
8553
+ referencePath: field(data, "refImagePath", "string"),
8554
+ processorId: field(data, "processorId", "string"),
8555
+ requestedFps: field(data, "requestedFps", "number"),
8556
+ resolvedFps: field(data, "resolvedFps", "number"),
8557
+ retryCount: field(data, "retryCount", "number"),
8558
+ videoDurationSeconds: field(data, "videoDurationSeconds", "number"),
8559
+ billableDurationSeconds: field(data, "billableDurationSeconds", "number"),
8560
+ chargePoints: field(data, "chargePoints", "number"),
8561
+ result: data.result == null ? void 0 : parseResult(data.result),
8562
+ processStartTime: field(data, "processStartTime", "string"),
8563
+ createTimestamp: field(data, "createTimestamp", "string"),
8564
+ updateTimestamp: field(data, "updateTimestamp", "string")
8565
+ });
8566
+ }
8567
+ function parseResult(value) {
8568
+ const data = record(value);
8569
+ return Object.freeze({
8570
+ url: field(data, "result_url", "string"),
8571
+ uploadError: field(data, "upload_error", "string"),
8572
+ uploadStatus: field(data, "upload_status", "string"),
8573
+ finalizeDurationMs: field(data, "finalize_duration_ms", "number"),
8574
+ compressionLevel: field(data, "compression_level", "string"),
8575
+ frameCount: field(data, "frame_count", "number"),
8576
+ fps: field(data, "fps", "number"),
8577
+ durationMs: field(data, "result_duration_ms", "number"),
8578
+ fileSizeBytes: field(data, "file_size_bytes", "number"),
8579
+ droppedFrameCount: field(data, "dropped_frame_count", "number"),
8580
+ originalFileSizeBytes: field(data, "original_file_size_bytes", "number")
8581
+ });
8582
+ }
8583
+
8584
+ // src/Service/NonRealtime/NonRealtimeTaskError.ts
8585
+ var NonRealtimeTaskError = class extends XmaxError {
8586
+ /**
8587
+ * 使用 API 错误分类,不虚构服务端未返回的业务错误码。
8588
+ */
8589
+ constructor(task, message) {
8590
+ super("API_ERROR" /* apiError */, message);
8591
+ /**
8592
+ * 失败上下文
8593
+ */
8594
+ __publicField(this, "task");
8595
+ this.name = "NonRealtimeTaskError";
8596
+ this.task = task;
8597
+ }
8598
+ };
8599
+
8600
+ // src/Core/NonRealtime/XmaxNonRealtimeManager.ts
8601
+ var XmaxNonRealtimeManager = class {
8602
+ /**
8603
+ * 注入非实时任务服务。
8604
+ */
8605
+ constructor(tasks) {
8606
+ /**
8607
+ * 服务层组件
8608
+ */
8609
+ __publicField(this, "tasks");
8610
+ this.tasks = tasks;
8611
+ }
8612
+ /**
8613
+ * 单次提交任务,不自动重试可能已扣费的请求。
8614
+ */
8615
+ submitTask(options) {
8616
+ return this.tasks.submitTask(options);
8617
+ }
8618
+ /**
8619
+ * 取得任务快照,不把服务端失败终态当成查询失败。
8620
+ */
8621
+ getTask(taskUid, options) {
8622
+ return this.tasks.getTask(taskUid, options?.signal);
8623
+ }
8624
+ /**
8625
+ * 批量取得任务快照。
8626
+ */
8627
+ getTasks(taskUids) {
8628
+ return this.tasks.getTasks(taskUids);
8629
+ }
8630
+ /**
8631
+ * 分页取得任务列表。
8632
+ */
8633
+ listTasks(options) {
8634
+ return this.tasks.listTasks(options);
8635
+ }
8636
+ /**
8637
+ * 等待任务完成;查询串行执行,瞬时错误有限退避,取消与总超时覆盖请求及等待阶段。
8638
+ */
8639
+ async waitForCompletion(taskUid, options = {}) {
8640
+ const uid = validateTaskUid(taskUid);
8641
+ const interval = options.intervalMs ?? 2e3;
8642
+ const retries = options.maxConsecutiveRetries ?? 3;
8643
+ validateDelay(interval);
8644
+ if (options.timeoutMs !== void 0) {
8645
+ validateDelay(options.timeoutMs);
8646
+ }
8647
+ if (!Number.isSafeInteger(retries) || retries < 0) {
8648
+ throw new XmaxError(
8649
+ "INVALID_CONFIGURATION" /* invalidConfiguration */,
8650
+ "Retry count must be a non-negative integer"
8651
+ );
8652
+ }
8653
+ const controller = new AbortController();
8654
+ let timedOut = false;
8655
+ const abort = () => controller.abort();
8656
+ options.signal?.addEventListener("abort", abort, { once: true });
8657
+ if (options.signal?.aborted) {
8658
+ abort();
8659
+ }
8660
+ const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => {
8661
+ timedOut = true;
8662
+ abort();
8663
+ }, options.timeoutMs);
8664
+ let failures = 0;
8665
+ try {
8666
+ for (; ; ) {
8667
+ ensureActive(controller.signal);
8668
+ let task;
8669
+ try {
8670
+ task = await this.tasks.getTask(uid, controller.signal);
8671
+ } catch (error) {
8672
+ ensureActive(controller.signal);
8673
+ if (!isTransient(error) || failures >= retries) {
8674
+ throw error;
8675
+ }
8676
+ failures++;
8677
+ await delay(Math.min(interval * 2 ** (failures - 1), 3e4), controller.signal);
8678
+ continue;
8679
+ }
8680
+ ensureActive(controller.signal);
8681
+ failures = 0;
8682
+ try {
8683
+ void Promise.resolve(options.onTaskUpdated?.(task)).catch(() => {
8684
+ });
8685
+ } catch {
8686
+ }
8687
+ ensureActive(controller.signal);
8688
+ if (task.status === "error" /* error */) {
8689
+ throw new NonRealtimeTaskError(task, `Non-realtime task ${uid} failed`);
8690
+ }
8691
+ if (task.status === "completed" /* completed */) {
8692
+ if (!isResultAvailable(task)) {
8693
+ throw new NonRealtimeTaskError(
8694
+ task,
8695
+ `Non-realtime task ${uid} completed without a usable result`
8696
+ );
8697
+ }
8698
+ return task;
8699
+ }
8700
+ await delay(interval, controller.signal);
8701
+ }
8702
+ } catch (error) {
8703
+ if (timedOut) {
8704
+ throw new XmaxError(
8705
+ "TIMEOUT" /* timeout */,
8706
+ `Waiting for task ${uid} timed out; the server task was not cancelled`
8707
+ );
8708
+ }
8709
+ ensureActive(controller.signal);
8710
+ throw error;
8711
+ } finally {
8712
+ clearTimeout(timer);
8713
+ options.signal?.removeEventListener("abort", abort);
8714
+ }
8715
+ }
8716
+ };
8717
+ function validateDelay(value) {
8718
+ if (!Number.isFinite(value) || value <= 0 || value > 2147483647) {
8719
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Polling durations must be positive and within the timer range");
8720
+ }
8721
+ }
8722
+ function ensureActive(signal) {
8723
+ if (signal.aborted) {
8724
+ throw new XmaxError(
8725
+ "CANCELLED" /* cancelled */,
8726
+ "Task wait was cancelled locally; the server task was not cancelled"
8727
+ );
8728
+ }
8729
+ }
8730
+ function delay(ms, signal) {
8731
+ return new Promise((resolve, reject) => {
8732
+ const finish = () => {
8733
+ clearTimeout(timer);
8734
+ signal.removeEventListener("abort", finish);
8735
+ try {
8736
+ ensureActive(signal);
8737
+ resolve();
8738
+ } catch (error) {
8739
+ reject(error);
8740
+ }
8741
+ };
8742
+ const timer = setTimeout(finish, ms);
8743
+ signal.addEventListener("abort", finish, { once: true });
8744
+ if (signal.aborted) {
8745
+ finish();
8746
+ }
8747
+ });
8748
+ }
8749
+ function isTransient(error) {
8750
+ if (!(error instanceof XmaxError)) {
8751
+ return false;
8752
+ }
8753
+ 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);
8754
+ }
8755
+ function isResultAvailable(task) {
8756
+ const result = task.result;
8757
+ if (!result?.url?.trim() || result.uploadError?.trim() || result.uploadStatus !== void 0 && result.uploadStatus !== "success") {
8758
+ return false;
8759
+ }
8760
+ try {
8761
+ const url = new URL(result.url);
8762
+ return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
8763
+ } catch {
8764
+ return false;
8765
+ }
8766
+ }
8767
+
7387
8768
  // src/Core/XmaxClient.ts
7388
8769
  var XmaxClient = class {
7389
8770
  /**
@@ -7429,6 +8810,17 @@ var XmaxClient = class {
7429
8810
  environment: this.configuration.environment
7430
8811
  });
7431
8812
  }
8813
+ /**
8814
+ * 创建非实时视频任务 Manager,使用全局 API Key 和当前环境的非实时任务服务地址。
8815
+ * 本方法不发起网络请求或加载 RTC,任务由服务端持久化,可按 ID 恢复查询。
8816
+ */
8817
+ createNonRealtimeManager() {
8818
+ const apiService = new ApiService({
8819
+ apiKey: this.configuration.apiKey,
8820
+ baseURL: nonRealtimeApiBaseURL(this.configuration.environment)
8821
+ });
8822
+ return new XmaxNonRealtimeManager(new NonRealtimeTaskService(apiService));
8823
+ }
7432
8824
  /**
7433
8825
  * 创建媒体处理与能力查询 Service。
7434
8826
  *
@@ -7440,7 +8832,7 @@ var XmaxClient = class {
7440
8832
  /**
7441
8833
  * 创建文件存储 Service。
7442
8834
  *
7443
- * @returns 可上传图片到对象存储的存储 Service,返回地址可用作参考图路径。
8835
+ * @returns 可上传图片和视频到对象存储的存储 Service。
7444
8836
  */
7445
8837
  createStorageService() {
7446
8838
  return new StorageService({ apiService: this.apiService });
@@ -7452,45 +8844,36 @@ var RealtimeConfiguration = class {
7452
8844
  /**
7453
8845
  * 创建实时业务配置。
7454
8846
  *
7455
- * @param init.model 实时生成业务使用的模型。
7456
- * @param init.provider RTC 提供方,默认 TRTC;创建后不可切换。
7457
- * @param init.isFrameInterpolationEnabled 是否默认开启远端生成画面的插帧。
7458
- * @param init.frameInterpolation 插帧开关和显示帧率上限的详细配置。
8847
+ * @param init.model 实时生成业务使用的模型;未知名称原样透传,默认配置同 x2.1-preview。
8848
+ * @param init.provider RTC 提供方,默认按模型定义选择;必须受模型支持,创建后不可切换。
8849
+ * @param init.isFrameInterpolationEnabled 是否默认开启远端生成画面的插帧,默认 false。
7459
8850
  */
7460
8851
  constructor(init) {
7461
- /**
7462
- * RTC 配置
7463
- */
7464
- __publicField(this, "provider");
7465
- /**
7466
- * 模型配置
7467
- */
7468
8852
  /**
7469
8853
  * 实时生成业务使用的模型。
7470
8854
  */
7471
8855
  __publicField(this, "model");
7472
8856
  /**
7473
- * 远端插帧配置
8857
+ * 实时连接使用的 RTC 提供方。
7474
8858
  */
8859
+ __publicField(this, "provider");
7475
8860
  /**
7476
8861
  * 是否默认请求开启远端生成画面的插帧。
7477
8862
  */
7478
8863
  __publicField(this, "isFrameInterpolationEnabled");
7479
- /**
7480
- * 已补全默认值的只读插帧配置。
7481
- */
7482
- __publicField(this, "frameInterpolation");
7483
- this.provider = init.provider ?? "trtc" /* trtc */;
8864
+ const providers = supportedRtcProviders(init.model);
8865
+ this.provider = init.provider ?? providers[0];
7484
8866
  if (!Object.values(RtcProvider).includes(this.provider)) {
7485
8867
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported RTC provider");
7486
8868
  }
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");
8869
+ if (!providers.includes(this.provider)) {
8870
+ throw new XmaxError(
8871
+ "INVALID_CONFIGURATION" /* invalidConfiguration */,
8872
+ `Model ${init.model} does not support RTC provider ${this.provider}`
8873
+ );
7491
8874
  }
7492
- this.isFrameInterpolationEnabled = init.frameInterpolation?.enabled ?? init.isFrameInterpolationEnabled ?? false;
7493
- this.frameInterpolation = Object.freeze({ enabled: this.isFrameInterpolationEnabled, targetFrameRate });
8875
+ this.model = init.model;
8876
+ this.isFrameInterpolationEnabled = init.isFrameInterpolationEnabled ?? false;
7494
8877
  }
7495
8878
  };
7496
8879
 
@@ -7501,29 +8884,6 @@ var VideoContentMode = /* @__PURE__ */ ((VideoContentMode2) => {
7501
8884
  return VideoContentMode2;
7502
8885
  })(VideoContentMode || {});
7503
8886
 
7504
- // src/Service/Realtime/RealtimeContext.ts
7505
- var RealtimeContext = class {
7506
- /**
7507
- * 创建实时生成条件并规范化文本和参考路径。
7508
- */
7509
- constructor(init) {
7510
- /**
7511
- * 生成条件
7512
- */
7513
- /**
7514
- * 实时生成使用的文本条件。
7515
- */
7516
- __publicField(this, "prompt");
7517
- /**
7518
- * 已上传参考图片的可选远端路径。
7519
- */
7520
- __publicField(this, "referencePath");
7521
- this.prompt = init.prompt.trim();
7522
- const normalizedReferencePath = init.referencePath?.trim();
7523
- this.referencePath = normalizedReferencePath || void 0;
7524
- }
7525
- };
7526
-
7527
8887
  // ../../node_modules/.pnpm/framegen@1.4.0/node_modules/framegen/rt.js
7528
8888
  var WG = 8;
7529
8889
  var texBgSeq = 0;
@@ -9516,11 +10876,11 @@ var _FrameInterpolationManager = class _FrameInterpolationManager {
9516
10876
  });
9517
10877
  }
9518
10878
  static async create(canvas, size, onFailure, signal) {
9519
- const ensureActive = () => {
10879
+ const ensureActive2 = () => {
9520
10880
  if (signal.aborted) throw new XmaxError("CANCELLED" /* cancelled */, "Interpolation initialization cancelled");
9521
10881
  };
9522
10882
  const adapter = await frameInterpolationAdapter(size);
9523
- ensureActive();
10883
+ ensureActive2();
9524
10884
  if (!adapter) throw new XmaxError("FRAME_INTERPOLATION_UNSUPPORTED" /* frameInterpolationUnsupported */, "WebGPU shader-f16 is unavailable");
9525
10885
  const device = await adapter.requestDevice({ requiredFeatures: ["shader-f16"] });
9526
10886
  const manager = new _FrameInterpolationManager(device, canvas, size, onFailure);
@@ -9531,9 +10891,9 @@ var _FrameInterpolationManager = class _FrameInterpolationManager {
9531
10891
  if (!manager.destroyed) onFailure(new Error(`Interpolation GPU device lost: ${info.message}`));
9532
10892
  });
9533
10893
  try {
9534
- ensureActive();
10894
+ ensureActive2();
9535
10895
  await manager.initialize();
9536
- ensureActive();
10896
+ ensureActive2();
9537
10897
  return manager;
9538
10898
  } catch (error) {
9539
10899
  manager.destroy();
@@ -9659,6 +11019,7 @@ __publicField(_FrameInterpolationManager, "originalSlots", 3);
9659
11019
  var FrameInterpolationManager = _FrameInterpolationManager;
9660
11020
 
9661
11021
  // src/Render/Video/RemoteVideoFramePipeline.ts
11022
+ var INTERPOLATION_FRAME_RATE = 60;
9662
11023
  var RemoteVideoFramePipeline = class {
9663
11024
  constructor(video, canvas, options, createProcessor = FrameInterpolationManager.create) {
9664
11025
  __publicField(this, "video", video);
@@ -9753,7 +11114,9 @@ var RemoteVideoFramePipeline = class {
9753
11114
  this.delay = interval;
9754
11115
  this.enqueue({ slot, time });
9755
11116
  this.startTick();
9756
- if (!consecutive || interval < 2e3 / this.options.targetFrameRate - 1 || previousSlot === void 0 || this.busy) return;
11117
+ if (!consecutive || interval < 2e3 / INTERPOLATION_FRAME_RATE - 1 || previousSlot === void 0 || this.busy) {
11118
+ return;
11119
+ }
9757
11120
  this.interpolate(previousSlot, slot, previous, time, interval);
9758
11121
  }
9759
11122
  /** 帧按显示时间升序入队;同一时间的内容后到先出。 */
@@ -9992,14 +11355,14 @@ var XmaxVideoView = class {
9992
11355
  attach(to) {
9993
11356
  this.detached = false;
9994
11357
  to.appendChild(this.element);
9995
- if (this.videoElement.srcObject && this.videoElement.paused && typeof this.videoElement.play === "function") {
11358
+ if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && this.videoElement.autoplay && !this.videoElement.ended && this.videoElement.paused && typeof this.videoElement.play === "function") {
9996
11359
  void this.videoElement.play().catch((error) => {
9997
11360
  XmaxLogger.render.warning(() => `\u6062\u590D\u89C6\u9891\u64AD\u653E\u5931\u8D25 (Failed to Resume Video Playback)
9998
11361
  \u2514\u2500 ${String(error)}`);
9999
11362
  });
10000
11363
  }
10001
11364
  this.startInterpolation();
10002
- if (this.videoElement.srcObject && !this.hasNotifiedFrameDisplay) {
11365
+ if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && !this.hasNotifiedFrameDisplay) {
10003
11366
  this.armFrameDisplayNotification();
10004
11367
  }
10005
11368
  }
@@ -10010,8 +11373,29 @@ var XmaxVideoView = class {
10010
11373
  this.detached = true;
10011
11374
  this.stopInterpolation();
10012
11375
  this.cancelFrameDisplayNotification();
11376
+ if (this.videoElement.getAttribute("src")) {
11377
+ this.videoElement.pause();
11378
+ }
10013
11379
  this.element.remove();
10014
11380
  }
11381
+ /**
11382
+ * 设置网络视频预览;静音播放一次,不捕获或发布媒体轨道。 @internal
11383
+ */
11384
+ setVideoURL(url) {
11385
+ this.stopInterpolation();
11386
+ this.cancelFrameDisplayNotification();
11387
+ this.hasNotifiedFrameDisplay = false;
11388
+ this.videoElement.pause();
11389
+ this.videoElement.srcObject = null;
11390
+ this.videoElement.removeAttribute("src");
11391
+ this.videoElement.muted = true;
11392
+ this.videoElement.loop = false;
11393
+ if (url) {
11394
+ this.videoElement.src = url;
11395
+ this.armFrameDisplayNotification();
11396
+ }
11397
+ this.videoElement.load();
11398
+ }
10015
11399
  /**
10016
11400
  * 直接设置渲染用的媒体流(绑定管线和直接渲染共用)。 @internal
10017
11401
  */
@@ -10024,6 +11408,7 @@ var XmaxVideoView = class {
10024
11408
  this.hasNotifiedFrameDisplay = false;
10025
11409
  this.videoElement.srcObject = stream;
10026
11410
  if (stream) {
11411
+ this.videoElement.autoplay = true;
10027
11412
  this.armFrameDisplayNotification();
10028
11413
  this.startInterpolation();
10029
11414
  }
@@ -10097,6 +11482,9 @@ var XmaxVideoView = class {
10097
11482
  this.setFrameInterpolation(void 0);
10098
11483
  this.cancelFrameDisplayNotification();
10099
11484
  this.videoElement.srcObject = null;
11485
+ if (this.videoElement.getAttribute("src")) {
11486
+ this.setVideoURL(null);
11487
+ }
10100
11488
  }
10101
11489
  /**
10102
11490
  * 首帧提交合成器时通知;旧浏览器回退到 playing 近似判断。
@@ -10324,25 +11712,31 @@ export {
10324
11712
  XmaxError,
10325
11713
  RealtimeVideoEncoderPreference,
10326
11714
  RealtimeVideoFormat,
11715
+ RtcProvider,
10327
11716
  RealtimeModel,
11717
+ modelDisplayName,
10328
11718
  defaultFrameRate,
10329
11719
  defaultCameraVideoFormat,
11720
+ supportedRtcProviders,
10330
11721
  MediaService,
10331
11722
  StoredFile,
10332
11723
  StorageService,
10333
11724
  CameraPosition,
10334
- RtcProvider,
10335
11725
  RealtimeVideoTrack,
10336
11726
  RealtimeMediaStream,
11727
+ RealtimeVideoSampleMethod,
11728
+ RealtimeContext,
10337
11729
  RealtimeConnectionState,
10338
11730
  RealtimeReason,
10339
11731
  RealtimeState,
10340
11732
  XmaxConfiguration,
11733
+ NonRealtimeQuality,
11734
+ NonRealtimeTaskStatus,
11735
+ NonRealtimeTaskError,
10341
11736
  XmaxClient,
10342
11737
  RealtimeConfiguration,
10343
11738
  VideoContentMode,
10344
- RealtimeContext,
10345
11739
  XmaxVideoView,
10346
11740
  XmaxRealtimeVideoView
10347
11741
  };
10348
- //# sourceMappingURL=chunk-FMAQXPTE.js.map
11742
+ //# sourceMappingURL=chunk-VMJXIVUF.js.map