@xmaxai/web-sdk 1.0.6 → 1.0.7

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.
@@ -205,7 +205,7 @@ var XmaxError = class _XmaxError extends Error {
205
205
  };
206
206
 
207
207
  // src/Foundation/Runtime/RuntimeInfo.ts
208
- var XMAX_SDK_VERSION = "1.0.6";
208
+ var XMAX_SDK_VERSION = "1.0.7";
209
209
  var _RuntimeInfo = class _RuntimeInfo {
210
210
  /**
211
211
  * 序列化为接口传输使用的 snake_case 字段。
@@ -1365,6 +1365,178 @@ ${JSON.stringify({
1365
1365
  }
1366
1366
  };
1367
1367
 
1368
+ // src/Foundation/Media/Camera/CameraRotation.ts
1369
+ function validateCameraRotation(rotation) {
1370
+ if (![0, 90, 180, 270].includes(rotation)) {
1371
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera rotation must be 0, 90, 180 or 270");
1372
+ }
1373
+ }
1374
+
1375
+ // src/Foundation/Media/Camera/CameraRotationProcessor.ts
1376
+ var CameraRotationProcessor = class {
1377
+ constructor(options) {
1378
+ __publicField(this, "options", options);
1379
+ __publicField(this, "video", document.createElement("video"));
1380
+ __publicField(this, "canvas", document.createElement("canvas"));
1381
+ __publicField(this, "context");
1382
+ __publicField(this, "stream");
1383
+ __publicField(this, "source");
1384
+ __publicField(this, "frameCallback");
1385
+ __publicField(this, "timer");
1386
+ __publicField(this, "cancelPending");
1387
+ __publicField(this, "stopped", false);
1388
+ __publicField(this, "version", 0);
1389
+ __publicField(this, "sourceEnded", () => {
1390
+ this.fail(new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera source ended during rotation"));
1391
+ });
1392
+ this.canvas.width = options.width;
1393
+ this.canvas.height = options.height;
1394
+ const context = this.canvas.getContext("2d", { alpha: false });
1395
+ if (!context || typeof this.canvas.captureStream !== "function") {
1396
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Canvas camera rotation is not supported by this browser");
1397
+ }
1398
+ this.context = context;
1399
+ this.video.muted = true;
1400
+ this.video.autoplay = true;
1401
+ this.video.playsInline = true;
1402
+ }
1403
+ /** 等待第一帧实际绘制成功;换摄像头时复用输出轨,不重建 sender。 */
1404
+ async setSource(source) {
1405
+ if (this.stopped) throw new XmaxError("CANCELLED" /* cancelled */, "Camera rotation has stopped");
1406
+ if (source.readyState === "ended") {
1407
+ this.stop();
1408
+ throw new XmaxError("MEDIA_ERROR" /* mediaError */, "Camera rotation requires a live source");
1409
+ }
1410
+ this.cancelPending?.();
1411
+ this.cancelFrame();
1412
+ this.source?.removeEventListener("ended", this.sourceEnded);
1413
+ const version = ++this.version;
1414
+ this.source = source;
1415
+ source.addEventListener("ended", this.sourceEnded);
1416
+ this.video.srcObject = new MediaStream([source]);
1417
+ try {
1418
+ await new Promise((resolve, reject) => {
1419
+ let settled = false;
1420
+ const finish = (error) => {
1421
+ if (settled) return;
1422
+ settled = true;
1423
+ clearTimeout(timeout);
1424
+ this.cancelPending = void 0;
1425
+ if (error) reject(error);
1426
+ else resolve();
1427
+ };
1428
+ const timeout = setTimeout(() => finish(new XmaxError("TIMEOUT" /* timeout */, "Timed out waiting for a camera rotation frame")), 1e4);
1429
+ this.cancelPending = () => finish(new XmaxError("CANCELLED" /* cancelled */, "Camera rotation source changed"));
1430
+ void this.video.play().then(() => {
1431
+ if (settled || this.stopped || version !== this.version) return;
1432
+ const firstFrame = () => {
1433
+ if (settled || this.stopped || version !== this.version) return;
1434
+ try {
1435
+ if (!this.draw()) {
1436
+ this.schedule(firstFrame);
1437
+ return;
1438
+ }
1439
+ this.stream ?? (this.stream = this.canvas.captureStream(this.options.frameRate));
1440
+ if (!this.stream.getVideoTracks()[0]) throw new Error("Canvas did not produce a video track");
1441
+ this.requestFrame();
1442
+ finish();
1443
+ this.schedule(() => this.render(version));
1444
+ } catch (error) {
1445
+ finish(error);
1446
+ }
1447
+ };
1448
+ this.schedule(firstFrame);
1449
+ }, finish);
1450
+ });
1451
+ if (this.stopped || version !== this.version) {
1452
+ throw new XmaxError("CANCELLED" /* cancelled */, "Camera rotation source changed");
1453
+ }
1454
+ return this.stream.getVideoTracks()[0];
1455
+ } catch (error) {
1456
+ if (version === this.version) this.stop();
1457
+ throw XmaxError.from(error);
1458
+ }
1459
+ }
1460
+ /** 换设备前解除旧原始轨监听,保留 Canvas 最后一帧及输出轨。 */
1461
+ pauseSource() {
1462
+ ++this.version;
1463
+ this.cancelPending?.();
1464
+ this.cancelFrame();
1465
+ this.source?.removeEventListener("ended", this.sourceEnded);
1466
+ this.source = void 0;
1467
+ this.video.pause();
1468
+ this.video.srcObject = null;
1469
+ }
1470
+ stop() {
1471
+ if (this.stopped) return;
1472
+ this.stopped = true;
1473
+ ++this.version;
1474
+ this.cancelPending?.();
1475
+ this.cancelFrame();
1476
+ this.source?.removeEventListener("ended", this.sourceEnded);
1477
+ this.source = void 0;
1478
+ this.video.pause();
1479
+ this.video.srcObject = null;
1480
+ this.stream?.getTracks().forEach((track) => track.stop());
1481
+ this.stream = void 0;
1482
+ this.canvas.width = this.canvas.height = 0;
1483
+ }
1484
+ draw() {
1485
+ const { videoWidth: width, videoHeight: height } = this.video;
1486
+ if (this.video.readyState < 2 || !width || !height) return false;
1487
+ const { width: outWidth, height: outHeight, rotation } = this.options;
1488
+ const quarterTurn = rotation === 90 || rotation === 270;
1489
+ const targetWidth = quarterTurn ? outHeight : outWidth;
1490
+ const targetHeight = quarterTurn ? outWidth : outHeight;
1491
+ const scale = Math.max(targetWidth / width, targetHeight / height);
1492
+ const cropWidth = targetWidth / scale, cropHeight = targetHeight / scale;
1493
+ this.context.setTransform(1, 0, 0, 1, outWidth / 2, outHeight / 2);
1494
+ this.context.rotate(rotation * Math.PI / 180);
1495
+ this.context.drawImage(
1496
+ this.video,
1497
+ (width - cropWidth) / 2,
1498
+ (height - cropHeight) / 2,
1499
+ cropWidth,
1500
+ cropHeight,
1501
+ -targetWidth / 2,
1502
+ -targetHeight / 2,
1503
+ targetWidth,
1504
+ targetHeight
1505
+ );
1506
+ return true;
1507
+ }
1508
+ render(version) {
1509
+ if (this.stopped || version !== this.version) return;
1510
+ try {
1511
+ if (this.draw()) this.requestFrame();
1512
+ this.schedule(() => this.render(version));
1513
+ } catch (error) {
1514
+ this.fail(XmaxError.from(error));
1515
+ }
1516
+ }
1517
+ requestFrame() {
1518
+ const track = this.stream?.getVideoTracks()[0];
1519
+ track?.requestFrame?.();
1520
+ }
1521
+ schedule(callback) {
1522
+ if (typeof this.video.requestVideoFrameCallback === "function") {
1523
+ this.frameCallback = this.video.requestVideoFrameCallback(callback);
1524
+ } else {
1525
+ this.timer = setTimeout(callback, 1e3 / this.options.frameRate);
1526
+ }
1527
+ }
1528
+ cancelFrame() {
1529
+ if (this.frameCallback !== void 0) this.video.cancelVideoFrameCallback(this.frameCallback);
1530
+ clearTimeout(this.timer);
1531
+ this.frameCallback = void 0;
1532
+ this.timer = void 0;
1533
+ }
1534
+ fail(error) {
1535
+ this.stop();
1536
+ this.options.onError(error);
1537
+ }
1538
+ };
1539
+
1368
1540
  // src/Foundation/RTC/Agora/AgoraRtcManager.ts
