@pickleball/expo-sdk 0.1.1 → 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 (117) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +163 -40
  3. package/dist/advanced-DcbHBCGb.d.ts +300 -0
  4. package/dist/advanced-DeQwg7MK.d.cts +300 -0
  5. package/dist/advanced.cjs +47 -0
  6. package/dist/advanced.d.cts +3 -0
  7. package/dist/advanced.d.ts +3 -0
  8. package/dist/advanced.js +47 -0
  9. package/dist/camera-controls-By8tHigY.d.cts +97 -0
  10. package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
  11. package/dist/chunk-2JHULJZ7.js +265 -0
  12. package/dist/chunk-62YV3PCD.cjs +139 -0
  13. package/dist/chunk-6F3HN6US.js +50 -0
  14. package/dist/chunk-6YE5BL3A.cjs +50 -0
  15. package/dist/chunk-7OX2DSKI.cjs +33 -0
  16. package/dist/chunk-7Z7TN5N3.js +318 -0
  17. package/dist/chunk-DHDX3ZOO.js +800 -0
  18. package/dist/chunk-GXQYMPHE.cjs +9 -0
  19. package/dist/chunk-IUIRVKJI.cjs +2290 -0
  20. package/dist/chunk-KFQGP6VL.js +33 -0
  21. package/dist/chunk-LCWGNGEN.cjs +265 -0
  22. package/dist/chunk-TBUEXRFQ.cjs +318 -0
  23. package/dist/chunk-XGP2MN5R.js +2290 -0
  24. package/dist/chunk-XL4CISVQ.js +9 -0
  25. package/dist/chunk-YGSQ25UA.cjs +800 -0
  26. package/dist/chunk-ZSKSXOCZ.js +139 -0
  27. package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
  28. package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
  29. package/dist/device-agent/index.cjs +1427 -0
  30. package/dist/device-agent/index.d.cts +493 -0
  31. package/dist/device-agent/index.d.ts +493 -0
  32. package/dist/device-agent/index.js +1427 -0
  33. package/dist/engine-BSvSdCSE.d.cts +741 -0
  34. package/dist/engine-BSvSdCSE.d.ts +741 -0
  35. package/dist/index.cjs +426 -6174
  36. package/dist/index.d.cts +299 -1400
  37. package/dist/index.d.ts +299 -1400
  38. package/dist/index.js +413 -6063
  39. package/dist/match/index.cjs +2456 -0
  40. package/dist/match/index.d.cts +253 -0
  41. package/dist/match/index.d.ts +253 -0
  42. package/dist/match/index.js +2456 -0
  43. package/dist/match/remote/index.cjs +21 -0
  44. package/dist/match/remote/index.d.cts +67 -0
  45. package/dist/match/remote/index.d.ts +67 -0
  46. package/dist/match/remote/index.js +21 -0
  47. package/dist/match/voice/index.cjs +23 -0
  48. package/dist/match/voice/index.d.cts +114 -0
  49. package/dist/match/voice/index.d.ts +114 -0
  50. package/dist/match/voice/index.js +23 -0
  51. package/dist/plugin.cjs +52 -71
  52. package/dist/plugin.d.cts +28 -1
  53. package/dist/plugin.d.ts +28 -1
  54. package/dist/plugin.js +26 -23
  55. package/dist/realtime/index.cjs +31 -0
  56. package/dist/realtime/index.d.cts +15 -0
  57. package/dist/realtime/index.d.ts +15 -0
  58. package/dist/realtime/index.js +31 -0
  59. package/dist/types-CrmcX408.d.cts +176 -0
  60. package/dist/types-CrmcX408.d.ts +176 -0
  61. package/dist/types-DiKWjwvN.d.cts +96 -0
  62. package/dist/types-DiKWjwvN.d.ts +96 -0
  63. package/dist/ui/index.cjs +1297 -0
  64. package/dist/ui/index.d.cts +493 -0
  65. package/dist/ui/index.d.ts +493 -0
  66. package/dist/ui/index.js +1297 -0
  67. package/dist/version.cjs +4 -32
  68. package/dist/version.js +2 -1
  69. package/package.json +95 -15
  70. package/src/advanced.ts +49 -0
  71. package/src/auto-resume.tsx +259 -0
  72. package/src/camera-controls.ts +99 -0
  73. package/src/camera-grant-provider.ts +392 -0
  74. package/src/camera-settings.ts +133 -0
  75. package/src/contracts.ts +73 -0
  76. package/src/device-agent/agent.ts +696 -0
  77. package/src/device-agent/beat-policy.ts +46 -0
  78. package/src/device-agent/component.tsx +60 -0
  79. package/src/device-agent/convex-beat-transport.ts +140 -0
  80. package/src/device-agent/convex-command-subscription.ts +101 -0
  81. package/src/device-agent/convex-control-channel.ts +88 -0
  82. package/src/device-agent/http-transport.ts +164 -0
  83. package/src/device-agent/index.ts +26 -0
  84. package/src/device-agent/mqtt-channel.ts +379 -0
  85. package/src/device-agent/mqtt-codec.ts +347 -0
  86. package/src/device-agent/tournament.ts +352 -0
  87. package/src/engine.ts +601 -20
  88. package/src/errors.ts +16 -0
  89. package/src/http-session-provider.ts +21 -183
  90. package/src/index.ts +117 -14
  91. package/src/local-video-quality.ts +43 -0
  92. package/src/match/index.ts +29 -0
  93. package/src/match/provider.tsx +4 -2
  94. package/src/match/remote/hid-gesture.ts +10 -2
  95. package/src/match/remote/types.ts +6 -4
  96. package/src/match/voice/scheduler.ts +6 -1
  97. package/src/match/voice/service.ts +16 -4
  98. package/src/native-runtime.ts +283 -77
  99. package/src/plugin.ts +54 -20
  100. package/src/provider.tsx +129 -22
  101. package/src/realtime/index.ts +56 -0
  102. package/src/resume-policy.ts +85 -0
  103. package/src/resume.ts +132 -0
  104. package/src/rtmp-room-adapter.ts +348 -0
  105. package/src/session-grant.ts +256 -0
  106. package/src/ui/camera-screen.tsx +484 -0
  107. package/src/ui/controls-sheet.tsx +445 -0
  108. package/src/ui/copy.ts +326 -0
  109. package/src/ui/helpers.ts +105 -0
  110. package/src/ui/hooks.ts +59 -0
  111. package/src/ui/index.ts +42 -0
  112. package/src/ui/preview-view.tsx +39 -0
  113. package/src/ui/primitives.tsx +304 -0
  114. package/src/uplink-check.ts +173 -0
  115. package/dist/chunk-VHLUJQG5.js +0 -17
  116. package/src/livekit-room-adapter.ts +0 -353
  117. package/src/livestream-screen.tsx +0 -980
