@pickleball/expo-sdk 0.1.1

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 (129) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +75 -0
  4. package/app.plugin.js +6 -0
  5. package/dist/chunk-VHLUJQG5.js +17 -0
  6. package/dist/index.cjs +6284 -0
  7. package/dist/index.d.cts +1436 -0
  8. package/dist/index.d.ts +1436 -0
  9. package/dist/index.js +6186 -0
  10. package/dist/plugin.cjs +153 -0
  11. package/dist/plugin.d.cts +21 -0
  12. package/dist/plugin.d.ts +21 -0
  13. package/dist/plugin.js +131 -0
  14. package/dist/version.cjs +35 -0
  15. package/dist/version.d.cts +4 -0
  16. package/dist/version.d.ts +4 -0
  17. package/dist/version.js +6 -0
  18. package/package.json +126 -0
  19. package/src/contracts.ts +211 -0
  20. package/src/engine.ts +1332 -0
  21. package/src/errors.ts +128 -0
  22. package/src/http-session-provider.ts +259 -0
  23. package/src/index.ts +16 -0
  24. package/src/livekit-room-adapter.ts +353 -0
  25. package/src/livestream-screen.tsx +980 -0
  26. package/src/match/contracts.ts +137 -0
  27. package/src/match/engine.ts +702 -0
  28. package/src/match/persistence/default-store.native.ts +20 -0
  29. package/src/match/persistence/default-store.ts +11 -0
  30. package/src/match/persistence/index.ts +6 -0
  31. package/src/match/persistence/memory-driver.ts +255 -0
  32. package/src/match/persistence/migrations.ts +40 -0
  33. package/src/match/persistence/sqlite-driver.ts +348 -0
  34. package/src/match/persistence/store.ts +497 -0
  35. package/src/match/persistence/types.ts +98 -0
  36. package/src/match/provider.tsx +728 -0
  37. package/src/match/remote/deduplicator.ts +54 -0
  38. package/src/match/remote/encoding.ts +12 -0
  39. package/src/match/remote/hid-gesture.ts +61 -0
  40. package/src/match/remote/index.ts +5 -0
  41. package/src/match/remote/native-service.ts +62 -0
  42. package/src/match/remote/parser.ts +134 -0
  43. package/src/match/remote/types.ts +116 -0
  44. package/src/match/replay.ts +102 -0
  45. package/src/match/state.ts +72 -0
  46. package/src/match/validation.ts +375 -0
  47. package/src/match/voice/assets/court-left.wav +0 -0
  48. package/src/match/voice/assets/court-right.wav +0 -0
  49. package/src/match/voice/assets/court-switch.wav +0 -0
  50. package/src/match/voice/assets/game-point.wav +0 -0
  51. package/src/match/voice/assets/game-won.wav +0 -0
  52. package/src/match/voice/assets/match-won.wav +0 -0
  53. package/src/match/voice/assets/number-0.wav +0 -0
  54. package/src/match/voice/assets/number-1.wav +0 -0
  55. package/src/match/voice/assets/number-10.wav +0 -0
  56. package/src/match/voice/assets/number-11.wav +0 -0
  57. package/src/match/voice/assets/number-12.wav +0 -0
  58. package/src/match/voice/assets/number-13.wav +0 -0
  59. package/src/match/voice/assets/number-14.wav +0 -0
  60. package/src/match/voice/assets/number-15.wav +0 -0
  61. package/src/match/voice/assets/number-16.wav +0 -0
  62. package/src/match/voice/assets/number-17.wav +0 -0
  63. package/src/match/voice/assets/number-18.wav +0 -0
  64. package/src/match/voice/assets/number-19.wav +0 -0
  65. package/src/match/voice/assets/number-2.wav +0 -0
  66. package/src/match/voice/assets/number-20.wav +0 -0
  67. package/src/match/voice/assets/number-3.wav +0 -0
  68. package/src/match/voice/assets/number-30.wav +0 -0
  69. package/src/match/voice/assets/number-4.wav +0 -0
  70. package/src/match/voice/assets/number-40.wav +0 -0
  71. package/src/match/voice/assets/number-5.wav +0 -0
  72. package/src/match/voice/assets/number-50.wav +0 -0
  73. package/src/match/voice/assets/number-6.wav +0 -0
  74. package/src/match/voice/assets/number-60.wav +0 -0
  75. package/src/match/voice/assets/number-7.wav +0 -0
  76. package/src/match/voice/assets/number-70.wav +0 -0
  77. package/src/match/voice/assets/number-8.wav +0 -0
  78. package/src/match/voice/assets/number-80.wav +0 -0
  79. package/src/match/voice/assets/number-9.wav +0 -0
  80. package/src/match/voice/assets/number-90.wav +0 -0
  81. package/src/match/voice/assets/number-lam.wav +0 -0
  82. package/src/match/voice/assets/number-mot.wav +0 -0
  83. package/src/match/voice/assets/score-deu-0.wav +0 -0
  84. package/src/match/voice/assets/score-deu-1.wav +0 -0
  85. package/src/match/voice/assets/score-deu-10.wav +0 -0
  86. package/src/match/voice/assets/score-deu-11.wav +0 -0
  87. package/src/match/voice/assets/score-deu-12.wav +0 -0
  88. package/src/match/voice/assets/score-deu-13.wav +0 -0
  89. package/src/match/voice/assets/score-deu-14.wav +0 -0
  90. package/src/match/voice/assets/score-deu-15.wav +0 -0
  91. package/src/match/voice/assets/score-deu-16.wav +0 -0
  92. package/src/match/voice/assets/score-deu-17.wav +0 -0
  93. package/src/match/voice/assets/score-deu-18.wav +0 -0
  94. package/src/match/voice/assets/score-deu-19.wav +0 -0
  95. package/src/match/voice/assets/score-deu-2.wav +0 -0
  96. package/src/match/voice/assets/score-deu-20.wav +0 -0
  97. package/src/match/voice/assets/score-deu-3.wav +0 -0
  98. package/src/match/voice/assets/score-deu-30.wav +0 -0
  99. package/src/match/voice/assets/score-deu-4.wav +0 -0
  100. package/src/match/voice/assets/score-deu-40.wav +0 -0
  101. package/src/match/voice/assets/score-deu-5.wav +0 -0
  102. package/src/match/voice/assets/score-deu-50.wav +0 -0
  103. package/src/match/voice/assets/score-deu-6.wav +0 -0
  104. package/src/match/voice/assets/score-deu-60.wav +0 -0
  105. package/src/match/voice/assets/score-deu-7.wav +0 -0
  106. package/src/match/voice/assets/score-deu-70.wav +0 -0
  107. package/src/match/voice/assets/score-deu-8.wav +0 -0
  108. package/src/match/voice/assets/score-deu-80.wav +0 -0
  109. package/src/match/voice/assets/score-deu-9.wav +0 -0
  110. package/src/match/voice/assets/score-deu-90.wav +0 -0
  111. package/src/match/voice/assets/score-deu-lam.wav +0 -0
  112. package/src/match/voice/assets/score-deu-mot.wav +0 -0
  113. package/src/match/voice/assets/server-1.wav +0 -0
  114. package/src/match/voice/assets/server-2.wav +0 -0
  115. package/src/match/voice/assets/service-change.wav +0 -0
  116. package/src/match/voice/assets/test-speaker.wav +0 -0
  117. package/src/match/voice/audio.ts +231 -0
  118. package/src/match/voice/bundled-assets.native.ts +47 -0
  119. package/src/match/voice/bundled-assets.ts +3 -0
  120. package/src/match/voice/gemini-engine.ts +167 -0
  121. package/src/match/voice/index.ts +7 -0
  122. package/src/match/voice/phrases.ts +185 -0
  123. package/src/match/voice/scheduler.ts +205 -0
  124. package/src/match/voice/service.ts +147 -0
  125. package/src/match/voice/types.ts +118 -0
  126. package/src/native-runtime.ts +241 -0
  127. package/src/plugin.ts +145 -0
  128. package/src/provider.tsx +176 -0
  129. package/src/version.ts +4 -0
