@pickleball/expo-sdk 0.2.0 → 1.0.0

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +155 -101
  3. package/dist/advanced-DcbHBCGb.d.ts +300 -0
  4. package/dist/advanced-DeQwg7MK.d.cts +300 -0
  5. package/dist/advanced.cjs +14 -2
  6. package/dist/advanced.d.cts +2 -2
  7. package/dist/advanced.d.ts +2 -2
  8. package/dist/advanced.js +17 -5
  9. package/dist/camera-controls-By8tHigY.d.cts +97 -0
  10. package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
  11. package/dist/chunk-62YV3PCD.cjs +139 -0
  12. package/dist/chunk-6F3HN6US.js +50 -0
  13. package/dist/chunk-6YE5BL3A.cjs +50 -0
  14. package/dist/chunk-7Z7TN5N3.js +318 -0
  15. package/dist/{chunk-TRLASNPQ.cjs → chunk-GXQYMPHE.cjs} +1 -1
  16. package/dist/{chunk-TBPR2QND.cjs → chunk-IUIRVKJI.cjs} +736 -399
  17. package/dist/chunk-TBUEXRFQ.cjs +318 -0
  18. package/dist/{chunk-FODE2W2H.js → chunk-XGP2MN5R.js} +688 -351
  19. package/dist/{chunk-6JOMESNT.js → chunk-XL4CISVQ.js} +1 -1
  20. package/dist/chunk-ZSKSXOCZ.js +139 -0
  21. package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
  22. package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
  23. package/dist/device-agent/index.cjs +973 -44
  24. package/dist/device-agent/index.d.cts +339 -3
  25. package/dist/device-agent/index.d.ts +339 -3
  26. package/dist/device-agent/index.js +940 -11
  27. package/dist/{engine-CCSSqPbZ.d.cts → engine-BSvSdCSE.d.cts} +227 -2
  28. package/dist/{engine-CCSSqPbZ.d.ts → engine-BSvSdCSE.d.ts} +227 -2
  29. package/dist/index.cjs +419 -937
  30. package/dist/index.d.cts +315 -32
  31. package/dist/index.d.ts +315 -32
  32. package/dist/index.js +423 -941
  33. package/dist/plugin.cjs +11 -13
  34. package/dist/plugin.d.cts +15 -1
  35. package/dist/plugin.d.ts +15 -1
  36. package/dist/plugin.js +7 -9
  37. package/dist/realtime/index.cjs +31 -0
  38. package/dist/realtime/index.d.cts +15 -0
  39. package/dist/realtime/index.d.ts +15 -0
  40. package/dist/realtime/index.js +31 -0
  41. package/dist/ui/index.cjs +1297 -0
  42. package/dist/ui/index.d.cts +493 -0
  43. package/dist/ui/index.d.ts +493 -0
  44. package/dist/ui/index.js +1297 -0
  45. package/dist/version.cjs +2 -2
  46. package/dist/version.js +1 -1
  47. package/package.json +31 -15
  48. package/src/advanced.ts +11 -9
  49. package/src/auto-resume.tsx +259 -0
  50. package/src/camera-controls.ts +99 -0
  51. package/src/camera-grant-provider.ts +392 -0
  52. package/src/camera-settings.ts +133 -0
  53. package/src/contracts.ts +73 -0
  54. package/src/device-agent/agent.ts +262 -9
  55. package/src/device-agent/beat-policy.ts +46 -0
  56. package/src/device-agent/component.tsx +11 -0
  57. package/src/device-agent/convex-beat-transport.ts +140 -0
  58. package/src/device-agent/convex-command-subscription.ts +101 -0
  59. package/src/device-agent/convex-control-channel.ts +7 -1
  60. package/src/device-agent/http-transport.ts +22 -1
  61. package/src/device-agent/index.ts +22 -0
  62. package/src/device-agent/mqtt-channel.ts +379 -0
  63. package/src/device-agent/mqtt-codec.ts +347 -0
  64. package/src/device-agent/tournament.ts +352 -0
  65. package/src/engine.ts +580 -15
  66. package/src/http-session-provider.ts +21 -189
  67. package/src/index.ts +84 -5
  68. package/src/local-video-quality.ts +43 -0
  69. package/src/native-runtime.ts +234 -78
  70. package/src/plugin.ts +19 -6
  71. package/src/provider.tsx +94 -18
  72. package/src/realtime/index.ts +56 -0
  73. package/src/resume-policy.ts +85 -0
  74. package/src/resume.ts +132 -0
  75. package/src/rtmp-room-adapter.ts +164 -12
  76. package/src/session-grant.ts +256 -0
  77. package/src/ui/camera-screen.tsx +484 -0
  78. package/src/ui/controls-sheet.tsx +445 -0
  79. package/src/ui/copy.ts +326 -0
  80. package/src/ui/helpers.ts +105 -0
  81. package/src/ui/hooks.ts +59 -0
  82. package/src/ui/index.ts +42 -0
  83. package/src/ui/preview-view.tsx +39 -0
  84. package/src/ui/primitives.tsx +304 -0
  85. package/src/uplink-check.ts +173 -0
  86. package/dist/advanced-BXDrDl1h.d.ts +0 -316
  87. package/dist/advanced-BdAa2Erv.d.cts +0 -316
  88. package/src/livekit-room-adapter.ts +0 -390
  89. package/src/livestream-screen.tsx +0 -1002
@@ -2,9 +2,22 @@ type LivestreamState = "idle" | "preparing" | "preview" | "connecting"
2
2
  /** Đã vào phòng, chưa phát — chờ lệnh phát từ app hoặc dashboard. */
3
3
  | "standby" | "live" | "reconnecting" | "ending" | "ended" | "error";
4
4
  type SdkErrorCode = "PERMISSION_DENIED" | "NETWORK_UNAVAILABLE" | "TOKEN_EXPIRED" | "SDK_UPGRADE_REQUIRED" | "SESSION_CONFLICT" | "RECORDING_FAILED" | "CONSENT_REQUIRED" | "SDK_DISABLED" | "UNKNOWN";
5
+ /**
6
+ * Vì sao 409 SESSION_CONFLICT bị nhận — ba ca cần app hành xử KHÁC NHAU mà
7
+ * trước đây chỉ phân biệt được bằng đọc chuỗi tiếng Anh trong `message`:
8
+ * - `superseded`: grant này đã bị grant mới thay (đối tác cấp lại camera-grant)
9
+ * → app DỪNG tại chỗ, không `end`, không quay số lại.
10
+ * - `ended`: phiên đã kết thúc → dọn state, không thử lại.
11
+ * - `expired`: phiên đã quá trần thời lượng credential → xin grant/phiên mới.
12
+ */
13
+ type SdkConflictReason = "superseded" | "ended" | "expired";
5
14
  interface SdkError {
6
15
  code: SdkErrorCode;
7
16
  message: string;
17
+ /** Chỉ có ở 409 SESSION_CONFLICT của namespace /api/v2/camera. */
18
+ reason?: SdkConflictReason;
19
+ /** Chỉ có ở 429 — cùng số với header `Retry-After` (giây, làm tròn lên). */
20
+ retryAfterMs?: number;
8
21
  }
