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