@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @pickleball/expo-sdk
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 04e5c5d: **1.0.0 — RTMP-only + camera grant (ADR-0002).** Điện thoại của user đối tác lên
8
+ sóng qua đúng pipeline device-rtmp của thiết bị Picklive; LiveKit gỡ hẳn khỏi SDK.
9
+
10
+ BREAKING
11
+ - Gỡ LiveKit: không còn `LiveKitRoomAdapter`, `createNativeLiveKitBindings`,
12
+ `LivestreamScreen` (thay bằng `PickleballCameraScreen` ở `./ui`), peer
13
+ `@livekit/react-native`, `@livekit/react-native-webrtc`, `livekit-client` và
14
+ dependency `@livekit/react-native-expo-plugin`. `createDefaultRuntime` /
15
+ `createDefaultRoomFactory` giờ là RTMP.
16
+ - `@pickleball/rtmp-native` `>=0.2.0` là peer BẮT BUỘC (không còn optional).
17
+ - Grant `serverUrl` phải là `rtmp(s)://` — `createHttpSessionProvider` /
18
+ `isSessionGrant` từ chối `wss://` (INVALID_RESPONSE).
19
+ - Config plugin không compose plugin LiveKit nữa; prop `modules.livekit` và
20
+ `liveKit` vẫn được nhận nhưng bị bỏ qua. Plugin dùng `PLUGIN_SCHEMA_VERSION`
21
+ ("1") thay cho version package trong `createRunOncePlugin` — bump SDK không
22
+ còn đổi fingerprint OTA của app.
23
+ - `withLocalVideoQuality` generic: trả đúng kiểu provider truyền vào (giữ
24
+ `resume`/`getActiveGrant`).
25
+
26
+ MỚI (từ 0.3 additive, gộp vào 1.0)
27
+ - `createCameraGrantProvider` / `useCameraGrant`: grant từ backend đối tác →
28
+ Bearer thẳng `/api/v2/camera/sessions/{id}/*`; tự publish khi start không
29
+ standby; nhận lại phiên sau khi app bị kill (`hasResumableSession`,
30
+ `clearStoredSession`); Bearer hết hạn không bao giờ khoá máy.
31
+ - Resume hạng nhất: `withResume`, `createHttpResumeFetcher`,
32
+ `PickleballAutoResume`, `PickleballAutoPrepare`, `useStandbySession`
33
+ (+ `resumeSessionId`), `resume-policy`.
34
+ - `./ui`: `PickleballCameraScreen` (casual/court, copy vi/en, lỗi bản địa hoá),
35
+ `CameraControlsSheet` (độ phân giải, fps theo capability, chống chớp, camera
36
+ mặc định, zoom/EV/khoá/đèn pin, đo uplink) + primitives/helpers/hooks.
37
+ - `./realtime`: `createGrantCommandSubscription`, `createConvexCommandSubscription`.
38
+ - `createRtmpRuntime`, `createRtmpRoomFactory` ở root; camera-settings store;
39
+ camera-controls (`typeof`-guard); `runUplinkCheck`/`VIDEO_BITRATE_BPS`.
40
+ - Engine: poll lệnh 409 → dừng tại chỗ; không xoay token mỗi giây ở phút cuối;
41
+ hành động của `useLivestream` ổn định qua re-render.
42
+ - `SdkError.reason` (409: superseded|ended|expired) và `retryAfterMs` (429).
43
+
3
44
  ## 0.2.0
4
45
 
5
46
  ### Minor Changes (BREAKING — modular subpath split)
package/README.md CHANGED
@@ -1,144 +1,198 @@
1
1
  # `@pickleball/expo-sdk`
2
2
 
3
- Foreground livestreaming for Expo iOS and Android, with a headless state-machine API and a ready-made recording screen. Every successful session uses server-side recording.
3
+ Livestream a pickleball match from the phone camera, straight into Pickleball
4
+ Live's RTMP pipeline (hardware H.264 → RTMP → HLS/VOD on R2), with a headless
5
+ state machine, React hooks, a ready-made camera screen, and an Expo config
6
+ plugin. **1.0 is RTMP-only**: LiveKit/WebRTC is gone, the only native
7
+ dependency is `@pickleball/rtmp-native`.
4
8
 
5
- ## Modules (v0.2 subpath split)
9
+ ## How a partner app goes live (camera grant)
6
10
 
