@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +163 -40
  3. package/dist/advanced-DcbHBCGb.d.ts +300 -0
  4. package/dist/advanced-DeQwg7MK.d.cts +300 -0
  5. package/dist/advanced.cjs +47 -0
  6. package/dist/advanced.d.cts +3 -0
  7. package/dist/advanced.d.ts +3 -0
  8. package/dist/advanced.js +47 -0
  9. package/dist/camera-controls-By8tHigY.d.cts +97 -0
  10. package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
  11. package/dist/chunk-2JHULJZ7.js +265 -0
  12. package/dist/chunk-62YV3PCD.cjs +139 -0
  13. package/dist/chunk-6F3HN6US.js +50 -0
  14. package/dist/chunk-6YE5BL3A.cjs +50 -0
  15. package/dist/chunk-7OX2DSKI.cjs +33 -0
  16. package/dist/chunk-7Z7TN5N3.js +318 -0
  17. package/dist/chunk-DHDX3ZOO.js +800 -0
  18. package/dist/chunk-GXQYMPHE.cjs +9 -0
  19. package/dist/chunk-IUIRVKJI.cjs +2290 -0
  20. package/dist/chunk-KFQGP6VL.js +33 -0
  21. package/dist/chunk-LCWGNGEN.cjs +265 -0
  22. package/dist/chunk-TBUEXRFQ.cjs +318 -0
  23. package/dist/chunk-XGP2MN5R.js +2290 -0
  24. package/dist/chunk-XL4CISVQ.js +9 -0
  25. package/dist/chunk-YGSQ25UA.cjs +800 -0
  26. package/dist/chunk-ZSKSXOCZ.js +139 -0
  27. package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
  28. package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
  29. package/dist/device-agent/index.cjs +1427 -0
  30. package/dist/device-agent/index.d.cts +493 -0
  31. package/dist/device-agent/index.d.ts +493 -0
  32. package/dist/device-agent/index.js +1427 -0
  33. package/dist/engine-BSvSdCSE.d.cts +741 -0
  34. package/dist/engine-BSvSdCSE.d.ts +741 -0
  35. package/dist/index.cjs +426 -6174
  36. package/dist/index.d.cts +299 -1400
  37. package/dist/index.d.ts +299 -1400
  38. package/dist/index.js +413 -6063
  39. package/dist/match/index.cjs +2456 -0
  40. package/dist/match/index.d.cts +253 -0
  41. package/dist/match/index.d.ts +253 -0
  42. package/dist/match/index.js +2456 -0
  43. package/dist/match/remote/index.cjs +21 -0
  44. package/dist/match/remote/index.d.cts +67 -0
  45. package/dist/match/remote/index.d.ts +67 -0
  46. package/dist/match/remote/index.js +21 -0
  47. package/dist/match/voice/index.cjs +23 -0
  48. package/dist/match/voice/index.d.cts +114 -0
  49. package/dist/match/voice/index.d.ts +114 -0
  50. package/dist/match/voice/index.js +23 -0
  51. package/dist/plugin.cjs +52 -71
  52. package/dist/plugin.d.cts +28 -1
  53. package/dist/plugin.d.ts +28 -1
  54. package/dist/plugin.js +26 -23
  55. package/dist/realtime/index.cjs +31 -0
  56. package/dist/realtime/index.d.cts +15 -0
  57. package/dist/realtime/index.d.ts +15 -0
  58. package/dist/realtime/index.js +31 -0
  59. package/dist/types-CrmcX408.d.cts +176 -0
  60. package/dist/types-CrmcX408.d.ts +176 -0
  61. package/dist/types-DiKWjwvN.d.cts +96 -0
  62. package/dist/types-DiKWjwvN.d.ts +96 -0
  63. package/dist/ui/index.cjs +1297 -0
  64. package/dist/ui/index.d.cts +493 -0
  65. package/dist/ui/index.d.ts +493 -0
  66. package/dist/ui/index.js +1297 -0
  67. package/dist/version.cjs +4 -32
  68. package/dist/version.js +2 -1
  69. package/package.json +95 -15
  70. package/src/advanced.ts +49 -0
  71. package/src/auto-resume.tsx +259 -0
  72. package/src/camera-controls.ts +99 -0
  73. package/src/camera-grant-provider.ts +392 -0
  74. package/src/camera-settings.ts +133 -0
  75. package/src/contracts.ts +73 -0
  76. package/src/device-agent/agent.ts +696 -0
  77. package/src/device-agent/beat-policy.ts +46 -0
  78. package/src/device-agent/component.tsx +60 -0
  79. package/src/device-agent/convex-beat-transport.ts +140 -0
  80. package/src/device-agent/convex-command-subscription.ts +101 -0
  81. package/src/device-agent/convex-control-channel.ts +88 -0
  82. package/src/device-agent/http-transport.ts +164 -0
  83. package/src/device-agent/index.ts +26 -0
  84. package/src/device-agent/mqtt-channel.ts +379 -0
  85. package/src/device-agent/mqtt-codec.ts +347 -0
  86. package/src/device-agent/tournament.ts +352 -0
  87. package/src/engine.ts +601 -20
  88. package/src/errors.ts +16 -0
  89. package/src/http-session-provider.ts +21 -183
  90. package/src/index.ts +117 -14
  91. package/src/local-video-quality.ts +43 -0
  92. package/src/match/index.ts +29 -0
  93. package/src/match/provider.tsx +4 -2
  94. package/src/match/remote/hid-gesture.ts +10 -2
  95. package/src/match/remote/types.ts +6 -4
  96. package/src/match/voice/scheduler.ts +6 -1
  97. package/src/match/voice/service.ts +16 -4
  98. package/src/native-runtime.ts +283 -77
  99. package/src/plugin.ts +54 -20
  100. package/src/provider.tsx +129 -22
  101. package/src/realtime/index.ts +56 -0
  102. package/src/resume-policy.ts +85 -0
  103. package/src/resume.ts +132 -0
  104. package/src/rtmp-room-adapter.ts +348 -0
  105. package/src/session-grant.ts +256 -0
  106. package/src/ui/camera-screen.tsx +484 -0
  107. package/src/ui/controls-sheet.tsx +445 -0
  108. package/src/ui/copy.ts +326 -0
  109. package/src/ui/helpers.ts +105 -0
  110. package/src/ui/hooks.ts +59 -0
  111. package/src/ui/index.ts +42 -0
  112. package/src/ui/preview-view.tsx +39 -0
  113. package/src/ui/primitives.tsx +304 -0
  114. package/src/uplink-check.ts +173 -0
  115. package/dist/chunk-VHLUJQG5.js +0 -17
  116. package/src/livekit-room-adapter.ts +0 -353
  117. package/src/livestream-screen.tsx +0 -980
