@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.
- package/CHANGELOG.md +65 -0
- package/README.md +163 -40
- package/dist/advanced-DcbHBCGb.d.ts +300 -0
- package/dist/advanced-DeQwg7MK.d.cts +300 -0
- package/dist/advanced.cjs +47 -0
- package/dist/advanced.d.cts +3 -0
- package/dist/advanced.d.ts +3 -0
- package/dist/advanced.js +47 -0
- package/dist/camera-controls-By8tHigY.d.cts +97 -0
- package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
- package/dist/chunk-2JHULJZ7.js +265 -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-7OX2DSKI.cjs +33 -0
- package/dist/chunk-7Z7TN5N3.js +318 -0
- package/dist/chunk-DHDX3ZOO.js +800 -0
- package/dist/chunk-GXQYMPHE.cjs +9 -0
- package/dist/chunk-IUIRVKJI.cjs +2290 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-LCWGNGEN.cjs +265 -0
- package/dist/chunk-TBUEXRFQ.cjs +318 -0
- package/dist/chunk-XGP2MN5R.js +2290 -0
- package/dist/chunk-XL4CISVQ.js +9 -0
- package/dist/chunk-YGSQ25UA.cjs +800 -0
- 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 +1427 -0
- package/dist/device-agent/index.d.cts +493 -0
- package/dist/device-agent/index.d.ts +493 -0
- package/dist/device-agent/index.js +1427 -0
- package/dist/engine-BSvSdCSE.d.cts +741 -0
- package/dist/engine-BSvSdCSE.d.ts +741 -0
- package/dist/index.cjs +426 -6174
- package/dist/index.d.cts +299 -1400
- package/dist/index.d.ts +299 -1400
- package/dist/index.js +413 -6063
- package/dist/match/index.cjs +2456 -0
- package/dist/match/index.d.cts +253 -0
- package/dist/match/index.d.ts +253 -0
- package/dist/match/index.js +2456 -0
- package/dist/match/remote/index.cjs +21 -0
- package/dist/match/remote/index.d.cts +67 -0
- package/dist/match/remote/index.d.ts +67 -0
- package/dist/match/remote/index.js +21 -0
- package/dist/match/voice/index.cjs +23 -0
- package/dist/match/voice/index.d.cts +114 -0
- package/dist/match/voice/index.d.ts +114 -0
- package/dist/match/voice/index.js +23 -0
- package/dist/plugin.cjs +52 -71
- package/dist/plugin.d.cts +28 -1
- package/dist/plugin.d.ts +28 -1
- package/dist/plugin.js +26 -23
- 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/types-CrmcX408.d.cts +176 -0
- package/dist/types-CrmcX408.d.ts +176 -0
- package/dist/types-DiKWjwvN.d.cts +96 -0
- package/dist/types-DiKWjwvN.d.ts +96 -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 +4 -32
- package/dist/version.js +2 -1
- package/package.json +95 -15
- package/src/advanced.ts +49 -0
- 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 +696 -0
- package/src/device-agent/beat-policy.ts +46 -0
- package/src/device-agent/component.tsx +60 -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 +88 -0
- package/src/device-agent/http-transport.ts +164 -0
- package/src/device-agent/index.ts +26 -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 +601 -20
- package/src/errors.ts +16 -0
- package/src/http-session-provider.ts +21 -183
- package/src/index.ts +117 -14
- package/src/local-video-quality.ts +43 -0
- package/src/match/index.ts +29 -0
- package/src/match/provider.tsx +4 -2
- package/src/match/remote/hid-gesture.ts +10 -2
- package/src/match/remote/types.ts +6 -4
- package/src/match/voice/scheduler.ts +6 -1
- package/src/match/voice/service.ts +16 -4
- package/src/native-runtime.ts +283 -77
- package/src/plugin.ts +54 -20
- package/src/provider.tsx +129 -22
- 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 +348 -0
- 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/chunk-VHLUJQG5.js +0 -17
- package/src/livekit-room-adapter.ts +0 -353
- package/src/livestream-screen.tsx +0 -980
package/src/errors.ts
CHANGED
|
@@ -126,3 +126,19 @@ export function normalizeError(error: unknown): PickleballExpoError {
|
|
|
126
126
|
}
|
|
127
127
|
return new UnknownSdkError();
|
|
128
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Phiên đã biến mất hẳn phía server (HTTP 404): bị watchdog dọn, thuộc
|
|
132
|
+
* 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
|
|
133
|
+
* 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.
|
|
134
|
+
*
|
|
135
|
+
* Mã HTTP được gắn vào lỗi ở http-session-provider vì envelope trả về chỉ có
|
|
136
|
+
* code "UNKNOWN", không phân biệt được 404 với 5xx.
|
|
137
|
+
*/
|
|
138
|
+
export function isSessionGoneError(error: unknown): boolean {
|
|
139
|
+
return (
|
|
140
|
+
typeof error === "object" &&
|
|
141
|
+
error !== null &&
|
|
142
|
+
(error as { httpStatus?: unknown }).httpStatus === 404
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -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 {
|
|
7
|
+
import { InvalidResponseError } from "./errors";
|
|
12
8
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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);
|
|
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,133 +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) {
|
|
172
|
-
if (
|
|
173
|
-
isRecord(envelope) &&
|
|
174
|
-
isRecord(envelope.error) &&
|
|
175
|
-
typeof envelope.error.code === "string"
|
|
176
|
-
) {
|
|
177
|
-
return errorFromCode(
|
|
178
|
-
envelope.error.code,
|
|
179
|
-
typeof envelope.error.message === "string"
|
|
180
|
-
? envelope.error.message
|
|
181
|
-
: undefined,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
return new InvalidResponseError();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
188
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function isFiniteNumber(value: unknown): value is number {
|
|
192
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function isBootstrap(value: unknown): value is SdkBootstrap {
|
|
196
|
-
return (
|
|
197
|
-
isRecord(value) &&
|
|
198
|
-
isSdkRemoteConfig(value.config) &&
|
|
199
|
-
isFiniteNumber(value.serverTime) &&
|
|
200
|
-
typeof value.rolloutEnabled === "boolean" &&
|
|
201
|
-
(value.message === undefined || typeof value.message === "string")
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function isTelemetry(value: unknown): value is SdkTelemetryCredentials {
|
|
206
|
-
return (
|
|
207
|
-
isRecord(value) &&
|
|
208
|
-
typeof value.endpoint === "string" &&
|
|
209
|
-
/^https:\/\//.test(value.endpoint) &&
|
|
210
|
-
typeof value.token === "string" &&
|
|
211
|
-
value.token.length > 0 &&
|
|
212
|
-
isFiniteNumber(value.expiresAt)
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isSessionGrant(value: unknown): value is SdkSessionGrant {
|
|
217
|
-
return (
|
|
218
|
-
isRecord(value) &&
|
|
219
|
-
typeof value.sessionId === "string" &&
|
|
220
|
-
value.sessionId.length > 0 &&
|
|
221
|
-
typeof value.serverUrl === "string" &&
|
|
222
|
-
/^wss:\/\//.test(value.serverUrl) &&
|
|
223
|
-
typeof value.participantToken === "string" &&
|
|
224
|
-
value.participantToken.length > 0 &&
|
|
225
|
-
(value.playbackUrl === null || typeof value.playbackUrl === "string") &&
|
|
226
|
-
isFiniteNumber(value.tokenExpiresAt) &&
|
|
227
|
-
isTelemetry(value.telemetry) &&
|
|
228
|
-
isSdkRemoteConfig(value.config)
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function isSessionStatus(
|
|
233
|
-
value: unknown,
|
|
234
|
-
): value is { sessionId: string; status: string } {
|
|
235
|
-
return (
|
|
236
|
-
isRecord(value) &&
|
|
237
|
-
typeof value.sessionId === "string" &&
|
|
238
|
-
value.sessionId.length > 0 &&
|
|
239
|
-
typeof value.status === "string" &&
|
|
240
|
-
value.status.length > 0
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function isSessionRefresh(value: unknown): value is SdkSessionRefresh {
|
|
245
|
-
if (!isRecord(value) || !isSdkRemoteConfig(value.config)) return false;
|
|
246
|
-
if (
|
|
247
|
-
value.participantToken !== undefined &&
|
|
248
|
-
(typeof value.participantToken !== "string" || !value.participantToken)
|
|
249
|
-
) {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
if (
|
|
253
|
-
value.tokenExpiresAt !== undefined &&
|
|
254
|
-
!isFiniteNumber(value.tokenExpiresAt)
|
|
255
|
-
) {
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
return value.telemetry === undefined || isTelemetry(value.telemetry);
|
|
259
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,119 @@
|
|
|
1
|
+
/**
|
|
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`)
|
|
6
|
+
* @pickleball/expo-sdk/match — scoreboard + persistence
|
|
7
|
+
* @pickleball/expo-sdk/match/voice — đọc điểm bằng loa
|
|
8
|
+
* @pickleball/expo-sdk/match/remote — remote BLE/HID
|
|
9
|
+
* @pickleball/expo-sdk/device-agent — điều khiển từ dashboard (app fleet)
|
|
10
|
+
* @pickleball/expo-sdk/advanced — engine internals, DI, mock/test
|
|
11
|
+
* Metro không tree-shake nên chỉ import subpath nào app thật sự dùng.
|
|
12
|
+
*/
|
|
1
13
|
export * from "./contracts";
|
|
2
|
-
export * from "./match/contracts";
|
|
3
|
-
export * from "./match/engine";
|
|
4
|
-
export { replayMatchEvents, undoLastMatchEvent } from "./match/replay";
|
|
5
|
-
export * from "./match/persistence";
|
|
6
|
-
export * from "./match/provider";
|
|
7
|
-
export * from "./match/remote";
|
|
8
|
-
export * from "./match/voice";
|
|
9
14
|
export * from "./errors";
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
export {
|
|
16
|
+
PickleballLiveProvider,
|
|
17
|
+
useLivestream,
|
|
18
|
+
defaultLivestreamTheme,
|
|
19
|
+
type PickleballLiveProviderProps,
|
|
20
|
+
type LivestreamContextValue,
|
|
21
|
+
type LivestreamTheme,
|
|
22
|
+
} from "./provider";
|
|
23
|
+
export {
|
|
24
|
+
createHttpSessionProvider,
|
|
25
|
+
type HttpSessionProviderOptions,
|
|
26
|
+
type HttpSessionProviderPaths,
|
|
27
|
+
} from "./http-session-provider";
|
|
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";
|
|
105
|
+
// Type-only: xuất hiện trong LivestreamContextValue/ProviderProps
|
|
106
|
+
export type {
|
|
107
|
+
CameraDeviceInfo,
|
|
108
|
+
CaptureOrientation,
|
|
109
|
+
DeviceMetricsSample,
|
|
110
|
+
LivestreamSnapshot,
|
|
111
|
+
RoomAdapter,
|
|
112
|
+
SdkNetwork,
|
|
113
|
+
SdkRuntime,
|
|
114
|
+
SdkStorage,
|
|
115
|
+
SessionCommandChannel,
|
|
116
|
+
SessionCommandSubscription,
|
|
117
|
+
TelemetrySink,
|
|
118
|
+
} from "./engine";
|
|
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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry cho tính năng trận đấu: scoreboard engine, persistence SQLite và
|
|
3
|
+
* PickleballMatchProvider. Voice/remote có subpath riêng
|
|
4
|
+
* (@pickleball/expo-sdk/match/voice, /match/remote) — ở đây chỉ re-export
|
|
5
|
+
* type-only các kiểu xuất hiện trong props của provider.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./contracts";
|
|
8
|
+
export * from "./engine";
|
|
9
|
+
export { replayMatchEvents, undoLastMatchEvent } from "./replay";
|
|
10
|
+
export * from "./persistence";
|
|
11
|
+
export * from "./provider";
|
|
12
|
+
export type {
|
|
13
|
+
MatchVoiceService,
|
|
14
|
+
VoiceAudioMode,
|
|
15
|
+
VoiceRouteState,
|
|
16
|
+
VoiceTtsOptions,
|
|
17
|
+
} from "./voice/types";
|
|
18
|
+
export type {
|
|
19
|
+
RemoteActionMapping,
|
|
20
|
+
RemoteBinding,
|
|
21
|
+
RemoteHidGestureMapping,
|
|
22
|
+
RemoteHidVolumeEvent,
|
|
23
|
+
RemoteInputMode,
|
|
24
|
+
RemoteMappedAction,
|
|
25
|
+
RemoteNativeState,
|
|
26
|
+
RemoteScanAdvertisement,
|
|
27
|
+
RemoteService,
|
|
28
|
+
RemoteState,
|
|
29
|
+
} from "./remote/types";
|
package/src/match/provider.tsx
CHANGED
|
@@ -523,7 +523,9 @@ export function PickleballMatchProvider({
|
|
|
523
523
|
? awardRallyToSide("right", "remote")
|
|
524
524
|
: mapped === "undo"
|
|
525
525
|
? undo("remote")
|
|
526
|
-
:
|
|
526
|
+
: mapped === "announce"
|
|
527
|
+
? replayAnnouncement()
|
|
528
|
+
: null;
|
|
527
529
|
if (action) void action.catch(() => undefined);
|
|
528
530
|
};
|
|
529
531
|
|
|
@@ -536,7 +538,7 @@ export function PickleballMatchProvider({
|
|
|
536
538
|
hidGestureRecognizerRef.current ??= new HidVolumeGestureRecognizer({
|
|
537
539
|
...hidGestureMapping,
|
|
538
540
|
onGesture: (gesture) => {
|
|
539
|
-
handleRemoteMappedActionRef.current(hidGestureMapping[gesture]);
|
|
541
|
+
handleRemoteMappedActionRef.current(hidGestureMapping[gesture] ?? "none");
|
|
540
542
|
},
|
|
541
543
|
});
|
|
542
544
|
hidGestureRecognizerRef.current.push(event);
|
|
@@ -16,7 +16,7 @@ export class HidVolumeGestureRecognizer {
|
|
|
16
16
|
constructor(private readonly options: HidVolumeGestureRecognizerOptions) {}
|
|
17
17
|
|
|
18
18
|
push(event: RemoteHidVolumeEvent) {
|
|
19
|
-
if (event.direction !== this.options.direction) return;
|
|
19
|
+
if (this.options.direction !== "any" && event.direction !== this.options.direction) return;
|
|
20
20
|
if (
|
|
21
21
|
this.lastTimestamp !== null &&
|
|
22
22
|
event.timestamp - this.lastTimestamp > this.options.settleMs
|
|
@@ -43,7 +43,15 @@ export class HidVolumeGestureRecognizer {
|
|
|
43
43
|
private finishBurst() {
|
|
44
44
|
this.clearTimer();
|
|
45
45
|
if (!this.resolved && this.count > 0) {
|
|
46
|
-
this.options.onGesture(
|
|
46
|
+
this.options.onGesture(
|
|
47
|
+
this.count === 1
|
|
48
|
+
? "single"
|
|
49
|
+
: this.count === 2
|
|
50
|
+
? "double"
|
|
51
|
+
: this.count === 3
|
|
52
|
+
? "triple"
|
|
53
|
+
: "quadruple",
|
|
54
|
+
);
|
|
47
55
|
}
|
|
48
56
|
this.reset();
|
|
49
57
|
}
|
|
@@ -2,7 +2,7 @@ export const BTHOME_SERVICE_UUID = "FCD2" as const;
|
|
|
2
2
|
|
|
3
3
|
export type RemoteButtonAction = "single" | "double" | "long";
|
|
4
4
|
export type RemoteInputMode = "bthome" | "hidVolume" | "both";
|
|
5
|
-
export type RemoteMappedAction = "left" | "right" | "undo" | "none";
|
|
5
|
+
export type RemoteMappedAction = "left" | "right" | "undo" | "announce" | "none";
|
|
6
6
|
export interface RemoteActionMapping {
|
|
7
7
|
readonly volumeUp: RemoteMappedAction;
|
|
8
8
|
readonly volumeDown: RemoteMappedAction;
|
|
@@ -19,17 +19,19 @@ export interface RemoteHidVolumeEvent {
|
|
|
19
19
|
readonly timestamp: number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export type RemoteHidGesture = "single" | "double" | "hold";
|
|
22
|
+
export type RemoteHidGesture = "single" | "double" | "triple" | "quadruple" | "hold";
|
|
23
23
|
|
|
24
24
|
export interface RemoteHidGestureMapping {
|
|
25
|
-
/** Raw HID direction emitted by the calibrated remote button. */
|
|
26
|
-
readonly direction: RemoteHidVolumeEvent["direction"];
|
|
25
|
+
/** Raw HID direction emitted by the calibrated remote button, or "any" for single-button remotes that alternate directions. */
|
|
26
|
+
readonly direction: RemoteHidVolumeEvent["direction"] | "any";
|
|
27
27
|
/** Quiet window used to close one physical gesture burst. */
|
|
28
28
|
readonly settleMs: number;
|
|
29
29
|
/** Repeated volume changes required to classify a hold before the burst ends. */
|
|
30
30
|
readonly holdEventCount: number;
|
|
31
31
|
readonly single: RemoteMappedAction;
|
|
32
32
|
readonly double: RemoteMappedAction;
|
|
33
|
+
readonly triple?: RemoteMappedAction;
|
|
34
|
+
readonly quadruple?: RemoteMappedAction;
|
|
33
35
|
readonly hold: RemoteMappedAction;
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -75,6 +75,7 @@ export class LatestAnnouncementScheduler {
|
|
|
75
75
|
]);
|
|
76
76
|
if (generation !== this.generation) return;
|
|
77
77
|
|
|
78
|
+
let neuralFailure: unknown = null;
|
|
78
79
|
try {
|
|
79
80
|
const avail = await this.options.engine.availability();
|
|
80
81
|
if (avail === "available") {
|
|
@@ -89,17 +90,21 @@ export class LatestAnnouncementScheduler {
|
|
|
89
90
|
}
|
|
90
91
|
} catch (error) {
|
|
91
92
|
if (controller.signal.aborted || generation !== this.generation) return;
|
|
92
|
-
|
|
93
|
+
// Đọc bằng WAV offline vẫn chạy được — chỉ báo lỗi TTS khi fallback cũng hỏng.
|
|
94
|
+
neuralFailure = error;
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
let fallbackPlayed = false;
|
|
95
98
|
for (const source of planBundledFallback(plan)) {
|
|
96
99
|
if (generation !== this.generation || controller.signal.aborted) return;
|
|
97
100
|
try {
|
|
98
101
|
await this.options.audio.play(source);
|
|
102
|
+
fallbackPlayed = true;
|
|
99
103
|
} catch (error) {
|
|
100
104
|
this.report(error);
|
|
101
105
|
}
|
|
102
106
|
}
|
|
107
|
+
if (neuralFailure && !fallbackPlayed) this.report(neuralFailure);
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
async replayLast(): Promise<void> {
|
|
@@ -56,11 +56,13 @@ export function createDefaultMatchVoiceService(
|
|
|
56
56
|
(options.tts
|
|
57
57
|
? createGeminiVoiceEngine({ ...options.tts, generatedRoot: generatedDirectory() })
|
|
58
58
|
: createUnavailableVoiceEngine());
|
|
59
|
+
let errorEpoch = 0;
|
|
59
60
|
const scheduler = new LatestAnnouncementScheduler({
|
|
60
61
|
engine,
|
|
61
62
|
audio,
|
|
62
63
|
onError(error) {
|
|
63
64
|
lastError = error;
|
|
65
|
+
errorEpoch += 1;
|
|
64
66
|
enqueueNotify();
|
|
65
67
|
},
|
|
66
68
|
});
|
|
@@ -118,10 +120,20 @@ export function createDefaultMatchVoiceService(
|
|
|
118
120
|
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
119
121
|
announce(previous, current, event) {
|
|
120
122
|
if (!stillActive()) return;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
const epochBefore = errorEpoch;
|
|
124
|
+
void scheduler.enqueue(buildVietnameseAnnouncement(previous, current, event)).then(
|
|
125
|
+
() => {
|
|
126
|
+
// Lần đọc trọn vẹn (không lỗi mới nào bị báo) xoá lỗi voice cũ khỏi UI thay vì hiển thị mãi.
|
|
127
|
+
if (lastError && errorEpoch === epochBefore && stillActive()) {
|
|
128
|
+
lastError = null;
|
|
129
|
+
notify();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
(error) => {
|
|
133
|
+
lastError = error instanceof Error ? error : new Error("Announcement failed");
|
|
134
|
+
notify();
|
|
135
|
+
},
|
|
136
|
+
);
|
|
125
137
|
},
|
|
126
138
|
replayAnnouncement: () => stillActive() ? scheduler.replayLast() : Promise.resolve(),
|
|
127
139
|
testSpeaker: () => stillActive() ? scheduler.testSpeaker() : Promise.resolve(),
|