@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
@@ -1,390 +0,0 @@
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
- // Khớp engine (isCameraFront: false) — xem ghi chú ở engine.ts.
97
- private isFront = false;
98
- private orientationOverride: "portrait" | "landscape" | null = null;
99
- private deviceOverride: string | null = null;
100
- private published = false;
101
- private disposed = false;
102
- private quality: 720 | 1080 = 720;
103
- private readonly nativeListeners: Array<readonly [string, (...args: unknown[]) => void]>;
104
-
105
- constructor(private readonly bindings: NativeLiveKitBindings) {
106
- bindings.registerGlobals();
107
- this.room = bindings.createRoom();
108
- this.nativeListeners = [
109
- [bindings.events.reconnecting, () => this.emit({ type: "reconnecting" })],
110
- [bindings.events.reconnected, () => this.emit({ type: "reconnected" })],
111
- [bindings.events.disconnected, (reason) => {
112
- this.published = false;
113
- this.emit({ type: "disconnected", reason: String(reason ?? "") });
114
- }],
115
- [bindings.events.mediaDevicesError, (error) =>
116
- this.emit({ type: "media-error", error })],
117
- ];
118
- if (bindings.events.dataReceived) {
119
- // Payload lệnh: {"type":"cmd","action":"start|stop|config",...} — cùng
120
- // giao thức với camera box (backend sendCameraCommand).
121
- this.nativeListeners.push([
122
- bindings.events.dataReceived,
123
- (payload) => {
124
- try {
125
- const text = new TextDecoder().decode(payload as Uint8Array);
126
- const parsed = JSON.parse(text) as { type?: string; action?: string };
127
- if (parsed?.type === "cmd" && typeof parsed.action === "string") {
128
- this.emit({ type: "command", action: parsed.action });
129
- }
130
- } catch {
131
- // Data message không phải lệnh — bỏ qua.
132
- }
133
- },
134
- ]);
135
- }
136
- for (const [event, listener] of this.nativeListeners) {
137
- this.room.on(event, listener);
138
- }
139
- }
140
-
141
- async preparePreview(quality: 720 | 1080) {
142
- this.ensureNotDisposed();
143
- this.quality = quality;
144
- await this.ensurePreparedTracks();
145
- this.ensureNotDisposed();
146
- return this.preview;
147
- }
148
-
149
- async connect(input: RoomConnection) {
150
- this.ensureNotDisposed();
151
- // Chặn lệch mode (đối xứng với guard trong RtmpRoomAdapter): nếu app trên
152
- // server đã chuyển sang device-rtmp thì grant trả rtmp:// — LiveKit không
153
- // hiểu URL đó và sẽ hỏng câm lặng nếu không báo ở đây.
154
- if (!/^wss?:\/\//i.test(input.serverUrl)) {
155
- throw new Error(
156
- `Mode LiveKit cần endpoint wss:// nhưng phiên trả về "${input.serverUrl}". ` +
157
- "App trên server đang đặt deliveryMode khác — đổi mode trong app cho khớp.",
158
- );
159
- }
160
- if (!this.preview) throw new Error("Preview must be prepared before connecting");
161
- if (this.room.state !== "disconnected") await this.disconnect(false);
162
- await this.ensurePreparedTracks();
163
- if (!this.videoTrack || !this.audioTrack || !this.preview) {
164
- throw new Error("Camera and microphone tracks are unavailable");
165
- }
166
- await this.bindings.startAudioSession();
167
- await this.room.connect(input.serverUrl, input.participantToken, {
168
- autoSubscribe: false,
169
- });
170
- this.published = false;
171
- if (input.microphoneEnabled) await this.audioTrack.unmute?.();
172
- else await this.audioTrack.mute?.();
173
- if (input.publish !== false) {
174
- await this.setPublishing(true);
175
- }
176
- }
177
-
178
- /** Bật/tắt publish track khi đã trong phòng — standby ↔ live. */
179
- async setPublishing(enabled: boolean) {
180
- this.ensureNotDisposed();
181
- if (enabled === this.published) return;
182
- if (!this.videoTrack || !this.audioTrack || !this.preview) {
183
- throw new Error("Camera and microphone tracks are unavailable");
184
- }
185
- if (enabled) {
186
- const publication = await this.room.localParticipant.publishTrack(
187
- this.videoTrack,
188
- this.videoPublishOptions(),
189
- );
190
- await this.room.localParticipant.publishTrack(this.audioTrack);
191
- this.preview.trackRef = {
192
- participant: this.room.localParticipant,
193
- publication,
194
- source: this.bindings.cameraSource,
195
- };
196
- this.published = true;
197
- return;
198
- }
199
- // Gỡ publish nhưng GIỮ track sống để preview local tiếp tục chạy.
200
- await this.room.localParticipant.unpublishTrack?.(this.videoTrack, false);
201
- await this.room.localParticipant.unpublishTrack?.(this.audioTrack, false);
202
- delete this.preview.trackRef;
203
- this.published = false;
204
- }
205
-
206
- async disconnect(stopTracks = true) {
207
- this.ensureNotDisposed();
208
- await this.room.disconnect(stopTracks);
209
- this.published = false;
210
- if (this.preview) delete this.preview.trackRef;
211
- await this.bindings.stopAudioSession();
212
- if (stopTracks) this.releaseTracks(true);
213
- }
214
-
215
- private captureResolution() {
216
- // iOS capture formats luôn là kích thước ngang; khung dọc/ngang thực tế
217
- // do hướng cầm máy quyết định (WebRTC gắn rotation theo từng frame).
218
- // Yêu cầu kích thước dọc khiến native chọn nhầm format nhỏ (640x480).
219
- const [long, short] = this.quality === 1080 ? [1920, 1080] : [1280, 720];
220
- return { width: long, height: short, frameRate: 30 };
221
- }
222
-
223
- /**
224
- * Tùy chọn publish video. Mặc định của livekit-client (VP8, không encoding,
225
- * không degradation preference) khiến uplink yếu bóp bitrate xuống ~600kbps
226
- * mà VẪN giữ 1080 @ ~10fps → khối bể nát, tệ hơn 720p sạch.
227
- *
228
- * - videoCodec H.264: HW-encode trên Android, hiệu quả hơn VP8 ở bitrate thấp,
229
- * trùng preset egress (H264_*) nên bỏ được 1 lần transcode phía egress.
230
- * - simulcast tắt: viewer chỉ xem HLS do egress xuất; egress subscribe đúng
231
- * 1 lớp đã publish, nên simulcast chỉ thêm tải/bất ổn encoder (H.264 simulcast
232
- * kém tin cậy trên Android) mà không lợi gì cho người xem ở kiến trúc này.
233
- * - maintain-framerate: pickleball chuyển động nhanh; khi uplink không gánh nổi
234
- * mức đích thì HẠ ĐỘ PHÂN GIẢI (giữ nét-trên-điểm-ảnh) thay vì đóng băng fps.
235
- * - maxBitrate: trần theo mức chất lượng; BWE vẫn tự hạ dưới trần khi mạng yếu.
236
- */
237
- private videoPublishOptions() {
238
- return {
239
- source: this.bindings.cameraSource,
240
- simulcast: false,
241
- videoCodec: "h264",
242
- degradationPreference: "maintain-framerate",
243
- videoEncoding: {
244
- maxBitrate: this.quality === 1080 ? 4_500_000 : 2_500_000,
245
- maxFramerate: 30,
246
- },
247
- };
248
- }
249
-
250
- async setCaptureOrientation(orientation: "portrait" | "landscape") {
251
- this.ensureNotDisposed();
252
- // Chỉ ghi nhận lựa chọn (phục vụ hint UI/telemetry); capture format không
253
- // đổi vì khung hình theo hướng cầm máy — xem captureResolution().
254
- this.orientationOverride = orientation;
255
- }
256
-
257
- /**
258
- * livekit-client restart() chỉ giữ deviceId/facingMode khi gọi getUserMedia
259
- * (vứt width/height/frameRate) nên track mới rơi về format mặc định
260
- * 640x480. Native applyConstraints thì ngược lại: chỉ xử lý
261
- * width/height/frameRate. Sau mỗi restartTrack phải nắn lại format trên
262
- * track mới bằng applyConstraints.
263
- */
264
- private async reapplyCaptureFormat() {
265
- const nativeTrack = this.videoTrack?.mediaStreamTrack;
266
- if (nativeTrack?.applyConstraints) {
267
- await nativeTrack.applyConstraints(this.captureResolution());
268
- }
269
- }
270
-
271
- async switchCamera() {
272
- this.ensureNotDisposed();
273
- const facingMode = this.isFront ? "environment" : "user";
274
- const nativeTrack = this.videoTrack?.mediaStreamTrack;
275
- // restartTrack đổi thiết bị theo facing tường minh; native applyConstraints
276
- // KHÔNG đổi được thiết bị (chỉ width/height/frameRate) nên không dùng
277
- // để lật camera. _switchCamera là toggle mù — fallback cuối.
278
- if (typeof this.videoTrack?.restartTrack === "function") {
279
- await this.videoTrack.restartTrack({ facingMode });
280
- await this.reapplyCaptureFormat();
281
- } else if (typeof nativeTrack?._switchCamera === "function") {
282
- nativeTrack._switchCamera();
283
- } else if (nativeTrack?.applyConstraints) {
284
- // Web fallback: applyConstraints chuẩn có thể đổi facing trên trình duyệt.
285
- await nativeTrack.applyConstraints({ facingMode });
286
- } else {
287
- throw new Error("Camera switching is unavailable");
288
- }
289
- // Lật theo hướng bỏ chọn ống kính cụ thể trước đó.
290
- this.deviceOverride = null;
291
- this.isFront = !this.isFront;
292
- }
293
-
294
- async listCameras(): Promise<CameraDeviceInfo[]> {
295
- this.ensureNotDisposed();
296
- return (await this.bindings.listCameras?.()) ?? [];
297
- }
298
-
299
- async selectCamera(deviceId: string, facing: CameraDeviceInfo["facing"]) {
300
- this.ensureNotDisposed();
301
- if (typeof this.videoTrack?.restartTrack !== "function") {
302
- throw new Error("Camera selection is unavailable");
303
- }
304
- await this.videoTrack.restartTrack({ deviceId });
305
- await this.reapplyCaptureFormat();
306
- this.deviceOverride = deviceId;
307
- if (facing !== "unknown") this.isFront = facing === "front";
308
- }
309
-
310
- async setMicrophoneEnabled(enabled: boolean) {
311
- this.ensureNotDisposed();
312
- if (!this.audioTrack) return;
313
- if (enabled) await this.audioTrack.unmute?.();
314
- else await this.audioTrack.mute?.();
315
- }
316
-
317
- subscribe(listener: (event: RoomEvent) => void) {
318
- this.ensureNotDisposed();
319
- this.listeners.add(listener);
320
- return () => this.listeners.delete(listener);
321
- }
322
-
323
- async dispose() {
324
- if (this.disposed) return;
325
- this.disposed = true;
326
- for (const [event, listener] of this.nativeListeners) {
327
- this.room.off(event, listener);
328
- }
329
- await this.room.disconnect(true);
330
- await this.bindings.stopAudioSession();
331
- this.releaseTracks(true);
332
- this.listeners.clear();
333
- }
334
-
335
- private releaseTracks(clearPreview: boolean) {
336
- for (const track of this.tracks) track.stop();
337
- this.tracks = [];
338
- this.videoTrack = undefined;
339
- this.audioTrack = undefined;
340
- if (clearPreview) this.preview = undefined;
341
- else if (this.preview) delete this.preview.trackRef;
342
- }
343
-
344
- private async ensurePreparedTracks() {
345
- this.ensureNotDisposed();
346
- const tracksReady =
347
- this.videoTrack?.mediaStreamTrack?.readyState !== "ended" &&
348
- this.audioTrack?.mediaStreamTrack?.readyState !== "ended" &&
349
- Boolean(this.videoTrack && this.audioTrack);
350
- if (tracksReady) return;
351
- if (this.tracks.length) this.releaseTracks(false);
352
- const resolution = this.captureResolution();
353
- const tracks = await this.bindings.createLocalTracks({
354
- audio: true,
355
- video: {
356
- ...(this.deviceOverride
357
- ? { deviceId: this.deviceOverride }
358
- : { facingMode: this.isFront ? "user" : "environment" }),
359
- resolution,
360
- },
361
- });
362
- if (this.disposed) {
363
- for (const track of tracks) track.stop();
364
- throw new Error("LiveKit room adapter has been disposed");
365
- }
366
- this.tracks = tracks;
367
- this.videoTrack = this.tracks.find((track) => track.kind === "video");
368
- this.audioTrack = this.tracks.find((track) => track.kind === "audio");
369
- if (!this.videoTrack || !this.audioTrack) {
370
- this.releaseTracks(true);
371
- throw new Error("LiveKit did not create camera and microphone tracks");
372
- }
373
- if (this.preview) {
374
- this.preview.videoTrack = this.videoTrack;
375
- delete this.preview.trackRef;
376
- } else {
377
- this.preview = { videoTrack: this.videoTrack };
378
- }
379
- }
380
-
381
- private emit(event: RoomEvent) {
382
- for (const listener of this.listeners) listener(event);
383
- }
384
-
385
- private ensureNotDisposed() {
386
- if (this.disposed) {
387
- throw new Error("LiveKit room adapter has been disposed");
388
- }
389
- }
390
- }