package/dist/version.cjs CHANGED
@@ -1,35 +1,7 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
19
2
 
20
- // src/version.ts
21
- var version_exports = {};
22
- __export(version_exports, {
23
- PICKLEBALL_EXPO_SDK_VERSION: () => PICKLEBALL_EXPO_SDK_VERSION
24
- });
25
- module.exports = __toCommonJS(version_exports);
3
+ var _chunkGXQYMPHEcjs = require('./chunk-GXQYMPHE.cjs');
4
+ require('./chunk-7OX2DSKI.cjs');
26
5
 
27
- // package.json
28
- var version = "0.1.1";
29
6
 
30
- // src/version.ts
31
- var PICKLEBALL_EXPO_SDK_VERSION = version;
32
- // Annotate the CommonJS export names for ESM import in node:
33
- 0 && (module.exports = {
34
- PICKLEBALL_EXPO_SDK_VERSION
35
- });
7
+ exports.PICKLEBALL_EXPO_SDK_VERSION = _chunkGXQYMPHEcjs.PICKLEBALL_EXPO_SDK_VERSION;
package/dist/version.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  PICKLEBALL_EXPO_SDK_VERSION
3
- } from "./chunk-VHLUJQG5.js";
3
+ } from "./chunk-XL4CISVQ.js";
4
+ import "./chunk-KFQGP6VL.js";
4
5
  export {
5
6
  PICKLEBALL_EXPO_SDK_VERSION
6
7
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pickleball/expo-sdk",
3
- "version": "0.1.1",
4
- "description": "Foreground livestream engine, React hooks, UI, and Expo config plugin for Pickleball Live",
3
+ "version": "1.0.0",
4
+ "description": "RTMP livestream engine, camera-grant session provider, React hooks, ready-made camera UI, and Expo config plugin for Pickleball Live",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -27,6 +27,61 @@
27
27
  "default": "./dist/index.cjs"
28
28
  }
29
29
  },
