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