@xmaxai/web-sdk 1.0.1 → 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.
@@ -9,10 +9,7 @@ var XmaxEnvironment = /* @__PURE__ */ ((XmaxEnvironment2) => {
9
9
  return XmaxEnvironment2;
10
10
  })(XmaxEnvironment || {});
11
11
  var API_BASE_URLS = {
12
- /**
13
- * 临时联调环境。
14
- */
15
- ["china" /* china */]: "https://dev.xmaxai.com/open/api/v1",
12
+ ["china" /* china */]: "https://cloud.xmax.22duck.cn/open/api/v1",
16
13
  ["global" /* global */]: "https://api.xmax.cloud/open/api/v1"
17
14
  };
18
15
  function apiBaseURL(environment) {
@@ -201,7 +198,7 @@ var XmaxError = class _XmaxError extends Error {
201
198
  };
202
199
 
203
200
  // src/Foundation/Runtime/RuntimeInfo.ts
204
- var XMAX_SDK_VERSION = "1.0.1";
201
+ var XMAX_SDK_VERSION = "1.0.3";
205
202
  var _RuntimeInfo = class _RuntimeInfo {
206
203
  /**
207
204
  * 序列化为接口传输使用的 snake_case 字段。
@@ -266,10 +263,12 @@ var _ApiService = class _ApiService {
266
263
  }
267
264
  /**
268
265
  * 发送请求并返回统一响应中的数据。
266
+ *
267
+ * @param options.keepalive 为 true 时请求可存活于页面卸载之后。
269
268
  */
270
- async request(method, path, body) {
269
+ async request(method, path, body, options) {
271
270
  this.validateConfiguration();
272
- const request = this.makeRequest(method, path, body);
271
+ const request = this.makeRequest(method, path, body, options);
273
272
  const startedAt = Date.now();
274
273
  let status;
275
274
  let text;
@@ -316,8 +315,8 @@ var _ApiService = class _ApiService {
316
315
  /**
317
316
  * 发送 DELETE 请求。
318
317
  */
319
- delete(path) {
320
- return this.request("DELETE" /* delete */, path);
318
+ delete(path, options) {
319
+ return this.request("DELETE" /* delete */, path, void 0, options);
321
320
  }
322
321
  /**
323
322
  * 校验 API Key 与 Base URL 配置。
@@ -347,7 +346,7 @@ var _ApiService = class _ApiService {
347
346
  /**
348
347
  * 构造请求地址、请求头与请求体;路径必须相对且与 Base URL 同源。
349
348
  */
350
- makeRequest(method, path, body) {
349
+ makeRequest(method, path, body, options) {
351
350
  const normalizedPath = path.trim();
352
351
  if (normalizedPath.length === 0 || normalizedPath.includes("://") || normalizedPath.startsWith("//")) {
353
352
  throw new XmaxError(
@@ -388,7 +387,8 @@ var _ApiService = class _ApiService {
388
387
  "X-SDK-Version": XMAX_SDK_VERSION
389
388
  },
390
389
  body: encodedBody,
391
- signal: AbortSignal.timeout(this.timeoutMs)
390
+ signal: AbortSignal.timeout(this.timeoutMs),
391
+ ...options?.keepalive ? { keepalive: true } : {}
392
392
  }
393
393
  };
394
394
  }
@@ -565,7 +565,7 @@ var RealtimeVideoFormat = class _RealtimeVideoFormat {
565
565
  var RealtimeModel = /* @__PURE__ */ ((RealtimeModel2) => {
566
566
  RealtimeModel2["x2_0"] = "x2.0";
567
567
  RealtimeModel2["x2_0_pro"] = "x2.0-pro";
568
- RealtimeModel2["x2_fast_1080p"] = "x2-fast-1080p";
568
+ RealtimeModel2["x2_0_trtc"] = "x2.0-trtc";
569
569
  return RealtimeModel2;
570
570
  })(RealtimeModel || {});
571
571
  var RESOLUTION_BUCKETS = {
@@ -574,7 +574,7 @@ var RESOLUTION_BUCKETS = {
574
574
  { width: 1024, height: 1920 },
575
575
  { width: 1920, height: 1024 }
576
576
  ],
577
- ["x2-fast-1080p" /* x2_fast_1080p */]: [
577
+ ["x2.0-trtc" /* x2_0_trtc */]: [
578
578
  { width: 1024, height: 1920 },
579
579
  { width: 1920, height: 1024 }
580
580
  ]
@@ -582,7 +582,7 @@ var RESOLUTION_BUCKETS = {
582
582
  var MAXIMUM_INPUT_PIXELS = {
583
583
  ["x2.0" /* x2_0 */]: 128e4,
584
584
  ["x2.0-pro" /* x2_0_pro */]: 21e5,
585
- ["x2-fast-1080p" /* x2_fast_1080p */]: 21e5
585
+ ["x2.0-trtc" /* x2_0_trtc */]: 21e5
586
586
  };
587
587
  function resolutionBuckets(model) {
588
588
  return RESOLUTION_BUCKETS[model];
@@ -625,9 +625,9 @@ var MediaService = class {
625
625
  /**
626
626
  * 创建媒体服务。
627
627
  *
628
- * @param model 媒体输入规则使用的模型,默认为 `x2-fast-1080p`。
628
+ * @param model 媒体输入规则使用的模型,默认为 `x2.0-trtc`。
629
629
  */
630
- constructor(model = "x2-fast-1080p" /* x2_fast_1080p */) {
630
+ constructor(model = "x2.0-trtc" /* x2_0_trtc */) {
631
631
  /**
632
632
  * 模型约束
633
633
  */
@@ -1009,6 +1009,68 @@ var CameraPosition = /* @__PURE__ */ ((CameraPosition3) => {
1009
1009
  return CameraPosition3;
1010
1010
  })(CameraPosition || {});
1011
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
+
1012
1074
  // src/Foundation/RTC/RtcEngineManager.ts
1013
1075
  var RtcEngineLease = class {
1014
1076
  /**
@@ -1242,6 +1304,33 @@ var RtcStatsLogger = class {
1242
1304
  }
1243
1305
  };
1244
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
+
1245
1334
  // src/Foundation/RTC/RtcManager.ts
1246
1335
  var RTC_EVENT = {
1247
1336
  remoteVideoAvailable: "remote-video-available",
@@ -1346,13 +1435,17 @@ var RtcManager = class {
1346
1435
  );
1347
1436
  }
1348
1437
  try {
1438
+ const captureSize = rtcOrientedVideoSize({
1439
+ width: options.width,
1440
+ height: options.height
1441
+ });
1349
1442
  await engine.startLocalVideo({
1350
1443
  publish: false,
1351
1444
  option: {
1352
1445
  useFrontCamera: options.position === "front" /* front */,
1353
1446
  profile: {
1354
- width: options.width,
1355
- height: options.height,
1447
+ width: captureSize.width,
1448
+ height: captureSize.height,
1356
1449
  frameRate: options.frameRate,
1357
1450
  bitrate: CAPTURE_PLACEHOLDER_BITRATE
1358
1451
  }
@@ -1472,7 +1565,7 @@ var RtcManager = class {
1472
1565
  /**
1473
1566
  * 配置本地视频编码参数。
1474
1567
  *
1475
- * TRTC 只接受单一目标码率,以 `maximumBitrate` 作为目标码率;
1568
+ * TRTC 只接受单一目标码率,以 `minimumBitrate`(流畅优先档)作为目标码率;
1476
1569
  * 编码策略偏好映射为 TRTC 弱网偏好。
1477
1570
  *
1478
1571
  * @throws 摄像头采集未启动或编码参数配置失败时抛出错误。
@@ -1486,13 +1579,17 @@ var RtcManager = class {
1486
1579
  );
1487
1580
  }
1488
1581
  try {
1582
+ const encodingSize = rtcOrientedVideoSize({
1583
+ width: configuration.width,
1584
+ height: configuration.height
1585
+ });
1489
1586
  await engine.updateLocalVideo({
1490
1587
  option: {
1491
1588
  profile: {
1492
- width: configuration.width,
1493
- height: configuration.height,
1589
+ width: encodingSize.width,
1590
+ height: encodingSize.height,
1494
1591
  frameRate: configuration.frameRate,
1495
- bitrate: configuration.maximumBitrate
1592
+ bitrate: configuration.minimumBitrate
1496
1593
  },
1497
1594
  qosPreference: QOS_PREFERENCE_MAP[configuration.encoderPreference]
1498
1595
  }
@@ -1610,14 +1707,34 @@ var RtcManager = class {
1610
1707
  throw this.mapError(error);
1611
1708
  }
1612
1709
  }
1710
+ /**
1711
+ * 当前平台是否支持远端音量控制(iOS Safari/WKWebView 不支持)。
1712
+ */
1713
+ get supportsRemoteAudioVolumeControl() {
1714
+ return canControlRemoteAudioVolume();
1715
+ }
1613
1716
  /**
1614
1717
  * 设置指定远端用户的音频播放音量。
1615
1718
  *
1719
+ * iOS Safari/WKWebView 不允许 JS 修改媒体元素音量(TRTC 官方标注
1720
+ * `setRemoteAudioVolume` 不支持 iOS Safari),该平台上降级为
1721
+ * 播放/静音二值控制:音量为 0 时停止播放该路音频,非 0 时恢复播放;
1722
+ * 恢复播放需要重新拉流,会有短暂缓冲。
1723
+ *
1616
1724
  * @param volume 音量,取值范围为 `0...100`,越界时收敛到边界。
1617
1725
  */
1618
1726
  setRemoteAudioVolume(volume, userID) {
1619
1727
  const engine = this.requireEngine();
1620
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
+ }
1621
1738
  engine.setRemoteAudioVolume(userID, normalized);
1622
1739
  }
1623
1740
  /**
@@ -1769,74 +1886,25 @@ var RtcManager = class {
1769
1886
  };
1770
1887
 
1771
1888
  // src/Foundation/Media/Camera/CameraFrameValidator.ts
1772
- var FALLBACK_POLL_INTERVAL_MS = 16;
1773
- var TIMEOUT_MS = 2e3;
1774
- var CameraFrameValidator = class {
1775
- /**
1776
- * 单帧中央区域亮度达标即放行,不等待连续帧或曝光稳定。
1777
- */
1778
- accept(pixels) {
1779
- const size = pixels.length / 4;
1780
- if (!size || !Number.isInteger(size)) return false;
1781
- const histogram = new Uint32Array(256);
1782
- let total = 0;
1783
- let dark = 0;
1784
- for (let i = 0; i < pixels.length; i += 4) {
1785
- const luminance = Math.round(0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2]);
1786
- histogram[luminance] = histogram[luminance] + 1;
1787
- total += luminance;
1788
- if (luminance < 12) dark++;
1789
- }
1790
- let median = 0;
1791
- let accumulated = 0;
1792
- for (; median < 255; median++) {
1793
- accumulated += histogram[median];
1794
- if (accumulated >= size / 2) break;
1795
- }
1796
- const mean = total / size;
1797
- return mean >= 24 && median >= 18 && dark / size <= 0.75;
1798
- }
1799
- };
1889
+ var CAMERA_WARMUP_MS = 200;
1800
1890
  function waitForValidCameraFrame(track, signal) {
1801
1891
  return new Promise((resolve, reject) => {
1802
- let video;
1803
- let timeout;
1804
- let poll;
1805
- let frameCallback;
1806
- let finished = false;
1807
- const started = performance.now();
1808
- const gate = new CameraFrameValidator();
1809
- const finish = (result = "valid") => {
1810
- if (finished) return;
1811
- finished = true;
1812
- clearTimeout(timeout);
1813
- clearTimeout(poll);
1814
- if (video) {
1815
- if (frameCallback !== void 0) video.cancelVideoFrameCallback(frameCallback);
1816
- video.pause();
1817
- video.srcObject = null;
1818
- video.remove();
1819
- }
1892
+ let timer;
1893
+ let settled = false;
1894
+ const finish = (error) => {
1895
+ if (settled) return;
1896
+ settled = true;
1897
+ clearTimeout(timer);
1820
1898
  signal.removeEventListener("abort", abort);
1821
1899
  track.removeEventListener("ended", ended);
1822
- if (result instanceof XmaxError) {
1823
- if (result.code !== "CANCELLED" /* cancelled */) XmaxLogger.media.warning(() => result.message);
1824
- reject(result);
1900
+ if (error) {
1901
+ reject(error);
1825
1902
  } else {
1826
- if (result === "timeout") {
1827
- XmaxLogger.media.warning(() => XmaxLogger.localized(
1828
- "\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",
1829
- "Camera Frame Validation Timed Out; Continuing\n\u2514\u2500 No sufficiently bright frame received; proceeding without further waiting."
1830
- ));
1831
- } else {
1832
- XmaxLogger.media.info(() => `\u76F8\u673A\u66DD\u5149\u68C0\u67E5\u901A\u8FC7 (Camera Exposure Ready)
1833
- \u2514\u2500 ${Math.round(performance.now() - started)} ms`);
1834
- }
1835
1903
  resolve();
1836
1904
  }
1837
1905
  };
1838
- const abort = () => finish(new XmaxError("CANCELLED" /* cancelled */, "Camera exposure check cancelled"));
1839
- const ended = () => finish(new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera track ended before exposure was ready"));
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"));
1840
1908
  signal.addEventListener("abort", abort, { once: true });
1841
1909
  track.addEventListener("ended", ended, { once: true });
1842
1910
  if (signal.aborted) {
@@ -1847,70 +1915,7 @@ function waitForValidCameraFrame(track, signal) {
1847
1915
  ended();
1848
1916
  return;
1849
1917
  }
1850
- try {
1851
- video = document.createElement("video");
1852
- video.muted = true;
1853
- video.autoplay = true;
1854
- video.playsInline = true;
1855
- video.setAttribute("aria-hidden", "true");
1856
- video.style.cssText = "position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;pointer-events:none;";
1857
- const canvas = document.createElement("canvas");
1858
- canvas.width = 64;
1859
- canvas.height = 48;
1860
- const context = canvas.getContext("2d", { willReadFrequently: true });
1861
- if (!context) throw new Error("Camera exposure sampling requires a 2D canvas");
1862
- video.srcObject = new MediaStream([track]);
1863
- document.body.appendChild(video);
1864
- const source = video;
1865
- let lastFrameTime = -Infinity;
1866
- const sample = (mediaTime) => {
1867
- if (source.readyState < 2 || !source.videoWidth || !source.videoHeight || track.muted || !track.enabled) {
1868
- return;
1869
- }
1870
- if (mediaTime === lastFrameTime) return;
1871
- lastFrameTime = mediaTime;
1872
- const width = source.videoWidth;
1873
- const height = source.videoHeight;
1874
- context.drawImage(source, width * 0.2, height * 0.2, width * 0.6, height * 0.6, 0, 0, 64, 48);
1875
- if (gate.accept(context.getImageData(0, 0, 64, 48).data)) finish();
1876
- };
1877
- const fail = (error) => finish(new XmaxError(
1878
- "MEDIA_ERROR" /* mediaError */,
1879
- `Camera exposure check failed: ${error instanceof Error ? error.message : String(error)}`
1880
- ));
1881
- const schedule = () => {
1882
- if (finished) return;
1883
- if (typeof source.requestVideoFrameCallback === "function") {
1884
- frameCallback = source.requestVideoFrameCallback((_now, metadata) => {
1885
- frameCallback = void 0;
1886
- try {
1887
- sample(metadata.mediaTime);
1888
- schedule();
1889
- } catch (error) {
1890
- fail(error);
1891
- }
1892
- });
1893
- } else {
1894
- poll = setTimeout(() => {
1895
- try {
1896
- const frameTime = typeof source.getVideoPlaybackQuality === "function" ? source.getVideoPlaybackQuality().totalVideoFrames : source.currentTime;
1897
- sample(frameTime);
1898
- schedule();
1899
- } catch (error) {
1900
- fail(error);
1901
- }
1902
- }, FALLBACK_POLL_INTERVAL_MS);
1903
- }
1904
- };
1905
- timeout = setTimeout(() => finish("timeout"), TIMEOUT_MS);
1906
- XmaxLogger.media.info(() => "\u7B49\u5F85\u9996\u5F20\u4EAE\u5EA6\u5408\u683C\u5E27 (Waiting for the First Sufficiently Bright Frame)");
1907
- void source.play().then(schedule, fail);
1908
- } catch (error) {
1909
- finish(new XmaxError(
1910
- "MEDIA_ERROR" /* mediaError */,
1911
- `Camera exposure check failed: ${error instanceof Error ? error.message : String(error)}`
1912
- ));
1913
- }
1918
+ timer = setTimeout(() => finish(), CAMERA_WARMUP_MS);
1914
1919
  });
1915
1920
  }
1916
1921
 
@@ -2147,13 +2152,13 @@ var _CameraController = class _CameraController {
2147
2152
  return this.activeTrack !== void 0 && this.storedUseMicrophone;
2148
2153
  }
2149
2154
  /**
2150
- * 当前活动相机流是否启用发布前亮度检测;没有活动流时为 false。
2155
+ * 当前活动相机流是否启用发布前预热等待;没有活动流时为 false。
2151
2156
  */
2152
2157
  get isFrameValidationEnabled() {
2153
2158
  return this.activeTrack !== void 0 && this.storedFrameValidationEnabled;
2154
2159
  }
2155
2160
  /**
2156
- * 等待当前轨道首张合格帧或超时放行;禁用时跳过,取消或轨道被替换时拒绝。
2161
+ * 等待当前轨道预热完成(固定 200ms);禁用时跳过,取消或轨道被替换时拒绝。
2157
2162
  */
2158
2163
  async waitForValidCameraFrame(signal) {
2159
2164
  const track = this.activeTrack;
@@ -2163,7 +2168,7 @@ var _CameraController = class _CameraController {
2163
2168
  if (!this.isFrameValidationEnabled) return;
2164
2169
  await waitForValidCameraFrame(mediaTrack, signal);
2165
2170
  if (this.activeTrack !== track || track.mediaStreamTrack !== mediaTrack) {
2166
- throw new XmaxError("CANCELLED" /* cancelled */, "Camera track changed during exposure check");
2171
+ throw new XmaxError("CANCELLED" /* cancelled */, "Camera track changed during warmup");
2167
2172
  }
2168
2173
  }
2169
2174
  /**
@@ -2182,7 +2187,7 @@ var _CameraController = class _CameraController {
2182
2187
  * @param options.videoFormat 期望的输出尺寸、帧率和编码配置;尺寸按模型规则调整。
2183
2188
  * @param options.position 首次启动时使用的摄像头位置。
2184
2189
  * @param options.useMicrophone 是否申请麦克风权限并允许实时连接时启动音频采集。
2185
- * @param options.enableFrameValidation 是否在发布前检测帧亮度,默认 true;false 跳过检测及其等待。
2190
+ * @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
2186
2191
  * @returns 包含本地相机视频轨道的媒体流。
2187
2192
  * @throws 已有活动相机流、格式无效、权限不足或采集启动失败时抛出错误。
2188
2193
  */
@@ -2531,12 +2536,13 @@ var RealtimeSessionService = class _RealtimeSessionService {
2531
2536
  this.heartbeatVersion += 1;
2532
2537
  }
2533
2538
  /**
2534
- * 关闭指定实时会话。
2539
+ * 关闭指定实时会话;请求带 keepalive,页面卸载后仍能发出。
2535
2540
  */
2536
2541
  async closeSession(sessionID) {
2537
2542
  try {
2538
2543
  await this.apiService.delete(
2539
- `/session/${sessionID}`
2544
+ `/session/${sessionID}`,
2545
+ { keepalive: true }
2540
2546
  );
2541
2547
  } catch (error) {
2542
2548
  throw XmaxError.from(error);
@@ -3996,14 +4002,23 @@ var StreamController = class {
3996
4002
  }
3997
4003
  }
3998
4004
  /**
3999
- * 订阅远端音频:先应用当前音量再订阅。
4005
+ * 订阅远端音频并按平台能力应用当前音量。
4006
+ *
4007
+ * 支持音量控制的平台先设音量再订阅,避免以默认音量短暂出声;
4008
+ * iOS 上音量降级为播放/静音二值控制,必须先完成订阅(恢复播放)
4009
+ * 再按需静音,否则订阅的恢复播放会覆盖音量 0 的静音状态。
4000
4010
  */
4001
4011
  async subscribeRemoteAudio(userID) {
4002
4012
  if (this.state.subscribedRemoteAudioUserIDs.has(userID)) {
4003
4013
  return;
4004
4014
  }
4005
- this.rtcManager.setRemoteAudioVolume(this.remoteAudioVolumePercentage, userID);
4006
- await this.rtcManager.subscribeRemoteAudio(userID, true);
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
+ }
4007
4022
  this.state.subscribedRemoteAudioUserIDs.add(userID);
4008
4023
  }
4009
4024
  /**
@@ -4591,17 +4606,25 @@ var RealtimeLaunchTimer = class {
4591
4606
  this.notify();
4592
4607
  return complete;
4593
4608
  }
4609
+ /**
4610
+ * 开始等待用户授权系统媒体权限,返回授权决定时调用的记录函数。
4611
+ * 仅在系统弹出授权请求时使用;已授权或未弹窗不记录该阶段。
4612
+ */
4613
+ startPermission() {
4614
+ return this.measure("permissionMs", performance.now());
4615
+ }
4594
4616
  /**
4595
4617
  * 开始会话与进房计时,返回连接阶段完成时调用的记录函数。
4618
+ * 相机预热等待计入连接耗时。
4596
4619
  */
4597
4620
  startConnection() {
4598
4621
  return this.measure("connectionMs", performance.now());
4599
4622
  }
4600
4623
  /**
4601
- * 开始亮度检测计时;合格帧到达或超时放行时调用返回的记录函数。
4624
+ * 开始发布本地流计时,返回发布完成时调用的记录函数。
4602
4625
  */
4603
- startFrameValidation() {
4604
- return this.measure("frameValidationMs", performance.now());
4626
+ startPublish() {
4627
+ return this.measure("publishMs", performance.now());
4605
4628
  }
4606
4629
  /**
4607
4630
  * 开始等待远端显示首帧;完成回调同时记录首帧耗时和实际总耗时。
@@ -4677,7 +4700,8 @@ var XmaxRealtimeConnectionManager = class {
4677
4700
  }
4678
4701
  /**
4679
4702
  * 创建会话、配置编码、进房并发布本地媒体,启动心跳后返回远端占位流。
4680
- * 进房结束即记录连接耗时;可选 beforePublish 检查只阻塞发布,不计入连接耗时。
4703
+ * 进房结束即记录连接耗时;可选 beforePublish 检查阻塞发布,计入连接耗时;
4704
+ * 发布本地流单独计时。
4681
4705
  * @throws 会话、进房、发布失败或操作租约已失效时抛出错误,由上层统一清理。
4682
4706
  */
4683
4707
  async connect(options) {
@@ -4704,15 +4728,17 @@ var XmaxRealtimeConnectionManager = class {
4704
4728
  }
4705
4729
  await streamController.setVideoEncoderConfig(videoFormat);
4706
4730
  options.ensureCurrent();
4731
+ let completePublish;
4707
4732
  await streamController.connect(connection, options.includeLocalAudio, () => {
4708
4733
  options.ensureCurrent();
4709
4734
  }, async () => {
4710
- options.ensureCurrent();
4711
- completeConnection();
4712
4735
  options.ensureCurrent();
4713
4736
  await options.beforePublish?.();
4737
+ completeConnection();
4738
+ completePublish = this.dependencies.timing.startPublish();
4714
4739
  });
4715
4740
  options.ensureCurrent();
4741
+ completePublish?.();
4716
4742
  options.onPublished();
4717
4743
  sessionService.startHeartbeat(session.id, {
4718
4744
  onFailure: (sessionID, error) => {
@@ -5101,6 +5127,10 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5101
5127
  */
5102
5128
  __publicField(this, "launchTimer", new RealtimeLaunchTimer());
5103
5129
  __publicField(this, "remoteFrameDisplayHandler");
5130
+ /**
5131
+ * 媒体权限弹窗观察
5132
+ */
5133
+ __publicField(this, "permissionWatchDispose");
5104
5134
  /**
5105
5135
  * 本地、远端和网络统计快照及观察者
5106
5136
  */
@@ -5419,7 +5449,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5419
5449
  *
5420
5450
  * 将返回的轨道绑定到预览视图;收到有效帧且视图已绑定后进入 `ready`。
5421
5451
  *
5422
- * @param options.enableFrameValidation 是否在发布前检测帧亮度,默认 true;false 跳过检测及其等待。
5452
+ * @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
5423
5453
  * @returns 包含本地相机视频轨道的媒体流。
5424
5454
  * @throws 模型不支持相机输入、配置无效、权限或采集启动失败时抛出错误。
5425
5455
  */
@@ -5437,11 +5467,20 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5437
5467
  token.ensureCurrent();
5438
5468
  const completeCamera = this.launchTimer.startCamera();
5439
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
+ }
5440
5478
  let stream;
5441
5479
  try {
5442
5480
  token.ensureCurrent();
5443
5481
  stream = await this.cameraController.createLocalCameraStream(options);
5444
5482
  } catch (error) {
5483
+ this.disposePermissionWatch();
5445
5484
  this.launchTimer.cancel();
5446
5485
  throw error;
5447
5486
  }
@@ -5454,6 +5493,13 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5454
5493
  }
5455
5494
  );
5456
5495
  }
5496
+ /**
5497
+ * 移除媒体权限弹窗观察;授权决定已记录或不再需要观察时调用。
5498
+ */
5499
+ disposePermissionWatch() {
5500
+ this.permissionWatchDispose?.();
5501
+ this.permissionWatchDispose = void 0;
5502
+ }
5457
5503
  /**
5458
5504
  * 停止本地相机流并释放本地预览与 RTC 资源。
5459
5505
  */
@@ -5464,6 +5510,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5464
5510
  async (token) => {
5465
5511
  this.launchTimer.cancel();
5466
5512
  this.clearVideoStatistics();
5513
+ this.disposePermissionWatch();
5467
5514
  await this.cameraController.stopLocalCameraStream();
5468
5515
  token.ensureCurrent();
5469
5516
  await this.coordinator.commit(
@@ -5497,8 +5544,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5497
5544
  * 使用当前 Manager 创建的本地流建立实时连接。
5498
5545
  *
5499
5546
  * 创建实时会话、加入 RTC 房间并发布本地流,成功后启动会话心跳。
5500
- * 启用帧检测时,发布前并行检查相机亮度,首张合格帧即放行;2 秒内未通过检查则
5501
- * 记录警告并按当前画面继续发布,不因环境较暗而阻断连接。
5547
+ * 启用帧检测时,发布前并行等待相机预热(固定 200ms),避免把黑帧推给 RTC。
5502
5548
  * 返回的远端媒体流在生成开始后承载远端生成画面。
5503
5549
  *
5504
5550
  * @param localStream 由 `createLocalCameraStream` 创建的本地媒体流。
@@ -5607,17 +5653,15 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5607
5653
  new RealtimeState({ connectionState: "Connecting" /* connecting */ }),
5608
5654
  token
5609
5655
  );
5610
- const exposureController = new AbortController();
5611
- const abortExposure = () => exposureController.abort();
5612
- token.signal.addEventListener("abort", abortExposure, { once: true });
5613
- if (token.signal.aborted) abortExposure();
5656
+ const warmupController = new AbortController();
5657
+ const abortWarmup = () => warmupController.abort();
5658
+ token.signal.addEventListener("abort", abortWarmup, { once: true });
5659
+ if (token.signal.aborted) abortWarmup();
5614
5660
  try {
5615
5661
  let beforePublish;
5616
5662
  if (this.cameraController.isFrameValidationEnabled) {
5617
- const completeFrameValidation = this.launchTimer.startFrameValidation();
5618
- const cameraReady = this.cameraController.waitForValidCameraFrame(exposureController.signal).then(() => {
5663
+ const cameraReady = this.cameraController.waitForValidCameraFrame(warmupController.signal).then(() => {
5619
5664
  token.ensureCurrent();
5620
- completeFrameValidation();
5621
5665
  });
5622
5666
  void cameraReady.catch(() => {
5623
5667
  });
@@ -5642,8 +5686,8 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5642
5686
  );
5643
5687
  return remote;
5644
5688
  } finally {
5645
- token.signal.removeEventListener("abort", abortExposure);
5646
- abortExposure();
5689
+ token.signal.removeEventListener("abort", abortWarmup);
5690
+ abortWarmup();
5647
5691
  }
5648
5692
  }
5649
5693
  /**
@@ -5700,6 +5744,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
5700
5744
  await this.generationManager.reset(taskID);
5701
5745
  const sessionID = await this.connectionManager.disconnect();
5702
5746
  if (scope === 1 /* all */) {
5747
+ this.disposePermissionWatch();
5703
5748
  try {
5704
5749
  await this.cameraController.stopLocalCameraStream();
5705
5750
  } catch (error) {
@@ -5815,9 +5860,9 @@ var XmaxClient = class {
5815
5860
  /**
5816
5861
  * 创建媒体处理与能力查询 Service。
5817
5862
  *
5818
- * @param model 媒体输入规则使用的模型,默认为 `x2-fast-1080p`。
5863
+ * @param model 媒体输入规则使用的模型,默认为 `x2.0-trtc`。
5819
5864
  */
5820
- createMediaService(model = "x2-fast-1080p" /* x2_fast_1080p */) {
5865
+ createMediaService(model = "x2.0-trtc" /* x2_0_trtc */) {
5821
5866
  return new MediaService(model);
5822
5867
  }
5823
5868
  /**
@@ -8718,4 +8763,4 @@ export {
8718
8763
  XmaxVideoView,
8719
8764
  XmaxRealtimeVideoView
8720
8765
  };
8721
- //# sourceMappingURL=chunk-NW6YF4JT.js.map
8766
+ //# sourceMappingURL=chunk-YMLRUDAO.js.map