30
+ "./match": {
31
+ "react-native": "./src/match/index.ts",
32
+ "import": {
33
+ "types": "./dist/match/index.d.ts",
34
+ "default": "./dist/match/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/match/index.d.cts",
38
+ "default": "./dist/match/index.cjs"
39
+ }
40
+ },
41
+ "./match/voice": {
42
+ "react-native": "./src/match/voice/index.ts",
43
+ "import": {
44
+ "types": "./dist/match/voice/index.d.ts",
45
+ "default": "./dist/match/voice/index.js"
46
+ },
47
+ "require": {
48
+ "types": "./dist/match/voice/index.d.cts",
49
+ "default": "./dist/match/voice/index.cjs"
50
+ }
51
+ },
52
+ "./match/remote": {
53
+ "react-native": "./src/match/remote/index.ts",
54
+ "import": {
55
+ "types": "./dist/match/remote/index.d.ts",
56
+ "default": "./dist/match/remote/index.js"
57
+ },
58
+ "require": {
59
+ "types": "./dist/match/remote/index.d.cts",
60
+ "default": "./dist/match/remote/index.cjs"
61
+ }
62
+ },
63
+ "./device-agent": {
64
+ "react-native": "./src/device-agent/index.ts",
65
+ "import": {
66
+ "types": "./dist/device-agent/index.d.ts",
67
+ "default": "./dist/device-agent/index.js"
68
+ },
69
+ "require": {
70
+ "types": "./dist/device-agent/index.d.cts",
71
+ "default": "./dist/device-agent/index.cjs"
72
+ }
73
+ },
74
+ "./advanced": {
75
+ "react-native": "./src/advanced.ts",
76
+ "import": {
77
+ "types": "./dist/advanced.d.ts",
78
+ "default": "./dist/advanced.js"
79
+ },
80
+ "require": {
81
+ "types": "./dist/advanced.d.cts",
82
+ "default": "./dist/advanced.cjs"
83
+ }
84
+ },
30
85
  "./plugin": {
31
86
  "import": {
32
87
  "types": "./dist/plugin.d.ts",
@@ -37,6 +92,29 @@
37
92
  "default": "./dist/plugin.cjs"
38
93
  }
39
94
  },