7
- The root entry is the lean livestream core. Every other feature lives on its own
8
- subpath **Metro does not tree-shake, so only import the subpaths your app
9
- actually uses**:
11
+ ```
12
+ Partner app (this SDK) Partner backend (holds the API key) Pickleball Live
13
+ ────────────────────── ─────────────────────────────────── ───────────────
14
+ user taps "Go live" ──────────► POST /api/v1/live-sessions
15
+ {cameraGrant:true, matchRef, title,
16
+ quality?, courtRef?} ── x-api-key ────► creates the session
17
+ ◄──── CameraGrant (verbatim) ─── ◄──── {session, cameraGrant} ─────────────
18
+ SDK: Bearer grant.telemetry.token ──────────────────────────────────────► /api/v2/camera/sessions/{id}/
19
+ publish|unpublish|refresh|end|
20
+ telemetry|commands
21
+ SDK: RTMP (grant.serverUrl + stream key) ───────────────────────────────► MediaMTX → HLS → R2 → VOD
22
+ ◄──── webhooks session.live / session.ended / recording.ready (matchRef)
23
+ ```
24
+
25
+ The app never holds an API key and never configures a Pickleball URL: every
26
+ endpoint is derived from the grant. The partner backend has exactly one job —
27
+ issue grants (`@pickleball/server-sdk` `liveSessions.createCameraSession`).
28
+
29
+ ## Modules (Metro does not tree-shake — import only what you use)
10
30
 
11
31
  | Import | Contents | Extra runtime deps |
12
32
  |---|---|---|
13
- | `@pickleball/expo-sdk` | `PickleballLiveProvider`, `useLivestream`, `LivestreamScreen`, `createHttpSessionProvider`, `createDefaultStorage`, contracts, errors | LiveKit (core) |
14
- | `@pickleball/expo-sdk/match` | Match scoreboard: `PickleballMatchProvider`, `usePickleballMatch`, scoring engine, SQLite persistence | `expo-sqlite` |
33
+ | `@pickleball/expo-sdk` | `PickleballLiveProvider`, `useLivestream`, `createCameraGrantProvider`, resume (`PickleballAutoResume`, `useStandbySession`, `withResume`), camera settings/controls, `runUplinkCheck`, `createRtmpRuntime`, contracts, errors | `@pickleball/rtmp-native` (peer) |
34
+ | `@pickleball/expo-sdk/ui` | `PickleballCameraScreen`, `CameraControlsSheet`, primitives, `statusPillFor`, copy vi/en | `react-native-safe-area-context` |
35
+ | `@pickleball/expo-sdk/realtime` | `createGrantCommandSubscription` — commands over a Convex websocket instead of 2 s polling | `convex` |
36
+ | `@pickleball/expo-sdk/match` | Match scoreboard: `PickleballMatchProvider`, `usePickleballMatch`, SQLite persistence | `expo-sqlite` |
15
37
  | `@pickleball/expo-sdk/match/voice` | Offline score announcer (VI/EN) + TTS | `@pickleball/voice-native`, `expo-audio` |
16
38
  | `@pickleball/expo-sdk/match/remote` | BLE/HID court remote input | `@pickleball/remote-native`, Bluetooth permissions |