@@ -0,0 +1,741 @@
1
+ type LivestreamState = "idle" | "preparing" | "preview" | "connecting"
2
+ /** Đã vào phòng, chưa phát — chờ lệnh phát từ app hoặc dashboard. */
3
+ | "standby" | "live" | "reconnecting" | "ending" | "ended" | "error";
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";
14
+ interface SdkError {
15
+ code: SdkErrorCode;
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;
21
+ }
22
+ interface BootstrapInput {
23
+ sdkVersion: string;
24
+ platform: "ios" | "android";
25
+ bundleId: string;
26
+ installationId: string;
27
+ }
28
+ interface SdkRemoteConfig {
29
+ enabled: boolean;
30
+ protocolVersion: 1;
31
+ minSdkVersion: string;
32
+ recommendedSdkVersion: string;
33
+ videoQuality: 720 | 1080;
34
+ reconnectTimeoutMs: number;
35
+ backgroundGraceMs: number;
36
+ telemetryIntervalMs: number;
37
+ maxSessionDurationMs: number;
38
+ /** Tự kết thúc phiên nếu ngồi standby quá lâu không phát (mặc định 15 phút). */
39
+ standbyTimeoutMs?: number;
40
+ }
41
+ interface SdkBootstrap {
42
+ config: SdkRemoteConfig;
43
+ serverTime: number;
44
+ rolloutEnabled: boolean;
45
+ message?: string;
46
+ }
47
+ interface StartLivestreamInput {
48
+ externalSessionId: string;
49
+ title: string;
50
+ visibility?: "public" | "private";
51
+ consentVersion: string;
52
+ metadata?: Record<string, string>;
53
+ /**
54
+ * true: vào phòng ở chế độ chờ (không publish) — server giữ status
55
+ * "scheduled" và KHÔNG khởi động recording egress cho tới khi publish.
56
+ * Engine tự đặt cờ này theo options.autoPublish; app không cần truyền.
57
+ */
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;
67
+ }
68
+ interface SdkTelemetryCredentials {
69
+ endpoint: string;
70
+ token: string;
71
+ expiresAt: number;
72
+ }
73
+ interface SdkSessionGrant {
74
+ sessionId: string;
75
+ serverUrl: string;
76
+ participantToken: string;
77
+ playbackUrl: string | null;
78
+ tokenExpiresAt: number;
79
+ telemetry: SdkTelemetryCredentials;
80
+ config: SdkRemoteConfig;
81
+ }
82
+ interface SdkSessionRefresh {
83
+ participantToken?: string;
84
+ tokenExpiresAt?: number;
85
+ telemetry?: SdkTelemetryCredentials;
86
+ config: SdkRemoteConfig;
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
+ }
118
+ interface EndLivestreamInput {
119
+ sessionId: string;
120
+ reason: "user" | "background_timeout" | "network_timeout" | "standby_timeout" | "error";
121
+ }
122
+ type EndReason = EndLivestreamInput["reason"];
123
+ declare const SDK_TELEMETRY_EVENT_NAMES: readonly ["state_changed", "connection_quality", "reconnect", "error", "heartbeat", "session_ended"];
124
+ type SdkTelemetryEventName = (typeof SDK_TELEMETRY_EVENT_NAMES)[number];
125
+ interface SdkTelemetryEvent {
126
+ sessionId: string;
127
+ at: number;
128
+ name: SdkTelemetryEventName;
129
+ data?: Record<string, unknown>;
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
+ }
147
+ interface LivestreamSessionProvider {
148
+ bootstrap(input: BootstrapInput): Promise<SdkBootstrap>;
149
+ start(input: StartLivestreamInput): Promise<SdkSessionGrant>;
150
+ refresh(sessionId: string): Promise<SdkSessionRefresh>;
151
+ end(input: EndLivestreamInput): Promise<void>;
152
+ /**
153
+ * Báo server bắt đầu phát thật (standby → live): server bật recording
154
+ * egress + chuyển status live TRƯỚC khi engine publish track. Optional để
155
+ * giữ tương thích provider cũ — thiếu thì engine bỏ qua bước server.
156
+ */
157
+ publish?(sessionId: string): Promise<void>;
158
+ /** Báo server tạm dừng phát (live → standby): dừng egress, status về chờ. */
159
+ unpublish?(sessionId: string): Promise<void>;
160
+ }
161
+ declare function isSdkRemoteConfig(value: unknown): value is SdkRemoteConfig;
162
+ declare function compareSemver(left: string, right: string): -1 | 0 | 1;
163
+ declare function isSdkVersionSupported(version: string, minimum: string): boolean;
164
+
165
+ type ExpoSdkErrorCode = SdkErrorCode | "CONFIGURATION_ERROR" | "UNSUPPORTED_RUNTIME" | "INVALID_RESPONSE" | "TIMEOUT";
166
+ declare class PickleballExpoError extends Error {
167
+ readonly code: ExpoSdkErrorCode;
168
+ constructor(message: string, code: ExpoSdkErrorCode, options?: ErrorOptions);
169
+ }
170
+ declare const PermissionDeniedError_base: {
171
+ new (message?: string, options?: ErrorOptions): {
172
+ readonly code: ExpoSdkErrorCode;
173
+ name: string;
174
+ message: string;
175
+ stack?: string;
176
+ cause?: unknown;
177
+ };
178
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
179
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
180
+ stackTraceLimit: number;
181
+ };
182
+ declare class PermissionDeniedError extends PermissionDeniedError_base {
183
+ }
184
+ declare const NetworkUnavailableError_base: {
185
+ new (message?: string, options?: ErrorOptions): {
186
+ readonly code: ExpoSdkErrorCode;
187
+ name: string;
188
+ message: string;
189
+ stack?: string;
190
+ cause?: unknown;
191
+ };
192
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
193
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
194
+ stackTraceLimit: number;
195
+ };
196
+ declare class NetworkUnavailableError extends NetworkUnavailableError_base {
197
+ }
198
+ declare const TokenExpiredError_base: {
199
+ new (message?: string, options?: ErrorOptions): {
200
+ readonly code: ExpoSdkErrorCode;
201
+ name: string;
202
+ message: string;
203
+ stack?: string;
204
+ cause?: unknown;
205
+ };
206
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
207
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
208
+ stackTraceLimit: number;
209
+ };
210
+ declare class TokenExpiredError extends TokenExpiredError_base {
211
+ }
212
+ declare const UnsupportedSdkError_base: {
213
+ new (message?: string, options?: ErrorOptions): {
214
+ readonly code: ExpoSdkErrorCode;
215
+ name: string;
216
+ message: string;
217
+ stack?: string;
218
+ cause?: unknown;
219
+ };
220
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
221
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
222
+ stackTraceLimit: number;
223
+ };
224
+ declare class UnsupportedSdkError extends UnsupportedSdkError_base {
225
+ }
226
+ declare const UnsupportedRuntimeError_base: {
227
+ new (message?: string, options?: ErrorOptions): {
228
+ readonly code: ExpoSdkErrorCode;
229
+ name: string;
230
+ message: string;
231
+ stack?: string;
232
+ cause?: unknown;
233
+ };
234
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
235
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
236
+ stackTraceLimit: number;
237
+ };
238
+ declare class UnsupportedRuntimeError extends UnsupportedRuntimeError_base {
239
+ }
240
+ declare const SessionConflictError_base: {
241
+ new (message?: string, options?: ErrorOptions): {
242
+ readonly code: ExpoSdkErrorCode;
243
+ name: string;
244
+ message: string;
245
+ stack?: string;
246
+ cause?: unknown;
247
+ };
248
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
249
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
250
+ stackTraceLimit: number;
251
+ };
252
+ declare class SessionConflictError extends SessionConflictError_base {
253
+ }
254
+ declare const RecordingFailureError_base: {
255
+ new (message?: string, options?: ErrorOptions): {
256
+ readonly code: ExpoSdkErrorCode;
257
+ name: string;
258
+ message: string;
259
+ stack?: string;
260
+ cause?: unknown;
261
+ };
262
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
263
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
264
+ stackTraceLimit: number;
265
+ };
266
+ declare class RecordingFailureError extends RecordingFailureError_base {
267
+ }
268
+ declare const ConsentRequiredError_base: {
269
+ new (message?: string, options?: ErrorOptions): {
270
+ readonly code: ExpoSdkErrorCode;
271
+ name: string;
272
+ message: string;
273
+ stack?: string;
274
+ cause?: unknown;
275
+ };
276
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
277
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
278
+ stackTraceLimit: number;
279
+ };
280
+ declare class ConsentRequiredError extends ConsentRequiredError_base {
281
+ }
282
+ declare const SdkDisabledError_base: {
283
+ new (message?: string, options?: ErrorOptions): {
284
+ readonly code: ExpoSdkErrorCode;
285
+ name: string;
286
+ message: string;
287
+ stack?: string;
288
+ cause?: unknown;
289
+ };
290
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
291
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
292
+ stackTraceLimit: number;
293
+ };
294
+ declare class SdkDisabledError extends SdkDisabledError_base {
295
+ }
296
+ declare const ExpoSdkConfigurationError_base: {
297
+ new (message?: string, options?: ErrorOptions): {
298
+ readonly code: ExpoSdkErrorCode;
299
+ name: string;
300
+ message: string;
301
+ stack?: string;
302
+ cause?: unknown;
303
+ };
304
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
305
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
306
+ stackTraceLimit: number;
307
+ };
308
+ declare class ExpoSdkConfigurationError extends ExpoSdkConfigurationError_base {
309
+ }
310
+ declare const InvalidResponseError_base: {
311
+ new (message?: string, options?: ErrorOptions): {
312
+ readonly code: ExpoSdkErrorCode;
313
+ name: string;
314
+ message: string;
315
+ stack?: string;
316
+ cause?: unknown;
317
+ };
318
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
319
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
320
+ stackTraceLimit: number;
321
+ };
322
+ declare class InvalidResponseError extends InvalidResponseError_base {
323
+ }
324
+ declare const ExpoSdkTimeoutError_base: {
325
+ new (message?: string, options?: ErrorOptions): {
326
+ readonly code: ExpoSdkErrorCode;
327
+ name: string;
328
+ message: string;
329
+ stack?: string;
330
+ cause?: unknown;
331
+ };
332
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
333
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
334
+ stackTraceLimit: number;
335
+ };
336
+ declare class ExpoSdkTimeoutError extends ExpoSdkTimeoutError_base {
337
+ }
338
+ declare const UnknownSdkError_base: {
339
+ new (message?: string, options?: ErrorOptions): {
340
+ readonly code: ExpoSdkErrorCode;
341
+ name: string;
342
+ message: string;
343
+ stack?: string;
344
+ cause?: unknown;
345
+ };
346
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
347
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
348
+ stackTraceLimit: number;
349
+ };
350
+ declare class UnknownSdkError extends UnknownSdkError_base {
351
+ }
352
+ declare function errorFromCode(code: string, message?: string): PickleballExpoError;
353
+ declare function normalizeError(error: unknown): PickleballExpoError;
354
+ /**
355
+ * Phiên đã biến mất hẳn phía server (HTTP 404): bị watchdog dọn, thuộc
356
+ * deployment khác, hoặc ID không hợp lệ. Khác hẳn lỗi mạng/5xx — thử lại không
357
+ * bao giờ cứu được, nên caller phải bỏ dấu khôi phục thay vì chặn vĩnh viễn.
358
+ *
359
+ * Mã HTTP được gắn vào lỗi ở http-session-provider vì envelope trả về chỉ có
360
+ * code "UNKNOWN", không phân biệt được 404 với 5xx.
361
+ */
362
+ declare function isSessionGoneError(error: unknown): boolean;
363
+
364
+ interface RoomConnection {
365
+ serverUrl: string;
366
+ participantToken: string;
367
+ microphoneEnabled: boolean;
368
+ /** false: vào phòng nhưng chưa publish track (trạng thái standby). */
369
+ publish?: boolean;
370
+ }
371
+ type RoomEvent = {
372
+ type: "reconnecting";
373
+ } | {
374
+ type: "reconnected";
375
+ } | {
376
+ type: "disconnected";
377
+ reason?: string;
378
+ } | {
379
+ type: "media-error";
380
+ error: unknown;
381
+ }
382
+ /** Lệnh điều khiển từ dashboard qua LiveKit data channel ({"type":"cmd"}). */
383
+ | {
384
+ type: "command";
385
+ action: string;
386
+ };
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;
402
+ /** Bật/tắt publish track khi đã ở trong phòng (standby ↔ live). */
403
+ setPublishing?(enabled: boolean): Promise<void>;
404
+ setCaptureOrientation?(orientation: CaptureOrientation): Promise<void>;
405
+ listCameras?(): Promise<CameraDeviceInfo[]>;
406
+ selectCamera?(deviceId: string, facing: CameraDeviceInfo["facing"]): Promise<void>;
407
+ preparePreview(quality: 720 | 1080): Promise<unknown>;
408
+ connect(input: RoomConnection): Promise<void>;
409
+ disconnect(stopTracks?: boolean): Promise<void>;
410
+ switchCamera(): Promise<void>;
411
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
412
+ subscribe(listener: (event: RoomEvent) => void): () => void;
413
+ dispose(): Promise<void>;
414
+ }
415
+ interface SdkRuntime {
416
+ assertSupported(): void;
417
+ requestPermissions(): Promise<boolean>;
418
+ activateKeepAwake(): void | Promise<void>;
419
+ deactivateKeepAwake(): void | Promise<void>;
420
+ }
421
+ interface SdkStorage {
422
+ getItem(key: string): Promise<string | null>;
423
+ setItem(key: string, value: string): Promise<void>;
424
+ removeItem(key: string): Promise<void>;
425
+ }
426
+ interface SdkNetwork {
427
+ isOnline(): boolean;
428
+ subscribe(listener: (online: boolean) => void): () => void;
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>;
446
+ interface TelemetrySink {
447
+ send(credentials: SdkTelemetryCredentials, events: readonly SdkTelemetryEvent[]): Promise<void>;
448
+ dispose?(): void;
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
+ }
474
+ interface EngineDependencies {
475
+ sessionProvider: LivestreamSessionProvider;
476
+ bootstrapInput: BootstrapInput;
477
+ roomFactory: () => RoomAdapter;
478
+ keepSessionOnRestart?: KeepSessionOnRestart;
479
+ runtime: SdkRuntime;
480
+ storage: SdkStorage;
481
+ network: SdkNetwork;
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;
490
+ /** Thu chỉ số thiết bị; gọi mỗi nhịp heartbeat khi đang phát. */
491
+ deviceMetrics?: () => Promise<DeviceMetricsSample | null>;
492
+ now?: () => number;
493
+ }
494
+ type CaptureOrientation = "portrait" | "landscape";
495
+ interface CameraDeviceInfo {
496
+ deviceId: string;
497
+ label: string;
498
+ facing: "front" | "environment" | "unknown";
499
+ }
500
+ /** Chỉ số thiết bị gửi kèm heartbeat (trường trùng schema deviceMetrics). */
501
+ interface DeviceMetricsSample {
502
+ batteryPct?: number;
503
+ batteryTempC?: number;
504
+ thermal?: number;
505
+ ramFreeMb?: number;
506
+ ramTotalMb?: number;
507
+ storageFreeGb?: number;
508
+ netType?: string;
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;
520
+ rssi?: number;
521
+ connQuality?: string;
522
+ charging?: boolean;
523
+ }
524
+ interface LivestreamSnapshot {
525
+ state: LivestreamState;
526
+ error: PickleballExpoError | null;
527
+ config: SdkRemoteConfig | null;
528
+ sessionId: string | null;
529
+ previewTrack: unknown;
530
+ isMicrophoneEnabled: boolean;
531
+ isCameraFront: boolean;
532
+ /** Explicit pre-live choice; null follows the window orientation. */
533
+ captureOrientation: CaptureOrientation | null;
534
+ /** Mẫu chỉ số thiết bị mới nhất (cập nhật mỗi heartbeat khi live). */
535
+ deviceMetrics: DeviceMetricsSample | null;
536
+ /** Cameras reported by the platform after preview was prepared. */
537
+ cameras: CameraDeviceInfo[];
538
+ /** Explicitly chosen camera; null when following facing (front/back). */
539
+ selectedCameraId: string | null;
540
+ elapsedMs: number;
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;
550
+ }
551
+ declare const PENDING_END_KEY = "@pickleball/expo-sdk/pending-end";
552
+ declare const LEGAL_LIVESTREAM_TRANSITIONS: Readonly<Record<LivestreamState, readonly LivestreamState[]>>;
553
+ declare class LivestreamEngine {
554
+ private readonly dependencies;
555
+ private room?;
556
+ private readonly now;
557
+ private snapshot;
558
+ private listeners;
559
+ private preparePromise?;
560
+ private startPromise?;
561
+ private stopPromise?;
562
+ private retryPromise?;
563
+ private credentialRefreshPromise?;
564
+ private pendingReplayPromise?;
565
+ private startupRecoveryPromise?;
566
+ private startupRecovered;
567
+ private pendingReplayError?;
568
+ private ownedSessionId?;
569
+ private commandTail;
570
+ private commandCount;
571
+ private roomUnsubscribe;
572
+ private networkUnsubscribe;
573
+ private networkSubscribed;
574
+ private latestAppState;
575
+ private backgroundTimer?;
576
+ private standbyTimer?;
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;
581
+ private heartbeatTimer?;
582
+ private elapsedTimer?;
583
+ private tokenRefreshTimer?;
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;
595
+ private grant?;
596
+ private startedAt?;
597
+ /** Đang publish track (live) hay chỉ ngồi trong phòng (standby). */
598
+ private publishing;
599
+ /** Tổng thời gian đã phát trước lần tạm dừng gần nhất. */
600
+ private pausedElapsedMs;
601
+ private sessionDeadline?;
602
+ private refreshRetryAttempt;
603
+ private operationGeneration;
604
+ private liveTimersStarted;
605
+ private keepAwakeActive;
606
+ private telemetryInFlight?;
607
+ private pendingCriticalTelemetry?;
608
+ private readonly sessionTerminations;
609
+ private disposePromise?;
610
+ private disposed;
611
+ constructor(dependencies: EngineDependencies);
612
+ getSnapshot: () => LivestreamSnapshot;
613
+ subscribe: (listener: () => void) => () => boolean;
614
+ initialize(): Promise<void>;
615
+ prepare(): Promise<void>;
616
+ private doPrepare;
617
+ start(input: StartLivestreamInput, options?: {
618
+ autoPublish?: boolean;
619
+ }): Promise<void>;
620
+ private doStart;
621
+ stop(reason?: EndReason): Promise<void>;
622
+ private doStop;
623
+ retry(): Promise<void>;
624
+ private doRetry;
625
+ switchCamera(): Promise<void>;
626
+ /** Chọn một ống kính cụ thể từ snapshot.cameras. */
627
+ selectCamera(deviceId: string): Promise<void>;
628
+ /** Chọn kiểu quay trước khi live; không đổi khi phiên đang hoạt động. */
629
+ setCaptureOrientation(orientation: CaptureOrientation): Promise<void>;
630
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
631
+ handleRoomEvent(event: RoomEvent): void;
632
+ handleAppState(state: "active" | "background" | "inactive"): void;
633
+ private ensureBackgroundGraceTimer;
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>;
646
+ private replayPendingEnd;
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>;
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
+ */
668
+ private ensurePendingEndResolved;
669
+ private readPendingEnd;
670
+ private ownsInMemorySession;
671
+ private startTimers;
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;
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;
709
+ /** Heartbeat kèm chỉ số thiết bị (nếu collector được cung cấp). */
710
+ private sendHeartbeat;
711
+ private sendTelemetry;
712
+ private startTelemetrySend;
713
+ private refreshCredentials;
714
+ private transition;
715
+ private startStandbyTimer;
716
+ private clearStandbyTimer;
717
+ private fail;
718
+ private recordError;
719
+ private update;
720
+ private ensureState;
721
+ private ensureNotDisposed;
722
+ private ensureRoom;
723
+ private ensureNetworkSubscription;
724
+ private requireRoom;
725
+ private releaseRoom;
726
+ private enqueueLifecycle;
727
+ private scheduleTokenRefresh;
728
+ private scheduleMaxSessionDuration;
729
+ private restartHeartbeat;
730
+ private enterSession;
731
+ /** Bắt đầu phát khi đang standby (từ nút trên app hoặc lệnh dashboard). */
732
+ startPublishing(): Promise<void>;
733
+ /** Tạm dừng phát: ngắt kết nối LiveKit hoàn toàn về standby, KHÔNG kết thúc phiên. */
734
+ stopPublishing(): Promise<void>;
735
+ private ensureSessionTermination;
736
+ private abandonGrant;
737
+ private isOperationCurrent;
738
+ private ensureOperation;
739
+ }
740
+
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 };