95
+ "./app.plugin.js": "./app.plugin.js",
96
+ "./realtime": {
97
+ "react-native": "./src/realtime/index.ts",
98
+ "import": {
99
+ "types": "./dist/realtime/index.d.ts",
100
+ "default": "./dist/realtime/index.js"
101
+ },
102
+ "require": {
103
+ "types": "./dist/realtime/index.d.cts",
104
+ "default": "./dist/realtime/index.cjs"
105
+ }
106
+ },
107
+ "./ui": {
108
+ "react-native": "./src/ui/index.ts",
109
+ "import": {
110
+ "types": "./dist/ui/index.d.ts",
111
+ "default": "./dist/ui/index.js"
112
+ },
113
+ "require": {
114
+ "types": "./dist/ui/index.d.cts",
115
+ "default": "./dist/ui/index.cjs"
116
+ }
117
+ },
40
118
  "./version": {
41
119
  "react-native": "./src/version.ts",
42
120
  "import": {
@@ -63,61 +141,63 @@
63
141
  "keywords": [
64
142
  "expo",
65
143
  "react-native",
66
- "livekit",
144
+ "rtmp",
67
145
  "livestream",
146
+ "pickleball",
68
147
  "video"
69
148
  ],
70
149
  "sideEffects": [
71
- "./dist/native-runtime.*"
150
+ "./dist/native-runtime.*",
151
+ "./src/native-runtime.ts"
72
152
  ],
73
153
  "peerDependencies": {
74
- "@livekit/react-native": ">=2.11.1 <3",
75
- "@livekit/react-native-webrtc": ">=144.1.1 <145",
154
+ "@pickleball/rtmp-native": ">=0.2.0 <1",
76
155
  "@react-native-community/netinfo": ">=11 <13",
77
156
  "expo": ">=56 <58",
78
157
  "expo-audio": ">=56 <58",
79
158
  "expo-file-system": ">=56 <58",
80
159
  "expo-keep-awake": ">=55 <58",
81
160
  "expo-sqlite": ">=55 <58",
82
- "livekit-client": ">=2.20.1 <3",
83
161
  "react": ">=19 <20",
84
162
  "react-native": ">=0.85 <0.87",
85
163
  "react-native-safe-area-context": ">=5 <6"
86
164
  },
87
165
  "dependencies": {
88
- "@livekit/react-native-expo-plugin": "1.0.2",
89
- "expo-speech": "^57.0.1",
166
+ "convex": "^1.42.1",
90
167
  "@pickleball/remote-native": "0.1.0",
91
168
  "@pickleball/voice-native": "0.1.0"
92
169
  },
93
170
  "devDependencies": {
94
171
  "@expo/config-plugins": "~57.0.0",
95
- "@livekit/react-native": "2.11.1",
96
- "@livekit/react-native-webrtc": "144.1.1",
97
172
  "@react-native-community/netinfo": "12.0.1",
98
173
  "@testing-library/react-native": "^13.3.3",
99
174
  "@types/jest": "^29.5.14",
100
175
  "@types/node": "^22.15.0",
101
176
  "@types/react": "~19.2.2",
102
- "expo": "57.0.4",
177
+ "expo": "~57.0.14",
103
178
  "expo-audio": "57.0.2",
104
179
  "expo-file-system": "57.0.0",
105
180
  "expo-keep-awake": "57.0.0",
106
181
  "expo-sqlite": "57.0.0",
107
182
  "jest": "^29.7.0",
108
183
  "jest-expo": "~57.0.0",
109
- "livekit-client": "2.20.1",
110
184
  "react": "19.2.3",
111
185
  "react-dom": "19.2.3",
112
- "react-native": "0.86.0",
186
+ "react-native": "0.86.2",
113
187
  "react-native-safe-area-context": "~5.7.0",
114
188
  "react-test-renderer": "19.2.3",
115
189
  "tsup": "^8.5.0",
116
190
  "typescript": "^5.8.0",
191
+ "@pickleball/rtmp-native": "0.2.0",
117
192
  "@pickleball/shared": "0.1.0"
118
193
  },
194
+ "peerDependenciesMeta": {
195
+ "expo-audio": {
196
+ "optional": true
197
+ }
198
+ },
119
199
  "scripts": {
120
- "build": "tsup src/index.ts src/plugin.ts src/version.ts --format esm,cjs --dts --clean --external react --external react-native --external expo --external expo-sqlite --external livekit-client --external @livekit/react-native --external @livekit/react-native-webrtc",
200
+ "build": "tsup",
121
201
  "test": "jest --runInBand",
122
202
  "typecheck": "tsc --noEmit",
123
203
  "verify:pack": "node scripts/verify-pack.mjs",
@@ -0,0 +1,49 @@
1
+ /**
2
+ * API nâng cao: engine internals, dependency injection, adapter RTMP và các
3
+ * factory runtime mặc định — dùng cho mock/test hoặc tuỳ biến sâu. App tích hợp
4
+ * thông thường không cần import từ đây.
5
+ */
6
+ export {
7
+ LivestreamEngine,
8
+ LEGAL_LIVESTREAM_TRANSITIONS,
9
+ PENDING_END_KEY,
10
+ type EngineDependencies,
11
+ type RoomAdapter,
12
+ type RoomConnection,
13
+ type RoomEvent,
14
+ type SdkRuntime,
15
+ type SdkStorage,
16
+ type SdkNetwork,
17
+ type SessionCommandChannel,
18
+ type TelemetrySink,
19
+ type LivestreamSnapshot,
20
+ type DeviceMetricsSample,
21
+ type CaptureOrientation,
22
+ type CameraDeviceInfo,
23
+ } from "./engine";
24
+ export {
25
+ RtmpRoomAdapter,
26
+ type NativeRtmpBindings,
27
+ type RtmpStatus,
28
+ } from "./rtmp-room-adapter";
29
+ export {
30
+ isBootstrap,
31
+ isCameraGrant,
32
+ isSessionGrant,
33
+ isSessionRefresh,
34
+ isTelemetryCredentials,
35
+ sessionApiBaseFromTelemetryEndpoint,
36
+ } from "./session-grant";
37
+ export {
38
+ createDefaultRuntime,
39
+ createRtmpRuntime,
40
+ createDefaultStorage,
41
+ createDefaultNetwork,
42
+ createDefaultTelemetry,
43
+ createDefaultRoomFactory,
44
+ createRtmpRoomFactory,
45
+ createNativeRtmpBindings,
46
+ createDefaultDeviceMetrics,
47
+ } from "./native-runtime";
48
+ // Context nội bộ cho module opt-in (PickleballDeviceAgent) cần engine trực tiếp
49
+ export { useLivestreamHost, type LivestreamHostContextValue } from "./provider";
@@ -0,0 +1,259 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { StartLivestreamInput } from "./contracts";
3
+ import { useLivestream, useLivestreamHost } from "./provider";
4
+ import type { ResumableSessionProvider, ResumeAttempt } from "./resume";
5
+ import { resumeBlocksArm, resumeExternalSessionId, resumeRetryDelayMs } from "./resume-policy";
6
+
7
+ /**
8
+ * Ba mảnh "tự động" chuyển từ apps/device-streamer vào SDK — mọi app RTMP cần
9
+ * chúng, không riêng máy fleet:
10
+ *
11
+ * - PickleballAutoPrepare: camera luôn bật (prepare khi idle/ended).
12
+ * - PickleballAutoResume: app mở lại thấy phiên còn sống → nhận lại đúng phiên.
13
+ * - useStandbySession: chế độ sân — arm một phiên chờ, server lật live bằng lệnh.
14
+ *
15
+ * Các bẫy đã đo trên fleet (18–19/08/2026) nằm nguyên trong đây: retry publish
16
+ * khi encoder chưa có SPS, thoát hố "error" khi còn phiên chờ nhận lại, và
17
+ * KHÔNG arm phiên mới khi còn phiên chờ nhận lại.
18
+ *
19
+ * Kỷ luật dependency: mọi effect ở đây chỉ phụ thuộc vào TRẠNG THÁI (state,
20
+ * sessionId, id phiên chờ, cờ enabled) — hành động của engine, props cấu hình
21
+ * (resumeInput, retryDelayMs, input) và callback đi qua ref. App đối tác thường
22
+ * truyền object/hàm inline mỗi lần render; đưa chúng vào deps là mỗi render
23
+ * huỷ lượt resume đang bay, bắn lượt mới ngay (spam refresh) và timer thử lại
24
+ * không bao giờ kịp nổ.
25
+ */
26
+
27
+ export type ResumeAttemptOutcome =
28
+ | ResumeAttempt["kind"]
29
+ | "start_failed"
30
+ | "publish_retry"
31
+ | "reprepare";
32
+
33
+ export interface PickleballAutoResumeProps {
34
+ provider: ResumableSessionProvider;
35
+ /** Thông tin dựng lượt start nhận lại (title mặc định khi không biết tên phiên). */
36
+ resumeInput: {
37
+ consentVersion: string;
38
+ visibility?: "public" | "private";
39
+ metadata?: Record<string, string>;
40
+ titleFor?: (sessionId: string) => string;
41
+ };
42
+ /** false = tạm dừng (VD máy đang nghỉ). */
43
+ enabled?: boolean;
44
+ /**
45
+ * Nguồn thứ hai của id phiên còn sống (fleet: serverSession từ heartbeat).
46
+ * adoptableSessionId (dấu trên đĩa) luôn được ưu tiên vì có sớm hơn vài giây.
47
+ * Truyền CÙNG giá trị này vào `useStandbySession({resumeSessionId})` nếu app
48
+ * arm phiên chờ, để arm nhường đường nhận lại.
49
+ */
50
+ sessionIdHint?: string | null;
51
+ retryDelayMs?: (attempt: number) => number;
52
+ onAttempt?: (sessionId: string, outcome: ResumeAttemptOutcome) => void;
53
+ }
54
+
55
+ function useLatest<T>(value: T) {
56
+ const ref = useRef(value);
57
+ ref.current = value;
58
+ return ref;
59
+ }
60
+
61
+ export function PickleballAutoResume(props: PickleballAutoResumeProps): null {
62
+ const {
63
+ provider,
64
+ resumeInput,
65
+ enabled = true,
66
+ sessionIdHint = null,
67
+ retryDelayMs = resumeRetryDelayMs,
68
+ onAttempt,
69
+ } = props;
70
+ const live = useLivestream();
71
+ const { engine } = useLivestreamHost();
72
+ const goneRef = useRef<string | null>(null);
73
+ const attemptRef = useRef(0);
74
+ const [retryTick, setRetryTick] = useState(0);
75
+ const { state, sessionId, adoptableSessionId } = live;
76
+ const resumeId = adoptableSessionId ?? sessionIdHint;
77
+ const liveRef = useLatest(live);
78
+ const onAttemptRef = useLatest(onAttempt);
79
+ const resumeInputRef = useLatest(resumeInput);
80
+ const retryDelayRef = useLatest(retryDelayMs);
81
+
82
+ useEffect(() => {
83
+ if (!enabled) return;
84
+ if (!resumeId || sessionId !== null || state !== "preview") return;
85
+ if (goneRef.current === resumeId) return;
86
+
87
+ let cancelled = false;
88
+ let timer: ReturnType<typeof setTimeout> | undefined;
89
+ const scheduleRetry = () => {
90
+ const delay = retryDelayRef.current(attemptRef.current);
91
+ attemptRef.current += 1;
92
+ timer = setTimeout(() => {
93
+ if (!cancelled) setRetryTick((tick) => tick + 1);
94
+ }, delay);
95
+ };
96
+
97
+ void (async () => {
98
+ const result = await provider.resume(resumeId);
99
+ if (cancelled) return;
100
+ onAttemptRef.current?.(resumeId, result.kind);
101
+ if (result.kind === "gone") {
102
+ // Server nói phiên không còn. PHẢI bỏ dấu, không chỉ ghi nhớ ở ref: dấu
103
+ // còn nằm đó là mọi lượt start phiên MỚI bị chặn, mà lượt duy nhất được
104
+ // cho qua lại là nhận lại chính phiên đã chết — máy kẹt vĩnh viễn ở "error".
105
+ goneRef.current = resumeId;
106
+ attemptRef.current = 0;
107
+ void engine.abandonAdoptableSession(resumeId).catch(() => undefined);
108
+ return;
109
+ }
110
+ if (result.kind === "retry") {
111
+ scheduleRetry();
112
+ return;
113
+ }
114
+ attemptRef.current = 0;
115
+ provider.adoptResumeGrant(result.grant);
116
+ const resumeInput = resumeInputRef.current;
117
+ await liveRef.current
118
+ .start(
119
+ {
120
+ externalSessionId: resumeExternalSessionId(resumeId),
121
+ title: resumeInput.titleFor?.(resumeId) ?? "Phiên đang khôi phục",
122
+ consentVersion: resumeInput.consentVersion,
123
+ visibility: resumeInput.visibility ?? "public",
124
+ ...(resumeInput.metadata ? { metadata: resumeInput.metadata } : {}),
125
+ },
126
+ // Trạng thái lấy từ chính phản hồi refresh, KHÔNG đoán: gán cứng "live" là
127
+ // camera tự lên sóng khi chủ nó chỉ đang trực sân (18/08/2026).
128
+ { autoPublish: result.live },
129
+ )
130
+ .catch(() => {
131
+ // start hỏng (camera chưa sẵn, mạng rớt giữa chừng) cũng phải thử lại —
132
+ // bỏ qua là để phiên sống mồ côi tới khi lưới server dọn.
133
+ onAttemptRef.current?.(resumeId, "start_failed");
134
+ if (!cancelled) scheduleRetry();
135
+ });
136
+ })();
137
+
138
+ return () => {
139
+ cancelled = true;
140
+ if (timer) clearTimeout(timer);
141
+ };
142
+ }, [enabled, provider, resumeId, sessionId, state, retryTick, engine, liveRef, onAttemptRef, resumeInputRef, retryDelayRef]);
143
+
144
+ // Nhận lại hỏng ở bước PUBLISH chứ không phải bước xin grant: native ném vì
145
+ // encoder chưa phát SPS (app vừa mở, camera vừa bật). Engine ĐÃ SỞ HỮU phiên
146
+ // nên gọi start() lần nữa là sai — đường đúng là retry(), dùng lại grant sẵn có.
147
+ useEffect(() => {
148
+ if (!enabled || state !== "error" || sessionId === null) return;
149
+ const timer = setTimeout(() => {
150
+ onAttemptRef.current?.(sessionId, "publish_retry");
151
+ void liveRef.current.retry().catch(() => undefined);
152
+ }, retryDelayRef.current(attemptRef.current++));
153
+ return () => clearTimeout(timer);
154
+ }, [enabled, state, sessionId, liveRef, onAttemptRef, retryDelayRef]);
155
+
156
+ // Thoát hố đen "error" khi CHƯA có session mà còn phiên chờ nhận lại: transition
157
+ // error → preparing hợp lệ, prepare lại để về preview cho vòng nhận lại chạy tiếp.
158
+ useEffect(() => {
159
+ if (!enabled || state !== "error" || sessionId !== null || !adoptableSessionId) return;
160
+ const timer = setTimeout(() => {
161
+ onAttemptRef.current?.(adoptableSessionId, "reprepare");
162
+ void liveRef.current.prepare().catch(() => undefined);
163
+ }, 2_000);
164
+ return () => clearTimeout(timer);
165
+ }, [enabled, state, sessionId, adoptableSessionId, liveRef, onAttemptRef]);
166
+
167
+ useEffect(() => {
168
+ if (state === "live" || state === "standby") attemptRef.current = 0;
169
+ }, [state]);
170
+
171
+ return null;
172
+ }
173
+
174
+ export interface PickleballAutoPrepareProps {
175
+ enabled?: boolean;
176
+ /** Khung cố định (fleet: landscape). Bỏ trống = theo hướng máy như engine mặc định. */
177
+ captureOrientation?: "portrait" | "landscape";
178
+ }
179
+
180
+ /**
181
+ * Camera luôn sẵn: prepare khi idle và SAU KHI phiên kết thúc (thiếu nhánh
182
+ * `ended` là camera tối sau phiên đầu tiên). Không chạy khi đang lỗi — người
183
+ * dùng cần thấy lỗi, và AutoResume lo nhánh lỗi có phiên chờ.
184
+ */
185
+ export function PickleballAutoPrepare(props: PickleballAutoPrepareProps): null {
186
+ const { enabled = true, captureOrientation } = props;
187
+ const live = useLivestream();
188
+ const { state } = live;
189
+ const liveRef = useLatest(live);
190
+ const orientedRef = useRef(false);
191
+
192
+ useEffect(() => {
193
+ if (!enabled || !captureOrientation || orientedRef.current) return;
194
+ orientedRef.current = true;
195
+ void liveRef.current.setCaptureOrientation(captureOrientation).catch(() => undefined);
196
+ }, [enabled, captureOrientation, liveRef]);
197
+
198
+ useEffect(() => {
199
+ if (!enabled) return;
200
+ if (state !== "idle" && state !== "ended") return;
201
+ void liveRef.current.prepare().catch(() => undefined);
202
+ }, [enabled, state, liveRef]);
203
+
204
+ return null;
205
+ }
206
+
207
+ export interface UseStandbySessionOptions {
208
+ /** Lượt start chờ; đổi `externalSessionId` (trận kế) mới arm lại sau khi phiên trước kết thúc. */
209
+ input: StartLivestreamInput;
210
+ enabled?: boolean;
211
+ /**
212
+ * Id phiên còn sống theo nguồn NGOÀI đĩa (fleet: heartbeat; app đối tác: id
213
+ * backend trả về) — cùng giá trị truyền vào `PickleballAutoResume.sessionIdHint`.
214
+ * Có nó là arm NHƯỜNG: không tạo phiên chờ thứ hai trong lúc AutoResume đang
215
+ * nối lại phiên cũ.
216
+ */
217
+ resumeSessionId?: string | null;
218
+ }
219
+
220
+ /**
221
+ * Chế độ SÂN: điện thoại đứng chờ (standby) với grant đã cấp; trọng tài bấm
222
+ * match-start là server gửi lệnh `start` qua kênh lệnh và engine tự phát.
223
+ * Arm đúng một lần cho mỗi externalSessionId, và NHƯỜNG khi còn phiên chờ nhận
224
+ * lại (resumeBlocksArm) để không tạo phiên chờ thứ hai đè phiên cũ.
225
+ */
226
+ export function useStandbySession(options: UseStandbySessionOptions): { armed: boolean } {
227
+ const { input, enabled = true, resumeSessionId = null } = options;
228
+ const live = useLivestream();
229
+ const { state, sessionId, adoptableSessionId } = live;
230
+ const liveRef = useLatest(live);
231
+ const armedForRef = useRef<string | null>(null);
232
+ // Chỉ externalSessionId là "khoá" của lượt arm; phần còn lại của input đọc
233
+ // qua ref để object inline mỗi lần render không kéo effect chạy lại.
234
+ const inputRef = useLatest(input);
235
+ const externalSessionId = input.externalSessionId;
236
+ const armed = sessionId !== null && armedForRef.current === externalSessionId;
237
+
238
+ useEffect(() => {
239
+ if (!enabled) return;
240
+ if (state !== "preview" || sessionId !== null) return;
241
+ if (armedForRef.current === externalSessionId) return;
242
+ if (
243
+ resumeBlocksArm({
244
+ adoptableSessionId,
245
+ serverSessionId: resumeSessionId,
246
+ engineSessionId: sessionId,
247
+ })
248
+ ) {
249
+ return;
250
+ }
251
+ armedForRef.current = externalSessionId;
252
+ void liveRef.current.start(inputRef.current, { autoPublish: false }).catch(() => {
253
+ // Cho phép thử lại ở lượt render sau (VD grant lỗi tạm thời).
254
+ armedForRef.current = null;
255
+ });
256
+ }, [enabled, state, sessionId, adoptableSessionId, resumeSessionId, externalSessionId, liveRef, inputRef]);
257
+
258
+ return { armed };
259
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Điều khiển camera RUNTIME (zoom, EV, khoá nét/sáng, đèn pin, chống chớp) —
3
+ * gọi THẲNG native module @pickleball/rtmp-native (singleton), chủ ý bypass
4
+ * engine/adapter vì đây là tinh chỉnh camera, không phải lifecycle phiên.
5
+ *
6
+ * Mọi hàm `typeof`-guard: binary cũ (rtmp-native 0.1.0 trên fleet 1.0.2) thiếu
7
+ * vài hàm thì trả null/no-op thay vì ném — UI ẩn control tương ứng.
8
+ */
9
+
10
+ declare const require: (name: string) => any;
11
+
12
+ export interface CameraCapabilityRange {
13
+ min: number;
14
+ max: number;
15
+ supported: boolean;
16
+ }
17
+
18
+ export interface CameraCapabilities {
19
+ zoom: CameraCapabilityRange;
20
+ exposureBias: CameraCapabilityRange & { step?: number };
21
+ torch: { supported: boolean };
22
+ focusLock: { supported: boolean };
23
+ exposureLock: { supported: boolean };
24
+ supportedFrameRates?: number[];
25
+ }
26
+
27
+ type NativeControls = {
28
+ getCameraCapabilities?: () => Promise<CameraCapabilities>;
29
+ setZoom?: (factor: number) => Promise<void>;
30
+ setExposureBias?: (ev: number) => Promise<void>;
31
+ setFocusLocked?: (locked: boolean) => Promise<void>;
32
+ setExposureLocked?: (locked: boolean) => Promise<void>;
33
+ setTorch?: (enabled: boolean) => Promise<void>;
34
+ setAntiFlicker?: (mode: "auto" | "50hz" | "60hz") => Promise<void>;
35
+ requestPermissions?: () => Promise<boolean>;
36
+ };
37
+
38
+ function nativeModule(): NativeControls | null {
39
+ try {
40
+ const loaded = require("@pickleball/rtmp-native");
41
+ const candidate = (loaded?.default ?? loaded) as NativeControls | undefined;
42
+ return candidate && typeof candidate === "object" ? candidate : null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ async function callOptional<T>(
49
+ name: keyof NativeControls,
50
+ invoke: (native: NativeControls) => Promise<T> | undefined,
51
+ fallback: T,
52
+ ): Promise<T> {
53
+ const native = nativeModule();
54
+ if (!native || typeof native[name] !== "function") return fallback;
55
+ try {
56
+ const result = await invoke(native);
57
+ return result === undefined ? fallback : result;
58
+ } catch {
59
+ return fallback;
60
+ }
61
+ }
62
+
63
+ /** Bounds + cờ hỗ trợ của lens đang gắn; null khi module/hàm chưa có. */
64
+ export function getCameraCapabilities(): Promise<CameraCapabilities | null> {
65
+ return callOptional("getCameraCapabilities", (n) => n.getCameraCapabilities?.(), null);
66
+ }
67
+
68
+ export function setZoom(factor: number): Promise<void> {
69
+ return callOptional("setZoom", (n) => n.setZoom?.(factor), undefined);
70
+ }
71
+
72
+ export function setExposureBias(ev: number): Promise<void> {
73
+ return callOptional("setExposureBias", (n) => n.setExposureBias?.(ev), undefined);
74
+ }
75
+
76
+ export function setFocusLocked(locked: boolean): Promise<void> {
77
+ return callOptional("setFocusLocked", (n) => n.setFocusLocked?.(locked), undefined);
78
+ }
79
+
80
+ export function setExposureLocked(locked: boolean): Promise<void> {
81
+ return callOptional("setExposureLocked", (n) => n.setExposureLocked?.(locked), undefined);
82
+ }
83
+
84
+ export function setTorch(enabled: boolean): Promise<void> {
85
+ return callOptional("setTorch", (n) => n.setTorch?.(enabled), undefined);
86
+ }
87
+
88
+ /** Android-only: iOS/binary cũ thiếu hàm → no-op. Native tự áp lại mỗi lần mở camera/đổi lens. */
89
+ export function applyAntiFlicker(mode: "auto" | "50hz" | "60hz"): Promise<void> {
90
+ return callOptional("setAntiFlicker", (n) => n.setAntiFlicker?.(mode), undefined);
91
+ }
92
+
93
+ /**
94
+ * Xin quyền camera/mic qua native (iOS, rtmp-native ≥ 0.2). null = native chưa
95
+ * có hàm → caller tự quyết (AVFoundation sẽ hỏi lúc mở camera).
96
+ */
97
+ export function requestNativePermissions(): Promise<boolean | null> {
98
+ return callOptional("requestPermissions", (n) => n.requestPermissions?.(), null);
99
+ }