@@ -0,0 +1,353 @@
1
+ import type {
2
+ CameraDeviceInfo,
3
+ RoomAdapter,
4
+ RoomConnection,
5
+ RoomEvent,
6
+ } from "./engine";
7
+
8
+ interface LocalTrackLike {
9
+ kind: string;
10
+ stop(): void;
11
+ mute?(): Promise<unknown>;
12
+ unmute?(): Promise<unknown>;
13
+ /** livekit-client LocalVideoTrack restart; deterministic facing switch. */
14
+ restartTrack?(options: {
15
+ facingMode?: "user" | "environment";
16
+ deviceId?: string;
17
+ resolution?: { width: number; height: number; frameRate: number };
18
+ }): Promise<unknown>;
19
+ mediaStreamTrack?: {
20
+ readyState?: string;
21
+ /**
22
+ * react-native-webrtc: đổi thiết bị/facing TẠI CHỖ trên cùng track native
23
+ * (mediaStreamTrackApplyConstraints) — preview và sender giữ nguyên.
24
+ */
25
+ applyConstraints?(constraints: {
26
+ facingMode?: "user" | "environment";
27
+ deviceId?: string;
28
+ width?: number;
29
+ height?: number;
30
+ frameRate?: number;
31
+ }): Promise<void>;
32
+ /** react-native-webrtc native camera flip; blind front/back toggle. */
33
+ _switchCamera?(): void;
34
+ };
35
+ }
36
+
37
+ interface RoomLike {
38
+ state: string;
39
+ localParticipant: {
40
+ publishTrack(track: LocalTrackLike, options?: unknown): Promise<unknown>;
41
+ unpublishTrack?(
42
+ track: LocalTrackLike,
43
+ stopOnUnpublish?: boolean,
44
+ ): Promise<unknown> | unknown;
45
+ };
46
+ connect(serverUrl: string, token: string, options?: unknown): Promise<void>;
47
+ disconnect(stopTracks?: boolean): Promise<void>;
48
+ on(event: string, listener: (...args: unknown[]) => void): unknown;
49
+ off(event: string, listener: (...args: unknown[]) => void): unknown;
50
+ }
51
+
52
+ export interface NativeLiveKitBindings {
53
+ registerGlobals(): void;
54
+ createRoom(): RoomLike;
55
+ createLocalTracks(options: {
56
+ audio: true;
57
+ video: {
58
+ facingMode?: "user" | "environment";
59
+ deviceId?: string;
60
+ resolution: { width: number; height: number; frameRate: number };
61
+ };
62
+ }): Promise<LocalTrackLike[]>;
63
+ startAudioSession(): Promise<void> | void;
64
+ stopAudioSession(): Promise<void> | void;
65
+ /** Current window orientation; capture dimensions follow it. */
66
+ isLandscape?(): boolean;
67
+ /** Physical/virtual cameras reported by the platform. */
68
+ listCameras?(): Promise<CameraDeviceInfo[]>;
69
+ cameraSource?: unknown;
70
+ events: {
71
+ reconnecting: string;
72
+ reconnected: string;
73
+ disconnected: string;
74
+ mediaDevicesError: string;
75
+ /** LiveKit RoomEvent.DataReceived — nhận lệnh điều khiển từ dashboard. */
76
+ dataReceived?: string;
77
+ };
78
+ }
79
+
80
+ export interface LocalPreviewReference {
81
+ videoTrack: unknown;
82
+ trackRef?: {
83
+ participant: RoomLike["localParticipant"];
84
+ publication: unknown;
85
+ source?: unknown;
86
+ };
87
+ }
88
+
89
+ export class LiveKitRoomAdapter implements RoomAdapter {
90
+ private readonly room: RoomLike;
91
+ private readonly listeners = new Set<(event: RoomEvent) => void>();
92
+ private tracks: LocalTrackLike[] = [];
93
+ private videoTrack?: LocalTrackLike;
94
+ private audioTrack?: LocalTrackLike;
95
+ private preview?: LocalPreviewReference;
96
+ private isFront = true;
97
+ private orientationOverride: "portrait" | "landscape" | null = null;
98
+ private deviceOverride: string | null = null;
99
+ private published = false;
100
+ private disposed = false;
101
+ private quality: 720 | 1080 = 720;
102
+ private readonly nativeListeners: Array<readonly [string, (...args: unknown[]) => void]>;
103
+
104
+ constructor(private readonly bindings: NativeLiveKitBindings) {
105
+ bindings.registerGlobals();
106
+ this.room = bindings.createRoom();
107
+ this.nativeListeners = [
108
+ [bindings.events.reconnecting, () => this.emit({ type: "reconnecting" })],
109
+ [bindings.events.reconnected, () => this.emit({ type: "reconnected" })],
110
+ [bindings.events.disconnected, (reason) => {
111
+ this.published = false;
112
+ this.emit({ type: "disconnected", reason: String(reason ?? "") });
113
+ }],
114
+ [bindings.events.mediaDevicesError, (error) =>
115
+ this.emit({ type: "media-error", error })],
116
+ ];
117
+ if (bindings.events.dataReceived) {
118
+ // Payload lệnh: {"type":"cmd","action":"start|stop|config",...} — cùng
119
+ // giao thức với camera box (backend sendCameraCommand).
120
+ this.nativeListeners.push([
121
+ bindings.events.dataReceived,
122
+ (payload) => {
123
+ try {
124
+ const text = new TextDecoder().decode(payload as Uint8Array);
125
+ const parsed = JSON.parse(text) as { type?: string; action?: string };
126
+ if (parsed?.type === "cmd" && typeof parsed.action === "string") {
127
+ this.emit({ type: "command", action: parsed.action });
128
+ }
129
+ } catch {
130
+ // Data message không phải lệnh — bỏ qua.
131
+ }
132
+ },
133
+ ]);
134
+ }
135
+ for (const [event, listener] of this.nativeListeners) {
136
+ this.room.on(event, listener);
137
+ }
138
+ }
139
+
140
+ async preparePreview(quality: 720 | 1080) {
141
+ this.ensureNotDisposed();
142
+ this.quality = quality;
143
+ await this.ensurePreparedTracks();
144
+ this.ensureNotDisposed();
145
+ return this.preview;
146
+ }
147
+
148
+ async connect(input: RoomConnection) {
149
+ this.ensureNotDisposed();
150
+ if (!this.preview) throw new Error("Preview must be prepared before connecting");
151
+ if (this.room.state !== "disconnected") await this.disconnect(false);
152
+ await this.ensurePreparedTracks();
153
+ if (!this.videoTrack || !this.audioTrack || !this.preview) {
154
+ throw new Error("Camera and microphone tracks are unavailable");
155
+ }
156
+ await this.bindings.startAudioSession();
157
+ await this.room.connect(input.serverUrl, input.participantToken, {
158
+ autoSubscribe: false,
159
+ });
160
+ this.published = false;
161
+ if (input.microphoneEnabled) await this.audioTrack.unmute?.();
162
+ else await this.audioTrack.mute?.();
163
+ if (input.publish !== false) {
164
+ await this.setPublishing(true);
165
+ }
166
+ }
167
+
168
+ /** Bật/tắt publish track khi đã trong phòng — standby ↔ live. */
169
+ async setPublishing(enabled: boolean) {
170
+ this.ensureNotDisposed();
171
+ if (enabled === this.published) return;
172
+ if (!this.videoTrack || !this.audioTrack || !this.preview) {
173
+ throw new Error("Camera and microphone tracks are unavailable");
174
+ }
175
+ if (enabled) {
176
+ const publication = await this.room.localParticipant.publishTrack(
177
+ this.videoTrack,
178
+ { simulcast: false, source: this.bindings.cameraSource },
179
+ );
180
+ await this.room.localParticipant.publishTrack(this.audioTrack);
181
+ this.preview.trackRef = {
182
+ participant: this.room.localParticipant,
183
+ publication,
184
+ source: this.bindings.cameraSource,
185
+ };
186
+ this.published = true;
187
+ return;
188
+ }
189
+ // Gỡ publish nhưng GIỮ track sống để preview local tiếp tục chạy.
190
+ await this.room.localParticipant.unpublishTrack?.(this.videoTrack, false);
191
+ await this.room.localParticipant.unpublishTrack?.(this.audioTrack, false);
192
+ delete this.preview.trackRef;
193
+ this.published = false;
194
+ }
195
+
196
+ async disconnect(stopTracks = true) {
197
+ this.ensureNotDisposed();
198
+ await this.room.disconnect(stopTracks);
199
+ this.published = false;
200
+ if (this.preview) delete this.preview.trackRef;
201
+ await this.bindings.stopAudioSession();
202
+ if (stopTracks) this.releaseTracks(true);
203
+ }
204
+
205
+ private captureResolution() {
206
+ // iOS capture formats luôn là kích thước ngang; khung dọc/ngang thực tế
207
+ // do hướng cầm máy quyết định (WebRTC gắn rotation theo từng frame).
208
+ // Yêu cầu kích thước dọc khiến native chọn nhầm format nhỏ (640x480).
209
+ const [long, short] = this.quality === 1080 ? [1920, 1080] : [1280, 720];
210
+ return { width: long, height: short, frameRate: 30 };
211
+ }
212
+
213
+ async setCaptureOrientation(orientation: "portrait" | "landscape") {
214
+ this.ensureNotDisposed();
215
+ // Chỉ ghi nhận lựa chọn (phục vụ hint UI/telemetry); capture format không
216
+ // đổi vì khung hình theo hướng cầm máy — xem captureResolution().
217
+ this.orientationOverride = orientation;
218
+ }
219
+
220
+ /**
221
+ * livekit-client restart() chỉ giữ deviceId/facingMode khi gọi getUserMedia
222
+ * (vứt width/height/frameRate) nên track mới rơi về format mặc định
223
+ * 640x480. Native applyConstraints thì ngược lại: chỉ xử lý
224
+ * width/height/frameRate. Sau mỗi restartTrack phải nắn lại format trên
225
+ * track mới bằng applyConstraints.
226
+ */
227
+ private async reapplyCaptureFormat() {
228
+ const nativeTrack = this.videoTrack?.mediaStreamTrack;
229
+ if (nativeTrack?.applyConstraints) {
230
+ await nativeTrack.applyConstraints(this.captureResolution());
231
+ }
232
+ }
233
+
234
+ async switchCamera() {
235
+ this.ensureNotDisposed();
236
+ const facingMode = this.isFront ? "environment" : "user";
237
+ const nativeTrack = this.videoTrack?.mediaStreamTrack;
238
+ // restartTrack đổi thiết bị theo facing tường minh; native applyConstraints
239
+ // KHÔNG đổi được thiết bị (chỉ width/height/frameRate) nên không dùng
240
+ // để lật camera. _switchCamera là toggle mù — fallback cuối.
241
+ if (typeof this.videoTrack?.restartTrack === "function") {
242
+ await this.videoTrack.restartTrack({ facingMode });
243
+ await this.reapplyCaptureFormat();
244
+ } else if (typeof nativeTrack?._switchCamera === "function") {
245
+ nativeTrack._switchCamera();
246
+ } else if (nativeTrack?.applyConstraints) {
247
+ // Web fallback: applyConstraints chuẩn có thể đổi facing trên trình duyệt.
248
+ await nativeTrack.applyConstraints({ facingMode });
249
+ } else {
250
+ throw new Error("Camera switching is unavailable");
251
+ }
252
+ // Lật theo hướng bỏ chọn ống kính cụ thể trước đó.
253
+ this.deviceOverride = null;
254
+ this.isFront = !this.isFront;
255
+ }
256
+
257
+ async listCameras(): Promise<CameraDeviceInfo[]> {
258
+ this.ensureNotDisposed();
259
+ return (await this.bindings.listCameras?.()) ?? [];
260
+ }
261
+
262
+ async selectCamera(deviceId: string, facing: CameraDeviceInfo["facing"]) {
263
+ this.ensureNotDisposed();
264
+ if (typeof this.videoTrack?.restartTrack !== "function") {
265
+ throw new Error("Camera selection is unavailable");
266
+ }
267
+ await this.videoTrack.restartTrack({ deviceId });
268
+ await this.reapplyCaptureFormat();
269
+ this.deviceOverride = deviceId;
270
+ if (facing !== "unknown") this.isFront = facing === "front";
271
+ }
272
+
273
+ async setMicrophoneEnabled(enabled: boolean) {
274
+ this.ensureNotDisposed();
275
+ if (!this.audioTrack) return;
276
+ if (enabled) await this.audioTrack.unmute?.();
277
+ else await this.audioTrack.mute?.();
278
+ }
279
+
280
+ subscribe(listener: (event: RoomEvent) => void) {
281
+ this.ensureNotDisposed();
282
+ this.listeners.add(listener);
283
+ return () => this.listeners.delete(listener);
284
+ }
285
+
286
+ async dispose() {
287
+ if (this.disposed) return;
288
+ this.disposed = true;
289
+ for (const [event, listener] of this.nativeListeners) {
290
+ this.room.off(event, listener);
291
+ }
292
+ await this.room.disconnect(true);
293
+ await this.bindings.stopAudioSession();
294
+ this.releaseTracks(true);
295
+ this.listeners.clear();
296
+ }
297
+
298
+ private releaseTracks(clearPreview: boolean) {
299
+ for (const track of this.tracks) track.stop();
300
+ this.tracks = [];
301
+ this.videoTrack = undefined;
302
+ this.audioTrack = undefined;
303
+ if (clearPreview) this.preview = undefined;
304
+ else if (this.preview) delete this.preview.trackRef;
305
+ }
306
+
307
+ private async ensurePreparedTracks() {
308
+ this.ensureNotDisposed();
309
+ const tracksReady =
310
+ this.videoTrack?.mediaStreamTrack?.readyState !== "ended" &&
311
+ this.audioTrack?.mediaStreamTrack?.readyState !== "ended" &&
312
+ Boolean(this.videoTrack && this.audioTrack);
313
+ if (tracksReady) return;
314
+ if (this.tracks.length) this.releaseTracks(false);
315
+ const resolution = this.captureResolution();
316
+ const tracks = await this.bindings.createLocalTracks({
317
+ audio: true,
318
+ video: {
319
+ ...(this.deviceOverride
320
+ ? { deviceId: this.deviceOverride }
321
+ : { facingMode: this.isFront ? "user" : "environment" }),
322
+ resolution,
323
+ },
324
+ });
325
+ if (this.disposed) {
326
+ for (const track of tracks) track.stop();
327
+ throw new Error("LiveKit room adapter has been disposed");
328
+ }
329
+ this.tracks = tracks;
330
+ this.videoTrack = this.tracks.find((track) => track.kind === "video");
331
+ this.audioTrack = this.tracks.find((track) => track.kind === "audio");
332
+ if (!this.videoTrack || !this.audioTrack) {
333
+ this.releaseTracks(true);
334
+ throw new Error("LiveKit did not create camera and microphone tracks");
335
+ }
336
+ if (this.preview) {
337
+ this.preview.videoTrack = this.videoTrack;
338
+ delete this.preview.trackRef;
339
+ } else {
340
+ this.preview = { videoTrack: this.videoTrack };
341
+ }
342
+ }
343
+
344
+ private emit(event: RoomEvent) {
345
+ for (const listener of this.listeners) listener(event);
346
+ }
347
+
348
+ private ensureNotDisposed() {
349
+ if (this.disposed) {
350
+ throw new Error("LiveKit room adapter has been disposed");
351
+ }
352
+ }
353
+ }