@xmaxai/web-sdk 1.0.4 → 1.0.6
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 +248 -21
- package/dist/{VideoContentMode-BM97_H8V.d.cts → VideoRenderStatistics-C7zqzfuS.d.cts} +23 -1
- package/dist/{VideoContentMode-BM97_H8V.d.ts → VideoRenderStatistics-C7zqzfuS.d.ts} +23 -1
- package/dist/{chunk-FEL5MJMJ.cjs → chunk-NWNRFTLE.cjs} +2627 -505
- package/dist/chunk-NWNRFTLE.cjs.map +1 -0
- package/dist/{chunk-FMAQXPTE.js → chunk-PLFWPS5A.js} +2439 -317
- package/dist/chunk-PLFWPS5A.js.map +1 -0
- package/dist/index.cjs +14 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +387 -96
- package/dist/index.d.ts +387 -96
- package/dist/index.js +15 -3
- package/dist/react/index.cjs +6 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +5 -1
- package/dist/react/index.d.ts +5 -1
- package/dist/react/index.js +4 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- 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.6";
|
|
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,50 +588,68 @@ 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";
|
|
567
602
|
RealtimeModel2["x2_0_trtc"] = "x2.0-trtc";
|
|
568
|
-
RealtimeModel2["x2_0_agora"] = "x2.0-agora";
|
|
569
603
|
RealtimeModel2["x2_1_preview"] = "x2.1-preview";
|
|
570
604
|
return RealtimeModel2;
|
|
571
605
|
})(RealtimeModel || {});
|
|
606
|
+
function isKnownModel(model) {
|
|
607
|
+
if (typeof model !== "string" || !model.trim()) {
|
|
608
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Realtime model must be a non-empty string");
|
|
609
|
+
}
|
|
610
|
+
return Object.values(RealtimeModel).includes(model);
|
|
611
|
+
}
|
|
612
|
+
function modelDefaults(model) {
|
|
613
|
+
return isKnownModel(model) ? model : "x2.1-preview" /* x2_1_preview */;
|
|
614
|
+
}
|
|
615
|
+
var MODEL_DISPLAY_NAMES = {
|
|
616
|
+
["x2.0" /* x2_0 */]: "X2.0",
|
|
617
|
+
["x2.0-trtc" /* x2_0_trtc */]: "X2.1-preview-trtc",
|
|
618
|
+
["x2.1-preview" /* x2_1_preview */]: "X2.1-preview-agora"
|
|
619
|
+
};
|
|
572
620
|
var MODEL_BASE_URLS = {
|
|
573
|
-
["x2.
|
|
574
|
-
["x2.1-preview" /* x2_1_preview */]: "https://dev.xmaxai.com/open/api/v1"
|
|
621
|
+
["x2.1-preview" /* x2_1_preview */]: "https://cloud.xmax.22duck.cn/open/api/v1"
|
|
575
622
|
};
|
|
576
623
|
var RESOLUTION_BUCKETS = {
|
|
577
|
-
["x2.
|
|
578
|
-
|
|
579
|
-
{ width: 1920, height: 1024 }
|
|
580
|
-
],
|
|
581
|
-
["x2.0-agora" /* x2_0_agora */]: [
|
|
624
|
+
["x2.0" /* x2_0 */]: [],
|
|
625
|
+
["x2.0-trtc" /* x2_0_trtc */]: [
|
|
582
626
|
{ width: 1024, height: 1920 },
|
|
583
627
|
{ width: 1920, height: 1024 }
|
|
584
628
|
],
|
|
585
|
-
["x2.
|
|
586
|
-
["x2.0-trtc" /* x2_0_trtc */]: [
|
|
629
|
+
["x2.1-preview" /* x2_1_preview */]: [
|
|
587
630
|
{ width: 1024, height: 1920 },
|
|
588
631
|
{ width: 1920, height: 1024 }
|
|
589
632
|
]
|
|
590
633
|
};
|
|
591
634
|
var MAXIMUM_INPUT_PIXELS = {
|
|
592
|
-
["x2.1-preview" /* x2_1_preview */]: 21e5,
|
|
593
|
-
["x2.0-agora" /* x2_0_agora */]: 21e5,
|
|
594
635
|
["x2.0" /* x2_0 */]: 128e4,
|
|
595
|
-
["x2.0-trtc" /* x2_0_trtc */]: 21e5
|
|
636
|
+
["x2.0-trtc" /* x2_0_trtc */]: 21e5,
|
|
637
|
+
["x2.1-preview" /* x2_1_preview */]: 21e5
|
|
596
638
|
};
|
|
639
|
+
function modelDisplayName(model) {
|
|
640
|
+
return isKnownModel(model) ? MODEL_DISPLAY_NAMES[model] : model;
|
|
641
|
+
}
|
|
597
642
|
function modelBaseURL(model) {
|
|
598
|
-
return MODEL_BASE_URLS[model];
|
|
643
|
+
return MODEL_BASE_URLS[modelDefaults(model)];
|
|
599
644
|
}
|
|
600
645
|
function resolutionBuckets(model) {
|
|
601
|
-
return RESOLUTION_BUCKETS[model];
|
|
646
|
+
return RESOLUTION_BUCKETS[modelDefaults(model)];
|
|
602
647
|
}
|
|
603
648
|
function minimumInputPixels(_model) {
|
|
604
649
|
return 6e5;
|
|
605
650
|
}
|
|
606
651
|
function maximumInputPixels(model) {
|
|
607
|
-
return MAXIMUM_INPUT_PIXELS[model];
|
|
652
|
+
return MAXIMUM_INPUT_PIXELS[modelDefaults(model)];
|
|
608
653
|
}
|
|
609
654
|
function inputSizeAlignment(_model) {
|
|
610
655
|
return 32;
|
|
@@ -613,11 +658,23 @@ function defaultFrameRate(_model) {
|
|
|
613
658
|
return 30;
|
|
614
659
|
}
|
|
615
660
|
function defaultCameraVideoFormat(model) {
|
|
616
|
-
if (model === "x2.0" /* x2_0 */) {
|
|
661
|
+
if (modelDefaults(model) === "x2.0" /* x2_0 */) {
|
|
617
662
|
return new RealtimeVideoFormat({ width: 832, height: 1472, fps: 30 });
|
|
618
663
|
}
|
|
619
664
|
return new RealtimeVideoFormat({ width: 1024, height: 1920, fps: 30 });
|
|
620
665
|
}
|
|
666
|
+
var MODEL_RTC_PROVIDERS = {
|
|
667
|
+
["x2.0" /* x2_0 */]: Object.freeze(["vertc" /* vertc */]),
|
|
668
|
+
["x2.0-trtc" /* x2_0_trtc */]: Object.freeze(["trtc" /* trtc */]),
|
|
669
|
+
["x2.1-preview" /* x2_1_preview */]: Object.freeze(["agora" /* agora */])
|
|
670
|
+
};
|
|
671
|
+
function supportedRtcProviders(model) {
|
|
672
|
+
return isKnownModel(model) ? MODEL_RTC_PROVIDERS[model] : CUSTOM_MODEL_RTC_PROVIDERS;
|
|
673
|
+
}
|
|
674
|
+
var CUSTOM_MODEL_RTC_PROVIDERS = Object.freeze([
|
|
675
|
+
...MODEL_RTC_PROVIDERS["x2.1-preview" /* x2_1_preview */],
|
|
676
|
+
...Object.values(RtcProvider).filter((provider) => !MODEL_RTC_PROVIDERS["x2.1-preview" /* x2_1_preview */].includes(provider))
|
|
677
|
+
]);
|
|
621
678
|
|
|
622
679
|
// src/Foundation/Media/Video/FrameInterpolationSupport.ts
|
|
623
680
|
async function frameInterpolationAdapter(size) {
|
|
@@ -802,7 +859,14 @@ var CONTENT_TYPES = {
|
|
|
802
859
|
gif: "image/gif",
|
|
803
860
|
bmp: "image/bmp",
|
|
804
861
|
heic: "image/heic",
|
|
805
|
-
heif: "image/heif"
|
|
862
|
+
heif: "image/heif",
|
|
863
|
+
mp4: "video/mp4",
|
|
864
|
+
m4v: "video/mp4",
|
|
865
|
+
mov: "video/quicktime",
|
|
866
|
+
webm: "video/webm",
|
|
867
|
+
avi: "video/x-msvideo",
|
|
868
|
+
mkv: "video/x-matroska",
|
|
869
|
+
ogv: "video/ogg"
|
|
806
870
|
};
|
|
807
871
|
var StorageService = class _StorageService {
|
|
808
872
|
/**
|
|
@@ -836,6 +900,81 @@ var StorageService = class _StorageService {
|
|
|
836
900
|
* 上传图片并返回访问地址。
|
|
837
901
|
*/
|
|
838
902
|
async uploadImage(options) {
|
|
903
|
+
return this.upload(options, "Image");
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* 普通上传原始视频并返回访问地址,不进行转码、压缩或分片。
|
|
907
|
+
*/
|
|
908
|
+
async uploadVideo(options) {
|
|
909
|
+
return this.upload(options, "Video");
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* 下载原始视频文件并交给浏览器保存,支持取消网络下载。
|
|
913
|
+
*/
|
|
914
|
+
async downloadVideo(options) {
|
|
915
|
+
const fileName = _StorageService.validateFileName(options.fileName);
|
|
916
|
+
let url;
|
|
917
|
+
try {
|
|
918
|
+
url = new URL(options.url.trim());
|
|
919
|
+
} catch {
|
|
920
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Invalid video download URL");
|
|
921
|
+
}
|
|
922
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
|
|
923
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video download requires an HTTP(S) URL without embedded credentials");
|
|
924
|
+
}
|
|
925
|
+
if (typeof document === "undefined" || !document.body) {
|
|
926
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video saving requires a browser document");
|
|
927
|
+
}
|
|
928
|
+
let objectURL;
|
|
929
|
+
let link;
|
|
930
|
+
let saveRequested = false;
|
|
931
|
+
try {
|
|
932
|
+
if (options.signal?.aborted) {
|
|
933
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
|
|
934
|
+
}
|
|
935
|
+
const response = await fetch(options.url.trim(), { credentials: "omit", signal: options.signal });
|
|
936
|
+
if (!response.ok) {
|
|
937
|
+
throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, "Video download failed", { httpStatus: response.status });
|
|
938
|
+
}
|
|
939
|
+
const data = await response.blob();
|
|
940
|
+
if (options.signal?.aborted) {
|
|
941
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
|
|
942
|
+
}
|
|
943
|
+
if (data.size === 0) {
|
|
944
|
+
throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, "Downloaded video is empty");
|
|
945
|
+
}
|
|
946
|
+
objectURL = URL.createObjectURL(data);
|
|
947
|
+
link = document.createElement("a");
|
|
948
|
+
link.href = objectURL;
|
|
949
|
+
link.download = fileName;
|
|
950
|
+
link.hidden = true;
|
|
951
|
+
document.body.appendChild(link);
|
|
952
|
+
link.click();
|
|
953
|
+
saveRequested = true;
|
|
954
|
+
} catch (error) {
|
|
955
|
+
if (options.signal?.aborted) {
|
|
956
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "Video download was cancelled");
|
|
957
|
+
}
|
|
958
|
+
if (error instanceof XmaxError) {
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
throw new XmaxError("DOWNLOAD_ERROR" /* downloadError */, error instanceof Error ? error.message : String(error));
|
|
962
|
+
} finally {
|
|
963
|
+
link?.remove();
|
|
964
|
+
if (objectURL) {
|
|
965
|
+
const resourceURL = objectURL;
|
|
966
|
+
if (saveRequested) {
|
|
967
|
+
setTimeout(() => URL.revokeObjectURL(resourceURL), 6e4);
|
|
968
|
+
} else {
|
|
969
|
+
URL.revokeObjectURL(resourceURL);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* 获取临时凭证并通过 COS putObject 上传文件,统一处理进度、结果和错误。
|
|
976
|
+
*/
|
|
977
|
+
async upload(options, mediaType) {
|
|
839
978
|
const startedAt = Date.now();
|
|
840
979
|
try {
|
|
841
980
|
const safeName = _StorageService.validateFileName(options.fileName);
|
|
@@ -844,7 +983,7 @@ var StorageService = class _StorageService {
|
|
|
844
983
|
if (body.size === 0) {
|
|
845
984
|
throw new XmaxError(
|
|
846
985
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
847
|
-
|
|
986
|
+
`${mediaType} data cannot be empty`
|
|
848
987
|
);
|
|
849
988
|
}
|
|
850
989
|
const temporary = await this.fetchStorageConfiguration();
|
|
@@ -982,7 +1121,7 @@ var StorageService = class _StorageService {
|
|
|
982
1121
|
if (!safeName || safeName.includes("/") || safeName.includes("\\")) {
|
|
983
1122
|
throw new XmaxError(
|
|
984
1123
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
985
|
-
"Invalid file name for storage
|
|
1124
|
+
"Invalid file name for storage operation"
|
|
986
1125
|
);
|
|
987
1126
|
}
|
|
988
1127
|
return safeName;
|
|
@@ -1121,6 +1260,11 @@ ${JSON.stringify({
|
|
|
1121
1260
|
receivedBytes: valid(rtc.RecvBytes),
|
|
1122
1261
|
localVideo,
|
|
1123
1262
|
remoteVideo: remote,
|
|
1263
|
+
capture: {
|
|
1264
|
+
width: valid(local.captureResolutionWidth, 1),
|
|
1265
|
+
height: valid(local.captureResolutionHeight, 1),
|
|
1266
|
+
frameRate: valid(local.captureFrameRate)
|
|
1267
|
+
},
|
|
1124
1268
|
localAudio: client.getLocalAudioStats(),
|
|
1125
1269
|
remoteAudio: client.getRemoteAudioStats()
|
|
1126
1270
|
}, null, 2)}`, 2 /* performance */);
|
|
@@ -1168,6 +1312,59 @@ function rtcOrientedVideoSize(size, environment) {
|
|
|
1168
1312
|
return shouldTransposeRtcVideoSize(environment) ? { width: size.height, height: size.width } : size;
|
|
1169
1313
|
}
|
|
1170
1314
|
|
|
1315
|
+
// src/Foundation/RTC/Agora/AgoraVideoSender.ts
|
|
1316
|
+
var AgoraVideoSender = class {
|
|
1317
|
+
static async apply(track, config, ensureActive2) {
|
|
1318
|
+
ensureActive2();
|
|
1319
|
+
const sender = track.getRTCRtpTransceiver()?.sender;
|
|
1320
|
+
if (!sender) throw new XmaxError("RTC_ERROR" /* rtcError */, "WebRTC video sender is unavailable");
|
|
1321
|
+
const capture = sender.track?.getSettings();
|
|
1322
|
+
if (!capture?.width || !capture.height) {
|
|
1323
|
+
throw new XmaxError("RTC_ERROR" /* rtcError */, "WebRTC capture dimensions are unavailable");
|
|
1324
|
+
}
|
|
1325
|
+
const scale = Math.max(1, capture.width / config.width, capture.height / config.height);
|
|
1326
|
+
const parameters = sender.getParameters();
|
|
1327
|
+
if (parameters.encodings?.length !== 1) {
|
|
1328
|
+
throw new XmaxError("RTC_ERROR" /* rtcError */, "Sender-only configuration requires one video encoding");
|
|
1329
|
+
}
|
|
1330
|
+
const encoding = parameters.encodings[0];
|
|
1331
|
+
const previous = { ...encoding };
|
|
1332
|
+
const previousPreference = parameters.degradationPreference;
|
|
1333
|
+
encoding.scaleResolutionDownBy = scale;
|
|
1334
|
+
encoding.maxFramerate = config.frameRate;
|
|
1335
|
+
encoding.maxBitrate = Math.round(config.maximumBitrate * 1e3);
|
|
1336
|
+
parameters.degradationPreference = config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "maintain-framerate" : "maintain-resolution";
|
|
1337
|
+
await sender.setParameters(parameters);
|
|
1338
|
+
ensureActive2();
|
|
1339
|
+
if (track.getRTCRtpTransceiver()?.sender !== sender) {
|
|
1340
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "WebRTC video sender changed during configuration");
|
|
1341
|
+
}
|
|
1342
|
+
const applied = sender.getParameters();
|
|
1343
|
+
const actual = applied.encodings?.[0];
|
|
1344
|
+
if (!actual || Math.abs((actual.scaleResolutionDownBy ?? 1) - scale) > 1e-4 || actual.maxFramerate !== encoding.maxFramerate || actual.maxBitrate !== encoding.maxBitrate) {
|
|
1345
|
+
if (actual && applied.encodings.length === 1) {
|
|
1346
|
+
for (const key of ["scaleResolutionDownBy", "maxFramerate", "maxBitrate"]) {
|
|
1347
|
+
if (previous[key] === void 0) delete actual[key];
|
|
1348
|
+
else actual[key] = previous[key];
|
|
1349
|
+
}
|
|
1350
|
+
if (previousPreference === void 0) delete applied.degradationPreference;
|
|
1351
|
+
else applied.degradationPreference = previousPreference;
|
|
1352
|
+
await sender.setParameters(applied);
|
|
1353
|
+
}
|
|
1354
|
+
throw new XmaxError("RTC_ERROR" /* rtcError */, "Browser did not retain WebRTC video sender parameters");
|
|
1355
|
+
}
|
|
1356
|
+
XmaxLogger.rtc.info(() => `Agora \u53D1\u9001\u53C2\u6570\u5B9E\u9A8C (Agora Sender Parameters)
|
|
1357
|
+
${JSON.stringify({
|
|
1358
|
+
capture: { width: capture.width, height: capture.height, frameRate: capture.frameRate },
|
|
1359
|
+
target: { width: config.width, height: config.height, frameRate: config.frameRate },
|
|
1360
|
+
scaleResolutionDownBy: actual.scaleResolutionDownBy,
|
|
1361
|
+
maxFramerate: actual.maxFramerate,
|
|
1362
|
+
maxBitrateKbps: actual.maxBitrate / 1e3,
|
|
1363
|
+
minimumBitrateApplied: false
|
|
1364
|
+
}, null, 2)}`, 2 /* performance */);
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1171
1368
|
// src/Foundation/RTC/Agora/AgoraRtcManager.ts
|
|
1172
1369
|
var _AgoraRtcManager = class _AgoraRtcManager {
|
|
1173
1370
|
/**
|
|
@@ -1185,6 +1382,12 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1185
1382
|
__publicField(this, "lifecycle", 0);
|
|
1186
1383
|
__publicField(this, "camera");
|
|
1187
1384
|
__publicField(this, "microphone");
|
|
1385
|
+
__publicField(this, "externalVideo");
|
|
1386
|
+
__publicField(this, "externalAudio");
|
|
1387
|
+
__publicField(this, "appliedOptimizationModes", /* @__PURE__ */ new WeakMap());
|
|
1388
|
+
__publicField(this, "cameraSenderConfiguration");
|
|
1389
|
+
__publicField(this, "senderUpdateTail", Promise.resolve());
|
|
1390
|
+
__publicField(this, "cameraTransceiverListener");
|
|
1188
1391
|
__publicField(this, "connection");
|
|
1189
1392
|
__publicField(this, "roomVersion", 0);
|
|
1190
1393
|
/**
|
|
@@ -1201,6 +1404,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1201
1404
|
*/
|
|
1202
1405
|
__publicField(this, "listener");
|
|
1203
1406
|
__publicField(this, "statisticsTimer");
|
|
1407
|
+
__publicField(this, "networkQualitySuspended", false);
|
|
1204
1408
|
__publicField(this, "audioVolumes", /* @__PURE__ */ new Map());
|
|
1205
1409
|
__publicField(this, "requestedAudio", /* @__PURE__ */ new Set());
|
|
1206
1410
|
/**
|
|
@@ -1267,12 +1471,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1267
1471
|
async destroy() {
|
|
1268
1472
|
this.lifecycle++;
|
|
1269
1473
|
const client = this.client;
|
|
1270
|
-
this.listener = void 0;
|
|
1271
1474
|
this.clearRoom();
|
|
1272
1475
|
this.client = void 0;
|
|
1273
1476
|
client?.removeAllListeners();
|
|
1477
|
+
this.releaseCameraSender();
|
|
1274
1478
|
this.camera?.close();
|
|
1275
1479
|
this.microphone?.close();
|
|
1480
|
+
this.externalVideo?.close();
|
|
1481
|
+
this.externalAudio?.close();
|
|
1482
|
+
this.externalVideo = void 0;
|
|
1483
|
+
this.externalAudio = void 0;
|
|
1276
1484
|
this.camera = void 0;
|
|
1277
1485
|
this.microphone = void 0;
|
|
1278
1486
|
const sdk = this.sdk;
|
|
@@ -1291,7 +1499,9 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1291
1499
|
*/
|
|
1292
1500
|
async startCameraCapture(options) {
|
|
1293
1501
|
const client = this.requireClient();
|
|
1294
|
-
if (this.camera
|
|
1502
|
+
if (this.camera || this.externalVideo) {
|
|
1503
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "A local video source is already active");
|
|
1504
|
+
}
|
|
1295
1505
|
try {
|
|
1296
1506
|
const captureSize = rtcOrientedVideoSize(options);
|
|
1297
1507
|
const camera = await this.sdk.createCameraVideoTrack({
|
|
@@ -1303,6 +1513,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1303
1513
|
throw this.cancelled();
|
|
1304
1514
|
}
|
|
1305
1515
|
this.camera = camera;
|
|
1516
|
+
this.cameraTransceiverListener = () => {
|
|
1517
|
+
if (this.client === client && client.localTracks.includes(camera) && this.videoPublishRequested && !this.rejoinTask) {
|
|
1518
|
+
void this.restoreCameraSender(camera).catch(() => {
|
|
1519
|
+
if (this.camera === camera && this.videoPublishRequested) {
|
|
1520
|
+
XmaxLogger.rtc.warning(() => "Agora \u53D1\u9001\u53C2\u6570\u6062\u590D\u5931\u8D25 (Agora Sender Parameters Restore Failed)");
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
camera.on("transceiver-updated", this.cameraTransceiverListener);
|
|
1306
1526
|
return camera.getMediaStreamTrack();
|
|
1307
1527
|
} catch (error) {
|
|
1308
1528
|
throw this.mapError(error, "CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */);
|
|
@@ -1315,6 +1535,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1315
1535
|
const camera = this.requireCamera();
|
|
1316
1536
|
await this.run(() => camera.setDevice({ facingMode: to === "front" /* front */ ? "user" : "environment" }));
|
|
1317
1537
|
if (this.camera !== camera) throw this.cancelled();
|
|
1538
|
+
if (this.videoPublishRequested) await this.restoreCameraSender(camera);
|
|
1318
1539
|
return camera.getMediaStreamTrack();
|
|
1319
1540
|
}
|
|
1320
1541
|
/**
|
|
@@ -1322,15 +1543,62 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1322
1543
|
*/
|
|
1323
1544
|
async stopCameraCapture() {
|
|
1324
1545
|
const camera = this.camera;
|
|
1546
|
+
this.releaseCameraSender();
|
|
1325
1547
|
this.camera = void 0;
|
|
1326
1548
|
camera?.close();
|
|
1327
1549
|
}
|
|
1328
1550
|
/**
|
|
1329
|
-
*
|
|
1551
|
+
* 将外部原生轨包装为 Agora 本地轨,不申请设备权限;失败时释放本次克隆。
|
|
1552
|
+
*/
|
|
1553
|
+
async setExternalMediaTracks(tracks) {
|
|
1554
|
+
this.requireClient();
|
|
1555
|
+
if (this.camera || this.microphone || this.externalVideo) {
|
|
1556
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
|
|
1557
|
+
}
|
|
1558
|
+
if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
|
|
1559
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media requires live video and optional audio tracks");
|
|
1560
|
+
}
|
|
1561
|
+
const video = tracks.videoTrack.clone();
|
|
1562
|
+
let audio;
|
|
1563
|
+
let wrappedVideo;
|
|
1564
|
+
let wrappedAudio;
|
|
1565
|
+
try {
|
|
1566
|
+
audio = tracks.audioTrack?.clone();
|
|
1567
|
+
wrappedVideo = this.sdk.createCustomVideoTrack({ mediaStreamTrack: video });
|
|
1568
|
+
if (audio) {
|
|
1569
|
+
wrappedAudio = this.sdk.createCustomAudioTrack({ mediaStreamTrack: audio, encoderConfig: "music_standard" });
|
|
1570
|
+
}
|
|
1571
|
+
this.externalVideo = wrappedVideo;
|
|
1572
|
+
this.externalAudio = wrappedAudio;
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
wrappedVideo?.close();
|
|
1575
|
+
wrappedAudio?.close();
|
|
1576
|
+
video.stop();
|
|
1577
|
+
audio?.stop();
|
|
1578
|
+
throw this.mapError(error);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* 摄像头实验走原生 sender(发布前暂存);外部视频仍走 Agora 编码配置。
|
|
1330
1583
|
*/
|
|
1331
1584
|
async configureVideoEncoding(config) {
|
|
1332
|
-
const camera = this.
|
|
1333
|
-
|
|
1585
|
+
const camera = this.requireLocalVideo();
|
|
1586
|
+
if (camera === this.camera) {
|
|
1587
|
+
const client = this.requireClient(), version = this.roomVersion;
|
|
1588
|
+
const published = this.videoPublishRequested;
|
|
1589
|
+
await this.run(() => this.queueSenderUpdate(async () => {
|
|
1590
|
+
const ensureActive2 = () => {
|
|
1591
|
+
if (this.camera !== camera || this.client !== client || this.roomVersion !== version || published && !this.videoPublishRequested) throw this.cancelled();
|
|
1592
|
+
};
|
|
1593
|
+
ensureActive2();
|
|
1594
|
+
if (this.videoPublishRequested) {
|
|
1595
|
+
await AgoraVideoSender.apply(camera, { ...config, ...rtcOrientedVideoSize(config) }, ensureActive2);
|
|
1596
|
+
}
|
|
1597
|
+
this.cameraSenderConfiguration = { ...config };
|
|
1598
|
+
}));
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const encodingSize = config;
|
|
1334
1602
|
await this.run(async () => {
|
|
1335
1603
|
await camera.setEncoderConfiguration({
|
|
1336
1604
|
width: encodingSize.width,
|
|
@@ -1339,9 +1607,37 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1339
1607
|
bitrateMin: config.minimumBitrate,
|
|
1340
1608
|
bitrateMax: config.maximumBitrate
|
|
1341
1609
|
});
|
|
1342
|
-
|
|
1610
|
+
if (this.appliedOptimizationModes.get(camera) !== config.encoderPreference) {
|
|
1611
|
+
await camera.setOptimizationMode(config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail");
|
|
1612
|
+
this.appliedOptimizationModes.set(camera, config.encoderPreference);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
queueSenderUpdate(operation) {
|
|
1617
|
+
const next = this.senderUpdateTail.then(operation);
|
|
1618
|
+
this.senderUpdateTail = next.catch(() => {
|
|
1619
|
+
});
|
|
1620
|
+
return next;
|
|
1621
|
+
}
|
|
1622
|
+
/** 发布/重建 sender 后恢复最近成功的目标配置;排队时不捕获过期的参数对象。 */
|
|
1623
|
+
restoreCameraSender(camera) {
|
|
1624
|
+
const client = this.client, version = this.roomVersion;
|
|
1625
|
+
return this.queueSenderUpdate(async () => {
|
|
1626
|
+
const ensureActive2 = () => {
|
|
1627
|
+
if (this.camera !== camera || this.client !== client || this.roomVersion !== version || !this.videoPublishRequested) {
|
|
1628
|
+
throw this.cancelled();
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
ensureActive2();
|
|
1632
|
+
const config = this.cameraSenderConfiguration;
|
|
1633
|
+
if (config) await AgoraVideoSender.apply(camera, { ...config, ...rtcOrientedVideoSize(config) }, ensureActive2);
|
|
1343
1634
|
});
|
|
1344
1635
|
}
|
|
1636
|
+
releaseCameraSender() {
|
|
1637
|
+
if (this.cameraTransceiverListener) this.camera?.off("transceiver-updated", this.cameraTransceiverListener);
|
|
1638
|
+
this.cameraTransceiverListener = void 0;
|
|
1639
|
+
this.cameraSenderConfiguration = void 0;
|
|
1640
|
+
}
|
|
1345
1641
|
/**
|
|
1346
1642
|
* 使用会话下发的字符串频道与 UID 入房;不采信后端区域覆盖。
|
|
1347
1643
|
*/
|
|
@@ -1349,7 +1645,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1349
1645
|
await this.rejoinTask?.catch(() => {
|
|
1350
1646
|
});
|
|
1351
1647
|
const client = this.requireClient();
|
|
1352
|
-
if (configuration.provider !== "agora" || this.connection) {
|
|
1648
|
+
if (configuration.provider !== "agora" /* agora */ || this.connection) {
|
|
1353
1649
|
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Agora credentials and an idle room are required");
|
|
1354
1650
|
}
|
|
1355
1651
|
const version = ++this.roomVersion;
|
|
@@ -1385,7 +1681,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1385
1681
|
if (signal?.aborted) throw this.cancelled();
|
|
1386
1682
|
const current = this.connection;
|
|
1387
1683
|
const client = this.requireClient();
|
|
1388
|
-
if (!current || config.provider !== "agora" || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
|
|
1684
|
+
if (!current || config.provider !== "agora" /* agora */ || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
|
|
1389
1685
|
throw new XmaxError("SESSION_ERROR" /* sessionError */, "Agora session binding changed");
|
|
1390
1686
|
}
|
|
1391
1687
|
const version = this.roomVersion;
|
|
@@ -1454,12 +1750,15 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1454
1750
|
await this.run(() => client.leave());
|
|
1455
1751
|
throw this.cancelled();
|
|
1456
1752
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1753
|
+
const video = this.externalVideo ?? this.camera;
|
|
1754
|
+
const audio = this.externalVideo ? this.externalAudio : this.microphone;
|
|
1755
|
+
if (this.videoPublishRequested && video) {
|
|
1756
|
+
await this.run(() => client.publish(video));
|
|
1459
1757
|
this.ensureRoomCurrent(client, version);
|
|
1758
|
+
if (video === this.camera) await this.restoreCameraSender(this.camera);
|
|
1460
1759
|
}
|
|
1461
|
-
if (this.audioPublishRequested &&
|
|
1462
|
-
await this.run(() => client.publish(
|
|
1760
|
+
if (this.audioPublishRequested && audio) {
|
|
1761
|
+
await this.run(() => client.publish(audio));
|
|
1463
1762
|
this.ensureRoomCurrent(client, version);
|
|
1464
1763
|
}
|
|
1465
1764
|
for (const user of client.remoteUsers) {
|
|
@@ -1480,12 +1779,13 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1480
1779
|
*/
|
|
1481
1780
|
async publishLocalVideo() {
|
|
1482
1781
|
const client = this.requireClient();
|
|
1483
|
-
const camera = this.
|
|
1782
|
+
const camera = this.requireLocalVideo();
|
|
1484
1783
|
this.videoPublishRequested = true;
|
|
1485
1784
|
await this.rejoinOperation;
|
|
1486
1785
|
if (client !== this.client || !this.videoPublishRequested) throw this.cancelled();
|
|
1487
1786
|
if (client.localTracks.includes(camera)) return;
|
|
1488
1787
|
await this.run(() => client.publish(camera));
|
|
1788
|
+
if (camera === this.camera) await this.run(() => this.restoreCameraSender(this.camera));
|
|
1489
1789
|
}
|
|
1490
1790
|
/**
|
|
1491
1791
|
* 停止视频发布但保留采集与预览。
|
|
@@ -1493,10 +1793,13 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1493
1793
|
async unpublishLocalVideo() {
|
|
1494
1794
|
this.videoPublishRequested = false;
|
|
1495
1795
|
if (this.rejoinTask || this.tokenExpired) return;
|
|
1496
|
-
|
|
1796
|
+
const video = this.externalVideo ?? this.camera;
|
|
1797
|
+
if (this.client && video) {
|
|
1798
|
+
await this.run(() => this.client.unpublish(video));
|
|
1799
|
+
}
|
|
1497
1800
|
}
|
|
1498
1801
|
/**
|
|
1499
|
-
*
|
|
1802
|
+
* 外部源只发布提供的音轨;相机源首次发布时申请麦克风,迟到采集立即释放。
|
|
1500
1803
|
*/
|
|
1501
1804
|
async publishLocalAudio() {
|
|
1502
1805
|
const client = this.requireClient();
|
|
@@ -1504,6 +1807,13 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1504
1807
|
await this.rejoinOperation;
|
|
1505
1808
|
const version = this.roomVersion;
|
|
1506
1809
|
if (client !== this.client || !this.audioPublishRequested) throw this.cancelled();
|
|
1810
|
+
if (this.externalVideo) {
|
|
1811
|
+
const audio = this.externalAudio;
|
|
1812
|
+
if (audio && !client.localTracks.includes(audio)) {
|
|
1813
|
+
await this.run(() => client.publish(audio));
|
|
1814
|
+
}
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1507
1817
|
try {
|
|
1508
1818
|
if (!this.microphone) {
|
|
1509
1819
|
const track = await this.sdk.createMicrophoneAudioTrack();
|
|
@@ -1524,7 +1834,10 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1524
1834
|
async unpublishLocalAudio() {
|
|
1525
1835
|
this.audioPublishRequested = false;
|
|
1526
1836
|
if (this.rejoinTask || this.tokenExpired) return;
|
|
1527
|
-
|
|
1837
|
+
const audio = this.externalVideo ? this.externalAudio : this.microphone;
|
|
1838
|
+
if (this.client && audio) {
|
|
1839
|
+
await this.run(() => this.client.unpublish(audio));
|
|
1840
|
+
}
|
|
1528
1841
|
}
|
|
1529
1842
|
/**
|
|
1530
1843
|
* 订阅远端主视频并返回原生轨道,不创建额外播放视图。
|
|
@@ -1670,6 +1983,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1670
1983
|
const handleExpired = () => {
|
|
1671
1984
|
if (!active() || this.tokenExpired) return;
|
|
1672
1985
|
this.tokenExpired = true;
|
|
1986
|
+
this.listener?.onNetworkStatistics?.(void 0);
|
|
1673
1987
|
this.listener?.onTokenExpired?.();
|
|
1674
1988
|
};
|
|
1675
1989
|
client.on("user-published", (user, kind) => {
|
|
@@ -1693,13 +2007,15 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1693
2007
|
if (mediaActive()) this.listener?.onCustomMessageReceived(String(uid), typeof data === "string" ? data : new TextDecoder().decode(data));
|
|
1694
2008
|
});
|
|
1695
2009
|
client.on("network-quality", (quality) => {
|
|
1696
|
-
if (mediaActive()) AgoraRtcStatistics.network(quality, this.listener);
|
|
2010
|
+
if (mediaActive() && !this.networkQualitySuspended) AgoraRtcStatistics.network(quality, this.listener);
|
|
1697
2011
|
});
|
|
1698
2012
|
client.on("token-privilege-will-expire", () => {
|
|
1699
2013
|
if (active() && !this.tokenExpired) this.listener?.onTokenWillExpire?.();
|
|
1700
2014
|
});
|
|
1701
2015
|
client.on("token-privilege-did-expire", handleExpired);
|
|
1702
2016
|
client.on("connection-state-change", (state2, previous, reason) => {
|
|
2017
|
+
if (this.client === client) this.networkQualitySuspended = state2 !== "CONNECTED";
|
|
2018
|
+
if (active() && state2 !== "CONNECTED") this.listener?.onNetworkStatistics?.(void 0);
|
|
1703
2019
|
if (state2 === "DISCONNECTED" && reason === "TOKEN_EXPIRE") {
|
|
1704
2020
|
handleExpired();
|
|
1705
2021
|
return;
|
|
@@ -1717,6 +2033,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1717
2033
|
this.cancelRejoin?.();
|
|
1718
2034
|
this.connection = void 0;
|
|
1719
2035
|
this.tokenExpired = false;
|
|
2036
|
+
this.networkQualitySuspended = false;
|
|
1720
2037
|
this.videoPublishRequested = false;
|
|
1721
2038
|
this.audioPublishRequested = false;
|
|
1722
2039
|
clearInterval(this.statisticsTimer);
|
|
@@ -1755,6 +2072,16 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1755
2072
|
if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
|
|
1756
2073
|
return this.camera;
|
|
1757
2074
|
}
|
|
2075
|
+
/**
|
|
2076
|
+
* 获取当前可编码、发布的视频轨,不要求来源为摄像头。
|
|
2077
|
+
*/
|
|
2078
|
+
requireLocalVideo() {
|
|
2079
|
+
const video = this.externalVideo ?? this.camera;
|
|
2080
|
+
if (!video) {
|
|
2081
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video source is not prepared");
|
|
2082
|
+
}
|
|
2083
|
+
return video;
|
|
2084
|
+
}
|
|
1758
2085
|
/**
|
|
1759
2086
|
* 执行声网操作并统一错误类型。
|
|
1760
2087
|
*/
|
|
@@ -1787,14 +2114,6 @@ var _AgoraRtcManager = class _AgoraRtcManager {
|
|
|
1787
2114
|
__publicField(_AgoraRtcManager, "regions", /* @__PURE__ */ new Map());
|
|
1788
2115
|
var AgoraRtcManager = _AgoraRtcManager;
|
|
1789
2116
|
|
|
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
2117
|
// src/Foundation/RTC/TRTC/RtcEngineManager.ts
|
|
1799
2118
|
var RtcEngineLease = class {
|
|
1800
2119
|
/**
|
|
@@ -2072,6 +2391,11 @@ var TrtcRtcManager = class {
|
|
|
2072
2391
|
*/
|
|
2073
2392
|
__publicField(this, "isCapturing", false);
|
|
2074
2393
|
__publicField(this, "isAudioCapturing", false);
|
|
2394
|
+
/**
|
|
2395
|
+
* 外部媒体的克隆轨;原始轨道由媒体源管理。
|
|
2396
|
+
*/
|
|
2397
|
+
__publicField(this, "externalVideo");
|
|
2398
|
+
__publicField(this, "externalAudio");
|
|
2075
2399
|
/**
|
|
2076
2400
|
* 房间状态
|
|
2077
2401
|
*/
|
|
@@ -2106,13 +2430,18 @@ var TrtcRtcManager = class {
|
|
|
2106
2430
|
async destroy() {
|
|
2107
2431
|
const lease = this.lease;
|
|
2108
2432
|
this.lease = void 0;
|
|
2109
|
-
|
|
2433
|
+
const externalVideo = this.externalVideo;
|
|
2434
|
+
const externalAudio = this.externalAudio;
|
|
2435
|
+
this.externalVideo = void 0;
|
|
2436
|
+
this.externalAudio = void 0;
|
|
2110
2437
|
this.isCapturing = false;
|
|
2111
2438
|
this.isAudioCapturing = false;
|
|
2112
2439
|
this.isInRoom = false;
|
|
2113
2440
|
this.removeStatsListeners?.();
|
|
2114
2441
|
this.removeStatsListeners = void 0;
|
|
2115
2442
|
if (!lease) {
|
|
2443
|
+
externalVideo?.stop();
|
|
2444
|
+
externalAudio?.stop();
|
|
2116
2445
|
return;
|
|
2117
2446
|
}
|
|
2118
2447
|
try {
|
|
@@ -2123,8 +2452,56 @@ var TrtcRtcManager = class {
|
|
|
2123
2452
|
await lease.engine.stopLocalVideo();
|
|
2124
2453
|
} catch {
|
|
2125
2454
|
}
|
|
2455
|
+
externalVideo?.stop();
|
|
2456
|
+
externalAudio?.stop();
|
|
2126
2457
|
this.engineManager.release(lease);
|
|
2127
2458
|
}
|
|
2459
|
+
/**
|
|
2460
|
+
* 准备外部音视频轨但不发布;失败时释放克隆轨,不影响原始媒体源。
|
|
2461
|
+
*/
|
|
2462
|
+
async setExternalMediaTracks(tracks) {
|
|
2463
|
+
const engine = this.requireEngine();
|
|
2464
|
+
if (this.isCapturing || this.isAudioCapturing || this.externalVideo) {
|
|
2465
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
|
|
2466
|
+
}
|
|
2467
|
+
if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
|
|
2468
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media tracks must be live video/audio tracks");
|
|
2469
|
+
}
|
|
2470
|
+
const video = tracks.videoTrack.clone();
|
|
2471
|
+
this.externalVideo = video;
|
|
2472
|
+
let audio;
|
|
2473
|
+
try {
|
|
2474
|
+
audio = tracks.audioTrack?.clone();
|
|
2475
|
+
this.externalAudio = audio;
|
|
2476
|
+
await engine.startLocalVideo({ publish: false, option: { videoTrack: video } });
|
|
2477
|
+
if (this.lease?.engine !== engine) {
|
|
2478
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "External media preparation was cancelled");
|
|
2479
|
+
}
|
|
2480
|
+
if (audio) {
|
|
2481
|
+
await engine.startLocalAudio({
|
|
2482
|
+
publish: false,
|
|
2483
|
+
option: { audioTrack: audio, echoCancellation: false, autoGainControl: false, noiseSuppression: false }
|
|
2484
|
+
});
|
|
2485
|
+
if (this.lease?.engine !== engine) {
|
|
2486
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "External media preparation was cancelled");
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
if (this.lease?.engine === engine) {
|
|
2491
|
+
await engine.stopLocalAudio().catch(() => {
|
|
2492
|
+
});
|
|
2493
|
+
await engine.stopLocalVideo().catch(() => {
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
if (this.externalVideo === video) {
|
|
2497
|
+
this.externalVideo = void 0;
|
|
2498
|
+
this.externalAudio = void 0;
|
|
2499
|
+
}
|
|
2500
|
+
video.stop();
|
|
2501
|
+
audio?.stop();
|
|
2502
|
+
throw this.mapError(error);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2128
2505
|
/**
|
|
2129
2506
|
* 启动摄像头采集(不发布)。
|
|
2130
2507
|
*
|
|
@@ -2133,7 +2510,7 @@ var TrtcRtcManager = class {
|
|
|
2133
2510
|
*/
|
|
2134
2511
|
async startCameraCapture(options) {
|
|
2135
2512
|
const engine = this.requireEngine();
|
|
2136
|
-
if (this.isCapturing) {
|
|
2513
|
+
if (this.isCapturing || this.externalVideo) {
|
|
2137
2514
|
throw new XmaxError(
|
|
2138
2515
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
2139
2516
|
"Camera capture is already running"
|
|
@@ -2203,6 +2580,9 @@ var TrtcRtcManager = class {
|
|
|
2203
2580
|
* 停止摄像头采集;引擎未初始化或采集未启动时不产生效果。
|
|
2204
2581
|
*/
|
|
2205
2582
|
async stopCameraCapture() {
|
|
2583
|
+
if (this.externalVideo) {
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2206
2586
|
const engine = this.lease?.engine;
|
|
2207
2587
|
this.isCapturing = false;
|
|
2208
2588
|
if (!engine) {
|
|
@@ -2220,7 +2600,7 @@ var TrtcRtcManager = class {
|
|
|
2220
2600
|
* @throws 引擎未初始化、进房参数无效或进房失败时抛出错误。
|
|
2221
2601
|
*/
|
|
2222
2602
|
async joinRoom(configuration) {
|
|
2223
|
-
if (configuration.provider !== "trtc") {
|
|
2603
|
+
if (configuration.provider !== "trtc" /* trtc */) {
|
|
2224
2604
|
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "TRTC credentials required");
|
|
2225
2605
|
}
|
|
2226
2606
|
const engine = this.requireEngine();
|
|
@@ -2276,18 +2656,18 @@ var TrtcRtcManager = class {
|
|
|
2276
2656
|
* TRTC 只接受单一目标码率,以 `minimumBitrate`(流畅优先档)作为目标码率;
|
|
2277
2657
|
* 编码策略偏好映射为 TRTC 弱网偏好。
|
|
2278
2658
|
*
|
|
2279
|
-
* @throws
|
|
2659
|
+
* @throws 本地视频未准备或编码参数配置失败时抛出错误。
|
|
2280
2660
|
*/
|
|
2281
2661
|
async configureVideoEncoding(configuration) {
|
|
2282
2662
|
const engine = this.requireEngine();
|
|
2283
|
-
if (!this.isCapturing) {
|
|
2663
|
+
if (!this.isCapturing && !this.externalVideo) {
|
|
2284
2664
|
throw new XmaxError(
|
|
2285
2665
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
2286
|
-
"
|
|
2666
|
+
"Local video is not prepared"
|
|
2287
2667
|
);
|
|
2288
2668
|
}
|
|
2289
2669
|
try {
|
|
2290
|
-
const encodingSize = rtcOrientedVideoSize({
|
|
2670
|
+
const encodingSize = this.externalVideo ? configuration : rtcOrientedVideoSize({
|
|
2291
2671
|
width: configuration.width,
|
|
2292
2672
|
height: configuration.height
|
|
2293
2673
|
});
|
|
@@ -2309,14 +2689,14 @@ var TrtcRtcManager = class {
|
|
|
2309
2689
|
/**
|
|
2310
2690
|
* 发布本地视频流。
|
|
2311
2691
|
*
|
|
2312
|
-
* @throws
|
|
2692
|
+
* @throws 本地视频未准备或发布失败时抛出错误。
|
|
2313
2693
|
*/
|
|
2314
2694
|
async publishLocalVideo() {
|
|
2315
2695
|
const engine = this.requireEngine();
|
|
2316
|
-
if (!this.isCapturing) {
|
|
2696
|
+
if (!this.isCapturing && !this.externalVideo) {
|
|
2317
2697
|
throw new XmaxError(
|
|
2318
2698
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
2319
|
-
"
|
|
2699
|
+
"Local video is not prepared"
|
|
2320
2700
|
);
|
|
2321
2701
|
}
|
|
2322
2702
|
try {
|
|
@@ -2330,7 +2710,7 @@ var TrtcRtcManager = class {
|
|
|
2330
2710
|
*/
|
|
2331
2711
|
async unpublishLocalVideo() {
|
|
2332
2712
|
const engine = this.lease?.engine;
|
|
2333
|
-
if (!engine || !this.isCapturing) {
|
|
2713
|
+
if (!engine || !this.isCapturing && !this.externalVideo) {
|
|
2334
2714
|
return;
|
|
2335
2715
|
}
|
|
2336
2716
|
try {
|
|
@@ -2340,14 +2720,18 @@ var TrtcRtcManager = class {
|
|
|
2340
2720
|
}
|
|
2341
2721
|
}
|
|
2342
2722
|
/**
|
|
2343
|
-
*
|
|
2723
|
+
* 发布文件音频或麦克风;无文件音轨时不启动麦克风。
|
|
2344
2724
|
*
|
|
2345
2725
|
* @throws 引擎未初始化、麦克风权限被拒绝或发布失败时抛出错误。
|
|
2346
2726
|
*/
|
|
2347
2727
|
async publishLocalAudio() {
|
|
2348
2728
|
const engine = this.requireEngine();
|
|
2349
2729
|
try {
|
|
2350
|
-
if (
|
|
2730
|
+
if (this.externalVideo) {
|
|
2731
|
+
if (this.externalAudio) {
|
|
2732
|
+
await engine.updateLocalAudio({ publish: true });
|
|
2733
|
+
}
|
|
2734
|
+
} else if (!this.isAudioCapturing) {
|
|
2351
2735
|
await engine.startLocalAudio();
|
|
2352
2736
|
this.isAudioCapturing = true;
|
|
2353
2737
|
} else {
|
|
@@ -2362,7 +2746,7 @@ var TrtcRtcManager = class {
|
|
|
2362
2746
|
*/
|
|
2363
2747
|
async unpublishLocalAudio() {
|
|
2364
2748
|
const engine = this.lease?.engine;
|
|
2365
|
-
if (!engine || !this.isAudioCapturing) {
|
|
2749
|
+
if (!engine || !this.isAudioCapturing && !this.externalAudio) {
|
|
2366
2750
|
return;
|
|
2367
2751
|
}
|
|
2368
2752
|
try {
|
|
@@ -2540,19 +2924,19 @@ var TrtcRtcManager = class {
|
|
|
2540
2924
|
source.off(RTC_EVENT.networkQuality, onNetworkQuality);
|
|
2541
2925
|
};
|
|
2542
2926
|
source.on(RTC_EVENT.remoteVideoAvailable, (event) => {
|
|
2543
|
-
if (event.streamType !== STREAM_TYPE_MAIN) {
|
|
2927
|
+
if (this.lease?.engine !== engine || event.streamType !== STREAM_TYPE_MAIN) {
|
|
2544
2928
|
return;
|
|
2545
2929
|
}
|
|
2546
2930
|
this.eventListener?.onRemoteVideoPublished(event.userId, true);
|
|
2547
2931
|
});
|
|
2548
2932
|
source.on(RTC_EVENT.remoteVideoUnavailable, (event) => {
|
|
2549
|
-
if (event.streamType !== STREAM_TYPE_MAIN) {
|
|
2933
|
+
if (this.lease?.engine !== engine || event.streamType !== STREAM_TYPE_MAIN) {
|
|
2550
2934
|
return;
|
|
2551
2935
|
}
|
|
2552
2936
|
this.eventListener?.onRemoteVideoPublished(event.userId, false);
|
|
2553
2937
|
});
|
|
2554
2938
|
source.on(RTC_EVENT.customMessage, (event) => {
|
|
2555
|
-
if (event.cmdId !== CUSTOM_MESSAGE_CMD_ID) {
|
|
2939
|
+
if (this.lease?.engine !== engine || event.cmdId !== CUSTOM_MESSAGE_CMD_ID) {
|
|
2556
2940
|
return;
|
|
2557
2941
|
}
|
|
2558
2942
|
let message;
|
|
@@ -2645,6 +3029,11 @@ var VeRtcManager = class {
|
|
|
2645
3029
|
__publicField(this, "captureVersion", 0);
|
|
2646
3030
|
__publicField(this, "camera");
|
|
2647
3031
|
__publicField(this, "microphone", false);
|
|
3032
|
+
/**
|
|
3033
|
+
* 外部媒体的克隆轨;原始轨道由媒体源管理。
|
|
3034
|
+
*/
|
|
3035
|
+
__publicField(this, "externalVideo");
|
|
3036
|
+
__publicField(this, "externalAudio");
|
|
2648
3037
|
__publicField(this, "connection");
|
|
2649
3038
|
__publicField(this, "joined", false);
|
|
2650
3039
|
__publicField(this, "roomVersion", 0);
|
|
@@ -2700,18 +3089,76 @@ var VeRtcManager = class {
|
|
|
2700
3089
|
this.captureVersion++;
|
|
2701
3090
|
const engine = this.engine;
|
|
2702
3091
|
const sdk = this.sdk;
|
|
2703
|
-
this.listener = void 0;
|
|
2704
3092
|
this.clearRoom();
|
|
2705
3093
|
this.engine = void 0;
|
|
2706
3094
|
this.sdk = void 0;
|
|
2707
3095
|
this.camera = void 0;
|
|
2708
3096
|
this.microphone = false;
|
|
2709
3097
|
engine?.removeAllListeners();
|
|
2710
|
-
|
|
3098
|
+
try {
|
|
3099
|
+
if (engine && sdk) sdk.default.destroyEngine(engine);
|
|
3100
|
+
} finally {
|
|
3101
|
+
this.externalVideo?.stop();
|
|
3102
|
+
this.externalAudio?.stop();
|
|
3103
|
+
this.externalVideo = void 0;
|
|
3104
|
+
this.externalAudio = void 0;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* 将克隆的文件音视频轨绑定到主流,不采集设备、不自动发布。
|
|
3109
|
+
*/
|
|
3110
|
+
async setExternalMediaTracks(tracks) {
|
|
3111
|
+
const engine = this.requireEngine();
|
|
3112
|
+
const sdk = this.sdk;
|
|
3113
|
+
if (this.camera || this.microphone || this.externalVideo) {
|
|
3114
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
|
|
3115
|
+
}
|
|
3116
|
+
if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
|
|
3117
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "External media tracks must be live video/audio tracks");
|
|
3118
|
+
}
|
|
3119
|
+
const video = tracks.videoTrack.clone();
|
|
3120
|
+
this.externalVideo = video;
|
|
3121
|
+
let audio;
|
|
3122
|
+
const index = sdk.StreamIndex.STREAM_INDEX_MAIN;
|
|
3123
|
+
const ensureCurrent = () => {
|
|
3124
|
+
if (this.engine !== engine || this.externalVideo !== video) {
|
|
3125
|
+
throw this.cancelled();
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
try {
|
|
3129
|
+
audio = tracks.audioTrack?.clone();
|
|
3130
|
+
this.externalAudio = audio;
|
|
3131
|
+
await engine.setVideoSourceType(index, sdk.VideoSourceType.VIDEO_SOURCE_TYPE_EXTERNAL);
|
|
3132
|
+
ensureCurrent();
|
|
3133
|
+
await engine.setExternalVideoTrack(index, video);
|
|
3134
|
+
ensureCurrent();
|
|
3135
|
+
if (audio) {
|
|
3136
|
+
await engine.setAudioSourceType(index, sdk.AudioSourceType.AUDIO_SOURCE_TYPE_EXTERNAL);
|
|
3137
|
+
ensureCurrent();
|
|
3138
|
+
await engine.setExternalAudioTrack(index, audio);
|
|
3139
|
+
ensureCurrent();
|
|
3140
|
+
}
|
|
3141
|
+
} catch (error) {
|
|
3142
|
+
if (this.engine === engine) {
|
|
3143
|
+
await engine.setAudioSourceType(index, sdk.AudioSourceType.AUDIO_SOURCE_TYPE_INTERNAL).catch(() => {
|
|
3144
|
+
});
|
|
3145
|
+
await engine.setVideoSourceType(index, sdk.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL).catch(() => {
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
if (this.externalVideo === video) {
|
|
3149
|
+
this.externalVideo = void 0;
|
|
3150
|
+
this.externalAudio = void 0;
|
|
3151
|
+
}
|
|
3152
|
+
video.stop();
|
|
3153
|
+
audio?.stop();
|
|
3154
|
+
throw this.mapError(error);
|
|
3155
|
+
}
|
|
2711
3156
|
}
|
|
2712
3157
|
async startCameraCapture(options) {
|
|
2713
3158
|
const engine = this.requireEngine();
|
|
2714
|
-
if (this.camera
|
|
3159
|
+
if (this.camera || this.externalVideo) {
|
|
3160
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video is already prepared");
|
|
3161
|
+
}
|
|
2715
3162
|
const version = ++this.captureVersion;
|
|
2716
3163
|
await this.run(() => engine.setVideoEncoderConfig({
|
|
2717
3164
|
...rtcOrientedVideoSize({ width: options.width, height: options.height }),
|
|
@@ -2751,15 +3198,20 @@ var VeRtcManager = class {
|
|
|
2751
3198
|
return track;
|
|
2752
3199
|
}
|
|
2753
3200
|
async stopCameraCapture() {
|
|
3201
|
+
if (this.externalVideo) {
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
2754
3204
|
this.captureVersion++;
|
|
2755
3205
|
this.camera = void 0;
|
|
2756
3206
|
if (this.engine) await this.run(() => this.engine.stopVideoCapture());
|
|
2757
3207
|
}
|
|
2758
3208
|
async configureVideoEncoding(config) {
|
|
2759
3209
|
const engine = this.requireEngine();
|
|
2760
|
-
this.
|
|
3210
|
+
this.requireLocalVideo();
|
|
3211
|
+
const size = this.externalVideo ? config : rtcOrientedVideoSize(config);
|
|
2761
3212
|
await this.run(() => engine.setVideoEncoderConfig({
|
|
2762
|
-
|
|
3213
|
+
width: size.width,
|
|
3214
|
+
height: size.height,
|
|
2763
3215
|
frameRate: config.frameRate,
|
|
2764
3216
|
maxKbps: config.maximumBitrate,
|
|
2765
3217
|
contentHint: config.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "motion" : "detail",
|
|
@@ -2772,7 +3224,7 @@ var VeRtcManager = class {
|
|
|
2772
3224
|
await this.recovery?.catch(() => {
|
|
2773
3225
|
});
|
|
2774
3226
|
const engine = this.requireEngine();
|
|
2775
|
-
if (config.provider !== "vertc" || config.appID !== VERTC_APP_ID || this.connection) {
|
|
3227
|
+
if (config.provider !== "vertc" /* vertc */ || config.appID !== VERTC_APP_ID || this.connection) {
|
|
2776
3228
|
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "VeRTC credentials for the configured AppID and an idle room are required");
|
|
2777
3229
|
}
|
|
2778
3230
|
this.clearRoom();
|
|
@@ -2819,7 +3271,7 @@ var VeRtcManager = class {
|
|
|
2819
3271
|
const engine = this.requireEngine();
|
|
2820
3272
|
const current = this.connection;
|
|
2821
3273
|
if (signal?.aborted) throw this.cancelled();
|
|
2822
|
-
if (!current || config.provider !== "vertc" || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
|
|
3274
|
+
if (!current || config.provider !== "vertc" /* vertc */ || config.appID !== current.appID || config.roomID !== current.roomID || config.userID !== current.userID) {
|
|
2823
3275
|
throw new XmaxError("SESSION_ERROR" /* sessionError */, "VeRTC session binding changed");
|
|
2824
3276
|
}
|
|
2825
3277
|
const version = this.roomVersion;
|
|
@@ -2861,7 +3313,7 @@ var VeRtcManager = class {
|
|
|
2861
3313
|
throw this.cancelled();
|
|
2862
3314
|
}
|
|
2863
3315
|
}
|
|
2864
|
-
if ((this.tokenExpired || this.publishPrivilegeExpired) && this.videoRequested && this.camera) {
|
|
3316
|
+
if ((this.tokenExpired || this.publishPrivilegeExpired) && this.videoRequested && (this.camera || this.externalVideo)) {
|
|
2865
3317
|
await this.run(() => engine.publishStream(this.sdk.MediaType.VIDEO));
|
|
2866
3318
|
this.ensureCurrent(engine, version);
|
|
2867
3319
|
if (!this.videoRequested) {
|
|
@@ -2869,7 +3321,7 @@ var VeRtcManager = class {
|
|
|
2869
3321
|
this.ensureCurrent(engine, version);
|
|
2870
3322
|
}
|
|
2871
3323
|
}
|
|
2872
|
-
if ((this.tokenExpired || this.publishPrivilegeExpired) && this.audioRequested && this.microphone) {
|
|
3324
|
+
if ((this.tokenExpired || this.publishPrivilegeExpired) && this.audioRequested && (this.microphone || this.externalAudio)) {
|
|
2873
3325
|
await this.run(() => engine.publishStream(this.sdk.MediaType.AUDIO));
|
|
2874
3326
|
this.ensureCurrent(engine, version);
|
|
2875
3327
|
if (!this.audioRequested) {
|
|
@@ -2893,7 +3345,7 @@ var VeRtcManager = class {
|
|
|
2893
3345
|
}
|
|
2894
3346
|
async publishLocalVideo() {
|
|
2895
3347
|
const engine = this.requireEngine();
|
|
2896
|
-
this.
|
|
3348
|
+
this.requireLocalVideo();
|
|
2897
3349
|
this.videoRequested = true;
|
|
2898
3350
|
const version = this.roomVersion;
|
|
2899
3351
|
await this.recovery;
|
|
@@ -2907,12 +3359,15 @@ var VeRtcManager = class {
|
|
|
2907
3359
|
}
|
|
2908
3360
|
async publishLocalAudio() {
|
|
2909
3361
|
const engine = this.requireEngine();
|
|
3362
|
+
if (this.externalVideo && !this.externalAudio) {
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
2910
3365
|
this.audioRequested = true;
|
|
2911
3366
|
const version = this.roomVersion;
|
|
2912
3367
|
await this.recovery;
|
|
2913
3368
|
this.ensureCurrent(engine, version);
|
|
2914
3369
|
try {
|
|
2915
|
-
if (!this.microphone) {
|
|
3370
|
+
if (!this.externalVideo && !this.microphone) {
|
|
2916
3371
|
await engine.startAudioCapture();
|
|
2917
3372
|
if (!this.isCurrent(engine, version)) {
|
|
2918
3373
|
await engine.stopAudioCapture().catch(() => {
|
|
@@ -2924,7 +3379,7 @@ var VeRtcManager = class {
|
|
|
2924
3379
|
if (!this.audioRequested) throw this.cancelled();
|
|
2925
3380
|
await engine.publishStream(this.sdk.MediaType.AUDIO);
|
|
2926
3381
|
} catch (error) {
|
|
2927
|
-
throw this.mapError(error, "MICROPHONE_PERMISSION_DENIED" /* microphonePermissionDenied */);
|
|
3382
|
+
throw this.mapError(error, this.externalVideo ? "RTC_ERROR" /* rtcError */ : "MICROPHONE_PERMISSION_DENIED" /* microphonePermissionDenied */);
|
|
2928
3383
|
}
|
|
2929
3384
|
}
|
|
2930
3385
|
async unpublishLocalAudio() {
|
|
@@ -3189,6 +3644,15 @@ var VeRtcManager = class {
|
|
|
3189
3644
|
if (!this.engine) throw new XmaxError("RTC_ERROR" /* rtcError */, "VeRTC is not initialized");
|
|
3190
3645
|
return this.engine;
|
|
3191
3646
|
}
|
|
3647
|
+
/**
|
|
3648
|
+
* 获取已准备的文件或摄像头视频轨。
|
|
3649
|
+
*/
|
|
3650
|
+
requireLocalVideo() {
|
|
3651
|
+
if (this.externalVideo) {
|
|
3652
|
+
return this.externalVideo;
|
|
3653
|
+
}
|
|
3654
|
+
return this.requireCamera();
|
|
3655
|
+
}
|
|
3192
3656
|
requireCamera() {
|
|
3193
3657
|
if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
|
|
3194
3658
|
return this.camera;
|
|
@@ -3215,12 +3679,14 @@ var VeRtcManager = class {
|
|
|
3215
3679
|
// src/Foundation/RTC/RtcFactory.ts
|
|
3216
3680
|
function createRtcManager(provider, environment) {
|
|
3217
3681
|
switch (provider) {
|
|
3682
|
+
case "trtc" /* trtc */:
|
|
3683
|
+
return new TrtcRtcManager();
|
|
3218
3684
|
case "agora" /* agora */:
|
|
3219
3685
|
return new AgoraRtcManager({ environment });
|
|
3220
3686
|
case "vertc" /* vertc */:
|
|
3221
3687
|
return new VeRtcManager();
|
|
3222
3688
|
default:
|
|
3223
|
-
|
|
3689
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported RTC provider");
|
|
3224
3690
|
}
|
|
3225
3691
|
}
|
|
3226
3692
|
|
|
@@ -3715,91 +4181,548 @@ var _CameraController = class _CameraController {
|
|
|
3715
4181
|
__publicField(_CameraController, "localVideoTrackID", "video0");
|
|
3716
4182
|
var CameraController = _CameraController;
|
|
3717
4183
|
|
|
3718
|
-
// src/Service/Realtime/
|
|
3719
|
-
var
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
*/
|
|
3729
|
-
constructor(init) {
|
|
3730
|
-
/**
|
|
3731
|
-
* 会话信息
|
|
3732
|
-
*/
|
|
3733
|
-
/**
|
|
3734
|
-
* 会话标识(sessionUid)。
|
|
3735
|
-
*/
|
|
3736
|
-
__publicField(this, "id");
|
|
3737
|
-
/**
|
|
3738
|
-
* 业务用户标识。
|
|
3739
|
-
*/
|
|
3740
|
-
__publicField(this, "userID");
|
|
3741
|
-
/**
|
|
3742
|
-
* 运行状态
|
|
3743
|
-
*/
|
|
3744
|
-
/**
|
|
3745
|
-
* 会话状态(如 ACTIVE);响应未携带时为空。
|
|
3746
|
-
*/
|
|
3747
|
-
__publicField(this, "status");
|
|
3748
|
-
/**
|
|
3749
|
-
* 会话关闭原因。
|
|
3750
|
-
*/
|
|
3751
|
-
__publicField(this, "closeReason");
|
|
4184
|
+
// src/Service/Realtime/RealtimeReferenceVideo.ts
|
|
4185
|
+
var RealtimeVideoSampleMethod = /* @__PURE__ */ ((RealtimeVideoSampleMethod2) => {
|
|
4186
|
+
RealtimeVideoSampleMethod2["time"] = "time";
|
|
4187
|
+
RealtimeVideoSampleMethod2["fps"] = "fps";
|
|
4188
|
+
return RealtimeVideoSampleMethod2;
|
|
4189
|
+
})(RealtimeVideoSampleMethod || {});
|
|
4190
|
+
|
|
4191
|
+
// src/Media/Video/NetworkVideoController.ts
|
|
4192
|
+
var NetworkVideoController = class {
|
|
4193
|
+
constructor() {
|
|
3752
4194
|
/**
|
|
3753
|
-
*
|
|
4195
|
+
* 当前视频源与完成通知
|
|
3754
4196
|
*/
|
|
4197
|
+
__publicField(this, "currentTrack");
|
|
4198
|
+
__publicField(this, "reference");
|
|
4199
|
+
__publicField(this, "onFinish");
|
|
3755
4200
|
/**
|
|
3756
|
-
*
|
|
4201
|
+
* 预览进度与视图资源
|
|
3757
4202
|
*/
|
|
3758
|
-
__publicField(this, "
|
|
3759
|
-
this
|
|
3760
|
-
this
|
|
3761
|
-
this.status = init.status;
|
|
3762
|
-
this.connection = init.connection;
|
|
3763
|
-
this.closeReason = init.closeReason;
|
|
4203
|
+
__publicField(this, "position", 0);
|
|
4204
|
+
__publicField(this, "ended", false);
|
|
4205
|
+
__publicField(this, "bindings", /* @__PURE__ */ new Map());
|
|
3764
4206
|
}
|
|
3765
|
-
};
|
|
3766
|
-
|
|
3767
|
-
// src/Foundation/RTC/RoomJoinConfiguration.ts
|
|
3768
|
-
var RoomJoinConfiguration = class {
|
|
3769
4207
|
/**
|
|
3770
|
-
*
|
|
3771
|
-
*
|
|
3772
|
-
* @param init.roomID TRTC 房间号。
|
|
3773
|
-
* @param init.userID TRTC 登录身份。
|
|
3774
|
-
* @param init.sdkAppID TRTC SDKAppID(字符串形式)。
|
|
3775
|
-
* @param init.userSig TRTC 登录签名。
|
|
3776
|
-
* @param init.privateMapKey TRTC 进房鉴权字段。
|
|
4208
|
+
* 创建网络视频源,校验地址、采样方式和模型支持的尺寸;不请求视频下载。
|
|
3777
4209
|
*/
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
4210
|
+
create(options, mediaService) {
|
|
4211
|
+
let url;
|
|
4212
|
+
try {
|
|
4213
|
+
url = new URL(options.url);
|
|
4214
|
+
} catch {
|
|
4215
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an absolute HTTP or HTTPS URL");
|
|
4216
|
+
}
|
|
4217
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password) {
|
|
4218
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Network video requires an HTTP or HTTPS URL without embedded credentials");
|
|
4219
|
+
}
|
|
4220
|
+
const sampleMethod = options.sampleMethod ?? "time" /* time */;
|
|
4221
|
+
if (!Object.values(RealtimeVideoSampleMethod).includes(sampleMethod)) {
|
|
4222
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported video sample method");
|
|
4223
|
+
}
|
|
4224
|
+
if (this.currentTrack) {
|
|
4225
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current network video stream before creating another one");
|
|
4226
|
+
}
|
|
4227
|
+
options.videoFormat.validate();
|
|
4228
|
+
const size = mediaService.resolveModelInputSize(options.videoFormat);
|
|
4229
|
+
const track = new RealtimeVideoTrack({
|
|
4230
|
+
id: "network-video",
|
|
4231
|
+
videoFormat: options.videoFormat.resized(size.width, size.height)
|
|
4232
|
+
});
|
|
4233
|
+
this.currentTrack = track;
|
|
4234
|
+
this.reference = Object.freeze({ path: options.url.trim(), sampleMethod });
|
|
4235
|
+
this.onFinish = options.onFinish;
|
|
4236
|
+
VideoRenderRegistry.register(track, {
|
|
4237
|
+
attachHandler: (view) => this.attach(view),
|
|
4238
|
+
detachHandler: (view) => this.detach(view)
|
|
4239
|
+
});
|
|
4240
|
+
return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
|
|
4241
|
+
}
|
|
4242
|
+
/**
|
|
4243
|
+
* 释放全部预览绑定及源信息,不影响其他媒体源。
|
|
4244
|
+
*/
|
|
4245
|
+
stop() {
|
|
4246
|
+
for (const view of this.bindings.keys()) {
|
|
4247
|
+
this.detach(view);
|
|
4248
|
+
}
|
|
4249
|
+
if (this.currentTrack) {
|
|
4250
|
+
VideoRenderRegistry.unregister(this.currentTrack);
|
|
4251
|
+
}
|
|
4252
|
+
this.currentTrack = void 0;
|
|
4253
|
+
this.reference = void 0;
|
|
4254
|
+
this.onFinish = void 0;
|
|
4255
|
+
this.position = 0;
|
|
4256
|
+
this.ended = false;
|
|
4257
|
+
}
|
|
4258
|
+
/**
|
|
4259
|
+
* 绑定播放器,恢复预览位置;本地播放错误仅记录,不中断服务端生成。
|
|
4260
|
+
*/
|
|
4261
|
+
attach(view) {
|
|
4262
|
+
if (!this.reference || !view.videoElement || !view.setVideoURL) {
|
|
4263
|
+
throw new XmaxError("MEDIA_ERROR" /* mediaError */, "This render target does not support network video preview");
|
|
4264
|
+
}
|
|
4265
|
+
this.detach(view);
|
|
4266
|
+
const video = view.videoElement;
|
|
4267
|
+
video.autoplay = !this.ended;
|
|
4268
|
+
const restore = () => {
|
|
4269
|
+
if (this.position > 0) {
|
|
4270
|
+
video.currentTime = Math.min(this.position, Math.max(0, video.duration - 1e-3) || this.position);
|
|
4271
|
+
}
|
|
4272
|
+
if (this.ended) {
|
|
4273
|
+
video.pause();
|
|
4274
|
+
}
|
|
4275
|
+
};
|
|
4276
|
+
const fail = () => {
|
|
4277
|
+
XmaxLogger.render.warning(() => "\u7F51\u7EDC\u89C6\u9891\u9884\u89C8\u5931\u8D25 (Network Video Preview Failed)");
|
|
4278
|
+
};
|
|
4279
|
+
video.addEventListener("loadedmetadata", restore);
|
|
4280
|
+
video.addEventListener("error", fail);
|
|
4281
|
+
this.bindings.set(view, () => {
|
|
4282
|
+
this.position = video.currentTime;
|
|
4283
|
+
this.ended = video.ended || this.ended;
|
|
4284
|
+
video.removeEventListener("loadedmetadata", restore);
|
|
4285
|
+
video.removeEventListener("error", fail);
|
|
4286
|
+
view.setVideoURL?.(null);
|
|
4287
|
+
});
|
|
4288
|
+
view.isMirrored = false;
|
|
4289
|
+
view.setVideoURL(this.reference.path);
|
|
4290
|
+
}
|
|
4291
|
+
/**
|
|
4292
|
+
* 保存预览进度并释放该视图的地址和事件监听。
|
|
4293
|
+
*/
|
|
4294
|
+
detach(view) {
|
|
4295
|
+
this.bindings.get(view)?.();
|
|
4296
|
+
this.bindings.delete(view);
|
|
4297
|
+
}
|
|
4298
|
+
};
|
|
4299
|
+
|
|
4300
|
+
// src/Media/Video/LocalVideoController.ts
|
|
4301
|
+
var LocalVideoController = class {
|
|
4302
|
+
/**
|
|
4303
|
+
* 保存媒体错误处理器,不创建浏览器资源。
|
|
4304
|
+
*/
|
|
4305
|
+
constructor(onError = () => {
|
|
4306
|
+
}) {
|
|
4307
|
+
/**
|
|
4308
|
+
* 当前文件资源与运行期错误通知
|
|
4309
|
+
*/
|
|
4310
|
+
__publicField(this, "source");
|
|
4311
|
+
__publicField(this, "onError");
|
|
4312
|
+
this.onError = onError;
|
|
4313
|
+
}
|
|
4314
|
+
/**
|
|
4315
|
+
* 是否持有文件资源,包含尚未完成准备的播放器。
|
|
4316
|
+
*/
|
|
4317
|
+
get isActive() {
|
|
4318
|
+
return this.source !== void 0;
|
|
4319
|
+
}
|
|
4320
|
+
/**
|
|
4321
|
+
* 准备完成的本地视频轨。
|
|
4322
|
+
*/
|
|
4323
|
+
get currentTrack() {
|
|
4324
|
+
return this.source?.track;
|
|
4325
|
+
}
|
|
4326
|
+
/**
|
|
4327
|
+
* 文件音频输出;禁用音频时为空,无音轨文件的 Web Audio 输出为静音。
|
|
4328
|
+
*/
|
|
4329
|
+
get audioTrack() {
|
|
4330
|
+
return this.source?.audioDestination?.stream.getAudioTracks()[0];
|
|
4331
|
+
}
|
|
4332
|
+
/**
|
|
4333
|
+
* 校验文件和格式,准备首帧与音频输出;应从用户点击事件直接调用以解锁播放。
|
|
4334
|
+
*/
|
|
4335
|
+
async create(options, mediaService, signal) {
|
|
4336
|
+
if (this.source) {
|
|
4337
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local video before creating another one");
|
|
4338
|
+
}
|
|
4339
|
+
if (!(options.file instanceof Blob) || options.file.size === 0) {
|
|
4340
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video requires a non-empty File or Blob");
|
|
4341
|
+
}
|
|
4342
|
+
const format = this.resolveVideoFormat(options.videoFormat, mediaService);
|
|
4343
|
+
if (signal.aborted) {
|
|
4344
|
+
throw this.cancelled();
|
|
4345
|
+
}
|
|
4346
|
+
if (typeof document === "undefined") {
|
|
4347
|
+
throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Local video requires a browser");
|
|
4348
|
+
}
|
|
4349
|
+
const video = document.createElement("video");
|
|
4350
|
+
const canvas = document.createElement("canvas");
|
|
4351
|
+
const context = canvas.getContext("2d");
|
|
4352
|
+
if (!context || typeof canvas.captureStream !== "function") {
|
|
4353
|
+
throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas video capture is unavailable");
|
|
4354
|
+
}
|
|
4355
|
+
canvas.width = format.width;
|
|
4356
|
+
canvas.height = format.height;
|
|
4357
|
+
const source = {
|
|
4358
|
+
video,
|
|
4359
|
+
canvas,
|
|
4360
|
+
context,
|
|
4361
|
+
url: URL.createObjectURL(options.file),
|
|
4362
|
+
abort: new AbortController(),
|
|
4363
|
+
targets: /* @__PURE__ */ new Set()
|
|
4364
|
+
};
|
|
4365
|
+
this.source = source;
|
|
4366
|
+
try {
|
|
4367
|
+
video.playsInline = true;
|
|
4368
|
+
video.loop = options.loop ?? true;
|
|
4369
|
+
video.preload = "auto";
|
|
4370
|
+
video.volume = 1;
|
|
4371
|
+
video.muted = options.includeAudio === false;
|
|
4372
|
+
if (options.includeAudio !== false) {
|
|
4373
|
+
if (typeof AudioContext === "undefined") {
|
|
4374
|
+
throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Web Audio is unavailable; disable file audio or use a supported browser");
|
|
4375
|
+
}
|
|
4376
|
+
const audio = new AudioContext();
|
|
4377
|
+
source.audioContext = audio;
|
|
4378
|
+
source.audioSource = audio.createMediaElementSource(video);
|
|
4379
|
+
source.audioDestination = audio.createMediaStreamDestination();
|
|
4380
|
+
source.audioSource.connect(source.audioDestination);
|
|
4381
|
+
}
|
|
4382
|
+
video.src = source.url;
|
|
4383
|
+
const resume = source.audioContext?.resume();
|
|
4384
|
+
const playing = this.play(source, signal);
|
|
4385
|
+
await this.wait(source, Promise.all([resume, playing]), signal);
|
|
4386
|
+
video.pause();
|
|
4387
|
+
if (!video.videoWidth || !video.videoHeight || !Number.isFinite(video.duration) || video.duration <= 0) {
|
|
4388
|
+
throw new XmaxError("MEDIA_ERROR" /* mediaError */, "The file has no playable finite video stream");
|
|
4389
|
+
}
|
|
4390
|
+
await this.rewind(source, signal);
|
|
4391
|
+
this.draw(source);
|
|
4392
|
+
source.stream = canvas.captureStream(format.fps);
|
|
4393
|
+
const nativeTrack = source.stream.getVideoTracks()[0];
|
|
4394
|
+
if (!nativeTrack) {
|
|
4395
|
+
throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas did not provide a video track");
|
|
4396
|
+
}
|
|
4397
|
+
const track = new RealtimeVideoTrack({ id: "local-video", videoFormat: format });
|
|
4398
|
+
track.mediaStreamTrack = nativeTrack;
|
|
4399
|
+
source.track = track;
|
|
4400
|
+
this.draw(source);
|
|
4401
|
+
source.drawTimer = setInterval(() => {
|
|
4402
|
+
if (this.source !== source) {
|
|
4403
|
+
return;
|
|
4404
|
+
}
|
|
4405
|
+
try {
|
|
4406
|
+
this.draw(source);
|
|
4407
|
+
} catch {
|
|
4408
|
+
this.handleError(source);
|
|
4409
|
+
}
|
|
4410
|
+
}, 1e3 / format.fps);
|
|
4411
|
+
source.onError = () => this.handleError(source);
|
|
4412
|
+
video.addEventListener("error", source.onError);
|
|
4413
|
+
VideoRenderRegistry.register(track, {
|
|
4414
|
+
attachHandler: (view) => {
|
|
4415
|
+
view.isMirrored = false;
|
|
4416
|
+
view.setMediaStream(source.stream);
|
|
4417
|
+
source.targets.add(view);
|
|
4418
|
+
},
|
|
4419
|
+
detachHandler: (view) => {
|
|
4420
|
+
source.targets.delete(view);
|
|
4421
|
+
view.setMediaStream(null);
|
|
4422
|
+
}
|
|
4423
|
+
});
|
|
4424
|
+
return new RealtimeMediaStream({ id: "stream0" /* local */, videoTrack: track });
|
|
4425
|
+
} catch (error) {
|
|
4426
|
+
await this.stop();
|
|
4427
|
+
throw XmaxError.from(error);
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
/**
|
|
4431
|
+
* 开始信令发送成功后,从文件开头同步启动音视频;不等待远端首帧。
|
|
4432
|
+
*/
|
|
4433
|
+
async start(signal) {
|
|
4434
|
+
const source = this.source;
|
|
4435
|
+
if (!source?.track) {
|
|
4436
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video is not prepared");
|
|
4437
|
+
}
|
|
4438
|
+
if (signal.aborted) {
|
|
4439
|
+
throw this.cancelled();
|
|
4440
|
+
}
|
|
4441
|
+
try {
|
|
4442
|
+
await this.rewind(source, signal);
|
|
4443
|
+
await this.wait(source, Promise.all([source.audioContext?.resume(), this.play(source, signal)]), signal);
|
|
4444
|
+
} catch (error) {
|
|
4445
|
+
source.video.pause();
|
|
4446
|
+
throw error;
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
/**
|
|
4450
|
+
* 暂停文件音视频,保留当前预览;重新开始生成时回到文件开头。
|
|
4451
|
+
*/
|
|
4452
|
+
pause() {
|
|
4453
|
+
this.source?.video.pause();
|
|
4454
|
+
}
|
|
4455
|
+
/**
|
|
4456
|
+
* 释放播放器、预览、定时器、轨道、音频上下文和对象 URL。
|
|
4457
|
+
*/
|
|
4458
|
+
async stop() {
|
|
4459
|
+
const source = this.source;
|
|
4460
|
+
this.source = void 0;
|
|
4461
|
+
if (!source) {
|
|
4462
|
+
return;
|
|
4463
|
+
}
|
|
4464
|
+
source.abort.abort();
|
|
4465
|
+
clearInterval(source.drawTimer);
|
|
4466
|
+
if (source.onError) {
|
|
4467
|
+
source.video.removeEventListener("error", source.onError);
|
|
4468
|
+
}
|
|
4469
|
+
if (source.track) {
|
|
4470
|
+
VideoRenderRegistry.unregister(source.track);
|
|
4471
|
+
source.track.mediaStreamTrack = void 0;
|
|
4472
|
+
}
|
|
4473
|
+
for (const view of source.targets) {
|
|
4474
|
+
try {
|
|
4475
|
+
view.setMediaStream(null);
|
|
4476
|
+
} catch {
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
source.targets.clear();
|
|
4480
|
+
source.video.pause();
|
|
4481
|
+
source.video.removeAttribute("src");
|
|
4482
|
+
source.video.load();
|
|
4483
|
+
source.stream?.getTracks().forEach((track) => track.stop());
|
|
4484
|
+
source.audioDestination?.stream.getTracks().forEach((track) => track.stop());
|
|
4485
|
+
source.audioSource?.disconnect();
|
|
4486
|
+
source.audioDestination?.disconnect();
|
|
4487
|
+
URL.revokeObjectURL(source.url);
|
|
4488
|
+
await source.audioContext?.close().catch(() => {
|
|
4489
|
+
});
|
|
4490
|
+
}
|
|
4491
|
+
/**
|
|
4492
|
+
* 播放被浏览器拒绝时给出可操作错误;关闭后的迟到播放立即暂停。
|
|
4493
|
+
*/
|
|
4494
|
+
play(source, signal) {
|
|
4495
|
+
return source.video.play().then(() => {
|
|
4496
|
+
if (this.source !== source || source.abort.signal.aborted || signal.aborted) {
|
|
4497
|
+
source.video.pause();
|
|
4498
|
+
throw this.cancelled();
|
|
4499
|
+
}
|
|
4500
|
+
}).catch((error) => {
|
|
4501
|
+
if (error instanceof XmaxError) {
|
|
4502
|
+
throw error;
|
|
4503
|
+
}
|
|
4504
|
+
throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Cannot play local video; call from a user click and use a browser-supported video codec");
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
/**
|
|
4508
|
+
* 等待 seek 完成,确保重新生成从首帧开始,而非使用旧缓存画面。
|
|
4509
|
+
*/
|
|
4510
|
+
async rewind(source, signal) {
|
|
4511
|
+
if (source.video.currentTime === 0) {
|
|
4512
|
+
return;
|
|
4513
|
+
}
|
|
4514
|
+
let seeked;
|
|
4515
|
+
const ready = new Promise((resolve) => {
|
|
4516
|
+
seeked = resolve;
|
|
4517
|
+
});
|
|
4518
|
+
source.video.addEventListener("seeked", seeked, { once: true });
|
|
4519
|
+
try {
|
|
4520
|
+
source.video.currentTime = 0;
|
|
4521
|
+
await this.wait(source, ready, signal);
|
|
4522
|
+
} finally {
|
|
4523
|
+
source.video.removeEventListener("seeked", seeked);
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
/**
|
|
4527
|
+
* 给浏览器准备和播放操作设置 10 秒上限,同时响应关闭、取消和解码错误。
|
|
4528
|
+
*/
|
|
4529
|
+
wait(source, operation, signal) {
|
|
4530
|
+
return new Promise((resolve, reject) => {
|
|
4531
|
+
const finish = (error) => {
|
|
4532
|
+
clearTimeout(timer);
|
|
4533
|
+
signal.removeEventListener("abort", abort);
|
|
4534
|
+
source.abort.signal.removeEventListener("abort", abort);
|
|
4535
|
+
source.video.removeEventListener("error", failed);
|
|
4536
|
+
if (error) {
|
|
4537
|
+
reject(error);
|
|
4538
|
+
} else {
|
|
4539
|
+
resolve();
|
|
4540
|
+
}
|
|
4541
|
+
};
|
|
4542
|
+
const abort = () => finish(this.cancelled());
|
|
4543
|
+
const failed = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "The browser cannot decode this video file"));
|
|
4544
|
+
const timer = setTimeout(() => finish(new XmaxError("TIMEOUT" /* timeout */, "Local video preparation timed out; start from a user click and check the video codec")), 1e4);
|
|
4545
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
4546
|
+
source.abort.signal.addEventListener("abort", abort, { once: true });
|
|
4547
|
+
source.video.addEventListener("error", failed, { once: true });
|
|
4548
|
+
operation.then(() => finish(), finish);
|
|
4549
|
+
if (signal.aborted || source.abort.signal.aborted) {
|
|
4550
|
+
abort();
|
|
4551
|
+
}
|
|
4552
|
+
});
|
|
4553
|
+
}
|
|
4554
|
+
/**
|
|
4555
|
+
* 等比缩放并居中补黑边,保留完整画面,不按设备方向转置文件尺寸。
|
|
4556
|
+
*/
|
|
4557
|
+
draw(source) {
|
|
4558
|
+
const { video, canvas, context } = source;
|
|
4559
|
+
if (video.readyState < 2) {
|
|
4560
|
+
return;
|
|
4561
|
+
}
|
|
4562
|
+
const scale = Math.min(canvas.width / video.videoWidth, canvas.height / video.videoHeight);
|
|
4563
|
+
const width = video.videoWidth * scale;
|
|
4564
|
+
const height = video.videoHeight * scale;
|
|
4565
|
+
context.fillStyle = "black";
|
|
4566
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
4567
|
+
context.drawImage(video, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
|
|
4568
|
+
}
|
|
4569
|
+
/**
|
|
4570
|
+
* 仅为本地文件选择最接近宽高比的模型档位;比例距离相同时优先横屏。
|
|
4571
|
+
* 无固定档位时保留公共尺寸解析规则,帧率和显式编码配置不变。
|
|
4572
|
+
*/
|
|
4573
|
+
resolveVideoFormat(format, mediaService) {
|
|
4574
|
+
format.validate();
|
|
4575
|
+
const buckets = resolutionBuckets(mediaService.model);
|
|
4576
|
+
let target = buckets.find((size2) => size2.width === format.width && size2.height === format.height);
|
|
4577
|
+
if (!target && buckets.length > 0) {
|
|
4578
|
+
const ratio = format.width / format.height;
|
|
4579
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
4580
|
+
for (const bucket of buckets) {
|
|
4581
|
+
const distance = Math.abs(Math.log(bucket.width / bucket.height / ratio));
|
|
4582
|
+
const tied = Math.abs(distance - bestDistance) < 1e-9;
|
|
4583
|
+
if (distance < bestDistance - 1e-9 || tied && bucket.width >= bucket.height && target && target.width < target.height) {
|
|
4584
|
+
target = bucket;
|
|
4585
|
+
bestDistance = distance;
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
const size = mediaService.resolveModelInputSize(target ?? format);
|
|
4590
|
+
return format.resized(size.width, size.height);
|
|
4591
|
+
}
|
|
4592
|
+
/**
|
|
4593
|
+
* 解码或绘制失败时停止媒体推进,并报告一次运行期错误。
|
|
4594
|
+
*/
|
|
4595
|
+
handleError(source) {
|
|
4596
|
+
if (this.source !== source || source.drawTimer === void 0) {
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4599
|
+
clearInterval(source.drawTimer);
|
|
4600
|
+
source.drawTimer = void 0;
|
|
4601
|
+
source.video.pause();
|
|
4602
|
+
XmaxLogger.realtime.warning(() => "\u672C\u5730\u89C6\u9891\u89E3\u7801\u5931\u8D25 (Local Video Decode Failed)");
|
|
4603
|
+
this.onError(new XmaxError("MEDIA_ERROR" /* mediaError */, "Local video decoding failed"));
|
|
4604
|
+
}
|
|
4605
|
+
/**
|
|
4606
|
+
* 构造统一的媒体操作取消错误。
|
|
4607
|
+
*/
|
|
4608
|
+
cancelled() {
|
|
4609
|
+
return new XmaxError("CANCELLED" /* cancelled */, "Local video operation was cancelled");
|
|
4610
|
+
}
|
|
4611
|
+
};
|
|
4612
|
+
|
|
4613
|
+
// src/Service/Realtime/RealtimeContext.ts
|
|
4614
|
+
var RealtimeContext = class {
|
|
4615
|
+
/**
|
|
4616
|
+
* 创建实时生成条件并规范化文本和参考路径。
|
|
4617
|
+
*/
|
|
4618
|
+
constructor(init) {
|
|
4619
|
+
/**
|
|
4620
|
+
* 服务端直接读取的网络视频。
|
|
4621
|
+
*/
|
|
4622
|
+
__publicField(this, "referenceVideo");
|
|
4623
|
+
/**
|
|
4624
|
+
* 生成条件
|
|
4625
|
+
*/
|
|
4626
|
+
/**
|
|
4627
|
+
* 实时生成使用的文本条件。
|
|
4628
|
+
*/
|
|
4629
|
+
__publicField(this, "prompt");
|
|
4630
|
+
/**
|
|
4631
|
+
* 已上传参考图片的可选远端路径。
|
|
4632
|
+
*/
|
|
4633
|
+
__publicField(this, "referencePath");
|
|
4634
|
+
this.referenceVideo = init.referenceVideo && Object.freeze({ ...init.referenceVideo });
|
|
4635
|
+
this.prompt = init.prompt.trim();
|
|
4636
|
+
const normalizedReferencePath = init.referencePath?.trim();
|
|
4637
|
+
this.referencePath = normalizedReferencePath || void 0;
|
|
4638
|
+
}
|
|
4639
|
+
};
|
|
4640
|
+
|
|
4641
|
+
// src/Service/Realtime/RealtimeSession.ts
|
|
4642
|
+
var RealtimeSession = class {
|
|
4643
|
+
/**
|
|
4644
|
+
* 创建实时会话。
|
|
4645
|
+
*
|
|
4646
|
+
* @param init.id 会话标识(sessionUid)。
|
|
4647
|
+
* @param init.userID 业务用户标识。
|
|
4648
|
+
* @param init.status 会话状态。
|
|
4649
|
+
* @param init.connection RTC 连接参数。
|
|
4650
|
+
* @param init.closeReason 会话关闭原因。
|
|
4651
|
+
*/
|
|
4652
|
+
constructor(init) {
|
|
4653
|
+
/**
|
|
4654
|
+
* 会话信息
|
|
4655
|
+
*/
|
|
4656
|
+
/**
|
|
4657
|
+
* 会话标识(sessionUid)。
|
|
4658
|
+
*/
|
|
4659
|
+
__publicField(this, "id");
|
|
4660
|
+
/**
|
|
4661
|
+
* 业务用户标识。
|
|
4662
|
+
*/
|
|
4663
|
+
__publicField(this, "userID");
|
|
4664
|
+
/**
|
|
4665
|
+
* 运行状态
|
|
4666
|
+
*/
|
|
4667
|
+
/**
|
|
4668
|
+
* 会话状态(如 ACTIVE);响应未携带时为空。
|
|
4669
|
+
*/
|
|
4670
|
+
__publicField(this, "status");
|
|
4671
|
+
/**
|
|
4672
|
+
* 会话关闭原因。
|
|
4673
|
+
*/
|
|
4674
|
+
__publicField(this, "closeReason");
|
|
4675
|
+
/**
|
|
4676
|
+
* RTC 连接
|
|
4677
|
+
*/
|
|
4678
|
+
/**
|
|
4679
|
+
* RTC 连接参数;心跳等响应未携带完整参数时为空。
|
|
4680
|
+
*/
|
|
4681
|
+
__publicField(this, "connection");
|
|
4682
|
+
this.id = init.id;
|
|
4683
|
+
this.userID = init.userID;
|
|
4684
|
+
this.status = init.status;
|
|
4685
|
+
this.connection = init.connection;
|
|
4686
|
+
this.closeReason = init.closeReason;
|
|
4687
|
+
}
|
|
4688
|
+
};
|
|
4689
|
+
|
|
4690
|
+
// src/Foundation/RTC/RoomJoinConfiguration.ts
|
|
4691
|
+
var RoomJoinConfiguration = class {
|
|
4692
|
+
/**
|
|
4693
|
+
* 创建房间加入参数。
|
|
4694
|
+
*
|
|
4695
|
+
* @param init.roomID TRTC 房间号。
|
|
4696
|
+
* @param init.userID TRTC 登录身份。
|
|
4697
|
+
* @param init.sdkAppID TRTC SDKAppID(字符串形式)。
|
|
4698
|
+
* @param init.userSig TRTC 登录签名。
|
|
4699
|
+
* @param init.privateMapKey TRTC 进房鉴权字段。
|
|
4700
|
+
*/
|
|
4701
|
+
constructor(init) {
|
|
4702
|
+
/**
|
|
4703
|
+
* RTC 提供方。
|
|
4704
|
+
*/
|
|
4705
|
+
__publicField(this, "provider", "trtc" /* trtc */);
|
|
4706
|
+
/**
|
|
4707
|
+
* 进房参数
|
|
4708
|
+
*/
|
|
4709
|
+
/**
|
|
4710
|
+
* TRTC 房间号(字符串房间号)。
|
|
4711
|
+
*/
|
|
4712
|
+
__publicField(this, "roomID");
|
|
4713
|
+
/**
|
|
4714
|
+
* TRTC 登录身份(`rtc_user_id`)。
|
|
4715
|
+
*/
|
|
4716
|
+
__publicField(this, "userID");
|
|
4717
|
+
/**
|
|
4718
|
+
* TRTC SDKAppID(字符串形式,进房时转换为数值)。
|
|
4719
|
+
*/
|
|
4720
|
+
__publicField(this, "sdkAppID");
|
|
4721
|
+
/**
|
|
4722
|
+
* TRTC 登录签名。
|
|
4723
|
+
*/
|
|
4724
|
+
__publicField(this, "userSig");
|
|
4725
|
+
/**
|
|
3803
4726
|
* TRTC 进房鉴权字段(字符串房间号对应的 privateMapKey)。
|
|
3804
4727
|
*/
|
|
3805
4728
|
__publicField(this, "privateMapKey");
|
|
@@ -3872,10 +4795,10 @@ var RealtimeSessionConnection = class {
|
|
|
3872
4795
|
}
|
|
3873
4796
|
};
|
|
3874
4797
|
function toRoomJoinConfiguration(connection) {
|
|
3875
|
-
return connection.provider === "trtc" ? new RoomJoinConfiguration(connection) : connection;
|
|
4798
|
+
return connection.provider === "trtc" /* trtc */ ? new RoomJoinConfiguration(connection) : connection;
|
|
3876
4799
|
}
|
|
3877
4800
|
function connectionAppID(connection) {
|
|
3878
|
-
return connection.provider === "trtc" ? connection.sdkAppID : connection.appID;
|
|
4801
|
+
return connection.provider === "trtc" /* trtc */ ? connection.sdkAppID : connection.appID;
|
|
3879
4802
|
}
|
|
3880
4803
|
|
|
3881
4804
|
// src/Service/Realtime/RealtimeSessionService.ts
|
|
@@ -4090,7 +5013,7 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
4090
5013
|
return void 0;
|
|
4091
5014
|
}
|
|
4092
5015
|
return new RealtimeSessionConnection({
|
|
4093
|
-
provider: "trtc"
|
|
5016
|
+
provider: "trtc" /* trtc */,
|
|
4094
5017
|
roomID,
|
|
4095
5018
|
sdkAppID,
|
|
4096
5019
|
userID,
|
|
@@ -4234,9 +5157,21 @@ var RoomEvent = class _RoomEvent {
|
|
|
4234
5157
|
static start(options) {
|
|
4235
5158
|
return _RoomEvent.encode({
|
|
4236
5159
|
event: "start",
|
|
4237
|
-
params:
|
|
5160
|
+
params: {
|
|
5161
|
+
..._RoomEvent.generationParameters(options),
|
|
5162
|
+
mirror: options.mirror ?? false,
|
|
5163
|
+
...options.context.referenceVideo && {
|
|
5164
|
+
ref_video_path: options.context.referenceVideo.path,
|
|
5165
|
+
sample_method: options.context.referenceVideo.sampleMethod,
|
|
5166
|
+
target_size: [
|
|
5167
|
+
options.targetSize?.width ?? options.videoFormat.width,
|
|
5168
|
+
options.targetSize?.height ?? options.videoFormat.height
|
|
5169
|
+
]
|
|
5170
|
+
}
|
|
5171
|
+
},
|
|
4238
5172
|
user_id: options.userID,
|
|
4239
|
-
uid: options.taskID
|
|
5173
|
+
uid: options.taskID,
|
|
5174
|
+
...options.context.referenceVideo && { session_uid: options.taskID }
|
|
4240
5175
|
});
|
|
4241
5176
|
}
|
|
4242
5177
|
/**
|
|
@@ -4452,20 +5387,20 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
4452
5387
|
return parsed;
|
|
4453
5388
|
}
|
|
4454
5389
|
const key = `${senderUserID}:${chunk.eventId}`;
|
|
4455
|
-
let
|
|
4456
|
-
if (!
|
|
4457
|
-
|
|
4458
|
-
this.records.set(key,
|
|
5390
|
+
let record2 = this.records.get(key);
|
|
5391
|
+
if (!record2) {
|
|
5392
|
+
record2 = { count: chunk.count, parts: /* @__PURE__ */ new Map() };
|
|
5393
|
+
this.records.set(key, record2);
|
|
4459
5394
|
this.evictOverflow();
|
|
4460
5395
|
}
|
|
4461
|
-
|
|
4462
|
-
if (
|
|
5396
|
+
record2.parts.set(chunk.index, chunk.data);
|
|
5397
|
+
if (record2.parts.size < record2.count) {
|
|
4463
5398
|
return void 0;
|
|
4464
5399
|
}
|
|
4465
5400
|
this.records.delete(key);
|
|
4466
5401
|
const parts = [];
|
|
4467
|
-
for (let index = 0; index <
|
|
4468
|
-
const part =
|
|
5402
|
+
for (let index = 0; index < record2.count; index += 1) {
|
|
5403
|
+
const part = record2.parts.get(index);
|
|
4469
5404
|
if (part === void 0) {
|
|
4470
5405
|
return void 0;
|
|
4471
5406
|
}
|
|
@@ -4495,16 +5430,16 @@ var _RoomMessageCodec = class _RoomMessageCodec {
|
|
|
4495
5430
|
if (this.chunkEvent === null || typeof parsed !== "object" || parsed === null) {
|
|
4496
5431
|
return void 0;
|
|
4497
5432
|
}
|
|
4498
|
-
const
|
|
4499
|
-
if (
|
|
5433
|
+
const record2 = parsed;
|
|
5434
|
+
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
5435
|
return void 0;
|
|
4501
5436
|
}
|
|
4502
|
-
const index =
|
|
4503
|
-
const count =
|
|
5437
|
+
const index = record2.index;
|
|
5438
|
+
const count = record2.count;
|
|
4504
5439
|
if (count < 1 || index < 0 || index >= count) {
|
|
4505
5440
|
return void 0;
|
|
4506
5441
|
}
|
|
4507
|
-
return { eventId:
|
|
5442
|
+
return { eventId: record2.eventId, index, count, data: record2.data };
|
|
4508
5443
|
}
|
|
4509
5444
|
/**
|
|
4510
5445
|
* 淘汰最旧的未完成记录,保持缓存不超过上限。
|
|
@@ -4616,9 +5551,9 @@ var RoomController = class {
|
|
|
4616
5551
|
*
|
|
4617
5552
|
* @throws 已有房间未离开、操作失效或进房失败时抛出错误。
|
|
4618
5553
|
*/
|
|
4619
|
-
async join(connection,
|
|
5554
|
+
async join(connection, ensureActive2) {
|
|
4620
5555
|
try {
|
|
4621
|
-
|
|
5556
|
+
ensureActive2();
|
|
4622
5557
|
} catch (error) {
|
|
4623
5558
|
throw XmaxError.from(error);
|
|
4624
5559
|
}
|
|
@@ -4633,10 +5568,10 @@ var RoomController = class {
|
|
|
4633
5568
|
this.heartbeat.stop();
|
|
4634
5569
|
try {
|
|
4635
5570
|
if (!this.customCodec) this.codec = new RoomMessageCodec(
|
|
4636
|
-
connection.provider === "vertc" ? null : connection.provider === "agora" ? "__agora_chunk__" : "__trtc_chunk__"
|
|
5571
|
+
connection.provider === "vertc" /* vertc */ ? null : connection.provider === "agora" /* agora */ ? "__agora_chunk__" : "__trtc_chunk__"
|
|
4637
5572
|
);
|
|
4638
5573
|
await this.rtcManager.joinRoom(toRoomJoinConfiguration(connection));
|
|
4639
|
-
|
|
5574
|
+
ensureActive2();
|
|
4640
5575
|
if (this.state.kind !== "joining" || this.state.operationID !== operationID || this.leaveOperation) {
|
|
4641
5576
|
throw new XmaxError("CANCELLED" /* cancelled */, "RTC room join was cancelled");
|
|
4642
5577
|
}
|
|
@@ -4677,6 +5612,7 @@ var RoomController = class {
|
|
|
4677
5612
|
startGeneration(options) {
|
|
4678
5613
|
return this.send(
|
|
4679
5614
|
RoomEvent.start({
|
|
5615
|
+
mirror: options.mirror ?? false,
|
|
4680
5616
|
userID: this.requireUserID(),
|
|
4681
5617
|
taskID: options.taskID,
|
|
4682
5618
|
videoFormat: options.videoFormat,
|
|
@@ -4828,6 +5764,7 @@ var _EncodingController = class _EncodingController {
|
|
|
4828
5764
|
* 基础层组件
|
|
4829
5765
|
*/
|
|
4830
5766
|
__publicField(this, "rtcManager");
|
|
5767
|
+
__publicField(this, "lastAppliedConfiguration");
|
|
4831
5768
|
this.rtcManager = options.rtcManager;
|
|
4832
5769
|
}
|
|
4833
5770
|
/**
|
|
@@ -4863,14 +5800,38 @@ var _EncodingController = class _EncodingController {
|
|
|
4863
5800
|
"Minimum bitrate must not exceed maximum bitrate after applying SDK defaults"
|
|
4864
5801
|
);
|
|
4865
5802
|
}
|
|
4866
|
-
|
|
5803
|
+
const configuration = {
|
|
4867
5804
|
width: videoFormat.width,
|
|
4868
5805
|
height: videoFormat.height,
|
|
4869
5806
|
frameRate: videoFormat.fps,
|
|
4870
5807
|
minimumBitrate: minimum,
|
|
4871
5808
|
maximumBitrate: maximum,
|
|
4872
5809
|
encoderPreference: ENCODER_PREFERENCE_MAP[videoFormat.encoderPreference]
|
|
5810
|
+
};
|
|
5811
|
+
await this.rtcManager.configureVideoEncoding(configuration);
|
|
5812
|
+
this.lastAppliedConfiguration = { ...configuration };
|
|
5813
|
+
}
|
|
5814
|
+
/** 只替换目标帧率,保留最近成功应用的尺寸、已解析码率区间和偏好。 */
|
|
5815
|
+
async updateFrameRate(fps) {
|
|
5816
|
+
const previous = this.lastAppliedConfiguration;
|
|
5817
|
+
if (!previous) {
|
|
5818
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Apply an encoder configuration before updating frame rate");
|
|
5819
|
+
}
|
|
5820
|
+
const format = new RealtimeVideoFormat({
|
|
5821
|
+
width: previous.width,
|
|
5822
|
+
height: previous.height,
|
|
5823
|
+
fps,
|
|
5824
|
+
minimumBitrate: previous.minimumBitrate,
|
|
5825
|
+
maximumBitrate: previous.maximumBitrate,
|
|
5826
|
+
encoderPreference: previous.encoderPreference === "maintainFramerate" /* maintainFramerate */ ? "maintainFramerate" /* maintainFramerate */ : "maintainQuality" /* maintainQuality */
|
|
4873
5827
|
});
|
|
5828
|
+
format.validate();
|
|
5829
|
+
if (fps !== previous.frameRate) {
|
|
5830
|
+
const next = { ...previous, frameRate: fps };
|
|
5831
|
+
await this.rtcManager.configureVideoEncoding(next);
|
|
5832
|
+
this.lastAppliedConfiguration = next;
|
|
5833
|
+
}
|
|
5834
|
+
return format;
|
|
4874
5835
|
}
|
|
4875
5836
|
/**
|
|
4876
5837
|
* 按像素面积与帧率计算码率区间(kbps):流畅优先为最低,画质优先为最高。
|
|
@@ -5019,6 +5980,7 @@ var StreamController = class {
|
|
|
5019
5980
|
* 运行状态
|
|
5020
5981
|
*/
|
|
5021
5982
|
__publicField(this, "state", makeInitialState());
|
|
5983
|
+
__publicField(this, "roomListener");
|
|
5022
5984
|
this.rtcManager = options.rtcManager;
|
|
5023
5985
|
this.roomController = options.roomController ?? new RoomController({ rtcManager: options.rtcManager });
|
|
5024
5986
|
this.encodingController = options.encodingController ?? new EncodingController({ rtcManager: options.rtcManager });
|
|
@@ -5027,13 +5989,19 @@ var StreamController = class {
|
|
|
5027
5989
|
this.remoteStreamListener = options.remoteStreamListener ?? (() => {
|
|
5028
5990
|
});
|
|
5029
5991
|
this.generationTimeoutMs = options.generationTimeoutMs ?? 3e4;
|
|
5992
|
+
this.roomController.setListener({
|
|
5993
|
+
onRoomMessage: (sender, message) => {
|
|
5994
|
+
this.handleNetworkVideoCompletion(sender, message);
|
|
5995
|
+
this.roomListener?.onRoomMessage(sender, message);
|
|
5996
|
+
}
|
|
5997
|
+
});
|
|
5030
5998
|
this.rtcManager.setEventListener({
|
|
5031
5999
|
onTokenWillExpire: options.onCredentialsRequired,
|
|
5032
6000
|
onTokenExpired: options.onCredentialsRequired,
|
|
5033
6001
|
onRoomRejoining: () => this.handleRoomRejoining(),
|
|
5034
6002
|
onError: (error) => this.errorListener(error),
|
|
5035
6003
|
onNetworkStatistics: (statistics) => {
|
|
5036
|
-
if (this.state.
|
|
6004
|
+
if (this.state.roomID) {
|
|
5037
6005
|
this.networkStatisticsListener?.(statistics);
|
|
5038
6006
|
}
|
|
5039
6007
|
},
|
|
@@ -5101,25 +6069,31 @@ var StreamController = class {
|
|
|
5101
6069
|
*
|
|
5102
6070
|
* @throws 格式无效、码率区间无效或 RTC 配置失败时抛出错误。
|
|
5103
6071
|
*/
|
|
5104
|
-
async
|
|
6072
|
+
async updateUplinkVideoFormat(videoFormat) {
|
|
5105
6073
|
await this.encodingController.configure(videoFormat);
|
|
5106
6074
|
}
|
|
6075
|
+
/** 不根据新帧率重新推算码率区间。 */
|
|
6076
|
+
async updateUplinkFrameRate(fps) {
|
|
6077
|
+
return this.encodingController.updateFrameRate(fps);
|
|
6078
|
+
}
|
|
5107
6079
|
/**
|
|
5108
6080
|
* 加入 RTC 房间并发布本地媒体流。
|
|
5109
6081
|
*
|
|
5110
6082
|
* @throws 连接已取消,或 RTC 进房、房间配置与本地流发布失败时抛出错误。
|
|
5111
6083
|
*/
|
|
5112
|
-
async connect(connection, includeLocalAudio,
|
|
6084
|
+
async connect(connection, includeLocalAudio, ensureActive2, beforePublish, publishLocalMedia = true) {
|
|
5113
6085
|
this.configureRoom({ roomID: connection.roomID, botID: connection.botID });
|
|
5114
|
-
await this.roomController.join(connection,
|
|
5115
|
-
|
|
6086
|
+
await this.roomController.join(connection, ensureActive2);
|
|
6087
|
+
ensureActive2();
|
|
5116
6088
|
XmaxLogger.stream.info(
|
|
5117
6089
|
() => `RTC \u623F\u95F4\u5DF2\u914D\u7F6E (RTC Room Configured)
|
|
5118
6090
|
\u2514\u2500 roomID: ${connection.roomID}, botID: ${connection.botID ?? "(\u672A\u8BBE\u7F6E)"}`
|
|
5119
6091
|
);
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
6092
|
+
if (publishLocalMedia) {
|
|
6093
|
+
await beforePublish?.();
|
|
6094
|
+
ensureActive2();
|
|
6095
|
+
await this.publishLocalStream(includeLocalAudio);
|
|
6096
|
+
}
|
|
5123
6097
|
}
|
|
5124
6098
|
/**
|
|
5125
6099
|
* 清理生成状态、本地发布和远端订阅,并离开当前 RTC 房间。
|
|
@@ -5180,18 +6154,22 @@ var StreamController = class {
|
|
|
5180
6154
|
}, this.generationTimeoutMs)
|
|
5181
6155
|
};
|
|
5182
6156
|
this.state.generationTaskID = taskID;
|
|
6157
|
+
this.state.networkVideoCompletion = options.context.referenceVideo ? { taskID, finished: false, scheduled: false } : void 0;
|
|
5183
6158
|
this.state.generationWaiter = waiter;
|
|
5184
6159
|
void confirmation.catch(() => {
|
|
5185
6160
|
});
|
|
5186
6161
|
try {
|
|
5187
6162
|
const sending = this.roomController.startGeneration({
|
|
5188
6163
|
taskID,
|
|
6164
|
+
mirror: options.mirror ?? false,
|
|
5189
6165
|
videoFormat: options.videoFormat,
|
|
5190
6166
|
targetSize: options.targetSize,
|
|
5191
6167
|
context: options.context
|
|
5192
6168
|
});
|
|
5193
6169
|
const state2 = this.state;
|
|
5194
|
-
const sent = Promise.resolve(sending).then(() => {
|
|
6170
|
+
const sent = Promise.resolve(sending).then(async () => {
|
|
6171
|
+
if (this.state !== state2 || state2.generationTaskID !== taskID) return;
|
|
6172
|
+
await options.onStartSent?.();
|
|
5195
6173
|
if (this.state !== state2 || state2.generationTaskID !== taskID) return;
|
|
5196
6174
|
for (const userID of state2.publishedRemoteUserIDs) {
|
|
5197
6175
|
void this.handleRemoteVideoPublished(userID, true);
|
|
@@ -5257,9 +6235,9 @@ var StreamController = class {
|
|
|
5257
6235
|
*
|
|
5258
6236
|
* @throws 操作已取消、房间未就绪或信令发送失败时抛出错误。
|
|
5259
6237
|
*/
|
|
5260
|
-
async
|
|
6238
|
+
async updateDownlinkVideoResolution(taskID, targetSize, ensureActive2) {
|
|
5261
6239
|
try {
|
|
5262
|
-
await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive });
|
|
6240
|
+
await this.roomController.changeTargetSize({ taskID, targetSize, ensureActive: ensureActive2 });
|
|
5263
6241
|
} catch (error) {
|
|
5264
6242
|
throw XmaxError.from(error);
|
|
5265
6243
|
}
|
|
@@ -5296,7 +6274,50 @@ var StreamController = class {
|
|
|
5296
6274
|
* 设置房间业务消息监听器,传入空值时清除监听器。
|
|
5297
6275
|
*/
|
|
5298
6276
|
setRoomListener(listener) {
|
|
5299
|
-
this.
|
|
6277
|
+
this.roomListener = listener;
|
|
6278
|
+
}
|
|
6279
|
+
/**
|
|
6280
|
+
* 生成状态已提交后登记完成通知,避免快速任务在启动返回前交付完成事件。
|
|
6281
|
+
*/
|
|
6282
|
+
activateNetworkVideoCompletion(onFinish) {
|
|
6283
|
+
const completion = this.state.networkVideoCompletion;
|
|
6284
|
+
if (completion) {
|
|
6285
|
+
completion.onFinish = onFinish;
|
|
6286
|
+
this.deliverNetworkVideoCompletion();
|
|
6287
|
+
}
|
|
6288
|
+
}
|
|
6289
|
+
/**
|
|
6290
|
+
* 仅接受当前任务和目标机器人的完成消息;不因完成事件立即退房。
|
|
6291
|
+
*/
|
|
6292
|
+
handleNetworkVideoCompletion(sender, message) {
|
|
6293
|
+
const state2 = this.state;
|
|
6294
|
+
const completion = state2.networkVideoCompletion;
|
|
6295
|
+
if (!state2.roomID || !completion || completion.taskID !== state2.generationTaskID || message.event !== "video_stopped" || message.uid !== completion.taskID || !sender || state2.botID && sender !== state2.botID) {
|
|
6296
|
+
return;
|
|
6297
|
+
}
|
|
6298
|
+
completion.finished = true;
|
|
6299
|
+
this.deliverNetworkVideoCompletion();
|
|
6300
|
+
}
|
|
6301
|
+
/**
|
|
6302
|
+
* 异步交付一次完成通知;执行前复核任务,隔离用户回调异常。
|
|
6303
|
+
*/
|
|
6304
|
+
deliverNetworkVideoCompletion() {
|
|
6305
|
+
const state2 = this.state;
|
|
6306
|
+
const completion = state2.networkVideoCompletion;
|
|
6307
|
+
if (!completion?.finished || !completion.onFinish || completion.scheduled) {
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
completion.scheduled = true;
|
|
6311
|
+
queueMicrotask(() => {
|
|
6312
|
+
if (this.state !== state2 || state2.generationTaskID !== completion.taskID) {
|
|
6313
|
+
return;
|
|
6314
|
+
}
|
|
6315
|
+
try {
|
|
6316
|
+
void Promise.resolve(completion.onFinish?.()).catch(() => {
|
|
6317
|
+
});
|
|
6318
|
+
} catch {
|
|
6319
|
+
}
|
|
6320
|
+
});
|
|
5300
6321
|
}
|
|
5301
6322
|
/**
|
|
5302
6323
|
* 配置当前房间;已有发布、订阅或生成任务时拒绝重新配置。
|
|
@@ -5527,6 +6548,7 @@ var StreamController = class {
|
|
|
5527
6548
|
const remoteAudioUserIDs = [...this.state.subscribedRemoteAudioUserIDs];
|
|
5528
6549
|
this.state.generationTaskID = void 0;
|
|
5529
6550
|
this.state.generationWaiter = void 0;
|
|
6551
|
+
this.state.networkVideoCompletion = void 0;
|
|
5530
6552
|
this.state.activeRemoteStream = void 0;
|
|
5531
6553
|
this.state.subscribedRemoteAudioUserIDs.clear();
|
|
5532
6554
|
if (waiter) {
|
|
@@ -5635,6 +6657,195 @@ var StreamController = class {
|
|
|
5635
6657
|
}
|
|
5636
6658
|
};
|
|
5637
6659
|
|
|
6660
|
+
// src/Stream/Quality/QualityTiers.ts
|
|
6661
|
+
var INITIAL_QUALITY_LEVEL = 2;
|
|
6662
|
+
var QUALITY_TIERS = {
|
|
6663
|
+
L1: { width: 1024, height: 1920, frameRate: 30 },
|
|
6664
|
+
L2: { width: 768, height: 1440, frameRate: 24 },
|
|
6665
|
+
L3: { width: 640, height: 1200, frameRate: 24 },
|
|
6666
|
+
L4: { width: 512, height: 960, frameRate: 20 },
|
|
6667
|
+
L5: { width: 384, height: 720, frameRate: 16 }
|
|
6668
|
+
};
|
|
6669
|
+
|
|
6670
|
+
// src/Stream/Quality/QualityPolicy.ts
|
|
6671
|
+
var DEFAULT_POLICY = {
|
|
6672
|
+
poorSamples: 3,
|
|
6673
|
+
goodSamples: 5,
|
|
6674
|
+
stabilizationMs: 4e3,
|
|
6675
|
+
probeMs: 2e4,
|
|
6676
|
+
upgradeCooldownMs: 6e4,
|
|
6677
|
+
sampleTimeoutMs: 5e3
|
|
6678
|
+
};
|
|
6679
|
+
|
|
6680
|
+
// src/Stream/Quality/QualityController.ts
|
|
6681
|
+
var ORDERED_QUALITY_TIERS = Object.values(QUALITY_TIERS);
|
|
6682
|
+
var QualityController = class {
|
|
6683
|
+
constructor(apply, now = () => performance.now(), policy = {}) {
|
|
6684
|
+
__publicField(this, "apply", apply);
|
|
6685
|
+
__publicField(this, "now", now);
|
|
6686
|
+
__publicField(this, "policy");
|
|
6687
|
+
__publicField(this, "active", false);
|
|
6688
|
+
__publicField(this, "index", 0);
|
|
6689
|
+
__publicField(this, "good", 0);
|
|
6690
|
+
__publicField(this, "poor", 0);
|
|
6691
|
+
__publicField(this, "lastSampleAt");
|
|
6692
|
+
__publicField(this, "stableUntil", 0);
|
|
6693
|
+
__publicField(this, "upgradeAfter", 0);
|
|
6694
|
+
__publicField(this, "probeUntil", 0);
|
|
6695
|
+
__publicField(this, "switching", false);
|
|
6696
|
+
__publicField(this, "lifecycle", new AbortController());
|
|
6697
|
+
this.policy = { ...DEFAULT_POLICY, ...policy };
|
|
6698
|
+
for (const value of Object.values(this.policy)) {
|
|
6699
|
+
if (!Number.isFinite(value) || value < 0) throw new Error("Invalid quality policy");
|
|
6700
|
+
}
|
|
6701
|
+
for (const value of [this.policy.poorSamples, this.policy.goodSamples]) {
|
|
6702
|
+
if (!Number.isInteger(value) || value < 1) throw new Error("Sample counts must be positive integers");
|
|
6703
|
+
}
|
|
6704
|
+
}
|
|
6705
|
+
get snapshot() {
|
|
6706
|
+
return Object.freeze({
|
|
6707
|
+
active: this.active,
|
|
6708
|
+
level: this.index + 1,
|
|
6709
|
+
...ORDERED_QUALITY_TIERS[this.index],
|
|
6710
|
+
switching: this.switching,
|
|
6711
|
+
upgradeAfter: this.upgradeAfter
|
|
6712
|
+
});
|
|
6713
|
+
}
|
|
6714
|
+
/** 传入调用方保存的档位,不在 start 中擅自改写编码配置。 */
|
|
6715
|
+
start(level = 1) {
|
|
6716
|
+
if (!Number.isInteger(level) || level < 1 || level > ORDERED_QUALITY_TIERS.length) {
|
|
6717
|
+
throw new Error("Unsupported quality level");
|
|
6718
|
+
}
|
|
6719
|
+
this.stop();
|
|
6720
|
+
this.lifecycle = new AbortController();
|
|
6721
|
+
this.index = level - 1;
|
|
6722
|
+
this.active = true;
|
|
6723
|
+
this.stableUntil = this.upgradeAfter = this.probeUntil = 0;
|
|
6724
|
+
}
|
|
6725
|
+
/** 取消旧会话的迟到结果;底层尚未完成时仍保持串行限制。 */
|
|
6726
|
+
stop() {
|
|
6727
|
+
this.active = false;
|
|
6728
|
+
this.lifecycle.abort();
|
|
6729
|
+
this.resetSamples();
|
|
6730
|
+
}
|
|
6731
|
+
/** 前台操作期间丢弃累计样本,但保留试升失败后的冷却时间。 */
|
|
6732
|
+
resetSamples() {
|
|
6733
|
+
this.resetCounts();
|
|
6734
|
+
this.lastSampleAt = void 0;
|
|
6735
|
+
}
|
|
6736
|
+
/** 只接收质量事件,不得用定时器重复提交同一份缓存统计。异常由调用方记录。 */
|
|
6737
|
+
async observe(quality, sampleAt = this.now()) {
|
|
6738
|
+
if (!this.active || this.switching || !Number.isFinite(sampleAt)) return;
|
|
6739
|
+
const now = this.now();
|
|
6740
|
+
if (sampleAt > now) return;
|
|
6741
|
+
if (this.lastSampleAt !== void 0) {
|
|
6742
|
+
if (sampleAt <= this.lastSampleAt) return;
|
|
6743
|
+
if (sampleAt - this.lastSampleAt > this.policy.sampleTimeoutMs) this.resetCounts();
|
|
6744
|
+
}
|
|
6745
|
+
this.lastSampleAt = sampleAt;
|
|
6746
|
+
if (now - sampleAt > this.policy.sampleTimeoutMs || sampleAt < this.stableUntil) {
|
|
6747
|
+
this.resetCounts();
|
|
6748
|
+
return;
|
|
6749
|
+
}
|
|
6750
|
+
if (quality === 4 || quality === 5) {
|
|
6751
|
+
this.good = 0;
|
|
6752
|
+
this.poor++;
|
|
6753
|
+
if (this.poor >= this.policy.poorSamples && this.index < ORDERED_QUALITY_TIERS.length - 1) {
|
|
6754
|
+
return this.change(this.index + 1, now < this.probeUntil ? "failedProbe" : "poorNetwork");
|
|
6755
|
+
}
|
|
6756
|
+
} else if (quality === 1 || quality === 2) {
|
|
6757
|
+
this.poor = 0;
|
|
6758
|
+
if (sampleAt < this.upgradeAfter) {
|
|
6759
|
+
this.good = 0;
|
|
6760
|
+
return;
|
|
6761
|
+
}
|
|
6762
|
+
this.good++;
|
|
6763
|
+
if (this.good >= this.policy.goodSamples && this.index > 0) {
|
|
6764
|
+
return this.change(this.index - 1, "probeUpgrade");
|
|
6765
|
+
}
|
|
6766
|
+
} else {
|
|
6767
|
+
this.resetCounts();
|
|
6768
|
+
}
|
|
6769
|
+
return;
|
|
6770
|
+
}
|
|
6771
|
+
resetCounts() {
|
|
6772
|
+
this.good = this.poor = 0;
|
|
6773
|
+
}
|
|
6774
|
+
async change(index, reason) {
|
|
6775
|
+
const lifecycle = this.lifecycle;
|
|
6776
|
+
const change = { from: this.index + 1, to: index + 1, ...ORDERED_QUALITY_TIERS[index], reason };
|
|
6777
|
+
this.switching = true;
|
|
6778
|
+
this.resetCounts();
|
|
6779
|
+
try {
|
|
6780
|
+
await this.apply(change, lifecycle.signal);
|
|
6781
|
+
if (lifecycle.signal.aborted) return;
|
|
6782
|
+
this.index = index;
|
|
6783
|
+
const now = this.now();
|
|
6784
|
+
this.stableUntil = now + this.policy.stabilizationMs;
|
|
6785
|
+
this.probeUntil = reason === "probeUpgrade" ? now + this.policy.probeMs : 0;
|
|
6786
|
+
if (reason === "failedProbe") this.upgradeAfter = now + this.policy.upgradeCooldownMs;
|
|
6787
|
+
return change;
|
|
6788
|
+
} catch (error) {
|
|
6789
|
+
if (!lifecycle.signal.aborted) {
|
|
6790
|
+
this.stableUntil = this.now() + this.policy.stabilizationMs;
|
|
6791
|
+
throw error;
|
|
6792
|
+
}
|
|
6793
|
+
return;
|
|
6794
|
+
} finally {
|
|
6795
|
+
this.switching = false;
|
|
6796
|
+
}
|
|
6797
|
+
}
|
|
6798
|
+
};
|
|
6799
|
+
|
|
6800
|
+
// src/Stream/Quality/QualityVideoFormat.ts
|
|
6801
|
+
function qualityVideoSize(baseline, level) {
|
|
6802
|
+
const tier = Object.values(QUALITY_TIERS)[level - 1];
|
|
6803
|
+
if (!Number.isInteger(level) || !tier) throw new Error("Unsupported quality level");
|
|
6804
|
+
baseline.validate();
|
|
6805
|
+
const scale = tier.width / QUALITY_TIERS.L1.width;
|
|
6806
|
+
const even = (value) => Math.max(2, Math.floor(value / 2) * 2);
|
|
6807
|
+
return { width: even(baseline.width * scale), height: even(baseline.height * scale) };
|
|
6808
|
+
}
|
|
6809
|
+
function qualityVideoFormat(baseline, level) {
|
|
6810
|
+
const size = qualityVideoSize(baseline, level);
|
|
6811
|
+
const tier = Object.values(QUALITY_TIERS)[level - 1];
|
|
6812
|
+
return new RealtimeVideoFormat({
|
|
6813
|
+
...baseline,
|
|
6814
|
+
...size,
|
|
6815
|
+
fps: Math.min(baseline.fps, tier.frameRate)
|
|
6816
|
+
});
|
|
6817
|
+
}
|
|
6818
|
+
|
|
6819
|
+
// src/Stream/Quality/QualityHistory.ts
|
|
6820
|
+
var QualityHistory = class {
|
|
6821
|
+
constructor(model) {
|
|
6822
|
+
__publicField(this, "model", model);
|
|
6823
|
+
}
|
|
6824
|
+
/** 无记录、损坏、SSR 或浏览器禁止存储时使用默认 L2。 */
|
|
6825
|
+
read(direction) {
|
|
6826
|
+
try {
|
|
6827
|
+
const level = Number(globalThis.localStorage.getItem(this.key(direction)));
|
|
6828
|
+
if (this.isValid(level)) return level;
|
|
6829
|
+
} catch {
|
|
6830
|
+
}
|
|
6831
|
+
return INITIAL_QUALITY_LEVEL;
|
|
6832
|
+
}
|
|
6833
|
+
/** 仅在应用成功后调用;下行成功表示信令已发送,不代表服务端已确认生效。 */
|
|
6834
|
+
write(direction, level) {
|
|
6835
|
+
if (!this.isValid(level)) return;
|
|
6836
|
+
try {
|
|
6837
|
+
globalThis.localStorage.setItem(this.key(direction), String(level));
|
|
6838
|
+
} catch {
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
key(direction) {
|
|
6842
|
+
return `xmax:quality:v1:agora:${encodeURIComponent(this.model)}:${direction}`;
|
|
6843
|
+
}
|
|
6844
|
+
isValid(level) {
|
|
6845
|
+
return Number.isInteger(level) && level >= 1 && level <= Object.keys(QUALITY_TIERS).length;
|
|
6846
|
+
}
|
|
6847
|
+
};
|
|
6848
|
+
|
|
5638
6849
|
// src/Core/Realtime/RealtimeCoordinator.ts
|
|
5639
6850
|
function scopeAffects(scope, kind) {
|
|
5640
6851
|
if (scope === 1 /* all */) {
|
|
@@ -5737,6 +6948,10 @@ var RealtimeCoordinator = class _RealtimeCoordinator {
|
|
|
5737
6948
|
get currentState() {
|
|
5738
6949
|
return this.state;
|
|
5739
6950
|
}
|
|
6951
|
+
/** 后台自动调档仅在没有前台操作或清理时申请操作租约。 */
|
|
6952
|
+
get isBusy() {
|
|
6953
|
+
return this.activeOperation !== void 0 || this.termination !== void 0;
|
|
6954
|
+
}
|
|
5740
6955
|
/**
|
|
5741
6956
|
* 设置实时状态监听器;设置后立即回放当前状态。
|
|
5742
6957
|
*
|
|
@@ -6078,13 +7293,18 @@ var RealtimeLaunchTimer = class {
|
|
|
6078
7293
|
* 开始新的启动周期并清空旧快照,返回记录相机创建完成的一次性回调。
|
|
6079
7294
|
*/
|
|
6080
7295
|
startCamera() {
|
|
7296
|
+
this.start();
|
|
7297
|
+
return this.measure("cameraMs", this.startedAt);
|
|
7298
|
+
}
|
|
7299
|
+
/**
|
|
7300
|
+
* 开始新的启动周期;非摄像头源不产生相机和发布耗时。
|
|
7301
|
+
*/
|
|
7302
|
+
start() {
|
|
6081
7303
|
this.sequence += 1;
|
|
6082
7304
|
this.active = true;
|
|
6083
7305
|
this.startedAt = performance.now();
|
|
6084
7306
|
this.snapshot = Object.freeze({});
|
|
6085
|
-
const complete = this.measure("cameraMs", this.startedAt);
|
|
6086
7307
|
this.notify();
|
|
6087
|
-
return complete;
|
|
6088
7308
|
}
|
|
6089
7309
|
/**
|
|
6090
7310
|
* 开始等待用户授权系统媒体权限,返回授权决定时调用的记录函数。
|
|
@@ -6208,14 +7428,19 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
6208
7428
|
}
|
|
6209
7429
|
const refreshVersion = ++this.refreshVersion;
|
|
6210
7430
|
this.credentialController = new AbortController();
|
|
6211
|
-
const videoFormat = options.localTrack.videoFormat;
|
|
7431
|
+
const videoFormat = options.localVideoFormat ?? options.localTrack.videoFormat;
|
|
6212
7432
|
if (!videoFormat) {
|
|
6213
7433
|
throw new XmaxError(
|
|
6214
7434
|
"INTERNAL_ERROR" /* internalError */,
|
|
6215
7435
|
"Local video stream has no video format"
|
|
6216
7436
|
);
|
|
6217
7437
|
}
|
|
6218
|
-
|
|
7438
|
+
const publishLocalMedia = options.publishLocalMedia ?? true;
|
|
7439
|
+
if (publishLocalMedia) {
|
|
7440
|
+
await streamController.updateUplinkVideoFormat(videoFormat);
|
|
7441
|
+
options.ensureCurrent();
|
|
7442
|
+
options.onVideoEncoderConfigured?.();
|
|
7443
|
+
}
|
|
6219
7444
|
options.ensureCurrent();
|
|
6220
7445
|
let completePublish;
|
|
6221
7446
|
await streamController.connect(connection, options.includeLocalAudio, () => {
|
|
@@ -6225,8 +7450,9 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
6225
7450
|
await options.beforePublish?.();
|
|
6226
7451
|
completeConnection();
|
|
6227
7452
|
completePublish = this.dependencies.timing.startPublish();
|
|
6228
|
-
});
|
|
7453
|
+
}, publishLocalMedia);
|
|
6229
7454
|
options.ensureCurrent();
|
|
7455
|
+
completeConnection();
|
|
6230
7456
|
completePublish?.();
|
|
6231
7457
|
options.onPublished();
|
|
6232
7458
|
sessionService.startHeartbeat(session.id, {
|
|
@@ -6275,7 +7501,7 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
6275
7501
|
async refreshCredentials() {
|
|
6276
7502
|
if (this.credentialRefresh) return this.credentialRefresh;
|
|
6277
7503
|
const current = this.activeSession;
|
|
6278
|
-
if (!current?.connection || current.connection.provider === "trtc") return;
|
|
7504
|
+
if (!current?.connection || current.connection.provider === "trtc" /* trtc */) return;
|
|
6279
7505
|
const version = this.refreshVersion;
|
|
6280
7506
|
const operation = (async () => {
|
|
6281
7507
|
try {
|
|
@@ -6360,7 +7586,7 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
6360
7586
|
},
|
|
6361
7587
|
attachHandler: (view) => {
|
|
6362
7588
|
this.remoteTarget?.setFrameInterpolation?.(void 0);
|
|
6363
|
-
view.isMirrored =
|
|
7589
|
+
view.isMirrored = false;
|
|
6364
7590
|
this.remoteTarget = view;
|
|
6365
7591
|
this.dependencies.onRenderAttached();
|
|
6366
7592
|
const mediaTrack = track.mediaStreamTrack;
|
|
@@ -6376,14 +7602,6 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
6376
7602
|
}
|
|
6377
7603
|
});
|
|
6378
7604
|
}
|
|
6379
|
-
/**
|
|
6380
|
-
* 同步远端结果画面的镜像状态:与当前本地摄像头位置保持一致。
|
|
6381
|
-
*/
|
|
6382
|
-
updateRemoteMirror() {
|
|
6383
|
-
if (this.remoteTarget) {
|
|
6384
|
-
this.remoteTarget.isMirrored = this.dependencies.isMirrored();
|
|
6385
|
-
}
|
|
6386
|
-
}
|
|
6387
7605
|
/**
|
|
6388
7606
|
* 构造当前远端生成结果媒体流。
|
|
6389
7607
|
*/
|
|
@@ -6428,7 +7646,7 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
6428
7646
|
);
|
|
6429
7647
|
return;
|
|
6430
7648
|
}
|
|
6431
|
-
if (next.provider !== "trtc" && (force || !previous || previous.provider === "trtc" || next.roomToken !== previous.roomToken)) {
|
|
7649
|
+
if (next.provider !== "trtc" /* trtc */ && (force || !previous || previous.provider === "trtc" /* trtc */ || next.roomToken !== previous.roomToken)) {
|
|
6432
7650
|
await this.dependencies.streamController.updateCredentials(next, this.credentialController?.signal);
|
|
6433
7651
|
if (version !== this.refreshVersion || this.activeSession !== current) return;
|
|
6434
7652
|
}
|
|
@@ -6526,7 +7744,10 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
6526
7744
|
const confirmation = this.streamController.beginGeneration({
|
|
6527
7745
|
taskID,
|
|
6528
7746
|
videoFormat: options.videoFormat,
|
|
6529
|
-
context
|
|
7747
|
+
context,
|
|
7748
|
+
...options.targetSize && { targetSize: options.targetSize },
|
|
7749
|
+
mirror: options.mirror ?? false,
|
|
7750
|
+
...options.onStartSent && { onStartSent: options.onStartSent }
|
|
6530
7751
|
});
|
|
6531
7752
|
await this.awaitConfirmation(confirmation, options.signal);
|
|
6532
7753
|
options.ensureCurrent();
|
|
@@ -6552,7 +7773,8 @@ var XmaxRealtimeGenerationManager = class _XmaxRealtimeGenerationManager {
|
|
|
6552
7773
|
await this.streamController.updateGeneration({
|
|
6553
7774
|
taskID,
|
|
6554
7775
|
videoFormat: options.videoFormat,
|
|
6555
|
-
context
|
|
7776
|
+
context,
|
|
7777
|
+
...options.targetSize && { targetSize: options.targetSize }
|
|
6556
7778
|
});
|
|
6557
7779
|
if (this.activeTaskID !== taskID) throw RealtimeCoordinator.cancelledError();
|
|
6558
7780
|
this.currentContext = context;
|
|
@@ -6659,6 +7881,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6659
7881
|
__publicField(this, "coordinator");
|
|
6660
7882
|
__publicField(this, "errorHandler");
|
|
6661
7883
|
__publicField(this, "cameraController");
|
|
7884
|
+
__publicField(this, "networkVideoController", new NetworkVideoController());
|
|
7885
|
+
__publicField(this, "localVideoController", new LocalVideoController((error) => {
|
|
7886
|
+
void this.coordinator.terminateWithError(error, 0 /* connection */);
|
|
7887
|
+
}));
|
|
7888
|
+
__publicField(this, "rtcManager");
|
|
6662
7889
|
/**
|
|
6663
7890
|
* 启动计时与首帧通知
|
|
6664
7891
|
*/
|
|
@@ -6684,6 +7911,14 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6684
7911
|
__publicField(this, "connectionManager");
|
|
6685
7912
|
__publicField(this, "generationManager");
|
|
6686
7913
|
__publicField(this, "streamController");
|
|
7914
|
+
__publicField(this, "qualityController");
|
|
7915
|
+
__publicField(this, "qualityHistory");
|
|
7916
|
+
__publicField(this, "qualityStates", /* @__PURE__ */ new WeakMap());
|
|
7917
|
+
__publicField(this, "downlinkQualityController");
|
|
7918
|
+
/** 无服务端生效 ACK;level/size 表示最后成功发送的目标,而非实际接收规格。 */
|
|
7919
|
+
__publicField(this, "downlinkQualityState");
|
|
7920
|
+
__publicField(this, "adaptiveQualityPending", false);
|
|
7921
|
+
__publicField(this, "adaptiveQualityRevision", 0);
|
|
6687
7922
|
/**
|
|
6688
7923
|
* 音量配置
|
|
6689
7924
|
*/
|
|
@@ -6695,6 +7930,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6695
7930
|
this.supportsInterpolation = dependencies?.supportsFrameInterpolation ?? (async (size) => await frameInterpolationAdapter(size) !== null);
|
|
6696
7931
|
this.errorHandler = new RealtimeErrorHandler();
|
|
6697
7932
|
const rtcManager = dependencies?.rtcManager ?? createRtcManager(options.provider, dependencies?.environment ?? "china" /* china */);
|
|
7933
|
+
this.rtcManager = rtcManager;
|
|
6698
7934
|
this.cameraController = dependencies?.cameraController ?? new CameraController({
|
|
6699
7935
|
rtcManager,
|
|
6700
7936
|
mediaService: this.mediaService,
|
|
@@ -6721,7 +7957,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6721
7957
|
sessionService,
|
|
6722
7958
|
streamController: this.streamController,
|
|
6723
7959
|
timing: this.launchTimer,
|
|
6724
|
-
isMirrored: () => this.cameraController.currentTrack?.position === "front" /* front */,
|
|
6725
7960
|
remoteAudioVolume: () => this.storedRemoteAudioVolume,
|
|
6726
7961
|
onHeartbeatFailure: (sessionID, error) => {
|
|
6727
7962
|
void this.handleHeartbeatFailure(sessionID, error);
|
|
@@ -6737,6 +7972,31 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6737
7972
|
errorHandler: this.errorHandler,
|
|
6738
7973
|
cleanup: (scope, taskID) => this.performCleanup(scope, taskID)
|
|
6739
7974
|
});
|
|
7975
|
+
if (options.provider === "agora" /* agora */) {
|
|
7976
|
+
this.qualityHistory = new QualityHistory(options.model);
|
|
7977
|
+
this.qualityController = new QualityController(async (change, signal) => {
|
|
7978
|
+
if (signal.aborted || this.currentState.connectionState !== "Generating" /* generating */) {
|
|
7979
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "Adaptive video format update was cancelled");
|
|
7980
|
+
}
|
|
7981
|
+
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
7982
|
+
const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
|
|
7983
|
+
const state2 = track && this.qualityStates.get(track);
|
|
7984
|
+
if (!track || !state2) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Adaptive video baseline is unavailable");
|
|
7985
|
+
const format = qualityVideoFormat(state2.baseline, change.to);
|
|
7986
|
+
await this.streamController.updateUplinkVideoFormat(format);
|
|
7987
|
+
token.ensureCurrent();
|
|
7988
|
+
this.generationVideoFormat ?? (this.generationVideoFormat = state2.baseline);
|
|
7989
|
+
track.updateVideoFormat(format);
|
|
7990
|
+
state2.level = change.to;
|
|
7991
|
+
this.qualityHistory?.write("uplink", change.to);
|
|
7992
|
+
});
|
|
7993
|
+
});
|
|
7994
|
+
this.downlinkQualityController = new QualityController(async (change, signal) => {
|
|
7995
|
+
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
7996
|
+
await this.applyDownlinkQuality(change.to, token, signal);
|
|
7997
|
+
});
|
|
7998
|
+
});
|
|
7999
|
+
}
|
|
6740
8000
|
this.streamController.setLocalVideoStatisticsListener((statistics) => {
|
|
6741
8001
|
if (this.acceptsVideoStatistics) {
|
|
6742
8002
|
this.localVideoStatistics = statistics ? Object.freeze({ ...statistics }) : void 0;
|
|
@@ -6746,6 +8006,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6746
8006
|
this.streamController.setNetworkStatisticsListener((statistics) => {
|
|
6747
8007
|
if (this.acceptsVideoStatistics) {
|
|
6748
8008
|
this.networkStatistics = statistics ? Object.freeze({ ...statistics }) : void 0;
|
|
8009
|
+
this.handleAdaptiveQuality(statistics);
|
|
6749
8010
|
this.notifyNetworkStatistics();
|
|
6750
8011
|
}
|
|
6751
8012
|
});
|
|
@@ -6773,7 +8034,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6773
8034
|
*/
|
|
6774
8035
|
async setFrameInterpolationEnabled(enabled) {
|
|
6775
8036
|
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
6776
|
-
const format = this.generationVideoFormat ?? this.
|
|
8037
|
+
const format = this.downlinkQualityState?.size ?? this.generationVideoFormat ?? this.currentLocalTrack?.videoFormat;
|
|
6777
8038
|
const size = format && enabled ? this.mediaService.resolveFrameInterpolationSize(format) : void 0;
|
|
6778
8039
|
if (enabled && !await this.checkInterpolationSupport(size, token)) {
|
|
6779
8040
|
token.ensureCurrent();
|
|
@@ -6785,6 +8046,43 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6785
8046
|
this.applyRemoteInterpolation();
|
|
6786
8047
|
});
|
|
6787
8048
|
}
|
|
8049
|
+
/** 手动调试与自动调档使用同一尺寸信令路径,不修改生成或上行配置。 */
|
|
8050
|
+
async adjustDownlinkQuality(direction) {
|
|
8051
|
+
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
8052
|
+
const controller = this.downlinkQualityController;
|
|
8053
|
+
const state2 = this.downlinkQualityState;
|
|
8054
|
+
if (!controller || !state2 || this.currentState.connectionState !== "Generating" /* generating */ || direction !== "upgrade" && direction !== "downgrade") {
|
|
8055
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Downlink quality adjustment requires an active Agora generation and a valid direction");
|
|
8056
|
+
}
|
|
8057
|
+
const level = Math.max(1, Math.min(Object.keys(QUALITY_TIERS).length, state2.level + (direction === "upgrade" ? -1 : 1)));
|
|
8058
|
+
if (level === state2.level) return;
|
|
8059
|
+
this.adaptiveQualityRevision++;
|
|
8060
|
+
controller.resetSamples();
|
|
8061
|
+
await this.applyDownlinkQuality(level, token);
|
|
8062
|
+
controller.start(level);
|
|
8063
|
+
});
|
|
8064
|
+
}
|
|
8065
|
+
async applyDownlinkQuality(level, token, signal) {
|
|
8066
|
+
const state2 = this.downlinkQualityState;
|
|
8067
|
+
const ensureActive2 = () => {
|
|
8068
|
+
token.ensureCurrent();
|
|
8069
|
+
if (signal?.aborted || !state2 || state2 !== this.downlinkQualityState || this.currentState.connectionState !== "Generating" /* generating */ || this.currentState.taskID !== state2.taskID) {
|
|
8070
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "Target size update was cancelled");
|
|
8071
|
+
}
|
|
8072
|
+
};
|
|
8073
|
+
ensureActive2();
|
|
8074
|
+
if (!state2) return;
|
|
8075
|
+
const size = qualityVideoSize(state2.baseline, level);
|
|
8076
|
+
await this.streamController.updateDownlinkVideoResolution(state2.taskID, size, ensureActive2);
|
|
8077
|
+
ensureActive2();
|
|
8078
|
+
state2.level = level;
|
|
8079
|
+
state2.size = size;
|
|
8080
|
+
this.qualityHistory?.write("downlink", level);
|
|
8081
|
+
if (this.interpolationRequested) {
|
|
8082
|
+
this.interpolationSize = size;
|
|
8083
|
+
this.applyRemoteInterpolation();
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
6788
8086
|
/**
|
|
6789
8087
|
* 根据当前视频尺寸准备插帧,能力不可用时降级为原视频,并在应用前校验租约。
|
|
6790
8088
|
*/
|
|
@@ -6838,7 +8136,6 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6838
8136
|
const size = this.interpolationSize;
|
|
6839
8137
|
this.connectionManager.setFrameInterpolation(size ? {
|
|
6840
8138
|
size,
|
|
6841
|
-
targetFrameRate: this.options.frameInterpolation.targetFrameRate,
|
|
6842
8139
|
onFailure: (error) => {
|
|
6843
8140
|
if (revision === this.interpolationRevision) this.handleInterpolationFailure(error);
|
|
6844
8141
|
}
|
|
@@ -6945,6 +8242,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6945
8242
|
* 停止接收媒体统计并清空各类快照,仅对曾有数据的统计发出清空通知。
|
|
6946
8243
|
*/
|
|
6947
8244
|
clearVideoStatistics() {
|
|
8245
|
+
this.qualityController?.stop();
|
|
8246
|
+
this.downlinkQualityController?.stop();
|
|
8247
|
+
this.downlinkQualityState = void 0;
|
|
8248
|
+
this.adaptiveQualityRevision++;
|
|
6948
8249
|
this.acceptsVideoStatistics = false;
|
|
6949
8250
|
const hadNetworkStatistics = this.networkStatistics !== void 0;
|
|
6950
8251
|
this.networkStatistics = void 0;
|
|
@@ -6999,6 +8300,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
6999
8300
|
"media" /* media */,
|
|
7000
8301
|
void 0,
|
|
7001
8302
|
async (token) => {
|
|
8303
|
+
this.ensureNoLocalSource();
|
|
7002
8304
|
await this.coordinator.commit(
|
|
7003
8305
|
new RealtimeState({
|
|
7004
8306
|
connectionState: "Preparing" /* preparing */
|
|
@@ -7035,6 +8337,63 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7035
8337
|
}
|
|
7036
8338
|
);
|
|
7037
8339
|
}
|
|
8340
|
+
/**
|
|
8341
|
+
* 创建网络视频源并初始化接收端 RTC;预览是否加载成功不影响服务端读取视频。
|
|
8342
|
+
*/
|
|
8343
|
+
async createNetworkVideoStream(options) {
|
|
8344
|
+
return this.coordinator.run("media" /* media */, void 0, async (token) => {
|
|
8345
|
+
this.ensureNoLocalSource();
|
|
8346
|
+
const stream = this.networkVideoController.create(options, this.mediaService);
|
|
8347
|
+
token.setFailureScope(1 /* all */);
|
|
8348
|
+
this.launchTimer.start();
|
|
8349
|
+
this.clearVideoStatistics();
|
|
8350
|
+
await this.rtcManager.initialize();
|
|
8351
|
+
token.ensureCurrent();
|
|
8352
|
+
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
8353
|
+
await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
|
|
8354
|
+
return stream;
|
|
8355
|
+
});
|
|
8356
|
+
}
|
|
8357
|
+
/**
|
|
8358
|
+
* 创建文件音视频源并准备外部轨道;准备失败时统一释放本次媒体和 RTC 资源。
|
|
8359
|
+
*/
|
|
8360
|
+
async createLocalVideoStream(options) {
|
|
8361
|
+
return this.coordinator.run("media" /* media */, void 0, async (token) => {
|
|
8362
|
+
this.ensureNoLocalSource();
|
|
8363
|
+
if (!this.rtcManager.setExternalMediaTracks) {
|
|
8364
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "The RTC provider does not support local video streams");
|
|
8365
|
+
}
|
|
8366
|
+
token.setFailureScope(1 /* all */);
|
|
8367
|
+
this.launchTimer.start();
|
|
8368
|
+
this.clearVideoStatistics();
|
|
8369
|
+
const stream = await this.localVideoController.create(options, this.mediaService, token.signal);
|
|
8370
|
+
token.ensureCurrent();
|
|
8371
|
+
await this.rtcManager.initialize();
|
|
8372
|
+
token.ensureCurrent();
|
|
8373
|
+
await this.rtcManager.setExternalMediaTracks({
|
|
8374
|
+
videoTrack: stream.videoTrack.mediaStreamTrack,
|
|
8375
|
+
audioTrack: this.localVideoController.audioTrack
|
|
8376
|
+
});
|
|
8377
|
+
token.ensureCurrent();
|
|
8378
|
+
this.generationVideoFormat = stream.videoTrack?.videoFormat;
|
|
8379
|
+
await this.coordinator.commit(new RealtimeState({ connectionState: "Ready" /* ready */ }), token);
|
|
8380
|
+
return stream;
|
|
8381
|
+
});
|
|
8382
|
+
}
|
|
8383
|
+
/**
|
|
8384
|
+
* 当前 Manager 拥有的媒体源轨道。
|
|
8385
|
+
*/
|
|
8386
|
+
get currentLocalTrack() {
|
|
8387
|
+
return this.localVideoController.currentTrack ?? this.networkVideoController.currentTrack ?? this.cameraController.currentTrack;
|
|
8388
|
+
}
|
|
8389
|
+
/**
|
|
8390
|
+
* 创建前拒绝覆盖活动源,避免旧预览或连接资源被静默遗留。
|
|
8391
|
+
*/
|
|
8392
|
+
ensureNoLocalSource() {
|
|
8393
|
+
if (this.currentLocalTrack || this.localVideoController.isActive) {
|
|
8394
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Close the current local stream before creating another one");
|
|
8395
|
+
}
|
|
8396
|
+
}
|
|
7038
8397
|
/**
|
|
7039
8398
|
* 移除媒体权限弹窗观察;授权决定已记录或不再需要观察时调用。
|
|
7040
8399
|
*/
|
|
@@ -7046,26 +8405,105 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7046
8405
|
* 应用完整的上行视频格式,成功后更新本地轨道元数据,不改变生成格式或生命周期。
|
|
7047
8406
|
* 复用配置操作准入,避免与建连、切换摄像头或其他配置更新并发。
|
|
7048
8407
|
*
|
|
7049
|
-
* @throws
|
|
8408
|
+
* @throws 无本地相机或文件视频流、参数无效、操作冲突或底层编码更新失败时抛错。
|
|
7050
8409
|
*/
|
|
7051
|
-
async
|
|
8410
|
+
async updateUplinkVideoFormat(videoFormat) {
|
|
7052
8411
|
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
7053
|
-
const track = this.cameraController.currentTrack;
|
|
8412
|
+
const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
|
|
7054
8413
|
const previousFormat = track?.videoFormat;
|
|
7055
8414
|
if (!track || !previousFormat) {
|
|
7056
|
-
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera stream is not started");
|
|
8415
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local camera or file video stream is not started");
|
|
7057
8416
|
}
|
|
7058
8417
|
videoFormat.validate();
|
|
7059
|
-
|
|
8418
|
+
this.qualityController?.stop();
|
|
8419
|
+
await this.streamController.updateUplinkVideoFormat(videoFormat);
|
|
7060
8420
|
token.ensureCurrent();
|
|
7061
8421
|
this.generationVideoFormat ?? (this.generationVideoFormat = previousFormat);
|
|
7062
8422
|
track.updateVideoFormat(videoFormat);
|
|
8423
|
+
this.qualityStates.set(track, { baseline: videoFormat, level: 1 });
|
|
8424
|
+
this.qualityHistory?.write("uplink", 1);
|
|
8425
|
+
});
|
|
8426
|
+
}
|
|
8427
|
+
/** 已连接时仅替换帧率;与其他配置及生命周期操作共享同一操作租约。 */
|
|
8428
|
+
async updateUplinkFrameRate(fps) {
|
|
8429
|
+
await this.coordinator.run("configuration" /* configuration */, void 0, async (token) => {
|
|
8430
|
+
this.qualityController?.stop();
|
|
8431
|
+
await this.applyUplinkFrameRate(fps, token);
|
|
8432
|
+
const track = this.currentLocalTrack;
|
|
8433
|
+
if (track?.videoFormat) {
|
|
8434
|
+
this.qualityStates.set(track, { baseline: track.videoFormat, level: 1 });
|
|
8435
|
+
this.qualityHistory?.write("uplink", 1);
|
|
8436
|
+
}
|
|
8437
|
+
});
|
|
8438
|
+
}
|
|
8439
|
+
/** 手动仅调帧接口,成功后的当前格式将成为新的自适应 L1 基准。 */
|
|
8440
|
+
async applyUplinkFrameRate(fps, token) {
|
|
8441
|
+
const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
|
|
8442
|
+
const previous = track?.videoFormat;
|
|
8443
|
+
const state2 = this.currentState.connectionState;
|
|
8444
|
+
if (!track || !previous || state2 !== "Connected" /* connected */ && state2 !== "Generating" /* generating */) {
|
|
8445
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Frame rate updates require a connected local camera or file stream");
|
|
8446
|
+
}
|
|
8447
|
+
if (!Number.isFinite(fps) || fps <= 0) {
|
|
8448
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Video frame rate must be greater than zero");
|
|
8449
|
+
}
|
|
8450
|
+
const format = await this.streamController.updateUplinkFrameRate(fps);
|
|
8451
|
+
token.ensureCurrent();
|
|
8452
|
+
this.generationVideoFormat ?? (this.generationVideoFormat = previous);
|
|
8453
|
+
track.updateVideoFormat(format);
|
|
8454
|
+
}
|
|
8455
|
+
/** 上下行独立判断;串行应用同一原始事件,避免同时降档争抢配置租约。 */
|
|
8456
|
+
handleAdaptiveQuality(statistics) {
|
|
8457
|
+
const revision = ++this.adaptiveQualityRevision;
|
|
8458
|
+
const controller = this.qualityController;
|
|
8459
|
+
const downlink = this.downlinkQualityController;
|
|
8460
|
+
if (!controller || !downlink) return;
|
|
8461
|
+
const taskID = this.currentState.taskID;
|
|
8462
|
+
if (this.currentState.connectionState !== "Generating" /* generating */ || !taskID) {
|
|
8463
|
+
controller.stop();
|
|
8464
|
+
downlink.stop();
|
|
8465
|
+
return;
|
|
8466
|
+
}
|
|
8467
|
+
if (!statistics || this.coordinator.isBusy || this.adaptiveQualityPending) {
|
|
8468
|
+
controller.resetSamples();
|
|
8469
|
+
downlink.resetSamples();
|
|
8470
|
+
return;
|
|
8471
|
+
}
|
|
8472
|
+
const track = this.localVideoController.currentTrack ?? this.cameraController.currentTrack;
|
|
8473
|
+
if (!track) {
|
|
8474
|
+
controller.stop();
|
|
8475
|
+
} else if (!controller.snapshot.active) {
|
|
8476
|
+
const format = track.videoFormat;
|
|
8477
|
+
if (format) {
|
|
8478
|
+
const state3 = this.qualityStates.get(track) ?? { baseline: format, level: 1 };
|
|
8479
|
+
this.qualityStates.set(track, state3);
|
|
8480
|
+
controller.start(state3.level);
|
|
8481
|
+
}
|
|
8482
|
+
}
|
|
8483
|
+
const state2 = this.downlinkQualityState;
|
|
8484
|
+
if (state2 && !downlink.snapshot.active) downlink.start(state2.level);
|
|
8485
|
+
const sampleAt = performance.now();
|
|
8486
|
+
this.adaptiveQualityPending = true;
|
|
8487
|
+
void (async () => {
|
|
8488
|
+
await controller.observe(statistics.uplinkQuality, sampleAt).catch(() => {
|
|
8489
|
+
});
|
|
8490
|
+
if (revision !== this.adaptiveQualityRevision || this.currentState.taskID !== taskID || state2 !== this.downlinkQualityState || this.currentState.connectionState !== "Generating" /* generating */ || this.coordinator.isBusy) {
|
|
8491
|
+
downlink.resetSamples();
|
|
8492
|
+
return;
|
|
8493
|
+
}
|
|
8494
|
+
await downlink.observe(statistics.downlinkQuality, sampleAt).catch(() => {
|
|
8495
|
+
});
|
|
8496
|
+
})().finally(() => {
|
|
8497
|
+
this.adaptiveQualityPending = false;
|
|
7063
8498
|
});
|
|
7064
8499
|
}
|
|
7065
8500
|
/**
|
|
7066
8501
|
* 停止本地相机流并释放本地预览与 RTC 资源。
|
|
7067
8502
|
*/
|
|
7068
8503
|
async stopLocalCameraStream() {
|
|
8504
|
+
if (this.networkVideoController.currentTrack || this.localVideoController.isActive) {
|
|
8505
|
+
return;
|
|
8506
|
+
}
|
|
7069
8507
|
await this.coordinator.run(
|
|
7070
8508
|
"media" /* media */,
|
|
7071
8509
|
void 0,
|
|
@@ -7096,9 +8534,9 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7096
8534
|
"cameraSwitch" /* cameraSwitch */,
|
|
7097
8535
|
void 0,
|
|
7098
8536
|
async (token) => {
|
|
8537
|
+
this.qualityController?.stop();
|
|
7099
8538
|
const stream = await this.cameraController.switchCamera();
|
|
7100
8539
|
token.ensureCurrent();
|
|
7101
|
-
this.connectionManager.updateRemoteMirror();
|
|
7102
8540
|
return stream;
|
|
7103
8541
|
}
|
|
7104
8542
|
);
|
|
@@ -7106,11 +8544,11 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7106
8544
|
/**
|
|
7107
8545
|
* 使用当前 Manager 创建的本地流建立实时连接。
|
|
7108
8546
|
*
|
|
7109
|
-
* 创建实时会话、加入 RTC
|
|
8547
|
+
* 创建实时会话、加入 RTC 房间并启动会话心跳;摄像头源发布本地流,网络视频源仅接收。
|
|
7110
8548
|
* 启用帧检测时,发布前并行等待相机预热(固定 200ms),避免把黑帧推给 RTC。
|
|
7111
8549
|
* 返回的远端媒体流在生成开始后承载远端生成画面。
|
|
7112
8550
|
*
|
|
7113
|
-
* @param localStream
|
|
8551
|
+
* @param localStream 由当前 Manager 创建的摄像头、本地文件或网络视频源。
|
|
7114
8552
|
* @returns 远端生成结果占位的媒体流。
|
|
7115
8553
|
* @throws 本地流不属于当前 Manager、已有活动连接、会话创建或进房
|
|
7116
8554
|
* 发布失败时抛出错误;失败时自动释放连接资源并恢复本地预览。
|
|
@@ -7137,7 +8575,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7137
8575
|
* 尚未连接时先建立实时连接;已在生成时仅更新生成条件,不重启生成。
|
|
7138
8576
|
* 首次生成必须提供条件上下文,之后缺省时复用最近一次缓存的上下文。
|
|
7139
8577
|
*
|
|
7140
|
-
* @param options.localStream
|
|
8578
|
+
* @param options.localStream 由当前 Manager 创建的摄像头、本地文件或网络视频源。
|
|
7141
8579
|
* @param options.context 本次生成使用的条件上下文;缺省时复用缓存。
|
|
7142
8580
|
* @returns 承载远端生成画面的媒体流。
|
|
7143
8581
|
* @throws 本地流不属于当前 Manager、缺少可用的条件上下文、信令发送
|
|
@@ -7149,7 +8587,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7149
8587
|
void 0,
|
|
7150
8588
|
async (token) => {
|
|
7151
8589
|
const localTrack = options.localStream.videoTrack;
|
|
7152
|
-
if (!localTrack || localTrack !== this.
|
|
8590
|
+
if (!localTrack || localTrack !== this.currentLocalTrack) {
|
|
7153
8591
|
throw new XmaxError(
|
|
7154
8592
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
7155
8593
|
"The local stream must be created and started by this realtime manager"
|
|
@@ -7166,12 +8604,18 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7166
8604
|
if (currentState.connectionState === "Generating" /* generating */) {
|
|
7167
8605
|
await this.generationManager.update(currentState.taskID, {
|
|
7168
8606
|
videoFormat,
|
|
8607
|
+
targetSize: this.downlinkQualityController ? this.downlinkQualityState?.size : void 0,
|
|
7169
8608
|
context: options.context
|
|
7170
8609
|
});
|
|
7171
8610
|
token.ensureCurrent();
|
|
7172
8611
|
return this.connectionManager.makeRemoteStream();
|
|
7173
8612
|
}
|
|
7174
|
-
const
|
|
8613
|
+
const supplied = this.generationManager.validateContext(options.context);
|
|
8614
|
+
const referenceVideo = this.networkVideoController.reference;
|
|
8615
|
+
if (!referenceVideo && supplied.referenceVideo) {
|
|
8616
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Create a network video stream before using a reference video");
|
|
8617
|
+
}
|
|
8618
|
+
const context = referenceVideo ? new RealtimeContext({ ...supplied, referenceVideo }) : supplied;
|
|
7175
8619
|
if (currentState.connectionState !== "Connected" /* connected */) {
|
|
7176
8620
|
await this.performConnect(options.localStream, token);
|
|
7177
8621
|
}
|
|
@@ -7183,6 +8627,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7183
8627
|
* 断开实时连接并保留当前本地媒体预览。
|
|
7184
8628
|
*/
|
|
7185
8629
|
async disconnect() {
|
|
8630
|
+
this.localVideoController.pause();
|
|
7186
8631
|
this.launchTimer.cancel();
|
|
7187
8632
|
this.clearVideoStatistics();
|
|
7188
8633
|
await this.coordinator.disconnect();
|
|
@@ -7192,10 +8637,20 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7192
8637
|
* 关闭期间重复调用会等待同一个释放任务;关闭完成后仍可重新创建本地流。
|
|
7193
8638
|
*/
|
|
7194
8639
|
async close() {
|
|
8640
|
+
this.localVideoController.pause();
|
|
7195
8641
|
this.launchTimer.cancel();
|
|
7196
8642
|
this.clearVideoStatistics();
|
|
7197
8643
|
await this.coordinator.terminate(1 /* all */);
|
|
7198
8644
|
}
|
|
8645
|
+
/** 仅为尚无档位记录的本地上行选择初始档位;不应用配置或覆盖重连状态。 */
|
|
8646
|
+
resolveInitialUplinkQuality(track) {
|
|
8647
|
+
if (!this.qualityController) return void 0;
|
|
8648
|
+
if (this.networkVideoController.currentTrack) return void 0;
|
|
8649
|
+
if (this.qualityStates.has(track)) return void 0;
|
|
8650
|
+
const baseline = track.videoFormat;
|
|
8651
|
+
if (!baseline) return void 0;
|
|
8652
|
+
return { baseline, level: this.qualityHistory.read("uplink") };
|
|
8653
|
+
}
|
|
7199
8654
|
/**
|
|
7200
8655
|
* 建立实时连接:创建会话、进房发布、启动心跳并准备远端渲染轨道。
|
|
7201
8656
|
*
|
|
@@ -7205,7 +8660,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7205
8660
|
*/
|
|
7206
8661
|
async performConnect(localStream, token) {
|
|
7207
8662
|
const localTrack = localStream.videoTrack;
|
|
7208
|
-
if (!localTrack || localTrack !== this.
|
|
8663
|
+
if (!localTrack || localTrack !== this.currentLocalTrack) {
|
|
7209
8664
|
throw new XmaxError(
|
|
7210
8665
|
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
7211
8666
|
"The local stream must be created and started by this realtime manager"
|
|
@@ -7223,7 +8678,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7223
8678
|
if (token.signal.aborted) abortWarmup();
|
|
7224
8679
|
try {
|
|
7225
8680
|
let beforePublish;
|
|
7226
|
-
if (this.cameraController.isFrameValidationEnabled) {
|
|
8681
|
+
if (!this.localVideoController.isActive && !this.networkVideoController.currentTrack && this.cameraController.isFrameValidationEnabled) {
|
|
7227
8682
|
const cameraReady = this.cameraController.waitForValidCameraFrame(warmupController.signal).then(() => {
|
|
7228
8683
|
token.ensureCurrent();
|
|
7229
8684
|
});
|
|
@@ -7231,11 +8686,22 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7231
8686
|
});
|
|
7232
8687
|
beforePublish = () => cameraReady;
|
|
7233
8688
|
}
|
|
8689
|
+
const initialQuality = this.resolveInitialUplinkQuality(localTrack);
|
|
8690
|
+
const initialFormat = initialQuality ? qualityVideoFormat(initialQuality.baseline, initialQuality.level) : void 0;
|
|
7234
8691
|
const remote = await this.connectionManager.connect({
|
|
7235
8692
|
localTrack,
|
|
7236
|
-
remoteVideoFormat: this.generationVideoFormat,
|
|
8693
|
+
remoteVideoFormat: this.generationVideoFormat ?? localTrack.videoFormat,
|
|
8694
|
+
localVideoFormat: initialFormat,
|
|
8695
|
+
onVideoEncoderConfigured: () => {
|
|
8696
|
+
if (!initialQuality || !initialFormat) return;
|
|
8697
|
+
this.generationVideoFormat ?? (this.generationVideoFormat = initialQuality.baseline);
|
|
8698
|
+
localTrack.updateVideoFormat(initialFormat);
|
|
8699
|
+
this.qualityStates.set(localTrack, initialQuality);
|
|
8700
|
+
this.qualityHistory?.write("uplink", initialQuality.level);
|
|
8701
|
+
},
|
|
7237
8702
|
model: this.options.model,
|
|
7238
|
-
includeLocalAudio: this.cameraController.useMicrophone,
|
|
8703
|
+
includeLocalAudio: this.localVideoController.isActive ? this.localVideoController.audioTrack !== void 0 : !this.networkVideoController.currentTrack && this.cameraController.useMicrophone,
|
|
8704
|
+
publishLocalMedia: !this.networkVideoController.currentTrack,
|
|
7239
8705
|
ensureCurrent: () => token.ensureCurrent(),
|
|
7240
8706
|
onPublished: () => {
|
|
7241
8707
|
this.acceptsVideoStatistics = true;
|
|
@@ -7260,16 +8726,25 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7260
8726
|
*/
|
|
7261
8727
|
async performStartGeneration(token, videoFormat, context) {
|
|
7262
8728
|
token.setFailureScope(0 /* connection */);
|
|
7263
|
-
|
|
8729
|
+
const initialLevel = this.qualityHistory?.read("downlink") ?? 1;
|
|
8730
|
+
const targetSize = this.downlinkQualityController ? qualityVideoSize(videoFormat, initialLevel) : void 0;
|
|
8731
|
+
await this.prepareFrameInterpolation(targetSize ?? videoFormat, token);
|
|
7264
8732
|
const completeFirstFrame = this.launchTimer.startFirstFrame();
|
|
7265
8733
|
this.remoteFrameDisplayHandler = () => {
|
|
7266
8734
|
if (!token.signal.aborted) completeFirstFrame();
|
|
7267
8735
|
};
|
|
7268
8736
|
const taskID = await this.generationManager.start({
|
|
7269
8737
|
videoFormat,
|
|
8738
|
+
targetSize,
|
|
7270
8739
|
context,
|
|
8740
|
+
mirror: this.currentLocalTrack === this.cameraController.currentTrack && this.cameraController.currentTrack?.position === "front" /* front */,
|
|
7271
8741
|
signal: token.signal,
|
|
7272
8742
|
ensureCurrent: () => token.ensureCurrent(),
|
|
8743
|
+
onStartSent: this.localVideoController.isActive ? async () => {
|
|
8744
|
+
token.ensureCurrent();
|
|
8745
|
+
await this.localVideoController.start(token.signal);
|
|
8746
|
+
token.ensureCurrent();
|
|
8747
|
+
} : void 0,
|
|
7273
8748
|
waitUntilRemoteReady: () => this.connectionManager.waitUntilRemoteTrackReady(
|
|
7274
8749
|
REMOTE_FIRST_FRAME_TIMEOUT_MS,
|
|
7275
8750
|
token.signal
|
|
@@ -7279,10 +8754,26 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7279
8754
|
if (!sessionID) {
|
|
7280
8755
|
throw new XmaxError("SESSION_ERROR" /* sessionError */, "Realtime session is unavailable");
|
|
7281
8756
|
}
|
|
8757
|
+
this.downlinkQualityController?.stop();
|
|
8758
|
+
this.downlinkQualityState = {
|
|
8759
|
+
taskID,
|
|
8760
|
+
baseline: videoFormat,
|
|
8761
|
+
level: initialLevel,
|
|
8762
|
+
size: targetSize ?? { width: videoFormat.width, height: videoFormat.height }
|
|
8763
|
+
};
|
|
7282
8764
|
await this.coordinator.commit(
|
|
7283
8765
|
new RealtimeState({ connectionState: "Generating" /* generating */, sessionID, taskID }),
|
|
7284
8766
|
token
|
|
7285
8767
|
);
|
|
8768
|
+
this.qualityHistory?.write("downlink", initialLevel);
|
|
8769
|
+
const onFinish = this.networkVideoController.onFinish;
|
|
8770
|
+
if (this.networkVideoController.currentTrack) {
|
|
8771
|
+
this.streamController.activateNetworkVideoCompletion(onFinish && (() => {
|
|
8772
|
+
if (this.currentState.taskID === taskID && this.currentState.connectionState === "Generating" /* generating */) {
|
|
8773
|
+
return onFinish();
|
|
8774
|
+
}
|
|
8775
|
+
}));
|
|
8776
|
+
}
|
|
7286
8777
|
return this.connectionManager.makeRemoteStream();
|
|
7287
8778
|
}
|
|
7288
8779
|
/**
|
|
@@ -7299,6 +8790,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7299
8790
|
* 统一清理顺序:停止心跳与生成,再断开连接;all 额外释放本地媒体。
|
|
7300
8791
|
*/
|
|
7301
8792
|
async performCleanup(scope, taskID) {
|
|
8793
|
+
this.localVideoController.pause();
|
|
7302
8794
|
this.launchTimer.cancel();
|
|
7303
8795
|
this.remoteFrameDisplayHandler = void 0;
|
|
7304
8796
|
this.clearVideoStatistics();
|
|
@@ -7309,20 +8801,30 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7309
8801
|
await this.generationManager.reset(taskID);
|
|
7310
8802
|
const sessionID = await this.connectionManager.disconnect();
|
|
7311
8803
|
if (scope === 1 /* all */) {
|
|
8804
|
+
const hadFileSource = this.networkVideoController.currentTrack !== void 0 || this.localVideoController.isActive;
|
|
8805
|
+
this.networkVideoController.stop();
|
|
7312
8806
|
this.generationVideoFormat = void 0;
|
|
7313
8807
|
this.disposePermissionWatch();
|
|
7314
8808
|
try {
|
|
7315
|
-
|
|
8809
|
+
if (hadFileSource) {
|
|
8810
|
+
await this.rtcManager.destroy();
|
|
8811
|
+
} else {
|
|
8812
|
+
await this.cameraController.stopLocalCameraStream();
|
|
8813
|
+
}
|
|
7316
8814
|
} catch (error) {
|
|
7317
8815
|
XmaxLogger.realtime.error(
|
|
7318
|
-
() => `\
|
|
8816
|
+
() => `\u91CA\u653E\u672C\u5730\u5A92\u4F53\u5931\u8D25 (Failed to Release Local Media)
|
|
7319
8817
|
\u2514\u2500 ${XmaxLogger.localized("\u539F\u56E0\uFF1A", "Reason: ")}${XmaxError.from(error).message}`
|
|
7320
8818
|
);
|
|
8819
|
+
} finally {
|
|
8820
|
+
if (this.localVideoController.isActive) {
|
|
8821
|
+
await this.localVideoController.stop();
|
|
8822
|
+
}
|
|
7321
8823
|
}
|
|
7322
8824
|
}
|
|
7323
8825
|
return {
|
|
7324
8826
|
sessionID,
|
|
7325
|
-
hasLocalMedia: this.
|
|
8827
|
+
hasLocalMedia: this.currentLocalTrack !== void 0
|
|
7326
8828
|
};
|
|
7327
8829
|
}
|
|
7328
8830
|
/**
|
|
@@ -7338,51 +8840,450 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
7338
8840
|
}
|
|
7339
8841
|
};
|
|
7340
8842
|
|
|
7341
|
-
// src/Core/XmaxConfiguration.ts
|
|
7342
|
-
var XmaxConfiguration = class {
|
|
8843
|
+
// src/Core/XmaxConfiguration.ts
|
|
8844
|
+
var XmaxConfiguration = class {
|
|
8845
|
+
/**
|
|
8846
|
+
* 创建 SDK 全局配置。
|
|
8847
|
+
*
|
|
8848
|
+
* @param init.apiKey 调用 Xmax 服务使用的 API Key。
|
|
8849
|
+
* @param init.environment SDK 连接的服务环境;默认为国内环境。
|
|
8850
|
+
* @param init.loggerOptions SDK 输出的日志类型;默认为空。
|
|
8851
|
+
*/
|
|
8852
|
+
constructor(init) {
|
|
8853
|
+
/**
|
|
8854
|
+
* 服务身份与环境
|
|
8855
|
+
*/
|
|
8856
|
+
/**
|
|
8857
|
+
* 调用 Xmax 服务使用的 API Key。
|
|
8858
|
+
*/
|
|
8859
|
+
__publicField(this, "apiKey");
|
|
8860
|
+
/**
|
|
8861
|
+
* SDK 连接的服务环境,同时决定日志细项语言:国内为中文,海外为英文。
|
|
8862
|
+
*/
|
|
8863
|
+
__publicField(this, "environment");
|
|
8864
|
+
/**
|
|
8865
|
+
* 日志配置
|
|
8866
|
+
*/
|
|
8867
|
+
/**
|
|
8868
|
+
* SDK 输出的日志类型;默认为不输出日志。
|
|
8869
|
+
*/
|
|
8870
|
+
__publicField(this, "loggerOptions");
|
|
8871
|
+
this.apiKey = init.apiKey.trim();
|
|
8872
|
+
this.environment = init.environment ?? "china" /* china */;
|
|
8873
|
+
this.loggerOptions = init.loggerOptions ?? 0 /* none */;
|
|
8874
|
+
}
|
|
8875
|
+
/**
|
|
8876
|
+
* 校验全局配置。
|
|
8877
|
+
* @throws API Key 为空时抛出 `XmaxError`。
|
|
8878
|
+
*/
|
|
8879
|
+
validate() {
|
|
8880
|
+
if (this.apiKey.length === 0) {
|
|
8881
|
+
throw new XmaxError(
|
|
8882
|
+
"INVALID_API_KEY" /* invalidAPIKey */,
|
|
8883
|
+
"API key cannot be empty"
|
|
8884
|
+
);
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
};
|
|
8888
|
+
|
|
8889
|
+
// src/Service/NonRealtime/NonRealtimeTask.ts
|
|
8890
|
+
var NonRealtimeQuality = /* @__PURE__ */ ((NonRealtimeQuality2) => {
|
|
8891
|
+
NonRealtimeQuality2["sd"] = "sd";
|
|
8892
|
+
NonRealtimeQuality2["hd"] = "hd";
|
|
8893
|
+
return NonRealtimeQuality2;
|
|
8894
|
+
})(NonRealtimeQuality || {});
|
|
8895
|
+
var NonRealtimeTaskStatus = /* @__PURE__ */ ((NonRealtimeTaskStatus2) => {
|
|
8896
|
+
NonRealtimeTaskStatus2["submitted"] = "submitted";
|
|
8897
|
+
NonRealtimeTaskStatus2["processing"] = "processing";
|
|
8898
|
+
NonRealtimeTaskStatus2["completed"] = "completed";
|
|
8899
|
+
NonRealtimeTaskStatus2["error"] = "error";
|
|
8900
|
+
return NonRealtimeTaskStatus2;
|
|
8901
|
+
})(NonRealtimeTaskStatus || {});
|
|
8902
|
+
|
|
8903
|
+
// src/Service/NonRealtime/NonRealtimeTaskService.ts
|
|
8904
|
+
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]);
|
|
8905
|
+
var NonRealtimeTaskService = class {
|
|
8906
|
+
/**
|
|
8907
|
+
* 注入非实时任务使用的 API 服务。
|
|
8908
|
+
*/
|
|
8909
|
+
constructor(api) {
|
|
8910
|
+
/**
|
|
8911
|
+
* 网络依赖
|
|
8912
|
+
*/
|
|
8913
|
+
__publicField(this, "api");
|
|
8914
|
+
this.api = api;
|
|
8915
|
+
}
|
|
8916
|
+
/**
|
|
8917
|
+
* 提交一次任务;请求结果不确定时也不自动重试,以免重复扣费。
|
|
8918
|
+
*/
|
|
8919
|
+
async submitTask(options) {
|
|
8920
|
+
const model = options.model?.trim();
|
|
8921
|
+
if (!model) {
|
|
8922
|
+
invalid("Model must not be empty");
|
|
8923
|
+
}
|
|
8924
|
+
const prompt = options.prompt.trim();
|
|
8925
|
+
if (!prompt || [...prompt].length > 1024) {
|
|
8926
|
+
invalid("Prompt must contain 1\u20131024 characters");
|
|
8927
|
+
}
|
|
8928
|
+
validateURL(options.videoPath);
|
|
8929
|
+
if (options.referencePath !== void 0) {
|
|
8930
|
+
validateURL(options.referencePath);
|
|
8931
|
+
}
|
|
8932
|
+
if (!Object.values(NonRealtimeQuality).includes(options.quality)) {
|
|
8933
|
+
invalid("Quality must be sd or hd");
|
|
8934
|
+
}
|
|
8935
|
+
if (options.fps !== void 0 && !SUPPORTED_FPS.has(options.fps)) {
|
|
8936
|
+
invalid("Unsupported processing frame rate");
|
|
8937
|
+
}
|
|
8938
|
+
const payload = await this.api.post("/offline-task", {
|
|
8939
|
+
model,
|
|
8940
|
+
prompt,
|
|
8941
|
+
refVideoPath: options.videoPath.trim(),
|
|
8942
|
+
...options.referencePath === void 0 ? {} : { refImagePath: options.referencePath.trim() },
|
|
8943
|
+
quality: options.quality,
|
|
8944
|
+
...options.fps === void 0 ? {} : { fps: options.fps }
|
|
8945
|
+
});
|
|
8946
|
+
return parseTask(payload);
|
|
8947
|
+
}
|
|
8948
|
+
/**
|
|
8949
|
+
* 查询任务,校验返回 ID;signal 只取消本次 HTTP 查询,不取消服务端任务。
|
|
8950
|
+
*/
|
|
8951
|
+
async getTask(taskUid, signal) {
|
|
8952
|
+
const uid = validateTaskUid(taskUid);
|
|
8953
|
+
const task = parseTask(await this.api.get(`/offline-task/${encodeURIComponent(uid)}`, { signal }));
|
|
8954
|
+
if (task.uid !== uid) {
|
|
8955
|
+
malformed("Task UID does not match the request");
|
|
8956
|
+
}
|
|
8957
|
+
return task;
|
|
8958
|
+
}
|
|
8959
|
+
/**
|
|
8960
|
+
* 查询最多 100 个 ID,按首次出现顺序去重;任一任务不可用时整体失败。
|
|
8961
|
+
*/
|
|
8962
|
+
async getTasks(taskUids) {
|
|
8963
|
+
if (taskUids.length < 1 || taskUids.length > 100) {
|
|
8964
|
+
invalid("Provide 1\u2013100 task UIDs");
|
|
8965
|
+
}
|
|
8966
|
+
const uids = [...new Set(taskUids.map(validateTaskUid))];
|
|
8967
|
+
const payload = record(await this.api.post("/offline-task/batch-query", { taskUids: uids }));
|
|
8968
|
+
const tasks = parseList(payload.list);
|
|
8969
|
+
if (tasks.length !== uids.length || tasks.some((task, index) => task.uid !== uids[index])) {
|
|
8970
|
+
malformed("Batch response does not match the requested task UIDs");
|
|
8971
|
+
}
|
|
8972
|
+
return tasks;
|
|
8973
|
+
}
|
|
8974
|
+
/**
|
|
8975
|
+
* 分页查询任务,不隐式加载其他页。
|
|
8976
|
+
*/
|
|
8977
|
+
async listTasks(options = {}) {
|
|
8978
|
+
const pageNumber = options.pageNumber ?? 1;
|
|
8979
|
+
const pageSize = options.pageSize ?? 10;
|
|
8980
|
+
if (!Number.isSafeInteger(pageNumber) || pageNumber < 1) {
|
|
8981
|
+
invalid("Page number must be a positive integer");
|
|
8982
|
+
}
|
|
8983
|
+
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) {
|
|
8984
|
+
invalid("Page size must be between 1 and 100");
|
|
8985
|
+
}
|
|
8986
|
+
if (options.status !== void 0 && !Object.values(NonRealtimeTaskStatus).includes(options.status)) {
|
|
8987
|
+
invalid("Invalid task status");
|
|
8988
|
+
}
|
|
8989
|
+
const query = new URLSearchParams({ pageNumber: String(pageNumber), pageSize: String(pageSize) });
|
|
8990
|
+
if (options.status !== void 0) {
|
|
8991
|
+
query.set("status", options.status);
|
|
8992
|
+
}
|
|
8993
|
+
const payload = record(await this.api.get(`/offline-task/page?${query}`));
|
|
8994
|
+
return Object.freeze({
|
|
8995
|
+
pageNumber: pageInteger(payload, "pageNumber", 1),
|
|
8996
|
+
pageSize: pageInteger(payload, "pageSize", 1),
|
|
8997
|
+
total: pageInteger(payload, "total", 0),
|
|
8998
|
+
list: parseList(payload.list)
|
|
8999
|
+
});
|
|
9000
|
+
}
|
|
9001
|
+
};
|
|
9002
|
+
function validateTaskUid(value) {
|
|
9003
|
+
const uid = value.trim();
|
|
9004
|
+
if (!uid || uid === "." || uid === "..") {
|
|
9005
|
+
invalid("Task UID must not be empty or a relative path segment");
|
|
9006
|
+
}
|
|
9007
|
+
return uid;
|
|
9008
|
+
}
|
|
9009
|
+
function validateURL(value) {
|
|
9010
|
+
let url;
|
|
9011
|
+
try {
|
|
9012
|
+
url = new URL(value.trim());
|
|
9013
|
+
} catch {
|
|
9014
|
+
return invalid("Resource must be an uploaded HTTP(S) URL");
|
|
9015
|
+
}
|
|
9016
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password) {
|
|
9017
|
+
invalid("Resource must be an uploaded HTTP(S) URL");
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
function invalid(message) {
|
|
9021
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, message);
|
|
9022
|
+
}
|
|
9023
|
+
function malformed(message = "Invalid non-realtime response data") {
|
|
9024
|
+
throw new XmaxError("API_ERROR" /* apiError */, message);
|
|
9025
|
+
}
|
|
9026
|
+
function record(value) {
|
|
9027
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9028
|
+
malformed();
|
|
9029
|
+
}
|
|
9030
|
+
return value;
|
|
9031
|
+
}
|
|
9032
|
+
function field(data, key, type) {
|
|
9033
|
+
const value = data[key];
|
|
9034
|
+
if (value === void 0 || value === null) {
|
|
9035
|
+
return void 0;
|
|
9036
|
+
}
|
|
9037
|
+
if (typeof value !== type || typeof value === "number" && (!Number.isFinite(value) || value < 0)) {
|
|
9038
|
+
malformed(`Invalid field: ${key}`);
|
|
9039
|
+
}
|
|
9040
|
+
return value;
|
|
9041
|
+
}
|
|
9042
|
+
function pageInteger(data, key, minimum) {
|
|
9043
|
+
const value = field(data, key, "number");
|
|
9044
|
+
if (value === void 0 || !Number.isSafeInteger(value) || value < minimum) {
|
|
9045
|
+
malformed(`Invalid field: ${key}`);
|
|
9046
|
+
}
|
|
9047
|
+
return value;
|
|
9048
|
+
}
|
|
9049
|
+
function parseList(value) {
|
|
9050
|
+
if (!Array.isArray(value)) {
|
|
9051
|
+
malformed("Missing task list");
|
|
9052
|
+
}
|
|
9053
|
+
return Object.freeze(value.map(parseTask));
|
|
9054
|
+
}
|
|
9055
|
+
function parseTask(value) {
|
|
9056
|
+
const data = record(value);
|
|
9057
|
+
const uid = field(data, "uid", "string");
|
|
9058
|
+
const status = field(data, "status", "string");
|
|
9059
|
+
const quality = field(data, "quality", "string");
|
|
9060
|
+
if (!uid?.trim() || !Object.values(NonRealtimeTaskStatus).includes(status)) {
|
|
9061
|
+
malformed("Missing task UID or invalid status");
|
|
9062
|
+
}
|
|
9063
|
+
if (quality !== void 0 && !Object.values(NonRealtimeQuality).includes(quality)) {
|
|
9064
|
+
malformed("Invalid task quality");
|
|
9065
|
+
}
|
|
9066
|
+
return Object.freeze({
|
|
9067
|
+
uid,
|
|
9068
|
+
status,
|
|
9069
|
+
quality,
|
|
9070
|
+
userUid: field(data, "userUid", "string"),
|
|
9071
|
+
prompt: field(data, "prompt", "string"),
|
|
9072
|
+
videoPath: field(data, "refVideoPath", "string"),
|
|
9073
|
+
referencePath: field(data, "refImagePath", "string"),
|
|
9074
|
+
processorId: field(data, "processorId", "string"),
|
|
9075
|
+
requestedFps: field(data, "requestedFps", "number"),
|
|
9076
|
+
resolvedFps: field(data, "resolvedFps", "number"),
|
|
9077
|
+
retryCount: field(data, "retryCount", "number"),
|
|
9078
|
+
videoDurationSeconds: field(data, "videoDurationSeconds", "number"),
|
|
9079
|
+
billableDurationSeconds: field(data, "billableDurationSeconds", "number"),
|
|
9080
|
+
chargePoints: field(data, "chargePoints", "number"),
|
|
9081
|
+
result: data.result == null ? void 0 : parseResult(data.result),
|
|
9082
|
+
processStartTime: field(data, "processStartTime", "string"),
|
|
9083
|
+
createTimestamp: field(data, "createTimestamp", "string"),
|
|
9084
|
+
updateTimestamp: field(data, "updateTimestamp", "string")
|
|
9085
|
+
});
|
|
9086
|
+
}
|
|
9087
|
+
function parseResult(value) {
|
|
9088
|
+
const data = record(value);
|
|
9089
|
+
return Object.freeze({
|
|
9090
|
+
url: field(data, "result_url", "string"),
|
|
9091
|
+
uploadError: field(data, "upload_error", "string"),
|
|
9092
|
+
uploadStatus: field(data, "upload_status", "string"),
|
|
9093
|
+
finalizeDurationMs: field(data, "finalize_duration_ms", "number"),
|
|
9094
|
+
compressionLevel: field(data, "compression_level", "string"),
|
|
9095
|
+
frameCount: field(data, "frame_count", "number"),
|
|
9096
|
+
fps: field(data, "fps", "number"),
|
|
9097
|
+
durationMs: field(data, "result_duration_ms", "number"),
|
|
9098
|
+
fileSizeBytes: field(data, "file_size_bytes", "number"),
|
|
9099
|
+
droppedFrameCount: field(data, "dropped_frame_count", "number"),
|
|
9100
|
+
originalFileSizeBytes: field(data, "original_file_size_bytes", "number")
|
|
9101
|
+
});
|
|
9102
|
+
}
|
|
9103
|
+
|
|
9104
|
+
// src/Service/NonRealtime/NonRealtimeTaskError.ts
|
|
9105
|
+
var NonRealtimeTaskError = class extends XmaxError {
|
|
7343
9106
|
/**
|
|
7344
|
-
*
|
|
7345
|
-
*
|
|
7346
|
-
* @param init.apiKey 调用 Xmax 服务使用的 API Key。
|
|
7347
|
-
* @param init.environment SDK 连接的服务环境;默认为国内环境。
|
|
7348
|
-
* @param init.loggerOptions SDK 输出的日志类型;默认为空。
|
|
9107
|
+
* 使用 API 错误分类,不虚构服务端未返回的业务错误码。
|
|
7349
9108
|
*/
|
|
7350
|
-
constructor(
|
|
7351
|
-
|
|
7352
|
-
* 服务身份与环境
|
|
7353
|
-
*/
|
|
7354
|
-
/**
|
|
7355
|
-
* 调用 Xmax 服务使用的 API Key。
|
|
7356
|
-
*/
|
|
7357
|
-
__publicField(this, "apiKey");
|
|
7358
|
-
/**
|
|
7359
|
-
* SDK 连接的服务环境,同时决定日志细项语言:国内为中文,海外为英文。
|
|
7360
|
-
*/
|
|
7361
|
-
__publicField(this, "environment");
|
|
9109
|
+
constructor(task, message) {
|
|
9110
|
+
super("API_ERROR" /* apiError */, message);
|
|
7362
9111
|
/**
|
|
7363
|
-
*
|
|
9112
|
+
* 失败上下文
|
|
7364
9113
|
*/
|
|
9114
|
+
__publicField(this, "task");
|
|
9115
|
+
this.name = "NonRealtimeTaskError";
|
|
9116
|
+
this.task = task;
|
|
9117
|
+
}
|
|
9118
|
+
};
|
|
9119
|
+
|
|
9120
|
+
// src/Core/NonRealtime/XmaxNonRealtimeManager.ts
|
|
9121
|
+
var XmaxNonRealtimeManager = class {
|
|
9122
|
+
/**
|
|
9123
|
+
* 注入非实时任务服务。
|
|
9124
|
+
*/
|
|
9125
|
+
constructor(tasks) {
|
|
7365
9126
|
/**
|
|
7366
|
-
*
|
|
9127
|
+
* 服务层组件
|
|
7367
9128
|
*/
|
|
7368
|
-
__publicField(this, "
|
|
7369
|
-
this.
|
|
7370
|
-
this.environment = init.environment ?? "china" /* china */;
|
|
7371
|
-
this.loggerOptions = init.loggerOptions ?? 0 /* none */;
|
|
9129
|
+
__publicField(this, "tasks");
|
|
9130
|
+
this.tasks = tasks;
|
|
7372
9131
|
}
|
|
7373
9132
|
/**
|
|
7374
|
-
*
|
|
7375
|
-
* @throws API Key 为空时抛出 `XmaxError`。
|
|
9133
|
+
* 单次提交任务,不自动重试可能已扣费的请求。
|
|
7376
9134
|
*/
|
|
7377
|
-
|
|
7378
|
-
|
|
9135
|
+
submitTask(options) {
|
|
9136
|
+
return this.tasks.submitTask(options);
|
|
9137
|
+
}
|
|
9138
|
+
/**
|
|
9139
|
+
* 取得任务快照,不把服务端失败终态当成查询失败。
|
|
9140
|
+
*/
|
|
9141
|
+
getTask(taskUid, options) {
|
|
9142
|
+
return this.tasks.getTask(taskUid, options?.signal);
|
|
9143
|
+
}
|
|
9144
|
+
/**
|
|
9145
|
+
* 批量取得任务快照。
|
|
9146
|
+
*/
|
|
9147
|
+
getTasks(taskUids) {
|
|
9148
|
+
return this.tasks.getTasks(taskUids);
|
|
9149
|
+
}
|
|
9150
|
+
/**
|
|
9151
|
+
* 分页取得任务列表。
|
|
9152
|
+
*/
|
|
9153
|
+
listTasks(options) {
|
|
9154
|
+
return this.tasks.listTasks(options);
|
|
9155
|
+
}
|
|
9156
|
+
/**
|
|
9157
|
+
* 等待任务完成;查询串行执行,瞬时错误有限退避,取消与总超时覆盖请求及等待阶段。
|
|
9158
|
+
*/
|
|
9159
|
+
async waitForCompletion(taskUid, options = {}) {
|
|
9160
|
+
const uid = validateTaskUid(taskUid);
|
|
9161
|
+
const interval = options.intervalMs ?? 2e3;
|
|
9162
|
+
const retries = options.maxConsecutiveRetries ?? 3;
|
|
9163
|
+
validateDelay(interval);
|
|
9164
|
+
if (options.timeoutMs !== void 0) {
|
|
9165
|
+
validateDelay(options.timeoutMs);
|
|
9166
|
+
}
|
|
9167
|
+
if (!Number.isSafeInteger(retries) || retries < 0) {
|
|
7379
9168
|
throw new XmaxError(
|
|
7380
|
-
"
|
|
7381
|
-
"
|
|
9169
|
+
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
9170
|
+
"Retry count must be a non-negative integer"
|
|
7382
9171
|
);
|
|
7383
9172
|
}
|
|
9173
|
+
const controller = new AbortController();
|
|
9174
|
+
let timedOut = false;
|
|
9175
|
+
const abort = () => controller.abort();
|
|
9176
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
9177
|
+
if (options.signal?.aborted) {
|
|
9178
|
+
abort();
|
|
9179
|
+
}
|
|
9180
|
+
const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
9181
|
+
timedOut = true;
|
|
9182
|
+
abort();
|
|
9183
|
+
}, options.timeoutMs);
|
|
9184
|
+
let failures = 0;
|
|
9185
|
+
try {
|
|
9186
|
+
for (; ; ) {
|
|
9187
|
+
ensureActive(controller.signal);
|
|
9188
|
+
let task;
|
|
9189
|
+
try {
|
|
9190
|
+
task = await this.tasks.getTask(uid, controller.signal);
|
|
9191
|
+
} catch (error) {
|
|
9192
|
+
ensureActive(controller.signal);
|
|
9193
|
+
if (!isTransient(error) || failures >= retries) {
|
|
9194
|
+
throw error;
|
|
9195
|
+
}
|
|
9196
|
+
failures++;
|
|
9197
|
+
await delay(Math.min(interval * 2 ** (failures - 1), 3e4), controller.signal);
|
|
9198
|
+
continue;
|
|
9199
|
+
}
|
|
9200
|
+
ensureActive(controller.signal);
|
|
9201
|
+
failures = 0;
|
|
9202
|
+
try {
|
|
9203
|
+
void Promise.resolve(options.onTaskUpdated?.(task)).catch(() => {
|
|
9204
|
+
});
|
|
9205
|
+
} catch {
|
|
9206
|
+
}
|
|
9207
|
+
ensureActive(controller.signal);
|
|
9208
|
+
if (task.status === "error" /* error */) {
|
|
9209
|
+
throw new NonRealtimeTaskError(task, `Non-realtime task ${uid} failed`);
|
|
9210
|
+
}
|
|
9211
|
+
if (task.status === "completed" /* completed */) {
|
|
9212
|
+
if (!isResultAvailable(task)) {
|
|
9213
|
+
throw new NonRealtimeTaskError(
|
|
9214
|
+
task,
|
|
9215
|
+
`Non-realtime task ${uid} completed without a usable result`
|
|
9216
|
+
);
|
|
9217
|
+
}
|
|
9218
|
+
return task;
|
|
9219
|
+
}
|
|
9220
|
+
await delay(interval, controller.signal);
|
|
9221
|
+
}
|
|
9222
|
+
} catch (error) {
|
|
9223
|
+
if (timedOut) {
|
|
9224
|
+
throw new XmaxError(
|
|
9225
|
+
"TIMEOUT" /* timeout */,
|
|
9226
|
+
`Waiting for task ${uid} timed out; the server task was not cancelled`
|
|
9227
|
+
);
|
|
9228
|
+
}
|
|
9229
|
+
ensureActive(controller.signal);
|
|
9230
|
+
throw error;
|
|
9231
|
+
} finally {
|
|
9232
|
+
clearTimeout(timer);
|
|
9233
|
+
options.signal?.removeEventListener("abort", abort);
|
|
9234
|
+
}
|
|
7384
9235
|
}
|
|
7385
9236
|
};
|
|
9237
|
+
function validateDelay(value) {
|
|
9238
|
+
if (!Number.isFinite(value) || value <= 0 || value > 2147483647) {
|
|
9239
|
+
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Polling durations must be positive and within the timer range");
|
|
9240
|
+
}
|
|
9241
|
+
}
|
|
9242
|
+
function ensureActive(signal) {
|
|
9243
|
+
if (signal.aborted) {
|
|
9244
|
+
throw new XmaxError(
|
|
9245
|
+
"CANCELLED" /* cancelled */,
|
|
9246
|
+
"Task wait was cancelled locally; the server task was not cancelled"
|
|
9247
|
+
);
|
|
9248
|
+
}
|
|
9249
|
+
}
|
|
9250
|
+
function delay(ms, signal) {
|
|
9251
|
+
return new Promise((resolve, reject) => {
|
|
9252
|
+
const finish = () => {
|
|
9253
|
+
clearTimeout(timer);
|
|
9254
|
+
signal.removeEventListener("abort", finish);
|
|
9255
|
+
try {
|
|
9256
|
+
ensureActive(signal);
|
|
9257
|
+
resolve();
|
|
9258
|
+
} catch (error) {
|
|
9259
|
+
reject(error);
|
|
9260
|
+
}
|
|
9261
|
+
};
|
|
9262
|
+
const timer = setTimeout(finish, ms);
|
|
9263
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
9264
|
+
if (signal.aborted) {
|
|
9265
|
+
finish();
|
|
9266
|
+
}
|
|
9267
|
+
});
|
|
9268
|
+
}
|
|
9269
|
+
function isTransient(error) {
|
|
9270
|
+
if (!(error instanceof XmaxError)) {
|
|
9271
|
+
return false;
|
|
9272
|
+
}
|
|
9273
|
+
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);
|
|
9274
|
+
}
|
|
9275
|
+
function isResultAvailable(task) {
|
|
9276
|
+
const result = task.result;
|
|
9277
|
+
if (!result?.url?.trim() || result.uploadError?.trim() || result.uploadStatus !== void 0 && result.uploadStatus !== "success") {
|
|
9278
|
+
return false;
|
|
9279
|
+
}
|
|
9280
|
+
try {
|
|
9281
|
+
const url = new URL(result.url);
|
|
9282
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
9283
|
+
} catch {
|
|
9284
|
+
return false;
|
|
9285
|
+
}
|
|
9286
|
+
}
|
|
7386
9287
|
|
|
7387
9288
|
// src/Core/XmaxClient.ts
|
|
7388
9289
|
var XmaxClient = class {
|
|
@@ -7429,6 +9330,17 @@ var XmaxClient = class {
|
|
|
7429
9330
|
environment: this.configuration.environment
|
|
7430
9331
|
});
|
|
7431
9332
|
}
|
|
9333
|
+
/**
|
|
9334
|
+
* 创建非实时视频任务 Manager,使用全局 API Key 和当前环境的非实时任务服务地址。
|
|
9335
|
+
* 本方法不发起网络请求或加载 RTC,任务由服务端持久化,可按 ID 恢复查询。
|
|
9336
|
+
*/
|
|
9337
|
+
createNonRealtimeManager() {
|
|
9338
|
+
const apiService = new ApiService({
|
|
9339
|
+
apiKey: this.configuration.apiKey,
|
|
9340
|
+
baseURL: nonRealtimeApiBaseURL(this.configuration.environment)
|
|
9341
|
+
});
|
|
9342
|
+
return new XmaxNonRealtimeManager(new NonRealtimeTaskService(apiService));
|
|
9343
|
+
}
|
|
7432
9344
|
/**
|
|
7433
9345
|
* 创建媒体处理与能力查询 Service。
|
|
7434
9346
|
*
|
|
@@ -7440,7 +9352,7 @@ var XmaxClient = class {
|
|
|
7440
9352
|
/**
|
|
7441
9353
|
* 创建文件存储 Service。
|
|
7442
9354
|
*
|
|
7443
|
-
* @returns
|
|
9355
|
+
* @returns 可上传图片和视频到对象存储的存储 Service。
|
|
7444
9356
|
*/
|
|
7445
9357
|
createStorageService() {
|
|
7446
9358
|
return new StorageService({ apiService: this.apiService });
|
|
@@ -7452,45 +9364,36 @@ var RealtimeConfiguration = class {
|
|
|
7452
9364
|
/**
|
|
7453
9365
|
* 创建实时业务配置。
|
|
7454
9366
|
*
|
|
7455
|
-
* @param init.model
|
|
7456
|
-
* @param init.provider RTC
|
|
7457
|
-
* @param init.isFrameInterpolationEnabled
|
|
7458
|
-
* @param init.frameInterpolation 插帧开关和显示帧率上限的详细配置。
|
|
9367
|
+
* @param init.model 实时生成业务使用的模型;未知名称原样透传,默认配置同 x2.1-preview。
|
|
9368
|
+
* @param init.provider RTC 提供方,默认按模型定义选择;必须受模型支持,创建后不可切换。
|
|
9369
|
+
* @param init.isFrameInterpolationEnabled 是否默认开启远端生成画面的插帧,默认 false。
|
|
7459
9370
|
*/
|
|
7460
9371
|
constructor(init) {
|
|
7461
|
-
/**
|
|
7462
|
-
* RTC 配置
|
|
7463
|
-
*/
|
|
7464
|
-
__publicField(this, "provider");
|
|
7465
|
-
/**
|
|
7466
|
-
* 模型配置
|
|
7467
|
-
*/
|
|
7468
9372
|
/**
|
|
7469
9373
|
* 实时生成业务使用的模型。
|
|
7470
9374
|
*/
|
|
7471
9375
|
__publicField(this, "model");
|
|
7472
9376
|
/**
|
|
7473
|
-
*
|
|
9377
|
+
* 实时连接使用的 RTC 提供方。
|
|
7474
9378
|
*/
|
|
9379
|
+
__publicField(this, "provider");
|
|
7475
9380
|
/**
|
|
7476
9381
|
* 是否默认请求开启远端生成画面的插帧。
|
|
7477
9382
|
*/
|
|
7478
9383
|
__publicField(this, "isFrameInterpolationEnabled");
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
*/
|
|
7482
|
-
__publicField(this, "frameInterpolation");
|
|
7483
|
-
this.provider = init.provider ?? "trtc" /* trtc */;
|
|
9384
|
+
const providers = supportedRtcProviders(init.model);
|
|
9385
|
+
this.provider = init.provider ?? providers[0];
|
|
7484
9386
|
if (!Object.values(RtcProvider).includes(this.provider)) {
|
|
7485
9387
|
throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Unsupported RTC provider");
|
|
7486
9388
|
}
|
|
7487
|
-
this.
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
9389
|
+
if (!providers.includes(this.provider)) {
|
|
9390
|
+
throw new XmaxError(
|
|
9391
|
+
"INVALID_CONFIGURATION" /* invalidConfiguration */,
|
|
9392
|
+
`Model ${init.model} does not support RTC provider ${this.provider}`
|
|
9393
|
+
);
|
|
7491
9394
|
}
|
|
7492
|
-
this.
|
|
7493
|
-
this.
|
|
9395
|
+
this.model = init.model;
|
|
9396
|
+
this.isFrameInterpolationEnabled = init.isFrameInterpolationEnabled ?? false;
|
|
7494
9397
|
}
|
|
7495
9398
|
};
|
|
7496
9399
|
|
|
@@ -7501,26 +9404,143 @@ var VideoContentMode = /* @__PURE__ */ ((VideoContentMode2) => {
|
|
|
7501
9404
|
return VideoContentMode2;
|
|
7502
9405
|
})(VideoContentMode || {});
|
|
7503
9406
|
|
|
7504
|
-
// src/
|
|
7505
|
-
var
|
|
9407
|
+
// src/Render/Video/RenderStatisticsController.ts
|
|
9408
|
+
var RenderStatisticsController = class {
|
|
7506
9409
|
/**
|
|
7507
|
-
*
|
|
9410
|
+
* 绑定用于观察原视频帧的元素,调用 start 后才开始采集。
|
|
7508
9411
|
*/
|
|
7509
|
-
constructor(
|
|
9412
|
+
constructor(video) {
|
|
9413
|
+
__publicField(this, "video", video);
|
|
7510
9414
|
/**
|
|
7511
|
-
*
|
|
9415
|
+
* 统计回调
|
|
7512
9416
|
*/
|
|
9417
|
+
__publicField(this, "handler");
|
|
7513
9418
|
/**
|
|
7514
|
-
*
|
|
9419
|
+
* 帧观察与采样生命周期
|
|
7515
9420
|
*/
|
|
7516
|
-
__publicField(this, "
|
|
9421
|
+
__publicField(this, "timer");
|
|
9422
|
+
__publicField(this, "callbackID");
|
|
9423
|
+
__publicField(this, "sequence", 0);
|
|
7517
9424
|
/**
|
|
7518
|
-
*
|
|
9425
|
+
* 采样窗口与输出方式
|
|
7519
9426
|
*/
|
|
7520
|
-
__publicField(this, "
|
|
7521
|
-
this
|
|
7522
|
-
|
|
7523
|
-
this
|
|
9427
|
+
__publicField(this, "startedAt", 0);
|
|
9428
|
+
__publicField(this, "originalFrames", 0);
|
|
9429
|
+
__publicField(this, "interpolatedFrames", 0);
|
|
9430
|
+
__publicField(this, "interpolationActive", false);
|
|
9431
|
+
__publicField(this, "previousPresentedFrames");
|
|
9432
|
+
}
|
|
9433
|
+
/**
|
|
9434
|
+
* 独立于统计面板可见性采集;没有监听者时不占用定时器。
|
|
9435
|
+
*/
|
|
9436
|
+
start() {
|
|
9437
|
+
if (this.timer !== void 0 || !this.video.srcObject && !this.video.getAttribute("src") || !this.handler || typeof this.video.requestVideoFrameCallback !== "function") {
|
|
9438
|
+
return;
|
|
9439
|
+
}
|
|
9440
|
+
this.reset();
|
|
9441
|
+
const sequence = this.sequence;
|
|
9442
|
+
const observe = () => {
|
|
9443
|
+
this.callbackID = this.video.requestVideoFrameCallback((_now, metadata) => {
|
|
9444
|
+
if (sequence !== this.sequence) {
|
|
9445
|
+
return;
|
|
9446
|
+
}
|
|
9447
|
+
this.callbackID = void 0;
|
|
9448
|
+
if (!this.interpolationActive && !document.hidden) {
|
|
9449
|
+
this.recordVideoFrames(metadata.presentedFrames);
|
|
9450
|
+
} else {
|
|
9451
|
+
this.previousPresentedFrames = void 0;
|
|
9452
|
+
}
|
|
9453
|
+
observe();
|
|
9454
|
+
});
|
|
9455
|
+
};
|
|
9456
|
+
observe();
|
|
9457
|
+
this.timer = setInterval(() => this.sample(), 1e3);
|
|
9458
|
+
}
|
|
9459
|
+
/**
|
|
9460
|
+
* 切换输出方式并清空采样窗口,避免原视频和画布重复计数。
|
|
9461
|
+
*/
|
|
9462
|
+
setInterpolationActive(active) {
|
|
9463
|
+
this.interpolationActive = active;
|
|
9464
|
+
this.reset();
|
|
9465
|
+
}
|
|
9466
|
+
/**
|
|
9467
|
+
* 记录实际提交到画布的原帧或插值帧,不统计未呈现的计算结果。
|
|
9468
|
+
*/
|
|
9469
|
+
recordCanvasFrame(interpolated) {
|
|
9470
|
+
if (interpolated) {
|
|
9471
|
+
this.interpolatedFrames++;
|
|
9472
|
+
} else {
|
|
9473
|
+
this.originalFrames++;
|
|
9474
|
+
}
|
|
9475
|
+
}
|
|
9476
|
+
/**
|
|
9477
|
+
* 按浏览器累计呈现帧数的增量统计,包含两次回调之间已呈现但未回调的帧。
|
|
9478
|
+
*/
|
|
9479
|
+
recordVideoFrames(presentedFrames) {
|
|
9480
|
+
if (!Number.isSafeInteger(presentedFrames) || presentedFrames < 0) {
|
|
9481
|
+
this.previousPresentedFrames = void 0;
|
|
9482
|
+
return;
|
|
9483
|
+
}
|
|
9484
|
+
const previous = this.previousPresentedFrames;
|
|
9485
|
+
this.previousPresentedFrames = presentedFrames;
|
|
9486
|
+
if (previous === void 0 || presentedFrames < previous) {
|
|
9487
|
+
this.originalFrames += presentedFrames > 0 ? 1 : 0;
|
|
9488
|
+
return;
|
|
9489
|
+
}
|
|
9490
|
+
this.originalFrames += presentedFrames - previous;
|
|
9491
|
+
}
|
|
9492
|
+
/**
|
|
9493
|
+
* 取消采样及帧观察,忽略已排队的旧回调并清空对外统计。
|
|
9494
|
+
*/
|
|
9495
|
+
stop() {
|
|
9496
|
+
this.sequence++;
|
|
9497
|
+
clearInterval(this.timer);
|
|
9498
|
+
this.timer = void 0;
|
|
9499
|
+
if (this.callbackID !== void 0) {
|
|
9500
|
+
this.video.cancelVideoFrameCallback(this.callbackID);
|
|
9501
|
+
}
|
|
9502
|
+
this.callbackID = void 0;
|
|
9503
|
+
this.reset();
|
|
9504
|
+
}
|
|
9505
|
+
/**
|
|
9506
|
+
* 清空采样计数,避免换流或切换输出方式时混入旧数据。
|
|
9507
|
+
*/
|
|
9508
|
+
reset() {
|
|
9509
|
+
this.startedAt = performance.now();
|
|
9510
|
+
this.originalFrames = 0;
|
|
9511
|
+
this.interpolatedFrames = 0;
|
|
9512
|
+
this.previousPresentedFrames = void 0;
|
|
9513
|
+
this.notify(void 0);
|
|
9514
|
+
}
|
|
9515
|
+
/**
|
|
9516
|
+
* 按实际经过的时间计算帧率;没有新呈现时输出 0,后台页面不报告帧率。
|
|
9517
|
+
*/
|
|
9518
|
+
sample() {
|
|
9519
|
+
const now = performance.now();
|
|
9520
|
+
const seconds = (now - this.startedAt) / 1e3;
|
|
9521
|
+
if (document.hidden || seconds <= 0) {
|
|
9522
|
+
this.reset();
|
|
9523
|
+
return;
|
|
9524
|
+
}
|
|
9525
|
+
const statistics = Object.freeze({
|
|
9526
|
+
source: this.interpolationActive ? "canvas" : "video",
|
|
9527
|
+
frameRate: (this.originalFrames + this.interpolatedFrames) / seconds,
|
|
9528
|
+
originalFrameRate: this.originalFrames / seconds,
|
|
9529
|
+
interpolatedFrameRate: this.interpolatedFrames / seconds
|
|
9530
|
+
});
|
|
9531
|
+
this.startedAt = now;
|
|
9532
|
+
this.originalFrames = 0;
|
|
9533
|
+
this.interpolatedFrames = 0;
|
|
9534
|
+
this.notify(statistics);
|
|
9535
|
+
}
|
|
9536
|
+
/**
|
|
9537
|
+
* 隔离接入方回调异常,避免影响播放和后续采样。
|
|
9538
|
+
*/
|
|
9539
|
+
notify(statistics) {
|
|
9540
|
+
try {
|
|
9541
|
+
this.handler?.(statistics);
|
|
9542
|
+
} catch {
|
|
9543
|
+
}
|
|
7524
9544
|
}
|
|
7525
9545
|
};
|
|
7526
9546
|
|
|
@@ -9516,11 +11536,11 @@ var _FrameInterpolationManager = class _FrameInterpolationManager {
|
|
|
9516
11536
|
});
|
|
9517
11537
|
}
|
|
9518
11538
|
static async create(canvas, size, onFailure, signal) {
|
|
9519
|
-
const
|
|
11539
|
+
const ensureActive2 = () => {
|
|
9520
11540
|
if (signal.aborted) throw new XmaxError("CANCELLED" /* cancelled */, "Interpolation initialization cancelled");
|
|
9521
11541
|
};
|
|
9522
11542
|
const adapter = await frameInterpolationAdapter(size);
|
|
9523
|
-
|
|
11543
|
+
ensureActive2();
|
|
9524
11544
|
if (!adapter) throw new XmaxError("FRAME_INTERPOLATION_UNSUPPORTED" /* frameInterpolationUnsupported */, "WebGPU shader-f16 is unavailable");
|
|
9525
11545
|
const device = await adapter.requestDevice({ requiredFeatures: ["shader-f16"] });
|
|
9526
11546
|
const manager = new _FrameInterpolationManager(device, canvas, size, onFailure);
|
|
@@ -9531,9 +11551,9 @@ var _FrameInterpolationManager = class _FrameInterpolationManager {
|
|
|
9531
11551
|
if (!manager.destroyed) onFailure(new Error(`Interpolation GPU device lost: ${info.message}`));
|
|
9532
11552
|
});
|
|
9533
11553
|
try {
|
|
9534
|
-
|
|
11554
|
+
ensureActive2();
|
|
9535
11555
|
await manager.initialize();
|
|
9536
|
-
|
|
11556
|
+
ensureActive2();
|
|
9537
11557
|
return manager;
|
|
9538
11558
|
} catch (error) {
|
|
9539
11559
|
manager.destroy();
|
|
@@ -9659,6 +11679,7 @@ __publicField(_FrameInterpolationManager, "originalSlots", 3);
|
|
|
9659
11679
|
var FrameInterpolationManager = _FrameInterpolationManager;
|
|
9660
11680
|
|
|
9661
11681
|
// src/Render/Video/RemoteVideoFramePipeline.ts
|
|
11682
|
+
var INTERPOLATION_FRAME_RATE = 60;
|
|
9662
11683
|
var RemoteVideoFramePipeline = class {
|
|
9663
11684
|
constructor(video, canvas, options, createProcessor = FrameInterpolationManager.create) {
|
|
9664
11685
|
__publicField(this, "video", video);
|
|
@@ -9686,6 +11707,10 @@ var RemoteVideoFramePipeline = class {
|
|
|
9686
11707
|
__publicField(this, "previousPresented");
|
|
9687
11708
|
__publicField(this, "slowPairs", 0);
|
|
9688
11709
|
__publicField(this, "active", false);
|
|
11710
|
+
/**
|
|
11711
|
+
* 帧时间基准:媒体时间异常后,本管线持续使用回调时钟,避免反复切换。
|
|
11712
|
+
*/
|
|
11713
|
+
__publicField(this, "timeSource", "mediaTime");
|
|
9689
11714
|
/** vsync 显示节拍:弹出到点的帧并显示最新的一张。 */
|
|
9690
11715
|
__publicField(this, "tick", (now) => {
|
|
9691
11716
|
if (this.stopped) return;
|
|
@@ -9732,8 +11757,8 @@ var RemoteVideoFramePipeline = class {
|
|
|
9732
11757
|
if (!this.loading) void this.initialize();
|
|
9733
11758
|
return;
|
|
9734
11759
|
}
|
|
9735
|
-
const time =
|
|
9736
|
-
if (
|
|
11760
|
+
const time = this.resolveFrameTime(now, metadata);
|
|
11761
|
+
if (time === void 0) return;
|
|
9737
11762
|
const previous = this.previousTime;
|
|
9738
11763
|
const interval = previous === void 0 ? 0 : time - previous;
|
|
9739
11764
|
const consecutive = this.previousPresented === void 0 || metadata.presentedFrames === this.previousPresented + 1;
|
|
@@ -9753,9 +11778,27 @@ var RemoteVideoFramePipeline = class {
|
|
|
9753
11778
|
this.delay = interval;
|
|
9754
11779
|
this.enqueue({ slot, time });
|
|
9755
11780
|
this.startTick();
|
|
9756
|
-
if (!consecutive || interval < 2e3 /
|
|
11781
|
+
if (!consecutive || interval < 2e3 / INTERPOLATION_FRAME_RATE - 1 || previousSlot === void 0 || this.busy) {
|
|
11782
|
+
return;
|
|
11783
|
+
}
|
|
9757
11784
|
this.interpolate(previousSlot, slot, previous, time, interval);
|
|
9758
11785
|
}
|
|
11786
|
+
/**
|
|
11787
|
+
* 优先使用媒体时间;新帧的媒体时间异常时切换到单调回调时钟。
|
|
11788
|
+
*/
|
|
11789
|
+
resolveFrameTime(now, metadata) {
|
|
11790
|
+
if (this.previousPresented !== void 0 && metadata.presentedFrames <= this.previousPresented) {
|
|
11791
|
+
return void 0;
|
|
11792
|
+
}
|
|
11793
|
+
let time = this.timeSource === "mediaTime" ? metadata.mediaTime * 1e3 : now;
|
|
11794
|
+
if (!Number.isFinite(time) || this.previousTime !== void 0 && time <= this.previousTime) {
|
|
11795
|
+
if (this.timeSource === "callback" || !Number.isFinite(now)) return void 0;
|
|
11796
|
+
this.timeSource = "callback";
|
|
11797
|
+
this.resetTimeline(true);
|
|
11798
|
+
time = now;
|
|
11799
|
+
}
|
|
11800
|
+
return time;
|
|
11801
|
+
}
|
|
9759
11802
|
/** 帧按显示时间升序入队;同一时间的内容后到先出。 */
|
|
9760
11803
|
enqueue(frame) {
|
|
9761
11804
|
const index = this.queue.findIndex((queued) => queued.time >= frame.time);
|
|
@@ -9782,8 +11825,10 @@ var RemoteVideoFramePipeline = class {
|
|
|
9782
11825
|
this.fail(new Error("Interpolation exceeded the GPU frame budget for 5 consecutive pairs"));
|
|
9783
11826
|
return;
|
|
9784
11827
|
}
|
|
9785
|
-
if (this.position(performance.now()) >= midpoint)
|
|
9786
|
-
|
|
11828
|
+
if (this.position(performance.now()) >= midpoint) {
|
|
11829
|
+
return;
|
|
11830
|
+
}
|
|
11831
|
+
this.enqueue({ slot, time: midpoint, interpolated: true });
|
|
9787
11832
|
}).catch((error) => {
|
|
9788
11833
|
if (!this.stopped) this.fail(error);
|
|
9789
11834
|
});
|
|
@@ -9802,6 +11847,7 @@ var RemoteVideoFramePipeline = class {
|
|
|
9802
11847
|
this.displayedTime = frame.time;
|
|
9803
11848
|
this.canvas.style.visibility = "visible";
|
|
9804
11849
|
this.setActive(true);
|
|
11850
|
+
this.options.onFramePresented?.(frame.interpolated ?? false);
|
|
9805
11851
|
}
|
|
9806
11852
|
async initialize() {
|
|
9807
11853
|
this.loading = true;
|
|
@@ -9831,11 +11877,13 @@ var RemoteVideoFramePipeline = class {
|
|
|
9831
11877
|
this.active = active;
|
|
9832
11878
|
this.options.onActiveChange?.(active);
|
|
9833
11879
|
}
|
|
9834
|
-
resetTimeline() {
|
|
11880
|
+
resetTimeline(preserveGpuTimeout = false) {
|
|
9835
11881
|
this.generation++;
|
|
9836
11882
|
this.queue = [];
|
|
9837
|
-
|
|
9838
|
-
|
|
11883
|
+
if (!preserveGpuTimeout) {
|
|
11884
|
+
clearTimeout(this.stallTimer);
|
|
11885
|
+
this.stallTimer = void 0;
|
|
11886
|
+
}
|
|
9839
11887
|
if (this.raf !== void 0) cancelAnimationFrame(this.raf);
|
|
9840
11888
|
this.raf = void 0;
|
|
9841
11889
|
this.anchor = void 0;
|
|
@@ -9914,6 +11962,10 @@ var XmaxVideoView = class {
|
|
|
9914
11962
|
__publicField(this, "interpolationOptions");
|
|
9915
11963
|
__publicField(this, "interpolationPipeline");
|
|
9916
11964
|
__publicField(this, "interpolationCanvas");
|
|
11965
|
+
/**
|
|
11966
|
+
* 呈现统计组件
|
|
11967
|
+
*/
|
|
11968
|
+
__publicField(this, "statisticsController");
|
|
9917
11969
|
/**
|
|
9918
11970
|
* DOM 挂载状态
|
|
9919
11971
|
*/
|
|
@@ -9924,6 +11976,7 @@ var XmaxVideoView = class {
|
|
|
9924
11976
|
this.element.style.overflow = "hidden";
|
|
9925
11977
|
this.element.style.backgroundColor = "black";
|
|
9926
11978
|
this.videoElement = document.createElement("video");
|
|
11979
|
+
this.statisticsController = new RenderStatisticsController(this.videoElement);
|
|
9927
11980
|
this.videoElement.autoplay = true;
|
|
9928
11981
|
this.videoElement.playsInline = true;
|
|
9929
11982
|
this.videoElement.muted = true;
|
|
@@ -9940,6 +11993,20 @@ var XmaxVideoView = class {
|
|
|
9940
11993
|
get track() {
|
|
9941
11994
|
return this.currentTrack;
|
|
9942
11995
|
}
|
|
11996
|
+
/**
|
|
11997
|
+
* 每秒接收当前视图的呈现统计;换流、卸载或不可统计时返回 undefined。
|
|
11998
|
+
*/
|
|
11999
|
+
get renderStatisticsHandler() {
|
|
12000
|
+
return this.statisticsController.handler;
|
|
12001
|
+
}
|
|
12002
|
+
/**
|
|
12003
|
+
* 更新统计回调,不影响播放;移除回调时停止采集。
|
|
12004
|
+
*/
|
|
12005
|
+
set renderStatisticsHandler(handler) {
|
|
12006
|
+
this.statisticsController.handler = handler;
|
|
12007
|
+
this.statisticsController.stop();
|
|
12008
|
+
this.startRenderStatistics();
|
|
12009
|
+
}
|
|
9943
12010
|
/**
|
|
9944
12011
|
* 解绑旧轨道后接入新轨道,并为新内容重新启用一次性首帧通知。
|
|
9945
12012
|
*/
|
|
@@ -9992,14 +12059,15 @@ var XmaxVideoView = class {
|
|
|
9992
12059
|
attach(to) {
|
|
9993
12060
|
this.detached = false;
|
|
9994
12061
|
to.appendChild(this.element);
|
|
9995
|
-
if (this.videoElement.srcObject && this.videoElement.paused && typeof this.videoElement.play === "function") {
|
|
12062
|
+
if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && this.videoElement.autoplay && !this.videoElement.ended && this.videoElement.paused && typeof this.videoElement.play === "function") {
|
|
9996
12063
|
void this.videoElement.play().catch((error) => {
|
|
9997
12064
|
XmaxLogger.render.warning(() => `\u6062\u590D\u89C6\u9891\u64AD\u653E\u5931\u8D25 (Failed to Resume Video Playback)
|
|
9998
12065
|
\u2514\u2500 ${String(error)}`);
|
|
9999
12066
|
});
|
|
10000
12067
|
}
|
|
10001
12068
|
this.startInterpolation();
|
|
10002
|
-
|
|
12069
|
+
this.startRenderStatistics();
|
|
12070
|
+
if ((this.videoElement.srcObject || this.videoElement.getAttribute("src")) && !this.hasNotifiedFrameDisplay) {
|
|
10003
12071
|
this.armFrameDisplayNotification();
|
|
10004
12072
|
}
|
|
10005
12073
|
}
|
|
@@ -10008,10 +12076,34 @@ var XmaxVideoView = class {
|
|
|
10008
12076
|
*/
|
|
10009
12077
|
detach() {
|
|
10010
12078
|
this.detached = true;
|
|
12079
|
+
this.statisticsController.stop();
|
|
10011
12080
|
this.stopInterpolation();
|
|
10012
12081
|
this.cancelFrameDisplayNotification();
|
|
12082
|
+
if (this.videoElement.getAttribute("src")) {
|
|
12083
|
+
this.videoElement.pause();
|
|
12084
|
+
}
|
|
10013
12085
|
this.element.remove();
|
|
10014
12086
|
}
|
|
12087
|
+
/**
|
|
12088
|
+
* 设置网络视频预览;静音播放一次,不捕获或发布媒体轨道。 @internal
|
|
12089
|
+
*/
|
|
12090
|
+
setVideoURL(url) {
|
|
12091
|
+
this.statisticsController.stop();
|
|
12092
|
+
this.stopInterpolation();
|
|
12093
|
+
this.cancelFrameDisplayNotification();
|
|
12094
|
+
this.hasNotifiedFrameDisplay = false;
|
|
12095
|
+
this.videoElement.pause();
|
|
12096
|
+
this.videoElement.srcObject = null;
|
|
12097
|
+
this.videoElement.removeAttribute("src");
|
|
12098
|
+
this.videoElement.muted = true;
|
|
12099
|
+
this.videoElement.loop = false;
|
|
12100
|
+
if (url) {
|
|
12101
|
+
this.videoElement.src = url;
|
|
12102
|
+
this.armFrameDisplayNotification();
|
|
12103
|
+
}
|
|
12104
|
+
this.videoElement.load();
|
|
12105
|
+
this.startRenderStatistics();
|
|
12106
|
+
}
|
|
10015
12107
|
/**
|
|
10016
12108
|
* 直接设置渲染用的媒体流(绑定管线和直接渲染共用)。 @internal
|
|
10017
12109
|
*/
|
|
@@ -10019,13 +12111,16 @@ var XmaxVideoView = class {
|
|
|
10019
12111
|
if (this.videoElement.srcObject === stream) {
|
|
10020
12112
|
return;
|
|
10021
12113
|
}
|
|
12114
|
+
this.statisticsController.stop();
|
|
10022
12115
|
this.stopInterpolation();
|
|
10023
12116
|
this.cancelFrameDisplayNotification();
|
|
10024
12117
|
this.hasNotifiedFrameDisplay = false;
|
|
10025
12118
|
this.videoElement.srcObject = stream;
|
|
10026
12119
|
if (stream) {
|
|
12120
|
+
this.videoElement.autoplay = true;
|
|
10027
12121
|
this.armFrameDisplayNotification();
|
|
10028
12122
|
this.startInterpolation();
|
|
12123
|
+
this.startRenderStatistics();
|
|
10029
12124
|
}
|
|
10030
12125
|
}
|
|
10031
12126
|
/**
|
|
@@ -10055,7 +12150,18 @@ var XmaxVideoView = class {
|
|
|
10055
12150
|
this.interpolationCanvas = canvas;
|
|
10056
12151
|
this.applyContentMode();
|
|
10057
12152
|
this.element.appendChild(canvas);
|
|
10058
|
-
const
|
|
12153
|
+
const options = this.interpolationOptions;
|
|
12154
|
+
const pipeline = new RemoteVideoFramePipeline(this.videoElement, canvas, {
|
|
12155
|
+
...options,
|
|
12156
|
+
onActiveChange: (active) => {
|
|
12157
|
+
this.statisticsController.setInterpolationActive(active);
|
|
12158
|
+
options.onActiveChange?.(active);
|
|
12159
|
+
},
|
|
12160
|
+
onFramePresented: (interpolated) => {
|
|
12161
|
+
this.statisticsController.recordCanvasFrame(interpolated);
|
|
12162
|
+
options.onFramePresented?.(interpolated);
|
|
12163
|
+
}
|
|
12164
|
+
});
|
|
10059
12165
|
this.interpolationPipeline = pipeline;
|
|
10060
12166
|
pipeline.start();
|
|
10061
12167
|
}
|
|
@@ -10090,6 +12196,7 @@ var XmaxVideoView = class {
|
|
|
10090
12196
|
* 解绑当前轨道并清空渲染内容。
|
|
10091
12197
|
*/
|
|
10092
12198
|
detachCurrentTrack() {
|
|
12199
|
+
this.statisticsController.stop();
|
|
10093
12200
|
const track = this.currentTrack;
|
|
10094
12201
|
if (track) {
|
|
10095
12202
|
VideoRenderRegistry.binding(track)?.detachHandler(this);
|
|
@@ -10097,6 +12204,9 @@ var XmaxVideoView = class {
|
|
|
10097
12204
|
this.setFrameInterpolation(void 0);
|
|
10098
12205
|
this.cancelFrameDisplayNotification();
|
|
10099
12206
|
this.videoElement.srcObject = null;
|
|
12207
|
+
if (this.videoElement.getAttribute("src")) {
|
|
12208
|
+
this.setVideoURL(null);
|
|
12209
|
+
}
|
|
10100
12210
|
}
|
|
10101
12211
|
/**
|
|
10102
12212
|
* 首帧提交合成器时通知;旧浏览器回退到 playing 近似判断。
|
|
@@ -10152,6 +12262,12 @@ var XmaxVideoView = class {
|
|
|
10152
12262
|
this.interpolationCanvas.style.transform = this.mirrored ? "scaleX(-1)" : "";
|
|
10153
12263
|
}
|
|
10154
12264
|
}
|
|
12265
|
+
/**
|
|
12266
|
+
* 视图未卸载时启动呈现统计,采样及媒体就绪检查由统计组件负责。
|
|
12267
|
+
*/
|
|
12268
|
+
startRenderStatistics() {
|
|
12269
|
+
if (!this.detached) this.statisticsController.start();
|
|
12270
|
+
}
|
|
10155
12271
|
};
|
|
10156
12272
|
|
|
10157
12273
|
// src/Render/Video/XmaxRealtimeVideoView.ts
|
|
@@ -10324,25 +12440,31 @@ export {
|
|
|
10324
12440
|
XmaxError,
|
|
10325
12441
|
RealtimeVideoEncoderPreference,
|
|
10326
12442
|
RealtimeVideoFormat,
|
|
12443
|
+
RtcProvider,
|
|
10327
12444
|
RealtimeModel,
|
|
12445
|
+
modelDisplayName,
|
|
10328
12446
|
defaultFrameRate,
|
|
10329
12447
|
defaultCameraVideoFormat,
|
|
12448
|
+
supportedRtcProviders,
|
|
10330
12449
|
MediaService,
|
|
10331
12450
|
StoredFile,
|
|
10332
12451
|
StorageService,
|
|
10333
12452
|
CameraPosition,
|
|
10334
|
-
RtcProvider,
|
|
10335
12453
|
RealtimeVideoTrack,
|
|
10336
12454
|
RealtimeMediaStream,
|
|
12455
|
+
RealtimeVideoSampleMethod,
|
|
12456
|
+
RealtimeContext,
|
|
10337
12457
|
RealtimeConnectionState,
|
|
10338
12458
|
RealtimeReason,
|
|
10339
12459
|
RealtimeState,
|
|
10340
12460
|
XmaxConfiguration,
|
|
12461
|
+
NonRealtimeQuality,
|
|
12462
|
+
NonRealtimeTaskStatus,
|
|
12463
|
+
NonRealtimeTaskError,
|
|
10341
12464
|
XmaxClient,
|
|
10342
12465
|
RealtimeConfiguration,
|
|
10343
12466
|
VideoContentMode,
|
|
10344
|
-
RealtimeContext,
|
|
10345
12467
|
XmaxVideoView,
|
|
10346
12468
|
XmaxRealtimeVideoView
|
|
10347
12469
|
};
|
|
10348
|
-
//# sourceMappingURL=chunk-
|
|
12470
|
+
//# sourceMappingURL=chunk-PLFWPS5A.js.map
|