1369
1541
  var _AgoraRtcManager = class _AgoraRtcManager {
1370
1542
  /**
@@ -1381,6 +1553,9 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1381
1553
  __publicField(this, "initialization");
1382
1554
  __publicField(this, "lifecycle", 0);
1383
1555
  __publicField(this, "camera");
1556
+ __publicField(this, "cameraSource");
1557
+ __publicField(this, "cameraRotation");
1558
+ __publicField(this, "captureVersion", 0);
1384
1559
  __publicField(this, "microphone");
1385
1560
  __publicField(this, "externalVideo");
1386
1561
  __publicField(this, "externalAudio");
@@ -1474,8 +1649,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1474
1649
  this.clearRoom();
1475
1650
  this.client = void 0;
1476
1651
  client?.removeAllListeners();
1477
- this.releaseCameraSender();
1478
- this.camera?.close();
1652
+ this.releaseCameraCapture();
1479
1653
  this.microphone?.close();
1480
1654
  this.externalVideo?.close();
1481
1655
  this.externalAudio?.close();
@@ -1499,19 +1673,41 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1499
1673
  */
1500
1674
  async startCameraCapture(options) {
1501
1675
  const client = this.requireClient();
1502
- if (this.camera || this.externalVideo) {
1676
+ if (this.camera || this.cameraSource || this.externalVideo) {
1503
1677
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "A local video source is already active");
1504
1678
  }
1679
+ const rotation = options.rotation ?? 0;
1680
+ validateCameraRotation(rotation);
1681
+ const version = ++this.captureVersion;
1682
+ let source;
1505
1683
  try {
1506
- const captureSize = rtcOrientedVideoSize(options);
1507
- const camera = await this.sdk.createCameraVideoTrack({
1684
+ const captureSize = rtcOrientedVideoSize(rotation === 90 || rotation === 270 ? { width: options.height, height: options.width } : options);
1685
+ source = await this.sdk.createCameraVideoTrack({
1508
1686
  facingMode: options.position === "front" /* front */ ? "user" : "environment",
1509
1687
  encoderConfig: { width: captureSize.width, height: captureSize.height, frameRate: options.frameRate }
1510
1688
  });
1511
- if (this.client !== client) {
1512
- camera.close();
1689
+ if (this.client !== client || version !== this.captureVersion) {
1690
+ source.close();
1513
1691
  throw this.cancelled();
1514
1692
  }
1693
+ this.cameraSource = source;
1694
+ let camera = source;
1695
+ if (rotation !== 0) {
1696
+ const processor = new CameraRotationProcessor({
1697
+ ...options,
1698
+ rotation,
1699
+ onError: (error) => this.listener?.onError?.(error)
1700
+ });
1701
+ this.cameraRotation = processor;
1702
+ const output = await processor.setSource(source.getMediaStreamTrack());
1703
+ if (this.client !== client || version !== this.captureVersion) throw this.cancelled();
1704
+ camera = this.sdk.createCustomVideoTrack({
1705
+ mediaStreamTrack: output,
1706
+ width: options.width,
1707
+ height: options.height,
1708
+ frameRate: options.frameRate
1709
+ });
1710
+ }
1515
1711
  this.camera = camera;
1516
1712
  this.cameraTransceiverListener = () => {
1517
1713
  if (this.client === client && client.localTracks.includes(camera) && this.videoPublishRequested && !this.rejoinTask) {
@@ -1525,6 +1721,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1525
1721
  camera.on("transceiver-updated", this.cameraTransceiverListener);
1526
1722
  return camera.getMediaStreamTrack();
1527
1723
  } catch (error) {
1724
+ if (version === this.captureVersion) this.releaseCameraCapture();
1528
1725
  throw this.mapError(error, "CAMERA_PERMISSION_DENIED" /* cameraPermissionDenied */);
1529
1726
  }
1530
1727
  }
@@ -1532,9 +1729,24 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1532
1729
  * 切换声网相机设备并返回切换后的原生轨道。
1533
1730
  */
1534
1731
  async switchCameraCapture(to) {
1535
- const camera = this.requireCamera();
1536
- await this.run(() => camera.setDevice({ facingMode: to === "front" /* front */ ? "user" : "environment" }));
1537
- if (this.camera !== camera) throw this.cancelled();
1732
+ const source = this.requireCamera();
1733
+ const camera = this.camera;
1734
+ this.cameraRotation?.pauseSource();
1735
+ try {
1736
+ await this.run(() => source.setDevice({ facingMode: to === "front" /* front */ ? "user" : "environment" }));
1737
+ } catch (error) {
1738
+ if (this.camera === camera && this.cameraRotation) {
1739
+ await this.cameraRotation.setSource(source.getMediaStreamTrack()).catch((restoreError) => {
1740
+ this.listener?.onError?.(XmaxError.from(restoreError));
1741
+ });
1742
+ }
1743
+ throw error;
1744
+ }
1745
+ if (this.camera !== camera || this.cameraSource !== source) throw this.cancelled();
1746
+ if (this.cameraRotation) {
1747
+ await this.cameraRotation.setSource(source.getMediaStreamTrack());
1748
+ if (this.camera !== camera) throw this.cancelled();
1749
+ }
1538
1750
  if (this.videoPublishRequested) await this.restoreCameraSender(camera);
1539
1751
  return camera.getMediaStreamTrack();
1540
1752
  }
@@ -1542,17 +1754,25 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1542
1754
  * 停止采集并释放摄像头。
1543
1755
  */
1544
1756
  async stopCameraCapture() {
1757
+ this.releaseCameraCapture();
1758
+ }
1759
+ releaseCameraCapture() {
1760
+ ++this.captureVersion;
1545
1761
  const camera = this.camera;
1546
1762
  this.releaseCameraSender();
1547
1763
  this.camera = void 0;
1548
1764
  camera?.close();
1765
+ this.cameraRotation?.stop();
1766
+ this.cameraRotation = void 0;
1767
+ if (this.cameraSource !== camera) this.cameraSource?.close();
1768
+ this.cameraSource = void 0;
1549
1769
  }
1550
1770
  /**
1551
1771
  * 将外部原生轨包装为 Agora 本地轨,不申请设备权限;失败时释放本次克隆。
1552
1772
  */
1553
1773
  async setExternalMediaTracks(tracks) {
1554
1774
  this.requireClient();
1555
- if (this.camera || this.microphone || this.externalVideo) {
1775
+ if (this.camera || this.cameraSource || this.microphone || this.externalVideo) {
1556
1776
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Release the current local media before attaching external tracks");
1557
1777
  }
1558
1778
  if (tracks.videoTrack.kind !== "video" || tracks.videoTrack.readyState !== "live" || tracks.audioTrack && (tracks.audioTrack.kind !== "audio" || tracks.audioTrack.readyState !== "live")) {
@@ -1592,7 +1812,7 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1592
1812
  };
1593
1813
  ensureActive2();
1594
1814
  if (this.videoPublishRequested) {
1595
- await AgoraVideoSender.apply(camera, { ...config, ...rtcOrientedVideoSize(config) }, ensureActive2);
1815
+ await AgoraVideoSender.apply(camera, this.cameraEncodingConfiguration(config), ensureActive2);
1596
1816
  }
1597
1817
  this.cameraSenderConfiguration = { ...config };
1598
1818
  }));
@@ -1630,9 +1850,12 @@ var _AgoraRtcManager = class _AgoraRtcManager {
1630
1850
  };
1631
1851
  ensureActive2();
1632
1852
  const config = this.cameraSenderConfiguration;
1633
- if (config) await AgoraVideoSender.apply(camera, { ...config, ...rtcOrientedVideoSize(config) }, ensureActive2);
1853
+ if (config) await AgoraVideoSender.apply(camera, this.cameraEncodingConfiguration(config), ensureActive2);
1634
1854
  });
1635
1855
  }
1856
+ cameraEncodingConfiguration(config) {
1857
+ return this.cameraRotation ? config : { ...config, ...rtcOrientedVideoSize(config) };
1858
+ }
1636
1859
  releaseCameraSender() {
1637
1860
  if (this.cameraTransceiverListener) this.camera?.off("transceiver-updated", this.cameraTransceiverListener);
1638
1861
  this.cameraTransceiverListener = void 0;
@@ -2069,8 +2292,8 @@ var _AgoraRtcManager = class _AgoraRtcManager {
2069
2292
  * 获取已启动的相机采集。
2070
2293
  */
2071
2294
  requireCamera() {
2072
- if (!this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
2073
- return this.camera;
2295
+ if (!this.cameraSource || !this.camera) throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera capture is not running");
2296
+ return this.cameraSource;
2074
2297
  }
2075
2298
  /**
2076
2299
  * 获取当前可编码、发布的视频轨,不要求来源为摄像头。
@@ -2509,6 +2732,9 @@ var TrtcRtcManager = class {
2509
2732
  * @throws 引擎未初始化、已在采集、设备不可用或权限被拒绝时抛出错误。
2510
2733
  */
2511
2734
  async startCameraCapture(options) {
2735
+ if (options.rotation !== void 0 && options.rotation !== 0) {
2736
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera rotation currently requires Agora");
2737
+ }
2512
2738
  const engine = this.requireEngine();
2513
2739
  if (this.isCapturing || this.externalVideo) {
2514
2740
  throw new XmaxError(
@@ -3155,6 +3381,9 @@ var VeRtcManager = class {
3155
3381
  }
3156
3382
  }
3157
3383
  async startCameraCapture(options) {
3384
+ if (options.rotation !== void 0 && options.rotation !== 0) {
3385
+ throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Camera rotation currently requires Agora");
3386
+ }
3158
3387
  const engine = this.requireEngine();
3159
3388
  if (this.camera || this.externalVideo) {
3160
3389
  throw new XmaxError("INVALID_CONFIGURATION" /* invalidConfiguration */, "Local video is already prepared");
@@ -3991,6 +4220,7 @@ var _CameraController = class _CameraController {
3991
4220
  *
3992
4221
  * @param options.videoFormat 期望的输出尺寸、帧率和编码配置;尺寸按模型规则调整。
3993
4222
  * @param options.position 首次启动时使用的摄像头位置。
4223
+ * @param options.rotation 输入帧顺时针旋转角度,默认 0;videoFormat 为旋转后的规格,目前仅 Agora 支持非零角度。
3994
4224
  * @param options.useMicrophone 是否申请麦克风权限并允许实时连接时启动音频采集。
3995
4225
  * @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
3996
4226
  * @returns 包含本地相机视频轨道的媒体流。
@@ -4003,6 +4233,7 @@ var _CameraController = class _CameraController {
4003
4233
  "Stop the current local camera stream before creating another one"
4004
4234
  );
4005
4235
  }
4236
+ validateCameraRotation(options.rotation ?? 0);
4006
4237
  const resolvedFormat = this.resolveVideoFormat(options.videoFormat);
4007
4238
  const track = new RealtimeVideoTrack({
4008
4239
  id: _CameraController.localVideoTrackID,
@@ -4025,7 +4256,8 @@ var _CameraController = class _CameraController {
4025
4256
  width: resolvedFormat.width,
4026
4257
  height: resolvedFormat.height,
4027
4258
  frameRate: resolvedFormat.fps,
4028
- position: options.position
4259
+ position: options.position,
4260
+ ...options.rotation !== void 0 ? { rotation: options.rotation } : {}
4029
4261
  });
4030
4262
  track.mediaStreamTrack = mediaTrack;
4031
4263
  this.previewStream = new MediaStream([mediaTrack]);
@@ -8292,6 +8524,7 @@ var XmaxRealtimeManager = class _XmaxRealtimeManager {
8292
8524
  * 将返回的轨道绑定到预览视图;收到有效帧且视图已绑定后进入 `ready`。
8293
8525
  *
8294
8526
  * @param options.enableFrameValidation 是否在发布前等待相机预热(固定 200ms),默认 true;false 跳过等待。
8527
+ * @param options.rotation 输入帧顺时针旋转角度,默认 0;videoFormat 为旋转后的规格,目前仅 Agora 支持非零角度。
8295
8528
  * @returns 包含本地相机视频轨道的媒体流。
8296
8529
  * @throws 模型不支持相机输入、配置无效、权限或采集启动失败时抛出错误。
8297
8530
  */
@@ -12467,4 +12700,4 @@ export {
12467
12700
  XmaxVideoView,
12468
12701
  XmaxRealtimeVideoView
12469
12702
  };
12470
- //# sourceMappingURL=chunk-PLFWPS5A.js.map
12703
+ //# sourceMappingURL=chunk-4O5BSMUX.js.map