@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
@@ -0,0 +1,445 @@
1
+ import React, { useEffect, useState, type ReactNode } from "react";
2
+ import {
3
+ Modal,
4
+ Platform,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Switch,
9
+ Text,
10
+ View,
11
+ useWindowDimensions,
12
+ } from "react-native";
13
+ import { SafeAreaView } from "react-native-safe-area-context";
14
+ import {
15
+ setExposureBias,
16
+ setExposureLocked,
17
+ setFocusLocked,
18
+ setTorch,
19
+ setZoom,
20
+ } from "../camera-controls";
21
+ import { useCameraSettings, type CameraSettings, type CameraSettingsStore } from "../camera-settings";
22
+ import { useLivestream } from "../provider";
23
+ import { runUplinkCheck, type UplinkCheckResult } from "../uplink-check";
24
+ import { uiCopy, type UiLocale } from "./copy";
25
+ import { useCameraCapabilities } from "./hooks";
26
+ import {
27
+ CameraOption,
28
+ ControlRow,
29
+ Note,
30
+ PresetButton,
31
+ Segmented,
32
+ uiColors,
33
+ uiStyles,
34
+ } from "./primitives";
35
+
36
+ /**
37
+ * Sheet cấu hình camera — menu ••• của Picklive Camera chuyển vào SDK, trừ ba
38
+ * mục kiosk (Nguồn, Thoát kiosk, badge OTA) mà app fleet đưa lại qua `footer`.
39
+ *
40
+ * Zoom/EV/khoá/đèn pin gọi thẳng native (camera-controls, typeof-guard) — tinh
41
+ * chỉnh camera runtime, không phải lifecycle phiên. Resolution/FPS/camera mặc
42
+ * định ghi vào CameraSettingsStore; app remount PickleballLiveProvider theo
43
+ * `cameraSettingsKey` để engine re-prepare.
44
+ */
45
+
46
+ const ZOOM_PRESETS = [1, 2, 3, 5] as const;
47
+ const EV_PRESETS = [-2, -1, 0, 1, 2] as const;
48
+ /** Tập tối đa; mức nào hiện thật do capabilities của lens quyết định. */
49
+ const FPS_CHOICES: Array<{ value: CameraSettings["fps"]; label: string }> = [
50
+ { value: 24, label: "24" },
51
+ { value: 30, label: "30" },
52
+ { value: 60, label: "60" },
53
+ ];
54
+
55
+ export interface CameraControlsSheetSections {
56
+ quality?: boolean;
57
+ antiFlicker?: boolean;
58
+ camera?: boolean;
59
+ zoom?: boolean;
60
+ exposure?: boolean;
61
+ locks?: boolean;
62
+ torch?: boolean;
63
+ uplink?: boolean;
64
+ }
65
+
66
+ export interface CameraControlsSheetProps {
67
+ visible: boolean;
68
+ onClose: () => void;
69
+ settings: CameraSettingsStore;
70
+ /** Ẩn mục không cần (mặc định hiện tất cả). */
71
+ sections?: CameraControlsSheetSections;
72
+ /** Đo mạng: đích POST + HEAD đo RTT (VD ingest của bạn). */
73
+ uplink?: { uploadUrl?: string; pingUrl?: string };
74
+ /**
75
+ * "side": panel nửa phải (màn ngang); "bottom": bottom-sheet (màn dọc);
76
+ * "auto" (mặc định) theo kích thước cửa sổ.
77
+ */
78
+ layout?: "auto" | "side" | "bottom";
79
+ title?: string;
80
+ /** Hàng thêm của app ở cuối sheet (Picklive Camera: Nguồn, Thoát kiosk, badge OTA). */
81
+ footer?: ReactNode;
82
+ locale?: UiLocale;
83
+ }
84
+
85
+ const IN_SESSION = new Set(["connecting", "standby", "live", "reconnecting", "ending"]);
86
+ const UPLINK_BUSY = new Set(["connecting", "live", "reconnecting", "ending"]);
87
+
88
+ export function CameraControlsSheet(props: CameraControlsSheetProps) {
89
+ const { visible, onClose, settings, uplink, footer } = props;
90
+ const sections = props.sections ?? {};
91
+ const show = (key: keyof CameraControlsSheetSections) => sections[key] !== false;
92
+ const live = useLivestream();
93
+ const locale = props.locale ?? live.locale;
94
+ const copy = uiCopy[locale].sheet;
95
+ const config = useCameraSettings(settings);
96
+ const { width, height } = useWindowDimensions();
97
+ const side =
98
+ props.layout === "side" || (props.layout !== "bottom" && width > height);
99
+
100
+ const caps = useCameraCapabilities(visible, live.selectedCameraId);
101
+ const [zoom, setZoomValue] = useState(1);
102
+ const [ev, setEvValue] = useState(0);
103
+ const [focusLocked, setFocusLockedValue] = useState(false);
104
+ const [exposureLocked, setExposureLockedValue] = useState(false);
105
+ const [torch, setTorchValue] = useState(false);
106
+ const [net, setNet] = useState<{
107
+ running: boolean;
108
+ result: UplinkCheckResult | null;
109
+ error: string | null;
110
+ }>({ running: false, result: null, error: null });
111
+
112
+ // Đổi resolution/fps/camera → remount provider + re-prepare — khoá khi đang trong phiên.
113
+ const configLocked = IN_SESSION.has(live.state);
114
+ // Đo mạng lúc ĐANG PHÁT là giành uplink với chính luồng video → cấm.
115
+ // Standby thì đo được: RTMP nối nhưng chưa publish, lưu lượng không đáng kể.
116
+ const netBusy = UPLINK_BUSY.has(live.state);
117
+
118
+ // Chỉ chào fps mà lens đang gắn chạy được. Native im lặng bỏ qua mức không
119
+ // hỗ trợ, nên nút chào bừa sẽ là nút chết: người dùng bấm 60, UI hiện 60,
120
+ // camera vẫn 30. Chưa đọc được capabilities thì giữ 24/30 như trước.
121
+ const supportedFps = caps?.supportedFrameRates ?? [24, 30];
122
+ const fpsOptions = FPS_CHOICES.filter((choice) => supportedFps.includes(choice.value));
123
+
124
+ // Đổi sang lens không có 60fps trong khi config đang để 60 → hạ về 30, nếu
125
+ // không Segmented không có ô nào sáng và người dùng kẹt ở giá trị vô hiệu.
126
+ useEffect(() => {
127
+ if (configLocked || !caps) return;
128
+ if (!supportedFps.includes(config.fps)) settings.set({ fps: 30 });
129
+ }, [caps, config.fps, configLocked, settings, supportedFps]);
130
+
131
+ async function runNetworkCheck() {
132
+ setNet({ running: true, result: null, error: null });
133
+ try {
134
+ const result = await runUplinkCheck({
135
+ resolution: config.resolution,
136
+ ...(uplink?.uploadUrl ? { uploadUrl: uplink.uploadUrl } : {}),
137
+ ...(uplink?.pingUrl ? { pingUrl: uplink.pingUrl } : {}),
138
+ });
139
+ setNet({ running: false, result, error: null });
140
+ } catch {
141
+ setNet({ running: false, result: null, error: copy.checkError });
142
+ }
143
+ }
144
+
145
+ return (
146
+ // supportedOrientations đủ cả dọc lẫn ngang: mặc định của Modal là
147
+ // ["portrait"], app khoá landscape (Info.plist chỉ có LandscapeLeft/Right)
148
+ // → iOS không tìm được hướng chung và ABORT ngay khi mount modal
149
+ // ("Supported orientations has no common orientation with the application").
150
+ <Modal
151
+ visible={visible}
152
+ transparent
153
+ animationType="fade"
154
+ onRequestClose={onClose}
155
+ // Android: cho modal phủ cả vùng status/navigation bar rồi tự né bằng
156
+ // SafeAreaView — không né là thanh điều hướng đè lên hàng nút.
157
+ statusBarTranslucent
158
+ navigationBarTranslucent
159
+ supportedOrientations={[
160
+ "portrait",
161
+ "portrait-upside-down",
162
+ "landscape",
163
+ "landscape-left",
164
+ "landscape-right",
165
+ ]}
166
+ >
167
+ <View style={side ? styles.overlaySide : styles.overlayBottom}>
168
+ <Pressable
169
+ style={styles.backdrop}
170
+ onPress={onClose}
171
+ accessibilityRole="button"
172
+ accessibilityLabel={copy.closeBackdrop}
173
+ />
174
+ <SafeAreaView
175
+ edges={side ? ["top", "right", "bottom"] : ["bottom", "left", "right"]}
176
+ style={side ? styles.panelSide : styles.panelBottom}
177
+ >
178
+ <View style={styles.panelHeader}>
179
+ <Text style={styles.heading}>{props.title ?? copy.title}</Text>
180
+ <Pressable
181
+ onPress={onClose}
182
+ style={styles.closeX}
183
+ accessibilityRole="button"
184
+ accessibilityLabel={copy.close}
185
+ >
186
+ <Text style={styles.closeXText}>✕</Text>
187
+ </Pressable>
188
+ </View>
189
+ <ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
190
+ {show("quality") ? (
191
+ <>
192
+ <ControlRow label={copy.resolution}>
193
+ <Segmented
194
+ options={[
195
+ { value: 720, label: "720p" },
196
+ { value: 1080, label: "1080p" },
197
+ ]}
198
+ value={config.resolution}
199
+ disabled={configLocked}
200
+ onChange={(value) => settings.set({ resolution: value })}
201
+ />
202
+ </ControlRow>
203
+ <ControlRow label={copy.fps}>
204
+ <Segmented
205
+ options={fpsOptions}
206
+ value={config.fps}
207
+ disabled={configLocked}
208
+ onChange={(value) => settings.set({ fps: value })}
209
+ />
210
+ </ControlRow>
211
+ {config.fps === 60 ? <Note>{copy.fps60Note}</Note> : null}
212
+ </>
213
+ ) : null}
214
+
215
+ {/* Chống chớp đèn — Android-only (iOS chưa có API antibanding).
216
+ Không khoá theo phiên: là key trên repeating request, đổi giữa
217
+ sóng an toàn — thấy vằn là chỉnh ngay được. */}
218
+ {show("antiFlicker") && Platform.OS === "android" ? (
219
+ <>
220
+ <ControlRow label={copy.antiFlicker}>
221
+ <Segmented
222
+ options={[
223
+ { value: "50hz", label: "50Hz" },
224
+ { value: "60hz", label: "60Hz" },
225
+ { value: "auto", label: copy.auto },
226
+ ]}
227
+ value={config.antiFlicker}
228
+ onChange={(value) => settings.set({ antiFlicker: value })}
229
+ />
230
+ </ControlRow>
231
+ <Note>{copy.antiFlickerNote}</Note>
232
+ </>
233
+ ) : null}
234
+ {configLocked && (show("quality") || show("camera")) ? (
235
+ <Note>{copy.lockedNote}</Note>
236
+ ) : null}
237
+
238
+ {/* Camera mặc định mỗi lần mở app — danh sách dọc kèm id vì máy
239
+ thật phơi ra nhiều camera TRÙNG NHÃN. */}
240
+ {show("camera") ? (
241
+ <>
242
+ <ControlRow label={copy.defaultCamera} />
243
+ <View style={[styles.cameraList, configLocked && uiStyles.disabled]}>
244
+ <CameraOption
245
+ label={copy.autoCamera}
246
+ hint={copy.autoCameraHint}
247
+ active={config.cameraId === null}
248
+ disabled={configLocked}
249
+ onPress={() => settings.set({ cameraId: null })}
250
+ />
251
+ {live.cameras.map((camera) => (
252
+ <CameraOption
253
+ key={camera.deviceId}
254
+ label={camera.facing === "front" ? copy.frontCamera : copy.backCamera}
255
+ hint={copy.cameraIdHint(camera.deviceId)}
256
+ active={config.cameraId === camera.deviceId}
257
+ disabled={configLocked}
258
+ onPress={() => settings.set({ cameraId: camera.deviceId })}
259
+ />
260
+ ))}
261
+ </View>
262
+ </>
263
+ ) : null}
264
+
265
+ {show("zoom") && caps?.zoom.supported ? (
266
+ <ControlRow label={copy.zoom}>
267
+ <View style={uiStyles.presetRow}>
268
+ {ZOOM_PRESETS.filter(
269
+ (preset) => preset >= caps.zoom.min && preset <= caps.zoom.max,
270
+ ).map((preset) => (
271
+ <PresetButton
272
+ key={preset}
273
+ label={`${preset}x`}
274
+ active={zoom === preset}
275
+ onPress={() => {
276
+ setZoomValue(preset);
277
+ void setZoom(preset);
278
+ }}
279
+ />
280
+ ))}
281
+ </View>
282
+ </ControlRow>
283
+ ) : null}
284
+
285
+ {show("exposure") && caps?.exposureBias.supported ? (
286
+ <ControlRow label={copy.ev}>
287
+ <View style={uiStyles.presetRow}>
288
+ {EV_PRESETS.filter(
289
+ (preset) =>
290
+ preset >= caps.exposureBias.min && preset <= caps.exposureBias.max,
291
+ ).map((preset) => (
292
+ <PresetButton
293
+ key={preset}
294
+ label={preset > 0 ? `+${preset}` : `${preset}`}
295
+ active={ev === preset}
296
+ onPress={() => {
297
+ setEvValue(preset);
298
+ void setExposureBias(preset);
299
+ }}
300
+ />
301
+ ))}
302
+ </View>
303
+ </ControlRow>
304
+ ) : null}
305
+
306
+ {show("locks") && caps?.focusLock.supported ? (
307
+ <ControlRow label={copy.focusLock}>
308
+ <Switch
309
+ value={focusLocked}
310
+ accessibilityLabel={copy.focusLock}
311
+ onValueChange={(value) => {
312
+ setFocusLockedValue(value);
313
+ void setFocusLocked(value);
314
+ }}
315
+ />
316
+ </ControlRow>
317
+ ) : null}
318
+ {show("locks") && caps?.exposureLock.supported ? (
319
+ <ControlRow label={copy.exposureLock}>
320
+ <Switch
321
+ value={exposureLocked}
322
+ accessibilityLabel={copy.exposureLock}
323
+ onValueChange={(value) => {
324
+ setExposureLockedValue(value);
325
+ void setExposureLocked(value);
326
+ }}
327
+ />
328
+ </ControlRow>
329
+ ) : null}
330
+ {show("torch") && caps?.torch.supported ? (
331
+ <ControlRow label={copy.torch}>
332
+ <Switch
333
+ value={torch}
334
+ accessibilityLabel={copy.torch}
335
+ onValueChange={(value) => {
336
+ setTorchValue(value);
337
+ void setTorch(value);
338
+ }}
339
+ />
340
+ </ControlRow>
341
+ ) : null}
342
+
343
+ {show("uplink") ? (
344
+ <>
345
+ <ControlRow label={copy.network} />
346
+ <Pressable
347
+ accessibilityRole="button"
348
+ disabled={net.running || netBusy}
349
+ onPress={() => void runNetworkCheck()}
350
+ style={[uiStyles.actionButton, (net.running || netBusy) && uiStyles.disabled]}
351
+ >
352
+ <Text style={uiStyles.actionButtonText}>
353
+ {net.running ? copy.running : copy.runCheck}
354
+ </Text>
355
+ </Pressable>
356
+ {netBusy ? <Note>{copy.busyNote}</Note> : null}
357
+ {net.result ? (
358
+ <Text
359
+ style={[
360
+ styles.netResult,
361
+ net.result.verdict === "good"
362
+ ? styles.netGood
363
+ : net.result.verdict === "ok"
364
+ ? styles.netOk
365
+ : styles.netLow,
366
+ ]}
367
+ >
368
+ {copy.resultLine({
369
+ uploadMbps: net.result.uploadMbps,
370
+ requiredMbps: net.result.requiredMbps,
371
+ resolution: config.resolution,
372
+ fps: config.fps,
373
+ pingMs: net.result.pingMs,
374
+ })}
375
+ {"\n"}
376
+ {net.result.verdict === "good"
377
+ ? copy.good
378
+ : net.result.verdict === "ok"
379
+ ? copy.ok
380
+ : copy.low}
381
+ </Text>
382
+ ) : null}
383
+ {net.error ? <Note>{net.error}</Note> : null}
384
+ </>
385
+ ) : null}
386
+
387
+ {footer}
388
+ </ScrollView>
389
+ </SafeAreaView>
390
+ </View>
391
+ </Modal>
392
+ );
393
+ }
394
+
395
+ const styles = StyleSheet.create({
396
+ overlaySide: { flex: 1, flexDirection: "row" },
397
+ overlayBottom: { flex: 1, flexDirection: "column" },
398
+ // Phần còn lại: mờ NHẸ để vẫn canh được khung hình trong lúc chỉnh; bấm là đóng.
399
+ backdrop: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
400
+ // Màn ngang: panel NỬA PHẢI — label và nút đứng gần nhau, nửa trái vẫn thấy
401
+ // khung hình. minWidth giữ dùng được trên màn hẹp, maxWidth để tablet không
402
+ // thành tấm bảng quá khổ.
403
+ panelSide: {
404
+ width: "50%",
405
+ minWidth: 340,
406
+ maxWidth: 480,
407
+ backgroundColor: uiColors.panel,
408
+ borderLeftWidth: 1,
409
+ borderLeftColor: uiColors.hairline,
410
+ },
411
+ // Màn dọc: bottom-sheet tối đa 70% chiều cao.
412
+ panelBottom: {
413
+ maxHeight: "70%",
414
+ backgroundColor: uiColors.panel,
415
+ borderTopLeftRadius: 18,
416
+ borderTopRightRadius: 18,
417
+ borderTopWidth: 1,
418
+ borderTopColor: uiColors.hairline,
419
+ },
420
+ panelHeader: {
421
+ flexDirection: "row",
422
+ alignItems: "center",
423
+ justifyContent: "space-between",
424
+ paddingHorizontal: 14,
425
+ paddingVertical: 10,
426
+ borderBottomWidth: 1,
427
+ borderBottomColor: uiColors.hairline,
428
+ },
429
+ closeX: {
430
+ width: 30,
431
+ height: 30,
432
+ borderRadius: 999,
433
+ backgroundColor: "rgba(255,255,255,0.1)",
434
+ alignItems: "center",
435
+ justifyContent: "center",
436
+ },
437
+ closeXText: { color: uiColors.textSecondary, fontSize: 14, fontWeight: "700" },
438
+ content: { padding: 14, paddingBottom: 24, gap: 10 },
439
+ heading: { color: uiColors.text, fontSize: 15, fontWeight: "800" },
440
+ netResult: { fontSize: 12, fontWeight: "700", lineHeight: 17 },
441
+ netGood: { color: uiColors.good },
442
+ netOk: { color: uiColors.ok },
443
+ netLow: { color: uiColors.low },
444
+ cameraList: { gap: 5 },
445
+ });