17
- | `@pickleball/expo-sdk/device-agent` | Device registry + dashboard remote control (`PickleballDeviceAgent`) | `convex` (bundled only via this subpath) |
18
- | `@pickleball/expo-sdk/advanced` | `LivestreamEngine`, DI interfaces (`RoomAdapter`, `TelemetrySink`…), default runtime factories, `LiveKitRoomAdapter` — for mocks/tests/custom wiring | — |
39
+ | `@pickleball/expo-sdk/device-agent` | Device registry + dashboard remote control (Pickleball's own fleet app) | `convex` |
40
+ | `@pickleball/expo-sdk/advanced` | `LivestreamEngine`, DI interfaces, `RtmpRoomAdapter`, grant validators — for mocks/tests/custom wiring | — |
19
41
 
20
- Migration from 0.1: symbols that used to come from the root barrel now import
21
- from the matching subpath above; the provider's `deviceAgent` prop was replaced
22
- by mounting `<PickleballDeviceAgent/>` (from `/device-agent`) inside the
23
- provider.
42
+ ## Install
24
43
 
25
- ## Native requirements
26
-
27
- This package requires an Expo Development Build. Expo Go and web are intentionally unsupported because LiveKit needs native WebRTC modules. Supported Expo SDK range: `>=56 <58`.
44
+ ```bash
45
+ npx expo install @pickleball/expo-sdk @pickleball/rtmp-native \
46
+ @react-native-community/netinfo expo-file-system expo-keep-awake expo-sqlite \
47
+ react-native-safe-area-context
48
+ ```
28
49
 
29
- Install the peer dependencies with the versions compatible with your Expo SDK, then add the config plugin and rebuild the native app:
50
+ Add the config plugin and rebuild the native app (Expo Development Build / EAS
51
+ Expo Go and web cannot run the camera module):
30
52
 
31
53
  ```json
32
54
  {
33
55
  "expo": {
34
56
  "plugins": [
35
- [
36
- "@pickleball/expo-sdk",
37
- {
38
- "cameraPermission": "Allow this app to livestream matches.",
39
- "microphonePermission": "Allow this app to capture match audio."
40
- }
41
- ]
57
+ ["@pickleball/expo-sdk", {
58
+ "cameraPermission": "Allow this app to livestream matches.",
59
+ "microphonePermission": "Allow this app to capture match audio.",
60
+ "modules": { "remote": false }
61
+ }]
42
62
  ]
43
63
  }
44
64
  }
45
65
  ```
46
66
 
47
- The plugin composes the official LiveKit Expo plugin and declares the WebRTC
48
- permissions required by LiveKit through Expo CNG; do not edit native projects
49
- manually. The permission set follows the native requirements documented by the
50
- MIT-licensed LiveKit React Native/WebRTC projects; this package does not bundle
51
- the community `@config-plugins/react-native-webrtc` plugin.
52
-
53
- ### Slimming the native footprint
54
-
55
- If the app never uses the BLE/HID court remote, disable its native config so
56
- the build drops the Bluetooth permissions, the iOS Bluetooth purpose string,
57
- and the MainActivity key-event patch:
58
-
59
- ```json
60
- ["@pickleball/expo-sdk", { "modules": { "remote": false } }]
61
- ```
62
-
63
- `modules.voice` is accepted for forward compatibility (microphone/audio
64
- permissions belong to livestreaming, so it currently gates nothing).
67
+ `modules.remote: false` drops the Bluetooth permissions and the MainActivity
68
+ key-event patch when the app does not use the court remote. `modules.livekit`
69
+ and `liveKit` from 0.x are still accepted and ignored. To keep the voice/remote
70
+ native modules out of the binary entirely, add them to `expo.autolinking.exclude`
71
+ in the app's `package.json`.
65
72
 
66
- To strip the unused native modules from the binary entirely, exclude them from
67
- autolinking in the **app's** `package.json` — safe only if the app never
68
- activates voice playback or remote scanning at runtime:
69
-
70
- ```json
71
- {
72
- "expo": {
73
- "autolinking": {
74
- "exclude": ["@pickleball/voice-native", "@pickleball/remote-native"]
75
- }
76
- }
77
- }
78
- ```
79
-
80
- ## Usage
73
+ ## Quick start (casual: the user goes live from the phone)
81
74
 
82
75
  ```tsx
83
76
  import {
84
- LivestreamScreen,
85
77
  PickleballLiveProvider,
86
- createHttpSessionProvider,
78
+ PickleballAutoResume,
79
+ createCameraGrantProvider,
80
+ createCameraSettingsStore,
81
+ createRtmpRuntime,
82
+ cameraSettingsKey,
83
+ rtmpRoomFactoryFor,
84
+ useCameraSettings,
85
+ useLivestream,
86
+ withLocalVideoQuality,
87
87
  } from "@pickleball/expo-sdk";
88
-
89
- const sessionProvider = createHttpSessionProvider({
90
- baseUrl: "https://partner.example.com/mobile/live",
91
- headers: async () => ({ Authorization: `Bearer ${await getUserToken()}` }),
88
+ import { PickleballCameraScreen } from "@pickleball/expo-sdk/ui";
89
+ import { createGrantCommandSubscription } from "@pickleball/expo-sdk/realtime";
90
+
91
+ const settings = createCameraSettingsStore(); // resolution/fps/camera, persisted
92
+ const grants = createCameraGrantProvider({
93
+ // Your backend: authenticates the user, calls Pickleball REST v1 with the
94
+ // API key, and returns the CameraGrant verbatim.
95
+ requestGrant: async (input) => {
96
+ const res = await fetch(`${API}/matches`, {
97
+ method: "POST",
98
+ headers: { Authorization: `Bearer ${await userToken()}`, "Content-Type": "application/json" },
99
+ body: JSON.stringify({ title: input.title, quality: input.quality }),
100
+ });
101
+ return (await res.json()).data.grant;
102
+ },
92
103
  });
104
+ const sessionProvider = withLocalVideoQuality(grants, () => settings.get().resolution);
93
105
 
94
- export function MatchLive() {
106
+ export function LiveRoot({ children }) {
107
+ const config = useCameraSettings(settings);
95
108
  return (
96
109
  <PickleballLiveProvider
110
+ key={cameraSettingsKey(config)} // remount → re-prepare on resolution/fps change
97
111
  sessionProvider={sessionProvider}
98
- bootstrapInput={{
99
- sdkVersion: "0.1.0",
100
- platform: "ios",
101
- bundleId: "com.example.app",
102
- installationId: "your-existing-installation-id",
103
- }}
112
+ bootstrapInput={identity} // sdkVersion, platform, bundleId, installationId
113
+ runtime={createRtmpRuntime()}
114
+ roomFactory={rtmpRoomFactoryFor(config)}
115
+ keepSessionOnRestart={() => grants.hasResumableSession()}
116
+ commandSubscription={createGrantCommandSubscription(() => grants.getActiveGrant()?.convexUrl)}
117
+ locale="vi"
104
118
  >
105
- <LivestreamScreen
106
- input={{
107
- externalSessionId: "match-123",
108
- title: "Court 1",
109
- consentVersion: "2026-01",
110
- }}
111
- />
119
+ <PickleballAutoResume provider={grants} resumeInput={{ consentVersion: "2026-09" }} />
120
+ {children}
112
121
  </PickleballLiveProvider>
113
122
  );
114
123
  }
115
- ```
116
-
117
- `createHttpSessionProvider` talks only to the partner-owned proxy routes `/bootstrap`, `/sessions`, `/sessions/:id/refresh`, and `/sessions/:id/end`. It has no API-key option. Keep the Pickleball API key in the backend.
118
- Production proxy URLs must use HTTPS; plain HTTP is accepted only for loopback development URLs such as `http://localhost:3000`.
119
-
120
- Tokens stay in memory. The only persisted value is a bounded `{sessionId, pendingEnd}` record used to finish a session after connectivity returns. Remote policy can change data and flags, but native changes require a semver release and a rebuilt app.
121
124
 
122
- For custom UI, use `useLivestream()` and its `prepare`, `start`, `stop`, `retry`, `switchCamera`, and `setMicrophoneEnabled` actions. `LivestreamScreen` supports Vietnamese/English copy, theme tokens, and preview/header/controls/error render overrides.
125
+ export function CameraRoute({ matchId, title }) {
126
+ const live = useLivestream();
127
+ return (
128
+ <PickleballCameraScreen
129
+ settings={settings}
130
+ title={title}
131
+ onGoLive={() =>
132
+ live.start(
133
+ { externalSessionId: `match-${matchId}`, title, consentVersion: "2026-09", quality: settings.get().resolution },
134
+ { autoPublish: true },
135
+ )
136
+ }
137
+ />
138
+ );
139
+ }
140
+ ```
123
141
 
124
- ## Optional: dashboard remote control
142
+ What happens: `start()` asks your backend for a grant, publishes on the server
143
+ (`/publish`), connects RTMP and goes live. The screen shows the status pill,
144
+ lens row, a two-step Stop, a "•••" sheet with resolution/fps/camera/zoom/EV/
145
+ locks/torch/upload-speed check, and dims itself after 30 s live to save heat.
125
146
 
126
- Opt in to device registry + remote start/stop from the operator dashboard by
127
- mounting `PickleballDeviceAgent` inside the provider (this is the only place
128
- that pulls the `convex` client into the bundle):
147
+ ### Court mode (the phone replaces a fixed court camera)
129
148
 
130
149
  ```tsx
131
- import {
132
- PickleballDeviceAgent,
133
- createHttpDeviceAgentTransport,
134
- } from "@pickleball/expo-sdk/device-agent";
135
-
136
- <PickleballLiveProvider sessionProvider={sessionProvider} bootstrapInput={identity}>
137
- <PickleballDeviceAgent
138
- transport={createHttpDeviceAgentTransport({ baseUrl: proxyUrl, headers })}
139
- remoteStart={{ consentVersion: "2026-01", defaultTitle: "Court 1" }}
140
- onRemoteSessionStarted={(sessionId) => router.push("/livestream")}
141
- />
142
- {children}
143
- </PickleballLiveProvider>
150
+ const { armed } = useStandbySession({
151
+ input: { externalSessionId: `court-${courtRef}-${n}`, title: `${courtRef} — chờ trận`,
152
+ consentVersion: "2026-09", metadata: { courtRef } },
153
+ resumeSessionId: hintFromYourBackend ?? null,
154
+ });
155
+ <PickleballCameraScreen mode="court" settings={settings} courtLabel={courtRef} />
144
156
  ```
157
+
158
+ Your backend forwards `courtRef` when it creates the session; the phone waits in
159
+ standby (12 h preset) and the referee's `POST /api/v1/courts/{courtRef}/match-start`
160
+ flips it live through the command channel; `match-end` ends it. Only one camera
161
+ can hold a court — a fleet device already assigned there returns
162
+ `COURT_HAS_DEVICE`.
163
+
164
+ ### Resume after the app is killed
165
+
166
+ The provider stores `{sessionId, base, Bearer}` in the app sandbox (expo-sqlite
167
+ localStorage by default — pass `storage` to use `expo-secure-store`). On the next
168
+ launch `PickleballAutoResume` refreshes the grant and re-joins the same session
169
+ (same watch link, one VOD); a session the server no longer holds is abandoned
170
+ cleanly. Call `grants.clearStoredSession()` on logout and end the session from
171
+ your backend.
172
+
173
+ ## Behaviour to know
174
+
175
+ - `standby` is free: no recording until `/publish`. Casual sessions end after
176
+ 15 min of standby, 90 s in background, 3 h max; court-bound sessions get
177
+ 12 h / 5 min / 6 h.
178
+ - A re-issued grant (your backend calls `/camera-grant` again) supersedes the
179
+ previous one: the old phone gets `409` with `reason: "superseded"` on its next
180
+ poll and stops at once. One grant = one phone.
181
+ - Errors surface as `PickleballExpoError` with `code` (`PERMISSION_DENIED`,
182
+ `NETWORK_UNAVAILABLE`, `TOKEN_EXPIRED`, `SESSION_CONFLICT`, …), `httpStatus`,
183
+ and for `SESSION_CONFLICT` a `reason` (`superseded | ended | expired`).
184
+ - Tokens live only in the grant; the SDK never sees an API key. Keep the key
185
+ in your backend.
186
+
187
+ ## Migration 0.2 → 1.0
188
+
189
+ | 0.2 | 1.0 |
190
+ |---|---|
191
+ | `LivestreamScreen` (root) | `PickleballCameraScreen` from `@pickleball/expo-sdk/ui` |
192
+ | `createHttpSessionProvider` + partner proxy (6 routes, authz gateway) | `createCameraGrantProvider` + one backend endpoint that returns a grant |
193
+ | `createDefaultRuntime` (LiveKit) | `createRtmpRuntime` (`createDefaultRuntime` is now an alias) |
194
+ | `createRtmpRoomFactory` from `/advanced` | root export |
195
+ | LiveKit peers + `modules.livekit` | removed; `@pickleball/rtmp-native >=0.2.0` required |
196
+
197
+ `createHttpSessionProvider` still exists for first-party namespaces, but only
198
+ accepts `rtmp(s)://` grants.
@@ -0,0 +1,300 @@
1
+ import { R as RoomAdapter, a1 as RoomConnection, e as CameraDeviceInfo, g as CaptureOrientation, a2 as RoomEvent, r as SdkBootstrap, C as CameraGrant, b as SdkSessionGrant, y as SdkSessionRefresh, z as SdkTelemetryCredentials, a3 as EngineDependencies, w as SdkNetwork, x as SdkRuntime, a as SdkStorage, T as TelemetrySink, a0 as LivestreamEngine, B as BootstrapInput, m as LivestreamSnapshot, c as StartLivestreamInput, i as EndReason, L as LivestreamSessionProvider, H as SessionCommandChannel, S as SessionCommandSubscription, o as PickleballExpoError } from './engine-BSvSdCSE.js';
2
+ import React, { ReactNode } from 'react';
3
+
4
+ /**
5
+ * Trạng thái từ tầng RTMP native (trùng NativeRtmpStatus của @pickleball/rtmp-native).
6
+ */
7
+ type RtmpStatus = {
8
+ readonly status: "idle" | "connecting" | "connected" | "publishing";
9
+ } | {
10
+ readonly status: "disconnected";
11
+ readonly reason?: string;
12
+ } | {
13
+ readonly status: "error";
14
+ readonly code: string;
15
+ readonly message: string;
16
+ };
17
+ /** Phản hồi băng thông từ native (trùng NativeRtmpBandwidth của rtmp-native). */
18
+ interface RtmpBandwidth {
19
+ /** false = encoder đang đẩy nhanh hơn đường truyền chịu được. */
20
+ readonly sufficient: boolean;
21
+ readonly videoBitrate: number;
22
+ }
23
+ /**
24
+ * Dấu hiệu để UI biết render nguồn nào. LiveKit trả về một track object; RTMP
25
+ * không có "track" ở JS (khung hình đi thẳng trong native), nên trả marker này
26
+ * — nếu trả undefined thì UI tưởng chưa có preview và vẽ màn TỐI ĐEN.
27
+ */
28
+ interface RtmpPreviewMarker {
29
+ readonly kind: "rtmp";
30
+ }
31
+ /** Bề mặt native mà adapter cần — tiêm vào để test được không cần thiết bị thật. */
32
+ interface NativeRtmpBindings {
33
+ prepare(options: {
34
+ quality: 720 | 1080;
35
+ landscape: boolean;
36
+ /** fps mong muốn (24 tiết kiệm băng thông, 30 mặc định) — thiếu = native tự chọn. */
37
+ frameRate?: 24 | 30 | 60;
38
+ /**
39
+ * Camera mở ngay từ đầu. Thiếu = native dùng camera sau mặc định. Đặt ở đây
40
+ * thay vì gọi selectCamera sau prepare để không phải mở camera hai lần.
41
+ */
42
+ cameraId?: string;
43
+ }): Promise<void>;
44
+ /**
45
+ * Xin quyền camera/mic bằng API quyền thuần (PermissionsAndroid, ...).
46
+ * Không có thì engine rơi về getUserMedia của WebRTC — thứ mở camera thật
47
+ * và đụng HaishinKit trên máy Android chỉ cho một camera mở cùng lúc.
48
+ */
49
+ requestPermissions?(): Promise<boolean>;
50
+ /** Hướng cầm máy hiện tại — cùng nguồn mà LiveKitRoomAdapter dùng. */
51
+ isLandscape?(): boolean;
52
+ connect(options: {
53
+ url: string;
54
+ videoBitrate: number;
55
+ publish: boolean;
56
+ }): Promise<void>;
57
+ setPublishing(enabled: boolean): Promise<void>;
58
+ disconnect(): Promise<void>;
59
+ switchCamera(): Promise<void>;
60
+ listCameras(): Array<{
61
+ deviceId: string;
62
+ label: string;
63
+ facing: string;
64
+ }>;
65
+ selectCamera(deviceId: string, facing: string): Promise<void>;
66
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
67
+ setVideoBitrate(bitsPerSecond: number): Promise<void>;
68
+ dispose(): Promise<void>;
69
+ addStatusListener(listener: (event: RtmpStatus) => void): {
70
+ remove(): void;
71
+ };
72
+ /** Optional: bản native cũ chưa phát onBandwidth thì adapter bỏ qua thích ứng. */
73
+ addBandwidthListener?(listener: (event: RtmpBandwidth) => void): {
74
+ remove(): void;
75
+ };
76
+ }
77
+ /**
78
+ * Adapter cho mode deliveryMode="device-rtmp".
79
+ *
80
+ * Thiết bị mã hoá H.264 bằng phần cứng MỘT lần rồi đẩy thẳng RTMP; server chỉ
81
+ * remux sang HLS. Không có phòng LiveKit, không egress → $0 transcode.
82
+ *
83
+ * Khác LiveKitRoomAdapter ở một điểm quan trọng: RTMP **không có kênh dữ liệu**,
84
+ * nên adapter này không bao giờ phát RoomEvent "command". Lệnh điều khiển từ
85
+ * dashboard đi đường khác (deviceControl trên Convex) — xem device-agent.
86
+ */
87
+ declare class RtmpRoomAdapter implements RoomAdapter {
88
+ private readonly bindings;
89
+ private readonly options;
90
+ /**
91
+ * RTMP không tự hồi: native chỉ phát hiện đứt (watchdog iOS / onDisconnect
92
+ * Android) và báo "disconnected" — không tự quay số lại. Engine thấy cờ này
93
+ * là chạy vòng redial backoff. Thiếu cờ: phiên ngồi chờ "reconnected" không
94
+ * bao giờ tới rồi chết network_timeout sau 30s (prod 17/08/2026).
95
+ */
96
+ readonly needsManualReconnect = true;
97
+ private readonly listeners;
98
+ private nativeSubscription?;
99
+ private bandwidthSubscription?;
100
+ private quality;
101
+ private connected;
102
+ private prepared;
103
+ /** Khung người dùng chọn trên UI (9:16/16:9) — ưu tiên hơn hướng cầm máy. */
104
+ private captureOrientation?;
105
+ private bitrateState?;
106
+ /**
107
+ * Chỉ tồn tại khi bindings hỗ trợ — engine kiểm tra truthiness để quyết định
108
+ * fallback về runtime.requestPermissions() (getUserMedia), nên KHÔNG được
109
+ * khai báo method luôn-có rồi tự fallback bên trong.
110
+ */
111
+ readonly requestPermissions?: () => Promise<boolean>;
112
+ constructor(bindings: NativeRtmpBindings, options?: {
113
+ frameRate?: 24 | 30 | 60;
114
+ cameraId?: string;
115
+ });
116
+ /**
117
+ * Phần tuỳ chọn của prepare(). Giữ dạng spread có điều kiện: truyền
118
+ * `frameRate: undefined` xuống Expo Record là ghi đè mặc định của native
119
+ * bằng null chứ không phải "bỏ qua trường".
120
+ */
121
+ private prepareOverrides;
122
+ preparePreview(quality: 720 | 1080): Promise<RtmpPreviewMarker>;
123
+ connect(input: RoomConnection): Promise<void>;
124
+ setPublishing(enabled: boolean): Promise<void>;
125
+ disconnect(_stopTracks?: boolean): Promise<void>;
126
+ switchCamera(): Promise<void>;
127
+ /** Ống kính vật lý (0.5x siêu rộng · 1x · tele) — không có thì UI kẹt ở mặc định. */
128
+ listCameras(): Promise<CameraDeviceInfo[]>;
129
+ selectCamera(deviceId: string, facing: CameraDeviceInfo["facing"]): Promise<void>;
130
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
131
+ /** Hạ chất lượng khi mạng yếu — thay cho BWE mà WebRTC cho sẵn. */
132
+ setVideoBitrate(bitsPerSecond: number): Promise<void>;
133
+ setCaptureOrientation(orientation: CaptureOrientation): Promise<void>;
134
+ subscribe(listener: (event: RoomEvent) => void): () => void;
135
+ dispose(): Promise<void>;
136
+ private handleBandwidth;
137
+ private emit;
138
+ }
139
+
140
+ /**
141
+ * Validator + helper HTTP dùng chung cho hai session provider:
142
+ * - createHttpSessionProvider (proxy đối tác / first-party device-app)
143
+ * - createCameraGrantProvider (điện thoại đối tác, grant từ backend đối tác)
144
+ *
145
+ * Tách ra để hai provider không chép luật "grant thế nào là hợp lệ" ra hai nơi
146
+ * rồi lệch nhau; engine chỉ tin `isSdkRemoteConfig`, còn hình dạng grant là
147
+ * việc của lớp này.
148
+ */
149
+ type Fetch = typeof globalThis.fetch;
150
+ declare function isBootstrap(value: unknown): value is SdkBootstrap;
151
+ declare function isTelemetryCredentials(value: unknown): value is SdkTelemetryCredentials;
152
+ /**
153
+ * Grant phiên. SDK 1.0 là RTMP-only: `serverUrl` phải là rtmp(s):// (ingest).
154
+ * Grant LiveKit (wss://) của mô hình cũ bị từ chối ngay ở đây — lỗi
155
+ * INVALID_RESPONSE rõ ràng thay vì HaishinKit ném lúc connect.
156
+ */
157
+ declare function isSessionGrant(value: unknown): value is SdkSessionGrant;
158
+ /**
159
+ * Camera grant (điện thoại đối tác): superset của SdkSessionGrant, BẮT BUỘC là
160
+ * RTMP và mang `status` để đường nhận lại biết phiên cũ đang phát hay đang chờ.
161
+ */
162
+ declare function isCameraGrant(value: unknown): value is CameraGrant;
163
+ declare function isSessionRefresh(value: unknown): value is SdkSessionRefresh;
164
+ /**
165
+ * Gốc API của phiên suy từ endpoint telemetry trong grant: `…/sessions/{id}/telemetry`
166
+ * → `…/sessions/{id}`. Đây là cách duy nhất SDK biết namespace (sdk / camera /
167
+ * device-app) mà không cần app cấu hình URL PickLive nào.
168
+ */
169
+ declare function sessionApiBaseFromTelemetryEndpoint(endpoint: string): string;
170
+
171
+ /**
172
+ * Runtime cho app RTMP-only (KHÔNG có LiveKit/WebRTC): assert bằng module
173
+ * @pickleball/rtmp-native, xin quyền bằng API quyền thuần (Android) hoặc hàm
174
+ * native `requestPermissions` (iOS, rtmp-native ≥ 0.2; thiếu thì AVFoundation
175
+ * hỏi lúc mở camera). Chuyển từ apps/device-streamer (createRtmpOnlyRuntime).
176
+ * Không dùng getUserMedia: máy MediaTek chỉ cho một camera mở cùng lúc.
177
+ */
178
+ declare function createRtmpRuntime(): SdkRuntime;
179
+ /** Runtime mặc định của SDK 1.0 = RTMP (LiveKit đã gỡ khỏi SDK — ADR-0002). */
180
+ declare const createDefaultRuntime: typeof createRtmpRuntime;
181
+ declare function createDefaultStorage(): SdkStorage;
182
+ declare function createDefaultNetwork(): SdkNetwork;
183
+ declare function createDefaultTelemetry(fetchImplementation?: typeof fetch, timeoutMs?: number): TelemetrySink;
184
+ /** roomFactory mặc định của SDK 1.0 = RTMP (fps/camera mặc định của native). */
185
+ declare function createDefaultRoomFactory(): EngineDependencies["roomFactory"];
186
+ /**
187
+ * Bindings native RTMP. `require` động: app chưa cài @pickleball/rtmp-native
188
+ * chỉ lỗi khi thật sự mở camera (UnsupportedRuntimeError rõ ràng), không gãy
189
+ * lúc import SDK — static-render web của expo-router vẫn nạp được module.
190
+ */
191
+ declare function createNativeRtmpBindings(): NativeRtmpBindings;
192
+ /** roomFactory cho mode device-rtmp (thiết bị đẩy RTMP, không dùng LiveKit). */
193
+ declare function createRtmpRoomFactory(options?: {
194
+ /**
195
+ * fps capture + encode (24 tiết kiệm băng thông, 30 mặc định của native).
196
+ * 60 chỉ nên truyền khi `getCameraCapabilities().supportedFrameRates` có nó —
197
+ * native bỏ qua trong im lặng nếu ống kính đang gắn không chạy được.
198
+ */
199
+ frameRate?: 24 | 30 | 60;
200
+ /** Camera mở sẵn lúc prepare — thiếu = camera sau mặc định của native. */
201
+ cameraId?: string;
202
+ }): EngineDependencies["roomFactory"];
203
+ /**
204
+ * Thu chỉ số thiết bị cho heartbeat. Mỗi nguồn là optional — thiếu package
205
+ * (expo-battery/expo-device/expo-file-system) thì bỏ qua trường đó thay vì lỗi.
206
+ */
207
+ declare function createDefaultDeviceMetrics(): NonNullable<EngineDependencies["deviceMetrics"]>;
208
+
209
+ interface LivestreamTheme {
210
+ background: string;
211
+ foreground: string;
212
+ accent: string;
213
+ danger: string;
214
+ /** Chữ phụ mờ nhất (labels, chú thích). */
215
+ muted: string;
216
+ overlay: string;
217
+ radius: number;
218
+ /** Token FintechX — đồng bộ với dashboard web. */
219
+ surface: string;
220
+ elevated: string;
221
+ hairline: string;
222
+ /** Màu chữ đặt trên nền accent. */
223
+ ink: string;
224
+ textSecondary: string;
225
+ radiusSm: number;
226
+ fontFamily?: string;
227
+ fontFamilyBold?: string;
228
+ displayFontFamily?: string;
229
+ }
230
+ /** FintechX design tokens — trùng bộ token trong apps/web/globals.css. */
231
+ declare const defaultLivestreamTheme: LivestreamTheme;
232
+ interface PickleballLiveProviderProps {
233
+ children?: ReactNode;
234
+ sessionProvider: LivestreamSessionProvider;
235
+ bootstrapInput: BootstrapInput;
236
+ /**
237
+ * Phiên phải SỐNG TIẾP khi app biến mất giữa chừng (sụp nguồn, bị vuốt tắt,
238
+ * component remount), để lần mở sau nhận lại đúng phiên cũ — giữ nguyên link
239
+ * xem và VOD liền một mạch.
240
+ *
241
+ * Mặc định false = hành vi cũ: mở app lại là dọn sạch phiên cũ. App gác sân bật
242
+ * cờ này khi máy đang được gán sân; máy không gán sân vẫn dọn như cũ để không
243
+ * để lại phiên treo trong các ca ngoài giải.
244
+ *
245
+ * Nhận boolean HOẶC hàm (được phép async). App nên truyền HÀM ĐỌC THẲNG ĐĨA:
246
+ * engine hỏi cờ này đúng lúc khởi động, thường TRƯỚC khi state React kịp
247
+ * restore — truyền boolean suy từ state là dính race keep=false oan (đã dính
248
+ * thật 18/08/2026, sinh phiên chờ thứ hai đè phiên live cũ).
249
+ */
250
+ keepSessionOnRestart?: boolean | (() => boolean | Promise<boolean>);
251
+ runtime?: SdkRuntime;
252
+ storage?: SdkStorage;
253
+ network?: SdkNetwork;
254
+ telemetry?: TelemetrySink;
255
+ /** Kênh poll lệnh cho mode device-rtmp — mặc định suy từ telemetry endpoint. */
256
+ commandChannel?: SessionCommandChannel;
257
+ /**
258
+ * Kênh nhận lệnh qua websocket (first-party nối thẳng Convex). Có thì lệnh về
259
+ * tức thì và poll tụt về nhịp reconcile chậm — cắt phần lớn function call của
260
+ * đường poll. Thiếu (máy đối tác qua proxy) thì chỉ dùng poll HTTP như cũ.
261
+ */
262
+ commandSubscription?: SessionCommandSubscription;
263
+ roomFactory?: () => RoomAdapter;
264
+ deviceMetrics?: EngineDependencies["deviceMetrics"];
265
+ theme?: Partial<LivestreamTheme>;
266
+ locale?: "vi" | "en";
267
+ onError?: (error: PickleballExpoError) => void;
268
+ }
269
+ /**
270
+ * Context nội bộ cho các module opt-in (VD PickleballDeviceAgent) cần truy cập
271
+ * engine trực tiếp — export qua "./advanced", không thuộc API tích hợp cơ bản.
272
+ */
273
+ interface LivestreamHostContextValue {
274
+ engine: LivestreamEngine;
275
+ bootstrapInput: BootstrapInput;
276
+ deviceMetrics: NonNullable<EngineDependencies["deviceMetrics"]>;
277
+ }
278
+ declare function useLivestreamHost(): LivestreamHostContextValue;
279
+ interface LivestreamContextValue extends LivestreamSnapshot {
280
+ locale: "vi" | "en";
281
+ theme: LivestreamTheme;
282
+ prepare(): Promise<void>;
283
+ start(input: StartLivestreamInput, options?: {
284
+ autoPublish?: boolean;
285
+ }): Promise<void>;
286
+ /** Bắt đầu phát khi đang standby. */
287
+ startPublishing(): Promise<void>;
288
+ /** Tạm dừng phát về standby (không kết thúc phiên). */
289
+ stopPublishing(): Promise<void>;
290
+ stop(reason?: EndReason): Promise<void>;
291
+ retry(): Promise<void>;
292
+ switchCamera(): Promise<void>;
293
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
294
+ setCaptureOrientation(orientation: "portrait" | "landscape"): Promise<void>;
295
+ selectCamera(deviceId: string): Promise<void>;
296
+ }
297
+ declare function PickleballLiveProvider(props: PickleballLiveProviderProps): React.JSX.Element;
298
+ declare function useLivestream(): LivestreamContextValue;
299
+
300
+ export { type Fetch as F, type LivestreamContextValue as L, type NativeRtmpBindings as N, PickleballLiveProvider as P, RtmpRoomAdapter as R, type LivestreamTheme as a, type PickleballLiveProviderProps as b, createDefaultStorage as c, createRtmpRoomFactory as d, createRtmpRuntime as e, defaultLivestreamTheme as f, type LivestreamHostContextValue as g, type RtmpStatus as h, createDefaultDeviceMetrics as i, createDefaultNetwork as j, createDefaultRoomFactory as k, createDefaultRuntime as l, createDefaultTelemetry as m, createNativeRtmpBindings as n, isBootstrap as o, isCameraGrant as p, isSessionGrant as q, isSessionRefresh as r, isTelemetryCredentials as s, sessionApiBaseFromTelemetryEndpoint as t, useLivestream as u, useLivestreamHost as v };