@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.
- package/CHANGELOG.md +41 -0
- package/README.md +155 -101
- package/dist/advanced-DcbHBCGb.d.ts +300 -0
- package/dist/advanced-DeQwg7MK.d.cts +300 -0
- package/dist/advanced.cjs +14 -2
- package/dist/advanced.d.cts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.js +17 -5
- package/dist/camera-controls-By8tHigY.d.cts +97 -0
- package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
- package/dist/chunk-62YV3PCD.cjs +139 -0
- package/dist/chunk-6F3HN6US.js +50 -0
- package/dist/chunk-6YE5BL3A.cjs +50 -0
- package/dist/chunk-7Z7TN5N3.js +318 -0
- package/dist/{chunk-TRLASNPQ.cjs → chunk-GXQYMPHE.cjs} +1 -1
- package/dist/{chunk-TBPR2QND.cjs → chunk-IUIRVKJI.cjs} +736 -399
- package/dist/chunk-TBUEXRFQ.cjs +318 -0
- package/dist/{chunk-FODE2W2H.js → chunk-XGP2MN5R.js} +688 -351
- package/dist/{chunk-6JOMESNT.js → chunk-XL4CISVQ.js} +1 -1
- package/dist/chunk-ZSKSXOCZ.js +139 -0
- package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
- package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
- package/dist/device-agent/index.cjs +973 -44
- package/dist/device-agent/index.d.cts +339 -3
- package/dist/device-agent/index.d.ts +339 -3
- package/dist/device-agent/index.js +940 -11
- package/dist/{engine-CCSSqPbZ.d.cts → engine-BSvSdCSE.d.cts} +227 -2
- package/dist/{engine-CCSSqPbZ.d.ts → engine-BSvSdCSE.d.ts} +227 -2
- package/dist/index.cjs +419 -937
- package/dist/index.d.cts +315 -32
- package/dist/index.d.ts +315 -32
- package/dist/index.js +423 -941
- package/dist/plugin.cjs +11 -13
- package/dist/plugin.d.cts +15 -1
- package/dist/plugin.d.ts +15 -1
- package/dist/plugin.js +7 -9
- package/dist/realtime/index.cjs +31 -0
- package/dist/realtime/index.d.cts +15 -0
- package/dist/realtime/index.d.ts +15 -0
- package/dist/realtime/index.js +31 -0
- package/dist/ui/index.cjs +1297 -0
- package/dist/ui/index.d.cts +493 -0
- package/dist/ui/index.d.ts +493 -0
- package/dist/ui/index.js +1297 -0
- package/dist/version.cjs +2 -2
- package/dist/version.js +1 -1
- package/package.json +31 -15
- package/src/advanced.ts +11 -9
- package/src/auto-resume.tsx +259 -0
- package/src/camera-controls.ts +99 -0
- package/src/camera-grant-provider.ts +392 -0
- package/src/camera-settings.ts +133 -0
- package/src/contracts.ts +73 -0
- package/src/device-agent/agent.ts +262 -9
- package/src/device-agent/beat-policy.ts +46 -0
- package/src/device-agent/component.tsx +11 -0
- package/src/device-agent/convex-beat-transport.ts +140 -0
- package/src/device-agent/convex-command-subscription.ts +101 -0
- package/src/device-agent/convex-control-channel.ts +7 -1
- package/src/device-agent/http-transport.ts +22 -1
- package/src/device-agent/index.ts +22 -0
- package/src/device-agent/mqtt-channel.ts +379 -0
- package/src/device-agent/mqtt-codec.ts +347 -0
- package/src/device-agent/tournament.ts +352 -0
- package/src/engine.ts +580 -15
- package/src/http-session-provider.ts +21 -189
- package/src/index.ts +84 -5
- package/src/local-video-quality.ts +43 -0
- package/src/native-runtime.ts +234 -78
- package/src/plugin.ts +19 -6
- package/src/provider.tsx +94 -18
- package/src/realtime/index.ts +56 -0
- package/src/resume-policy.ts +85 -0
- package/src/resume.ts +132 -0
- package/src/rtmp-room-adapter.ts +164 -12
- package/src/session-grant.ts +256 -0
- package/src/ui/camera-screen.tsx +484 -0
- package/src/ui/controls-sheet.tsx +445 -0
- package/src/ui/copy.ts +326 -0
- package/src/ui/helpers.ts +105 -0
- package/src/ui/hooks.ts +59 -0
- package/src/ui/index.ts +42 -0
- package/src/ui/preview-view.tsx +39 -0
- package/src/ui/primitives.tsx +304 -0
- package/src/uplink-check.ts +173 -0
- package/dist/advanced-BXDrDl1h.d.ts +0 -316
- package/dist/advanced-BdAa2Erv.d.cts +0 -316
- package/src/livekit-room-adapter.ts +0 -390
- package/src/livestream-screen.tsx +0 -1002
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CameraGrant,
|
|
3
|
+
SdkBootstrap,
|
|
4
|
+
SdkRemoteConfig,
|
|
5
|
+
SdkSessionGrant,
|
|
6
|
+
SdkSessionRefresh,
|
|
7
|
+
SdkTelemetryCredentials,
|
|
8
|
+
} from "./contracts";
|
|
9
|
+
import { isSdkRemoteConfig } from "./contracts";
|
|
10
|
+
import {
|
|
11
|
+
ExpoSdkConfigurationError,
|
|
12
|
+
ExpoSdkTimeoutError,
|
|
13
|
+
InvalidResponseError,
|
|
14
|
+
NetworkUnavailableError,
|
|
15
|
+
PickleballExpoError,
|
|
16
|
+
errorFromCode,
|
|
17
|
+
} from "./errors";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validator + helper HTTP dùng chung cho hai session provider:
|
|
21
|
+
* - createHttpSessionProvider (proxy đối tác / first-party device-app)
|
|
22
|
+
* - createCameraGrantProvider (điện thoại đối tác, grant từ backend đối tác)
|
|
23
|
+
*
|
|
24
|
+
* Tách ra để hai provider không chép luật "grant thế nào là hợp lệ" ra hai nơi
|
|
25
|
+
* rồi lệch nhau; engine chỉ tin `isSdkRemoteConfig`, còn hình dạng grant là
|
|
26
|
+
* việc của lớp này.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export type Fetch = typeof globalThis.fetch;
|
|
30
|
+
|
|
31
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isFiniteNumber(value: unknown): value is number {
|
|
36
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isBootstrap(value: unknown): value is SdkBootstrap {
|
|
40
|
+
return (
|
|
41
|
+
isRecord(value) &&
|
|
42
|
+
isSdkRemoteConfig(value.config) &&
|
|
43
|
+
isFiniteNumber(value.serverTime) &&
|
|
44
|
+
typeof value.rolloutEnabled === "boolean" &&
|
|
45
|
+
(value.message === undefined || typeof value.message === "string")
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isTelemetryCredentials(value: unknown): value is SdkTelemetryCredentials {
|
|
50
|
+
return (
|
|
51
|
+
isRecord(value) &&
|
|
52
|
+
typeof value.endpoint === "string" &&
|
|
53
|
+
/^https:\/\//.test(value.endpoint) &&
|
|
54
|
+
typeof value.token === "string" &&
|
|
55
|
+
value.token.length > 0 &&
|
|
56
|
+
isFiniteNumber(value.expiresAt)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Grant phiên. SDK 1.0 là RTMP-only: `serverUrl` phải là rtmp(s):// (ingest).
|
|
62
|
+
* Grant LiveKit (wss://) của mô hình cũ bị từ chối ngay ở đây — lỗi
|
|
63
|
+
* INVALID_RESPONSE rõ ràng thay vì HaishinKit ném lúc connect.
|
|
64
|
+
*/
|
|
65
|
+
export function isSessionGrant(value: unknown): value is SdkSessionGrant {
|
|
66
|
+
return (
|
|
67
|
+
isRecord(value) &&
|
|
68
|
+
typeof value.sessionId === "string" &&
|
|
69
|
+
value.sessionId.length > 0 &&
|
|
70
|
+
typeof value.serverUrl === "string" &&
|
|
71
|
+
/^rtmps?:\/\//i.test(value.serverUrl) &&
|
|
72
|
+
typeof value.participantToken === "string" &&
|
|
73
|
+
value.participantToken.length > 0 &&
|
|
74
|
+
(value.playbackUrl === null || typeof value.playbackUrl === "string") &&
|
|
75
|
+
isFiniteNumber(value.tokenExpiresAt) &&
|
|
76
|
+
isTelemetryCredentials(value.telemetry) &&
|
|
77
|
+
isSdkRemoteConfig(value.config)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function optionalNullableString(value: unknown): boolean {
|
|
82
|
+
return value === undefined || value === null || typeof value === "string";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Camera grant (điện thoại đối tác): superset của SdkSessionGrant, BẮT BUỘC là
|
|
87
|
+
* RTMP và mang `status` để đường nhận lại biết phiên cũ đang phát hay đang chờ.
|
|
88
|
+
*/
|
|
89
|
+
export function isCameraGrant(value: unknown): value is CameraGrant {
|
|
90
|
+
return (
|
|
91
|
+
isSessionGrant(value) &&
|
|
92
|
+
/^rtmps?:\/\//i.test(value.serverUrl) &&
|
|
93
|
+
((value as { status?: unknown }).status === "scheduled" ||
|
|
94
|
+
(value as { status?: unknown }).status === "live") &&
|
|
95
|
+
optionalNullableString((value as { matchRef?: unknown }).matchRef) &&
|
|
96
|
+
optionalNullableString((value as { courtRef?: unknown }).courtRef) &&
|
|
97
|
+
optionalNullableString((value as { watchUrl?: unknown }).watchUrl) &&
|
|
98
|
+
optionalNullableString((value as { convexUrl?: unknown }).convexUrl)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isSessionRefresh(value: unknown): value is SdkSessionRefresh {
|
|
103
|
+
if (!isRecord(value) || !isSdkRemoteConfig(value.config)) return false;
|
|
104
|
+
if (
|
|
105
|
+
value.participantToken !== undefined &&
|
|
106
|
+
(typeof value.participantToken !== "string" || !value.participantToken)
|
|
107
|
+
) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (value.tokenExpiresAt !== undefined && !isFiniteNumber(value.tokenExpiresAt)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return value.telemetry === undefined || isTelemetryCredentials(value.telemetry);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isSessionStatus(
|
|
117
|
+
value: unknown,
|
|
118
|
+
): value is { sessionId: string; status: string } {
|
|
119
|
+
return (
|
|
120
|
+
isRecord(value) &&
|
|
121
|
+
typeof value.sessionId === "string" &&
|
|
122
|
+
value.sessionId.length > 0 &&
|
|
123
|
+
typeof value.status === "string" &&
|
|
124
|
+
value.status.length > 0
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Gốc API của phiên suy từ endpoint telemetry trong grant: `…/sessions/{id}/telemetry`
|
|
130
|
+
* → `…/sessions/{id}`. Đây là cách duy nhất SDK biết namespace (sdk / camera /
|
|
131
|
+
* device-app) mà không cần app cấu hình URL PickLive nào.
|
|
132
|
+
*/
|
|
133
|
+
export function sessionApiBaseFromTelemetryEndpoint(endpoint: string): string {
|
|
134
|
+
if (!/^https:\/\//.test(endpoint) || !endpoint.endsWith("/telemetry")) {
|
|
135
|
+
throw new InvalidResponseError("The telemetry endpoint must be an HTTPS …/telemetry URL");
|
|
136
|
+
}
|
|
137
|
+
return endpoint.slice(0, -"/telemetry".length);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
declare const __DEV__: boolean | undefined;
|
|
141
|
+
|
|
142
|
+
// RFC 1918 private ranges: 10/8, 172.16/12, 192.168/16.
|
|
143
|
+
const PRIVATE_LAN_HOST =
|
|
144
|
+
/^(?: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})$/;
|
|
145
|
+
|
|
146
|
+
export function normalizeBaseUrl(value: string): string {
|
|
147
|
+
try {
|
|
148
|
+
const url = new URL(value);
|
|
149
|
+
const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
150
|
+
const secure = url.protocol === "https:";
|
|
151
|
+
const localDevelopment = url.protocol === "http:" && loopbackHosts.has(url.hostname);
|
|
152
|
+
// Dev bundle only (Metro/dev client): a device on the same network must
|
|
153
|
+
// reach the dev proxy over plain HTTP at a private LAN address. Release
|
|
154
|
+
// bundles compile __DEV__ to false, so production keeps the HTTPS rule.
|
|
155
|
+
const devPrivateLan =
|
|
156
|
+
typeof __DEV__ !== "undefined" &&
|
|
157
|
+
__DEV__ === true &&
|
|
158
|
+
url.protocol === "http:" &&
|
|
159
|
+
PRIVATE_LAN_HOST.test(url.hostname);
|
|
160
|
+
if ((!secure && !localDevelopment && !devPrivateLan) || url.username || url.password) {
|
|
161
|
+
throw new Error();
|
|
162
|
+
}
|
|
163
|
+
return value.replace(/\/+$/, "");
|
|
164
|
+
} catch {
|
|
165
|
+
throw new ExpoSdkConfigurationError(
|
|
166
|
+
"baseUrl must use HTTPS (HTTP is allowed only for localhost loopback)",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function joinUrl(base: string, path: string) {
|
|
172
|
+
return `${base}/${path.replace(/^\/+/, "")}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Lỗi từ envelope `{error:{code,message}}` + mã HTTP. Mã HTTP đi kèm vì envelope
|
|
177
|
+
* không phân biệt được "phiên đã biến mất" (404) với "server trục trặc" (5xx) —
|
|
178
|
+
* engine cần 404/409 để quyết dừng quay số hay thử lại.
|
|
179
|
+
*/
|
|
180
|
+
export function parseEnvelopeError(
|
|
181
|
+
envelope: unknown,
|
|
182
|
+
httpStatus: number,
|
|
183
|
+
): PickleballExpoError & { httpStatus: number; reason?: string; retryAfterMs?: number } {
|
|
184
|
+
const body = isRecord(envelope) && isRecord(envelope.error) ? envelope.error : null;
|
|
185
|
+
const error =
|
|
186
|
+
body && typeof body.code === "string"
|
|
187
|
+
? errorFromCode(body.code, typeof body.message === "string" ? body.message : undefined)
|
|
188
|
+
: new InvalidResponseError();
|
|
189
|
+
// `reason` (409: superseded|ended|expired) và `retryAfterMs` (429) đi kèm để
|
|
190
|
+
// app phân biệt ba ca 409 mà không phải đọc chuỗi message.
|
|
191
|
+
return Object.assign(
|
|
192
|
+
error,
|
|
193
|
+
{ httpStatus },
|
|
194
|
+
body && typeof body.reason === "string" ? { reason: body.reason } : {},
|
|
195
|
+
body && typeof body.retryAfterMs === "number" ? { retryAfterMs: body.retryAfterMs } : {},
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface JsonPosterOptions {
|
|
200
|
+
fetch?: Fetch;
|
|
201
|
+
timeoutMs?: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* POST JSON → mở bọc `{data}`; lỗi mạng/timeout/envelope map thành lỗi SDK
|
|
206
|
+
* (giữ `httpStatus`). Dùng chung cho mọi provider.
|
|
207
|
+
*/
|
|
208
|
+
export function createJsonPoster(options: JsonPosterOptions = {}) {
|
|
209
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
210
|
+
if (typeof fetchImplementation !== "function") {
|
|
211
|
+
throw new ExpoSdkConfigurationError("A Fetch API implementation is required");
|
|
212
|
+
}
|
|
213
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
214
|
+
return async function post(
|
|
215
|
+
url: string,
|
|
216
|
+
body: unknown,
|
|
217
|
+
headers: Record<string, string> = {},
|
|
218
|
+
): Promise<unknown> {
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
221
|
+
try {
|
|
222
|
+
const response = await fetchImplementation(url, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
225
|
+
body: JSON.stringify(body),
|
|
226
|
+
signal: controller.signal,
|
|
227
|
+
redirect: "error",
|
|
228
|
+
});
|
|
229
|
+
const envelope = await response.json().catch(() => {
|
|
230
|
+
throw new InvalidResponseError();
|
|
231
|
+
});
|
|
232
|
+
if (!response.ok) throw parseEnvelopeError(envelope, response.status);
|
|
233
|
+
if (!isRecord(envelope) || !("data" in envelope)) {
|
|
234
|
+
throw new InvalidResponseError();
|
|
235
|
+
}
|
|
236
|
+
return envelope.data;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (controller.signal.aborted) throw new ExpoSdkTimeoutError();
|
|
239
|
+
if (error instanceof PickleballExpoError) throw error;
|
|
240
|
+
if (error instanceof TypeError) {
|
|
241
|
+
throw new NetworkUnavailableError(undefined, { cause: error });
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
} finally {
|
|
245
|
+
clearTimeout(timeout);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Mã HTTP gắn trên lỗi provider, nếu có. */
|
|
251
|
+
export function httpStatusOf(error: unknown): number | undefined {
|
|
252
|
+
const status = (error as { httpStatus?: unknown } | null)?.httpStatus;
|
|
253
|
+
return typeof status === "number" ? status : undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type { SdkRemoteConfig };
|