@xmaxai/web-sdk 1.0.2 → 1.0.3
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/dist/{chunk-5NIEZJP5.cjs → chunk-JWAAS6P4.cjs} +324 -276
- package/dist/chunk-JWAAS6P4.cjs.map +1 -0
- package/dist/{chunk-6RIEDVRQ.js → chunk-YMLRUDAO.js} +213 -165
- package/dist/chunk-YMLRUDAO.js.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -8
- package/dist/index.d.ts +17 -8
- package/dist/index.js +1 -1
- package/dist/react/index.cjs +3 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5NIEZJP5.cjs.map +0 -1
- package/dist/chunk-6RIEDVRQ.js.map +0 -1
|
@@ -198,7 +198,7 @@ var XmaxError = class _XmaxError extends Error {
|
|
|
198
198
|
};
|
|
199
199
|
|
|
200
200
|
// src/Foundation/Runtime/RuntimeInfo.ts
|
|
201
|
-
var XMAX_SDK_VERSION = "1.0.
|
|
201
|
+
var XMAX_SDK_VERSION = "1.0.3";
|
|
202
202
|
var _RuntimeInfo = class _RuntimeInfo {
|
|
203
203
|
/**
|
|
204
204
|
* 序列化为接口传输使用的 snake_case 字段。
|
|
@@ -263,10 +263,12 @@ var _ApiService = class _ApiService {
|
|
|
263
263
|
}
|
|
264
264
|
/**
|
|
265
265
|
* 发送请求并返回统一响应中的数据。
|
|
266
|
+
*
|
|
267
|
+
* @param options.keepalive 为 true 时请求可存活于页面卸载之后。
|
|
266
268
|
*/
|
|
267
|
-
async request(method, path, body) {
|
|
269
|
+
async request(method, path, body, options) {
|
|
268
270
|
this.validateConfiguration();
|
|
269
|
-
const request = this.makeRequest(method, path, body);
|
|
271
|
+
const request = this.makeRequest(method, path, body, options);
|
|
270
272
|
const startedAt = Date.now();
|
|
271
273
|
let status;
|
|
272
274
|
let text;
|
|
@@ -313,8 +315,8 @@ var _ApiService = class _ApiService {
|
|
|
313
315
|
/**
|
|
314
316
|
* 发送 DELETE 请求。
|
|
315
317
|
*/
|
|
316
|
-
delete(path) {
|
|
317
|
-
return this.request("DELETE" /* delete */, path);
|
|
318
|
+
delete(path, options) {
|
|
319
|
+
return this.request("DELETE" /* delete */, path, void 0, options);
|
|
318
320
|
}
|
|
319
321
|
/**
|
|
320
322
|
* 校验 API Key 与 Base URL 配置。
|
|
@@ -344,7 +346,7 @@ var _ApiService = class _ApiService {
|
|
|
344
346
|
/**
|
|
345
347
|
* 构造请求地址、请求头与请求体;路径必须相对且与 Base URL 同源。
|
|
346
348
|
*/
|
|
347
|
-
makeRequest(method, path, body) {
|
|
349
|
+
makeRequest(method, path, body, options) {
|
|
348
350
|
const normalizedPath = path.trim();
|
|
349
351
|
if (normalizedPath.length === 0 || normalizedPath.includes("://") || normalizedPath.startsWith("//")) {
|
|
350
352
|
throw new XmaxError(
|
|
@@ -385,7 +387,8 @@ var _ApiService = class _ApiService {
|
|
|
385
387
|
"X-SDK-Version": XMAX_SDK_VERSION
|
|
386
388
|
},
|
|
387
389
|
body: encodedBody,
|
|
388
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
390
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
391
|
+
...options?.keepalive ? { keepalive: true } : {}
|
|
389
392
|
}
|
|
390
393
|
};
|
|
391
394
|
}
|
|
@@ -1006,6 +1009,68 @@ var CameraPosition = /* @__PURE__ */ ((CameraPosition3) => {
|
|
|
1006
1009
|
return CameraPosition3;
|
|
1007
1010
|
})(CameraPosition || {});
|
|
1008
1011
|
|
|
1012
|
+
// src/Foundation/Permissions/MediaPermissionWatch.ts
|
|
1013
|
+
var PERMISSION_POLL_INTERVAL_MS = 100;
|
|
1014
|
+
async function watchMediaPermissionPrompt(includeMicrophone) {
|
|
1015
|
+
if (typeof navigator === "undefined" || !navigator.permissions?.query) {
|
|
1016
|
+
return void 0;
|
|
1017
|
+
}
|
|
1018
|
+
const names = ["camera", ...includeMicrophone ? ["microphone"] : []];
|
|
1019
|
+
const queryOnce = () => Promise.all(
|
|
1020
|
+
names.map(async (name) => {
|
|
1021
|
+
const status = await navigator.permissions.query({
|
|
1022
|
+
name
|
|
1023
|
+
});
|
|
1024
|
+
return status.state;
|
|
1025
|
+
})
|
|
1026
|
+
);
|
|
1027
|
+
const queryStates = () => Promise.all(
|
|
1028
|
+
names.map(async (name) => {
|
|
1029
|
+
try {
|
|
1030
|
+
const status = await navigator.permissions.query({
|
|
1031
|
+
name
|
|
1032
|
+
});
|
|
1033
|
+
return status.state;
|
|
1034
|
+
} catch {
|
|
1035
|
+
return "prompt";
|
|
1036
|
+
}
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
1039
|
+
let states;
|
|
1040
|
+
try {
|
|
1041
|
+
states = await queryOnce();
|
|
1042
|
+
} catch {
|
|
1043
|
+
return void 0;
|
|
1044
|
+
}
|
|
1045
|
+
if (states.every((state2) => state2 === "granted")) {
|
|
1046
|
+
return { decided: Promise.resolve(), dispose() {
|
|
1047
|
+
} };
|
|
1048
|
+
}
|
|
1049
|
+
if (!states.includes("prompt")) {
|
|
1050
|
+
return void 0;
|
|
1051
|
+
}
|
|
1052
|
+
let disposed = false;
|
|
1053
|
+
let resolveDecided;
|
|
1054
|
+
const decided = new Promise((resolve) => {
|
|
1055
|
+
resolveDecided = resolve;
|
|
1056
|
+
});
|
|
1057
|
+
const finishIfDecided = (current) => {
|
|
1058
|
+
if (disposed || current.includes("prompt")) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
dispose();
|
|
1062
|
+
resolveDecided();
|
|
1063
|
+
};
|
|
1064
|
+
const poll = setInterval(() => {
|
|
1065
|
+
void queryStates().then(finishIfDecided);
|
|
1066
|
+
}, PERMISSION_POLL_INTERVAL_MS);
|
|
1067
|
+
function dispose() {
|
|
1068
|
+
disposed = true;
|
|
1069
|
+
clearInterval(poll);
|
|
1070
|
+
}
|
|
1071
|
+
return { decided, dispose };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1009
1074
|
// src/Foundation/RTC/RtcEngineManager.ts
|
|
1010
1075
|
var RtcEngineLease = class {
|
|
1011
1076
|
/**
|
|
@@ -1239,6 +1304,33 @@ var RtcStatsLogger = class {
|
|
|
1239
1304
|
}
|
|
1240
1305
|
};
|
|
1241
1306
|
|
|
1307
|
+
// src/Foundation/RTC/RtcVideoOrientation.ts
|
|
1308
|
+
function shouldTransposeRtcVideoSize(environment) {
|
|
1309
|
+
const userAgent = environment?.userAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
|
|
1310
|
+
const maxTouchPoints = environment?.maxTouchPoints ?? (typeof navigator === "undefined" ? 0 : navigator.maxTouchPoints ?? 0);
|
|
1311
|
+
const mobile = /Android|webOS|iPhone|iPad|iPod|Mobile/i.test(userAgent) || /Macintosh/i.test(userAgent) && maxTouchPoints > 1;
|
|
1312
|
+
if (!mobile) {
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
const orientationType = environment?.orientationType ?? (typeof window === "undefined" ? void 0 : window.screen?.orientation?.type);
|
|
1316
|
+
if (orientationType !== void 0) {
|
|
1317
|
+
return orientationType.includes("portrait");
|
|
1318
|
+
}
|
|
1319
|
+
const degrees = environment?.orientationDegrees ?? (typeof window === "undefined" ? void 0 : window.orientation);
|
|
1320
|
+
return degrees === 0 || degrees === 180;
|
|
1321
|
+
}
|
|
1322
|
+
function rtcOrientedVideoSize(size, environment) {
|
|
1323
|
+
return shouldTransposeRtcVideoSize(environment) ? { width: size.height, height: size.width } : size;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// src/Foundation/RTC/RemoteAudioVolumeControl.ts
|
|
1327
|
+
function canControlRemoteAudioVolume(environment) {
|
|
1328
|
+
const userAgent = environment?.userAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
|
|
1329
|
+
const maxTouchPoints = environment?.maxTouchPoints ?? (typeof navigator === "undefined" ? 0 : navigator.maxTouchPoints ?? 0);
|
|
1330
|
+
const ios = /iPhone|iPad|iPod/i.test(userAgent) || /Macintosh/i.test(userAgent) && maxTouchPoints > 1;
|
|
1331
|
+
return !ios;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1242
1334
|
// src/Foundation/RTC/RtcManager.ts
|
|
1243
1335
|
var RTC_EVENT = {
|
|
1244
1336
|
remoteVideoAvailable: "remote-video-available",
|
|
@@ -1343,13 +1435,17 @@ var RtcManager = class {
|
|
|
1343
1435
|
);
|
|
1344
1436
|
}
|
|
1345
1437
|
try {
|
|
1438
|
+
const captureSize = rtcOrientedVideoSize({
|
|
1439
|
+
width: options.width,
|
|
1440
|
+
height: options.height
|
|
1441
|
+
});
|
|
1346
1442
|
await engine.startLocalVideo({
|
|
1347
1443
|
publish: false,
|
|
1348
1444
|
option: {
|
|
1349
1445
|
useFrontCamera: options.position === "front" /* front */,
|
|
1350
1446
|
profile: {
|
|
1351
|
-
width:
|
|
1352
|
-
height:
|
|
1447
|
+
width: captureSize.width,
|
|
1448
|
+
height: captureSize.height,
|
|
1353
1449
|
frameRate: options.frameRate,
|
|
1354
1450
|
bitrate: CAPTURE_PLACEHOLDER_BITRATE
|
|
1355
1451
|
}
|
|
@@ -1469,7 +1565,7 @@ var RtcManager = class {
|
|
|
1469
1565
|
/**
|
|
1470
1566
|
* 配置本地视频编码参数。
|
|
1471
1567
|
*
|
|
1472
|
-
* TRTC 只接受单一目标码率,以 `
|
|
1568
|
+
* TRTC 只接受单一目标码率,以 `minimumBitrate`(流畅优先档)作为目标码率;
|
|
1473
1569
|
* 编码策略偏好映射为 TRTC 弱网偏好。
|
|
1474
1570
|
*
|
|
1475
1571
|
* @throws 摄像头采集未启动或编码参数配置失败时抛出错误。
|
|
@@ -1483,13 +1579,17 @@ var RtcManager = class {
|
|
|
1483
1579
|
);
|
|
1484
1580
|
}
|
|
1485
1581
|
try {
|
|
1582
|
+
const encodingSize = rtcOrientedVideoSize({
|
|
1583
|
+
width: configuration.width,
|
|
1584
|
+
height: configuration.height
|
|
1585
|
+
});
|
|
1486
1586
|
await engine.updateLocalVideo({
|
|
1487
1587
|
option: {
|
|
1488
1588
|
profile: {
|
|
1489
|
-
width:
|
|
1490
|
-
height:
|
|
1589
|
+
width: encodingSize.width,
|
|
1590
|
+
height: encodingSize.height,
|
|
1491
1591
|
frameRate: configuration.frameRate,
|
|
1492
|
-
bitrate: configuration.
|
|
1592
|
+
bitrate: configuration.minimumBitrate
|
|
1493
1593
|
},
|
|
1494
1594
|
qosPreference: QOS_PREFERENCE_MAP[configuration.encoderPreference]
|
|
1495
1595
|
}
|
|
@@ -1607,14 +1707,34 @@ var RtcManager = class {
|
|
|
1607
1707
|
throw this.mapError(error);
|
|
1608
1708
|
}
|
|
1609
1709
|
}
|
|
1710
|
+
/**
|
|
1711
|
+
* 当前平台是否支持远端音量控制(iOS Safari/WKWebView 不支持)。
|
|
1712
|
+
*/
|
|
1713
|
+
get supportsRemoteAudioVolumeControl() {
|
|
1714
|
+
return canControlRemoteAudioVolume();
|
|
1715
|
+
}
|
|
1610
1716
|
/**
|
|
1611
1717
|
* 设置指定远端用户的音频播放音量。
|
|
1612
1718
|
*
|
|
1719
|
+
* iOS Safari/WKWebView 不允许 JS 修改媒体元素音量(TRTC 官方标注
|
|
1720
|
+
* `setRemoteAudioVolume` 不支持 iOS Safari),该平台上降级为
|
|
1721
|
+
* 播放/静音二值控制:音量为 0 时停止播放该路音频,非 0 时恢复播放;
|
|
1722
|
+
* 恢复播放需要重新拉流,会有短暂缓冲。
|
|
1723
|
+
*
|
|
1613
1724
|
* @param volume 音量,取值范围为 `0...100`,越界时收敛到边界。
|
|
1614
1725
|
*/
|
|
1615
1726
|
setRemoteAudioVolume(volume, userID) {
|
|
1616
1727
|
const engine = this.requireEngine();
|
|
1617
1728
|
const normalized = Math.min(Math.max(Math.round(volume), 0), 100);
|
|
1729
|
+
if (!canControlRemoteAudioVolume()) {
|
|
1730
|
+
void engine.muteRemoteAudio(userID, normalized === 0).catch((error) => {
|
|
1731
|
+
XmaxLogger.rtc.warning(
|
|
1732
|
+
() => `\u8FDC\u7AEF\u97F3\u9891\u64AD\u653E\u72B6\u6001\u5207\u6362\u5931\u8D25 (Failed to Toggle Remote Audio Playback)
|
|
1733
|
+
\u2514\u2500 ${XmaxLogger.localized("\u539F\u56E0\uFF1A", "Reason: ")}${XmaxError.from(error).message}`
|
|
1734
|
+
);
|
|
1735
|
+
});
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1618
1738
|
engine.setRemoteAudioVolume(userID, normalized);
|
|
1619
1739
|
}
|
|
1620
1740
|
/**
|
|
@@ -1766,74 +1886,25 @@ var RtcManager = class {
|
|
|
1766
1886
|
};
|
|
1767
1887
|
|
|
1768
1888
|
// src/Foundation/Media/Camera/CameraFrameValidator.ts
|
|
1769
|
-
var
|
|
1770
|
-
var TIMEOUT_MS = 2e3;
|
|
1771
|
-
var CameraFrameValidator = class {
|
|
1772
|
-
/**
|
|
1773
|
-
* 单帧中央区域亮度达标即放行,不等待连续帧或曝光稳定。
|
|
1774
|
-
*/
|
|
1775
|
-
accept(pixels) {
|
|
1776
|
-
const size = pixels.length / 4;
|
|
1777
|
-
if (!size || !Number.isInteger(size)) return false;
|
|
1778
|
-
const histogram = new Uint32Array(256);
|
|
1779
|
-
let total = 0;
|
|
1780
|
-
let dark = 0;
|
|
1781
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
1782
|
-
const luminance = Math.round(0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2]);
|
|
1783
|
-
histogram[luminance] = histogram[luminance] + 1;
|
|
1784
|
-
total += luminance;
|
|
1785
|
-
if (luminance < 12) dark++;
|
|
1786
|
-
}
|
|
1787
|
-
let median = 0;
|
|
1788
|
-
let accumulated = 0;
|
|
1789
|
-
for (; median < 255; median++) {
|
|
1790
|
-
accumulated += histogram[median];
|
|
1791
|
-
if (accumulated >= size / 2) break;
|
|
1792
|
-
}
|
|
1793
|
-
const mean = total / size;
|
|
1794
|
-
return mean >= 24 && median >= 18 && dark / size <= 0.75;
|
|
1795
|
-
}
|
|
1796
|
-
};
|
|
1889
|
+
var CAMERA_WARMUP_MS = 200;
|
|
1797
1890
|
function waitForValidCameraFrame(track, signal) {
|
|
1798
1891
|
return new Promise((resolve, reject) => {
|
|
1799
|
-
let
|
|
1800
|
-
let
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
const gate = new CameraFrameValidator();
|
|
1806
|
-
const finish = (result = "valid") => {
|
|
1807
|
-
if (finished) return;
|
|
1808
|
-
finished = true;
|
|
1809
|
-
clearTimeout(timeout);
|
|
1810
|
-
clearTimeout(poll);
|
|
1811
|
-
if (video) {
|
|
1812
|
-
if (frameCallback !== void 0) video.cancelVideoFrameCallback(frameCallback);
|
|
1813
|
-
video.pause();
|
|
1814
|
-
video.srcObject = null;
|
|
1815
|
-
video.remove();
|
|
1816
|
-
}
|
|
1892
|
+
let timer;
|
|
1893
|
+
let settled = false;
|
|
1894
|
+
const finish = (error) => {
|
|
1895
|
+
if (settled) return;
|
|
1896
|
+
settled = true;
|
|
1897
|
+
clearTimeout(timer);
|
|
1817
1898
|
signal.removeEventListener("abort", abort);
|
|
1818
1899
|
track.removeEventListener("ended", ended);
|
|
1819
|
-
if (
|
|
1820
|
-
|
|
1821
|
-
reject(result);
|
|
1900
|
+
if (error) {
|
|
1901
|
+
reject(error);
|
|
1822
1902
|
} else {
|
|
1823
|
-
if (result === "timeout") {
|
|
1824
|
-
XmaxLogger.media.warning(() => XmaxLogger.localized(
|
|
1825
|
-
"\u76F8\u673A\u4EAE\u5EA6\u68C0\u67E5\u8D85\u65F6\uFF0C\u6309\u5F53\u524D\u753B\u9762\u7EE7\u7EED\u751F\u6210 (Camera Frame Validation Timed Out; Continuing)\n\u2514\u2500 \u672A\u83B7\u5F97\u4EAE\u5EA6\u5408\u683C\u5E27\uFF0C\u4E0D\u518D\u7B49\u5F85\u3002",
|
|
1826
|
-
"Camera Frame Validation Timed Out; Continuing\n\u2514\u2500 No sufficiently bright frame received; proceeding without further waiting."
|
|
1827
|
-
));
|
|
1828
|
-
} else {
|
|
1829
|
-
XmaxLogger.media.info(() => `\u76F8\u673A\u66DD\u5149\u68C0\u67E5\u901A\u8FC7 (Camera Exposure Ready)
|
|
1830
|
-
\u2514\u2500 ${Math.round(performance.now() - started)} ms`);
|
|
1831
|
-
}
|
|
1832
1903
|
resolve();
|
|
1833
1904
|
}
|
|
1834
1905
|
};
|
|
1835
|
-
const abort = () => finish(new XmaxError("CANCELLED" /* cancelled */, "Camera
|
|
1836
|
-
const ended = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera track ended before
|
|
1906
|
+
const abort = () => finish(new XmaxError("CANCELLED" /* cancelled */, "Camera warmup cancelled"));
|
|
1907
|
+
const ended = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera track ended before warmup completed"));
|
|
1837
1908
|
signal.addEventListener("abort", abort, { once: true });
|
|
1838
1909
|
track.addEventListener("ended", ended, { once: true });
|
|
1839
1910
|
if (signal.aborted) {
|
|
@@ -1844,70 +1915,7 @@ function waitForValidCameraFrame(track, signal) {
|
|
|
1844
1915
|
ended();
|
|
1845
1916
|
return;
|
|
1846
1917
|
}
|
|
1847
|
-
|
|
1848
|
-
video = document.createElement("video");
|
|
1849
|
-
video.muted = true;
|
|
1850
|
-
video.autoplay = true;
|
|
1851
|
-
video.playsInline = true;
|
|
1852
|
-
video.setAttribute("aria-hidden", "true");
|
|
1853
|
-
video.style.cssText = "position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
1854
|
-
const canvas = document.createElement("canvas");
|
|
1855
|
-
canvas.width = 64;
|
|
1856
|
-
canvas.height = 48;
|
|
1857
|
-
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
1858
|
-
if (!context) throw new Error("Camera exposure sampling requires a 2D canvas");
|
|
1859
|
-
video.srcObject = new MediaStream([track]);
|
|
1860
|
-
document.body.appendChild(video);
|
|
1861
|
-
const source = video;
|
|
1862
|
-
let lastFrameTime = -Infinity;
|
|
1863
|
-
const sample = (mediaTime) => {
|
|
1864
|
-
if (source.readyState < 2 || !source.videoWidth || !source.videoHeight || track.muted || !track.enabled) {
|
|
1865
|
-
return;
|
|
1866
|
-
}
|
|
1867
|
-
if (mediaTime === lastFrameTime) return;
|
|
1868
|
-
lastFrameTime = mediaTime;
|
|
1869
|
-
const width = source.videoWidth;
|
|
1870
|
-
const height = source.videoHeight;
|
|
1871
|
-
context.drawImage(source, width * 0.2, height * 0.2, width * 0.6, height * 0.6, 0, 0, 64, 48);
|
|
1872
|
-
if (gate.accept(context.getImageData(0, 0, 64, 48).data)) finish();
|
|
1873
|
-
};
|
|
1874
|
-
const fail = (error) => finish(new XmaxError(
|
|
1875
|
-
"MEDIA_ERROR" /* mediaError */,
|
|
1876
|
-
`Camera exposure check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1877
|
-
));
|
|
1878
|
-
const schedule = () => {
|
|
1879
|
-
if (finished) return;
|
|
1880
|
-
if (typeof source.requestVideoFrameCallback === "function") {
|
|
1881
|
-
frameCallback = source.requestVideoFrameCallback((_now, metadata) => {
|
|
1882
|
-
frameCallback = void 0;
|
|
1883
|
-
try {
|
|
1884
|
-
sample(metadata.mediaTime);
|
|
1885
|
-
schedule();
|
|
1886
|
-
} catch (error) {
|
|
1887
|
-
fail(error);
|
|
1888
|
-
}
|
|
1889
|
-
});
|
|
1890
|
-
} else {
|
|
1891
|
-
poll = setTimeout(() => {
|
|
1892
|
-
try {
|
|
1893
|
-
const frameTime = typeof source.getVideoPlaybackQuality === "function" ? source.getVideoPlaybackQuality().totalVideoFrames : source.currentTime;
|
|
1894
|
-
sample(frameTime);
|
|
1895
|
-
schedule();
|
|
1896
|
-
} catch (error) {
|
|
1897
|
-
fail(error);
|
|
1898
|
-
}
|
|
1899
|
-
}, FALLBACK_POLL_INTERVAL_MS);
|
|
1900
|
-
}
|
|
1901
|
-
};
|
|
1902
|
-
timeout = setTimeout(() => finish("timeout"), TIMEOUT_MS);
|
|
1903
|
-
XmaxLogger.media.info(() => "\u7B49\u5F85\u9996\u5F20\u4EAE\u5EA6\u5408\u683C\u5E27 (Waiting for the First Sufficiently Bright Frame)");
|
|
1904
|
-
void source.play().then(schedule, fail);
|
|
1905
|
-
} catch (error) {
|
|
1906
|
-
finish(new XmaxError(
|
|
1907
|
-
"MEDIA_ERROR" /* mediaError */,
|
|
1908
|
-
`Camera exposure check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1909
|
-
));
|
|
1910
|
-
}
|
|
1918
|
+
timer = setTimeout(() => finish(), CAMERA_WARMUP_MS);
|
|
1911
1919
|
});
|
|
1912
1920
|
}
|
|
1913
1921
|
|
|
@@ -2144,13 +2152,13 @@ var _CameraController = class _CameraController {
|
|
|
2144
2152
|
return this.activeTrack !== void 0 && this.storedUseMicrophone;
|
|
2145
2153
|
}
|
|
2146
2154
|
/**
|
|
2147
|
-
*
|
|
2155
|
+
* 当前活动相机流是否启用发布前预热等待;没有活动流时为 false。
|
|
2148
2156
|
*/
|
|
2149
2157
|
get isFrameValidationEnabled() {
|
|
2150
2158
|
return this.activeTrack !== void 0 && this.storedFrameValidationEnabled;
|
|
2151
2159
|
}
|
|
2152
2160
|
/**
|
|
2153
|
-
*
|
|
2161
|
+
* 等待当前轨道预热完成(固定 200ms);禁用时跳过,取消或轨道被替换时拒绝。
|
|
2154
2162
|
*/
|
|
2155
2163
|
async waitForValidCameraFrame(signal) {
|
|
2156
2164
|
const track = this.activeTrack;
|
|
@@ -2160,7 +2168,7 @@ var _CameraController = class _CameraController {
|
|
|
2160
2168
|
if (!this.isFrameValidationEnabled) return;
|
|
2161
2169
|
await waitForValidCameraFrame(mediaTrack, signal);
|
|
2162
2170
|
if (this.activeTrack !== track || track.mediaStreamTrack !== mediaTrack) {
|
|
2163
|
-
throw new XmaxError("CANCELLED" /* cancelled */, "Camera track changed during
|
|
2171
|
+
throw new XmaxError("CANCELLED" /* cancelled */, "Camera track changed during warmup");
|
|
2164
2172
|
}
|
|
2165
2173
|
}
|
|
2166
2174
|
/**
|
|
@@ -2179,7 +2187,7 @@ var _CameraController = class _CameraController {
|
|
|
2179
2187
|
* @param options.videoFormat 期望的输出尺寸、帧率和编码配置;尺寸按模型规则调整。
|
|
2180
2188
|
* @param options.position 首次启动时使用的摄像头位置。
|
|
2181
2189
|
* @param options.useMicrophone 是否申请麦克风权限并允许实时连接时启动音频采集。
|
|
2182
|
-
* @param options.enableFrameValidation
|
|
2190
|
+
* @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
|
|
2183
2191
|
* @returns 包含本地相机视频轨道的媒体流。
|
|
2184
2192
|
* @throws 已有活动相机流、格式无效、权限不足或采集启动失败时抛出错误。
|
|
2185
2193
|
*/
|
|
@@ -2528,12 +2536,13 @@ var RealtimeSessionService = class _RealtimeSessionService {
|
|
|
2528
2536
|
this.heartbeatVersion += 1;
|
|
2529
2537
|
}
|
|
2530
2538
|
/**
|
|
2531
|
-
*
|
|
2539
|
+
* 关闭指定实时会话;请求带 keepalive,页面卸载后仍能发出。
|
|
2532
2540
|
*/
|
|
2533
2541
|
async closeSession(sessionID) {
|
|
2534
2542
|
try {
|
|
2535
2543
|
await this.apiService.delete(
|
|
2536
|
-
`/session/${sessionID}
|
|
2544
|
+
`/session/${sessionID}`,
|
|
2545
|
+
{ keepalive: true }
|
|
2537
2546
|
);
|
|
2538
2547
|
} catch (error) {
|
|
2539
2548
|
throw XmaxError.from(error);
|
|
@@ -3993,14 +4002,23 @@ var StreamController = class {
|
|
|
3993
4002
|
}
|
|
3994
4003
|
}
|
|
3995
4004
|
/**
|
|
3996
|
-
*
|
|
4005
|
+
* 订阅远端音频并按平台能力应用当前音量。
|
|
4006
|
+
*
|
|
4007
|
+
* 支持音量控制的平台先设音量再订阅,避免以默认音量短暂出声;
|
|
4008
|
+
* iOS 上音量降级为播放/静音二值控制,必须先完成订阅(恢复播放)
|
|
4009
|
+
* 再按需静音,否则订阅的恢复播放会覆盖音量 0 的静音状态。
|
|
3997
4010
|
*/
|
|
3998
4011
|
async subscribeRemoteAudio(userID) {
|
|
3999
4012
|
if (this.state.subscribedRemoteAudioUserIDs.has(userID)) {
|
|
4000
4013
|
return;
|
|
4001
4014
|
}
|
|
4002
|
-
this.rtcManager.
|
|
4003
|
-
|
|
4015
|
+
if (this.rtcManager.supportsRemoteAudioVolumeControl) {
|
|
4016
|
+
this.rtcManager.setRemoteAudioVolume(this.remoteAudioVolumePercentage, userID);
|
|
4017
|
+
await this.rtcManager.subscribeRemoteAudio(userID, true);
|
|
4018
|
+
} else {
|
|
4019
|
+
await this.rtcManager.subscribeRemoteAudio(userID, true);
|
|
4020
|
+
this.rtcManager.setRemoteAudioVolume(this.remoteAudioVolumePercentage, userID);
|
|
4021
|
+
}
|
|
4004
4022
|
this.state.subscribedRemoteAudioUserIDs.add(userID);
|
|
4005
4023
|
}
|
|
4006
4024
|
/**
|
|
@@ -4588,17 +4606,25 @@ var RealtimeLaunchTimer = class {
|
|
|
4588
4606
|
this.notify();
|
|
4589
4607
|
return complete;
|
|
4590
4608
|
}
|
|
4609
|
+
/**
|
|
4610
|
+
* 开始等待用户授权系统媒体权限,返回授权决定时调用的记录函数。
|
|
4611
|
+
* 仅在系统弹出授权请求时使用;已授权或未弹窗不记录该阶段。
|
|
4612
|
+
*/
|
|
4613
|
+
startPermission() {
|
|
4614
|
+
return this.measure("permissionMs", performance.now());
|
|
4615
|
+
}
|
|
4591
4616
|
/**
|
|
4592
4617
|
* 开始会话与进房计时,返回连接阶段完成时调用的记录函数。
|
|
4618
|
+
* 相机预热等待计入连接耗时。
|
|
4593
4619
|
*/
|
|
4594
4620
|
startConnection() {
|
|
4595
4621
|
return this.measure("connectionMs", performance.now());
|
|
4596
4622
|
}
|
|
4597
4623
|
/**
|
|
4598
|
-
*
|
|
4624
|
+
* 开始发布本地流计时,返回发布完成时调用的记录函数。
|
|
4599
4625
|
*/
|
|
4600
|
-
|
|
4601
|
-
return this.measure("
|
|
4626
|
+
startPublish() {
|
|
4627
|
+
return this.measure("publishMs", performance.now());
|
|
4602
4628
|
}
|
|
4603
4629
|
/**
|
|
4604
4630
|
* 开始等待远端显示首帧;完成回调同时记录首帧耗时和实际总耗时。
|
|
@@ -4674,7 +4700,8 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4674
4700
|
}
|
|
4675
4701
|
/**
|
|
4676
4702
|
* 创建会话、配置编码、进房并发布本地媒体,启动心跳后返回远端占位流。
|
|
4677
|
-
* 进房结束即记录连接耗时;可选 beforePublish
|
|
4703
|
+
* 进房结束即记录连接耗时;可选 beforePublish 检查阻塞发布,计入连接耗时;
|
|
4704
|
+
* 发布本地流单独计时。
|
|
4678
4705
|
* @throws 会话、进房、发布失败或操作租约已失效时抛出错误,由上层统一清理。
|
|
4679
4706
|
*/
|
|
4680
4707
|
async connect(options) {
|
|
@@ -4701,15 +4728,17 @@ var XmaxRealtimeConnectionManager = class {
|
|
|
4701
4728
|
}
|
|
4702
4729
|
await streamController.setVideoEncoderConfig(videoFormat);
|
|
4703
4730
|
options.ensureCurrent();
|
|
4731
|
+
let completePublish;
|
|
4704
4732
|
await streamController.connect(connection, options.includeLocalAudio, () => {
|
|
4705
4733
|
options.ensureCurrent();
|
|
4706
4734
|
}, async () => {
|
|
4707
|
-
options.ensureCurrent();
|
|
4708
|
-
completeConnection();
|
|
4709
4735
|
options.ensureCurrent();
|
|
4710
4736
|
await options.beforePublish?.();
|
|
4737
|
+
completeConnection();
|
|
4738
|
+
completePublish = this.dependencies.timing.startPublish();
|
|
4711
4739
|
});
|
|
4712
4740
|
options.ensureCurrent();
|
|
4741
|
+
completePublish?.();
|
|
4713
4742
|
options.onPublished();
|
|
4714
4743
|
sessionService.startHeartbeat(session.id, {
|
|
4715
4744
|
onFailure: (sessionID, error) => {
|
|
@@ -5098,6 +5127,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5098
5127
|
*/
|
|
5099
5128
|
__publicField(this, "launchTimer", new RealtimeLaunchTimer());
|
|
5100
5129
|
__publicField(this, "remoteFrameDisplayHandler");
|
|
5130
|
+
/**
|
|
5131
|
+
* 媒体权限弹窗观察
|
|
5132
|
+
*/
|
|
5133
|
+
__publicField(this, "permissionWatchDispose");
|
|
5101
5134
|
/**
|
|
5102
5135
|
* 本地、远端和网络统计快照及观察者
|
|
5103
5136
|
*/
|
|
@@ -5416,7 +5449,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5416
5449
|
*
|
|
5417
5450
|
* 将返回的轨道绑定到预览视图;收到有效帧且视图已绑定后进入 `ready`。
|
|
5418
5451
|
*
|
|
5419
|
-
* @param options.enableFrameValidation
|
|
5452
|
+
* @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
|
|
5420
5453
|
* @returns 包含本地相机视频轨道的媒体流。
|
|
5421
5454
|
* @throws 模型不支持相机输入、配置无效、权限或采集启动失败时抛出错误。
|
|
5422
5455
|
*/
|
|
@@ -5434,11 +5467,20 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5434
5467
|
token.ensureCurrent();
|
|
5435
5468
|
const completeCamera = this.launchTimer.startCamera();
|
|
5436
5469
|
this.clearVideoStatistics();
|
|
5470
|
+
this.disposePermissionWatch();
|
|
5471
|
+
const permissionWatch = await watchMediaPermissionPrompt(options?.useMicrophone ?? false);
|
|
5472
|
+
if (permissionWatch) {
|
|
5473
|
+
const completePermission = this.launchTimer.startPermission();
|
|
5474
|
+
void permissionWatch.decided.then(() => completePermission()).catch(() => {
|
|
5475
|
+
});
|
|
5476
|
+
this.permissionWatchDispose = permissionWatch.dispose;
|
|
5477
|
+
}
|
|
5437
5478
|
let stream;
|
|
5438
5479
|
try {
|
|
5439
5480
|
token.ensureCurrent();
|
|
5440
5481
|
stream = await this.cameraController.createLocalCameraStream(options);
|
|
5441
5482
|
} catch (error) {
|
|
5483
|
+
this.disposePermissionWatch();
|
|
5442
5484
|
this.launchTimer.cancel();
|
|
5443
5485
|
throw error;
|
|
5444
5486
|
}
|
|
@@ -5451,6 +5493,13 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5451
5493
|
}
|
|
5452
5494
|
);
|
|
5453
5495
|
}
|
|
5496
|
+
/**
|
|
5497
|
+
* 移除媒体权限弹窗观察;授权决定已记录或不再需要观察时调用。
|
|
5498
|
+
*/
|
|
5499
|
+
disposePermissionWatch() {
|
|
5500
|
+
this.permissionWatchDispose?.();
|
|
5501
|
+
this.permissionWatchDispose = void 0;
|
|
5502
|
+
}
|
|
5454
5503
|
/**
|
|
5455
5504
|
* 停止本地相机流并释放本地预览与 RTC 资源。
|
|
5456
5505
|
*/
|
|
@@ -5461,6 +5510,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5461
5510
|
async (token) => {
|
|
5462
5511
|
this.launchTimer.cancel();
|
|
5463
5512
|
this.clearVideoStatistics();
|
|
5513
|
+
this.disposePermissionWatch();
|
|
5464
5514
|
await this.cameraController.stopLocalCameraStream();
|
|
5465
5515
|
token.ensureCurrent();
|
|
5466
5516
|
await this.coordinator.commit(
|
|
@@ -5494,8 +5544,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5494
5544
|
* 使用当前 Manager 创建的本地流建立实时连接。
|
|
5495
5545
|
*
|
|
5496
5546
|
* 创建实时会话、加入 RTC 房间并发布本地流,成功后启动会话心跳。
|
|
5497
|
-
*
|
|
5498
|
-
* 记录警告并按当前画面继续发布,不因环境较暗而阻断连接。
|
|
5547
|
+
* 启用帧检测时,发布前并行等待相机预热(固定 200ms),避免把黑帧推给 RTC。
|
|
5499
5548
|
* 返回的远端媒体流在生成开始后承载远端生成画面。
|
|
5500
5549
|
*
|
|
5501
5550
|
* @param localStream 由 `createLocalCameraStream` 创建的本地媒体流。
|
|
@@ -5604,17 +5653,15 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5604
5653
|
new RealtimeState({ connectionState: "Connecting" /* connecting */ }),
|
|
5605
5654
|
token
|
|
5606
5655
|
);
|
|
5607
|
-
const
|
|
5608
|
-
const
|
|
5609
|
-
token.signal.addEventListener("abort",
|
|
5610
|
-
if (token.signal.aborted)
|
|
5656
|
+
const warmupController = new AbortController();
|
|
5657
|
+
const abortWarmup = () => warmupController.abort();
|
|
5658
|
+
token.signal.addEventListener("abort", abortWarmup, { once: true });
|
|
5659
|
+
if (token.signal.aborted) abortWarmup();
|
|
5611
5660
|
try {
|
|
5612
5661
|
let beforePublish;
|
|
5613
5662
|
if (this.cameraController.isFrameValidationEnabled) {
|
|
5614
|
-
const
|
|
5615
|
-
const cameraReady = this.cameraController.waitForValidCameraFrame(exposureController.signal).then(() => {
|
|
5663
|
+
const cameraReady = this.cameraController.waitForValidCameraFrame(warmupController.signal).then(() => {
|
|
5616
5664
|
token.ensureCurrent();
|
|
5617
|
-
completeFrameValidation();
|
|
5618
5665
|
});
|
|
5619
5666
|
void cameraReady.catch(() => {
|
|
5620
5667
|
});
|
|
@@ -5639,8 +5686,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5639
5686
|
);
|
|
5640
5687
|
return remote;
|
|
5641
5688
|
} finally {
|
|
5642
|
-
token.signal.removeEventListener("abort",
|
|
5643
|
-
|
|
5689
|
+
token.signal.removeEventListener("abort", abortWarmup);
|
|
5690
|
+
abortWarmup();
|
|
5644
5691
|
}
|
|
5645
5692
|
}
|
|
5646
5693
|
/**
|
|
@@ -5697,6 +5744,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
|
|
|
5697
5744
|
await this.generationManager.reset(taskID);
|
|
5698
5745
|
const sessionID = await this.connectionManager.disconnect();
|
|
5699
5746
|
if (scope === 1 /* all */) {
|
|
5747
|
+
this.disposePermissionWatch();
|
|
5700
5748
|
try {
|
|
5701
5749
|
await this.cameraController.stopLocalCameraStream();
|
|
5702
5750
|
} catch (error) {
|
|
@@ -8715,4 +8763,4 @@ export {
|
|
|
8715
8763
|
XmaxVideoView,
|
|
8716
8764
|
XmaxRealtimeVideoView
|
|
8717
8765
|
};
|
|
8718
|
-
//# sourceMappingURL=chunk-
|
|
8766
|
+
//# sourceMappingURL=chunk-YMLRUDAO.js.map
|