9
22
  interface BootstrapInput {
10
23
  sdkVersion: string;
@@ -43,6 +56,14 @@ interface StartLivestreamInput {
43
56
  * Engine tự đặt cờ này theo options.autoPublish; app không cần truyền.
44
57
  */
45
58
  standby?: boolean;
59
+ /**
60
+ * Độ phân giải app THỰC SỰ quay, để server ghi đúng vào phiên. Chỉ app biết
61
+ * con số này: nó là lựa chọn local và app còn override `config.videoQuality`
62
+ * mà server trả về. Không khai thì server dùng mặc định của nó (1080) và bản
63
+ * ghi phiên nói dối — đã cắn 18/08/2026, dashboard báo 1080 trong khi máy
64
+ * phát 720 suốt.
65
+ */
66
+ quality?: 720 | 1080;
46
67
  }
47
68
  interface SdkTelemetryCredentials {
48
69
  endpoint: string;
@@ -64,6 +85,36 @@ interface SdkSessionRefresh {
64
85
  telemetry?: SdkTelemetryCredentials;
65
86
  config: SdkRemoteConfig;
66
87
  }
88
+ type CameraGrantStatus = "scheduled" | "live";
89
+ /**
90
+ * Camera grant — điện thoại của user ĐỐI TÁC lên sóng (ADR-0002). Backend đối
91
+ * tác nhận từ REST v1 (POST /api/v1/live-sessions {cameraGrant:true} hoặc
92
+ * POST /api/v1/live-sessions/{id}/camera-grant) và chuyển NGUYÊN TRẠNG cho app;
93
+ * app dùng `telemetry.token` làm Bearer với namespace /api/v2/camera/sessions.
94
+ *
95
+ * Superset nghiêm ngặt của SdkSessionGrant: `serverUrl` là RTMP ingest
96
+ * (`rtmp://host:1935/`), `participantToken` là stream key — SDK 0.2 đã nhận
97
+ * hình dạng này qua isSessionGrant nên không cần validator mới ở lớp engine.
98
+ */
99
+ interface CameraGrant extends SdkSessionGrant {
100
+ /** Không bao giờ "ended" — endpoint trả 409 thay vì grant. */
101
+ status: CameraGrantStatus;
102
+ matchRef: string | null;
103
+ courtRef: string | null;
104
+ watchUrl: string | null;
105
+ /**
106
+ * Origin websocket Convex (https://<dep>.convex.cloud) để SDK subscribe lệnh
107
+ * thay vì poll HTTP 2s; null khi deployment không lộ origin chuẩn → SDK poll.
108
+ */
109
+ convexUrl: string | null;
110
+ issuedAt: number;
111
+ /**
112
+ * Hạn dùng của grant. Phiên chờ mà app CHƯA từng kết nối bị dọn sau 30 phút
113
+ * (RTMP_STANDBY_SILENT_MS) — xin grant đúng lúc user mở camera, đừng cấp sớm
114
+ * hàng giờ trước trận.
115
+ */
116
+ expiresAt: number;
117
+ }
67
118
  interface EndLivestreamInput {
68
119
  sessionId: string;
69
120
  reason: "user" | "background_timeout" | "network_timeout" | "standby_timeout" | "error";
@@ -77,6 +128,22 @@ interface SdkTelemetryEvent {
77
128
  name: SdkTelemetryEventName;
78
129
  data?: Record<string, unknown>;
79
130
  }
131
+ /** Lệnh điều khiển phiên giao qua kênh poll (mode device-rtmp). */
132
+ type SessionCommandAction = "start" | "stop" | "end";
133
+ interface SessionCommandRecord {
134
+ id: string;
135
+ action: SessionCommandAction;
136
+ /** Con trỏ tăng nghiêm ngặt theo phiên — client gửi lại làm `after`. */
137
+ issuedAt: number;
138
+ }
139
+ interface SessionCommandPollResult {
140
+ commands: SessionCommandRecord[];
141
+ /**
142
+ * Trạng thái phiên phía server — lưới an toàn khi lệnh rơi: "live" mà máy
143
+ * còn standby thì tự phát, "ended" thì tự kết thúc.
144
+ */
145
+ status: "scheduled" | "live" | "ended";
146
+ }
80
147
  interface LivestreamSessionProvider {
81
148
  bootstrap(input: BootstrapInput): Promise<SdkBootstrap>;
82
149
  start(input: StartLivestreamInput): Promise<SdkSessionGrant>;
@@ -318,6 +385,20 @@ type RoomEvent = {
318
385
  action: string;
319
386
  };
320
387
  interface RoomAdapter {
388
+ /**
389
+ * Xin quyền camera/mic theo cách của mode. Không có thì engine dùng
390
+ * runtime.requestPermissions() (getUserMedia của WebRTC — mở camera thật
391
+ * ~400ms). Mode device-rtmp PHẢI tự xin bằng API quyền thuần: máy Android
392
+ * MediaTek chỉ cho một camera mở, camera WebRTC chưa nhả là HaishinKit dính
393
+ * "Too many users (-87)" và preview đen.
394
+ */
395
+ requestPermissions?(): Promise<boolean>;
396
+ /**
397
+ * true = adapter KHÔNG tự nối lại khi rớt (RTMP: native chỉ báo
398
+ * "disconnected" rồi đứng im) — engine phải chủ động quay số lại qua retry().
399
+ * LiveKit bỏ trống: SDK của nó tự hồi và tự bắn "reconnected".
400
+ */
401
+ readonly needsManualReconnect?: boolean;
321
402
  /** Bật/tắt publish track khi đã ở trong phòng (standby ↔ live). */
322
403
  setPublishing?(enabled: boolean): Promise<void>;
323
404
  setCaptureOrientation?(orientation: CaptureOrientation): Promise<void>;
@@ -346,18 +427,66 @@ interface SdkNetwork {
346
427
  isOnline(): boolean;
347
428
  subscribe(listener: (online: boolean) => void): () => void;
348
429
  }
430
+ /**
431
+ * Khi app khởi động lại và thấy dấu của một phiên chưa được dừng có chủ ý, hàm
432
+ * này quyết định: GIỮ phiên đó để nhận lại, hay kết thúc nó như trước.
433
+ *
434
+ * Mặc định (không truyền) = kết thúc, giữ nguyên hành vi cho mọi SDK đối tác.
435
+ * App first-party trả true khi máy đang gán sân: lúc đó máy sụp nguồn giữa trận
436
+ * là chuyện của thiết bị gác sân, và nối lại đúng phiên cũ mới giữ được link xem
437
+ * và VOD liền mạch. Máy không gán sân thì vẫn dọn sạch như cũ.
438
+ */
439
+ /**
440
+ * Được phép async có chủ đích: câu trả lời đúng nằm TRÊN ĐĨA (sân đã gán có
441
+ * persist), còn React state lúc engine khởi động thường CHƯA restore xong.
442
+ * Bản sync-đọc-state đã gây lỗi thật (18/08/2026): recover chạy trước restore
443
+ * → keep=false oan → app tự kết án phiên đáng giữ rồi arm phiên chờ THỨ HAI.
444
+ */
445
+ type KeepSessionOnRestart = () => boolean | Promise<boolean>;
349
446
  interface TelemetrySink {
350
447
  send(credentials: SdkTelemetryCredentials, events: readonly SdkTelemetryEvent[]): Promise<void>;
351
448
  dispose?(): void;
352
449
  }
450
+ /**
451
+ * Kênh nhận lệnh điều khiển cho mode device-rtmp: RTMP không có data channel
452
+ * nên start/stop/end từ server (match-start của trọng tài, dashboard) về qua
453
+ * HTTP poll, auth bằng chính telemetry token trong grant.
454
+ */
455
+ interface SessionCommandChannel {
456
+ poll(credentials: SdkTelemetryCredentials, after: number): Promise<SessionCommandPollResult>;
457
+ dispose?(): void;
458
+ }
459
+ /**
460
+ * Kênh nhận lệnh device-rtmp qua WEBSOCKET (Convex subscription) — thay poll
461
+ * HTTP 2s cho máy first-party nối thẳng Convex. Lệnh về tức thì, lúc rảnh gần
462
+ * như 0 function call (poll 2s ở 200 máy là ~518M call/tháng). Có kênh này thì
463
+ * engine hạ poll về nhịp reconcile chậm làm lưới an toàn; thiếu thì poll như cũ.
464
+ */
465
+ interface SessionCommandSubscription {
466
+ /**
467
+ * Mở subscription cho phiên; `onCommands` được gọi mỗi lần hàng lệnh đổi
468
+ * (kể cả lần đầu). `onError` báo websocket/token hỏng để engine tăng lại nhịp
469
+ * poll. Trả hàm huỷ subscription (KHÔNG đóng client — tái dùng qua phiên).
470
+ */
471
+ subscribe(credentials: SdkTelemetryCredentials, sessionId: string, onCommands: (commands: SessionCommandRecord[]) => void, onError?: (error: Error) => void): () => void;
472
+ dispose?(): void;
473
+ }
353
474
  interface EngineDependencies {
354
475
  sessionProvider: LivestreamSessionProvider;
355
476
  bootstrapInput: BootstrapInput;
356
477
  roomFactory: () => RoomAdapter;
478
+ keepSessionOnRestart?: KeepSessionOnRestart;
357
479
  runtime: SdkRuntime;
358
480
  storage: SdkStorage;
359
481
  network: SdkNetwork;
360
482
  telemetry?: TelemetrySink;
483
+ /** Kênh poll lệnh cho phiên device-rtmp; thiếu thì mode này điếc lệnh server. */
484
+ commandChannel?: SessionCommandChannel;
485
+ /**
486
+ * Kênh nhận lệnh qua websocket (first-party). Có thì lệnh về tức thì và poll
487
+ * tụt về nhịp reconcile; thiếu (máy đối tác qua proxy) thì chỉ dùng poll.
488
+ */
489
+ commandSubscription?: SessionCommandSubscription;
361
490
  /** Thu chỉ số thiết bị; gọi mỗi nhịp heartbeat khi đang phát. */
362
491
  deviceMetrics?: () => Promise<DeviceMetricsSample | null>;
363
492
  now?: () => number;
@@ -378,6 +507,16 @@ interface DeviceMetricsSample {
378
507
  storageFreeGb?: number;
379
508
  netType?: string;
380
509
  linkMbps?: number;
510
+ /** Cường độ sóng WiFi 0-100% — CHỈ Android (iOS không mở cho app). */
511
+ signalPct?: number;
512
+ /** IP nội bộ — phân biệt máy đang bám router nào, không cần quyền vị trí. */
513
+ localIp?: string;
514
+ /**
515
+ * Tần số kênh WiFi (MHz) — CHỈ Android. 2412–2484 = băng 2.4 GHz, ≥ 5000 =
516
+ * 5 GHz. Thay cho SSID (bị khoá sau quyền vị trí): 22/08/2026 một máy rơi từ
517
+ * AP 5 GHz của giải sang WiFi quán 2.4 GHz mà heartbeat không phân biệt được.
518
+ */
519
+ freqMhz?: number;
381
520
  rssi?: number;
382
521
  connQuality?: string;
383
522
  charging?: boolean;
@@ -400,6 +539,14 @@ interface LivestreamSnapshot {
400
539
  selectedCameraId: string | null;
401
540
  elapsedMs: number;
402
541
  isOnline: boolean;
542
+ /**
543
+ * Phiên cũ còn sống mà app vừa khởi động lại có thể NHẬN LẠI (thay vì tạo phiên
544
+ * mới). Chỉ khác null khi engine tìm thấy dấu của một phiên chưa được dừng có
545
+ * chủ ý VÀ caller nói phiên đó đáng giữ (keepSessionOnRestart). Tầng trên
546
+ * (AutoResume) đọc trường này để nối lại đúng phiên cũ — giữ nguyên link xem và
547
+ * VOD liền một mạch.
548
+ */
549
+ adoptableSessionId: string | null;
403
550
  }
404
551
  declare const PENDING_END_KEY = "@pickleball/expo-sdk/pending-end";
405
552
  declare const LEGAL_LIVESTREAM_TRANSITIONS: Readonly<Record<LivestreamState, readonly LivestreamState[]>>;
@@ -428,10 +575,23 @@ declare class LivestreamEngine {
428
575
  private backgroundTimer?;
429
576
  private standbyTimer?;
430
577
  private reconnectTimer?;
578
+ private redialTimer?;
579
+ /** Đang trong vòng quay số lại — xem doRetry() để biết vì sao cần cờ này. */
580
+ private redialing;
431
581
  private heartbeatTimer?;
432
582
  private elapsedTimer?;
433
583
  private tokenRefreshTimer?;
434
584
  private maxSessionTimer?;
585
+ private commandPollTimer?;
586
+ /** Huỷ subscription websocket lệnh của phiên hiện tại (nếu đang mở). */
587
+ private commandUnsubscribe?;
588
+ /** Khoá phiên+token đang subscribe — đổi thì resubscribe (token refresh/đổi phiên). */
589
+ private commandSubscriptionKey?;
590
+ /** Websocket đã đẩy được ít nhất một lần → coi là sống, cho phép hạ nhịp poll. */
591
+ private commandSubscriptionActive;
592
+ /** issuedAt của lệnh cuối đã xử lý — server chỉ trả lệnh mới hơn mốc này. */
593
+ private commandCursor;
594
+ private commandPollBackoffMs;
435
595
  private grant?;
436
596
  private startedAt?;
437
597
  /** Đang publish track (live) hay chỉ ngồi trong phòng (standby). */
@@ -471,16 +631,81 @@ declare class LivestreamEngine {
471
631
  handleRoomEvent(event: RoomEvent): void;
472
632
  handleAppState(state: "active" | "background" | "inactive"): void;
473
633
  private ensureBackgroundGraceTimer;
474
- dispose(): Promise<void>;
634
+ /**
635
+ * `keepSession` = engine bị tháo nhưng phiên phải sống tiếp để lần mở app sau
636
+ * nhận lại. Mặc định false: giữ NGUYÊN hành vi cũ cho mọi SDK đối tác.
637
+ *
638
+ * Lưu ý dispose KHÔNG BAO GIỜ là đường của "người dùng bấm dừng" — nút dừng đi
639
+ * qua doStop, mà doStop xoá ownedSessionId trước, nên tới đây sessionId đã là
640
+ * undefined. Vì vậy nhánh end ở đây luôn là teardown NGOÀI Ý MUỐN (app bị giết,
641
+ * component remount), đúng chỗ cần giữ phiên.
642
+ */
643
+ dispose(options?: {
644
+ keepSession?: boolean;
645
+ }): Promise<void>;
475
646
  private replayPendingEnd;
476
647
  private doReplayPendingEnd;
648
+ /**
649
+ * Phiên tưởng đáng nhận lại hoá ra đã chết phía server → BỎ dấu để máy arm
650
+ * phiên mới được.
651
+ *
652
+ * Thiếu hàm này là máy kẹt cứng: dấu vẫn nằm đó chặn mọi lượt start (đúng vai
653
+ * trò chống phiên song song của nó), mà lượt start duy nhất được cho qua lại là
654
+ * lượt nhận lại chính cái phiên đã chết — không bao giờ thành công. Đã dính
655
+ * thật 18/08/2026: lưới standby 30 phút kết thúc phiên trong lúc máy ngủ, mở
656
+ * lại thì app đứng ở "error" vĩnh viễn.
657
+ */
658
+ abandonAdoptableSession(sessionId: string): Promise<void>;
477
659
  private recoverStartupSession;
660
+ /**
661
+ * Dấu phiên cũ chặn thao tác mới cho tới khi nó được giải quyết.
662
+ *
663
+ * `externalSessionId` = id lượt start đang xin đi qua; bỏ trống nghĩa là
664
+ * prepare (mở camera), vốn phải luôn được phép khi đang định nhận lại — không
665
+ * mở được camera thì không nhận lại được, và đó chính là bẫy làm máy kẹt cứng ở
666
+ * màn lỗi "The previous livestream is still pending end".
667
+ */
478
668
  private ensurePendingEndResolved;
479
669
  private readPendingEnd;
480
670
  private ownsInMemorySession;
481
671
  private startTimers;
482
672
  private clearTimers;
673
+ /**
674
+ * Quay số lại với backoff cho adapter cần reconnect thủ công. Mỗi nhịp gọi
675
+ * retry() (refresh credentials + room.connect + enterSession). Thành công →
676
+ * enterSession phát "publishing" → nhánh "reconnected" dọn cả hai timer.
677
+ * Thất bại → doRetry() đưa state về "error" (vẫn còn sessionId) và nhịp sau
678
+ * retry() lại kéo về "reconnecting" — lặp tới khi doom timer 30s kết liễu.
679
+ * Backoff thưa dần để không dội server khi ingest thật sự sập.
680
+ */
681
+ private scheduleRedial;
682
+ private clearRedialTimer;
683
+ private stopRedialing;
483
684
  private clearReconnectTimer;
685
+ /**
686
+ * Poll lệnh chỉ khi phiên là device-rtmp (grant rtmp://) và đang standby/
687
+ * live. Ngoài hai trạng thái đó không có gì để điều khiển: connecting chưa
688
+ * có phiên chạy, reconnecting đã có reconnect timer lo, ending/ended thì thôi.
689
+ */
690
+ private commandPollingWanted;
691
+ private syncCommandPolling;
692
+ /**
693
+ * Mở/đổi websocket nhận lệnh cho phiên hiện tại. Chỉ máy first-party có
694
+ * dependency này (đối tác qua proxy thì undefined → chỉ poll). Resubscribe
695
+ * khi đổi phiên HOẶC token (refresh) vì cả hai đi vào khoá.
696
+ */
697
+ private syncCommandSubscription;
698
+ private stopCommandSubscription;
699
+ private stopCommandPolling;
700
+ private scheduleCommandPoll;
701
+ private commandPollInterval;
702
+ /**
703
+ * Áp lệnh từ poll HOẶC subscription vào cùng một state machine. Subscription
704
+ * trả cả lệnh CŨ (after:0) nên phải lọc theo con trỏ ở đây; poll đã lọc
705
+ * server-side (after=cursor) nên bước lọc này vô hại với nó.
706
+ */
707
+ private applyCommands;
708
+ private pollSessionCommands;
484
709
  /** Heartbeat kèm chỉ số thiết bị (nếu collector được cung cấp). */
485
710
  private sendHeartbeat;
486
711
  private sendTelemetry;
@@ -513,4 +738,4 @@ declare class LivestreamEngine {
513
738
  private ensureOperation;
514
739
  }
515
740
 
516
- export { UnsupportedRuntimeError as A, type BootstrapInput as B, type CameraDeviceInfo as C, type DeviceMetricsSample as D, type EndLivestreamInput as E, UnsupportedSdkError as F, compareSemver as G, errorFromCode as H, InvalidResponseError as I, isSdkRemoteConfig as J, isSdkVersionSupported as K, type LivestreamSessionProvider as L, isSessionGoneError as M, NetworkUnavailableError as N, normalizeError as O, PermissionDeniedError as P, LivestreamEngine as Q, RecordingFailureError as R, type StartLivestreamInput as S, type TelemetrySink as T, UnknownSdkError as U, type RoomConnection as V, type RoomEvent as W, type EngineDependencies as X, LEGAL_LIVESTREAM_TRANSITIONS as Y, PENDING_END_KEY as Z, type CaptureOrientation as a, ConsentRequiredError as b, type EndReason as c, ExpoSdkConfigurationError as d, type ExpoSdkErrorCode as e, ExpoSdkTimeoutError as f, type LivestreamSnapshot as g, type LivestreamState as h, PickleballExpoError as i, type RoomAdapter as j, SDK_TELEMETRY_EVENT_NAMES as k, type SdkBootstrap as l, SdkDisabledError as m, type SdkError as n, type SdkErrorCode as o, type SdkNetwork as p, type SdkRemoteConfig as q, type SdkRuntime as r, type SdkSessionGrant as s, type SdkSessionRefresh as t, type SdkStorage as u, type SdkTelemetryCredentials as v, type SdkTelemetryEvent as w, type SdkTelemetryEventName as x, SessionConflictError as y, TokenExpiredError as z };
741
+ export { normalizeError as $, type SdkTelemetryEvent as A, type BootstrapInput as B, type CameraGrant as C, type DeviceMetricsSample as D, type EndLivestreamInput as E, type SdkTelemetryEventName as F, type SessionCommandAction as G, type SessionCommandChannel as H, InvalidResponseError as I, type SessionCommandPollResult as J, type SessionCommandRecord as K, type LivestreamSessionProvider as L, SessionConflictError as M, NetworkUnavailableError as N, TokenExpiredError as O, PermissionDeniedError as P, UnsupportedRuntimeError as Q, type RoomAdapter as R, type SessionCommandSubscription as S, type TelemetrySink as T, UnknownSdkError as U, UnsupportedSdkError as V, compareSemver as W, errorFromCode as X, isSdkRemoteConfig as Y, isSdkVersionSupported as Z, isSessionGoneError as _, type SdkStorage as a, LivestreamEngine as a0, type RoomConnection as a1, type RoomEvent as a2, type EngineDependencies as a3, LEGAL_LIVESTREAM_TRANSITIONS as a4, PENDING_END_KEY as a5, type SdkSessionGrant as b, type StartLivestreamInput as c, type SdkRemoteConfig as d, type CameraDeviceInfo as e, type CameraGrantStatus as f, type CaptureOrientation as g, ConsentRequiredError as h, type EndReason as i, ExpoSdkConfigurationError as j, type ExpoSdkErrorCode as k, ExpoSdkTimeoutError as l, type LivestreamSnapshot as m, type LivestreamState as n, PickleballExpoError as o, RecordingFailureError as p, SDK_TELEMETRY_EVENT_NAMES as q, type SdkBootstrap as r, type SdkConflictReason as s, SdkDisabledError as t, type SdkError as u, type SdkErrorCode as v, type SdkNetwork as w, type SdkRuntime as x, type SdkSessionRefresh as y, type SdkTelemetryCredentials as z };
@@ -2,9 +2,22 @@ type LivestreamState = "idle" | "preparing" | "preview" | "connecting"
2
2
  /** Đã vào phòng, chưa phát — chờ lệnh phát từ app hoặc dashboard. */
3
3
  | "standby" | "live" | "reconnecting" | "ending" | "ended" | "error";
4
4
  type SdkErrorCode = "PERMISSION_DENIED" | "NETWORK_UNAVAILABLE" | "TOKEN_EXPIRED" | "SDK_UPGRADE_REQUIRED" | "SESSION_CONFLICT" | "RECORDING_FAILED" | "CONSENT_REQUIRED" | "SDK_DISABLED" | "UNKNOWN";
5
+ /**
6
+ * Vì sao 409 SESSION_CONFLICT bị nhận — ba ca cần app hành xử KHÁC NHAU mà
7
+ * trước đây chỉ phân biệt được bằng đọc chuỗi tiếng Anh trong `message`:
8
+ * - `superseded`: grant này đã bị grant mới thay (đối tác cấp lại camera-grant)
9
+ * → app DỪNG tại chỗ, không `end`, không quay số lại.
10
+ * - `ended`: phiên đã kết thúc → dọn state, không thử lại.
11
+ * - `expired`: phiên đã quá trần thời lượng credential → xin grant/phiên mới.
12
+ */
13
+ type SdkConflictReason = "superseded" | "ended" | "expired";
5
14
  interface SdkError {
6
15
  code: SdkErrorCode;
7
16
  message: string;
17
+ /** Chỉ có ở 409 SESSION_CONFLICT của namespace /api/v2/camera. */
18
+ reason?: SdkConflictReason;
19
+ /** Chỉ có ở 429 — cùng số với header `Retry-After` (giây, làm tròn lên). */
20
+ retryAfterMs?: number;
8
21
  }
9
22
  interface BootstrapInput {
10
23
  sdkVersion: string;
@@ -43,6 +56,14 @@ interface StartLivestreamInput {
43
56
  * Engine tự đặt cờ này theo options.autoPublish; app không cần truyền.
44
57
  */
45
58
  standby?: boolean;
59
+ /**
60
+ * Độ phân giải app THỰC SỰ quay, để server ghi đúng vào phiên. Chỉ app biết
61
+ * con số này: nó là lựa chọn local và app còn override `config.videoQuality`
62
+ * mà server trả về. Không khai thì server dùng mặc định của nó (1080) và bản
63
+ * ghi phiên nói dối — đã cắn 18/08/2026, dashboard báo 1080 trong khi máy
64
+ * phát 720 suốt.
65
+ */
66
+ quality?: 720 | 1080;
46
67
  }
47
68
  interface SdkTelemetryCredentials {
48
69
  endpoint: string;
@@ -64,6 +85,36 @@ interface SdkSessionRefresh {
64
85
  telemetry?: SdkTelemetryCredentials;
65
86
  config: SdkRemoteConfig;
66
87
  }
88
+ type CameraGrantStatus = "scheduled" | "live";
89
+ /**
90
+ * Camera grant — điện thoại của user ĐỐI TÁC lên sóng (ADR-0002). Backend đối
91
+ * tác nhận từ REST v1 (POST /api/v1/live-sessions {cameraGrant:true} hoặc
92
+ * POST /api/v1/live-sessions/{id}/camera-grant) và chuyển NGUYÊN TRẠNG cho app;
93
+ * app dùng `telemetry.token` làm Bearer với namespace /api/v2/camera/sessions.
94
+ *
95
+ * Superset nghiêm ngặt của SdkSessionGrant: `serverUrl` là RTMP ingest
96
+ * (`rtmp://host:1935/`), `participantToken` là stream key — SDK 0.2 đã nhận
97
+ * hình dạng này qua isSessionGrant nên không cần validator mới ở lớp engine.
98
+ */
99
+ interface CameraGrant extends SdkSessionGrant {
100
+ /** Không bao giờ "ended" — endpoint trả 409 thay vì grant. */
101
+ status: CameraGrantStatus;
102
+ matchRef: string | null;
103
+ courtRef: string | null;
104
+ watchUrl: string | null;
105
+ /**
106
+ * Origin websocket Convex (https://<dep>.convex.cloud) để SDK subscribe lệnh
107
+ * thay vì poll HTTP 2s; null khi deployment không lộ origin chuẩn → SDK poll.
108
+ */
109
+ convexUrl: string | null;
110
+ issuedAt: number;
111
+ /**
112
+ * Hạn dùng của grant. Phiên chờ mà app CHƯA từng kết nối bị dọn sau 30 phút
113
+ * (RTMP_STANDBY_SILENT_MS) — xin grant đúng lúc user mở camera, đừng cấp sớm
114
+ * hàng giờ trước trận.
115
+ */
116
+ expiresAt: number;
117
+ }
67
118
  interface EndLivestreamInput {
68
119
  sessionId: string;
69
120
  reason: "user" | "background_timeout" | "network_timeout" | "standby_timeout" | "error";
@@ -77,6 +128,22 @@ interface SdkTelemetryEvent {
77
128
  name: SdkTelemetryEventName;
78
129
  data?: Record<string, unknown>;
79
130
  }
131
+ /** Lệnh điều khiển phiên giao qua kênh poll (mode device-rtmp). */
132
+ type SessionCommandAction = "start" | "stop" | "end";
133
+ interface SessionCommandRecord {
134
+ id: string;
135
+ action: SessionCommandAction;
136
+ /** Con trỏ tăng nghiêm ngặt theo phiên — client gửi lại làm `after`. */
137
+ issuedAt: number;
138
+ }
139
+ interface SessionCommandPollResult {
140
+ commands: SessionCommandRecord[];
141
+ /**
142
+ * Trạng thái phiên phía server — lưới an toàn khi lệnh rơi: "live" mà máy
143
+ * còn standby thì tự phát, "ended" thì tự kết thúc.
144
+ */
145
+ status: "scheduled" | "live" | "ended";
146
+ }
80
147
  interface LivestreamSessionProvider {
81
148
  bootstrap(input: BootstrapInput): Promise<SdkBootstrap>;
82
149
  start(input: StartLivestreamInput): Promise<SdkSessionGrant>;
@@ -318,6 +385,20 @@ type RoomEvent = {
318
385
  action: string;
319
386
  };
320
387
  interface RoomAdapter {
388
+ /**
389
+ * Xin quyền camera/mic theo cách của mode. Không có thì engine dùng
390
+ * runtime.requestPermissions() (getUserMedia của WebRTC — mở camera thật
391
+ * ~400ms). Mode device-rtmp PHẢI tự xin bằng API quyền thuần: máy Android
392
+ * MediaTek chỉ cho một camera mở, camera WebRTC chưa nhả là HaishinKit dính
393
+ * "Too many users (-87)" và preview đen.
394
+ */
395
+ requestPermissions?(): Promise<boolean>;
396
+ /**
397
+ * true = adapter KHÔNG tự nối lại khi rớt (RTMP: native chỉ báo
398
+ * "disconnected" rồi đứng im) — engine phải chủ động quay số lại qua retry().
399
+ * LiveKit bỏ trống: SDK của nó tự hồi và tự bắn "reconnected".
400
+ */
401
+ readonly needsManualReconnect?: boolean;
321
402
  /** Bật/tắt publish track khi đã ở trong phòng (standby ↔ live). */
322
403
  setPublishing?(enabled: boolean): Promise<void>;
323
404
  setCaptureOrientation?(orientation: CaptureOrientation): Promise<void>;
@@ -346,18 +427,66 @@ interface SdkNetwork {
346
427
  isOnline(): boolean;
347
428
  subscribe(listener: (online: boolean) => void): () => void;
348
429
  }
430
+ /**
431
+ * Khi app khởi động lại và thấy dấu của một phiên chưa được dừng có chủ ý, hàm
432
+ * này quyết định: GIỮ phiên đó để nhận lại, hay kết thúc nó như trước.
433
+ *
434
+ * Mặc định (không truyền) = kết thúc, giữ nguyên hành vi cho mọi SDK đối tác.
435
+ * App first-party trả true khi máy đang gán sân: lúc đó máy sụp nguồn giữa trận
436
+ * là chuyện của thiết bị gác sân, và nối lại đúng phiên cũ mới giữ được link xem
437
+ * và VOD liền mạch. Máy không gán sân thì vẫn dọn sạch như cũ.
438
+ */
439
+ /**
440
+ * Được phép async có chủ đích: câu trả lời đúng nằm TRÊN ĐĨA (sân đã gán có
441
+ * persist), còn React state lúc engine khởi động thường CHƯA restore xong.
442
+ * Bản sync-đọc-state đã gây lỗi thật (18/08/2026): recover chạy trước restore
443
+ * → keep=false oan → app tự kết án phiên đáng giữ rồi arm phiên chờ THỨ HAI.
444
+ */
445
+ type KeepSessionOnRestart = () => boolean | Promise<boolean>;
349
446
  interface TelemetrySink {
350
447
  send(credentials: SdkTelemetryCredentials, events: readonly SdkTelemetryEvent[]): Promise<void>;
351
448
  dispose?(): void;
352
449
  }
450
+ /**
451
+ * Kênh nhận lệnh điều khiển cho mode device-rtmp: RTMP không có data channel
452
+ * nên start/stop/end từ server (match-start của trọng tài, dashboard) về qua
453
+ * HTTP poll, auth bằng chính telemetry token trong grant.
454
+ */
455
+ interface SessionCommandChannel {
456
+ poll(credentials: SdkTelemetryCredentials, after: number): Promise<SessionCommandPollResult>;
457
+ dispose?(): void;
458
+ }
459
+ /**
460
+ * Kênh nhận lệnh device-rtmp qua WEBSOCKET (Convex subscription) — thay poll
461
+ * HTTP 2s cho máy first-party nối thẳng Convex. Lệnh về tức thì, lúc rảnh gần
462
+ * như 0 function call (poll 2s ở 200 máy là ~518M call/tháng). Có kênh này thì
463
+ * engine hạ poll về nhịp reconcile chậm làm lưới an toàn; thiếu thì poll như cũ.
464
+ */
465
+ interface SessionCommandSubscription {
466
+ /**
467
+ * Mở subscription cho phiên; `onCommands` được gọi mỗi lần hàng lệnh đổi
468
+ * (kể cả lần đầu). `onError` báo websocket/token hỏng để engine tăng lại nhịp
469
+ * poll. Trả hàm huỷ subscription (KHÔNG đóng client — tái dùng qua phiên).
470
+ */
471
+ subscribe(credentials: SdkTelemetryCredentials, sessionId: string, onCommands: (commands: SessionCommandRecord[]) => void, onError?: (error: Error) => void): () => void;
472
+ dispose?(): void;
473
+ }
353
474
  interface EngineDependencies {
354
475
  sessionProvider: LivestreamSessionProvider;
355
476
  bootstrapInput: BootstrapInput;
356
477
  roomFactory: () => RoomAdapter;
478
+ keepSessionOnRestart?: KeepSessionOnRestart;
357
479
  runtime: SdkRuntime;
358
480
  storage: SdkStorage;
359
481
  network: SdkNetwork;
360
482
  telemetry?: TelemetrySink;
483
+ /** Kênh poll lệnh cho phiên device-rtmp; thiếu thì mode này điếc lệnh server. */
484
+ commandChannel?: SessionCommandChannel;
485
+ /**
486
+ * Kênh nhận lệnh qua websocket (first-party). Có thì lệnh về tức thì và poll
487
+ * tụt về nhịp reconcile; thiếu (máy đối tác qua proxy) thì chỉ dùng poll.
488
+ */
489
+ commandSubscription?: SessionCommandSubscription;
361
490
  /** Thu chỉ số thiết bị; gọi mỗi nhịp heartbeat khi đang phát. */
362
491
  deviceMetrics?: () => Promise<DeviceMetricsSample | null>;
363
492
  now?: () => number;
@@ -378,6 +507,16 @@ interface DeviceMetricsSample {
378
507
  storageFreeGb?: number;
379
508
  netType?: string;
380
509
  linkMbps?: number;
510
+ /** Cường độ sóng WiFi 0-100% — CHỈ Android (iOS không mở cho app). */
511
+ signalPct?: number;
512
+ /** IP nội bộ — phân biệt máy đang bám router nào, không cần quyền vị trí. */
513
+ localIp?: string;
514
+ /**
515
+ * Tần số kênh WiFi (MHz) — CHỈ Android. 2412–2484 = băng 2.4 GHz, ≥ 5000 =
516
+ * 5 GHz. Thay cho SSID (bị khoá sau quyền vị trí): 22/08/2026 một máy rơi từ
517
+ * AP 5 GHz của giải sang WiFi quán 2.4 GHz mà heartbeat không phân biệt được.
518
+ */
519
+ freqMhz?: number;
381
520
  rssi?: number;
382
521
  connQuality?: string;
383
522
  charging?: boolean;
@@ -400,6 +539,14 @@ interface LivestreamSnapshot {
400
539
  selectedCameraId: string | null;
401
540
  elapsedMs: number;
402
541
  isOnline: boolean;
542
+ /**
543
+ * Phiên cũ còn sống mà app vừa khởi động lại có thể NHẬN LẠI (thay vì tạo phiên
544
+ * mới). Chỉ khác null khi engine tìm thấy dấu của một phiên chưa được dừng có
545
+ * chủ ý VÀ caller nói phiên đó đáng giữ (keepSessionOnRestart). Tầng trên
546
+ * (AutoResume) đọc trường này để nối lại đúng phiên cũ — giữ nguyên link xem và
547
+ * VOD liền một mạch.
548
+ */
549
+ adoptableSessionId: string | null;
403
550
  }
404
551
  declare const PENDING_END_KEY = "@pickleball/expo-sdk/pending-end";
405
552
  declare const LEGAL_LIVESTREAM_TRANSITIONS: Readonly<Record<LivestreamState, readonly LivestreamState[]>>;
@@ -428,10 +575,23 @@ declare class LivestreamEngine {
428
575
  private backgroundTimer?;
429
576
  private standbyTimer?;
430
577
  private reconnectTimer?;
578
+ private redialTimer?;
579
+ /** Đang trong vòng quay số lại — xem doRetry() để biết vì sao cần cờ này. */
580
+ private redialing;
431
581
  private heartbeatTimer?;
432
582
  private elapsedTimer?;
433
583
  private tokenRefreshTimer?;
434
584
  private maxSessionTimer?;
585
+ private commandPollTimer?;
586
+ /** Huỷ subscription websocket lệnh của phiên hiện tại (nếu đang mở). */
587
+ private commandUnsubscribe?;
588
+ /** Khoá phiên+token đang subscribe — đổi thì resubscribe (token refresh/đổi phiên). */
589
+ private commandSubscriptionKey?;
590
+ /** Websocket đã đẩy được ít nhất một lần → coi là sống, cho phép hạ nhịp poll. */
591
+ private commandSubscriptionActive;
592
+ /** issuedAt của lệnh cuối đã xử lý — server chỉ trả lệnh mới hơn mốc này. */
593
+ private commandCursor;
594
+ private commandPollBackoffMs;
435
595
  private grant?;
436
596
  private startedAt?;
437
597
  /** Đang publish track (live) hay chỉ ngồi trong phòng (standby). */
@@ -471,16 +631,81 @@ declare class LivestreamEngine {
471
631
  handleRoomEvent(event: RoomEvent): void;
472
632
  handleAppState(state: "active" | "background" | "inactive"): void;
473
633
  private ensureBackgroundGraceTimer;
474
- dispose(): Promise<void>;
634
+ /**
635
+ * `keepSession` = engine bị tháo nhưng phiên phải sống tiếp để lần mở app sau
636
+ * nhận lại. Mặc định false: giữ NGUYÊN hành vi cũ cho mọi SDK đối tác.
637
+ *
638
+ * Lưu ý dispose KHÔNG BAO GIỜ là đường của "người dùng bấm dừng" — nút dừng đi
639
+ * qua doStop, mà doStop xoá ownedSessionId trước, nên tới đây sessionId đã là
640
+ * undefined. Vì vậy nhánh end ở đây luôn là teardown NGOÀI Ý MUỐN (app bị giết,
641
+ * component remount), đúng chỗ cần giữ phiên.
642
+ */
643
+ dispose(options?: {
644
+ keepSession?: boolean;
645
+ }): Promise<void>;
475
646
  private replayPendingEnd;
476
647
  private doReplayPendingEnd;
648
+ /**
649
+ * Phiên tưởng đáng nhận lại hoá ra đã chết phía server → BỎ dấu để máy arm
650
+ * phiên mới được.
651
+ *
652
+ * Thiếu hàm này là máy kẹt cứng: dấu vẫn nằm đó chặn mọi lượt start (đúng vai
653
+ * trò chống phiên song song của nó), mà lượt start duy nhất được cho qua lại là
654
+ * lượt nhận lại chính cái phiên đã chết — không bao giờ thành công. Đã dính
655
+ * thật 18/08/2026: lưới standby 30 phút kết thúc phiên trong lúc máy ngủ, mở
656
+ * lại thì app đứng ở "error" vĩnh viễn.
657
+ */
658
+ abandonAdoptableSession(sessionId: string): Promise<void>;
477
659
  private recoverStartupSession;
660
+ /**
661
+ * Dấu phiên cũ chặn thao tác mới cho tới khi nó được giải quyết.
662
+ *
663
+ * `externalSessionId` = id lượt start đang xin đi qua; bỏ trống nghĩa là
664
+ * prepare (mở camera), vốn phải luôn được phép khi đang định nhận lại — không
665
+ * mở được camera thì không nhận lại được, và đó chính là bẫy làm máy kẹt cứng ở
666
+ * màn lỗi "The previous livestream is still pending end".
667
+ */
478
668
  private ensurePendingEndResolved;
479
669
  private readPendingEnd;
480
670
  private ownsInMemorySession;
481
671
  private startTimers;
482
672
  private clearTimers;
673
+ /**
674
+ * Quay số lại với backoff cho adapter cần reconnect thủ công. Mỗi nhịp gọi
675
+ * retry() (refresh credentials + room.connect + enterSession). Thành công →
676
+ * enterSession phát "publishing" → nhánh "reconnected" dọn cả hai timer.
677
+ * Thất bại → doRetry() đưa state về "error" (vẫn còn sessionId) và nhịp sau
678
+ * retry() lại kéo về "reconnecting" — lặp tới khi doom timer 30s kết liễu.
679
+ * Backoff thưa dần để không dội server khi ingest thật sự sập.
680
+ */
681
+ private scheduleRedial;
682
+ private clearRedialTimer;
683
+ private stopRedialing;
483
684
  private clearReconnectTimer;
685
+ /**
686
+ * Poll lệnh chỉ khi phiên là device-rtmp (grant rtmp://) và đang standby/
687
+ * live. Ngoài hai trạng thái đó không có gì để điều khiển: connecting chưa
688
+ * có phiên chạy, reconnecting đã có reconnect timer lo, ending/ended thì thôi.
689
+ */
690
+ private commandPollingWanted;
691
+ private syncCommandPolling;
692
+ /**
693
+ * Mở/đổi websocket nhận lệnh cho phiên hiện tại. Chỉ máy first-party có
694
+ * dependency này (đối tác qua proxy thì undefined → chỉ poll). Resubscribe
695
+ * khi đổi phiên HOẶC token (refresh) vì cả hai đi vào khoá.
696
+ */
697
+ private syncCommandSubscription;
698
+ private stopCommandSubscription;
699
+ private stopCommandPolling;
700
+ private scheduleCommandPoll;
701
+ private commandPollInterval;
702
+ /**
703
+ * Áp lệnh từ poll HOẶC subscription vào cùng một state machine. Subscription
704
+ * trả cả lệnh CŨ (after:0) nên phải lọc theo con trỏ ở đây; poll đã lọc
705
+ * server-side (after=cursor) nên bước lọc này vô hại với nó.
706
+ */
707
+ private applyCommands;
708
+ private pollSessionCommands;
484
709
  /** Heartbeat kèm chỉ số thiết bị (nếu collector được cung cấp). */
485
710
  private sendHeartbeat;
486
711
  private sendTelemetry;
@@ -513,4 +738,4 @@ declare class LivestreamEngine {
513
738
  private ensureOperation;
514
739
  }
515
740
 
516
- export { UnsupportedRuntimeError as A, type BootstrapInput as B, type CameraDeviceInfo as C, type DeviceMetricsSample as D, type EndLivestreamInput as E, UnsupportedSdkError as F, compareSemver as G, errorFromCode as H, InvalidResponseError as I, isSdkRemoteConfig as J, isSdkVersionSupported as K, type LivestreamSessionProvider as L, isSessionGoneError as M, NetworkUnavailableError as N, normalizeError as O, PermissionDeniedError as P, LivestreamEngine as Q, RecordingFailureError as R, type StartLivestreamInput as S, type TelemetrySink as T, UnknownSdkError as U, type RoomConnection as V, type RoomEvent as W, type EngineDependencies as X, LEGAL_LIVESTREAM_TRANSITIONS as Y, PENDING_END_KEY as Z, type CaptureOrientation as a, ConsentRequiredError as b, type EndReason as c, ExpoSdkConfigurationError as d, type ExpoSdkErrorCode as e, ExpoSdkTimeoutError as f, type LivestreamSnapshot as g, type LivestreamState as h, PickleballExpoError as i, type RoomAdapter as j, SDK_TELEMETRY_EVENT_NAMES as k, type SdkBootstrap as l, SdkDisabledError as m, type SdkError as n, type SdkErrorCode as o, type SdkNetwork as p, type SdkRemoteConfig as q, type SdkRuntime as r, type SdkSessionGrant as s, type SdkSessionRefresh as t, type SdkStorage as u, type SdkTelemetryCredentials as v, type SdkTelemetryEvent as w, type SdkTelemetryEventName as x, SessionConflictError as y, TokenExpiredError as z };
741
+ export { normalizeError as $, type SdkTelemetryEvent as A, type BootstrapInput as B, type CameraGrant as C, type DeviceMetricsSample as D, type EndLivestreamInput as E, type SdkTelemetryEventName as F, type SessionCommandAction as G, type SessionCommandChannel as H, InvalidResponseError as I, type SessionCommandPollResult as J, type SessionCommandRecord as K, type LivestreamSessionProvider as L, SessionConflictError as M, NetworkUnavailableError as N, TokenExpiredError as O, PermissionDeniedError as P, UnsupportedRuntimeError as Q, type RoomAdapter as R, type SessionCommandSubscription as S, type TelemetrySink as T, UnknownSdkError as U, UnsupportedSdkError as V, compareSemver as W, errorFromCode as X, isSdkRemoteConfig as Y, isSdkVersionSupported as Z, isSessionGoneError as _, type SdkStorage as a, LivestreamEngine as a0, type RoomConnection as a1, type RoomEvent as a2, type EngineDependencies as a3, LEGAL_LIVESTREAM_TRANSITIONS as a4, PENDING_END_KEY as a5, type SdkSessionGrant as b, type StartLivestreamInput as c, type SdkRemoteConfig as d, type CameraDeviceInfo as e, type CameraGrantStatus as f, type CaptureOrientation as g, ConsentRequiredError as h, type EndReason as i, ExpoSdkConfigurationError as j, type ExpoSdkErrorCode as k, ExpoSdkTimeoutError as l, type LivestreamSnapshot as m, type LivestreamState as n, PickleballExpoError as o, RecordingFailureError as p, SDK_TELEMETRY_EVENT_NAMES as q, type SdkBootstrap as r, type SdkConflictReason as s, SdkDisabledError as t, type SdkError as u, type SdkErrorCode as v, type SdkNetwork as w, type SdkRuntime as x, type SdkSessionRefresh as y, type SdkTelemetryCredentials as z };