@xmaxai/web-sdk 1.0.8 → 1.0.9

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.
@@ -1,6 +1,6 @@
1
- var __defProp = Object.defineProperty;
2
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
1
+ import {
2
+ __publicField
3
+ } from "./chunk-V6TY7KAL.js";
4
4
 
5
5
  // src/Foundation/Runtime/XmaxEnvironment.ts
6
6
  var XmaxEnvironment = /* @__PURE__ */ ((XmaxEnvironment3) => {
@@ -205,7 +205,7 @@ var XmaxError = class _XmaxError extends Error {
205
205
  };
206
206
 
207
207
  // src/Foundation/Runtime/RuntimeInfo.ts
208
- var XMAX_SDK_VERSION = "1.0.8";
208
+ var XMAX_SDK_VERSION = "1.0.9";
209
209
  var _RuntimeInfo = class _RuntimeInfo {
210
210
  /**
211
211
  * 序列化为接口传输使用的 snake_case 字段。
@@ -602,6 +602,8 @@ var RealtimeModel = /* @__PURE__ */ ((RealtimeModel2) => {
602
602
  RealtimeModel2["x2_0_trtc"] = "x2.0-trtc";
603
603
  RealtimeModel2["x2_1_preview"] = "x2.1-preview";
604
604
  RealtimeModel2["x2_1_preview_1014"] = "x2.1-preview-1014";
605
+ RealtimeModel2["x2_1_1_preview_cn"] = "x2.1.1-preview-cn";
606
+ RealtimeModel2["x2_1_1_preview_us"] = "x2.1.1-preview-us";
605
607
  return RealtimeModel2;
606
608
  })(RealtimeModel || {});
607
609
  function isKnownModel(model) {
@@ -617,11 +619,15 @@ var MODEL_DISPLAY_NAMES = {
617
619
  ["x2.0" /* x2_0 */]: "X2.0",
618
620
  ["x2.0-trtc" /* x2_0_trtc */]: "X2.1-preview-trtc",
619
621
  ["x2.1-preview" /* x2_1_preview */]: "X2.1-preview-agora",
620
- ["x2.1-preview-1014" /* x2_1_preview_1014 */]: "X2.1-preview-1014"
622
+ ["x2.1-preview-1014" /* x2_1_preview_1014 */]: "X2.1-preview-1014",
623
+ ["x2.1.1-preview-cn" /* x2_1_1_preview_cn */]: "X2.1.1-preview-cn",
624
+ ["x2.1.1-preview-us" /* x2_1_1_preview_us */]: "X2.1.1-preview-us"
621
625
  };
622
626
  var MODEL_BASE_URLS = {
623
627
  ["x2.1-preview" /* x2_1_preview */]: "https://cloud.xmax.22duck.cn/open/api/v1",
624
- ["x2.1-preview-1014" /* x2_1_preview_1014 */]: "https://dev.xmaxai.com/open/api/v1"
628
+ ["x2.1-preview-1014" /* x2_1_preview_1014 */]: "https://dev.xmaxai.com/open/api/v1",
629
+ ["x2.1.1-preview-cn" /* x2_1_1_preview_cn */]: "https://cloud.xmax.22duck.cn/open/api/v1",
630
+ ["x2.1.1-preview-us" /* x2_1_1_preview_us */]: "https://api.xmax.ai/open/api/v1"
625
631
  };
626
632
  var RESOLUTION_BUCKETS = {
627
633
  ["x2.0" /* x2_0 */]: [],
@@ -636,13 +642,23 @@ var RESOLUTION_BUCKETS = {
636
642
  ["x2.1-preview-1014" /* x2_1_preview_1014 */]: [
637
643
  { width: 1024, height: 1920 },
638
644
  { width: 1920, height: 1024 }
645
+ ],
646
+ ["x2.1.1-preview-cn" /* x2_1_1_preview_cn */]: [
647
+ { width: 1024, height: 1920 },
648
+ { width: 1920, height: 1024 }
649
+ ],
650
+ ["x2.1.1-preview-us" /* x2_1_1_preview_us */]: [
651
+ { width: 1024, height: 1920 },
652
+ { width: 1920, height: 1024 }
639
653
  ]
640
654
  };
641
655
  var MAXIMUM_INPUT_PIXELS = {
642
656
  ["x2.0" /* x2_0 */]: 128e4,
643
657
  ["x2.0-trtc" /* x2_0_trtc */]: 21e5,
644
658
  ["x2.1-preview" /* x2_1_preview */]: 21e5,
645
- ["x2.1-preview-1014" /* x2_1_preview_1014 */]: 21e5
659
+ ["x2.1-preview-1014" /* x2_1_preview_1014 */]: 21e5,
660
+ ["x2.1.1-preview-cn" /* x2_1_1_preview_cn */]: 21e5,
661
+ ["x2.1.1-preview-us" /* x2_1_1_preview_us */]: 21e5
646
662
  };
647
663
  function modelDisplayName(model) {
648
664
  return isKnownModel(model) ? MODEL_DISPLAY_NAMES[model] : model;
@@ -675,7 +691,9 @@ var MODEL_RTC_PROVIDERS = {
675
691
  ["x2.0" /* x2_0 */]: Object.freeze(["vertc" /* vertc */]),
676
692
  ["x2.0-trtc" /* x2_0_trtc */]: Object.freeze(["trtc" /* trtc */]),
677
693
  ["x2.1-preview" /* x2_1_preview */]: Object.freeze(["agora" /* agora */]),
678
- ["x2.1-preview-1014" /* x2_1_preview_1014 */]: Object.freeze(["agora" /* agora */])
694
+ ["x2.1-preview-1014" /* x2_1_preview_1014 */]: Object.freeze(["agora" /* agora */]),
695
+ ["x2.1.1-preview-cn" /* x2_1_1_preview_cn */]: Object.freeze(["agora" /* agora */]),
696
+ ["x2.1.1-preview-us" /* x2_1_1_preview_us */]: Object.freeze(["agora" /* agora */])
679
697
  };
680
698
  function supportedRtcProviders(model) {
681
699
  return isKnownModel(model) ? MODEL_RTC_PROVIDERS[model] : CUSTOM_MODEL_RTC_PROVIDERS;
@@ -2133,6 +2151,12 @@ var _AgoraRtcManager = class _AgoraRtcManager {
2133
2151
  if (volume > 0 && this.requestedAudio.has(String(user.uid))) track.play();
2134
2152
  else track.stop();
2135
2153
  }
2154
+ /**
2155
+ * 获取原始远端音轨,不依赖声网音频播放器是否处于播放状态。
2156
+ */
2157
+ getRemoteAudioTrack(userID) {
2158
+ return this.client?.remoteUsers.find((user) => String(user.uid) === userID)?.audioTrack?.getMediaStreamTrack();
2159
+ }
2136
2160
  /**
2137
2161
  * 按房间顺序发送消息,限制每秒包数与总字节;失败返回给原业务调用。
2138
2162
  */
@@ -3031,6 +3055,15 @@ var TrtcRtcManager = class {
3031
3055
  get supportsRemoteAudioVolumeControl() {
3032
3056
  return canControlTrtcRemoteAudioVolume();
3033
3057
  }
3058
+ /**
3059
+ * 获取原始远端音轨;不支持独立播放音量的平台暂不开放录制。
3060
+ */
3061
+ getRemoteAudioTrack(userID) {
3062
+ if (!canControlTrtcRemoteAudioVolume()) {
3063
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "TRTC recording cannot preserve remote audio while muted on this platform");
3064
+ }
3065
+ return this.requireEngine().getAudioTrack(userID) ?? void 0;
3066
+ }
3034
3067
  /**
3035
3068
  * 设置指定远端用户的音频播放音量。
3036
3069
  *
@@ -3656,6 +3689,12 @@ var VeRtcManager = class {
3656
3689
  this.audioPlayers.set(userID, player);
3657
3690
  this.applyAudioVolume(userID);
3658
3691
  }
3692
+ /**
3693
+ * 获取原始远端音轨,独立于 audio 元素的静音和播放音量。
3694
+ */
3695
+ getRemoteAudioTrack(userID) {
3696
+ return this.requireEngine().getRemoteStreamTrack(userID, this.sdk.StreamIndex.STREAM_INDEX_MAIN, "audio");
3697
+ }
3659
3698
  setRemoteAudioVolume(volume, userID) {
3660
3699
  this.audioVolumes.set(userID, Number.isFinite(volume) ? Math.max(0, Math.min(100, Math.round(volume))) : 0);
3661
3700
  this.applyAudioVolume(userID);
@@ -3919,6 +3958,48 @@ function createRtcManager(provider, environment) {
3919
3958
  }
3920
3959
  }
3921
3960
 
3961
+ // src/Service/Realtime/RealtimeExtraParams.ts
3962
+ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
3963
+ "model",
3964
+ "face_replace",
3965
+ "size",
3966
+ "target_size",
3967
+ "prompt",
3968
+ "ref_image_path",
3969
+ "mirror",
3970
+ "ref_video_path",
3971
+ "sample_method"
3972
+ ]);
3973
+ function snapshotExtraParams(params) {
3974
+ if (params === void 0) return void 0;
3975
+ const invalid2 = (message) => {
3976
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, message);
3977
+ };
3978
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
3979
+ return invalid2("extraParams must be a JSON object");
3980
+ }
3981
+ for (const key of Object.keys(params)) {
3982
+ if (RESERVED_PARAMS.has(key)) return invalid2(`extraParams conflicts with SDK parameter: ${key}`);
3983
+ }
3984
+ const ancestors = /* @__PURE__ */ new Set();
3985
+ const validate = (value) => {
3986
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
3987
+ if (typeof value === "number" && Number.isFinite(value)) return;
3988
+ if (typeof value !== "object" || !value) return invalid2("extraParams must contain only JSON values");
3989
+ if (ancestors.has(value)) return invalid2("extraParams must not contain circular references");
3990
+ const prototype = Object.getPrototypeOf(value);
3991
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
3992
+ return invalid2("extraParams must contain only JSON objects and arrays");
3993
+ }
3994
+ if (Object.getOwnPropertySymbols(value).length) return invalid2("extraParams must not contain symbol keys");
3995
+ ancestors.add(value);
3996
+ for (const child of Array.isArray(value) ? value : Object.values(value)) validate(child);
3997
+ ancestors.delete(value);
3998
+ };
3999
+ validate(params);
4000
+ return JSON.parse(JSON.stringify(params));
4001
+ }
4002
+
3922
4003
  // src/Foundation/Media/Camera/CameraFrameValidator.ts
3923
4004
  var CAMERA_WARMUP_MS = 200;
3924
4005
  function waitForValidCameraFrame(track, signal) {
@@ -4413,121 +4494,26 @@ var _CameraController = class _CameraController {
4413
4494
  __publicField(_CameraController, "localVideoTrackID", "video0");
4414
4495
  var CameraController = _CameraController;
4415
4496
 
4416
- // src/Service/Realtime/RealtimeReferenceVideo.ts
4417
- var RealtimeVideoSampleMethod = /* @__PURE__ */ ((RealtimeVideoSampleMethod2) => {
4418
- RealtimeVideoSampleMethod2["time"] = "time";
4419
- RealtimeVideoSampleMethod2["fps"] = "fps";
4420
- return RealtimeVideoSampleMethod2;
4421
- })(RealtimeVideoSampleMethod || {});
4422
-
4423
- // src/Media/Video/NetworkVideoController.ts
4424
- var NetworkVideoController = class {
4425
- constructor() {
4426
- /**
4427
- * 当前视频源与完成通知
4428
- */
4429
- __publicField(this, "currentTrack");
4430
- __publicField(this, "reference");
4431
- __publicField(this, "onFinish");
4432
- /**
4433
- * 预览进度与视图资源
4434
- */
4435
- __publicField(this, "position", 0);
4436
- __publicField(this, "ended", false);
4437
- __publicField(this, "bindings", /* @__PURE__ */ new Map());
4438
- }
4439
- /**
4440
- * 创建网络视频源,校验地址、采样方式和模型支持的尺寸;不请求视频下载。
4441
- */
4442
- create(options, mediaService) {
4443
- let url;
4444
- try {
4445
- url = new URL(options.url);
4446
- } catch {
4447
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an absolute HTTP or HTTPS URL");
4448
- }
4449
- if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password) {
4450
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an HTTP or HTTPS URL without embedded credentials");
4451
- }
4452
- const sampleMethod = options.sampleMethod ?? "time" /* time */;
4453
- if (!Object.values(RealtimeVideoSampleMethod).includes(sampleMethod)) {
4454
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported video sample method");
4455
- }
4456
- if (this.currentTrack) {
4457
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current network video stream before creating another one");
4458
- }
4459
- options.videoFormat.validate();
4460
- const size = mediaService.resolveModelInputSize(options.videoFormat);
4461
- const track = new RealtimeVideoTrack({
4462
- id: "network-video",
4463
- videoFormat: options.videoFormat.resized(size.width, size.height)
4464
- });
4465
- this.currentTrack = track;
4466
- this.reference = Object.freeze({ path: options.url.trim(), sampleMethod });
4467
- this.onFinish = options.onFinish;
4468
- VideoRenderRegistry.register(track, {
4469
- attachHandler: (view) => this.attach(view),
4470
- detachHandler: (view) => this.detach(view)
4471
- });
4472
- return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
4473
- }
4474
- /**
4475
- * 释放全部预览绑定及源信息,不影响其他媒体源。
4476
- */
4477
- stop() {
4478
- for (const view of this.bindings.keys()) {
4479
- this.detach(view);
4480
- }
4481
- if (this.currentTrack) {
4482
- VideoRenderRegistry.unregister(this.currentTrack);
4483
- }
4484
- this.currentTrack = void 0;
4485
- this.reference = void 0;
4486
- this.onFinish = void 0;
4487
- this.position = 0;
4488
- this.ended = false;
4489
- }
4490
- /**
4491
- * 绑定播放器,恢复预览位置;本地播放错误仅记录,不中断服务端生成。
4492
- */
4493
- attach(view) {
4494
- if (!this.reference || !view.videoElement || !view.setVideoURL) {
4495
- throw new XmaxError("MEDIA_ERROR" /* mediaError */, "This render target does not support network video preview");
4496
- }
4497
- this.detach(view);
4498
- const video = view.videoElement;
4499
- video.autoplay = !this.ended;
4500
- const restore = () => {
4501
- if (this.position > 0) {
4502
- video.currentTime = Math.min(this.position, Math.max(0, video.duration - 1e-3) || this.position);
4503
- }
4504
- if (this.ended) {
4505
- video.pause();
4497
+ // src/Media/LocalMediaVideoFormat.ts
4498
+ function resolveLocalMediaVideoFormat(format, mediaService) {
4499
+ format.validate();
4500
+ const buckets = resolutionBuckets(mediaService.model);
4501
+ let target = buckets.find((size2) => size2.width === format.width && size2.height === format.height);
4502
+ if (!target && buckets.length > 0) {
4503
+ const ratio = format.width / format.height;
4504
+ let bestDistance = Number.POSITIVE_INFINITY;
4505
+ for (const bucket of buckets) {
4506
+ const distance = Math.abs(Math.log(bucket.width / bucket.height / ratio));
4507
+ const tied = Math.abs(distance - bestDistance) < 1e-9;
4508
+ if (distance < bestDistance - 1e-9 || tied && bucket.width >= bucket.height && target && target.width < target.height) {
4509
+ target = bucket;
4510
+ bestDistance = distance;
4506
4511
  }
4507
- };
4508
- const fail = () => {
4509
- XmaxLogger.render.warning(() => "\u7F51\u7EDC\u89C6\u9891\u9884\u89C8\u5931\u8D25 (Network Video Preview Failed)");
4510
- };
4511
- video.addEventListener("loadedmetadata", restore);
4512
- video.addEventListener("error", fail);
4513
- this.bindings.set(view, () => {
4514
- this.position = video.currentTime;
4515
- this.ended = video.ended || this.ended;
4516
- video.removeEventListener("loadedmetadata", restore);
4517
- video.removeEventListener("error", fail);
4518
- view.setVideoURL?.(null);
4519
- });
4520
- view.isMirrored = false;
4521
- view.setVideoURL(this.reference.path);
4522
- }
4523
- /**
4524
- * 保存预览进度并释放该视图的地址和事件监听。
4525
- */
4526
- detach(view) {
4527
- this.bindings.get(view)?.();
4528
- this.bindings.delete(view);
4512
+ }
4529
4513
  }
4530
- };
4514
+ const size = mediaService.resolveModelInputSize(target ?? format);
4515
+ return format.resized(size.width, size.height);
4516
+ }
4531
4517
 
4532
4518
  // src/Media/Video/LocalVideoController.ts
4533
4519
  var LocalVideoController = class {
@@ -4571,7 +4557,7 @@ var LocalVideoController = class {
4571
4557
  if (!(options.file instanceof Blob) || options.file.size === 0) {
4572
4558
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video requires a non-empty File or Blob");
4573
4559
  }
4574
- const format = this.resolveVideoFormat(options.videoFormat, mediaService);
4560
+ const format = resolveLocalMediaVideoFormat(options.videoFormat, mediaService);
4575
4561
  if (signal.aborted) {
4576
4562
  throw this.cancelled();
4577
4563
  }
@@ -4798,29 +4784,6 @@ var LocalVideoController = class {
4798
4784
  context.fillRect(0, 0, canvas.width, canvas.height);
4799
4785
  context.drawImage(video, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
4800
4786
  }
4801
- /**
4802
- * 仅为本地文件选择最接近宽高比的模型档位;比例距离相同时优先横屏。
4803
- * 无固定档位时保留公共尺寸解析规则,帧率和显式编码配置不变。
4804
- */
4805
- resolveVideoFormat(format, mediaService) {
4806
- format.validate();
4807
- const buckets = resolutionBuckets(mediaService.model);
4808
- let target = buckets.find((size2) => size2.width === format.width && size2.height === format.height);
4809
- if (!target && buckets.length > 0) {
4810
- const ratio = format.width / format.height;
4811
- let bestDistance = Number.POSITIVE_INFINITY;
4812
- for (const bucket of buckets) {
4813
- const distance = Math.abs(Math.log(bucket.width / bucket.height / ratio));
4814
- const tied = Math.abs(distance - bestDistance) < 1e-9;
4815
- if (distance < bestDistance - 1e-9 || tied && bucket.width >= bucket.height && target && target.width < target.height) {
4816
- target = bucket;
4817
- bestDistance = distance;
4818
- }
4819
- }
4820
- }
4821
- const size = mediaService.resolveModelInputSize(target ?? format);
4822
- return format.resized(size.width, size.height);
4823
- }
4824
4787
  /**
4825
4788
  * 解码或绘制失败时停止媒体推进,并报告一次运行期错误。
4826
4789
  */
@@ -4842,59 +4805,1271 @@ var LocalVideoController = class {
4842
4805
  }
4843
4806
  };
4844
4807
 
4845
- // src/Media/Video/ExternalVideoController.ts
4846
- var ExternalVideoController = class {
4847
- constructor(onError) {
4848
- __publicField(this, "onError", onError);
4849
- __publicField(this, "track");
4850
- __publicField(this, "onEnded", () => {
4851
- this.onError(new XmaxError("MEDIA_ERROR" /* mediaError */, "External video source ended"));
4852
- });
4853
- }
4854
- get currentTrack() {
4855
- return this.track;
4808
+ // src/Service/Realtime/RealtimeReferenceVideo.ts
4809
+ var RealtimeVideoSampleMethod = /* @__PURE__ */ ((RealtimeVideoSampleMethod2) => {
4810
+ RealtimeVideoSampleMethod2["time"] = "time";
4811
+ RealtimeVideoSampleMethod2["fps"] = "fps";
4812
+ return RealtimeVideoSampleMethod2;
4813
+ })(RealtimeVideoSampleMethod || {});
4814
+
4815
+ // src/Media/Video/NetworkVideoController.ts
4816
+ var NetworkVideoController = class {
4817
+ constructor() {
4818
+ /**
4819
+ * 当前视频源与完成通知
4820
+ */
4821
+ __publicField(this, "currentTrack");
4822
+ __publicField(this, "reference");
4823
+ __publicField(this, "onFinish");
4824
+ /**
4825
+ * 预览进度与视图资源
4826
+ */
4827
+ __publicField(this, "position", 0);
4828
+ __publicField(this, "ended", false);
4829
+ __publicField(this, "bindings", /* @__PURE__ */ new Map());
4856
4830
  }
4831
+ /**
4832
+ * 创建网络视频源,校验地址、采样方式和模型支持的尺寸;不请求视频下载。
4833
+ */
4857
4834
  create(options, mediaService) {
4858
- if (this.track) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current external video first");
4859
- const { videoTrack, videoFormat } = options;
4860
- if (!videoTrack || videoTrack.kind !== "video" || videoTrack.readyState !== "live" || typeof videoTrack.clone !== "function") {
4861
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External video requires a live video MediaStreamTrack");
4835
+ let url;
4836
+ try {
4837
+ url = new URL(options.url);
4838
+ } catch {
4839
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an absolute HTTP or HTTPS URL");
4862
4840
  }
4863
- videoFormat.validate();
4864
- const size = mediaService.resolveModelInputSize(videoFormat);
4865
- const format = videoFormat.resized(size.width, size.height);
4866
- const nativeTrack = videoTrack.clone();
4867
- if (nativeTrack.readyState !== "live") {
4868
- nativeTrack.stop();
4869
- throw new XmaxError("MEDIA_ERROR" /* mediaError */, "External video source ended during setup");
4841
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password) {
4842
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an HTTP or HTTPS URL without embedded credentials");
4870
4843
  }
4871
- const track = new RealtimeVideoTrack({ id: "external-video", videoFormat: format });
4872
- track.mediaStreamTrack = nativeTrack;
4873
- nativeTrack.addEventListener("ended", this.onEnded);
4874
- this.track = track;
4844
+ const sampleMethod = options.sampleMethod ?? "time" /* time */;
4845
+ if (!Object.values(RealtimeVideoSampleMethod).includes(sampleMethod)) {
4846
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported video sample method");
4847
+ }
4848
+ if (this.currentTrack) {
4849
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current network video stream before creating another one");
4850
+ }
4851
+ options.videoFormat.validate();
4852
+ const size = mediaService.resolveModelInputSize(options.videoFormat);
4853
+ const track = new RealtimeVideoTrack({
4854
+ id: "network-video",
4855
+ videoFormat: options.videoFormat.resized(size.width, size.height)
4856
+ });
4857
+ this.currentTrack = track;
4858
+ this.reference = Object.freeze({ path: options.url.trim(), sampleMethod });
4859
+ this.onFinish = options.onFinish;
4860
+ VideoRenderRegistry.register(track, {
4861
+ attachHandler: (view) => this.attach(view),
4862
+ detachHandler: (view) => this.detach(view)
4863
+ });
4875
4864
  return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
4876
4865
  }
4866
+ /**
4867
+ * 释放全部预览绑定及源信息,不影响其他媒体源。
4868
+ */
4877
4869
  stop() {
4878
- const nativeTrack = this.track?.mediaStreamTrack;
4879
- this.track = void 0;
4880
- nativeTrack?.removeEventListener("ended", this.onEnded);
4881
- nativeTrack?.stop();
4870
+ for (const view of this.bindings.keys()) {
4871
+ this.detach(view);
4872
+ }
4873
+ if (this.currentTrack) {
4874
+ VideoRenderRegistry.unregister(this.currentTrack);
4875
+ }
4876
+ this.currentTrack = void 0;
4877
+ this.reference = void 0;
4878
+ this.onFinish = void 0;
4879
+ this.position = 0;
4880
+ this.ended = false;
4882
4881
  }
4883
- };
4884
-
4885
- // src/Service/Realtime/RealtimeContext.ts
4886
- var RealtimeContext = class {
4887
4882
  /**
4888
- * 创建实时生成条件并规范化文本和参考路径。
4883
+ * 绑定播放器,恢复预览位置;本地播放错误仅记录,不中断服务端生成。
4889
4884
  */
4890
- constructor(init) {
4891
- /**
4892
- * 服务端直接读取的网络视频。
4893
- */
4894
- __publicField(this, "referenceVideo");
4895
- /**
4896
- * 生成条件
4897
- */
4885
+ attach(view) {
4886
+ if (!this.reference || !view.videoElement || !view.setVideoURL) {
4887
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "This render target does not support network video preview");
4888
+ }
4889
+ this.detach(view);
4890
+ const video = view.videoElement;
4891
+ video.autoplay = !this.ended;
4892
+ const restore = () => {
4893
+ if (this.position > 0) {
4894
+ video.currentTime = Math.min(this.position, Math.max(0, video.duration - 1e-3) || this.position);
4895
+ }
4896
+ if (this.ended) {
4897
+ video.pause();
4898
+ }
4899
+ };
4900
+ const fail = () => {
4901
+ XmaxLogger.render.warning(() => "\u7F51\u7EDC\u89C6\u9891\u9884\u89C8\u5931\u8D25 (Network Video Preview Failed)");
4902
+ };
4903
+ video.addEventListener("loadedmetadata", restore);
4904
+ video.addEventListener("error", fail);
4905
+ this.bindings.set(view, () => {
4906
+ this.position = video.currentTime;
4907
+ this.ended = video.ended || this.ended;
4908
+ video.removeEventListener("loadedmetadata", restore);
4909
+ video.removeEventListener("error", fail);
4910
+ view.setVideoURL?.(null);
4911
+ });
4912
+ view.isMirrored = false;
4913
+ view.setVideoURL(this.reference.path);
4914
+ }
4915
+ /**
4916
+ * 保存预览进度并释放该视图的地址和事件监听。
4917
+ */
4918
+ detach(view) {
4919
+ this.bindings.get(view)?.();
4920
+ this.bindings.delete(view);
4921
+ }
4922
+ };
4923
+
4924
+ // src/Media/Video/ExternalVideoController.ts
4925
+ var ExternalVideoController = class {
4926
+ constructor(onError) {
4927
+ __publicField(this, "onError", onError);
4928
+ __publicField(this, "track");
4929
+ __publicField(this, "onEnded", () => {
4930
+ this.onError(new XmaxError("MEDIA_ERROR" /* mediaError */, "External video source ended"));
4931
+ });
4932
+ }
4933
+ get currentTrack() {
4934
+ return this.track;
4935
+ }
4936
+ create(options, mediaService) {
4937
+ if (this.track) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current external video first");
4938
+ const { videoTrack, videoFormat } = options;
4939
+ if (!videoTrack || videoTrack.kind !== "video" || videoTrack.readyState !== "live" || typeof videoTrack.clone !== "function") {
4940
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External video requires a live video MediaStreamTrack");
4941
+ }
4942
+ videoFormat.validate();
4943
+ const size = mediaService.resolveModelInputSize(videoFormat);
4944
+ const format = videoFormat.resized(size.width, size.height);
4945
+ const nativeTrack = videoTrack.clone();
4946
+ if (nativeTrack.readyState !== "live") {
4947
+ nativeTrack.stop();
4948
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "External video source ended during setup");
4949
+ }
4950
+ const track = new RealtimeVideoTrack({ id: "external-video", videoFormat: format });
4951
+ track.mediaStreamTrack = nativeTrack;
4952
+ nativeTrack.addEventListener("ended", this.onEnded);
4953
+ this.track = track;
4954
+ return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
4955
+ }
4956
+ stop() {
4957
+ const nativeTrack = this.track?.mediaStreamTrack;
4958
+ this.track = void 0;
4959
+ nativeTrack?.removeEventListener("ended", this.onEnded);
4960
+ nativeTrack?.stop();
4961
+ }
4962
+ };
4963
+
4964
+ // src/Media/Image/LocalImageController.ts
4965
+ var LocalImageController = class {
4966
+ /**
4967
+ * 注入运行期错误通知,不创建浏览器资源。
4968
+ */
4969
+ constructor(onError) {
4970
+ __publicField(this, "onError", onError);
4971
+ /**
4972
+ * 当前图片源,包含尚未完成的解码操作。
4973
+ */
4974
+ __publicField(this, "source");
4975
+ }
4976
+ /**
4977
+ * 是否持有正在准备或可用的图片源。
4978
+ */
4979
+ get isActive() {
4980
+ return this.source !== void 0;
4981
+ }
4982
+ /**
4983
+ * 已准备的本地图片视频轨。
4984
+ */
4985
+ get currentTrack() {
4986
+ return this.source?.track;
4987
+ }
4988
+ /**
4989
+ * 解码图片、准备静止画面并持续输出帧;取消或失败时释放本次资源。
4990
+ */
4991
+ async create(options, mediaService, signal) {
4992
+ if (this.source) {
4993
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local image before creating another one");
4994
+ }
4995
+ if (!(options.file instanceof Blob) || options.file.size === 0) {
4996
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local image requires a non-empty File or Blob");
4997
+ }
4998
+ const format = resolveLocalMediaVideoFormat(options.videoFormat, mediaService);
4999
+ if (signal.aborted) {
5000
+ throw this.cancelled();
5001
+ }
5002
+ if (typeof document === "undefined") {
5003
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Local image requires a browser");
5004
+ }
5005
+ const image = document.createElement("img");
5006
+ const canvas = document.createElement("canvas");
5007
+ const context = canvas.getContext("2d");
5008
+ if (!context || typeof canvas.captureStream !== "function") {
5009
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas video capture is unavailable");
5010
+ }
5011
+ canvas.width = format.width;
5012
+ canvas.height = format.height;
5013
+ const source = {
5014
+ image,
5015
+ canvas,
5016
+ url: URL.createObjectURL(options.file),
5017
+ abort: new AbortController(),
5018
+ targets: /* @__PURE__ */ new Set()
5019
+ };
5020
+ this.source = source;
5021
+ try {
5022
+ await this.load(source, signal);
5023
+ if (signal.aborted || this.source !== source) {
5024
+ throw this.cancelled();
5025
+ }
5026
+ if (!image.naturalWidth || !image.naturalHeight) {
5027
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "The image has no valid dimensions");
5028
+ }
5029
+ const scale = Math.min(canvas.width / image.naturalWidth, canvas.height / image.naturalHeight);
5030
+ const width = image.naturalWidth * scale;
5031
+ const height = image.naturalHeight * scale;
5032
+ context.fillStyle = "black";
5033
+ context.fillRect(0, 0, canvas.width, canvas.height);
5034
+ context.drawImage(image, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
5035
+ source.stream = canvas.captureStream(format.fps);
5036
+ const nativeTrack = source.stream.getVideoTracks()[0];
5037
+ if (!nativeTrack || nativeTrack.readyState !== "live") {
5038
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas did not provide a live video track");
5039
+ }
5040
+ const track = new RealtimeVideoTrack({ id: "local-image", videoFormat: format });
5041
+ track.mediaStreamTrack = nativeTrack;
5042
+ source.track = track;
5043
+ const refresh = () => {
5044
+ context.drawImage(canvas, 0, 0);
5045
+ if (typeof nativeTrack.requestFrame === "function") {
5046
+ nativeTrack.requestFrame();
5047
+ }
5048
+ };
5049
+ refresh();
5050
+ source.timer = setInterval(() => {
5051
+ if (this.source !== source) {
5052
+ return;
5053
+ }
5054
+ try {
5055
+ refresh();
5056
+ } catch {
5057
+ this.handleError(source);
5058
+ }
5059
+ }, 1e3 / format.fps);
5060
+ source.onEnded = () => this.handleError(source);
5061
+ nativeTrack.addEventListener("ended", source.onEnded);
5062
+ VideoRenderRegistry.register(track, {
5063
+ attachHandler: (view) => {
5064
+ if (this.source !== source) {
5065
+ return;
5066
+ }
5067
+ view.isMirrored = false;
5068
+ view.setMediaStream(source.stream);
5069
+ source.targets.add(view);
5070
+ },
5071
+ detachHandler: (view) => {
5072
+ source.targets.delete(view);
5073
+ view.setMediaStream(null);
5074
+ }
5075
+ });
5076
+ return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
5077
+ } catch (error) {
5078
+ if (this.source === source) {
5079
+ this.stop();
5080
+ }
5081
+ throw XmaxError.from(error);
5082
+ }
5083
+ }
5084
+ /**
5085
+ * 取消解码、清空预览并释放视频轨、定时器和对象 URL。
5086
+ */
5087
+ stop() {
5088
+ const source = this.source;
5089
+ this.source = void 0;
5090
+ if (!source) {
5091
+ return;
5092
+ }
5093
+ source.abort.abort();
5094
+ clearInterval(source.timer);
5095
+ if (source.track) {
5096
+ VideoRenderRegistry.unregister(source.track);
5097
+ if (source.onEnded) {
5098
+ source.track.mediaStreamTrack?.removeEventListener("ended", source.onEnded);
5099
+ }
5100
+ source.track.mediaStreamTrack = void 0;
5101
+ }
5102
+ for (const view of source.targets) {
5103
+ try {
5104
+ view.setMediaStream(null);
5105
+ } catch {
5106
+ }
5107
+ }
5108
+ source.targets.clear();
5109
+ source.stream?.getTracks().forEach((track) => track.stop());
5110
+ source.image.removeAttribute("src");
5111
+ URL.revokeObjectURL(source.url);
5112
+ source.canvas.width = 0;
5113
+ source.canvas.height = 0;
5114
+ }
5115
+ /**
5116
+ * 等待浏览器完成图片解码,支持主动停止、操作取消和超时。
5117
+ */
5118
+ load(source, signal) {
5119
+ return new Promise((resolve, reject) => {
5120
+ const finish = (error) => {
5121
+ clearTimeout(timer);
5122
+ source.image.removeEventListener("load", loaded);
5123
+ source.image.removeEventListener("error", failed);
5124
+ signal.removeEventListener("abort", aborted);
5125
+ source.abort.signal.removeEventListener("abort", aborted);
5126
+ if (error) {
5127
+ reject(error);
5128
+ } else {
5129
+ resolve();
5130
+ }
5131
+ };
5132
+ const loaded = () => finish();
5133
+ const failed = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "The browser cannot decode this image file"));
5134
+ const aborted = () => finish(this.cancelled());
5135
+ const timer = setTimeout(() => finish(new XmaxError("TIMEOUT" /* timeout */, "Local image preparation timed out")), 1e4);
5136
+ source.image.addEventListener("load", loaded, { once: true });
5137
+ source.image.addEventListener("error", failed, { once: true });
5138
+ signal.addEventListener("abort", aborted, { once: true });
5139
+ source.abort.signal.addEventListener("abort", aborted, { once: true });
5140
+ if (signal.aborted || source.abort.signal.aborted) {
5141
+ aborted();
5142
+ } else {
5143
+ source.image.src = source.url;
5144
+ }
5145
+ });
5146
+ }
5147
+ /**
5148
+ * 输出失效时停止刷新并仅报告一次,资源由业务生命周期统一释放。
5149
+ */
5150
+ handleError(source) {
5151
+ if (this.source !== source || source.timer === void 0) {
5152
+ return;
5153
+ }
5154
+ clearInterval(source.timer);
5155
+ source.timer = void 0;
5156
+ this.onError(new XmaxError("MEDIA_ERROR" /* mediaError */, "Local image video output failed"));
5157
+ }
5158
+ /**
5159
+ * 构造统一的图片操作取消错误。
5160
+ */
5161
+ cancelled() {
5162
+ return new XmaxError("CANCELLED" /* cancelled */, "Local image operation was cancelled");
5163
+ }
5164
+ };
5165
+
5166
+ // src/Foundation/Media/Video/VideoContentMode.ts
5167
+ var VideoContentMode = /* @__PURE__ */ ((VideoContentMode2) => {
5168
+ VideoContentMode2["fit"] = "fit";
5169
+ VideoContentMode2["fill"] = "fill";
5170
+ return VideoContentMode2;
5171
+ })(VideoContentMode || {});
5172
+
5173
+ // src/Service/Realtime/RealtimePoint.ts
5174
+ var RealtimePoint = class {
5175
+ /**
5176
+ * 创建模型输入像素坐标点。
5177
+ *
5178
+ * @param init.x 模型输入像素横坐标。
5179
+ * @param init.y 模型输入像素纵坐标。
5180
+ */
5181
+ constructor(init) {
5182
+ /**
5183
+ * 坐标
5184
+ */
5185
+ /**
5186
+ * 模型输入像素横坐标。
5187
+ */
5188
+ __publicField(this, "x");
5189
+ /**
5190
+ * 模型输入像素纵坐标。
5191
+ */
5192
+ __publicField(this, "y");
5193
+ this.x = init.x;
5194
+ this.y = init.y;
5195
+ }
5196
+ };
5197
+
5198
+ // src/Media/Interaction/InteractionCoordinateMapper.ts
5199
+ var InteractionCoordinateMapper = class {
5200
+ /**
5201
+ * 计算居中显示的视频矩形;fill 允许矩形超出视口,fit 留出黑边。
5202
+ */
5203
+ static displayedFrame(viewport, video, mode) {
5204
+ if (![viewport.width, viewport.height, video.width, video.height].every((value) => Number.isFinite(value) && value > 0)) {
5205
+ return void 0;
5206
+ }
5207
+ const scale = (mode === "fill" /* fill */ ? Math.max : Math.min)(viewport.width / video.width, viewport.height / video.height);
5208
+ const width = video.width * scale;
5209
+ const height = video.height * scale;
5210
+ return { x: (viewport.width - width) / 2, y: (viewport.height - height) / 2, width, height };
5211
+ }
5212
+ /**
5213
+ * 忽略黑边和无效点,撤销显示镜像后取整并限制在模型输入范围内。
5214
+ */
5215
+ static map(point, frame, video) {
5216
+ const rect = this.displayedFrame(frame.viewportSize, video, frame.contentMode);
5217
+ if (!rect || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
5218
+ return void 0;
5219
+ }
5220
+ if (frame.contentMode === "fit" /* fit */ && (point.x < rect.x || point.x > rect.x + rect.width || point.y < rect.y || point.y > rect.y + rect.height)) {
5221
+ return void 0;
5222
+ }
5223
+ const x = (point.x - rect.x) / rect.width;
5224
+ const y = (point.y - rect.y) / rect.height;
5225
+ return new RealtimePoint({
5226
+ x: Math.min(video.width - 1, Math.max(0, Math.round((frame.mirrored ? 1 - x : x) * video.width))),
5227
+ y: Math.min(video.height - 1, Math.max(0, Math.round(y * video.height)))
5228
+ });
5229
+ }
5230
+ };
5231
+
5232
+ // src/Media/Interaction/InteractionController.ts
5233
+ var InteractionController = class {
5234
+ /**
5235
+ * 注入发送能力,不持有房间或具体 RTC。
5236
+ */
5237
+ constructor(listener) {
5238
+ __publicField(this, "listener", listener);
5239
+ /**
5240
+ * 活动任务与待发送采样;上下文对象身份隔离任务切换。
5241
+ */
5242
+ __publicField(this, "active");
5243
+ __publicField(this, "pending");
5244
+ __publicField(this, "draining");
5245
+ }
5246
+ /**
5247
+ * 激活或更新任务输入格式,清除旧采样。
5248
+ */
5249
+ startInteraction(taskID, videoFormat) {
5250
+ this.stopInteraction();
5251
+ if (taskID) {
5252
+ this.active = { taskID, videoFormat };
5253
+ }
5254
+ }
5255
+ /**
5256
+ * 立即使交互上下文失效;已提交给 RTC 的消息无法撤回。
5257
+ */
5258
+ stopInteraction() {
5259
+ this.active = void 0;
5260
+ this.pending = void 0;
5261
+ this.draining = void 0;
5262
+ }
5263
+ /**
5264
+ * 同步转换数据,发送忙时用最新采样覆盖旧采样,避免弱网积压。
5265
+ */
5266
+ submitInteraction(frame) {
5267
+ const context = this.active;
5268
+ if (!context) {
5269
+ return;
5270
+ }
5271
+ const points = frame.points.flatMap((point) => {
5272
+ const mapped = InteractionCoordinateMapper.map(point, frame, context.videoFormat);
5273
+ return mapped ? [mapped] : [];
5274
+ });
5275
+ if (!points.length) {
5276
+ return;
5277
+ }
5278
+ this.pending = { context, points };
5279
+ if (this.draining !== context) {
5280
+ void this.drain(context);
5281
+ }
5282
+ }
5283
+ /**
5284
+ * 顺序发送;失败只丢弃当前帧,不终止生成或阻止后续交互。
5285
+ */
5286
+ async drain(context) {
5287
+ this.draining = context;
5288
+ try {
5289
+ while (this.active === context && this.pending?.context === context) {
5290
+ const { points } = this.pending;
5291
+ this.pending = void 0;
5292
+ try {
5293
+ await this.listener(context.taskID, points);
5294
+ } catch {
5295
+ if (context === this.active) {
5296
+ XmaxLogger.interaction.warning(() => "\u53D1\u9001\u4EA4\u4E92\u8F68\u8FF9\u5931\u8D25\uFF0C\u5DF2\u4E22\u5F03\u5F53\u524D\u91C7\u6837\u5E27 (Interaction Sample Dropped)");
5297
+ }
5298
+ }
5299
+ }
5300
+ } finally {
5301
+ if (this.draining === context) {
5302
+ this.draining = void 0;
5303
+ }
5304
+ }
5305
+ }
5306
+ };
5307
+
5308
+ // src/Media/MediaController.ts
5309
+ var MediaController = class {
5310
+ /**
5311
+ * 注入基础能力与错误、交互输出,不自行创建跨层业务组件。
5312
+ */
5313
+ constructor(dependencies) {
5314
+ __publicField(this, "dependencies", dependencies);
5315
+ /**
5316
+ * 本地媒体与交互组件
5317
+ */
5318
+ __publicField(this, "camera");
5319
+ __publicField(this, "video");
5320
+ __publicField(this, "image");
5321
+ __publicField(this, "network", new NetworkVideoController());
5322
+ __publicField(this, "external");
5323
+ __publicField(this, "interaction");
5324
+ this.camera = dependencies.cameraController ?? new CameraController({
5325
+ rtcManager: dependencies.rtcManager,
5326
+ mediaService: dependencies.mediaService,
5327
+ errorListener: dependencies.onCameraError
5328
+ });
5329
+ this.video = new LocalVideoController(dependencies.onVideoError);
5330
+ this.image = new LocalImageController(dependencies.onImageError);
5331
+ this.external = new ExternalVideoController(dependencies.onExternalError);
5332
+ this.interaction = new InteractionController(dependencies.interactionListener);
5333
+ }
5334
+ /**
5335
+ * 当前可发布的轨道。
5336
+ */
5337
+ get uplinkTrack() {
5338
+ return this.external.currentTrack ?? this.video.currentTrack ?? this.image.currentTrack ?? this.camera.currentTrack;
5339
+ }
5340
+ /**
5341
+ * 当前本地源轨道,包含无上行的网络视频。
5342
+ */
5343
+ get currentTrack() {
5344
+ return this.uplinkTrack ?? this.network.currentTrack;
5345
+ }
5346
+ /**
5347
+ * 从实际资源推导来源,不维护另一份媒体状态。
5348
+ */
5349
+ get source() {
5350
+ if (this.external.currentTrack) {
5351
+ return "externalVideo";
5352
+ }
5353
+ if (this.video.isActive) {
5354
+ return "localVideo";
5355
+ }
5356
+ if (this.image.isActive) {
5357
+ return "localImage";
5358
+ }
5359
+ if (this.network.currentTrack) {
5360
+ return "networkVideo";
5361
+ }
5362
+ if (this.camera.currentTrack) {
5363
+ return "camera";
5364
+ }
5365
+ return void 0;
5366
+ }
5367
+ /**
5368
+ * 当前源需要发布的音频能力。
5369
+ */
5370
+ get hasAudio() {
5371
+ return this.video.isActive ? !!this.video.audioTrack : this.source === "camera" && this.camera.useMicrophone;
5372
+ }
5373
+ /**
5374
+ * 非相机源不等待相机预热。
5375
+ */
5376
+ get isFrameValidationEnabled() {
5377
+ return this.source === "camera" && this.camera.isFrameValidationEnabled;
5378
+ }
5379
+ /**
5380
+ * 网络视频的服务端读取参数。
5381
+ */
5382
+ get referenceVideo() {
5383
+ return this.network.reference;
5384
+ }
5385
+ /**
5386
+ * 网络视频完成回调。
5387
+ */
5388
+ get onNetworkVideoFinish() {
5389
+ return this.network.onFinish;
5390
+ }
5391
+ /**
5392
+ * 创建前拒绝覆盖活动源或尚未完成的文件准备。
5393
+ */
5394
+ ensureNoLocalSource() {
5395
+ if (this.currentTrack || this.video.isActive || this.image.isActive) {
5396
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local stream before creating another one");
5397
+ }
5398
+ }
5399
+ /**
5400
+ * 保持现有外部轨道能力校验与错误语义。
5401
+ */
5402
+ validateExternalMediaSupport(kind) {
5403
+ if (!this.dependencies.rtcManager.setExternalMediaTracks) {
5404
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, `The RTC provider does not support ${kind} video streams`);
5405
+ }
5406
+ }
5407
+ /**
5408
+ * 创建相机流,由相机组件管理 RTC 和预览。
5409
+ */
5410
+ createLocalCameraStream(options) {
5411
+ this.ensureNoLocalSource();
5412
+ return this.camera.createLocalCameraStream(options);
5413
+ }
5414
+ /**
5415
+ * 同步准备网络视频源;地址校验失败不初始化 RTC。
5416
+ */
5417
+ createNetworkVideoStream(options) {
5418
+ this.ensureNoLocalSource();
5419
+ return this.network.create(options, this.dependencies.mediaService);
5420
+ }
5421
+ /**
5422
+ * 先解锁用户触发的播放,再由 prepareSource 初始化 RTC。
5423
+ */
5424
+ createLocalVideoStream(options, signal) {
5425
+ this.ensureNoLocalSource();
5426
+ return this.video.create(options, this.dependencies.mediaService, signal);
5427
+ }
5428
+ /**
5429
+ * 解码本地图片并准备静止视频轨,不申请设备权限。
5430
+ */
5431
+ createLocalImageStream(options, signal) {
5432
+ this.ensureNoLocalSource();
5433
+ return this.image.create(options, this.dependencies.mediaService, signal);
5434
+ }
5435
+ /**
5436
+ * 接管外部视频轨副本,不停止调用方原始轨道。
5437
+ */
5438
+ createExternalVideoStream(options) {
5439
+ this.ensureNoLocalSource();
5440
+ return this.external.create(options, this.dependencies.mediaService);
5441
+ }
5442
+ /**
5443
+ * 初始化 RTC 并绑定非相机轨道;取消后不继续安装轨道。
5444
+ */
5445
+ async prepareSource(ensureCurrent) {
5446
+ const rtc = this.dependencies.rtcManager;
5447
+ await rtc.initialize();
5448
+ ensureCurrent();
5449
+ const track = this.external.currentTrack ?? this.video.currentTrack ?? this.image.currentTrack;
5450
+ if (track?.mediaStreamTrack) {
5451
+ await rtc.setExternalMediaTracks({ videoTrack: track.mediaStreamTrack, audioTrack: this.video.audioTrack });
5452
+ ensureCurrent();
5453
+ }
5454
+ }
5455
+ /**
5456
+ * 设置相机预览就绪通知。
5457
+ */
5458
+ setCameraPreviewReadyHandler(handler) {
5459
+ this.camera.setPreviewReadyHandler(handler);
5460
+ }
5461
+ /**
5462
+ * 等待相机预热。
5463
+ */
5464
+ waitForValidCameraFrame(signal) {
5465
+ return this.camera.waitForValidCameraFrame(signal);
5466
+ }
5467
+ /**
5468
+ * 切换相机;外部采集不由 SDK 控制。
5469
+ */
5470
+ switchCamera() {
5471
+ if (this.external.currentTrack) {
5472
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External video capture is managed by the caller");
5473
+ }
5474
+ return this.camera.switchCamera();
5475
+ }
5476
+ /**
5477
+ * 停止相机资源。
5478
+ */
5479
+ stopLocalCameraStream() {
5480
+ return this.camera.stopLocalCameraStream();
5481
+ }
5482
+ /**
5483
+ * 生成开始后启动文件播放。
5484
+ */
5485
+ startLocalVideo(signal) {
5486
+ return this.video.start(signal);
5487
+ }
5488
+ /**
5489
+ * 断开时保留文件预览资源但暂停时间线。
5490
+ */
5491
+ pauseLocalVideo() {
5492
+ this.video.pause();
5493
+ }
5494
+ /**
5495
+ * 释放当前源,RTC 销毁失败也释放外部轨和解码器。
5496
+ */
5497
+ async stopLocalStream() {
5498
+ this.stopInteraction();
5499
+ const external = this.source !== void 0 && this.source !== "camera";
5500
+ this.network.stop();
5501
+ try {
5502
+ if (external) {
5503
+ await this.dependencies.rtcManager.destroy();
5504
+ } else {
5505
+ await this.camera.stopLocalCameraStream();
5506
+ }
5507
+ } finally {
5508
+ this.external.stop();
5509
+ if (this.image.isActive) {
5510
+ this.image.stop();
5511
+ }
5512
+ if (this.video.isActive) {
5513
+ await this.video.stop();
5514
+ }
5515
+ }
5516
+ }
5517
+ /**
5518
+ * 使用生成格式启用交互,不读取弱网调档后的上行规格。
5519
+ */
5520
+ startInteraction(taskID, format) {
5521
+ this.interaction.startInteraction(taskID, format);
5522
+ }
5523
+ /**
5524
+ * 结束任务时立即清除待发采样。
5525
+ */
5526
+ stopInteraction() {
5527
+ this.interaction.stopInteraction();
5528
+ }
5529
+ /**
5530
+ * 接收渲染层快照并交给坐标与发送队列处理。
5531
+ */
5532
+ submitInteraction(frame) {
5533
+ this.interaction.submitInteraction(frame);
5534
+ }
5535
+ };
5536
+
5537
+ // src/Service/Realtime/RealtimeRecordingEvent.ts
5538
+ var RealtimeRecordingEventType = /* @__PURE__ */ ((RealtimeRecordingEventType2) => {
5539
+ RealtimeRecordingEventType2["completed"] = "completed";
5540
+ RealtimeRecordingEventType2["interrupted"] = "interrupted";
5541
+ RealtimeRecordingEventType2["failed"] = "failed";
5542
+ return RealtimeRecordingEventType2;
5543
+ })(RealtimeRecordingEventType || {});
5544
+ var RealtimeRecordingFinishReason = /* @__PURE__ */ ((RealtimeRecordingFinishReason2) => {
5545
+ RealtimeRecordingFinishReason2["durationLimit"] = "durationLimit";
5546
+ RealtimeRecordingFinishReason2["generationEnded"] = "generationEnded";
5547
+ RealtimeRecordingFinishReason2["restarted"] = "restarted";
5548
+ RealtimeRecordingFinishReason2["mediaInterrupted"] = "mediaInterrupted";
5549
+ return RealtimeRecordingFinishReason2;
5550
+ })(RealtimeRecordingFinishReason || {});
5551
+
5552
+ // src/Media/Recording/RecordingController.ts
5553
+ var _RecordingController = class _RecordingController {
5554
+ constructor() {
5555
+ /**
5556
+ * 任务资源与结果监听
5557
+ */
5558
+ __publicField(this, "session");
5559
+ __publicField(this, "listener");
5560
+ }
5561
+ /**
5562
+ * 设置统一结果监听器;传入 undefined 清除,不停止正在进行的录制。
5563
+ */
5564
+ setListener(listener) {
5565
+ this.listener = listener;
5566
+ }
5567
+ /**
5568
+ * 开始准备当前任务的录制;准备和录制失败只通过独立事件通知。
5569
+ */
5570
+ start(taskID, readTracks) {
5571
+ if (this.session && !this.session.finished) {
5572
+ void this.stop("restarted" /* restarted */);
5573
+ }
5574
+ try {
5575
+ const tracks = readTracks();
5576
+ if (!tracks || tracks.videoTrack.readyState === "ended") {
5577
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Remote video is unavailable for recording");
5578
+ }
5579
+ if (typeof MediaRecorder === "undefined" || typeof AudioContext === "undefined") {
5580
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Audio/video recording is not supported by this browser");
5581
+ }
5582
+ let resolve;
5583
+ const completion = new Promise((done) => {
5584
+ resolve = done;
5585
+ });
5586
+ const session = {
5587
+ taskID,
5588
+ readTracks,
5589
+ videoTrack: tracks.videoTrack,
5590
+ chunks: [],
5591
+ bytes: 0,
5592
+ hasAudio: false,
5593
+ stopping: false,
5594
+ finished: false,
5595
+ reason: "durationLimit" /* durationLimit */,
5596
+ completion,
5597
+ resolve
5598
+ };
5599
+ this.session = session;
5600
+ session.timer = setTimeout(() => {
5601
+ this.fail(session, new XmaxError("TIMEOUT" /* timeout */, "Timed out preparing remote recording"));
5602
+ }, _RecordingController.preparationTimeoutMs);
5603
+ void this.prepare(session).catch((error) => this.fail(session, XmaxError.from(error)));
5604
+ } catch (error) {
5605
+ this.reportFailure(taskID, XmaxError.from(error));
5606
+ }
5607
+ }
5608
+ /**
5609
+ * 请求收尾并等待最终数据;重复停止共享完成结果,且绝不停止 RTC 原始轨道。
5610
+ */
5611
+ stop(reason) {
5612
+ const session = this.session;
5613
+ if (!session) {
5614
+ return Promise.resolve();
5615
+ }
5616
+ this.stopSession(session, reason);
5617
+ return session.completion;
5618
+ }
5619
+ /**
5620
+ * 不支持的录制请求使用同一错误出口,不改变生成生命周期。
5621
+ */
5622
+ reportFailure(taskID, error) {
5623
+ this.emit({ type: "failed" /* failed */, taskID, error });
5624
+ }
5625
+ /**
5626
+ * 使用固定音频输出轨处理晚到的音频;不在 MediaRecorder 运行中增删轨道。
5627
+ */
5628
+ async prepare(session) {
5629
+ const context = new AudioContext();
5630
+ session.audioContext = context;
5631
+ session.audioDestination = context.createMediaStreamDestination();
5632
+ const silence = context.createConstantSource();
5633
+ silence.offset.value = 0;
5634
+ silence.connect(session.audioDestination);
5635
+ silence.start();
5636
+ session.silenceSource = silence;
5637
+ await context.resume();
5638
+ if (session.stopping || session.finished) {
5639
+ return;
5640
+ }
5641
+ if (context.state !== "running") {
5642
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Browser did not allow recording audio to start");
5643
+ }
5644
+ this.updateAudio(session);
5645
+ const video = document.createElement("video");
5646
+ session.video = video;
5647
+ video.muted = true;
5648
+ video.playsInline = true;
5649
+ video.srcObject = new MediaStream([session.videoTrack]);
5650
+ const ready = () => {
5651
+ if (!session.stopping && !session.finished && video.readyState >= 2) {
5652
+ this.beginRecording(session);
5653
+ }
5654
+ };
5655
+ if (typeof video.requestVideoFrameCallback === "function") {
5656
+ session.frameCallback = video.requestVideoFrameCallback(ready);
5657
+ } else {
5658
+ video.onloadeddata = ready;
5659
+ video.onplaying = ready;
5660
+ }
5661
+ video.onerror = () => this.fail(session, new XmaxError("MEDIA_ERROR" /* mediaError */, "Cannot read remote recording video"));
5662
+ try {
5663
+ await video.play();
5664
+ } catch (error) {
5665
+ if (!session.recorder && !session.stopping && !session.finished) {
5666
+ throw error;
5667
+ }
5668
+ }
5669
+ if (typeof video.requestVideoFrameCallback !== "function") {
5670
+ ready();
5671
+ }
5672
+ }
5673
+ /**
5674
+ * 首帧可用后启动单个音视频录制器,计时不包含连接和媒体准备阶段。
5675
+ */
5676
+ beginRecording(session) {
5677
+ if (session.recorder || session.stopping || session.finished) {
5678
+ return;
5679
+ }
5680
+ try {
5681
+ session.hasAudio = false;
5682
+ this.updateAudio(session);
5683
+ const stream = new MediaStream([session.videoTrack, ...session.audioDestination.stream.getAudioTracks()]);
5684
+ const mimeType = ["video/mp4;codecs=avc1,mp4a.40.2", "video/webm;codecs=vp8,opus", "video/mp4", "video/webm"].find((type) => MediaRecorder.isTypeSupported(type));
5685
+ if (!mimeType) {
5686
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "No supported audio/video recording format");
5687
+ }
5688
+ const recorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 6e6, audioBitsPerSecond: 128e3 });
5689
+ session.recorder = recorder;
5690
+ recorder.ondataavailable = (event) => {
5691
+ if (session.finished || session.error || !event.data.size) {
5692
+ return;
5693
+ }
5694
+ if (session.bytes + event.data.size > _RecordingController.maximumBytes) {
5695
+ this.fail(session, new XmaxError("MEDIA_ERROR" /* mediaError */, "Recording exceeded the memory limit"));
5696
+ return;
5697
+ }
5698
+ session.chunks.push(event.data);
5699
+ session.bytes += event.data.size;
5700
+ };
5701
+ recorder.onerror = () => {
5702
+ this.fail(session, new XmaxError("MEDIA_ERROR" /* mediaError */, "Remote recording encoder failed"));
5703
+ };
5704
+ recorder.onstop = () => {
5705
+ if (!session.stopping) {
5706
+ session.reason = "mediaInterrupted" /* mediaInterrupted */;
5707
+ }
5708
+ this.finish(session);
5709
+ };
5710
+ recorder.start(1e3);
5711
+ clearTimeout(session.timer);
5712
+ session.timer = setTimeout(() => this.stopSession(session, "durationLimit" /* durationLimit */), _RecordingController.durationMs);
5713
+ session.monitor = setInterval(() => {
5714
+ try {
5715
+ const tracks = session.readTracks();
5716
+ if (!tracks || tracks.videoTrack !== session.videoTrack || session.videoTrack.readyState === "ended") {
5717
+ this.stopSession(session, "mediaInterrupted" /* mediaInterrupted */);
5718
+ } else if (session.audioContext?.state !== "running") {
5719
+ this.fail(session, new XmaxError("MEDIA_ERROR" /* mediaError */, "Recording audio context was suspended"));
5720
+ } else {
5721
+ this.updateAudio(session);
5722
+ }
5723
+ } catch (error) {
5724
+ this.fail(session, XmaxError.from(error));
5725
+ }
5726
+ }, 100);
5727
+ this.releaseVideoProbe(session);
5728
+ } catch (error) {
5729
+ this.fail(session, XmaxError.from(error));
5730
+ }
5731
+ }
5732
+ /**
5733
+ * 轮询仅在录制期间运行;接入原始音频,不连接扬声器,不受播放音量影响。
5734
+ */
5735
+ updateAudio(session) {
5736
+ const track = session.readTracks()?.audioTrack;
5737
+ const liveTrack = track?.readyState === "live" ? track : void 0;
5738
+ if (liveTrack !== session.audioTrack) {
5739
+ session.audioSource?.disconnect();
5740
+ session.audioSource = void 0;
5741
+ session.audioTrack = liveTrack;
5742
+ if (liveTrack) {
5743
+ session.audioSource = session.audioContext.createMediaStreamSource(new MediaStream([liveTrack]));
5744
+ session.audioSource.connect(session.audioDestination);
5745
+ }
5746
+ }
5747
+ if (liveTrack && !liveTrack.muted && liveTrack.enabled) {
5748
+ session.hasAudio = true;
5749
+ }
5750
+ }
5751
+ /**
5752
+ * 标记失败并尽力停止编码;任何错误都不能冒泡到实时生成。
5753
+ */
5754
+ fail(session, error) {
5755
+ if (session.finished) {
5756
+ return;
5757
+ }
5758
+ session.error ?? (session.error = error);
5759
+ this.stopSession(session, "mediaInterrupted" /* mediaInterrupted */);
5760
+ }
5761
+ /**
5762
+ * 保留最后一个 dataavailable,等待 stop 后再封装;异常路径有收尾超时保护。
5763
+ */
5764
+ stopSession(session, reason) {
5765
+ if (session.stopping || session.finished) {
5766
+ return;
5767
+ }
5768
+ session.stopping = true;
5769
+ session.reason = reason;
5770
+ clearTimeout(session.timer);
5771
+ clearInterval(session.monitor);
5772
+ this.releaseVideoProbe(session);
5773
+ if (!session.recorder || session.recorder.state === "inactive") {
5774
+ this.finish(session);
5775
+ return;
5776
+ }
5777
+ session.finalizationTimer = setTimeout(() => {
5778
+ session.error ?? (session.error = new XmaxError("TIMEOUT" /* timeout */, "Timed out finalizing remote recording"));
5779
+ this.finish(session);
5780
+ }, _RecordingController.finalizationTimeoutMs);
5781
+ try {
5782
+ session.recorder.stop();
5783
+ } catch (error) {
5784
+ session.error ?? (session.error = XmaxError.from(error));
5785
+ this.finish(session);
5786
+ }
5787
+ }
5788
+ /**
5789
+ * 一次性交付结果并释放所有自有资源,不保留文件副本。
5790
+ */
5791
+ finish(session) {
5792
+ if (session.finished) {
5793
+ return;
5794
+ }
5795
+ session.finished = true;
5796
+ clearTimeout(session.timer);
5797
+ clearTimeout(session.finalizationTimer);
5798
+ clearInterval(session.monitor);
5799
+ this.releaseVideoProbe(session);
5800
+ const blob = new Blob(session.chunks, { type: session.recorder?.mimeType ?? session.chunks[0]?.type });
5801
+ session.chunks = [];
5802
+ session.audioSource?.disconnect();
5803
+ session.silenceSource?.stop();
5804
+ session.silenceSource?.disconnect();
5805
+ session.audioDestination?.stream.getTracks().forEach((track) => track.stop());
5806
+ if (session.audioContext && session.audioContext.state !== "closed") {
5807
+ void session.audioContext.close().catch(() => {
5808
+ });
5809
+ }
5810
+ if (session.recorder) {
5811
+ session.recorder.ondataavailable = null;
5812
+ session.recorder.onerror = null;
5813
+ session.recorder.onstop = null;
5814
+ }
5815
+ if (this.session === session) {
5816
+ this.session = void 0;
5817
+ }
5818
+ if (session.error || !blob.size) {
5819
+ this.reportFailure(session.taskID, session.error ?? new XmaxError("MEDIA_ERROR" /* mediaError */, "Recording ended without usable media"));
5820
+ } else {
5821
+ void this.deliverResult(session.taskID, blob, session.hasAudio, session.reason);
5822
+ }
5823
+ session.resolve();
5824
+ }
5825
+ /**
5826
+ * 按需加载后处理,成功后回调;处理失败仍交付原文件,不丢失已完成的录像。
5827
+ */
5828
+ async deliverResult(taskID, original, hasAudio, reason) {
5829
+ let blob = original;
5830
+ try {
5831
+ const { RecordingProcessor } = await import("./RecordingProcessor-ZSBJFF4J.js");
5832
+ blob = await new RecordingProcessor().process(original);
5833
+ } catch (error) {
5834
+ XmaxLogger.realtime.warning(() => `Recording gap removal failed; keeping original clip: ${XmaxError.from(error).message}`);
5835
+ }
5836
+ this.emit({
5837
+ type: reason === "durationLimit" /* durationLimit */ ? "completed" /* completed */ : "interrupted" /* interrupted */,
5838
+ taskID,
5839
+ result: { blob, hasAudio, reason }
5840
+ });
5841
+ }
5842
+ /**
5843
+ * 释放首帧探测元素;srcObject 原始轨道不调用 stop。
5844
+ */
5845
+ releaseVideoProbe(session) {
5846
+ const video = session.video;
5847
+ if (!video) {
5848
+ return;
5849
+ }
5850
+ if (session.frameCallback !== void 0) {
5851
+ video.cancelVideoFrameCallback(session.frameCallback);
5852
+ }
5853
+ video.onloadeddata = null;
5854
+ video.onplaying = null;
5855
+ video.onerror = null;
5856
+ video.pause();
5857
+ video.srcObject = null;
5858
+ session.video = void 0;
5859
+ }
5860
+ /**
5861
+ * 隔离接入方监听器异常,包括异步监听器的 Promise 拒绝。
5862
+ */
5863
+ emit(event) {
5864
+ try {
5865
+ void Promise.resolve(this.listener?.(event)).catch(() => {
5866
+ XmaxLogger.realtime.warning(() => "Recording listener rejected");
5867
+ });
5868
+ } catch {
5869
+ XmaxLogger.realtime.warning(() => "Recording listener threw an error");
5870
+ }
5871
+ }
5872
+ };
5873
+ /**
5874
+ * 录制资源上限
5875
+ */
5876
+ __publicField(_RecordingController, "durationMs", 2e4);
5877
+ __publicField(_RecordingController, "maximumBytes", 64 * 1024 * 1024);
5878
+ __publicField(_RecordingController, "preparationTimeoutMs", 1e4);
5879
+ __publicField(_RecordingController, "finalizationTimeoutMs", 5e3);
5880
+ var RecordingController = _RecordingController;
5881
+
5882
+ // src/Render/Trajectory/TrajectoryBinding.ts
5883
+ var TrajectoryBinding = class {
5884
+ /**
5885
+ * 注入采样回调及模型输入格式,不引用 Media Controller 实例。
5886
+ */
5887
+ constructor(listener, format) {
5888
+ __publicField(this, "listener", listener);
5889
+ __publicField(this, "format", format);
5890
+ /**
5891
+ * 绑定视图与可用性
5892
+ */
5893
+ __publicField(this, "overlay");
5894
+ __publicField(this, "enabled", false);
5895
+ __publicField(this, "valid", true);
5896
+ }
5897
+ /**
5898
+ * 一个远端绑定只由一个交互视图持有,迁移时取消旧视图的触点。
5899
+ */
5900
+ attach(overlay) {
5901
+ if (!this.valid) {
5902
+ return;
5903
+ }
5904
+ if (this.overlay !== overlay) {
5905
+ this.overlay?.setBinding(void 0);
5906
+ }
5907
+ this.overlay = overlay;
5908
+ overlay.setBinding(this, this.format, this.enabled);
5909
+ }
5910
+ /**
5911
+ * 只解除当前所有者,旧视图迟到的 detach 不影响新视图。
5912
+ */
5913
+ detach(overlay) {
5914
+ if (this.overlay === overlay) {
5915
+ this.overlay = void 0;
5916
+ overlay.setBinding(void 0);
5917
+ }
5918
+ }
5919
+ /**
5920
+ * 开始、更新或结束任务时同步交互能力,并取消旧手势。
5921
+ */
5922
+ update(enabled, format = this.format) {
5923
+ this.enabled = enabled;
5924
+ this.format = format;
5925
+ this.overlay?.setBinding(this, format, enabled);
5926
+ }
5927
+ /**
5928
+ * 转发当前视图采样;注销或停用后忽略。
5929
+ */
5930
+ submit(frame) {
5931
+ if (this.valid && this.enabled) {
5932
+ this.listener(frame);
5933
+ }
5934
+ }
5935
+ /**
5936
+ * 注销绑定并释放视图交互资源,不允许旧引用重新挂载。
5937
+ */
5938
+ invalidate() {
5939
+ this.valid = false;
5940
+ this.overlay?.setBinding(void 0);
5941
+ this.overlay = void 0;
5942
+ }
5943
+ };
5944
+
5945
+ // src/Render/Trajectory/TrajectoryRegistry.ts
5946
+ var TrajectoryRegistry = class {
5947
+ /**
5948
+ * 替换绑定前先使旧绑定失效。
5949
+ */
5950
+ static register(track, binding) {
5951
+ this.unregister(track);
5952
+ this.bindings.set(track, binding);
5953
+ }
5954
+ /**
5955
+ * 查询交互能力,本地轨道不注册该能力。
5956
+ */
5957
+ static binding(track) {
5958
+ return this.bindings.get(track);
5959
+ }
5960
+ /**
5961
+ * 主动清理视图资源,不能仅依赖 WeakMap 垃圾回收。
5962
+ */
5963
+ static unregister(track) {
5964
+ this.bindings.get(track)?.invalidate();
5965
+ this.bindings.delete(track);
5966
+ }
5967
+ };
5968
+ __publicField(TrajectoryRegistry, "bindings", /* @__PURE__ */ new WeakMap());
5969
+
5970
+ // src/Render/RenderController.ts
5971
+ var RenderController = class {
5972
+ /**
5973
+ * 注入显示生命周期通知,不持有 Core 对象。
5974
+ */
5975
+ constructor(callbacks = {}) {
5976
+ __publicField(this, "callbacks", callbacks);
5977
+ /**
5978
+ * 当前轨道与唯一渲染目标
5979
+ */
5980
+ __publicField(this, "track");
5981
+ __publicField(this, "target");
5982
+ __publicField(this, "interactionEnabled", false);
5983
+ }
5984
+ /**
5985
+ * 注册新轨道并使旧绑定失效;本地轨道不经过该入口。
5986
+ */
5987
+ registerRemoteTrack(track, listener) {
5988
+ this.resetRemoteTrack();
5989
+ this.track = track;
5990
+ if (track.videoFormat) {
5991
+ TrajectoryRegistry.register(track, new TrajectoryBinding(listener, track.videoFormat));
5992
+ }
5993
+ VideoRenderRegistry.register(track, {
5994
+ frameDisplayHandler: () => {
5995
+ if (this.track === track) {
5996
+ this.callbacks.onFrameDisplayed?.();
5997
+ }
5998
+ },
5999
+ attachHandler: (view) => {
6000
+ if (this.track !== track) {
6001
+ return;
6002
+ }
6003
+ this.target?.setFrameInterpolation?.(void 0);
6004
+ view.isMirrored = false;
6005
+ this.target = view;
6006
+ this.callbacks.onAttached?.();
6007
+ view.setMediaStream(track.mediaStreamTrack ? new MediaStream([track.mediaStreamTrack]) : null);
6008
+ },
6009
+ detachHandler: (view) => {
6010
+ if (this.target === view) {
6011
+ this.callbacks.onDetached?.();
6012
+ this.target = void 0;
6013
+ }
6014
+ view.setFrameInterpolation?.(void 0);
6015
+ view.setMediaStream(null);
6016
+ }
6017
+ });
6018
+ }
6019
+ /**
6020
+ * 更新媒体画面;暂停显示时同时禁用交互。
6021
+ */
6022
+ setRemoteMedia(track) {
6023
+ if (this.track) {
6024
+ TrajectoryRegistry.binding(this.track)?.update(!!track && this.interactionEnabled);
6025
+ }
6026
+ this.target?.setMediaStream(track ? new MediaStream([track]) : null);
6027
+ }
6028
+ /**
6029
+ * 转交插帧配置,不重建交互视图。
6030
+ */
6031
+ setFrameInterpolation(options) {
6032
+ this.target?.setFrameInterpolation?.(options);
6033
+ }
6034
+ /**
6035
+ * 更新交互绑定的输入格式和可用性。
6036
+ */
6037
+ setInteractionEnabled(enabled, format) {
6038
+ this.interactionEnabled = enabled;
6039
+ if (this.track) {
6040
+ TrajectoryRegistry.binding(this.track)?.update(enabled, format);
6041
+ }
6042
+ }
6043
+ /**
6044
+ * 先使能力失效再清空画面,迟到首帧和旧手势不能恢复已结束任务。
6045
+ */
6046
+ resetRemoteTrack() {
6047
+ const track = this.track;
6048
+ this.track = void 0;
6049
+ this.interactionEnabled = false;
6050
+ if (track) {
6051
+ TrajectoryRegistry.unregister(track);
6052
+ VideoRenderRegistry.unregister(track);
6053
+ }
6054
+ this.target?.setFrameInterpolation?.(void 0);
6055
+ this.target?.setMediaStream(null);
6056
+ this.target = void 0;
6057
+ }
6058
+ };
6059
+
6060
+ // src/Service/Realtime/RealtimeContext.ts
6061
+ var RealtimeContext = class {
6062
+ /**
6063
+ * 创建实时生成条件并规范化文本和参考路径。
6064
+ */
6065
+ constructor(init) {
6066
+ /**
6067
+ * 服务端直接读取的网络视频。
6068
+ */
6069
+ __publicField(this, "referenceVideo");
6070
+ /**
6071
+ * 生成条件
6072
+ */
4898
6073
  /**
4899
6074
  * 实时生成使用的文本条件。
4900
6075
  */
@@ -5425,6 +6600,16 @@ var RemoteStream = class {
5425
6600
  var RoomEvent = class _RoomEvent {
5426
6601
  /**
5427
6602
  * 生成开始信令。
6603
+ *
6604
+ * @param options.userID 业务用户标识。
6605
+ * @param options.taskID 当前生成任务标识。
6606
+ * @param options.videoFormat 模型生成使用的视频规格。
6607
+ * @param options.context 本次生成条件。
6608
+ * @param options.targetSize 回传尺寸,缺省时不发送;网络视频默认使用生成尺寸。
6609
+ * @param options.mirror 是否由后端镜像输入画面,默认 false。
6610
+ * @param options.faceRestoration 是否回贴原始人脸,默认 false。
6611
+ * @param options.extraParams 已校验的本次扩展参数,合并到 params。
6612
+ * @returns 序列化后的 start 房间消息。
5428
6613
  */
5429
6614
  static start(options) {
5430
6615
  return _RoomEvent.encode({
@@ -5448,6 +6633,15 @@ var RoomEvent = class _RoomEvent {
5448
6633
  }
5449
6634
  /**
5450
6635
  * 生成条件变更信令。
6636
+ *
6637
+ * @param options.userID 业务用户标识。
6638
+ * @param options.taskID 当前生成任务标识。
6639
+ * @param options.videoFormat 模型生成使用的视频规格。
6640
+ * @param options.context 本次生成条件。
6641
+ * @param options.targetSize 回传尺寸,缺省时不发送。
6642
+ * @param options.faceRestoration 是否回贴原始人脸,默认 false。
6643
+ * @param options.extraParams 已校验的本次扩展参数,合并到 params。
6644
+ * @returns 序列化后的 change_condition 房间消息。
5451
6645
  */
5452
6646
  static changeCondition(options) {
5453
6647
  return _RoomEvent.encode({
@@ -5504,7 +6698,10 @@ var RoomEvent = class _RoomEvent {
5504
6698
  });
5505
6699
  }
5506
6700
  /**
5507
- * 生成条件参数:模型、生成尺寸、回传尺寸、文本与参考图路径。
6701
+ * 组装生成条件参数,包含人脸回贴配置及本次扩展参数。
6702
+ *
6703
+ * @param options 本次生成条件类信令的参数。
6704
+ * @returns start 与 change_condition 共用的 params 字段。
5508
6705
  */
5509
6706
  static generationParameters(options) {
5510
6707
  return {
@@ -5516,7 +6713,8 @@ var RoomEvent = class _RoomEvent {
5516
6713
  Math.round(options.targetSize.height)
5517
6714
  ] : void 0,
5518
6715
  prompt: options.context.prompt,
5519
- ref_image_path: options.context.referencePath
6716
+ ref_image_path: options.context.referencePath,
6717
+ ...options.extraParams
5520
6718
  };
5521
6719
  }
5522
6720
  /**
@@ -5880,46 +7078,50 @@ var RoomController = class {
5880
7078
  /**
5881
7079
  * 发送生成开始信令。
5882
7080
  *
5883
- * @param options.faceRestoration 是否回贴原始人脸,默认 false。
5884
- * @param options.mirror 是否由后端镜像输入画面,默认 false。
5885
7081
  * @param options.taskID 当前生成任务标识。
5886
7082
  * @param options.videoFormat 模型生成使用的视频规格。
5887
- * @param options.targetSize 服务端生成后的回传尺寸,缺省使用生成尺寸。
5888
7083
  * @param options.context 本次生成条件。
7084
+ * @param options.targetSize 回传尺寸,缺省时不发送;网络视频默认使用生成尺寸。
7085
+ * @param options.mirror 是否由后端镜像输入画面,默认 false。
7086
+ * @param options.faceRestoration 是否回贴原始人脸,默认 false。
7087
+ * @param options.extraParams 合并到本次 start 的 params,不缓存。
5889
7088
  * @throws 房间未就绪或信令发送失败时抛出错误。
5890
7089
  */
5891
7090
  startGeneration(options) {
5892
7091
  return this.send(
5893
7092
  RoomEvent.start({
5894
- mirror: options.mirror ?? false,
5895
- faceRestoration: options.faceRestoration ?? false,
5896
7093
  userID: this.requireUserID(),
5897
7094
  taskID: options.taskID,
5898
7095
  videoFormat: options.videoFormat,
7096
+ context: options.context,
5899
7097
  targetSize: options.targetSize,
5900
- context: options.context
7098
+ mirror: options.mirror ?? false,
7099
+ faceRestoration: options.faceRestoration ?? false,
7100
+ extraParams: options.extraParams
5901
7101
  })
5902
7102
  );
5903
7103
  }
5904
7104
  /**
5905
7105
  * 发送生成条件变更信令。
5906
7106
  *
5907
- * @param options.faceRestoration 是否回贴原始人脸,默认 false。
5908
7107
  * @param options.taskID 当前生成任务标识。
5909
7108
  * @param options.videoFormat 模型生成使用的视频规格。
5910
- * @param options.targetSize 服务端生成后的回传尺寸。
5911
7109
  * @param options.context 本次生成条件。
7110
+ * @param options.targetSize 服务端生成后的回传尺寸,缺省时不发送。
7111
+ * @param options.faceRestoration 是否回贴原始人脸,默认 false。
7112
+ * @param options.extraParams 合并到本次 change_condition 的 params,不缓存。
5912
7113
  * @throws 房间未就绪或信令发送失败时抛出错误。
5913
7114
  */
5914
7115
  changeGenerationCondition(options) {
5915
7116
  return this.send(
5916
7117
  RoomEvent.changeCondition({
5917
- faceRestoration: options.faceRestoration ?? false,
5918
7118
  userID: this.requireUserID(),
5919
7119
  taskID: options.taskID,
5920
7120
  videoFormat: options.videoFormat,
7121
+ context: options.context,
5921
7122
  targetSize: options.targetSize,
5922
- context: options.context
7123
+ faceRestoration: options.faceRestoration ?? false,
7124
+ extraParams: options.extraParams
5923
7125
  })
5924
7126
  );
5925
7127
  }
@@ -6316,6 +7518,17 @@ var StreamController = class {
6316
7518
  get hasGenerationTask() {
6317
7519
  return this.state.generationTaskID !== void 0;
6318
7520
  }
7521
+ /**
7522
+ * 按活动结果流身份读取原始音视频,避免录制房间内其他用户的媒体。
7523
+ */
7524
+ getRemoteRecordingTracks() {
7525
+ const userID = this.state.activeRemoteStream?.userID;
7526
+ const videoTrack = userID && this.state.remoteVideoTracks.get(userID);
7527
+ if (!userID || !videoTrack) {
7528
+ return void 0;
7529
+ }
7530
+ return { videoTrack, audioTrack: this.rtcManager.getRemoteAudioTrack(userID) };
7531
+ }
6319
7532
  /**
6320
7533
  * 设置本地主视频流统计监听器,不改变 RTC 事件监听权。
6321
7534
  */
@@ -6397,8 +7610,17 @@ var StreamController = class {
6397
7610
  /**
6398
7611
  * 建立生成任务并发送开始信令。
6399
7612
  *
7613
+ * @param options.taskID 当前生成任务标识。
7614
+ * @param options.videoFormat 模型生成使用的视频规格。
7615
+ * @param options.context 本次生成条件。
7616
+ * @param options.restart 是否保留连接并替换当前生成任务,默认 false;不预先发送 stop。
7617
+ * @param options.targetSize 回传尺寸,缺省时不发送;网络视频默认使用生成尺寸。
7618
+ * @param options.mirror 是否由后端镜像输入画面,默认 false。
7619
+ * @param options.faceRestoration 是否回贴原始人脸,默认 false。
7620
+ * @param options.onStartSent start 发送成功后的回调,不等待生成确认。
7621
+ * @param options.extraParams 合并到本次 start 的 params,不缓存。
6400
7622
  * @returns 等待远端结果流确认的 Promise。
6401
- * @throws 任务标识无效、RTC 房间未就绪或已有生成任务时同步抛错;
7623
+ * @throws 任务标识无效、RTC 房间未就绪、旧任务仍在启动或未指定 restart 且已有生成任务时同步抛错;
6402
7624
  * 发送失败或确认超时通过返回的 Promise 拒绝。
6403
7625
  */
6404
7626
  beginGeneration(options) {
@@ -6412,7 +7634,7 @@ var StreamController = class {
6412
7634
  if (!this.state.roomID) {
6413
7635
  throw new XmaxError("RTC_ERROR" /* rtcError */, "RTC room is not configured");
6414
7636
  }
6415
- if (this.state.generationTaskID) {
7637
+ if (this.state.generationTaskID && (!options.restart || this.state.generationWaiter)) {
6416
7638
  throw new XmaxError(
6417
7639
  "RTC_ERROR" /* rtcError */,
6418
7640
  "Realtime generation is already active"
@@ -6447,11 +7669,12 @@ var StreamController = class {
6447
7669
  try {
6448
7670
  const sending = this.roomController.startGeneration({
6449
7671
  taskID,
6450
- mirror: options.mirror ?? false,
6451
- faceRestoration: options.faceRestoration ?? false,
6452
7672
  videoFormat: options.videoFormat,
7673
+ context: options.context,
6453
7674
  targetSize: options.targetSize,
6454
- context: options.context
7675
+ mirror: options.mirror ?? false,
7676
+ faceRestoration: options.faceRestoration ?? false,
7677
+ extraParams: options.extraParams
6455
7678
  });
6456
7679
  const state2 = this.state;
6457
7680
  const sent = Promise.resolve(sending).then(async () => {
@@ -6503,16 +7726,26 @@ var StreamController = class {
6503
7726
  /**
6504
7727
  * 发送生成条件变更信令。
6505
7728
  *
7729
+ * @param options.taskID 当前生成任务标识。
7730
+ * @param options.videoFormat 模型生成使用的视频规格。
7731
+ * @param options.context 本次生成条件。
7732
+ * @param options.restart 本次条件更新不使用该字段。
7733
+ * @param options.targetSize 回传尺寸,缺省时不发送。
7734
+ * @param options.mirror 本次条件更新不使用该字段。
7735
+ * @param options.faceRestoration 是否回贴原始人脸,默认 false。
7736
+ * @param options.onStartSent 本次条件更新不调用该回调。
7737
+ * @param options.extraParams 合并到本次 change_condition 的 params,不缓存。
6506
7738
  * @throws RTC 房间未就绪或条件变更信令发送失败时抛出错误。
6507
7739
  */
6508
7740
  async updateGeneration(options) {
6509
7741
  try {
6510
7742
  await this.roomController.changeGenerationCondition({
6511
7743
  taskID: options.taskID,
6512
- faceRestoration: options.faceRestoration ?? false,
6513
7744
  videoFormat: options.videoFormat,
7745
+ context: options.context,
6514
7746
  targetSize: options.targetSize,
6515
- context: options.context
7747
+ faceRestoration: options.faceRestoration ?? false,
7748
+ extraParams: options.extraParams
6516
7749
  });
6517
7750
  } catch (error) {
6518
7751
  throw XmaxError.from(error);
@@ -7106,8 +8339,9 @@ function qualityVideoFormat(baseline, level) {
7106
8339
 
7107
8340
  // src/Stream/Quality/QualityHistory.ts
7108
8341
  var QualityHistory = class {
7109
- constructor(model) {
8342
+ constructor(model, provider) {
7110
8343
  __publicField(this, "model", model);
8344
+ __publicField(this, "provider", provider);
7111
8345
  }
7112
8346
  /** 无记录、损坏、SSR 或浏览器禁止存储时使用默认 L2。 */
7113
8347
  read(direction) {
@@ -7127,7 +8361,7 @@ var QualityHistory = class {
7127
8361
  }
7128
8362
  }
7129
8363
  key(direction) {
7130
- return `xmax:quality:v1:agora:${encodeURIComponent(this.model)}:${direction}`;
8364
+ return `xmax:quality:v1:${this.provider}:${encodeURIComponent(this.model)}:${direction}`;
7131
8365
  }
7132
8366
  isValid(level) {
7133
8367
  return Number.isInteger(level) && level >= 1 && level <= Object.keys(QUALITY_TIERS).length;
@@ -7676,7 +8910,6 @@ var XmaxRealtimeConnectionManager = class {
7676
8910
  * 远端轨道与渲染绑定
7677
8911
  */
7678
8912
  __publicField(this, "activeRemoteTrack");
7679
- __publicField(this, "remoteTarget");
7680
8913
  }
7681
8914
  /**
7682
8915
  * 当前活动会话标识;尚未连接或清理完成后为空。
@@ -7766,21 +8999,15 @@ var XmaxRealtimeConnectionManager = class {
7766
8999
  videoFormat: options.remoteVideoFormat ?? options.localTrack.videoFormat
7767
9000
  });
7768
9001
  this.activeRemoteTrack = remoteTrack;
7769
- this.registerRemoteBinding(remoteTrack);
9002
+ this.dependencies.renderController.registerRemoteTrack(remoteTrack, this.dependencies.onInteraction);
7770
9003
  streamController.setRemoteAudioVolume(this.dependencies.remoteAudioVolume());
7771
9004
  return new RealtimeMediaStream({ id: "stream1" /* remote */, videoTrack: remoteTrack });
7772
9005
  }
7773
- /**
7774
- * 更新已绑定远端视图的插帧配置;传 undefined 停用插帧。
7775
- */
7776
- setFrameInterpolation(options) {
7777
- this.remoteTarget?.setFrameInterpolation?.(options);
7778
- }
7779
9006
  /**
7780
9007
  * 清空远端视图的媒体流,保留轨道与视图绑定以供统一清理。
7781
9008
  */
7782
9009
  clearRemoteMedia() {
7783
- this.remoteTarget?.setMediaStream(null);
9010
+ this.dependencies.renderController.setRemoteMedia(null);
7784
9011
  }
7785
9012
  /**
7786
9013
  * 停止会话心跳;未配置会话服务时不执行操作。
@@ -7860,7 +9087,7 @@ var XmaxRealtimeConnectionManager = class {
7860
9087
  if (track) {
7861
9088
  track.mediaStreamTrack = void 0;
7862
9089
  }
7863
- this.remoteTarget?.setMediaStream(null);
9090
+ this.dependencies.renderController.setRemoteMedia(null);
7864
9091
  return;
7865
9092
  }
7866
9093
  if (!track) {
@@ -7870,35 +9097,7 @@ var XmaxRealtimeConnectionManager = class {
7870
9097
  );
7871
9098
  }
7872
9099
  track.mediaStreamTrack = binding.videoTrack;
7873
- this.remoteTarget?.setMediaStream(new MediaStream([binding.videoTrack]));
7874
- }
7875
- /**
7876
- * 为远端轨道注册渲染绑定:attach 时挂流占位,媒体轨到达后送入画面。
7877
- */
7878
- registerRemoteBinding(track) {
7879
- VideoRenderRegistry.register(track, {
7880
- frameDisplayHandler: () => {
7881
- if (this.activeRemoteTrack === track) {
7882
- this.dependencies.onFrameDisplayed();
7883
- }
7884
- },
7885
- attachHandler: (view) => {
7886
- this.remoteTarget?.setFrameInterpolation?.(void 0);
7887
- view.isMirrored = false;
7888
- this.remoteTarget = view;
7889
- this.dependencies.onRenderAttached();
7890
- const mediaTrack = track.mediaStreamTrack;
7891
- view.setMediaStream(mediaTrack ? new MediaStream([mediaTrack]) : null);
7892
- },
7893
- detachHandler: (view) => {
7894
- if (this.remoteTarget === view) {
7895
- this.dependencies.onRenderDetached();
7896
- this.remoteTarget = void 0;
7897
- }
7898
- view.setFrameInterpolation?.(void 0);
7899
- view.setMediaStream(null);
7900
- }
7901
- });
9100
+ this.dependencies.renderController.setRemoteMedia(binding.videoTrack);
7902
9101
  }
7903
9102
  /**
7904
9103
  * 构造当前远端生成结果媒体流。
@@ -7971,12 +9170,11 @@ var XmaxRealtimeConnectionManager = class {
7971
9170
  }
7972
9171
  const track = this.activeRemoteTrack;
7973
9172
  if (track) {
7974
- VideoRenderRegistry.unregister(track);
9173
+ this.dependencies.renderController.resetRemoteTrack();
7975
9174
  track.mediaStreamTrack = void 0;
7976
9175
  }
7977
9176
  this.activeSession = void 0;
7978
9177
  this.activeRemoteTrack = void 0;
7979
- this.remoteTarget = void 0;
7980
9178
  if (sessionID && this.dependencies.sessionService) {
7981
9179
  try {
7982
9180
  await this.dependencies.sessionService.closeSession(sessionID);
@@ -8006,8 +9204,10 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
8006
9204
  /**
8007
9205
  * 注入房间传输组件和任务标识生成器,不创建会话或启动生成。
8008
9206
  */
8009
- constructor(streamController, taskIDGenerator = _XmaxRealtimeGenerationManager.createTaskID) {
9207
+ constructor(streamController, interactionController, renderController, taskIDGenerator = _XmaxRealtimeGenerationManager.createTaskID) {
8010
9208
  __publicField(this, "streamController", streamController);
9209
+ __publicField(this, "interactionController", interactionController);
9210
+ __publicField(this, "renderController", renderController);
8011
9211
  __publicField(this, "taskIDGenerator", taskIDGenerator);
8012
9212
  /**
8013
9213
  * 当前生成条件与任务标识
@@ -8033,37 +9233,50 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
8033
9233
  * 发送生成指令并等待确认、远端就绪和音频激活,成功后返回任务标识。
8034
9234
  * 条件上下文在生成确认后缓存;启动失败时尽力停止本次任务并保留原始错误。
8035
9235
  * @param options.videoFormat 模型生成使用的视频规格。
8036
- * @param options.targetSize 服务端生成后的回传尺寸,缺省使用生成尺寸。
8037
- * @param options.context 生成条件,缺省复用最近缓存的上下文。
8038
- * @param options.faceRestoration 是否回贴原始人脸,默认 false。
8039
- * @param options.mirror 是否由后端水平翻转输入,默认 false;仅用于 start 信令。
8040
9236
  * @param options.signal 取消本次生成启动的信号。
8041
9237
  * @param options.ensureCurrent 校验当前操作仍有效。
8042
9238
  * @param options.waitUntilRemoteReady 等待远端生成流就绪。
9239
+ * @param options.context 生成条件,缺省复用最近缓存的上下文。
9240
+ * @param options.restart 是否替换当前任务并重新发送 start,默认 false;不发送 stop。
9241
+ * @param options.targetSize 服务端生成后的回传尺寸,缺省时不发送。
9242
+ * @param options.mirror 是否由后端水平翻转输入,默认 false;仅用于 start 信令。
9243
+ * @param options.faceRestoration 是否回贴原始人脸,默认 false。
8043
9244
  * @param options.onStartSent start 信令发送成功后的可选异步回调。
9245
+ * @param options.onRemoteReady 远端视频就绪后通知任务标识,不等待录制等附加功能。
9246
+ * @param options.extraParams 合并到本次 start 的 params,不缓存。
8044
9247
  */
8045
9248
  async start(options) {
8046
9249
  const context = this.validateContext(options.context);
8047
9250
  options.ensureCurrent();
8048
9251
  const taskID = this.taskIDGenerator();
9252
+ if (options.restart) {
9253
+ this.interactionController.stopInteraction();
9254
+ this.renderController.setInteractionEnabled(false);
9255
+ }
8049
9256
  this.activeTaskID = taskID;
8050
9257
  try {
8051
9258
  const confirmation = this.streamController.beginGeneration({
8052
9259
  taskID,
8053
9260
  videoFormat: options.videoFormat,
8054
9261
  context,
9262
+ restart: options.restart,
8055
9263
  ...options.targetSize && { targetSize: options.targetSize },
8056
9264
  mirror: options.mirror ?? false,
8057
9265
  faceRestoration: options.faceRestoration ?? false,
8058
- ...options.onStartSent && { onStartSent: options.onStartSent }
9266
+ ...options.onStartSent && { onStartSent: options.onStartSent },
9267
+ extraParams: options.extraParams
8059
9268
  });
8060
9269
  await this.awaitConfirmation(confirmation, options.signal);
8061
9270
  options.ensureCurrent();
8062
9271
  this.currentContext = context;
8063
9272
  await options.waitUntilRemoteReady();
8064
9273
  options.ensureCurrent();
9274
+ options.onRemoteReady?.(taskID);
9275
+ options.ensureCurrent();
8065
9276
  await this.streamController.activateRemoteAudio();
8066
9277
  options.ensureCurrent();
9278
+ this.interactionController.startInteraction(taskID, options.videoFormat);
9279
+ this.renderController.setInteractionEnabled(true, options.videoFormat);
8067
9280
  return taskID;
8068
9281
  } catch (error) {
8069
9282
  await this.stop(taskID);
@@ -8074,9 +9287,10 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
8074
9287
  * 更新活动任务的生成条件;信令发送成功后才覆盖缓存,无任务或上下文时抛错。
8075
9288
  * @param taskID 当前活动任务标识。
8076
9289
  * @param options.videoFormat 模型生成使用的视频规格。
8077
- * @param options.targetSize 服务端生成后的回传尺寸。
8078
9290
  * @param options.context 生成条件,缺省复用最近缓存的上下文。
9291
+ * @param options.targetSize 服务端生成后的回传尺寸,缺省时不发送。
8079
9292
  * @param options.faceRestoration 是否回贴原始人脸,默认 false。
9293
+ * @param options.extraParams 合并到本次 change_condition 的 params,不缓存。
8080
9294
  */
8081
9295
  async update(taskID, options) {
8082
9296
  const context = options.context ?? this.currentContext;
@@ -8087,11 +9301,14 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
8087
9301
  taskID,
8088
9302
  videoFormat: options.videoFormat,
8089
9303
  context,
9304
+ ...options.targetSize && { targetSize: options.targetSize },
8090
9305
  faceRestoration: options.faceRestoration ?? false,
8091
- ...options.targetSize && { targetSize: options.targetSize }
9306
+ extraParams: options.extraParams
8092
9307
  });
8093
9308
  if (this.activeTaskID !== taskID) throw RealtimeCoordinator.cancelledError();
8094
9309
  this.currentContext = context;
9310
+ this.interactionController.startInteraction(taskID, options.videoFormat);
9311
+ this.renderController.setInteractionEnabled(true, options.videoFormat);
8095
9312
  }
8096
9313
  /**
8097
9314
  * 幂等停止指定活动任务;并发调用共享结果,停止失败仅记录日志。
@@ -8102,6 +9319,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
8102
9319
  }
8103
9320
  if (!taskID || this.activeTaskID !== taskID) return;
8104
9321
  this.activeTaskID = void 0;
9322
+ this.interactionController.stopInteraction();
9323
+ this.renderController.setInteractionEnabled(false);
8105
9324
  const completion = (async () => {
8106
9325
  try {
8107
9326
  await this.streamController.stopGeneration(taskID);
@@ -8122,6 +9341,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
8122
9341
  */
8123
9342
  async reset(taskID) {
8124
9343
  this.currentContext = void 0;
9344
+ this.interactionController.stopInteraction();
9345
+ this.renderController.setInteractionEnabled(false);
8125
9346
  await this.stop(this.activeTaskID ?? (taskID || void 0));
8126
9347
  }
8127
9348
  /**
@@ -8178,6 +9399,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8178
9399
  */
8179
9400
  __publicField(this, "options");
8180
9401
  __publicField(this, "mediaService");
9402
+ /**
9403
+ * 可选的任务录制;未开启时不分配浏览器录制资源。
9404
+ */
9405
+ __publicField(this, "recordingController", new RecordingController());
8181
9406
  /**
8182
9407
  * 本地流创建时的生成格式;上行编码调整不改变模型输入配置与远端插帧尺寸。
8183
9408
  */
@@ -8194,15 +9419,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8194
9419
  */
8195
9420
  __publicField(this, "coordinator");
8196
9421
  __publicField(this, "errorHandler");
8197
- __publicField(this, "cameraController");
8198
- __publicField(this, "networkVideoController", new NetworkVideoController());
8199
- __publicField(this, "localVideoController", new LocalVideoController((error) => {
8200
- void this.coordinator.terminateWithError(error, 0 /* connection */);
8201
- }));
8202
- __publicField(this, "externalVideoController", new ExternalVideoController((error) => {
8203
- void this.coordinator.terminateWithError(error, 1 /* all */);
8204
- }));
8205
- __publicField(this, "rtcManager");
9422
+ __publicField(this, "mediaController");
9423
+ __publicField(this, "renderController");
8206
9424
  /**
8207
9425
  * 启动计时与首帧通知
8208
9426
  */
@@ -8247,14 +9465,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8247
9465
  this.supportsInterpolation = dependencies?.supportsFrameInterpolation ?? (async (size) => await frameInterpolationAdapter(size) !== null);
8248
9466
  this.errorHandler = new RealtimeErrorHandler();
8249
9467
  const rtcManager = dependencies?.rtcManager ?? createRtcManager(options.provider, dependencies?.environment ?? "china" /* china */);
8250
- this.rtcManager = rtcManager;
8251
- this.cameraController = dependencies?.cameraController ?? new CameraController({
8252
- rtcManager,
8253
- mediaService: this.mediaService,
8254
- errorListener: (error) => {
8255
- void this.errorHandler.report(error);
8256
- }
8257
- });
8258
9468
  const sessionService = dependencies?.sessionService ?? (dependencies?.apiService ? new RealtimeSessionService({ apiService: dependencies.apiService, provider: options.provider }) : void 0);
8259
9469
  this.streamController = dependencies?.streamController ?? new StreamController({
8260
9470
  rtcManager,
@@ -8265,10 +9475,38 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8265
9475
  void this.errorHandler.report(error);
8266
9476
  },
8267
9477
  remoteStreamListener: (binding) => {
9478
+ if (!binding) {
9479
+ void this.recordingController.stop("mediaInterrupted" /* mediaInterrupted */);
9480
+ }
8268
9481
  this.clearRemoteVideoStatistics();
8269
9482
  this.connectionManager.handleRemoteStreamBinding(binding);
8270
9483
  }
8271
9484
  });
9485
+ this.mediaController = new MediaController({
9486
+ rtcManager,
9487
+ mediaService: this.mediaService,
9488
+ cameraController: dependencies?.cameraController,
9489
+ onCameraError: (error) => {
9490
+ void this.errorHandler.report(error);
9491
+ },
9492
+ onVideoError: (error) => {
9493
+ void this.coordinator.terminateWithError(error, 0 /* connection */);
9494
+ },
9495
+ onExternalError: (error) => {
9496
+ void this.coordinator.terminateWithError(error, 1 /* all */);
9497
+ },
9498
+ onImageError: (error) => {
9499
+ void this.coordinator.terminateWithError(error, 1 /* all */);
9500
+ },
9501
+ interactionListener: (taskID, points) => this.streamController.sendTracks(taskID, points)
9502
+ });
9503
+ this.renderController = new RenderController({
9504
+ onFrameDisplayed: () => this.remoteFrameDisplayHandler?.(),
9505
+ onAttached: () => this.applyRemoteInterpolation(),
9506
+ onDetached: () => {
9507
+ this.interpolationRevision++;
9508
+ }
9509
+ });
8272
9510
  this.connectionManager = new XmaxRealtimeConnectionManager({
8273
9511
  provider: options.provider,
8274
9512
  sessionService,
@@ -8278,34 +9516,19 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8278
9516
  onHeartbeatFailure: (sessionID, error) => {
8279
9517
  void this.handleHeartbeatFailure(sessionID, error);
8280
9518
  },
8281
- onFrameDisplayed: () => this.remoteFrameDisplayHandler?.(),
8282
- onRenderAttached: () => this.applyRemoteInterpolation(),
8283
- onRenderDetached: () => {
8284
- this.interpolationRevision++;
8285
- }
9519
+ renderController: this.renderController,
9520
+ onInteraction: (frame) => this.mediaController.submitInteraction(frame)
8286
9521
  });
8287
- this.generationManager = new XmaxRealtimeGenerationManager(this.streamController);
9522
+ this.generationManager = new XmaxRealtimeGenerationManager(this.streamController, this.mediaController, this.renderController);
8288
9523
  this.coordinator = new RealtimeCoordinator({
8289
9524
  errorHandler: this.errorHandler,
8290
9525
  cleanup: (scope, taskID) => this.performCleanup(scope, taskID)
8291
9526
  });
8292
- if (options.provider === "agora" /* agora */) {
8293
- this.qualityHistory = new QualityHistory(options.model);
9527
+ if (options.provider === "agora" /* agora */ || options.provider === "trtc" /* trtc */) {
9528
+ this.qualityHistory = new QualityHistory(options.model, options.provider);
8294
9529
  this.qualityController = new QualityController(async (change, signal) => {
8295
- if (signal.aborted || this.currentState.connectionState !== "Generating" /* generating */) {
8296
- throw new XmaxError("CANCELLED" /* cancelled */, "Adaptive video format update was cancelled");
8297
- }
8298
9530
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8299
- const track = this.currentUplinkTrack;
8300
- const state2 = track && this.qualityStates.get(track);
8301
- if (!track || !state2) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Adaptive video baseline is unavailable");
8302
- const format = qualityVideoFormat(state2.baseline, change.to);
8303
- await this.streamController.updateUplinkVideoFormat(format);
8304
- token.ensureCurrent();
8305
- this.generationVideoFormat ?? (this.generationVideoFormat = state2.baseline);
8306
- track.updateVideoFormat(format);
8307
- state2.level = change.to;
8308
- this.qualityHistory?.write("uplink", change.to);
9531
+ await this.applyUplinkQuality(change.to, token, signal);
8309
9532
  });
8310
9533
  });
8311
9534
  this.downlinkQualityController = new QualityController(async (change, signal) => {
@@ -8363,13 +9586,58 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8363
9586
  this.applyRemoteInterpolation();
8364
9587
  });
8365
9588
  }
9589
+ /**
9590
+ * 手动调整上行档位,保留原始基准,自动调档从新档位继续。
9591
+ * @param direction 升一档或降一档;到达 L1/L5 边界时不重复应用。
9592
+ */
9593
+ async adjustUplinkQuality(direction) {
9594
+ await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
9595
+ const controller = this.qualityController;
9596
+ const track = this.currentUplinkTrack;
9597
+ const state2 = track && this.qualityStates.get(track);
9598
+ if (!controller || !state2 || this.currentState.connectionState !== "Generating" /* generating */ || direction !== "upgrade" && direction !== "downgrade") {
9599
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Uplink quality adjustment requires an active adaptive-quality generation and a valid direction");
9600
+ }
9601
+ const level = Math.max(1, Math.min(Object.keys(QUALITY_TIERS).length, state2.level + (direction === "upgrade" ? -1 : 1)));
9602
+ if (level === state2.level) return;
9603
+ this.adaptiveQualityRevision++;
9604
+ controller.resetSamples();
9605
+ await this.applyUplinkQuality(level, token);
9606
+ controller.start(level);
9607
+ });
9608
+ }
9609
+ async applyUplinkQuality(level, token, signal) {
9610
+ const track = this.currentUplinkTrack;
9611
+ const state2 = track && this.qualityStates.get(track);
9612
+ const ensureActive2 = () => {
9613
+ token.ensureCurrent();
9614
+ if (signal?.aborted || !track || !state2 || track !== this.currentUplinkTrack || state2 !== this.qualityStates.get(track) || this.currentState.connectionState !== "Generating" /* generating */) {
9615
+ throw new XmaxError("CANCELLED" /* cancelled */, "Adaptive video format update was cancelled");
9616
+ }
9617
+ };
9618
+ ensureActive2();
9619
+ if (!track || !state2) return;
9620
+ const target = this.uplinkQualityFormat(state2.baseline, level);
9621
+ let format;
9622
+ if (this.options.provider === "trtc" /* trtc */) {
9623
+ format = await this.streamController.updateUplinkFrameRate(target.fps);
9624
+ } else {
9625
+ await this.streamController.updateUplinkVideoFormat(target);
9626
+ format = target;
9627
+ }
9628
+ ensureActive2();
9629
+ this.generationVideoFormat ?? (this.generationVideoFormat = state2.baseline);
9630
+ track.updateVideoFormat(format);
9631
+ state2.level = level;
9632
+ this.qualityHistory?.write("uplink", level);
9633
+ }
8366
9634
  /** 手动调试与自动调档使用同一尺寸信令路径,不修改生成或上行配置。 */
8367
9635
  async adjustDownlinkQuality(direction) {
8368
9636
  await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
8369
9637
  const controller = this.downlinkQualityController;
8370
9638
  const state2 = this.downlinkQualityState;
8371
9639
  if (!controller || !state2 || this.currentState.connectionState !== "Generating" /* generating */ || direction !== "upgrade" && direction !== "downgrade") {
8372
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Downlink quality adjustment requires an active Agora generation and a valid direction");
9640
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Downlink quality adjustment requires an active adaptive-quality generation and a valid direction");
8373
9641
  }
8374
9642
  const level = Math.max(1, Math.min(Object.keys(QUALITY_TIERS).length, state2.level + (direction === "upgrade" ? -1 : 1)));
8375
9643
  if (level === state2.level) return;
@@ -8451,7 +9719,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8451
9719
  applyRemoteInterpolation() {
8452
9720
  const revision = ++this.interpolationRevision;
8453
9721
  const size = this.interpolationSize;
8454
- this.connectionManager.setFrameInterpolation(size ? {
9722
+ this.renderController.setFrameInterpolation(size ? {
8455
9723
  size,
8456
9724
  onFailure: (error) => {
8457
9725
  if (revision === this.interpolationRevision) this.handleInterpolationFailure(error);
@@ -8621,7 +9889,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8621
9889
  "media" /* media */,
8622
9890
  void 0,
8623
9891
  async (token) => {
8624
- this.ensureNoLocalSource();
9892
+ this.mediaController.ensureNoLocalSource();
8625
9893
  await this.coordinator.commit(
8626
9894
  new RealtimeState({
8627
9895
  connectionState: "Preparing" /* preparing */
@@ -8642,7 +9910,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8642
9910
  let stream;
8643
9911
  try {
8644
9912
  token.ensureCurrent();
8645
- stream = await this.cameraController.createLocalCameraStream(options);
9913
+ stream = await this.mediaController.createLocalCameraStream(options);
8646
9914
  } catch (error) {
8647
9915
  this.disposePermissionWatch();
8648
9916
  this.launchTimer.cancel();
@@ -8651,7 +9919,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8651
9919
  token.ensureCurrent();
8652
9920
  this.generationVideoFormat = stream.videoTrack?.videoFormat;
8653
9921
  completeCamera();
8654
- this.cameraController.setPreviewReadyHandler((isCurrent) => {
9922
+ this.mediaController.setCameraPreviewReadyHandler((isCurrent) => {
8655
9923
  void this.coordinator.localPreviewDidBecomeReady(isCurrent);
8656
9924
  });
8657
9925
  return stream;
@@ -8667,12 +9935,12 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8667
9935
  */
8668
9936
  async createNetworkVideoStream(options) {
8669
9937
  return this.coordinator.run("media" /* media */, void 0, async (token) => {
8670
- this.ensureNoLocalSource();
8671
- const stream = this.networkVideoController.create(options, this.mediaService);
9938
+ this.mediaController.ensureNoLocalSource();
9939
+ const stream = this.mediaController.createNetworkVideoStream(options);
8672
9940
  token.setFailureScope(1 /* all */);
8673
9941
  this.launchTimer.start();
8674
9942
  this.clearVideoStatistics();
8675
- await this.rtcManager.initialize();
9943
+ await this.mediaController.prepareSource(() => token.ensureCurrent());
8676
9944
  token.ensureCurrent();
8677
9945
  this.generationVideoFormat = stream.videoTrack?.videoFormat;
8678
9946
  await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
@@ -8688,22 +9956,32 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8688
9956
  */
8689
9957
  async createLocalVideoStream(options) {
8690
9958
  return this.coordinator.run("media" /* media */, void 0, async (token) => {
8691
- this.ensureNoLocalSource();
8692
- if (!this.rtcManager.setExternalMediaTracks) {
8693
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "The RTC provider does not support local video streams");
8694
- }
9959
+ this.mediaController.ensureNoLocalSource();
9960
+ this.mediaController.validateExternalMediaSupport("local");
8695
9961
  token.setFailureScope(1 /* all */);
8696
9962
  this.launchTimer.start();
8697
9963
  this.clearVideoStatistics();
8698
- const stream = await this.localVideoController.create(options, this.mediaService, token.signal);
8699
- token.ensureCurrent();
8700
- await this.rtcManager.initialize();
9964
+ const stream = await this.mediaController.createLocalVideoStream(options, token.signal);
8701
9965
  token.ensureCurrent();
8702
- await this.rtcManager.setExternalMediaTracks({
8703
- videoTrack: stream.videoTrack.mediaStreamTrack,
8704
- audioTrack: this.localVideoController.audioTrack
8705
- });
9966
+ await this.mediaController.prepareSource(() => token.ensureCurrent());
9967
+ this.generationVideoFormat = stream.videoTrack?.videoFormat;
9968
+ await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
9969
+ return stream;
9970
+ });
9971
+ }
9972
+ /**
9973
+ * 创建本地图片静止视频源并准备 RTC,失败或取消时释放本次资源。
9974
+ */
9975
+ async createLocalImageStream(options) {
9976
+ return this.coordinator.run("media" /* media */, void 0, async (token) => {
9977
+ this.mediaController.ensureNoLocalSource();
9978
+ this.mediaController.validateExternalMediaSupport("image");
9979
+ token.setFailureScope(1 /* all */);
9980
+ this.launchTimer.start();
9981
+ this.clearVideoStatistics();
9982
+ const stream = await this.mediaController.createLocalImageStream(options, token.signal);
8706
9983
  token.ensureCurrent();
9984
+ await this.mediaController.prepareSource(() => token.ensureCurrent());
8707
9985
  this.generationVideoFormat = stream.videoTrack?.videoFormat;
8708
9986
  await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
8709
9987
  return stream;
@@ -8716,18 +9994,13 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8716
9994
  */
8717
9995
  async createExternalVideoStream(options) {
8718
9996
  return this.coordinator.run("media" /* media */, void 0, async (token) => {
8719
- this.ensureNoLocalSource();
8720
- if (!this.rtcManager.setExternalMediaTracks) {
8721
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "The RTC provider does not support external video streams");
8722
- }
8723
- const stream = this.externalVideoController.create(options, this.mediaService);
9997
+ this.mediaController.ensureNoLocalSource();
9998
+ this.mediaController.validateExternalMediaSupport("external");
9999
+ const stream = this.mediaController.createExternalVideoStream(options);
8724
10000
  token.setFailureScope(1 /* all */);
8725
10001
  this.launchTimer.start();
8726
10002
  this.clearVideoStatistics();
8727
- await this.rtcManager.initialize();
8728
- token.ensureCurrent();
8729
- await this.rtcManager.setExternalMediaTracks({ videoTrack: stream.videoTrack.mediaStreamTrack });
8730
- token.ensureCurrent();
10003
+ await this.mediaController.prepareSource(() => token.ensureCurrent());
8731
10004
  this.generationVideoFormat = stream.videoTrack?.videoFormat;
8732
10005
  await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
8733
10006
  return stream;
@@ -8735,19 +10008,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8735
10008
  }
8736
10009
  /** 当前可发布的本地视频轨,不包含服务端直接读取的网络视频。 */
8737
10010
  get currentUplinkTrack() {
8738
- return this.externalVideoController.currentTrack ?? this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
10011
+ return this.mediaController.uplinkTrack;
8739
10012
  }
8740
10013
  /** 当前 Manager 拥有的媒体源轨道。 */
8741
10014
  get currentLocalTrack() {
8742
- return this.currentUplinkTrack ?? this.networkVideoController.currentTrack;
8743
- }
8744
- /**
8745
- * 创建前拒绝覆盖活动源,避免旧预览或连接资源被静默遗留。
8746
- */
8747
- ensureNoLocalSource() {
8748
- if (this.currentLocalTrack || this.localVideoController.isActive) {
8749
- throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local stream before creating another one");
8750
- }
10015
+ return this.mediaController.currentTrack;
8751
10016
  }
8752
10017
  /**
8753
10018
  * 移除媒体权限弹窗观察;授权决定已记录或不再需要观察时调用。
@@ -8856,7 +10121,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8856
10121
  * 停止本地相机流并释放本地预览与 RTC 资源。
8857
10122
  */
8858
10123
  async stopLocalCameraStream() {
8859
- if (this.networkVideoController.currentTrack || this.localVideoController.isActive || this.externalVideoController.currentTrack) {
10124
+ if (this.mediaController.source && this.mediaController.source !== "camera") {
8860
10125
  return;
8861
10126
  }
8862
10127
  await this.coordinator.run(
@@ -8866,7 +10131,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8866
10131
  this.launchTimer.cancel();
8867
10132
  this.clearVideoStatistics();
8868
10133
  this.disposePermissionWatch();
8869
- await this.cameraController.stopLocalCameraStream();
10134
+ await this.mediaController.stopLocalCameraStream();
8870
10135
  token.ensureCurrent();
8871
10136
  this.generationVideoFormat = void 0;
8872
10137
  await this.coordinator.commit(
@@ -8889,11 +10154,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8889
10154
  "cameraSwitch" /* cameraSwitch */,
8890
10155
  void 0,
8891
10156
  async (token) => {
8892
- if (this.externalVideoController.currentTrack) {
10157
+ if (this.mediaController.source === "externalVideo") {
8893
10158
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External video capture is managed by the caller");
8894
10159
  }
8895
10160
  this.qualityController?.stop();
8896
- const stream = await this.cameraController.switchCamera();
10161
+ const stream = await this.mediaController.switchCamera();
8897
10162
  token.ensureCurrent();
8898
10163
  return stream;
8899
10164
  }
@@ -8930,19 +10195,24 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8930
10195
  /**
8931
10196
  * 按需建立连接并开始生成。
8932
10197
  *
8933
- * 尚未连接时先建立实时连接;已在生成时仅更新生成条件,不重启生成。
10198
+ * 尚未连接时先建立实时连接;已在生成时默认更新条件,restart 为 true 时重新开始生成。
8934
10199
  * 首次生成必须提供条件上下文,之后缺省时复用最近一次缓存的上下文。
8935
10200
  *
8936
10201
  * @param options.localStream 由当前 Manager 创建的摄像头、外部视频轨、本地文件或网络视频源。
8937
10202
  * @param options.context 本次生成使用的条件上下文;缺省时复用缓存。
10203
+ * @param options.restart 是否保留 RTC 连接并创建新任务发送 start,默认 false;不预先发送 stop。
8938
10204
  * @param options.faceRestoration 是否启用原始人脸回贴,默认 false;正在生成时通过 change_condition 更新。
10205
+ * @param options.extraParams 合并到本次 start 或 change_condition 的 params;不缓存,自动恢复时不携带,不允许覆盖 SDK 字段。
10206
+ * @param options.record 是否录制远端原始音视频约 20 秒,默认 false;仅 start 有效,条件更新时忽略,重启录制暂不支持。
8939
10207
  * @returns 承载远端生成画面的媒体流。
10208
+ * @throws extraParams 包含非 JSON 数据、循环引用或 SDK 保留字段时,在连接或更新前抛出 invalidConfiguration。
8940
10209
  * @throws 本地流不属于当前 Manager、缺少可用的条件上下文、信令发送
8941
10210
  * 或生成确认失败时抛出错误;失败时自动停止生成任务并释放连接资源。
8942
10211
  */
8943
10212
  async startGeneration(options) {
10213
+ const extraParams = snapshotExtraParams(options.extraParams);
8944
10214
  return this.coordinator.run(
8945
- "generation" /* generation */,
10215
+ options.restart === true ? "generationRestart" /* generationRestart */ : "generation" /* generation */,
8946
10216
  void 0,
8947
10217
  async (token) => {
8948
10218
  const localTrack = options.localStream.videoTrack;
@@ -8960,34 +10230,50 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8960
10230
  );
8961
10231
  }
8962
10232
  const currentState = this.coordinator.currentState;
8963
- if (currentState.connectionState === "Generating" /* generating */) {
10233
+ const isGenerating = currentState.connectionState === "Generating" /* generating */;
10234
+ if (isGenerating && options.restart !== true) {
8964
10235
  await this.generationManager.update(currentState.taskID, {
8965
10236
  videoFormat,
8966
- faceRestoration: options.faceRestoration ?? false,
10237
+ context: options.context,
8967
10238
  targetSize: this.downlinkQualityController ? this.downlinkQualityState?.size : void 0,
8968
- context: options.context
10239
+ faceRestoration: options.faceRestoration ?? false,
10240
+ extraParams
8969
10241
  });
8970
10242
  token.ensureCurrent();
8971
10243
  return this.connectionManager.makeRemoteStream();
8972
10244
  }
8973
10245
  const supplied = this.generationManager.validateContext(options.context);
8974
- const referenceVideo = this.networkVideoController.reference;
10246
+ const referenceVideo = this.mediaController.referenceVideo;
8975
10247
  if (!referenceVideo && supplied.referenceVideo) {
8976
10248
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Create a network video stream before using a reference video");
8977
10249
  }
8978
10250
  const context = referenceVideo ? new RealtimeContext({ ...supplied, referenceVideo }) : supplied;
8979
- if (currentState.connectionState !== "Connected" /* connected */) {
10251
+ if (currentState.connectionState !== "Connected" /* connected */ && !isGenerating) {
8980
10252
  await this.performConnect(options.localStream, token);
8981
10253
  }
8982
- return this.performStartGeneration(token, videoFormat, context, options.faceRestoration ?? false);
10254
+ return this.performStartGeneration(
10255
+ token,
10256
+ videoFormat,
10257
+ context,
10258
+ options.restart ?? false,
10259
+ options.faceRestoration ?? false,
10260
+ extraParams,
10261
+ options.record ?? false
10262
+ );
8983
10263
  }
8984
10264
  );
8985
10265
  }
10266
+ /**
10267
+ * 设置录制最终事件监听器;清除监听不停止录制,录制错误不影响生成。
10268
+ */
10269
+ setRecordingListener(listener) {
10270
+ this.recordingController.setListener(listener);
10271
+ }
8986
10272
  /**
8987
10273
  * 断开实时连接并保留当前本地媒体预览。
8988
10274
  */
8989
10275
  async disconnect() {
8990
- this.localVideoController.pause();
10276
+ this.mediaController.pauseLocalVideo();
8991
10277
  this.launchTimer.cancel();
8992
10278
  this.clearVideoStatistics();
8993
10279
  await this.coordinator.disconnect();
@@ -8997,15 +10283,20 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8997
10283
  * 关闭期间重复调用会等待同一个释放任务;关闭完成后仍可重新创建本地流。
8998
10284
  */
8999
10285
  async close() {
9000
- this.localVideoController.pause();
10286
+ this.mediaController.pauseLocalVideo();
9001
10287
  this.launchTimer.cancel();
9002
10288
  this.clearVideoStatistics();
9003
10289
  await this.coordinator.terminate(1 /* all */);
9004
10290
  }
10291
+ /** TRTC 临时只按档位调整帧率;启动时也保留原始分辨率。 */
10292
+ uplinkQualityFormat(baseline, level) {
10293
+ const format = qualityVideoFormat(baseline, level);
10294
+ return this.options.provider === "trtc" /* trtc */ ? format.resized(baseline.width, baseline.height) : format;
10295
+ }
9005
10296
  /** 仅为尚无档位记录的本地上行选择初始档位;不应用配置或覆盖重连状态。 */
9006
10297
  resolveInitialUplinkQuality(track) {
9007
10298
  if (!this.qualityController) return void 0;
9008
- if (this.networkVideoController.currentTrack) return void 0;
10299
+ if (this.mediaController.source === "networkVideo") return void 0;
9009
10300
  if (this.qualityStates.has(track)) return void 0;
9010
10301
  const baseline = track.videoFormat;
9011
10302
  if (!baseline) return void 0;
@@ -9038,8 +10329,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
9038
10329
  if (token.signal.aborted) abortWarmup();
9039
10330
  try {
9040
10331
  let beforePublish;
9041
- if (localTrack === this.cameraController.currentTrack && this.cameraController.isFrameValidationEnabled) {
9042
- const cameraReady = this.cameraController.waitForValidCameraFrame(warmupController.signal).then(() => {
10332
+ if (this.mediaController.isFrameValidationEnabled) {
10333
+ const cameraReady = this.mediaController.waitForValidCameraFrame(warmupController.signal).then(() => {
9043
10334
  token.ensureCurrent();
9044
10335
  });
9045
10336
  void cameraReady.catch(() => {
@@ -9047,7 +10338,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
9047
10338
  beforePublish = () => cameraReady;
9048
10339
  }
9049
10340
  const initialQuality = this.resolveInitialUplinkQuality(localTrack);
9050
- const initialFormat = initialQuality ? qualityVideoFormat(initialQuality.baseline, initialQuality.level) : void 0;
10341
+ const initialFormat = initialQuality ? this.uplinkQualityFormat(initialQuality.baseline, initialQuality.level) : void 0;
9051
10342
  const remote = await this.connectionManager.connect({
9052
10343
  localTrack,
9053
10344
  remoteVideoFormat: this.generationVideoFormat ?? localTrack.videoFormat,
@@ -9060,8 +10351,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
9060
10351
  this.qualityHistory?.write("uplink", initialQuality.level);
9061
10352
  },
9062
10353
  model: this.options.model,
9063
- includeLocalAudio: this.localVideoController.isActive ? this.localVideoController.audioTrack !== void 0 : localTrack === this.cameraController.currentTrack && this.cameraController.useMicrophone,
9064
- publishLocalMedia: !this.networkVideoController.currentTrack,
10354
+ includeLocalAudio: this.mediaController.hasAudio,
10355
+ publishLocalMedia: this.mediaController.source !== "networkVideo",
9065
10356
  ensureCurrent: () => token.ensureCurrent(),
9066
10357
  onPublished: () => {
9067
10358
  this.acceptsVideoStatistics = true;
@@ -9083,9 +10374,21 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
9083
10374
  }
9084
10375
  /**
9085
10376
  * 编排插帧、首帧计时和生成状态,任务与确认由生成管理器负责。
9086
- */
9087
- async performStartGeneration(token, videoFormat, context, faceRestoration) {
10377
+ *
10378
+ * @param token 当前生成操作的取消与状态控制令牌。
10379
+ * @param videoFormat 模型生成使用的视频规格。
10380
+ * @param context 本次已解析的生成条件。
10381
+ * @param restart 是否替换当前生成任务并重新发送 start。
10382
+ * @param faceRestoration 是否回贴原始人脸。
10383
+ * @param extraParams 合并到本次 start 的 params,不缓存。
10384
+ * @param record 是否为本次任务开启独立录制,不影响生成返回时间。
10385
+ * @returns 承载生成结果的远端媒体流。
10386
+ */
10387
+ async performStartGeneration(token, videoFormat, context, restart, faceRestoration, extraParams, record2 = false) {
9088
10388
  token.setFailureScope(0 /* connection */);
10389
+ if (restart) {
10390
+ void this.recordingController.stop("restarted" /* restarted */);
10391
+ }
9089
10392
  const initialLevel = this.qualityHistory?.read("downlink") ?? 1;
9090
10393
  const targetSize = this.downlinkQualityController ? qualityVideoSize(videoFormat, initialLevel) : void 0;
9091
10394
  await this.prepareFrameInterpolation(targetSize ?? videoFormat, token);
@@ -9095,21 +10398,33 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
9095
10398
  };
9096
10399
  const taskID = await this.generationManager.start({
9097
10400
  videoFormat,
9098
- targetSize,
9099
- context,
9100
- faceRestoration,
9101
- mirror: this.currentLocalTrack === this.cameraController.currentTrack && this.cameraController.currentTrack?.position === "front" /* front */,
9102
10401
  signal: token.signal,
9103
10402
  ensureCurrent: () => token.ensureCurrent(),
9104
- onStartSent: this.localVideoController.isActive ? async () => {
9105
- token.ensureCurrent();
9106
- await this.localVideoController.start(token.signal);
9107
- token.ensureCurrent();
9108
- } : void 0,
9109
10403
  waitUntilRemoteReady: () => this.connectionManager.waitUntilRemoteTrackReady(
9110
10404
  REMOTE_FIRST_FRAME_TIMEOUT_MS,
9111
10405
  token.signal
9112
- )
10406
+ ),
10407
+ context,
10408
+ restart,
10409
+ onRemoteReady: record2 ? (taskID2) => {
10410
+ if (restart) {
10411
+ this.recordingController.reportFailure(taskID2, new XmaxError(
10412
+ "INVALID_CONFIGURATION" /* invalidConfiguration */,
10413
+ "Recording restarted generation tasks is not supported"
10414
+ ));
10415
+ } else {
10416
+ this.recordingController.start(taskID2, () => this.streamController.getRemoteRecordingTracks());
10417
+ }
10418
+ } : void 0,
10419
+ targetSize,
10420
+ mirror: this.mediaController.source === "camera" && this.currentLocalTrack?.position === "front" /* front */,
10421
+ faceRestoration,
10422
+ onStartSent: this.mediaController.source === "localVideo" ? async () => {
10423
+ token.ensureCurrent();
10424
+ await this.mediaController.startLocalVideo(token.signal);
10425
+ token.ensureCurrent();
10426
+ } : void 0,
10427
+ extraParams
9113
10428
  });
9114
10429
  const sessionID = this.connectionManager.currentSessionID;
9115
10430
  if (!sessionID) {
@@ -9127,13 +10442,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
9127
10442
  token
9128
10443
  );
9129
10444
  this.qualityHistory?.write("downlink", initialLevel);
9130
- const onFinish = this.networkVideoController.onFinish;
9131
- if (this.networkVideoController.currentTrack) {
9132
- this.streamController.activateNetworkVideoCompletion(onFinish && (() => {
10445
+ const onFinish = this.mediaController.onNetworkVideoFinish;
10446
+ if (this.mediaController.source === "networkVideo") {
10447
+ this.streamController.activateNetworkVideoCompletion(() => {
9133
10448
  if (this.currentState.taskID === taskID && this.currentState.connectionState === "Generating" /* generating */) {
9134
- return onFinish();
10449
+ void this.recordingController.stop("generationEnded" /* generationEnded */);
10450
+ return onFinish?.();
9135
10451
  }
9136
- }));
10452
+ });
9137
10453
  }
9138
10454
  return this.connectionManager.makeRemoteStream();
9139
10455
  }
@@ -9151,7 +10467,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
9151
10467
  * 统一清理顺序:停止心跳与生成,再断开连接;all 额外释放本地媒体。
9152
10468
  */
9153
10469
  async performCleanup(scope, taskID) {
9154
- this.localVideoController.pause();
10470
+ const recordingFinished = this.recordingController.stop("generationEnded" /* generationEnded */);
10471
+ this.mediaController.pauseLocalVideo();
9155
10472
  this.launchTimer.cancel();
9156
10473
  this.remoteFrameDisplayHandler = void 0;
9157
10474
  this.clearVideoStatistics();
@@ -9161,27 +10478,17 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
9161
10478
  this.connectionManager.stopHeartbeat();
9162
10479
  await this.generationManager.reset(taskID);
9163
10480
  const sessionID = await this.connectionManager.disconnect();
10481
+ await recordingFinished;
9164
10482
  if (scope === 1 /* all */) {
9165
- const hadExternalSource = this.networkVideoController.currentTrack !== void 0 || this.localVideoController.isActive || this.externalVideoController.currentTrack !== void 0;
9166
- this.networkVideoController.stop();
9167
10483
  this.generationVideoFormat = void 0;
9168
10484
  this.disposePermissionWatch();
9169
10485
  try {
9170
- if (hadExternalSource) {
9171
- await this.rtcManager.destroy();
9172
- } else {
9173
- await this.cameraController.stopLocalCameraStream();
9174
- }
10486
+ await this.mediaController.stopLocalStream();
9175
10487
  } catch (error) {
9176
10488
  XmaxLogger.realtime.error(
9177
10489
  () => `\u91CA\u653E\u672C\u5730\u5A92\u4F53\u5931\u8D25 (Failed to Release Local Media)
9178
10490
  \u2514\u2500 ${XmaxLogger.localized("\u539F\u56E0\uFF1A", "Reason: ")}${XmaxError.from(error).message}`
9179
10491
  );
9180
- } finally {
9181
- this.externalVideoController.stop();
9182
- if (this.localVideoController.isActive) {
9183
- await this.localVideoController.stop();
9184
- }
9185
10492
  }
9186
10493
  }
9187
10494
  return {
@@ -9759,13 +11066,6 @@ var RealtimeConfiguration = class {
9759
11066
  }
9760
11067
  };
9761
11068
 
9762
- // src/Foundation/Media/Video/VideoContentMode.ts
9763
- var VideoContentMode = /* @__PURE__ */ ((VideoContentMode2) => {
9764
- VideoContentMode2["fit"] = "fit";
9765
- VideoContentMode2["fill"] = "fill";
9766
- return VideoContentMode2;
9767
- })(VideoContentMode || {});
9768
-
9769
11069
  // src/Render/Video/RenderStatisticsController.ts
9770
11070
  var RenderStatisticsController = class {
9771
11071
  /**
@@ -12275,6 +13575,498 @@ var RemoteVideoFramePipeline = class {
12275
13575
  }
12276
13576
  };
12277
13577
 
13578
+ // src/Render/Trajectory/TrajectoryOverlayView.ts
13579
+ var TrajectoryOverlayView = class {
13580
+ /**
13581
+ * 安装透明交互层及独立 Renderer;只有任务就绪时接管指针。
13582
+ */
13583
+ constructor(renderer) {
13584
+ __publicField(this, "renderer", renderer);
13585
+ /**
13586
+ * 视图与显示配置
13587
+ */
13588
+ __publicField(this, "element", document.createElement("div"));
13589
+ __publicField(this, "binding");
13590
+ __publicField(this, "format");
13591
+ __publicField(this, "contentMode", "fill" /* fill */);
13592
+ __publicField(this, "mirrored", false);
13593
+ __publicField(this, "requested", true);
13594
+ __publicField(this, "enabled", false);
13595
+ /**
13596
+ * 手势采样与生命周期资源
13597
+ */
13598
+ __publicField(this, "pointers", /* @__PURE__ */ new Map());
13599
+ __publicField(this, "nextID", 0);
13600
+ __publicField(this, "animation");
13601
+ __publicField(this, "lastSample", 0);
13602
+ __publicField(this, "resizeObserver");
13603
+ __publicField(this, "viewport");
13604
+ __publicField(this, "visibilityChanged", () => {
13605
+ if (document.hidden) {
13606
+ this.cancel();
13607
+ }
13608
+ this.updateStyle();
13609
+ });
13610
+ __publicField(this, "onDown", (event) => this.begin(event));
13611
+ __publicField(this, "onMove", (event) => this.move(event));
13612
+ __publicField(this, "onEnd", (event) => this.end(event.pointerId));
13613
+ Object.assign(this.element.style, { position: "absolute", inset: "0", zIndex: "2", overflow: "hidden" });
13614
+ Object.assign(renderer.element.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
13615
+ this.element.appendChild(renderer.element);
13616
+ this.element.addEventListener("pointerdown", this.onDown);
13617
+ this.element.addEventListener("pointermove", this.onMove);
13618
+ this.element.addEventListener("pointerup", this.onEnd);
13619
+ this.element.addEventListener("pointercancel", this.onEnd);
13620
+ this.element.addEventListener("lostpointercapture", this.onEnd);
13621
+ document.addEventListener("visibilitychange", this.visibilityChanged);
13622
+ if (typeof ResizeObserver !== "undefined") {
13623
+ this.resizeObserver = new ResizeObserver(() => this.cancel());
13624
+ this.resizeObserver.observe(this.element);
13625
+ }
13626
+ this.updateStyle();
13627
+ }
13628
+ /**
13629
+ * 更新任务绑定,清除旧任务手势和采样。
13630
+ */
13631
+ setBinding(binding, format, enabled = false) {
13632
+ this.cancel();
13633
+ this.binding = binding;
13634
+ this.format = format;
13635
+ this.enabled = enabled;
13636
+ this.updateStyle();
13637
+ }
13638
+ /**
13639
+ * 同步显示配置;配置变化后旧坐标不再继续发送。
13640
+ */
13641
+ configure(mode, mirrored, enabled) {
13642
+ if (this.contentMode !== mode || this.mirrored !== mirrored || this.requested !== enabled) {
13643
+ this.cancel();
13644
+ }
13645
+ this.contentMode = mode;
13646
+ this.mirrored = mirrored;
13647
+ this.requested = enabled;
13648
+ this.updateStyle();
13649
+ }
13650
+ /**
13651
+ * 卸载时取消采样、动画、捕获和事件监听;Renderer 可以再次挂载。
13652
+ */
13653
+ dispose() {
13654
+ this.binding?.detach(this);
13655
+ this.cancel();
13656
+ this.resizeObserver?.disconnect();
13657
+ document.removeEventListener("visibilitychange", this.visibilityChanged);
13658
+ this.element.removeEventListener("pointerdown", this.onDown);
13659
+ this.element.removeEventListener("pointermove", this.onMove);
13660
+ this.element.removeEventListener("pointerup", this.onEnd);
13661
+ this.element.removeEventListener("pointercancel", this.onEnd);
13662
+ this.element.removeEventListener("lostpointercapture", this.onEnd);
13663
+ this.element.remove();
13664
+ }
13665
+ /**
13666
+ * 无任务或被禁用时不阻止页面滚动。
13667
+ */
13668
+ updateStyle() {
13669
+ const active = this.requested && this.enabled && !!this.binding && !document.hidden;
13670
+ this.element.style.pointerEvents = active ? "auto" : "none";
13671
+ this.element.style.touchAction = active ? "none" : "auto";
13672
+ this.element.style.userSelect = active ? "none" : "auto";
13673
+ }
13674
+ /**
13675
+ * 根据实际显示区域拒绝黑边,移动到视口外时限制在可见视频边缘。
13676
+ */
13677
+ point(event, id, clamp) {
13678
+ const viewport = this.element.getBoundingClientRect();
13679
+ if (!this.format) {
13680
+ return void 0;
13681
+ }
13682
+ const rect = InteractionCoordinateMapper.displayedFrame(viewport, this.format, this.contentMode);
13683
+ if (!rect || !Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) {
13684
+ return void 0;
13685
+ }
13686
+ const minX = Math.max(0, rect.x);
13687
+ const minY = Math.max(0, rect.y);
13688
+ const maxX = Math.min(viewport.width, rect.x + rect.width);
13689
+ const maxY = Math.min(viewport.height, rect.y + rect.height);
13690
+ let x = event.clientX - viewport.left;
13691
+ let y = event.clientY - viewport.top;
13692
+ if (!clamp && (x < minX || x > maxX || y < minY || y > maxY)) {
13693
+ return void 0;
13694
+ }
13695
+ x = Math.max(minX, Math.min(maxX, x));
13696
+ y = Math.max(minY, Math.min(maxY, y));
13697
+ return Object.freeze({
13698
+ id,
13699
+ location: Object.freeze({ x, y }),
13700
+ normalizedLocation: Object.freeze({ x: (x - rect.x) / rect.width, y: (y - rect.y) / rect.height }),
13701
+ timestamp: performance.now()
13702
+ });
13703
+ }
13704
+ /**
13705
+ * 按下立即采样,不等待定时窗口;只接受鼠标主键。
13706
+ */
13707
+ begin(event) {
13708
+ if (!this.requested || !this.enabled || !this.binding || document.hidden || event.button !== 0) {
13709
+ return;
13710
+ }
13711
+ const point = this.point(event, ++this.nextID, false);
13712
+ if (!point) {
13713
+ return;
13714
+ }
13715
+ event.preventDefault();
13716
+ this.pointers.set(event.pointerId, point);
13717
+ try {
13718
+ this.element.setPointerCapture(event.pointerId);
13719
+ } catch {
13720
+ }
13721
+ this.render(() => this.renderer.renderBegan([point]));
13722
+ this.sample(performance.now(), true);
13723
+ this.schedule();
13724
+ }
13725
+ /**
13726
+ * 高频指针事件只更新最新位置及视觉效果,不直接发送网络消息。
13727
+ */
13728
+ move(event) {
13729
+ const previous = this.pointers.get(event.pointerId);
13730
+ if (!previous) {
13731
+ return;
13732
+ }
13733
+ const point = this.point(event, previous.id, true);
13734
+ if (point) {
13735
+ this.pointers.set(event.pointerId, point);
13736
+ this.render(() => this.renderer.renderMoved([point]));
13737
+ }
13738
+ }
13739
+ /**
13740
+ * 释放单个指针,多指交互继续;最后一个指针释放后停止采样。
13741
+ */
13742
+ end(pointerID) {
13743
+ const point = this.pointers.get(pointerID);
13744
+ if (!point) {
13745
+ return;
13746
+ }
13747
+ this.pointers.delete(pointerID);
13748
+ try {
13749
+ this.element.releasePointerCapture(pointerID);
13750
+ } catch {
13751
+ }
13752
+ this.render(() => this.renderer.renderEnded([point.id]));
13753
+ if (!this.pointers.size) {
13754
+ this.stopSampling();
13755
+ }
13756
+ }
13757
+ /**
13758
+ * 取消全部手势及视觉效果,不保留跨生命周期轨迹。
13759
+ */
13760
+ cancel() {
13761
+ for (const id of [...this.pointers.keys()]) {
13762
+ this.end(id);
13763
+ }
13764
+ this.stopSampling();
13765
+ this.render(() => this.renderer.reset());
13766
+ }
13767
+ /**
13768
+ * 清理采样回调与视口快照。
13769
+ */
13770
+ stopSampling() {
13771
+ if (this.animation !== void 0) {
13772
+ cancelAnimationFrame(this.animation);
13773
+ }
13774
+ this.animation = void 0;
13775
+ this.viewport = void 0;
13776
+ this.lastSample = 0;
13777
+ }
13778
+ /**
13779
+ * 只在存在活动触点时运行动画时钟。
13780
+ */
13781
+ schedule() {
13782
+ if (this.animation === void 0 && this.pointers.size) {
13783
+ this.animation = requestAnimationFrame((now) => {
13784
+ this.animation = void 0;
13785
+ this.sample(now);
13786
+ this.schedule();
13787
+ });
13788
+ }
13789
+ }
13790
+ /**
13791
+ * 固定频率发送所有活动触点的快照,布局变化时取消而非发送旧坐标。
13792
+ */
13793
+ sample(now, force = false) {
13794
+ if (document.hidden) {
13795
+ this.cancel();
13796
+ return;
13797
+ }
13798
+ const { width, height } = this.element.getBoundingClientRect();
13799
+ if (this.viewport && (width !== this.viewport.width || height !== this.viewport.height)) {
13800
+ this.cancel();
13801
+ return;
13802
+ }
13803
+ this.viewport = { width, height };
13804
+ if (!force && now - this.lastSample < 1e3 / 30) {
13805
+ return;
13806
+ }
13807
+ this.lastSample = now;
13808
+ this.binding?.submit({
13809
+ points: [...this.pointers.values()].map((point) => point.location),
13810
+ viewportSize: this.viewport,
13811
+ contentMode: this.contentMode,
13812
+ mirrored: this.mirrored
13813
+ });
13814
+ }
13815
+ /**
13816
+ * 自定义效果异常不影响触点采集和网络发送。
13817
+ */
13818
+ render(action) {
13819
+ try {
13820
+ action();
13821
+ } catch {
13822
+ XmaxLogger.render.warning(() => "\u8F68\u8FF9\u6548\u679C\u7ED8\u5236\u5931\u8D25 (Trajectory Rendering Failed)");
13823
+ }
13824
+ }
13825
+ };
13826
+
13827
+ // src/Render/Trajectory/DefaultTrajectoryEffectRenderer.ts
13828
+ var VISUAL = {
13829
+ outerWidth: 18,
13830
+ outerBlur: 12,
13831
+ outerAlpha: 0.35,
13832
+ middleWidth: 10,
13833
+ middleBlur: 6,
13834
+ middleAlpha: 0.75,
13835
+ coreWidth: 3,
13836
+ headRadius: 5,
13837
+ headGlowRadius: 14,
13838
+ headGlowAlpha: 0.9,
13839
+ ringCount: 2,
13840
+ ringStartRadius: 10,
13841
+ ringSpacing: 14,
13842
+ ringLineWidth: 2,
13843
+ ringPulseSpeed: 1.2,
13844
+ ringMaxAlpha: 0.6,
13845
+ particleCount: 4,
13846
+ particleRadius: 3,
13847
+ particleOrbitRadius: 22,
13848
+ particleSpeed: 0.06,
13849
+ particleAlpha: 0.6,
13850
+ fadeAlpha: 0.05,
13851
+ idleFadeFrames: 64
13852
+ };
13853
+ var DefaultTrajectoryEffectRenderer = class {
13854
+ /**
13855
+ * 安装独立视觉图层,不注册指针事件。
13856
+ */
13857
+ constructor() {
13858
+ /**
13859
+ * 画布与动画资源;尾迹保留像素渐隐,触点装饰每帧重绘。
13860
+ */
13861
+ __publicField(this, "element", document.createElement("div"));
13862
+ __publicField(this, "trailCanvas", document.createElement("canvas"));
13863
+ __publicField(this, "effectCanvas", document.createElement("canvas"));
13864
+ __publicField(this, "animation");
13865
+ __publicField(this, "hasTrailPixels", false);
13866
+ __publicField(this, "idleFadeFrames", 0);
13867
+ /**
13868
+ * 活动触点和等待下一帧绘制的线段。
13869
+ */
13870
+ __publicField(this, "pointers", /* @__PURE__ */ new Map());
13871
+ __publicField(this, "segments", []);
13872
+ for (const canvas of [this.trailCanvas, this.effectCanvas]) {
13873
+ Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
13874
+ this.element.appendChild(canvas);
13875
+ }
13876
+ }
13877
+ /**
13878
+ * 开始触点装饰,脉冲动画以本次按下时间为起点。
13879
+ */
13880
+ renderBegan(points) {
13881
+ for (const point of points) {
13882
+ this.pointers.set(point.id, { point, startedAt: point.timestamp });
13883
+ }
13884
+ this.idleFadeFrames = 0;
13885
+ this.schedule();
13886
+ }
13887
+ /**
13888
+ * 只绘制新线段,旧像素自然衰减,不反复加亮整条历史轨迹。
13889
+ */
13890
+ renderMoved(points) {
13891
+ for (const point of points) {
13892
+ const pointer = this.pointers.get(point.id);
13893
+ if (pointer) {
13894
+ this.segments.push({ from: pointer.point, to: point });
13895
+ pointer.point = point;
13896
+ }
13897
+ }
13898
+ this.schedule();
13899
+ }
13900
+ /**
13901
+ * 释放触点后移除装饰,仅保留尾迹的渐隐动画。
13902
+ */
13903
+ renderEnded(identifiers) {
13904
+ for (const id of identifiers) {
13905
+ this.pointers.delete(id);
13906
+ }
13907
+ this.schedule();
13908
+ }
13909
+ /**
13910
+ * 清除画面、待绘制数据及动画,允许同一 Renderer 再次挂载。
13911
+ */
13912
+ reset() {
13913
+ if (this.animation !== void 0) {
13914
+ cancelAnimationFrame(this.animation);
13915
+ }
13916
+ this.animation = void 0;
13917
+ this.pointers.clear();
13918
+ this.segments.length = 0;
13919
+ this.hasTrailPixels = false;
13920
+ this.idleFadeFrames = 0;
13921
+ for (const canvas of [this.trailCanvas, this.effectCanvas]) {
13922
+ canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
13923
+ }
13924
+ }
13925
+ /**
13926
+ * 仅在触点、待绘制线段或残留尾迹存在时申请下一帧。
13927
+ */
13928
+ schedule() {
13929
+ if (this.animation === void 0 && (this.pointers.size || this.segments.length || this.hasTrailPixels)) {
13930
+ this.animation = requestAnimationFrame((now) => this.draw(now));
13931
+ }
13932
+ }
13933
+ /**
13934
+ * 同步画布分辨率,分别更新尾迹和触点视觉图层。
13935
+ */
13936
+ draw(now) {
13937
+ this.animation = void 0;
13938
+ const trail = this.trailCanvas.getContext("2d");
13939
+ const effects = this.effectCanvas.getContext("2d");
13940
+ if (!trail || !effects) {
13941
+ this.reset();
13942
+ return;
13943
+ }
13944
+ const rect = this.element.getBoundingClientRect();
13945
+ const dpr = globalThis.devicePixelRatio || 1;
13946
+ const width = Math.max(1, Math.round(rect.width * dpr));
13947
+ const height = Math.max(1, Math.round(rect.height * dpr));
13948
+ for (const canvas of [this.trailCanvas, this.effectCanvas]) {
13949
+ if (canvas.width !== width || canvas.height !== height) {
13950
+ canvas.width = width;
13951
+ canvas.height = height;
13952
+ this.hasTrailPixels = false;
13953
+ }
13954
+ }
13955
+ if (this.hasTrailPixels) {
13956
+ trail.save();
13957
+ trail.globalCompositeOperation = "destination-out";
13958
+ trail.fillStyle = "rgba(0,0,0," + VISUAL.fadeAlpha + ")";
13959
+ trail.fillRect(0, 0, width, height);
13960
+ trail.restore();
13961
+ }
13962
+ if (this.segments.length) {
13963
+ this.drawSegments(trail, dpr);
13964
+ this.segments.length = 0;
13965
+ this.hasTrailPixels = true;
13966
+ this.idleFadeFrames = 0;
13967
+ }
13968
+ effects.clearRect(0, 0, width, height);
13969
+ effects.save();
13970
+ effects.globalCompositeOperation = "lighter";
13971
+ for (const { point, startedAt } of this.pointers.values()) {
13972
+ const x = point.location.x * dpr;
13973
+ const y = point.location.y * dpr;
13974
+ if (this.pointers.size === 1) {
13975
+ this.drawRings(effects, x, y, dpr, (now - startedAt) / 1e3);
13976
+ this.drawParticles(effects, x, y, dpr, (now - startedAt) / 1e3);
13977
+ }
13978
+ this.drawGlow(effects, x, y, VISUAL.headGlowRadius * dpr, VISUAL.headGlowAlpha, true);
13979
+ effects.fillStyle = "rgba(255,255,255,1)";
13980
+ effects.beginPath();
13981
+ effects.arc(x, y, VISUAL.headRadius * dpr, 0, Math.PI * 2);
13982
+ effects.fill();
13983
+ }
13984
+ effects.restore();
13985
+ if (this.pointers.size) {
13986
+ this.idleFadeFrames = 0;
13987
+ } else if (this.hasTrailPixels && ++this.idleFadeFrames > VISUAL.idleFadeFrames) {
13988
+ trail.clearRect(0, 0, width, height);
13989
+ this.hasTrailPixels = false;
13990
+ this.idleFadeFrames = 0;
13991
+ }
13992
+ this.schedule();
13993
+ }
13994
+ /**
13995
+ * 叠加绿色外晕、中层和白色线芯;多指时省略阴影降低绘制开销。
13996
+ */
13997
+ drawSegments(context, dpr) {
13998
+ const layers = [
13999
+ { width: VISUAL.outerWidth, blur: VISUAL.outerBlur, color: this.green(VISUAL.outerAlpha) },
14000
+ { width: VISUAL.middleWidth, blur: VISUAL.middleBlur, color: this.green(VISUAL.middleAlpha) },
14001
+ { width: VISUAL.coreWidth, blur: 0, color: "rgba(255,255,255,1)" }
14002
+ ];
14003
+ for (const layer of layers) {
14004
+ context.save();
14005
+ context.globalCompositeOperation = "lighter";
14006
+ context.lineCap = "round";
14007
+ context.lineJoin = "round";
14008
+ context.lineWidth = layer.width * dpr;
14009
+ context.strokeStyle = layer.color;
14010
+ context.shadowBlur = this.pointers.size > 1 ? 0 : layer.blur * dpr;
14011
+ context.shadowColor = context.shadowBlur ? layer.color : "transparent";
14012
+ context.beginPath();
14013
+ for (const { from, to } of this.segments) {
14014
+ context.moveTo(from.location.x * dpr, from.location.y * dpr);
14015
+ context.lineTo(to.location.x * dpr, to.location.y * dpr);
14016
+ }
14017
+ context.stroke();
14018
+ context.restore();
14019
+ }
14020
+ }
14021
+ /**
14022
+ * 绘制触点周围两层不同相位的脉冲圆环。
14023
+ */
14024
+ drawRings(context, x, y, dpr, elapsed) {
14025
+ context.lineWidth = VISUAL.ringLineWidth * dpr;
14026
+ for (let index = 0; index < VISUAL.ringCount; index++) {
14027
+ const pulse = (Math.sin((elapsed * VISUAL.ringPulseSpeed + index * 0.5) * Math.PI * 2) + 1) / 2;
14028
+ const radius = (VISUAL.ringStartRadius + index * VISUAL.ringSpacing + pulse * 6) * dpr;
14029
+ context.strokeStyle = this.green(VISUAL.ringMaxAlpha * (1 - index * 0.2) * (0.5 + pulse * 0.5));
14030
+ context.beginPath();
14031
+ context.arc(x, y, radius, 0, Math.PI * 2);
14032
+ context.stroke();
14033
+ }
14034
+ }
14035
+ /**
14036
+ * 绘制四个反向交错运动的柔和环绕粒子。
14037
+ */
14038
+ drawParticles(context, x, y, dpr, elapsed) {
14039
+ for (let index = 0; index < VISUAL.particleCount; index++) {
14040
+ const direction = index % 2 === 0 ? 1 : -1;
14041
+ const angle = index / VISUAL.particleCount * Math.PI * 2 + elapsed * VISUAL.particleSpeed * direction;
14042
+ const radius = VISUAL.particleOrbitRadius * dpr;
14043
+ const alpha = VISUAL.particleAlpha * (0.6 + Math.sin(elapsed * 3 + index) * 0.4);
14044
+ this.drawGlow(context, x + Math.cos(angle) * radius, y + Math.sin(angle) * radius, VISUAL.particleRadius * 2 * dpr, alpha);
14045
+ }
14046
+ }
14047
+ /**
14048
+ * 绘制径向绿色光晕,触点核心增加中间色阶。
14049
+ */
14050
+ drawGlow(context, x, y, radius, alpha, head = false) {
14051
+ const gradient = context.createRadialGradient(x, y, 0, x, y, radius);
14052
+ gradient.addColorStop(0, this.green(alpha));
14053
+ if (head) {
14054
+ gradient.addColorStop(0.4, this.green(alpha * 0.6));
14055
+ }
14056
+ gradient.addColorStop(1, this.green(0));
14057
+ context.fillStyle = gradient;
14058
+ context.beginPath();
14059
+ context.arc(x, y, radius, 0, Math.PI * 2);
14060
+ context.fill();
14061
+ }
14062
+ /**
14063
+ * 统一轨迹主题色,透明度由各视觉层指定。
14064
+ */
14065
+ green(alpha) {
14066
+ return "rgba(0,255,100," + alpha + ")";
14067
+ }
14068
+ };
14069
+
12278
14070
  // src/Render/Video/XmaxVideoView.ts
12279
14071
  var OBJECT_FIT = {
12280
14072
  ["fit" /* fit */]: "contain",
@@ -12332,7 +14124,14 @@ var XmaxVideoView = class {
12332
14124
  * DOM 挂载状态
12333
14125
  */
12334
14126
  __publicField(this, "detached", false);
14127
+ /**
14128
+ * 轨迹交互配置与懒加载视图
14129
+ */
14130
+ __publicField(this, "interactionEnabled", true);
14131
+ __publicField(this, "customTrajectoryRenderer");
14132
+ __publicField(this, "trajectoryOverlay");
12335
14133
  this.contentMode = options?.videoContentMode ?? "fill" /* fill */;
14134
+ this.interactionEnabled = options?.isInteractionEnabled ?? true;
12336
14135
  this.element = document.createElement("div");
12337
14136
  this.element.style.position = "relative";
12338
14137
  this.element.style.overflow = "hidden";
@@ -12349,6 +14148,36 @@ var XmaxVideoView = class {
12349
14148
  this.applyContentMode();
12350
14149
  this.element.appendChild(this.videoElement);
12351
14150
  }
14151
+ /**
14152
+ * 是否允许远端轨迹交互;本地轨道没有交互绑定,不受此开关影响。
14153
+ */
14154
+ get isInteractionEnabled() {
14155
+ return this.interactionEnabled;
14156
+ }
14157
+ /**
14158
+ * 关闭时立即取消当前手势,不影响视频播放。
14159
+ */
14160
+ set isInteractionEnabled(enabled) {
14161
+ this.interactionEnabled = enabled;
14162
+ this.trajectoryOverlay?.configure(this.contentMode, this.mirrored, enabled);
14163
+ }
14164
+ /**
14165
+ * 自定义轨迹视觉效果;undefined 表示使用 SDK 默认效果。
14166
+ */
14167
+ get trajectoryRenderer() {
14168
+ return this.customTrajectoryRenderer;
14169
+ }
14170
+ /**
14171
+ * 替换效果前释放旧动画,采样及网络发送仍由 SDK 管理。
14172
+ */
14173
+ set trajectoryRenderer(renderer) {
14174
+ if (this.customTrajectoryRenderer === renderer) {
14175
+ return;
14176
+ }
14177
+ this.disposeTrajectory();
14178
+ this.customTrajectoryRenderer = renderer;
14179
+ this.attachTrajectory();
14180
+ }
12352
14181
  /**
12353
14182
  * 当前显示的视频轨道;切换时自动解绑旧轨道。
12354
14183
  */
@@ -12421,6 +14250,7 @@ var XmaxVideoView = class {
12421
14250
  attach(to) {
12422
14251
  this.detached = false;
12423
14252
  to.appendChild(this.element);
14253
+ this.attachTrajectory();
12424
14254
  if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && this.videoElement.autoplay && !this.videoElement.ended && this.videoElement.paused && typeof this.videoElement.play === "function") {
12425
14255
  void this.videoElement.play().catch((error) => {
12426
14256
  XmaxLogger.render.warning(() => `\u6062\u590D\u89C6\u9891\u64AD\u653E\u5931\u8D25 (Failed to Resume Video Playback)
@@ -12438,6 +14268,7 @@ var XmaxVideoView = class {
12438
14268
  */
12439
14269
  detach() {
12440
14270
  this.detached = true;
14271
+ this.disposeTrajectory();
12441
14272
  this.statisticsController.stop();
12442
14273
  this.stopInterpolation();
12443
14274
  this.cancelFrameDisplayNotification();
@@ -12544,6 +14375,7 @@ var XmaxVideoView = class {
12544
14375
  const binding = VideoRenderRegistry.binding(track);
12545
14376
  if (binding) {
12546
14377
  binding.attachHandler(this, this.contentMode);
14378
+ this.attachTrajectory();
12547
14379
  return;
12548
14380
  }
12549
14381
  if (track.mediaStreamTrack) {
@@ -12558,6 +14390,7 @@ var XmaxVideoView = class {
12558
14390
  * 解绑当前轨道并清空渲染内容。
12559
14391
  */
12560
14392
  detachCurrentTrack() {
14393
+ this.disposeTrajectory();
12561
14394
  this.statisticsController.stop();
12562
14395
  const track = this.currentTrack;
12563
14396
  if (track) {
@@ -12617,6 +14450,7 @@ var XmaxVideoView = class {
12617
14450
  * 应用显示模式与镜像样式。
12618
14451
  */
12619
14452
  applyContentMode() {
14453
+ this.trajectoryOverlay?.configure(this.contentMode, this.mirrored, this.interactionEnabled);
12620
14454
  this.videoElement.style.objectFit = OBJECT_FIT[this.contentMode];
12621
14455
  this.videoElement.style.transform = this.mirrored ? "scaleX(-1)" : "";
12622
14456
  if (this.interpolationCanvas) {
@@ -12630,6 +14464,30 @@ var XmaxVideoView = class {
12630
14464
  startRenderStatistics() {
12631
14465
  if (!this.detached) this.statisticsController.start();
12632
14466
  }
14467
+ /**
14468
+ * 仅为具备交互能力的轨道创建覆盖层,普通视频和本地预览不分配 Canvas。
14469
+ */
14470
+ attachTrajectory() {
14471
+ if (this.detached || this.trajectoryOverlay || !this.currentTrack) {
14472
+ return;
14473
+ }
14474
+ const binding = TrajectoryRegistry.binding(this.currentTrack);
14475
+ if (!binding) {
14476
+ return;
14477
+ }
14478
+ const overlay = new TrajectoryOverlayView(this.customTrajectoryRenderer ?? new DefaultTrajectoryEffectRenderer());
14479
+ this.trajectoryOverlay = overlay;
14480
+ overlay.configure(this.contentMode, this.mirrored, this.interactionEnabled);
14481
+ this.element.appendChild(overlay.element);
14482
+ binding.attach(overlay);
14483
+ }
14484
+ /**
14485
+ * 换轨或卸载时移除全部交互资源;再次挂载时重新建立绑定。
14486
+ */
14487
+ disposeTrajectory() {
14488
+ this.trajectoryOverlay?.dispose();
14489
+ this.trajectoryOverlay = void 0;
14490
+ }
12633
14491
  };
12634
14492
 
12635
14493
  // src/Render/Video/XmaxRealtimeVideoView.ts
@@ -12668,8 +14526,8 @@ var XmaxRealtimeVideoView = class {
12668
14526
  this.element.style.position = "relative";
12669
14527
  this.element.style.overflow = "hidden";
12670
14528
  this.element.style.backgroundColor = "black";
12671
- this.localVideoView = new XmaxVideoView({ videoContentMode: this.contentMode });
12672
- this.remoteVideoView = new XmaxVideoView({ videoContentMode: this.contentMode });
14529
+ this.localVideoView = new XmaxVideoView({ videoContentMode: this.contentMode, isInteractionEnabled: false });
14530
+ this.remoteVideoView = new XmaxVideoView({ videoContentMode: this.contentMode, isInteractionEnabled: options?.isInteractionEnabled });
12673
14531
  for (const view of [this.localVideoView, this.remoteVideoView]) {
12674
14532
  view.element.style.position = "absolute";
12675
14533
  view.element.style.inset = "0";
@@ -12684,6 +14542,30 @@ var XmaxRealtimeVideoView = class {
12684
14542
  get localTrack() {
12685
14543
  return this.currentLocalTrack;
12686
14544
  }
14545
+ /**
14546
+ * 只控制远端视图的轨迹交互,本地预览始终不接管手势。
14547
+ */
14548
+ get isInteractionEnabled() {
14549
+ return this.remoteVideoView.isInteractionEnabled;
14550
+ }
14551
+ /**
14552
+ * 转交远端交互开关。
14553
+ */
14554
+ set isInteractionEnabled(enabled) {
14555
+ this.remoteVideoView.isInteractionEnabled = enabled;
14556
+ }
14557
+ /**
14558
+ * 远端视图的自定义轨迹效果。
14559
+ */
14560
+ get trajectoryRenderer() {
14561
+ return this.remoteVideoView.trajectoryRenderer;
14562
+ }
14563
+ /**
14564
+ * undefined 恢复默认效果,不改变轨迹发送逻辑。
14565
+ */
14566
+ set trajectoryRenderer(renderer) {
14567
+ this.remoteVideoView.trajectoryRenderer = renderer;
14568
+ }
12687
14569
  /**
12688
14570
  * 替换本地预览轨道;轨道未变化时保留当前绑定。
12689
14571
  */
@@ -12815,6 +14697,9 @@ export {
12815
14697
  RealtimeVideoTrack,
12816
14698
  RealtimeMediaStream,
12817
14699
  RealtimeVideoSampleMethod,
14700
+ VideoContentMode,
14701
+ RealtimeRecordingEventType,
14702
+ RealtimeRecordingFinishReason,
12818
14703
  RealtimeContext,
12819
14704
  RealtimeConnectionState,
12820
14705
  RealtimeReason,
@@ -12825,8 +14710,7 @@ export {
12825
14710
  NonRealtimeTaskError,
12826
14711
  XmaxClient,
12827
14712
  RealtimeConfiguration,
12828
- VideoContentMode,
12829
14713
  XmaxVideoView,
12830
14714
  XmaxRealtimeVideoView
12831
14715
  };
12832
- //# sourceMappingURL=chunk-JGDZ3RD3.js.map
14716
+ //# sourceMappingURL=chunk-ILZL4W2G.js.map