@xmaxai/web-sdk 1.0.7 → 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.
- package/README.md +117 -18
- package/dist/RecordingProcessor-4TP5NICO.cjs +211 -0
- package/dist/RecordingProcessor-4TP5NICO.cjs.map +1 -0
- package/dist/RecordingProcessor-ZSBJFF4J.js +211 -0
- package/dist/RecordingProcessor-ZSBJFF4J.js.map +1 -0
- package/dist/{VideoRenderStatistics-C7zqzfuS.d.cts → TrajectoryEffectRendering-C7E2KJbV.d.cts} +48 -1
- package/dist/{VideoRenderStatistics-C7zqzfuS.d.ts → TrajectoryEffectRendering-C7E2KJbV.d.ts} +48 -1
- package/dist/chunk-4DE2IREA.cjs +8 -0
- package/dist/chunk-4DE2IREA.cjs.map +1 -0
- package/dist/{chunk-4O5BSMUX.js → chunk-ILZL4W2G.js} +2438 -425
- package/dist/chunk-ILZL4W2G.js.map +1 -0
- package/dist/{chunk-EKOYOFES.cjs → chunk-KROY2O4A.cjs} +2999 -986
- package/dist/chunk-KROY2O4A.cjs.map +1 -0
- package/dist/chunk-V6TY7KAL.js +8 -0
- package/dist/chunk-V6TY7KAL.js.map +1 -0
- package/dist/index.cjs +7 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +197 -43
- package/dist/index.d.ts +197 -43
- package/dist/index.js +6 -1
- package/dist/react/index.cjs +25 -4
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +17 -1
- package/dist/react/index.d.ts +17 -1
- package/dist/react/index.js +22 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-4O5BSMUX.js.map +0 -1
- package/dist/chunk-EKOYOFES.cjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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.
|
|
208
|
+
var XMAX_SDK_VERSION = "1.0.9";
|
|
209
209
|
var _RuntimeInfo = class _RuntimeInfo {
|
|
210
210
|
/**
|
|
211
211
|
* 序列化为接口传输使用的 snake_case 字段。
|
|
@@ -601,6 +601,9 @@ var RealtimeModel = /* @__PURE__ */ ((RealtimeModel2) => {
|
|
|
601
601
|
RealtimeModel2["x2_0"] = "x2.0";
|
|
602
602
|
RealtimeModel2["x2_0_trtc"] = "x2.0-trtc";
|
|
603
603
|
RealtimeModel2["x2_1_preview"] = "x2.1-preview";
|
|
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";
|
|
604
607
|
return RealtimeModel2;
|
|
605
608
|
})(RealtimeModel || {});
|
|
606
609
|
function isKnownModel(model) {
|
|
@@ -615,10 +618,16 @@ function modelDefaults(model) {
|
|
|
615
618
|
var MODEL_DISPLAY_NAMES = {
|
|
616
619
|
["x2.0" /* x2_0 */]: "X2.0",
|
|
617
620
|
["x2.0-trtc" /* x2_0_trtc */]: "X2.1-preview-trtc",
|
|
618
|
-
["x2.1-preview" /* x2_1_preview */]: "X2.1-preview-agora"
|
|
621
|
+
["x2.1-preview" /* x2_1_preview */]: "X2.1-preview-agora",
|
|
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"
|
|
619
625
|
};
|
|
620
626
|
var MODEL_BASE_URLS = {
|
|
621
|
-
["x2.1-preview" /* x2_1_preview */]: "https://cloud.xmax.22duck.cn/open/api/v1"
|
|
627
|
+
["x2.1-preview" /* x2_1_preview */]: "https://cloud.xmax.22duck.cn/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"
|
|
622
631
|
};
|
|
623
632
|
var RESOLUTION_BUCKETS = {
|
|
624
633
|
["x2.0" /* x2_0 */]: [],
|
|
@@ -629,12 +638,27 @@ var RESOLUTION_BUCKETS = {
|
|
|
629
638
|
["x2.1-preview" /* x2_1_preview */]: [
|
|
630
639
|
{ width: 1024, height: 1920 },
|
|
631
640
|
{ width: 1920, height: 1024 }
|
|
641
|
+
],
|
|
642
|
+
["x2.1-preview-1014" /* x2_1_preview_1014 */]: [
|
|
643
|
+
{ width: 1024, height: 1920 },
|
|
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 }
|
|
632
653
|
]
|
|
633
654
|
};
|
|
634
655
|
var MAXIMUM_INPUT_PIXELS = {
|
|
635
656
|
["x2.0" /* x2_0 */]: 128e4,
|
|
636
657
|
["x2.0-trtc" /* x2_0_trtc */]: 21e5,
|
|
637
|
-
["x2.1-preview" /* x2_1_preview */]: 21e5
|
|
658
|
+
["x2.1-preview" /* x2_1_preview */]: 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
|
|
638
662
|
};
|
|
639
663
|
function modelDisplayName(model) {
|
|
640
664
|
return isKnownModel(model) ? MODEL_DISPLAY_NAMES[model] : model;
|
|
@@ -666,7 +690,10 @@ function defaultCameraVideoFormat(model) {
|
|
|
666
690
|
var MODEL_RTC_PROVIDERS = {
|
|
667
691
|
["x2.0" /* x2_0 */]: Object.freeze(["vertc" /* vertc */]),
|
|
668
692
|
["x2.0-trtc" /* x2_0_trtc */]: Object.freeze(["trtc" /* trtc */]),
|
|
669
|
-
["x2.1-preview" /* x2_1_preview */]: Object.freeze(["agora" /* agora */])
|
|
693
|
+
["x2.1-preview" /* x2_1_preview */]: 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 */])
|
|
670
697
|
};
|
|
671
698
|
function supportedRtcProviders(model) {
|
|
672
699
|
return isKnownModel(model) ? MODEL_RTC_PROVIDERS[model] : CUSTOM_MODEL_RTC_PROVIDERS;
|
|
@@ -1353,15 +1380,6 @@ var AgoraVideoSender = class {
|
|
|
1353
1380
|
}
|
|
1354
1381
|
throw new XmaxError("RTC_ERROR" /* rtcError */, "Browser did not retain WebRTC video sender parameters");
|
|
1355
1382
|
}
|
|
1356
|
-
XmaxLogger.rtc.info(() => `Agora \u53D1\u9001\u53C2\u6570\u5B9E\u9A8C (Agora Sender Parameters)
|
|
1357
|
-
${JSON.stringify({
|
|
1358
|
-
capture: { width: capture.width, height: capture.height, frameRate: capture.frameRate },
|
|
1359
|
-
target: { width: config.width, height: config.height, frameRate: config.frameRate },
|
|
1360
|
-
scaleResolutionDownBy: actual.scaleResolutionDownBy,
|
|
1361
|
-
maxFramerate: actual.maxFramerate,
|
|
1362
|
-
maxBitrateKbps: actual.maxBitrate / 1e3,
|
|
1363
|
-
minimumBitrateApplied: false
|
|
1364
|
-
}, null, 2)}`, 2 /* performance */);
|
|
1365
1383
|
}
|
|
1366
1384
|
};
|
|
1367
1385
|
|
|
@@ -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/
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
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 =
|
|
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,101 +4805,1353 @@ var LocalVideoController = class {
|
|
|
4842
4805
|
}
|
|
4843
4806
|
};
|
|
4844
4807
|
|
|
4845
|
-
// src/Service/Realtime/
|
|
4846
|
-
var
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
/**
|
|
4852
|
-
* 服务端直接读取的网络视频。
|
|
4853
|
-
*/
|
|
4854
|
-
__publicField(this, "referenceVideo");
|
|
4855
|
-
/**
|
|
4856
|
-
* 生成条件
|
|
4857
|
-
*/
|
|
4858
|
-
/**
|
|
4859
|
-
* 实时生成使用的文本条件。
|
|
4860
|
-
*/
|
|
4861
|
-
__publicField(this, "prompt");
|
|
4862
|
-
/**
|
|
4863
|
-
* 已上传参考图片的可选远端路径。
|
|
4864
|
-
*/
|
|
4865
|
-
__publicField(this, "referencePath");
|
|
4866
|
-
this.referenceVideo = init.referenceVideo && Object.freeze({ ...init.referenceVideo });
|
|
4867
|
-
this.prompt = init.prompt.trim();
|
|
4868
|
-
const normalizedReferencePath = init.referencePath?.trim();
|
|
4869
|
-
this.referencePath = normalizedReferencePath || void 0;
|
|
4870
|
-
}
|
|
4871
|
-
};
|
|
4808
|
+
// src/Service/Realtime/RealtimeReferenceVideo.ts
|
|
4809
|
+
var RealtimeVideoSampleMethod = /* @__PURE__ */ ((RealtimeVideoSampleMethod2) => {
|
|
4810
|
+
RealtimeVideoSampleMethod2["time"] = "time";
|
|
4811
|
+
RealtimeVideoSampleMethod2["fps"] = "fps";
|
|
4812
|
+
return RealtimeVideoSampleMethod2;
|
|
4813
|
+
})(RealtimeVideoSampleMethod || {});
|
|
4872
4814
|
|
|
4873
|
-
// src/
|
|
4874
|
-
var
|
|
4875
|
-
|
|
4876
|
-
* 创建实时会话。
|
|
4877
|
-
*
|
|
4878
|
-
* @param init.id 会话标识(sessionUid)。
|
|
4879
|
-
* @param init.userID 业务用户标识。
|
|
4880
|
-
* @param init.status 会话状态。
|
|
4881
|
-
* @param init.connection RTC 连接参数。
|
|
4882
|
-
* @param init.closeReason 会话关闭原因。
|
|
4883
|
-
*/
|
|
4884
|
-
constructor(init) {
|
|
4885
|
-
/**
|
|
4886
|
-
* 会话信息
|
|
4887
|
-
*/
|
|
4888
|
-
/**
|
|
4889
|
-
* 会话标识(sessionUid)。
|
|
4890
|
-
*/
|
|
4891
|
-
__publicField(this, "id");
|
|
4892
|
-
/**
|
|
4893
|
-
* 业务用户标识。
|
|
4894
|
-
*/
|
|
4895
|
-
__publicField(this, "userID");
|
|
4896
|
-
/**
|
|
4897
|
-
* 运行状态
|
|
4898
|
-
*/
|
|
4899
|
-
/**
|
|
4900
|
-
* 会话状态(如 ACTIVE);响应未携带时为空。
|
|
4901
|
-
*/
|
|
4902
|
-
__publicField(this, "status");
|
|
4903
|
-
/**
|
|
4904
|
-
* 会话关闭原因。
|
|
4905
|
-
*/
|
|
4906
|
-
__publicField(this, "closeReason");
|
|
4815
|
+
// src/Media/Video/NetworkVideoController.ts
|
|
4816
|
+
var NetworkVideoController = class {
|
|
4817
|
+
constructor() {
|
|
4907
4818
|
/**
|
|
4908
|
-
*
|
|
4819
|
+
* 当前视频源与完成通知
|
|
4909
4820
|
*/
|
|
4821
|
+
__publicField(this, "currentTrack");
|
|
4822
|
+
__publicField(this, "reference");
|
|
4823
|
+
__publicField(this, "onFinish");
|
|
4910
4824
|
/**
|
|
4911
|
-
*
|
|
4825
|
+
* 预览进度与视图资源
|
|
4912
4826
|
*/
|
|
4913
|
-
__publicField(this, "
|
|
4914
|
-
this
|
|
4915
|
-
this
|
|
4916
|
-
this.status = init.status;
|
|
4917
|
-
this.connection = init.connection;
|
|
4918
|
-
this.closeReason = init.closeReason;
|
|
4827
|
+
__publicField(this, "position", 0);
|
|
4828
|
+
__publicField(this, "ended", false);
|
|
4829
|
+
__publicField(this, "bindings", /* @__PURE__ */ new Map());
|
|
4919
4830
|
}
|
|
4920
|
-
};
|
|
4921
|
-
|
|
4922
|
-
// src/Foundation/RTC/RoomJoinConfiguration.ts
|
|
4923
|
-
var RoomJoinConfiguration = class {
|
|
4924
4831
|
/**
|
|
4925
|
-
*
|
|
4926
|
-
*
|
|
4927
|
-
* @param init.roomID TRTC 房间号。
|
|
4928
|
-
* @param init.userID TRTC 登录身份。
|
|
4929
|
-
* @param init.sdkAppID TRTC SDKAppID(字符串形式)。
|
|
4930
|
-
* @param init.userSig TRTC 登录签名。
|
|
4931
|
-
* @param init.privateMapKey TRTC 进房鉴权字段。
|
|
4832
|
+
* 创建网络视频源,校验地址、采样方式和模型支持的尺寸;不请求视频下载。
|
|
4932
4833
|
*/
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4834
|
+
create(options, mediaService) {
|
|
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");
|
|
4840
|
+
}
|
|
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");
|
|
4843
|
+
}
|
|
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
|
+
});
|
|
4864
|
+
return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
|
|
4865
|
+
}
|
|
4866
|
+
/**
|
|
4867
|
+
* 释放全部预览绑定及源信息,不影响其他媒体源。
|
|
4868
|
+
*/
|
|
4869
|
+
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;
|
|
4881
|
+
}
|
|
4882
|
+
/**
|
|
4883
|
+
* 绑定播放器,恢复预览位置;本地播放错误仅记录,不中断服务端生成。
|
|
4884
|
+
*/
|
|
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
|
+
*/
|
|
6073
|
+
/**
|
|
6074
|
+
* 实时生成使用的文本条件。
|
|
6075
|
+
*/
|
|
6076
|
+
__publicField(this, "prompt");
|
|
6077
|
+
/**
|
|
6078
|
+
* 已上传参考图片的可选远端路径。
|
|
6079
|
+
*/
|
|
6080
|
+
__publicField(this, "referencePath");
|
|
6081
|
+
this.referenceVideo = init.referenceVideo && Object.freeze({ ...init.referenceVideo });
|
|
6082
|
+
this.prompt = init.prompt.trim();
|
|
6083
|
+
const normalizedReferencePath = init.referencePath?.trim();
|
|
6084
|
+
this.referencePath = normalizedReferencePath || void 0;
|
|
6085
|
+
}
|
|
6086
|
+
};
|
|
6087
|
+
|
|
6088
|
+
// src/Service/Realtime/RealtimeSession.ts
|
|
6089
|
+
var RealtimeSession = class {
|
|
6090
|
+
/**
|
|
6091
|
+
* 创建实时会话。
|
|
6092
|
+
*
|
|
6093
|
+
* @param init.id 会话标识(sessionUid)。
|
|
6094
|
+
* @param init.userID 业务用户标识。
|
|
6095
|
+
* @param init.status 会话状态。
|
|
6096
|
+
* @param init.connection RTC 连接参数。
|
|
6097
|
+
* @param init.closeReason 会话关闭原因。
|
|
6098
|
+
*/
|
|
6099
|
+
constructor(init) {
|
|
6100
|
+
/**
|
|
6101
|
+
* 会话信息
|
|
6102
|
+
*/
|
|
6103
|
+
/**
|
|
6104
|
+
* 会话标识(sessionUid)。
|
|
6105
|
+
*/
|
|
6106
|
+
__publicField(this, "id");
|
|
6107
|
+
/**
|
|
6108
|
+
* 业务用户标识。
|
|
6109
|
+
*/
|
|
6110
|
+
__publicField(this, "userID");
|
|
6111
|
+
/**
|
|
6112
|
+
* 运行状态
|
|
6113
|
+
*/
|
|
6114
|
+
/**
|
|
6115
|
+
* 会话状态(如 ACTIVE);响应未携带时为空。
|
|
6116
|
+
*/
|
|
6117
|
+
__publicField(this, "status");
|
|
6118
|
+
/**
|
|
6119
|
+
* 会话关闭原因。
|
|
6120
|
+
*/
|
|
6121
|
+
__publicField(this, "closeReason");
|
|
6122
|
+
/**
|
|
6123
|
+
* RTC 连接
|
|
6124
|
+
*/
|
|
6125
|
+
/**
|
|
6126
|
+
* RTC 连接参数;心跳等响应未携带完整参数时为空。
|
|
6127
|
+
*/
|
|
6128
|
+
__publicField(this, "connection");
|
|
6129
|
+
this.id = init.id;
|
|
6130
|
+
this.userID = init.userID;
|
|
6131
|
+
this.status = init.status;
|
|
6132
|
+
this.connection = init.connection;
|
|
6133
|
+
this.closeReason = init.closeReason;
|
|
6134
|
+
}
|
|
6135
|
+
};
|
|
6136
|
+
|
|
6137
|
+
// src/Foundation/RTC/RoomJoinConfiguration.ts
|
|
6138
|
+
var RoomJoinConfiguration = class {
|
|
6139
|
+
/**
|
|
6140
|
+
* 创建房间加入参数。
|
|
6141
|
+
*
|
|
6142
|
+
* @param init.roomID TRTC 房间号。
|
|
6143
|
+
* @param init.userID TRTC 登录身份。
|
|
6144
|
+
* @param init.sdkAppID TRTC SDKAppID(字符串形式)。
|
|
6145
|
+
* @param init.userSig TRTC 登录签名。
|
|
6146
|
+
* @param init.privateMapKey TRTC 进房鉴权字段。
|
|
6147
|
+
*/
|
|
6148
|
+
constructor(init) {
|
|
6149
|
+
/**
|
|
6150
|
+
* RTC 提供方。
|
|
6151
|
+
*/
|
|
6152
|
+
__publicField(this, "provider", "trtc" /* trtc */);
|
|
6153
|
+
/**
|
|
6154
|
+
* 进房参数
|
|
4940
6155
|
*/
|
|
4941
6156
|
/**
|
|
4942
6157
|
* TRTC 房间号(字符串房间号)。
|
|
@@ -5385,6 +6600,16 @@ var RemoteStream = class {
|
|
|
5385
6600
|
var RoomEvent = class _RoomEvent {
|
|
5386
6601
|
/**
|
|
5387
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 房间消息。
|
|
5388
6613
|
*/
|
|
5389
6614
|
static start(options) {
|
|
5390
6615
|
return _RoomEvent.encode({
|
|
@@ -5408,6 +6633,15 @@ var RoomEvent = class _RoomEvent {
|
|
|
5408
6633
|
}
|
|
5409
6634
|
/**
|
|
5410
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 房间消息。
|
|
5411
6645
|
*/
|
|
5412
6646
|
static changeCondition(options) {
|
|
5413
6647
|
return _RoomEvent.encode({
|
|
@@ -5464,18 +6698,23 @@ var RoomEvent = class _RoomEvent {
|
|
|
5464
6698
|
});
|
|
5465
6699
|
}
|
|
5466
6700
|
/**
|
|
5467
|
-
*
|
|
6701
|
+
* 组装生成条件参数,包含人脸回贴配置及本次扩展参数。
|
|
6702
|
+
*
|
|
6703
|
+
* @param options 本次生成条件类信令的参数。
|
|
6704
|
+
* @returns start 与 change_condition 共用的 params 字段。
|
|
5468
6705
|
*/
|
|
5469
6706
|
static generationParameters(options) {
|
|
5470
6707
|
return {
|
|
5471
6708
|
model: "default",
|
|
6709
|
+
face_replace: options.faceRestoration ?? false,
|
|
5472
6710
|
size: [options.videoFormat.width, options.videoFormat.height],
|
|
5473
6711
|
target_size: options.targetSize ? [
|
|
5474
6712
|
Math.round(options.targetSize.width),
|
|
5475
6713
|
Math.round(options.targetSize.height)
|
|
5476
6714
|
] : void 0,
|
|
5477
6715
|
prompt: options.context.prompt,
|
|
5478
|
-
ref_image_path: options.context.referencePath
|
|
6716
|
+
ref_image_path: options.context.referencePath,
|
|
6717
|
+
...options.extraParams
|
|
5479
6718
|
};
|
|
5480
6719
|
}
|
|
5481
6720
|
/**
|
|
@@ -5839,23 +7078,38 @@ var RoomController = class {
|
|
|
5839
7078
|
/**
|
|
5840
7079
|
* 发送生成开始信令。
|
|
5841
7080
|
*
|
|
7081
|
+
* @param options.taskID 当前生成任务标识。
|
|
7082
|
+
* @param options.videoFormat 模型生成使用的视频规格。
|
|
7083
|
+
* @param options.context 本次生成条件。
|
|
7084
|
+
* @param options.targetSize 回传尺寸,缺省时不发送;网络视频默认使用生成尺寸。
|
|
7085
|
+
* @param options.mirror 是否由后端镜像输入画面,默认 false。
|
|
7086
|
+
* @param options.faceRestoration 是否回贴原始人脸,默认 false。
|
|
7087
|
+
* @param options.extraParams 合并到本次 start 的 params,不缓存。
|
|
5842
7088
|
* @throws 房间未就绪或信令发送失败时抛出错误。
|
|
5843
7089
|
*/
|
|
5844
7090
|
startGeneration(options) {
|
|
5845
7091
|
return this.send(
|
|
5846
7092
|
RoomEvent.start({
|
|
5847
|
-
mirror: options.mirror ?? false,
|
|
5848
7093
|
userID: this.requireUserID(),
|
|
5849
7094
|
taskID: options.taskID,
|
|
5850
7095
|
videoFormat: options.videoFormat,
|
|
7096
|
+
context: options.context,
|
|
5851
7097
|
targetSize: options.targetSize,
|
|
5852
|
-
|
|
7098
|
+
mirror: options.mirror ?? false,
|
|
7099
|
+
faceRestoration: options.faceRestoration ?? false,
|
|
7100
|
+
extraParams: options.extraParams
|
|
5853
7101
|
})
|
|
5854
7102
|
);
|
|
5855
7103
|
}
|
|
5856
7104
|
/**
|
|
5857
7105
|
* 发送生成条件变更信令。
|
|
5858
7106
|
*
|
|
7107
|
+
* @param options.taskID 当前生成任务标识。
|
|
7108
|
+
* @param options.videoFormat 模型生成使用的视频规格。
|
|
7109
|
+
* @param options.context 本次生成条件。
|
|
7110
|
+
* @param options.targetSize 服务端生成后的回传尺寸,缺省时不发送。
|
|
7111
|
+
* @param options.faceRestoration 是否回贴原始人脸,默认 false。
|
|
7112
|
+
* @param options.extraParams 合并到本次 change_condition 的 params,不缓存。
|
|
5859
7113
|
* @throws 房间未就绪或信令发送失败时抛出错误。
|
|
5860
7114
|
*/
|
|
5861
7115
|
changeGenerationCondition(options) {
|
|
@@ -5864,8 +7118,10 @@ var RoomController = class {
|
|
|
5864
7118
|
userID: this.requireUserID(),
|
|
5865
7119
|
taskID: options.taskID,
|
|
5866
7120
|
videoFormat: options.videoFormat,
|
|
7121
|
+
context: options.context,
|
|
5867
7122
|
targetSize: options.targetSize,
|
|
5868
|
-
|
|
7123
|
+
faceRestoration: options.faceRestoration ?? false,
|
|
7124
|
+
extraParams: options.extraParams
|
|
5869
7125
|
})
|
|
5870
7126
|
);
|
|
5871
7127
|
}
|
|
@@ -6262,6 +7518,17 @@ var StreamController = class {
|
|
|
6262
7518
|
get hasGenerationTask() {
|
|
6263
7519
|
return this.state.generationTaskID !== void 0;
|
|
6264
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
|
+
}
|
|
6265
7532
|
/**
|
|
6266
7533
|
* 设置本地主视频流统计监听器,不改变 RTC 事件监听权。
|
|
6267
7534
|
*/
|
|
@@ -6343,8 +7610,17 @@ var StreamController = class {
|
|
|
6343
7610
|
/**
|
|
6344
7611
|
* 建立生成任务并发送开始信令。
|
|
6345
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,不缓存。
|
|
6346
7622
|
* @returns 等待远端结果流确认的 Promise。
|
|
6347
|
-
* @throws 任务标识无效、RTC
|
|
7623
|
+
* @throws 任务标识无效、RTC 房间未就绪、旧任务仍在启动或未指定 restart 且已有生成任务时同步抛错;
|
|
6348
7624
|
* 发送失败或确认超时通过返回的 Promise 拒绝。
|
|
6349
7625
|
*/
|
|
6350
7626
|
beginGeneration(options) {
|
|
@@ -6358,7 +7634,7 @@ var StreamController = class {
|
|
|
6358
7634
|
if (!this.state.roomID) {
|
|
6359
7635
|
throw new XmaxError("RTC_ERROR" /* rtcError */, "RTC room is not configured");
|
|
6360
7636
|
}
|
|
6361
|
-
if (this.state.generationTaskID) {
|
|
7637
|
+
if (this.state.generationTaskID && (!options.restart || this.state.generationWaiter)) {
|
|
6362
7638
|
throw new XmaxError(
|
|
6363
7639
|
"RTC_ERROR" /* rtcError */,
|
|
6364
7640
|
"Realtime generation is already active"
|
|
@@ -6393,10 +7669,12 @@ var StreamController = class {
|
|
|
6393
7669
|
try {
|
|
6394
7670
|
const sending = this.roomController.startGeneration({
|
|
6395
7671
|
taskID,
|
|
6396
|
-
mirror: options.mirror ?? false,
|
|
6397
7672
|
videoFormat: options.videoFormat,
|
|
7673
|
+
context: options.context,
|
|
6398
7674
|
targetSize: options.targetSize,
|
|
6399
|
-
|
|
7675
|
+
mirror: options.mirror ?? false,
|
|
7676
|
+
faceRestoration: options.faceRestoration ?? false,
|
|
7677
|
+
extraParams: options.extraParams
|
|
6400
7678
|
});
|
|
6401
7679
|
const state2 = this.state;
|
|
6402
7680
|
const sent = Promise.resolve(sending).then(async () => {
|
|
@@ -6448,6 +7726,15 @@ var StreamController = class {
|
|
|
6448
7726
|
/**
|
|
6449
7727
|
* 发送生成条件变更信令。
|
|
6450
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,不缓存。
|
|
6451
7738
|
* @throws RTC 房间未就绪或条件变更信令发送失败时抛出错误。
|
|
6452
7739
|
*/
|
|
6453
7740
|
async updateGeneration(options) {
|
|
@@ -6455,8 +7742,10 @@ var StreamController = class {
|
|
|
6455
7742
|
await this.roomController.changeGenerationCondition({
|
|
6456
7743
|
taskID: options.taskID,
|
|
6457
7744
|
videoFormat: options.videoFormat,
|
|
7745
|
+
context: options.context,
|
|
6458
7746
|
targetSize: options.targetSize,
|
|
6459
|
-
|
|
7747
|
+
faceRestoration: options.faceRestoration ?? false,
|
|
7748
|
+
extraParams: options.extraParams
|
|
6460
7749
|
});
|
|
6461
7750
|
} catch (error) {
|
|
6462
7751
|
throw XmaxError.from(error);
|
|
@@ -7050,8 +8339,9 @@ function qualityVideoFormat(baseline, level) {
|
|
|
7050
8339
|
|
|
7051
8340
|
// src/Stream/Quality/QualityHistory.ts
|
|
7052
8341
|
var QualityHistory = class {
|
|
7053
|
-
constructor(model) {
|
|
8342
|
+
constructor(model, provider) {
|
|
7054
8343
|
__publicField(this, "model", model);
|
|
8344
|
+
__publicField(this, "provider", provider);
|
|
7055
8345
|
}
|
|
7056
8346
|
/** 无记录、损坏、SSR 或浏览器禁止存储时使用默认 L2。 */
|
|
7057
8347
|
read(direction) {
|
|
@@ -7071,7 +8361,7 @@ var QualityHistory = class {
|
|
|
7071
8361
|
}
|
|
7072
8362
|
}
|
|
7073
8363
|
key(direction) {
|
|
7074
|
-
return `xmax:quality:v1
|
|
8364
|
+
return `xmax:quality:v1:${this.provider}:${encodeURIComponent(this.model)}:${direction}`;
|
|
7075
8365
|
}
|
|
7076
8366
|
isValid(level) {
|
|
7077
8367
|
return Number.isInteger(level) && level >= 1 && level <= Object.keys(QUALITY_TIERS).length;
|
|
@@ -7620,7 +8910,6 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
7620
8910
|
* 远端轨道与渲染绑定
|
|
7621
8911
|
*/
|
|
7622
8912
|
__publicField(this, "activeRemoteTrack");
|
|
7623
|
-
__publicField(this, "remoteTarget");
|
|
7624
8913
|
}
|
|
7625
8914
|
/**
|
|
7626
8915
|
* 当前活动会话标识;尚未连接或清理完成后为空。
|
|
@@ -7638,6 +8927,16 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
7638
8927
|
* 创建会话、配置编码、进房并发布本地媒体,启动心跳后返回远端占位流。
|
|
7639
8928
|
* 进房结束即记录连接耗时;可选 beforePublish 检查阻塞发布,计入连接耗时;
|
|
7640
8929
|
* 发布本地流单独计时。
|
|
8930
|
+
* @param options.localTrack 当前本地媒体轨道。
|
|
8931
|
+
* @param options.remoteVideoFormat 原始生成及远端画面规格,缺省使用本地轨道规格。
|
|
8932
|
+
* @param options.localVideoFormat 发布前实际应用的上行编码格式;与原始生成及远端格式分开。
|
|
8933
|
+
* @param options.onVideoEncoderConfigured 上行编码配置成功后的回调。
|
|
8934
|
+
* @param options.model 创建业务会话使用的模型名称。
|
|
8935
|
+
* @param options.includeLocalAudio 是否发布本地音频。
|
|
8936
|
+
* @param options.ensureCurrent 校验当前操作仍有效。
|
|
8937
|
+
* @param options.onPublished 本地发布完成后的回调。
|
|
8938
|
+
* @param options.beforePublish 发布前的可选异步检查。
|
|
8939
|
+
* @param options.publishLocalMedia 是否发布本地媒体,默认 true;网络视频源传 false。
|
|
7641
8940
|
* @throws 会话、进房、发布失败或操作租约已失效时抛出错误,由上层统一清理。
|
|
7642
8941
|
*/
|
|
7643
8942
|
async connect(options) {
|
|
@@ -7700,21 +8999,15 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
7700
8999
|
videoFormat: options.remoteVideoFormat ?? options.localTrack.videoFormat
|
|
7701
9000
|
});
|
|
7702
9001
|
this.activeRemoteTrack = remoteTrack;
|
|
7703
|
-
this.
|
|
9002
|
+
this.dependencies.renderController.registerRemoteTrack(remoteTrack, this.dependencies.onInteraction);
|
|
7704
9003
|
streamController.setRemoteAudioVolume(this.dependencies.remoteAudioVolume());
|
|
7705
9004
|
return new RealtimeMediaStream({ id: "stream1" /* remote */, videoTrack: remoteTrack });
|
|
7706
9005
|
}
|
|
7707
|
-
/**
|
|
7708
|
-
* 更新已绑定远端视图的插帧配置;传 undefined 停用插帧。
|
|
7709
|
-
*/
|
|
7710
|
-
setFrameInterpolation(options) {
|
|
7711
|
-
this.remoteTarget?.setFrameInterpolation?.(options);
|
|
7712
|
-
}
|
|
7713
9006
|
/**
|
|
7714
9007
|
* 清空远端视图的媒体流,保留轨道与视图绑定以供统一清理。
|
|
7715
9008
|
*/
|
|
7716
9009
|
clearRemoteMedia() {
|
|
7717
|
-
this.
|
|
9010
|
+
this.dependencies.renderController.setRemoteMedia(null);
|
|
7718
9011
|
}
|
|
7719
9012
|
/**
|
|
7720
9013
|
* 停止会话心跳;未配置会话服务时不执行操作。
|
|
@@ -7794,7 +9087,7 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
7794
9087
|
if (track) {
|
|
7795
9088
|
track.mediaStreamTrack = void 0;
|
|
7796
9089
|
}
|
|
7797
|
-
this.
|
|
9090
|
+
this.dependencies.renderController.setRemoteMedia(null);
|
|
7798
9091
|
return;
|
|
7799
9092
|
}
|
|
7800
9093
|
if (!track) {
|
|
@@ -7804,35 +9097,7 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
7804
9097
|
);
|
|
7805
9098
|
}
|
|
7806
9099
|
track.mediaStreamTrack = binding.videoTrack;
|
|
7807
|
-
this.
|
|
7808
|
-
}
|
|
7809
|
-
/**
|
|
7810
|
-
* 为远端轨道注册渲染绑定:attach 时挂流占位,媒体轨到达后送入画面。
|
|
7811
|
-
*/
|
|
7812
|
-
registerRemoteBinding(track) {
|
|
7813
|
-
VideoRenderRegistry.register(track, {
|
|
7814
|
-
frameDisplayHandler: () => {
|
|
7815
|
-
if (this.activeRemoteTrack === track) {
|
|
7816
|
-
this.dependencies.onFrameDisplayed();
|
|
7817
|
-
}
|
|
7818
|
-
},
|
|
7819
|
-
attachHandler: (view) => {
|
|
7820
|
-
this.remoteTarget?.setFrameInterpolation?.(void 0);
|
|
7821
|
-
view.isMirrored = false;
|
|
7822
|
-
this.remoteTarget = view;
|
|
7823
|
-
this.dependencies.onRenderAttached();
|
|
7824
|
-
const mediaTrack = track.mediaStreamTrack;
|
|
7825
|
-
view.setMediaStream(mediaTrack ? new MediaStream([mediaTrack]) : null);
|
|
7826
|
-
},
|
|
7827
|
-
detachHandler: (view) => {
|
|
7828
|
-
if (this.remoteTarget === view) {
|
|
7829
|
-
this.dependencies.onRenderDetached();
|
|
7830
|
-
this.remoteTarget = void 0;
|
|
7831
|
-
}
|
|
7832
|
-
view.setFrameInterpolation?.(void 0);
|
|
7833
|
-
view.setMediaStream(null);
|
|
7834
|
-
}
|
|
7835
|
-
});
|
|
9100
|
+
this.dependencies.renderController.setRemoteMedia(binding.videoTrack);
|
|
7836
9101
|
}
|
|
7837
9102
|
/**
|
|
7838
9103
|
* 构造当前远端生成结果媒体流。
|
|
@@ -7905,12 +9170,11 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
7905
9170
|
}
|
|
7906
9171
|
const track = this.activeRemoteTrack;
|
|
7907
9172
|
if (track) {
|
|
7908
|
-
|
|
9173
|
+
this.dependencies.renderController.resetRemoteTrack();
|
|
7909
9174
|
track.mediaStreamTrack = void 0;
|
|
7910
9175
|
}
|
|
7911
9176
|
this.activeSession = void 0;
|
|
7912
9177
|
this.activeRemoteTrack = void 0;
|
|
7913
|
-
this.remoteTarget = void 0;
|
|
7914
9178
|
if (sessionID && this.dependencies.sessionService) {
|
|
7915
9179
|
try {
|
|
7916
9180
|
await this.dependencies.sessionService.closeSession(sessionID);
|
|
@@ -7940,8 +9204,10 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
7940
9204
|
/**
|
|
7941
9205
|
* 注入房间传输组件和任务标识生成器,不创建会话或启动生成。
|
|
7942
9206
|
*/
|
|
7943
|
-
constructor(streamController, taskIDGenerator = _XmaxRealtimeGenerationManager.createTaskID) {
|
|
9207
|
+
constructor(streamController, interactionController, renderController, taskIDGenerator = _XmaxRealtimeGenerationManager.createTaskID) {
|
|
7944
9208
|
__publicField(this, "streamController", streamController);
|
|
9209
|
+
__publicField(this, "interactionController", interactionController);
|
|
9210
|
+
__publicField(this, "renderController", renderController);
|
|
7945
9211
|
__publicField(this, "taskIDGenerator", taskIDGenerator);
|
|
7946
9212
|
/**
|
|
7947
9213
|
* 当前生成条件与任务标识
|
|
@@ -7966,28 +9232,51 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
7966
9232
|
/**
|
|
7967
9233
|
* 发送生成指令并等待确认、远端就绪和音频激活,成功后返回任务标识。
|
|
7968
9234
|
* 条件上下文在生成确认后缓存;启动失败时尽力停止本次任务并保留原始错误。
|
|
9235
|
+
* @param options.videoFormat 模型生成使用的视频规格。
|
|
9236
|
+
* @param options.signal 取消本次生成启动的信号。
|
|
9237
|
+
* @param options.ensureCurrent 校验当前操作仍有效。
|
|
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。
|
|
9244
|
+
* @param options.onStartSent start 信令发送成功后的可选异步回调。
|
|
9245
|
+
* @param options.onRemoteReady 远端视频就绪后通知任务标识,不等待录制等附加功能。
|
|
9246
|
+
* @param options.extraParams 合并到本次 start 的 params,不缓存。
|
|
7969
9247
|
*/
|
|
7970
9248
|
async start(options) {
|
|
7971
9249
|
const context = this.validateContext(options.context);
|
|
7972
9250
|
options.ensureCurrent();
|
|
7973
9251
|
const taskID = this.taskIDGenerator();
|
|
9252
|
+
if (options.restart) {
|
|
9253
|
+
this.interactionController.stopInteraction();
|
|
9254
|
+
this.renderController.setInteractionEnabled(false);
|
|
9255
|
+
}
|
|
7974
9256
|
this.activeTaskID = taskID;
|
|
7975
9257
|
try {
|
|
7976
9258
|
const confirmation = this.streamController.beginGeneration({
|
|
7977
9259
|
taskID,
|
|
7978
9260
|
videoFormat: options.videoFormat,
|
|
7979
9261
|
context,
|
|
9262
|
+
restart: options.restart,
|
|
7980
9263
|
...options.targetSize && { targetSize: options.targetSize },
|
|
7981
9264
|
mirror: options.mirror ?? false,
|
|
7982
|
-
|
|
9265
|
+
faceRestoration: options.faceRestoration ?? false,
|
|
9266
|
+
...options.onStartSent && { onStartSent: options.onStartSent },
|
|
9267
|
+
extraParams: options.extraParams
|
|
7983
9268
|
});
|
|
7984
9269
|
await this.awaitConfirmation(confirmation, options.signal);
|
|
7985
9270
|
options.ensureCurrent();
|
|
7986
9271
|
this.currentContext = context;
|
|
7987
9272
|
await options.waitUntilRemoteReady();
|
|
7988
9273
|
options.ensureCurrent();
|
|
9274
|
+
options.onRemoteReady?.(taskID);
|
|
9275
|
+
options.ensureCurrent();
|
|
7989
9276
|
await this.streamController.activateRemoteAudio();
|
|
7990
9277
|
options.ensureCurrent();
|
|
9278
|
+
this.interactionController.startInteraction(taskID, options.videoFormat);
|
|
9279
|
+
this.renderController.setInteractionEnabled(true, options.videoFormat);
|
|
7991
9280
|
return taskID;
|
|
7992
9281
|
} catch (error) {
|
|
7993
9282
|
await this.stop(taskID);
|
|
@@ -7996,6 +9285,12 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
7996
9285
|
}
|
|
7997
9286
|
/**
|
|
7998
9287
|
* 更新活动任务的生成条件;信令发送成功后才覆盖缓存,无任务或上下文时抛错。
|
|
9288
|
+
* @param taskID 当前活动任务标识。
|
|
9289
|
+
* @param options.videoFormat 模型生成使用的视频规格。
|
|
9290
|
+
* @param options.context 生成条件,缺省复用最近缓存的上下文。
|
|
9291
|
+
* @param options.targetSize 服务端生成后的回传尺寸,缺省时不发送。
|
|
9292
|
+
* @param options.faceRestoration 是否回贴原始人脸,默认 false。
|
|
9293
|
+
* @param options.extraParams 合并到本次 change_condition 的 params,不缓存。
|
|
7999
9294
|
*/
|
|
8000
9295
|
async update(taskID, options) {
|
|
8001
9296
|
const context = options.context ?? this.currentContext;
|
|
@@ -8006,10 +9301,14 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
8006
9301
|
taskID,
|
|
8007
9302
|
videoFormat: options.videoFormat,
|
|
8008
9303
|
context,
|
|
8009
|
-
...options.targetSize && { targetSize: options.targetSize }
|
|
9304
|
+
...options.targetSize && { targetSize: options.targetSize },
|
|
9305
|
+
faceRestoration: options.faceRestoration ?? false,
|
|
9306
|
+
extraParams: options.extraParams
|
|
8010
9307
|
});
|
|
8011
9308
|
if (this.activeTaskID !== taskID) throw RealtimeCoordinator.cancelledError();
|
|
8012
9309
|
this.currentContext = context;
|
|
9310
|
+
this.interactionController.startInteraction(taskID, options.videoFormat);
|
|
9311
|
+
this.renderController.setInteractionEnabled(true, options.videoFormat);
|
|
8013
9312
|
}
|
|
8014
9313
|
/**
|
|
8015
9314
|
* 幂等停止指定活动任务;并发调用共享结果,停止失败仅记录日志。
|
|
@@ -8020,6 +9319,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
8020
9319
|
}
|
|
8021
9320
|
if (!taskID || this.activeTaskID !== taskID) return;
|
|
8022
9321
|
this.activeTaskID = void 0;
|
|
9322
|
+
this.interactionController.stopInteraction();
|
|
9323
|
+
this.renderController.setInteractionEnabled(false);
|
|
8023
9324
|
const completion = (async () => {
|
|
8024
9325
|
try {
|
|
8025
9326
|
await this.streamController.stopGeneration(taskID);
|
|
@@ -8040,6 +9341,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
8040
9341
|
*/
|
|
8041
9342
|
async reset(taskID) {
|
|
8042
9343
|
this.currentContext = void 0;
|
|
9344
|
+
this.interactionController.stopInteraction();
|
|
9345
|
+
this.renderController.setInteractionEnabled(false);
|
|
8043
9346
|
await this.stop(this.activeTaskID ?? (taskID || void 0));
|
|
8044
9347
|
}
|
|
8045
9348
|
/**
|
|
@@ -8096,6 +9399,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8096
9399
|
*/
|
|
8097
9400
|
__publicField(this, "options");
|
|
8098
9401
|
__publicField(this, "mediaService");
|
|
9402
|
+
/**
|
|
9403
|
+
* 可选的任务录制;未开启时不分配浏览器录制资源。
|
|
9404
|
+
*/
|
|
9405
|
+
__publicField(this, "recordingController", new RecordingController());
|
|
8099
9406
|
/**
|
|
8100
9407
|
* 本地流创建时的生成格式;上行编码调整不改变模型输入配置与远端插帧尺寸。
|
|
8101
9408
|
*/
|
|
@@ -8112,12 +9419,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8112
9419
|
*/
|
|
8113
9420
|
__publicField(this, "coordinator");
|
|
8114
9421
|
__publicField(this, "errorHandler");
|
|
8115
|
-
__publicField(this, "
|
|
8116
|
-
__publicField(this, "
|
|
8117
|
-
__publicField(this, "localVideoController", new LocalVideoController((error) => {
|
|
8118
|
-
void this.coordinator.terminateWithError(error, 0 /* connection */);
|
|
8119
|
-
}));
|
|
8120
|
-
__publicField(this, "rtcManager");
|
|
9422
|
+
__publicField(this, "mediaController");
|
|
9423
|
+
__publicField(this, "renderController");
|
|
8121
9424
|
/**
|
|
8122
9425
|
* 启动计时与首帧通知
|
|
8123
9426
|
*/
|
|
@@ -8162,14 +9465,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8162
9465
|
this.supportsInterpolation = dependencies?.supportsFrameInterpolation ?? (async (size) => await frameInterpolationAdapter(size) !== null);
|
|
8163
9466
|
this.errorHandler = new RealtimeErrorHandler();
|
|
8164
9467
|
const rtcManager = dependencies?.rtcManager ?? createRtcManager(options.provider, dependencies?.environment ?? "china" /* china */);
|
|
8165
|
-
this.rtcManager = rtcManager;
|
|
8166
|
-
this.cameraController = dependencies?.cameraController ?? new CameraController({
|
|
8167
|
-
rtcManager,
|
|
8168
|
-
mediaService: this.mediaService,
|
|
8169
|
-
errorListener: (error) => {
|
|
8170
|
-
void this.errorHandler.report(error);
|
|
8171
|
-
}
|
|
8172
|
-
});
|
|
8173
9468
|
const sessionService = dependencies?.sessionService ?? (dependencies?.apiService ? new RealtimeSessionService({ apiService: dependencies.apiService, provider: options.provider }) : void 0);
|
|
8174
9469
|
this.streamController = dependencies?.streamController ?? new StreamController({
|
|
8175
9470
|
rtcManager,
|
|
@@ -8180,10 +9475,38 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8180
9475
|
void this.errorHandler.report(error);
|
|
8181
9476
|
},
|
|
8182
9477
|
remoteStreamListener: (binding) => {
|
|
9478
|
+
if (!binding) {
|
|
9479
|
+
void this.recordingController.stop("mediaInterrupted" /* mediaInterrupted */);
|
|
9480
|
+
}
|
|
8183
9481
|
this.clearRemoteVideoStatistics();
|
|
8184
9482
|
this.connectionManager.handleRemoteStreamBinding(binding);
|
|
8185
9483
|
}
|
|
8186
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
|
+
});
|
|
8187
9510
|
this.connectionManager = new XmaxRealtimeConnectionManager({
|
|
8188
9511
|
provider: options.provider,
|
|
8189
9512
|
sessionService,
|
|
@@ -8193,34 +9516,19 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8193
9516
|
onHeartbeatFailure: (sessionID, error) => {
|
|
8194
9517
|
void this.handleHeartbeatFailure(sessionID, error);
|
|
8195
9518
|
},
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
onRenderDetached: () => {
|
|
8199
|
-
this.interpolationRevision++;
|
|
8200
|
-
}
|
|
9519
|
+
renderController: this.renderController,
|
|
9520
|
+
onInteraction: (frame) => this.mediaController.submitInteraction(frame)
|
|
8201
9521
|
});
|
|
8202
|
-
this.generationManager = new XmaxRealtimeGenerationManager(this.streamController);
|
|
9522
|
+
this.generationManager = new XmaxRealtimeGenerationManager(this.streamController, this.mediaController, this.renderController);
|
|
8203
9523
|
this.coordinator = new RealtimeCoordinator({
|
|
8204
9524
|
errorHandler: this.errorHandler,
|
|
8205
9525
|
cleanup: (scope, taskID) => this.performCleanup(scope, taskID)
|
|
8206
9526
|
});
|
|
8207
|
-
if (options.provider === "agora" /* agora */) {
|
|
8208
|
-
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);
|
|
8209
9529
|
this.qualityController = new QualityController(async (change, signal) => {
|
|
8210
|
-
if (signal.aborted || this.currentState.connectionState !== "Generating" /* generating */) {
|
|
8211
|
-
throw new XmaxError("CANCELLED" /* cancelled */, "Adaptive video format update was cancelled");
|
|
8212
|
-
}
|
|
8213
9530
|
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
8214
|
-
|
|
8215
|
-
const state2 = track && this.qualityStates.get(track);
|
|
8216
|
-
if (!track || !state2) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Adaptive video baseline is unavailable");
|
|
8217
|
-
const format = qualityVideoFormat(state2.baseline, change.to);
|
|
8218
|
-
await this.streamController.updateUplinkVideoFormat(format);
|
|
8219
|
-
token.ensureCurrent();
|
|
8220
|
-
this.generationVideoFormat ?? (this.generationVideoFormat = state2.baseline);
|
|
8221
|
-
track.updateVideoFormat(format);
|
|
8222
|
-
state2.level = change.to;
|
|
8223
|
-
this.qualityHistory?.write("uplink", change.to);
|
|
9531
|
+
await this.applyUplinkQuality(change.to, token, signal);
|
|
8224
9532
|
});
|
|
8225
9533
|
});
|
|
8226
9534
|
this.downlinkQualityController = new QualityController(async (change, signal) => {
|
|
@@ -8278,13 +9586,58 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8278
9586
|
this.applyRemoteInterpolation();
|
|
8279
9587
|
});
|
|
8280
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
|
+
}
|
|
8281
9634
|
/** 手动调试与自动调档使用同一尺寸信令路径,不修改生成或上行配置。 */
|
|
8282
9635
|
async adjustDownlinkQuality(direction) {
|
|
8283
9636
|
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
8284
9637
|
const controller = this.downlinkQualityController;
|
|
8285
9638
|
const state2 = this.downlinkQualityState;
|
|
8286
9639
|
if (!controller || !state2 || this.currentState.connectionState !== "Generating" /* generating */ || direction !== "upgrade" && direction !== "downgrade") {
|
|
8287
|
-
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Downlink quality adjustment requires an active
|
|
9640
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Downlink quality adjustment requires an active adaptive-quality generation and a valid direction");
|
|
8288
9641
|
}
|
|
8289
9642
|
const level = Math.max(1, Math.min(Object.keys(QUALITY_TIERS).length, state2.level + (direction === "upgrade" ? -1 : 1)));
|
|
8290
9643
|
if (level === state2.level) return;
|
|
@@ -8366,7 +9719,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8366
9719
|
applyRemoteInterpolation() {
|
|
8367
9720
|
const revision = ++this.interpolationRevision;
|
|
8368
9721
|
const size = this.interpolationSize;
|
|
8369
|
-
this.
|
|
9722
|
+
this.renderController.setFrameInterpolation(size ? {
|
|
8370
9723
|
size,
|
|
8371
9724
|
onFailure: (error) => {
|
|
8372
9725
|
if (revision === this.interpolationRevision) this.handleInterpolationFailure(error);
|
|
@@ -8523,6 +9876,9 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8523
9876
|
*
|
|
8524
9877
|
* 将返回的轨道绑定到预览视图;收到有效帧且视图已绑定后进入 `ready`。
|
|
8525
9878
|
*
|
|
9879
|
+
* @param options.videoFormat 旋转后的目标规格,尺寸按模型规则解析。
|
|
9880
|
+
* @param options.position 前置或后置摄像头。
|
|
9881
|
+
* @param options.useMicrophone 是否采集并发布麦克风音频。
|
|
8526
9882
|
* @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
|
|
8527
9883
|
* @param options.rotation 输入帧顺时针旋转角度,默认 0;videoFormat 为旋转后的规格,目前仅 Agora 支持非零角度。
|
|
8528
9884
|
* @returns 包含本地相机视频轨道的媒体流。
|
|
@@ -8533,7 +9889,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8533
9889
|
"media" /* media */,
|
|
8534
9890
|
void 0,
|
|
8535
9891
|
async (token) => {
|
|
8536
|
-
this.ensureNoLocalSource();
|
|
9892
|
+
this.mediaController.ensureNoLocalSource();
|
|
8537
9893
|
await this.coordinator.commit(
|
|
8538
9894
|
new RealtimeState({
|
|
8539
9895
|
connectionState: "Preparing" /* preparing */
|
|
@@ -8554,7 +9910,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8554
9910
|
let stream;
|
|
8555
9911
|
try {
|
|
8556
9912
|
token.ensureCurrent();
|
|
8557
|
-
stream = await this.
|
|
9913
|
+
stream = await this.mediaController.createLocalCameraStream(options);
|
|
8558
9914
|
} catch (error) {
|
|
8559
9915
|
this.disposePermissionWatch();
|
|
8560
9916
|
this.launchTimer.cancel();
|
|
@@ -8563,7 +9919,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8563
9919
|
token.ensureCurrent();
|
|
8564
9920
|
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
8565
9921
|
completeCamera();
|
|
8566
|
-
this.
|
|
9922
|
+
this.mediaController.setCameraPreviewReadyHandler((isCurrent) => {
|
|
8567
9923
|
void this.coordinator.localPreviewDidBecomeReady(isCurrent);
|
|
8568
9924
|
});
|
|
8569
9925
|
return stream;
|
|
@@ -8572,15 +9928,19 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8572
9928
|
}
|
|
8573
9929
|
/**
|
|
8574
9930
|
* 创建网络视频源并初始化接收端 RTC;预览是否加载成功不影响服务端读取视频。
|
|
9931
|
+
* @param options.url 服务端可直接访问的 HTTP/HTTPS 视频地址。
|
|
9932
|
+
* @param options.videoFormat 模型输入及生成画面规格,不是上行编码参数。
|
|
9933
|
+
* @param options.sampleMethod 服务端采样方式,默认 time。
|
|
9934
|
+
* @param options.onFinish 服务端完成当前任务后的回调,不代表尾帧已播放完。
|
|
8575
9935
|
*/
|
|
8576
9936
|
async createNetworkVideoStream(options) {
|
|
8577
9937
|
return this.coordinator.run("media" /* media */, void 0, async (token) => {
|
|
8578
|
-
this.ensureNoLocalSource();
|
|
8579
|
-
const stream = this.
|
|
9938
|
+
this.mediaController.ensureNoLocalSource();
|
|
9939
|
+
const stream = this.mediaController.createNetworkVideoStream(options);
|
|
8580
9940
|
token.setFailureScope(1 /* all */);
|
|
8581
9941
|
this.launchTimer.start();
|
|
8582
9942
|
this.clearVideoStatistics();
|
|
8583
|
-
await this.
|
|
9943
|
+
await this.mediaController.prepareSource(() => token.ensureCurrent());
|
|
8584
9944
|
token.ensureCurrent();
|
|
8585
9945
|
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
8586
9946
|
await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
|
|
@@ -8589,43 +9949,70 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8589
9949
|
}
|
|
8590
9950
|
/**
|
|
8591
9951
|
* 创建文件音视频源并准备外部轨道;准备失败时统一释放本次媒体和 RTC 资源。
|
|
9952
|
+
* @param options.file 包含可解码视频的本地 File 或 Blob。
|
|
9953
|
+
* @param options.videoFormat 期望的视频规格;尺寸按宽高比匹配模型档位。
|
|
9954
|
+
* @param options.loop 是否循环播放,默认 true。
|
|
9955
|
+
* @param options.includeAudio 是否发送文件音频,默认 true。
|
|
8592
9956
|
*/
|
|
8593
9957
|
async createLocalVideoStream(options) {
|
|
8594
9958
|
return this.coordinator.run("media" /* media */, void 0, async (token) => {
|
|
8595
|
-
this.ensureNoLocalSource();
|
|
8596
|
-
|
|
8597
|
-
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "The RTC provider does not support local video streams");
|
|
8598
|
-
}
|
|
9959
|
+
this.mediaController.ensureNoLocalSource();
|
|
9960
|
+
this.mediaController.validateExternalMediaSupport("local");
|
|
8599
9961
|
token.setFailureScope(1 /* all */);
|
|
8600
9962
|
this.launchTimer.start();
|
|
8601
9963
|
this.clearVideoStatistics();
|
|
8602
|
-
const stream = await this.
|
|
8603
|
-
token.ensureCurrent();
|
|
8604
|
-
await this.rtcManager.initialize();
|
|
8605
|
-
token.ensureCurrent();
|
|
8606
|
-
await this.rtcManager.setExternalMediaTracks({
|
|
8607
|
-
videoTrack: stream.videoTrack.mediaStreamTrack,
|
|
8608
|
-
audioTrack: this.localVideoController.audioTrack
|
|
8609
|
-
});
|
|
9964
|
+
const stream = await this.mediaController.createLocalVideoStream(options, token.signal);
|
|
8610
9965
|
token.ensureCurrent();
|
|
9966
|
+
await this.mediaController.prepareSource(() => token.ensureCurrent());
|
|
8611
9967
|
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
8612
9968
|
await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
|
|
8613
9969
|
return stream;
|
|
8614
9970
|
});
|
|
8615
9971
|
}
|
|
8616
9972
|
/**
|
|
8617
|
-
*
|
|
9973
|
+
* 创建本地图片静止视频源并准备 RTC,失败或取消时释放本次资源。
|
|
8618
9974
|
*/
|
|
8619
|
-
|
|
8620
|
-
return this.
|
|
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);
|
|
9983
|
+
token.ensureCurrent();
|
|
9984
|
+
await this.mediaController.prepareSource(() => token.ensureCurrent());
|
|
9985
|
+
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
9986
|
+
await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
|
|
9987
|
+
return stream;
|
|
9988
|
+
});
|
|
8621
9989
|
}
|
|
8622
9990
|
/**
|
|
8623
|
-
*
|
|
9991
|
+
* 接入外部视频轨;不创建采集设备或预览,所有异步失败统一释放 SDK 副本。
|
|
9992
|
+
* @param options.videoTrack 用户自行采集或处理的 live 视频轨,原始轨道归调用方所有。
|
|
9993
|
+
* @param options.videoFormat 期望的上传及生成规格,须符合模型规则;不代表修改输入像素。
|
|
8624
9994
|
*/
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
9995
|
+
async createExternalVideoStream(options) {
|
|
9996
|
+
return this.coordinator.run("media" /* media */, void 0, async (token) => {
|
|
9997
|
+
this.mediaController.ensureNoLocalSource();
|
|
9998
|
+
this.mediaController.validateExternalMediaSupport("external");
|
|
9999
|
+
const stream = this.mediaController.createExternalVideoStream(options);
|
|
10000
|
+
token.setFailureScope(1 /* all */);
|
|
10001
|
+
this.launchTimer.start();
|
|
10002
|
+
this.clearVideoStatistics();
|
|
10003
|
+
await this.mediaController.prepareSource(() => token.ensureCurrent());
|
|
10004
|
+
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
10005
|
+
await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
|
|
10006
|
+
return stream;
|
|
10007
|
+
});
|
|
10008
|
+
}
|
|
10009
|
+
/** 当前可发布的本地视频轨,不包含服务端直接读取的网络视频。 */
|
|
10010
|
+
get currentUplinkTrack() {
|
|
10011
|
+
return this.mediaController.uplinkTrack;
|
|
10012
|
+
}
|
|
10013
|
+
/** 当前 Manager 拥有的媒体源轨道。 */
|
|
10014
|
+
get currentLocalTrack() {
|
|
10015
|
+
return this.mediaController.currentTrack;
|
|
8629
10016
|
}
|
|
8630
10017
|
/**
|
|
8631
10018
|
* 移除媒体权限弹窗观察;授权决定已记录或不再需要观察时调用。
|
|
@@ -8638,14 +10025,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8638
10025
|
* 应用完整的上行视频格式,成功后更新本地轨道元数据,不改变生成格式或生命周期。
|
|
8639
10026
|
* 复用配置操作准入,避免与建连、切换摄像头或其他配置更新并发。
|
|
8640
10027
|
*
|
|
8641
|
-
* @throws
|
|
10028
|
+
* @throws 无本地相机、外部或文件视频流、参数无效、操作冲突或底层编码更新失败时抛错。
|
|
8642
10029
|
*/
|
|
8643
10030
|
async updateUplinkVideoFormat(videoFormat) {
|
|
8644
10031
|
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
8645
|
-
const track = this.
|
|
10032
|
+
const track = this.currentUplinkTrack;
|
|
8646
10033
|
const previousFormat = track?.videoFormat;
|
|
8647
10034
|
if (!track || !previousFormat) {
|
|
8648
|
-
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera or file video stream is not started");
|
|
10035
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera, external or file video stream is not started");
|
|
8649
10036
|
}
|
|
8650
10037
|
videoFormat.validate();
|
|
8651
10038
|
this.qualityController?.stop();
|
|
@@ -8671,11 +10058,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8671
10058
|
}
|
|
8672
10059
|
/** 手动仅调帧接口,成功后的当前格式将成为新的自适应 L1 基准。 */
|
|
8673
10060
|
async applyUplinkFrameRate(fps, token) {
|
|
8674
|
-
const track = this.
|
|
10061
|
+
const track = this.currentUplinkTrack;
|
|
8675
10062
|
const previous = track?.videoFormat;
|
|
8676
10063
|
const state2 = this.currentState.connectionState;
|
|
8677
10064
|
if (!track || !previous || state2 !== "Connected" /* connected */ && state2 !== "Generating" /* generating */) {
|
|
8678
|
-
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Frame rate updates require a connected local camera or file stream");
|
|
10065
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Frame rate updates require a connected local camera, external or file stream");
|
|
8679
10066
|
}
|
|
8680
10067
|
if (!Number.isFinite(fps) || fps <= 0) {
|
|
8681
10068
|
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video frame rate must be greater than zero");
|
|
@@ -8702,7 +10089,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8702
10089
|
downlink.resetSamples();
|
|
8703
10090
|
return;
|
|
8704
10091
|
}
|
|
8705
|
-
const track = this.
|
|
10092
|
+
const track = this.currentUplinkTrack;
|
|
8706
10093
|
if (!track) {
|
|
8707
10094
|
controller.stop();
|
|
8708
10095
|
} else if (!controller.snapshot.active) {
|
|
@@ -8734,7 +10121,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8734
10121
|
* 停止本地相机流并释放本地预览与 RTC 资源。
|
|
8735
10122
|
*/
|
|
8736
10123
|
async stopLocalCameraStream() {
|
|
8737
|
-
if (this.
|
|
10124
|
+
if (this.mediaController.source && this.mediaController.source !== "camera") {
|
|
8738
10125
|
return;
|
|
8739
10126
|
}
|
|
8740
10127
|
await this.coordinator.run(
|
|
@@ -8744,7 +10131,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8744
10131
|
this.launchTimer.cancel();
|
|
8745
10132
|
this.clearVideoStatistics();
|
|
8746
10133
|
this.disposePermissionWatch();
|
|
8747
|
-
await this.
|
|
10134
|
+
await this.mediaController.stopLocalCameraStream();
|
|
8748
10135
|
token.ensureCurrent();
|
|
8749
10136
|
this.generationVideoFormat = void 0;
|
|
8750
10137
|
await this.coordinator.commit(
|
|
@@ -8767,8 +10154,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8767
10154
|
"cameraSwitch" /* cameraSwitch */,
|
|
8768
10155
|
void 0,
|
|
8769
10156
|
async (token) => {
|
|
10157
|
+
if (this.mediaController.source === "externalVideo") {
|
|
10158
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External video capture is managed by the caller");
|
|
10159
|
+
}
|
|
8770
10160
|
this.qualityController?.stop();
|
|
8771
|
-
const stream = await this.
|
|
10161
|
+
const stream = await this.mediaController.switchCamera();
|
|
8772
10162
|
token.ensureCurrent();
|
|
8773
10163
|
return stream;
|
|
8774
10164
|
}
|
|
@@ -8781,7 +10171,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8781
10171
|
* 启用帧检测时,发布前并行等待相机预热(固定 200ms),避免把黑帧推给 RTC。
|
|
8782
10172
|
* 返回的远端媒体流在生成开始后承载远端生成画面。
|
|
8783
10173
|
*
|
|
8784
|
-
* @param localStream 由当前 Manager
|
|
10174
|
+
* @param localStream 由当前 Manager 创建的摄像头、外部视频轨、本地文件或网络视频源。
|
|
8785
10175
|
* @returns 远端生成结果占位的媒体流。
|
|
8786
10176
|
* @throws 本地流不属于当前 Manager、已有活动连接、会话创建或进房
|
|
8787
10177
|
* 发布失败时抛出错误;失败时自动释放连接资源并恢复本地预览。
|
|
@@ -8805,18 +10195,24 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8805
10195
|
/**
|
|
8806
10196
|
* 按需建立连接并开始生成。
|
|
8807
10197
|
*
|
|
8808
|
-
*
|
|
10198
|
+
* 尚未连接时先建立实时连接;已在生成时默认更新条件,restart 为 true 时重新开始生成。
|
|
8809
10199
|
* 首次生成必须提供条件上下文,之后缺省时复用最近一次缓存的上下文。
|
|
8810
10200
|
*
|
|
8811
|
-
* @param options.localStream 由当前 Manager
|
|
10201
|
+
* @param options.localStream 由当前 Manager 创建的摄像头、外部视频轨、本地文件或网络视频源。
|
|
8812
10202
|
* @param options.context 本次生成使用的条件上下文;缺省时复用缓存。
|
|
10203
|
+
* @param options.restart 是否保留 RTC 连接并创建新任务发送 start,默认 false;不预先发送 stop。
|
|
10204
|
+
* @param options.faceRestoration 是否启用原始人脸回贴,默认 false;正在生成时通过 change_condition 更新。
|
|
10205
|
+
* @param options.extraParams 合并到本次 start 或 change_condition 的 params;不缓存,自动恢复时不携带,不允许覆盖 SDK 字段。
|
|
10206
|
+
* @param options.record 是否录制远端原始音视频约 20 秒,默认 false;仅 start 有效,条件更新时忽略,重启录制暂不支持。
|
|
8813
10207
|
* @returns 承载远端生成画面的媒体流。
|
|
10208
|
+
* @throws extraParams 包含非 JSON 数据、循环引用或 SDK 保留字段时,在连接或更新前抛出 invalidConfiguration。
|
|
8814
10209
|
* @throws 本地流不属于当前 Manager、缺少可用的条件上下文、信令发送
|
|
8815
10210
|
* 或生成确认失败时抛出错误;失败时自动停止生成任务并释放连接资源。
|
|
8816
10211
|
*/
|
|
8817
10212
|
async startGeneration(options) {
|
|
10213
|
+
const extraParams = snapshotExtraParams(options.extraParams);
|
|
8818
10214
|
return this.coordinator.run(
|
|
8819
|
-
"generation" /* generation */,
|
|
10215
|
+
options.restart === true ? "generationRestart" /* generationRestart */ : "generation" /* generation */,
|
|
8820
10216
|
void 0,
|
|
8821
10217
|
async (token) => {
|
|
8822
10218
|
const localTrack = options.localStream.videoTrack;
|
|
@@ -8834,33 +10230,50 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8834
10230
|
);
|
|
8835
10231
|
}
|
|
8836
10232
|
const currentState = this.coordinator.currentState;
|
|
8837
|
-
|
|
10233
|
+
const isGenerating = currentState.connectionState === "Generating" /* generating */;
|
|
10234
|
+
if (isGenerating && options.restart !== true) {
|
|
8838
10235
|
await this.generationManager.update(currentState.taskID, {
|
|
8839
10236
|
videoFormat,
|
|
10237
|
+
context: options.context,
|
|
8840
10238
|
targetSize: this.downlinkQualityController ? this.downlinkQualityState?.size : void 0,
|
|
8841
|
-
|
|
10239
|
+
faceRestoration: options.faceRestoration ?? false,
|
|
10240
|
+
extraParams
|
|
8842
10241
|
});
|
|
8843
10242
|
token.ensureCurrent();
|
|
8844
10243
|
return this.connectionManager.makeRemoteStream();
|
|
8845
10244
|
}
|
|
8846
10245
|
const supplied = this.generationManager.validateContext(options.context);
|
|
8847
|
-
const referenceVideo = this.
|
|
10246
|
+
const referenceVideo = this.mediaController.referenceVideo;
|
|
8848
10247
|
if (!referenceVideo && supplied.referenceVideo) {
|
|
8849
10248
|
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Create a network video stream before using a reference video");
|
|
8850
10249
|
}
|
|
8851
10250
|
const context = referenceVideo ? new RealtimeContext({ ...supplied, referenceVideo }) : supplied;
|
|
8852
|
-
if (currentState.connectionState !== "Connected" /* connected */) {
|
|
10251
|
+
if (currentState.connectionState !== "Connected" /* connected */ && !isGenerating) {
|
|
8853
10252
|
await this.performConnect(options.localStream, token);
|
|
8854
10253
|
}
|
|
8855
|
-
return this.performStartGeneration(
|
|
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
|
+
);
|
|
8856
10263
|
}
|
|
8857
10264
|
);
|
|
8858
10265
|
}
|
|
10266
|
+
/**
|
|
10267
|
+
* 设置录制最终事件监听器;清除监听不停止录制,录制错误不影响生成。
|
|
10268
|
+
*/
|
|
10269
|
+
setRecordingListener(listener) {
|
|
10270
|
+
this.recordingController.setListener(listener);
|
|
10271
|
+
}
|
|
8859
10272
|
/**
|
|
8860
10273
|
* 断开实时连接并保留当前本地媒体预览。
|
|
8861
10274
|
*/
|
|
8862
10275
|
async disconnect() {
|
|
8863
|
-
this.
|
|
10276
|
+
this.mediaController.pauseLocalVideo();
|
|
8864
10277
|
this.launchTimer.cancel();
|
|
8865
10278
|
this.clearVideoStatistics();
|
|
8866
10279
|
await this.coordinator.disconnect();
|
|
@@ -8870,15 +10283,20 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8870
10283
|
* 关闭期间重复调用会等待同一个释放任务;关闭完成后仍可重新创建本地流。
|
|
8871
10284
|
*/
|
|
8872
10285
|
async close() {
|
|
8873
|
-
this.
|
|
10286
|
+
this.mediaController.pauseLocalVideo();
|
|
8874
10287
|
this.launchTimer.cancel();
|
|
8875
10288
|
this.clearVideoStatistics();
|
|
8876
10289
|
await this.coordinator.terminate(1 /* all */);
|
|
8877
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
|
+
}
|
|
8878
10296
|
/** 仅为尚无档位记录的本地上行选择初始档位;不应用配置或覆盖重连状态。 */
|
|
8879
10297
|
resolveInitialUplinkQuality(track) {
|
|
8880
10298
|
if (!this.qualityController) return void 0;
|
|
8881
|
-
if (this.
|
|
10299
|
+
if (this.mediaController.source === "networkVideo") return void 0;
|
|
8882
10300
|
if (this.qualityStates.has(track)) return void 0;
|
|
8883
10301
|
const baseline = track.videoFormat;
|
|
8884
10302
|
if (!baseline) return void 0;
|
|
@@ -8911,8 +10329,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8911
10329
|
if (token.signal.aborted) abortWarmup();
|
|
8912
10330
|
try {
|
|
8913
10331
|
let beforePublish;
|
|
8914
|
-
if (
|
|
8915
|
-
const cameraReady = this.
|
|
10332
|
+
if (this.mediaController.isFrameValidationEnabled) {
|
|
10333
|
+
const cameraReady = this.mediaController.waitForValidCameraFrame(warmupController.signal).then(() => {
|
|
8916
10334
|
token.ensureCurrent();
|
|
8917
10335
|
});
|
|
8918
10336
|
void cameraReady.catch(() => {
|
|
@@ -8920,7 +10338,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8920
10338
|
beforePublish = () => cameraReady;
|
|
8921
10339
|
}
|
|
8922
10340
|
const initialQuality = this.resolveInitialUplinkQuality(localTrack);
|
|
8923
|
-
const initialFormat = initialQuality ?
|
|
10341
|
+
const initialFormat = initialQuality ? this.uplinkQualityFormat(initialQuality.baseline, initialQuality.level) : void 0;
|
|
8924
10342
|
const remote = await this.connectionManager.connect({
|
|
8925
10343
|
localTrack,
|
|
8926
10344
|
remoteVideoFormat: this.generationVideoFormat ?? localTrack.videoFormat,
|
|
@@ -8933,8 +10351,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8933
10351
|
this.qualityHistory?.write("uplink", initialQuality.level);
|
|
8934
10352
|
},
|
|
8935
10353
|
model: this.options.model,
|
|
8936
|
-
includeLocalAudio: this.
|
|
8937
|
-
publishLocalMedia:
|
|
10354
|
+
includeLocalAudio: this.mediaController.hasAudio,
|
|
10355
|
+
publishLocalMedia: this.mediaController.source !== "networkVideo",
|
|
8938
10356
|
ensureCurrent: () => token.ensureCurrent(),
|
|
8939
10357
|
onPublished: () => {
|
|
8940
10358
|
this.acceptsVideoStatistics = true;
|
|
@@ -8956,9 +10374,21 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8956
10374
|
}
|
|
8957
10375
|
/**
|
|
8958
10376
|
* 编排插帧、首帧计时和生成状态,任务与确认由生成管理器负责。
|
|
8959
|
-
|
|
8960
|
-
|
|
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) {
|
|
8961
10388
|
token.setFailureScope(0 /* connection */);
|
|
10389
|
+
if (restart) {
|
|
10390
|
+
void this.recordingController.stop("restarted" /* restarted */);
|
|
10391
|
+
}
|
|
8962
10392
|
const initialLevel = this.qualityHistory?.read("downlink") ?? 1;
|
|
8963
10393
|
const targetSize = this.downlinkQualityController ? qualityVideoSize(videoFormat, initialLevel) : void 0;
|
|
8964
10394
|
await this.prepareFrameInterpolation(targetSize ?? videoFormat, token);
|
|
@@ -8968,20 +10398,33 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8968
10398
|
};
|
|
8969
10399
|
const taskID = await this.generationManager.start({
|
|
8970
10400
|
videoFormat,
|
|
8971
|
-
targetSize,
|
|
8972
|
-
context,
|
|
8973
|
-
mirror: this.currentLocalTrack === this.cameraController.currentTrack && this.cameraController.currentTrack?.position === "front" /* front */,
|
|
8974
10401
|
signal: token.signal,
|
|
8975
10402
|
ensureCurrent: () => token.ensureCurrent(),
|
|
8976
|
-
onStartSent: this.localVideoController.isActive ? async () => {
|
|
8977
|
-
token.ensureCurrent();
|
|
8978
|
-
await this.localVideoController.start(token.signal);
|
|
8979
|
-
token.ensureCurrent();
|
|
8980
|
-
} : void 0,
|
|
8981
10403
|
waitUntilRemoteReady: () => this.connectionManager.waitUntilRemoteTrackReady(
|
|
8982
10404
|
REMOTE_FIRST_FRAME_TIMEOUT_MS,
|
|
8983
10405
|
token.signal
|
|
8984
|
-
)
|
|
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
|
|
8985
10428
|
});
|
|
8986
10429
|
const sessionID = this.connectionManager.currentSessionID;
|
|
8987
10430
|
if (!sessionID) {
|
|
@@ -8999,13 +10442,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
8999
10442
|
token
|
|
9000
10443
|
);
|
|
9001
10444
|
this.qualityHistory?.write("downlink", initialLevel);
|
|
9002
|
-
const onFinish = this.
|
|
9003
|
-
if (this.
|
|
9004
|
-
this.streamController.activateNetworkVideoCompletion(
|
|
10445
|
+
const onFinish = this.mediaController.onNetworkVideoFinish;
|
|
10446
|
+
if (this.mediaController.source === "networkVideo") {
|
|
10447
|
+
this.streamController.activateNetworkVideoCompletion(() => {
|
|
9005
10448
|
if (this.currentState.taskID === taskID && this.currentState.connectionState === "Generating" /* generating */) {
|
|
9006
|
-
|
|
10449
|
+
void this.recordingController.stop("generationEnded" /* generationEnded */);
|
|
10450
|
+
return onFinish?.();
|
|
9007
10451
|
}
|
|
9008
|
-
})
|
|
10452
|
+
});
|
|
9009
10453
|
}
|
|
9010
10454
|
return this.connectionManager.makeRemoteStream();
|
|
9011
10455
|
}
|
|
@@ -9023,7 +10467,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
9023
10467
|
* 统一清理顺序:停止心跳与生成,再断开连接;all 额外释放本地媒体。
|
|
9024
10468
|
*/
|
|
9025
10469
|
async performCleanup(scope, taskID) {
|
|
9026
|
-
this.
|
|
10470
|
+
const recordingFinished = this.recordingController.stop("generationEnded" /* generationEnded */);
|
|
10471
|
+
this.mediaController.pauseLocalVideo();
|
|
9027
10472
|
this.launchTimer.cancel();
|
|
9028
10473
|
this.remoteFrameDisplayHandler = void 0;
|
|
9029
10474
|
this.clearVideoStatistics();
|
|
@@ -9033,26 +10478,17 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
9033
10478
|
this.connectionManager.stopHeartbeat();
|
|
9034
10479
|
await this.generationManager.reset(taskID);
|
|
9035
10480
|
const sessionID = await this.connectionManager.disconnect();
|
|
10481
|
+
await recordingFinished;
|
|
9036
10482
|
if (scope === 1 /* all */) {
|
|
9037
|
-
const hadFileSource = this.networkVideoController.currentTrack !== void 0 || this.localVideoController.isActive;
|
|
9038
|
-
this.networkVideoController.stop();
|
|
9039
10483
|
this.generationVideoFormat = void 0;
|
|
9040
10484
|
this.disposePermissionWatch();
|
|
9041
10485
|
try {
|
|
9042
|
-
|
|
9043
|
-
await this.rtcManager.destroy();
|
|
9044
|
-
} else {
|
|
9045
|
-
await this.cameraController.stopLocalCameraStream();
|
|
9046
|
-
}
|
|
10486
|
+
await this.mediaController.stopLocalStream();
|
|
9047
10487
|
} catch (error) {
|
|
9048
10488
|
XmaxLogger.realtime.error(
|
|
9049
10489
|
() => `\u91CA\u653E\u672C\u5730\u5A92\u4F53\u5931\u8D25 (Failed to Release Local Media)
|
|
9050
10490
|
\u2514\u2500 ${XmaxLogger.localized("\u539F\u56E0\uFF1A", "Reason: ")}${XmaxError.from(error).message}`
|
|
9051
10491
|
);
|
|
9052
|
-
} finally {
|
|
9053
|
-
if (this.localVideoController.isActive) {
|
|
9054
|
-
await this.localVideoController.stop();
|
|
9055
|
-
}
|
|
9056
10492
|
}
|
|
9057
10493
|
}
|
|
9058
10494
|
return {
|
|
@@ -9630,13 +11066,6 @@ var RealtimeConfiguration = class {
|
|
|
9630
11066
|
}
|
|
9631
11067
|
};
|
|
9632
11068
|
|
|
9633
|
-
// src/Foundation/Media/Video/VideoContentMode.ts
|
|
9634
|
-
var VideoContentMode = /* @__PURE__ */ ((VideoContentMode2) => {
|
|
9635
|
-
VideoContentMode2["fit"] = "fit";
|
|
9636
|
-
VideoContentMode2["fill"] = "fill";
|
|
9637
|
-
return VideoContentMode2;
|
|
9638
|
-
})(VideoContentMode || {});
|
|
9639
|
-
|
|
9640
11069
|
// src/Render/Video/RenderStatisticsController.ts
|
|
9641
11070
|
var RenderStatisticsController = class {
|
|
9642
11071
|
/**
|
|
@@ -12146,6 +13575,498 @@ var RemoteVideoFramePipeline = class {
|
|
|
12146
13575
|
}
|
|
12147
13576
|
};
|
|
12148
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
|
+
|
|
12149
14070
|
// src/Render/Video/XmaxVideoView.ts
|
|
12150
14071
|
var OBJECT_FIT = {
|
|
12151
14072
|
["fit" /* fit */]: "contain",
|
|
@@ -12203,7 +14124,14 @@ var XmaxVideoView = class {
|
|
|
12203
14124
|
* DOM 挂载状态
|
|
12204
14125
|
*/
|
|
12205
14126
|
__publicField(this, "detached", false);
|
|
14127
|
+
/**
|
|
14128
|
+
* 轨迹交互配置与懒加载视图
|
|
14129
|
+
*/
|
|
14130
|
+
__publicField(this, "interactionEnabled", true);
|
|
14131
|
+
__publicField(this, "customTrajectoryRenderer");
|
|
14132
|
+
__publicField(this, "trajectoryOverlay");
|
|
12206
14133
|
this.contentMode = options?.videoContentMode ?? "fill" /* fill */;
|
|
14134
|
+
this.interactionEnabled = options?.isInteractionEnabled ?? true;
|
|
12207
14135
|
this.element = document.createElement("div");
|
|
12208
14136
|
this.element.style.position = "relative";
|
|
12209
14137
|
this.element.style.overflow = "hidden";
|
|
@@ -12220,6 +14148,36 @@ var XmaxVideoView = class {
|
|
|
12220
14148
|
this.applyContentMode();
|
|
12221
14149
|
this.element.appendChild(this.videoElement);
|
|
12222
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
|
+
}
|
|
12223
14181
|
/**
|
|
12224
14182
|
* 当前显示的视频轨道;切换时自动解绑旧轨道。
|
|
12225
14183
|
*/
|
|
@@ -12292,6 +14250,7 @@ var XmaxVideoView = class {
|
|
|
12292
14250
|
attach(to) {
|
|
12293
14251
|
this.detached = false;
|
|
12294
14252
|
to.appendChild(this.element);
|
|
14253
|
+
this.attachTrajectory();
|
|
12295
14254
|
if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && this.videoElement.autoplay && !this.videoElement.ended && this.videoElement.paused && typeof this.videoElement.play === "function") {
|
|
12296
14255
|
void this.videoElement.play().catch((error) => {
|
|
12297
14256
|
XmaxLogger.render.warning(() => `\u6062\u590D\u89C6\u9891\u64AD\u653E\u5931\u8D25 (Failed to Resume Video Playback)
|
|
@@ -12309,6 +14268,7 @@ var XmaxVideoView = class {
|
|
|
12309
14268
|
*/
|
|
12310
14269
|
detach() {
|
|
12311
14270
|
this.detached = true;
|
|
14271
|
+
this.disposeTrajectory();
|
|
12312
14272
|
this.statisticsController.stop();
|
|
12313
14273
|
this.stopInterpolation();
|
|
12314
14274
|
this.cancelFrameDisplayNotification();
|
|
@@ -12415,6 +14375,7 @@ var XmaxVideoView = class {
|
|
|
12415
14375
|
const binding = VideoRenderRegistry.binding(track);
|
|
12416
14376
|
if (binding) {
|
|
12417
14377
|
binding.attachHandler(this, this.contentMode);
|
|
14378
|
+
this.attachTrajectory();
|
|
12418
14379
|
return;
|
|
12419
14380
|
}
|
|
12420
14381
|
if (track.mediaStreamTrack) {
|
|
@@ -12429,6 +14390,7 @@ var XmaxVideoView = class {
|
|
|
12429
14390
|
* 解绑当前轨道并清空渲染内容。
|
|
12430
14391
|
*/
|
|
12431
14392
|
detachCurrentTrack() {
|
|
14393
|
+
this.disposeTrajectory();
|
|
12432
14394
|
this.statisticsController.stop();
|
|
12433
14395
|
const track = this.currentTrack;
|
|
12434
14396
|
if (track) {
|
|
@@ -12488,6 +14450,7 @@ var XmaxVideoView = class {
|
|
|
12488
14450
|
* 应用显示模式与镜像样式。
|
|
12489
14451
|
*/
|
|
12490
14452
|
applyContentMode() {
|
|
14453
|
+
this.trajectoryOverlay?.configure(this.contentMode, this.mirrored, this.interactionEnabled);
|
|
12491
14454
|
this.videoElement.style.objectFit = OBJECT_FIT[this.contentMode];
|
|
12492
14455
|
this.videoElement.style.transform = this.mirrored ? "scaleX(-1)" : "";
|
|
12493
14456
|
if (this.interpolationCanvas) {
|
|
@@ -12501,6 +14464,30 @@ var XmaxVideoView = class {
|
|
|
12501
14464
|
startRenderStatistics() {
|
|
12502
14465
|
if (!this.detached) this.statisticsController.start();
|
|
12503
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
|
+
}
|
|
12504
14491
|
};
|
|
12505
14492
|
|
|
12506
14493
|
// src/Render/Video/XmaxRealtimeVideoView.ts
|
|
@@ -12539,8 +14526,8 @@ var XmaxRealtimeVideoView = class {
|
|
|
12539
14526
|
this.element.style.position = "relative";
|
|
12540
14527
|
this.element.style.overflow = "hidden";
|
|
12541
14528
|
this.element.style.backgroundColor = "black";
|
|
12542
|
-
this.localVideoView = new XmaxVideoView({ videoContentMode: this.contentMode });
|
|
12543
|
-
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 });
|
|
12544
14531
|
for (const view of [this.localVideoView, this.remoteVideoView]) {
|
|
12545
14532
|
view.element.style.position = "absolute";
|
|
12546
14533
|
view.element.style.inset = "0";
|
|
@@ -12555,6 +14542,30 @@ var XmaxRealtimeVideoView = class {
|
|
|
12555
14542
|
get localTrack() {
|
|
12556
14543
|
return this.currentLocalTrack;
|
|
12557
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
|
+
}
|
|
12558
14569
|
/**
|
|
12559
14570
|
* 替换本地预览轨道;轨道未变化时保留当前绑定。
|
|
12560
14571
|
*/
|
|
@@ -12686,6 +14697,9 @@ export {
|
|
|
12686
14697
|
RealtimeVideoTrack,
|
|
12687
14698
|
RealtimeMediaStream,
|
|
12688
14699
|
RealtimeVideoSampleMethod,
|
|
14700
|
+
VideoContentMode,
|
|
14701
|
+
RealtimeRecordingEventType,
|
|
14702
|
+
RealtimeRecordingFinishReason,
|
|
12689
14703
|
RealtimeContext,
|
|
12690
14704
|
RealtimeConnectionState,
|
|
12691
14705
|
RealtimeReason,
|
|
@@ -12696,8 +14710,7 @@ export {
|
|
|
12696
14710
|
NonRealtimeTaskError,
|
|
12697
14711
|
XmaxClient,
|
|
12698
14712
|
RealtimeConfiguration,
|
|
12699
|
-
VideoContentMode,
|
|
12700
14713
|
XmaxVideoView,
|
|
12701
14714
|
XmaxRealtimeVideoView
|
|
12702
14715
|
};
|
|
12703
|
-
//# sourceMappingURL=chunk-
|
|
14716
|
+
//# sourceMappingURL=chunk-ILZL4W2G.js.map
|