@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,23 +2,20 @@ import type {
2
2
  BootstrapInput,
3
3
  EndLivestreamInput,
4
4
  LivestreamSessionProvider,
5
- SdkBootstrap,
6
- SdkSessionGrant,
7
- SdkSessionRefresh,
8
- SdkTelemetryCredentials,
9
5
  StartLivestreamInput,
10
6
  } from "./contracts";
11
- import { isSdkRemoteConfig } from "./contracts";
7
+ import { InvalidResponseError } from "./errors";
12
8
  import {
13
- ExpoSdkConfigurationError,
14
- ExpoSdkTimeoutError,
15
- InvalidResponseError,
16
- NetworkUnavailableError,
17
- PickleballExpoError,
18
- errorFromCode,
19
- } from "./errors";
9
+ createJsonPoster,
10
+ isBootstrap,
11
+ isSessionGrant,
12
+ isSessionRefresh,
13
+ isSessionStatus,
14
+ joinUrl,
15
+ normalizeBaseUrl,
16
+ type Fetch,
17
+ } from "./session-grant";
20
18
 
21
- type Fetch = typeof globalThis.fetch;
22
19
  type HeadersInput = Record<string, string>;
23
20
 
24
21
  export interface HttpSessionProviderPaths {
@@ -31,7 +28,11 @@ export interface HttpSessionProviderPaths {
31
28
  }
32
29
 
33
30
  export interface HttpSessionProviderOptions {
34
- /** Base URL of the partner-owned mobile proxy. Never the Pickleball API URL. */
31
+ /**
32
+ * Base URL của proxy do đối tác host (mô hình cũ) hoặc namespace first-party
33
+ * `/api/v2/device-app` (app fleet). Không bao giờ là URL PickLive kèm API key.
34
+ * App đối tác dùng camera grant thì dùng createCameraGrantProvider thay vì đây.
35
+ */
35
36
  baseUrl: string;
36
37
  fetch?: Fetch;
37
38
  headers?: HeadersInput | (() => HeadersInput | Promise<HeadersInput>);
@@ -53,48 +54,15 @@ export function createHttpSessionProvider(
53
54
  options: HttpSessionProviderOptions,
54
55
  ): LivestreamSessionProvider {
55
56
  const baseUrl = normalizeBaseUrl(options.baseUrl);
56
- const fetchImplementation = options.fetch ?? globalThis.fetch;
57
- if (typeof fetchImplementation !== "function") {
58
- throw new ExpoSdkConfigurationError("A Fetch API implementation is required");
59
- }
57
+ const postJson = createJsonPoster({ fetch: options.fetch, timeoutMs: options.timeoutMs });
60
58
  const paths = { ...defaultPaths, ...options.paths };
61
59
 
62
60
  async function post(path: string, body: unknown): Promise<unknown> {
63
- const controller = new AbortController();
64
- const timeout = setTimeout(
65
- () => controller.abort(),
66
- options.timeoutMs ?? 10_000,
67
- );
68
- try {
69
- const extraHeaders =
70
- typeof options.headers === "function"
71
- ? await options.headers()
72
- : (options.headers ?? {});
73
- const response = await fetchImplementation(joinUrl(baseUrl, path), {
74
- method: "POST",
75
- headers: { "Content-Type": "application/json", ...extraHeaders },
76
- body: JSON.stringify(body),
77
- signal: controller.signal,
78
- redirect: "error",
79
- });
80
- const envelope = await response.json().catch(() => {
81
- throw new InvalidResponseError();
82
- });
83
- if (!response.ok) throw parseProxyError(envelope, response.status);
84
- if (!isRecord(envelope) || !("data" in envelope)) {
85
- throw new InvalidResponseError();
86
- }
87
- return envelope.data;
88
- } catch (error) {
89
- if (controller.signal.aborted) throw new ExpoSdkTimeoutError();
90
- if (error instanceof PickleballExpoError) throw error;
91
- if (error instanceof TypeError) {
92
- throw new NetworkUnavailableError(undefined, { cause: error });
93
- }
94
- throw error;
95
- } finally {
96
- clearTimeout(timeout);
97
- }
61
+ const extraHeaders =
62
+ typeof options.headers === "function"
63
+ ? await options.headers()
64
+ : (options.headers ?? {});
65
+ return postJson(joinUrl(baseUrl, path), body, extraHeaders);
98
66
  }
99
67
 
100
68
  return {
@@ -127,139 +95,3 @@ export function createHttpSessionProvider(
127
95
  },
128
96
  };
129
97
  }
130
-
131
- declare const __DEV__: boolean | undefined;
132
-
133
- // RFC 1918 private ranges: 10/8, 172.16/12, 192.168/16.
134
- const PRIVATE_LAN_HOST =
135
- /^(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})$/;
136
-
137
- function normalizeBaseUrl(value: string) {
138
- try {
139
- const url = new URL(value);
140
- const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
141
- const secure = url.protocol === "https:";
142
- const localDevelopment =
143
- url.protocol === "http:" && loopbackHosts.has(url.hostname);
144
- // Dev bundle only (Metro/dev client): a device on the same network must
145
- // reach the dev proxy over plain HTTP at a private LAN address. Release
146
- // bundles compile __DEV__ to false, so production keeps the HTTPS rule.
147
- const devPrivateLan =
148
- typeof __DEV__ !== "undefined" &&
149
- __DEV__ === true &&
150
- url.protocol === "http:" &&
151
- PRIVATE_LAN_HOST.test(url.hostname);
152
- if (
153
- (!secure && !localDevelopment && !devPrivateLan) ||
154
- url.username ||
155
- url.password
156
- ) {
157
- throw new Error();
158
- }
159
- return value.replace(/\/+$/, "");
160
- } catch {
161
- throw new ExpoSdkConfigurationError(
162
- "baseUrl must use HTTPS (HTTP is allowed only for localhost loopback)",
163
- );
164
- }
165
- }
166
-
167
- function joinUrl(base: string, path: string) {
168
- return `${base}/${path.replace(/^\/+/, "")}`;
169
- }
170
-
171
- function parseProxyError(envelope: unknown, httpStatus: number) {
172
- const error =
173
- isRecord(envelope) &&
174
- isRecord(envelope.error) &&
175
- typeof envelope.error.code === "string"
176
- ? errorFromCode(
177
- envelope.error.code,
178
- typeof envelope.error.message === "string"
179
- ? envelope.error.message
180
- : undefined,
181
- )
182
- : new InvalidResponseError();
183
- // Mã lỗi trong envelope không phân biệt được "phiên đã biến mất" (404) với
184
- // "server đang trục trặc" (5xx) — cả hai đều là UNKNOWN. Giữ lại mã HTTP để
185
- // engine quyết được: 404 thì xoá dấu pending-end, 5xx thì giữ và thử lại.
186
- return Object.assign(error, { httpStatus });
187
- }
188
-
189
-
190
- function isRecord(value: unknown): value is Record<string, unknown> {
191
- return typeof value === "object" && value !== null && !Array.isArray(value);
192
- }
193
-
194
- function isFiniteNumber(value: unknown): value is number {
195
- return typeof value === "number" && Number.isFinite(value);
196
- }
197
-
198
- function isBootstrap(value: unknown): value is SdkBootstrap {
199
- return (
200
- isRecord(value) &&
201
- isSdkRemoteConfig(value.config) &&
202
- isFiniteNumber(value.serverTime) &&
203
- typeof value.rolloutEnabled === "boolean" &&
204
- (value.message === undefined || typeof value.message === "string")
205
- );
206
- }
207
-
208
- function isTelemetry(value: unknown): value is SdkTelemetryCredentials {
209
- return (
210
- isRecord(value) &&
211
- typeof value.endpoint === "string" &&
212
- /^https:\/\//.test(value.endpoint) &&
213
- typeof value.token === "string" &&
214
- value.token.length > 0 &&
215
- isFiniteNumber(value.expiresAt)
216
- );
217
- }
218
-
219
- function isSessionGrant(value: unknown): value is SdkSessionGrant {
220
- return (
221
- isRecord(value) &&
222
- typeof value.sessionId === "string" &&
223
- value.sessionId.length > 0 &&
224
- typeof value.serverUrl === "string" &&
225
- // wss:// cho mode LiveKit, rtmp(s):// cho mode device-rtmp. Trước đây chỉ
226
- // chấp nhận wss:// nên grant RTMP bị coi là "phản hồi máy chủ không hợp lệ".
227
- // Vẫn chặn ws:// trần để không rơi xuống WebSocket không mã hoá.
228
- /^(wss|rtmps?):\/\//.test(value.serverUrl) &&
229
- typeof value.participantToken === "string" &&
230
- value.participantToken.length > 0 &&
231
- (value.playbackUrl === null || typeof value.playbackUrl === "string") &&
232
- isFiniteNumber(value.tokenExpiresAt) &&
233
- isTelemetry(value.telemetry) &&
234
- isSdkRemoteConfig(value.config)
235
- );
236
- }
237
-
238
- function isSessionStatus(
239
- value: unknown,
240
- ): value is { sessionId: string; status: string } {
241
- return (
242
- isRecord(value) &&
243
- typeof value.sessionId === "string" &&
244
- value.sessionId.length > 0 &&
245
- typeof value.status === "string" &&
246
- value.status.length > 0
247
- );
248
- }
249
-
250
- function isSessionRefresh(value: unknown): value is SdkSessionRefresh {
251
- if (!isRecord(value) || !isSdkRemoteConfig(value.config)) return false;
252
- if (
253
- value.participantToken !== undefined &&
254
- (typeof value.participantToken !== "string" || !value.participantToken)
255
- ) {
256
- return false;
257
- }
258
- if (
259
- value.tokenExpiresAt !== undefined &&
260
- !isFiniteNumber(value.tokenExpiresAt)
261
- ) {
262
- return false;
263
- }
264
- return value.telemetry === undefined || isTelemetry(value.telemetry);
265
- }
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Entry gọn cho tích hợp livestream cơ bản. Các tính năng khác nằm ở subpath:
2
+ * Entry gọn cho tích hợp livestream cơ bản (SDK 1.0: RTMP-only, camera grant).
3
+ * Các tính năng khác nằm ở subpath:
4
+ * @pickleball/expo-sdk/ui — màn hình camera + sheet cấu hình dựng sẵn
5
+ * @pickleball/expo-sdk/realtime — nhận lệnh qua websocket Convex (kéo `convex`)
3
6
  * @pickleball/expo-sdk/match — scoreboard + persistence
4
7
  * @pickleball/expo-sdk/match/voice — đọc điểm bằng loa
5
8
  * @pickleball/expo-sdk/match/remote — remote BLE/HID
6
- * @pickleball/expo-sdk/device-agent — điều khiển từ dashboard
9
+ * @pickleball/expo-sdk/device-agent — điều khiển từ dashboard (app fleet)
7
10
  * @pickleball/expo-sdk/advanced — engine internals, DI, mock/test
8
11
  * Metro không tree-shake nên chỉ import subpath nào app thật sự dùng.
9
12
  */
@@ -17,14 +20,88 @@ export {
17
20
  type LivestreamContextValue,
18
21
  type LivestreamTheme,
19
22
  } from "./provider";
20
- export { LivestreamScreen, type LivestreamScreenProps } from "./livestream-screen";
21
23
  export {
22
24
  createHttpSessionProvider,
23
25
  type HttpSessionProviderOptions,
24
26
  type HttpSessionProviderPaths,
25
27
  } from "./http-session-provider";
26
- // Partner dùng để persist token/cấu hình (expo-sqlite localStorage)
27
- export { createDefaultStorage } from "./native-runtime";
28
+ // Điện thoại đối tác camera grant từ backend đối tác (ADR-0002)
29
+ export {
30
+ CAMERA_GRANT_STORAGE_KEY,
31
+ DEFAULT_LOCAL_CONFIG,
32
+ createCameraGrantProvider,
33
+ useCameraGrant,
34
+ type CameraGrantHandle,
35
+ type CameraGrantPhase,
36
+ type CameraGrantProvider,
37
+ type CameraGrantProviderOptions,
38
+ } from "./camera-grant-provider";
39
+ // Nhận lại phiên sau khi app bị kill
40
+ export {
41
+ createHttpResumeFetcher,
42
+ grantFromRefreshPayload,
43
+ withResume,
44
+ type HttpResumeFetcherOptions,
45
+ type ResumableSessionProvider,
46
+ type ResumeAttempt,
47
+ } from "./resume";
48
+ export {
49
+ RESUME_RETRY_DELAYS_MS,
50
+ classifyResumeStatus,
51
+ resumeBlocksArm,
52
+ resumeExternalSessionId,
53
+ resumeGrantMatches,
54
+ resumeRetryDelayMs,
55
+ type ResumeVerdict,
56
+ } from "./resume-policy";
57
+ export {
58
+ PickleballAutoPrepare,
59
+ PickleballAutoResume,
60
+ useStandbySession,
61
+ type PickleballAutoPrepareProps,
62
+ type PickleballAutoResumeProps,
63
+ type ResumeAttemptOutcome,
64
+ type UseStandbySessionOptions,
65
+ } from "./auto-resume";
66
+ export { withLocalVideoQuality } from "./local-video-quality";
67
+ // Cấu hình camera local + điều khiển runtime + đo mạng
68
+ export {
69
+ CAMERA_SETTINGS_STORAGE_KEY,
70
+ DEFAULT_CAMERA_SETTINGS,
71
+ cameraSettingsKey,
72
+ createCameraSettingsStore,
73
+ parseCameraSettings,
74
+ rtmpRoomFactoryFor,
75
+ useCameraSettings,
76
+ type AntiFlickerMode,
77
+ type CameraSettings,
78
+ type CameraSettingsStore,
79
+ type CameraSettingsStoreOptions,
80
+ } from "./camera-settings";
81
+ export {
82
+ applyAntiFlicker,
83
+ getCameraCapabilities,
84
+ requestNativePermissions,
85
+ setExposureBias,
86
+ setExposureLocked,
87
+ setFocusLocked,
88
+ setTorch,
89
+ setZoom,
90
+ type CameraCapabilities,
91
+ type CameraCapabilityRange,
92
+ } from "./camera-controls";
93
+ export {
94
+ AUDIO_BITRATE_BPS,
95
+ DEFAULT_UPLOAD_URL,
96
+ VIDEO_BITRATE_BPS,
97
+ requiredMbpsFor,
98
+ runUplinkCheck,
99
+ verdictFor,
100
+ type UplinkCheckOptions,
101
+ type UplinkCheckResult,
102
+ } from "./uplink-check";
103
+ // Runtime RTMP-only + roomFactory RTMP (mode mặc định cho app camera)
104
+ export { createDefaultStorage, createRtmpRoomFactory, createRtmpRuntime } from "./native-runtime";
28
105
  // Type-only: xuất hiện trong LivestreamContextValue/ProviderProps
29
106
  export type {
30
107
  CameraDeviceInfo,
@@ -35,6 +112,8 @@ export type {
35
112
  SdkNetwork,
36
113
  SdkRuntime,
37
114
  SdkStorage,
115
+ SessionCommandChannel,
116
+ SessionCommandSubscription,
38
117
  TelemetrySink,
39
118
  } from "./engine";
40
119
  export { PICKLEBALL_EXPO_SDK_VERSION } from "./version";
@@ -0,0 +1,43 @@
1
+ import type { LivestreamSessionProvider } from "./contracts";
2
+
3
+ /**
4
+ * Độ phân giải là lựa chọn LOCAL của máy — override `videoQuality` mà server trả
5
+ * trong bootstrap/start/refresh (engine truyền config.videoQuality vào
6
+ * room.preparePreview nên không cần sửa engine).
7
+ *
8
+ * Lúc TẠO phiên phải khai ngược lên server (`quality` trong StartLivestreamInput),
9
+ * nếu không bản ghi phiên nói dối: mặc định local 720 còn server 1080 — dashboard
10
+ * báo 1080 trong khi máy phát 720 suốt (18/08/2026). Chốt MỘT lần lúc start:
11
+ * gọi `resolution()` hai lần có thể ra hai giá trị khác nhau, khai một đằng dùng
12
+ * một nẻo thì tệ hơn là không khai.
13
+ */
14
+ export function withLocalVideoQuality<P extends LivestreamSessionProvider>(
15
+ provider: P,
16
+ resolution: () => 720 | 1080,
17
+ ): P {
18
+ // Spread giữ nguyên MỌI method khác của provider (resume/adoptResumeGrant của
19
+ // ResumableSessionProvider, getActiveGrant/subscribe của CameraGrantProvider)
20
+ // — bọc mà rơi mất chúng là đường nhận lại phiên gãy im lặng ở tầng type.
21
+ // Provider là object thuần (closure) nên spread đủ; class có method prototype
22
+ // thì không hỗ trợ.
23
+ return {
24
+ ...provider,
25
+ async bootstrap(input) {
26
+ const result = await provider.bootstrap(input);
27
+ return { ...result, config: { ...result.config, videoQuality: resolution() } };
28
+ },
29
+ async start(input) {
30
+ const quality = resolution();
31
+ const result = await provider.start({ ...input, quality });
32
+ return { ...result, config: { ...result.config, videoQuality: quality } };
33
+ },
34
+ async refresh(sessionId) {
35
+ const result = await provider.refresh(sessionId);
36
+ return { ...result, config: { ...result.config, videoQuality: resolution() } };
37
+ },
38
+ end: (input) => provider.end(input),
39
+ // publish/unpublish là optional trong contract — forward nguyên trạng.
40
+ publish: provider.publish ? (sessionId) => provider.publish!(sessionId) : undefined,
41
+ unpublish: provider.unpublish ? (sessionId) => provider.unpublish!(sessionId) : undefined,
42
+ } as P;
43
+ }