@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.
- package/CHANGELOG.md +41 -0
- package/README.md +155 -101
- package/dist/advanced-DcbHBCGb.d.ts +300 -0
- package/dist/advanced-DeQwg7MK.d.cts +300 -0
- package/dist/advanced.cjs +14 -2
- package/dist/advanced.d.cts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.js +17 -5
- package/dist/camera-controls-By8tHigY.d.cts +97 -0
- package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
- package/dist/chunk-62YV3PCD.cjs +139 -0
- package/dist/chunk-6F3HN6US.js +50 -0
- package/dist/chunk-6YE5BL3A.cjs +50 -0
- package/dist/chunk-7Z7TN5N3.js +318 -0
- package/dist/{chunk-TRLASNPQ.cjs → chunk-GXQYMPHE.cjs} +1 -1
- package/dist/{chunk-TBPR2QND.cjs → chunk-IUIRVKJI.cjs} +736 -399
- package/dist/chunk-TBUEXRFQ.cjs +318 -0
- package/dist/{chunk-FODE2W2H.js → chunk-XGP2MN5R.js} +688 -351
- package/dist/{chunk-6JOMESNT.js → chunk-XL4CISVQ.js} +1 -1
- package/dist/chunk-ZSKSXOCZ.js +139 -0
- package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
- package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
- package/dist/device-agent/index.cjs +973 -44
- package/dist/device-agent/index.d.cts +339 -3
- package/dist/device-agent/index.d.ts +339 -3
- package/dist/device-agent/index.js +940 -11
- package/dist/{engine-CCSSqPbZ.d.cts → engine-BSvSdCSE.d.cts} +227 -2
- package/dist/{engine-CCSSqPbZ.d.ts → engine-BSvSdCSE.d.ts} +227 -2
- package/dist/index.cjs +419 -937
- package/dist/index.d.cts +315 -32
- package/dist/index.d.ts +315 -32
- package/dist/index.js +423 -941
- package/dist/plugin.cjs +11 -13
- package/dist/plugin.d.cts +15 -1
- package/dist/plugin.d.ts +15 -1
- package/dist/plugin.js +7 -9
- package/dist/realtime/index.cjs +31 -0
- package/dist/realtime/index.d.cts +15 -0
- package/dist/realtime/index.d.ts +15 -0
- package/dist/realtime/index.js +31 -0
- package/dist/ui/index.cjs +1297 -0
- package/dist/ui/index.d.cts +493 -0
- package/dist/ui/index.d.ts +493 -0
- package/dist/ui/index.js +1297 -0
- package/dist/version.cjs +2 -2
- package/dist/version.js +1 -1
- package/package.json +31 -15
- package/src/advanced.ts +11 -9
- package/src/auto-resume.tsx +259 -0
- package/src/camera-controls.ts +99 -0
- package/src/camera-grant-provider.ts +392 -0
- package/src/camera-settings.ts +133 -0
- package/src/contracts.ts +73 -0
- package/src/device-agent/agent.ts +262 -9
- package/src/device-agent/beat-policy.ts +46 -0
- package/src/device-agent/component.tsx +11 -0
- package/src/device-agent/convex-beat-transport.ts +140 -0
- package/src/device-agent/convex-command-subscription.ts +101 -0
- package/src/device-agent/convex-control-channel.ts +7 -1
- package/src/device-agent/http-transport.ts +22 -1
- package/src/device-agent/index.ts +22 -0
- package/src/device-agent/mqtt-channel.ts +379 -0
- package/src/device-agent/mqtt-codec.ts +347 -0
- package/src/device-agent/tournament.ts +352 -0
- package/src/engine.ts +580 -15
- package/src/http-session-provider.ts +21 -189
- package/src/index.ts +84 -5
- package/src/local-video-quality.ts +43 -0
- package/src/native-runtime.ts +234 -78
- package/src/plugin.ts +19 -6
- package/src/provider.tsx +94 -18
- package/src/realtime/index.ts +56 -0
- package/src/resume-policy.ts +85 -0
- package/src/resume.ts +132 -0
- package/src/rtmp-room-adapter.ts +164 -12
- package/src/session-grant.ts +256 -0
- package/src/ui/camera-screen.tsx +484 -0
- package/src/ui/controls-sheet.tsx +445 -0
- package/src/ui/copy.ts +326 -0
- package/src/ui/helpers.ts +105 -0
- package/src/ui/hooks.ts +59 -0
- package/src/ui/index.ts +42 -0
- package/src/ui/preview-view.tsx +39 -0
- package/src/ui/primitives.tsx +304 -0
- package/src/uplink-check.ts +173 -0
- package/dist/advanced-BXDrDl1h.d.ts +0 -316
- package/dist/advanced-BdAa2Erv.d.cts +0 -316
- package/src/livekit-room-adapter.ts +0 -390
- package/src/livestream-screen.tsx +0 -1002
package/src/ui/copy.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chữ trên màn hình camera + sheet cấu hình (vi/en). Chuyển từ
|
|
3
|
+
* apps/device-streamer; bản en là bản dịch tương đương để app đối tác dùng
|
|
4
|
+
* được ngay. App muốn chữ khác thì truyền prop (statusPill/statusHint/title)
|
|
5
|
+
* — không cần fork màn hình.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExpoSdkErrorCode } from "../errors";
|
|
9
|
+
|
|
10
|
+
export type UiLocale = "vi" | "en";
|
|
11
|
+
|
|
12
|
+
export interface UiCopy {
|
|
13
|
+
pill: {
|
|
14
|
+
opening: string;
|
|
15
|
+
ready: string;
|
|
16
|
+
connecting: string;
|
|
17
|
+
paused: string;
|
|
18
|
+
court: string;
|
|
19
|
+
standby: string;
|
|
20
|
+
live: string;
|
|
21
|
+
reconnecting: string;
|
|
22
|
+
ending: string;
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
|
25
|
+
hint: {
|
|
26
|
+
casualPreview: string;
|
|
27
|
+
casualStandby: string;
|
|
28
|
+
paused: string;
|
|
29
|
+
courtPreview: string;
|
|
30
|
+
courtStandby: (court: string) => string;
|
|
31
|
+
courtStandbyNoCourt: string;
|
|
32
|
+
connecting: string;
|
|
33
|
+
reconnecting: string;
|
|
34
|
+
ending: string;
|
|
35
|
+
errorFallback: string;
|
|
36
|
+
cameraOpening: string;
|
|
37
|
+
cameraError: string;
|
|
38
|
+
};
|
|
39
|
+
button: {
|
|
40
|
+
goLive: string;
|
|
41
|
+
publishNow: string;
|
|
42
|
+
resume: string;
|
|
43
|
+
stop: string;
|
|
44
|
+
stopQuestion: string;
|
|
45
|
+
confirmStop: string;
|
|
46
|
+
cancel: string;
|
|
47
|
+
retry: string;
|
|
48
|
+
connecting: string;
|
|
49
|
+
ending: string;
|
|
50
|
+
};
|
|
51
|
+
lens: { front: string };
|
|
52
|
+
thermal: { title: string; subtitle: string };
|
|
53
|
+
/** Tiêu đề + cách xử lý theo mã lỗi (chuyển từ LivestreamScreen 0.x). */
|
|
54
|
+
errors: {
|
|
55
|
+
title: Record<ExpoSdkErrorCode, string>;
|
|
56
|
+
remediation: Record<ExpoSdkErrorCode, string>;
|
|
57
|
+
technicalDetail: string;
|
|
58
|
+
};
|
|
59
|
+
sheet: {
|
|
60
|
+
title: string;
|
|
61
|
+
close: string;
|
|
62
|
+
/** Nhãn a11y của vùng nền bấm-để-đóng (khác nút ✕ để test/đọc màn hình phân biệt). */
|
|
63
|
+
closeBackdrop: string;
|
|
64
|
+
resolution: string;
|
|
65
|
+
fps: string;
|
|
66
|
+
fps60Note: string;
|
|
67
|
+
antiFlicker: string;
|
|
68
|
+
antiFlickerNote: string;
|
|
69
|
+
auto: string;
|
|
70
|
+
lockedNote: string;
|
|
71
|
+
defaultCamera: string;
|
|
72
|
+
autoCamera: string;
|
|
73
|
+
autoCameraHint: string;
|
|
74
|
+
frontCamera: string;
|
|
75
|
+
backCamera: string;
|
|
76
|
+
cameraIdHint: (id: string) => string;
|
|
77
|
+
zoom: string;
|
|
78
|
+
ev: string;
|
|
79
|
+
focusLock: string;
|
|
80
|
+
exposureLock: string;
|
|
81
|
+
torch: string;
|
|
82
|
+
network: string;
|
|
83
|
+
runCheck: string;
|
|
84
|
+
running: string;
|
|
85
|
+
busyNote: string;
|
|
86
|
+
resultLine: (input: {
|
|
87
|
+
uploadMbps: number;
|
|
88
|
+
requiredMbps: number;
|
|
89
|
+
resolution: number;
|
|
90
|
+
fps: number;
|
|
91
|
+
pingMs: number | null;
|
|
92
|
+
}) => string;
|
|
93
|
+
good: string;
|
|
94
|
+
ok: string;
|
|
95
|
+
low: string;
|
|
96
|
+
checkError: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const vi: UiCopy = {
|
|
101
|
+
pill: {
|
|
102
|
+
opening: "ĐANG MỞ CAMERA…",
|
|
103
|
+
ready: "SẴN SÀNG",
|
|
104
|
+
connecting: "ĐANG KẾT NỐI…",
|
|
105
|
+
paused: "TẠM DỪNG",
|
|
106
|
+
court: "TRỰC SÂN",
|
|
107
|
+
standby: "SẴN SÀNG PHÁT",
|
|
108
|
+
live: "LIVE",
|
|
109
|
+
reconnecting: "MẤT KẾT NỐI…",
|
|
110
|
+
ending: "ĐANG DỪNG…",
|
|
111
|
+
error: "LỖI",
|
|
112
|
+
},
|
|
113
|
+
hint: {
|
|
114
|
+
casualPreview: "Bấm “Bắt đầu phát” để lên sóng",
|
|
115
|
+
casualStandby: "Phiên đã sẵn sàng — bấm “Phát ngay”",
|
|
116
|
+
paused: "Đang tạm dừng — bấm “Phát tiếp” để tiếp tục",
|
|
117
|
+
courtPreview: "Đang chuẩn bị camera…",
|
|
118
|
+
courtStandby: (court) => `Đang gác ${court} — trọng tài bấm bắt đầu là tự phát`,
|
|
119
|
+
courtStandbyNoCourt: "Đang trực — chờ lệnh phát",
|
|
120
|
+
connecting: "Đang kết nối tới máy chủ…",
|
|
121
|
+
reconnecting: "Mất kết nối — đang thử lại…",
|
|
122
|
+
ending: "Đang dừng phát…",
|
|
123
|
+
errorFallback: "Có lỗi — thử lại",
|
|
124
|
+
cameraOpening: "Đang mở camera…",
|
|
125
|
+
cameraError: "Không mở được camera",
|
|
126
|
+
},
|
|
127
|
+
button: {
|
|
128
|
+
goLive: "Bắt đầu phát",
|
|
129
|
+
publishNow: "Phát ngay",
|
|
130
|
+
resume: "Phát tiếp",
|
|
131
|
+
stop: "Dừng",
|
|
132
|
+
stopQuestion: "Dừng phát?",
|
|
133
|
+
confirmStop: "Xác nhận dừng",
|
|
134
|
+
cancel: "Huỷ",
|
|
135
|
+
retry: "Thử lại",
|
|
136
|
+
connecting: "Đang kết nối…",
|
|
137
|
+
ending: "Đang dừng…",
|
|
138
|
+
},
|
|
139
|
+
lens: { front: "Trước" },
|
|
140
|
+
thermal: {
|
|
141
|
+
title: "Tiết kiệm nhiệt & pin",
|
|
142
|
+
subtitle: "Chạm vào màn hình để bật lại giao diện",
|
|
143
|
+
},
|
|
144
|
+
errors: {
|
|
145
|
+
title: {
|
|
146
|
+
PERMISSION_DENIED: "Cần quyền Camera và Micrô",
|
|
147
|
+
NETWORK_UNAVAILABLE: "Không có kết nối mạng",
|
|
148
|
+
TOKEN_EXPIRED: "Phiên xác thực đã hết hạn",
|
|
149
|
+
SDK_UPGRADE_REQUIRED: "Cần cập nhật ứng dụng",
|
|
150
|
+
UNSUPPORTED_RUNTIME: "Môi trường chạy không được hỗ trợ",
|
|
151
|
+
SESSION_CONFLICT: "Đang có phiên phát khác",
|
|
152
|
+
RECORDING_FAILED: "Không thể ghi hình",
|
|
153
|
+
CONSENT_REQUIRED: "Cần đồng ý ghi hình",
|
|
154
|
+
SDK_DISABLED: "Tính năng phát trực tiếp đang tắt",
|
|
155
|
+
CONFIGURATION_ERROR: "Cấu hình SDK chưa đúng",
|
|
156
|
+
INVALID_RESPONSE: "Phản hồi máy chủ không hợp lệ",
|
|
157
|
+
TIMEOUT: "Yêu cầu quá thời gian",
|
|
158
|
+
UNKNOWN: "Đã xảy ra lỗi",
|
|
159
|
+
},
|
|
160
|
+
remediation: {
|
|
161
|
+
PERMISSION_DENIED: "Mở Cài đặt và cho phép Camera cùng Micrô, sau đó thử lại.",
|
|
162
|
+
NETWORK_UNAVAILABLE: "Kiểm tra Wi-Fi hoặc dữ liệu di động rồi thử lại.",
|
|
163
|
+
TOKEN_EXPIRED: "Phiên xác thực đã hết hạn. Hãy kết nối mạng và thử lại.",
|
|
164
|
+
SDK_UPGRADE_REQUIRED: "Cập nhật ứng dụng lên bản mới nhất và cài lại Development Build.",
|
|
165
|
+
UNSUPPORTED_RUNTIME: "Cài Development Build trên iOS hoặc Android; Expo Go và web không được hỗ trợ.",
|
|
166
|
+
SESSION_CONFLICT: "Kết thúc phiên phát đang hoạt động trước khi bắt đầu phiên mới.",
|
|
167
|
+
RECORDING_FAILED: "Không thể tạo bản ghi máy chủ. Hãy thử lại hoặc liên hệ hỗ trợ.",
|
|
168
|
+
CONSENT_REQUIRED: "Đồng ý ghi hình trước khi bắt đầu phát trực tiếp.",
|
|
169
|
+
SDK_DISABLED: "Tính năng đang bị tắt cho ứng dụng này. Hãy liên hệ quản trị viên.",
|
|
170
|
+
CONFIGURATION_ERROR: "Kiểm tra cấu hình Provider và config plugin, sau đó rebuild ứng dụng.",
|
|
171
|
+
INVALID_RESPONSE: "Máy chủ trả về dữ liệu không hợp lệ. Hãy thử lại hoặc liên hệ hỗ trợ.",
|
|
172
|
+
TIMEOUT: "Yêu cầu quá thời gian. Kiểm tra kết nối rồi thử lại.",
|
|
173
|
+
UNKNOWN: "Thử lại. Nếu lỗi tiếp diễn, hãy liên hệ hỗ trợ.",
|
|
174
|
+
},
|
|
175
|
+
technicalDetail: "Chi tiết kỹ thuật",
|
|
176
|
+
},
|
|
177
|
+
sheet: {
|
|
178
|
+
title: "Cấu hình camera",
|
|
179
|
+
close: "Đóng",
|
|
180
|
+
closeBackdrop: "Đóng cấu hình",
|
|
181
|
+
resolution: "Độ phân giải",
|
|
182
|
+
fps: "FPS",
|
|
183
|
+
fps60Note: "60fps gấp đôi bitrate và toả nhiệt — chỉ nên dùng khi sân đủ sáng và mạng khoẻ.",
|
|
184
|
+
antiFlicker: "Chống chớp đèn",
|
|
185
|
+
antiFlickerNote: "Điện VN 50Hz — đèn trong nhà thấy vằn sáng tối hay hình phập phồng thì giữ 50Hz.",
|
|
186
|
+
auto: "Auto",
|
|
187
|
+
lockedNote: "Đang trong phiên — đổi độ phân giải/FPS/camera sau khi dừng phát.",
|
|
188
|
+
defaultCamera: "Camera mặc định",
|
|
189
|
+
autoCamera: "Tự động",
|
|
190
|
+
autoCameraHint: "Camera sau mặc định của máy",
|
|
191
|
+
frontCamera: "Camera trước",
|
|
192
|
+
backCamera: "Camera sau",
|
|
193
|
+
cameraIdHint: (id) => `id ${id}`,
|
|
194
|
+
zoom: "Zoom",
|
|
195
|
+
ev: "EV",
|
|
196
|
+
focusLock: "Khoá nét",
|
|
197
|
+
exposureLock: "Khoá sáng",
|
|
198
|
+
torch: "Đèn pin",
|
|
199
|
+
network: "Mạng",
|
|
200
|
+
runCheck: "Kiểm tra tốc độ mạng",
|
|
201
|
+
running: "Đang đo tải lên… (~10 giây)",
|
|
202
|
+
busyNote: "Đang phát — đo lúc này sẽ giành băng thông với sóng. Dừng phát rồi đo.",
|
|
203
|
+
resultLine: ({ uploadMbps, requiredMbps, resolution, fps, pingMs }) =>
|
|
204
|
+
`Tải lên ${uploadMbps.toFixed(1)} Mbps · cần ~${requiredMbps.toFixed(1)} Mbps cho ${resolution}p${fps}` +
|
|
205
|
+
(pingMs !== null ? ` · ping ${pingMs}ms` : ""),
|
|
206
|
+
good: "ĐỦ TỐT — dư băng thông cho cấu hình này.",
|
|
207
|
+
ok: "TẠM ĐỦ — mạng chùng là chất lượng phải hạ theo.",
|
|
208
|
+
low: "KHÔNG ĐỦ — hạ độ phân giải hoặc đổi mạng trước khi lên sóng.",
|
|
209
|
+
checkError: "Không đo được — kiểm tra kết nối mạng rồi thử lại.",
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const en: UiCopy = {
|
|
214
|
+
pill: {
|
|
215
|
+
opening: "OPENING CAMERA…",
|
|
216
|
+
ready: "READY",
|
|
217
|
+
connecting: "CONNECTING…",
|
|
218
|
+
paused: "PAUSED",
|
|
219
|
+
court: "ON COURT",
|
|
220
|
+
standby: "READY TO GO LIVE",
|
|
221
|
+
live: "LIVE",
|
|
222
|
+
reconnecting: "RECONNECTING…",
|
|
223
|
+
ending: "STOPPING…",
|
|
224
|
+
error: "ERROR",
|
|
225
|
+
},
|
|
226
|
+
hint: {
|
|
227
|
+
casualPreview: "Tap “Go live” to start streaming",
|
|
228
|
+
casualStandby: "Session ready — tap “Go live now”",
|
|
229
|
+
paused: "Paused — tap “Resume” to continue",
|
|
230
|
+
courtPreview: "Preparing the camera…",
|
|
231
|
+
courtStandby: (court) => `Standing by on ${court} — the referee's start button goes live automatically`,
|
|
232
|
+
courtStandbyNoCourt: "Standing by — waiting for the start command",
|
|
233
|
+
connecting: "Connecting to the server…",
|
|
234
|
+
reconnecting: "Connection lost — retrying…",
|
|
235
|
+
ending: "Stopping the stream…",
|
|
236
|
+
errorFallback: "Something went wrong — retry",
|
|
237
|
+
cameraOpening: "Opening the camera…",
|
|
238
|
+
cameraError: "The camera could not be opened",
|
|
239
|
+
},
|
|
240
|
+
button: {
|
|
241
|
+
goLive: "Go live",
|
|
242
|
+
publishNow: "Go live now",
|
|
243
|
+
resume: "Resume",
|
|
244
|
+
stop: "Stop",
|
|
245
|
+
stopQuestion: "Stop streaming?",
|
|
246
|
+
confirmStop: "Yes, stop",
|
|
247
|
+
cancel: "Cancel",
|
|
248
|
+
retry: "Retry",
|
|
249
|
+
connecting: "Connecting…",
|
|
250
|
+
ending: "Stopping…",
|
|
251
|
+
},
|
|
252
|
+
lens: { front: "Front" },
|
|
253
|
+
thermal: {
|
|
254
|
+
title: "Saving heat & battery",
|
|
255
|
+
subtitle: "Tap the screen to show the controls again",
|
|
256
|
+
},
|
|
257
|
+
errors: {
|
|
258
|
+
title: {
|
|
259
|
+
PERMISSION_DENIED: "Camera and Microphone access required",
|
|
260
|
+
NETWORK_UNAVAILABLE: "No network connection",
|
|
261
|
+
TOKEN_EXPIRED: "Authentication expired",
|
|
262
|
+
SDK_UPGRADE_REQUIRED: "App update required",
|
|
263
|
+
UNSUPPORTED_RUNTIME: "Unsupported runtime",
|
|
264
|
+
SESSION_CONFLICT: "Another livestream is active",
|
|
265
|
+
RECORDING_FAILED: "Recording failed",
|
|
266
|
+
CONSENT_REQUIRED: "Recording consent required",
|
|
267
|
+
SDK_DISABLED: "Livestreaming is disabled",
|
|
268
|
+
CONFIGURATION_ERROR: "SDK configuration error",
|
|
269
|
+
INVALID_RESPONSE: "Invalid server response",
|
|
270
|
+
TIMEOUT: "Request timed out",
|
|
271
|
+
UNKNOWN: "Something went wrong",
|
|
272
|
+
},
|
|
273
|
+
remediation: {
|
|
274
|
+
PERMISSION_DENIED: "Open Settings and allow Camera and Microphone, then try again.",
|
|
275
|
+
NETWORK_UNAVAILABLE: "Check Wi-Fi or mobile data, then try again.",
|
|
276
|
+
TOKEN_EXPIRED: "Your authentication has expired. Connect to the network and try again.",
|
|
277
|
+
SDK_UPGRADE_REQUIRED: "Update the app to the latest version and install a new Development Build.",
|
|
278
|
+
UNSUPPORTED_RUNTIME: "Install an iOS or Android Development Build; Expo Go and web are unsupported.",
|
|
279
|
+
SESSION_CONFLICT: "End the active livestream before starting another one.",
|
|
280
|
+
RECORDING_FAILED: "The server recording could not be created. Try again or contact support.",
|
|
281
|
+
CONSENT_REQUIRED: "Accept recording consent before going live.",
|
|
282
|
+
SDK_DISABLED: "Livestreaming is disabled for this app. Contact an administrator.",
|
|
283
|
+
CONFIGURATION_ERROR: "Check the Provider and config plugin setup, then rebuild the app.",
|
|
284
|
+
INVALID_RESPONSE: "The server returned invalid data. Try again or contact support.",
|
|
285
|
+
TIMEOUT: "The request timed out. Check your connection and try again.",
|
|
286
|
+
UNKNOWN: "Try again. If the problem continues, contact support.",
|
|
287
|
+
},
|
|
288
|
+
technicalDetail: "Technical details",
|
|
289
|
+
},
|
|
290
|
+
sheet: {
|
|
291
|
+
title: "Camera settings",
|
|
292
|
+
close: "Close",
|
|
293
|
+
closeBackdrop: "Close settings",
|
|
294
|
+
resolution: "Resolution",
|
|
295
|
+
fps: "FPS",
|
|
296
|
+
fps60Note: "60 fps doubles the bitrate and heat — only for bright courts on a strong network.",
|
|
297
|
+
antiFlicker: "Anti-flicker",
|
|
298
|
+
antiFlickerNote: "Match the local mains frequency (VN: 50 Hz). Banding or pulsing indoor light means the wrong setting.",
|
|
299
|
+
auto: "Auto",
|
|
300
|
+
lockedNote: "In a session — change resolution/FPS/camera after stopping.",
|
|
301
|
+
defaultCamera: "Default camera",
|
|
302
|
+
autoCamera: "Automatic",
|
|
303
|
+
autoCameraHint: "The device's default back camera",
|
|
304
|
+
frontCamera: "Front camera",
|
|
305
|
+
backCamera: "Back camera",
|
|
306
|
+
cameraIdHint: (id) => `id ${id}`,
|
|
307
|
+
zoom: "Zoom",
|
|
308
|
+
ev: "EV",
|
|
309
|
+
focusLock: "Lock focus",
|
|
310
|
+
exposureLock: "Lock exposure",
|
|
311
|
+
torch: "Torch",
|
|
312
|
+
network: "Network",
|
|
313
|
+
runCheck: "Check upload speed",
|
|
314
|
+
running: "Measuring upload… (~10 s)",
|
|
315
|
+
busyNote: "Streaming — measuring now would compete with the stream. Stop first.",
|
|
316
|
+
resultLine: ({ uploadMbps, requiredMbps, resolution, fps, pingMs }) =>
|
|
317
|
+
`Upload ${uploadMbps.toFixed(1)} Mbps · needs ~${requiredMbps.toFixed(1)} Mbps for ${resolution}p${fps}` +
|
|
318
|
+
(pingMs !== null ? ` · ping ${pingMs} ms` : ""),
|
|
319
|
+
good: "GOOD — plenty of headroom for this configuration.",
|
|
320
|
+
ok: "MARGINAL — quality will drop when the network dips.",
|
|
321
|
+
low: "NOT ENOUGH — lower the resolution or switch networks before going live.",
|
|
322
|
+
checkError: "Could not measure — check the connection and try again.",
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const uiCopy: Record<UiLocale, UiCopy> = { vi, en };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { LivestreamState } from "../contracts";
|
|
2
|
+
import type { CameraDeviceInfo } from "../engine";
|
|
3
|
+
import { uiCopy, type UiLocale } from "./copy";
|
|
4
|
+
|
|
5
|
+
/** Luật THUẦN của màn hình camera — test được, không React. */
|
|
6
|
+
|
|
7
|
+
export interface StatusPill {
|
|
8
|
+
label: string;
|
|
9
|
+
color: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Màu ngữ nghĩa của pill — trùng bảng màu app Picklive Camera (không theo theme). */
|
|
13
|
+
export const PILL_COLORS = Object.freeze({
|
|
14
|
+
gray: "#6b7280",
|
|
15
|
+
green: "#22c55e",
|
|
16
|
+
amber: "#f59e0b",
|
|
17
|
+
orange: "#f97316",
|
|
18
|
+
red: "#ef4444",
|
|
19
|
+
darkRed: "#b91c1c",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export interface StatusPillOptions {
|
|
23
|
+
/** elapsedMs > 0 ở standby = đã phát rồi hạ về chờ (tạm dừng). */
|
|
24
|
+
elapsedMs?: number;
|
|
25
|
+
/** Phiên gắn sân: standby là "TRỰC SÂN" thay vì "SẴN SÀNG PHÁT". */
|
|
26
|
+
courtBound?: boolean;
|
|
27
|
+
locale?: UiLocale;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pill theo TỪNG trạng thái engine — không gộp chung "Đang chờ". */
|
|
31
|
+
export function statusPillFor(state: LivestreamState, options: StatusPillOptions = {}): StatusPill {
|
|
32
|
+
const copy = uiCopy[options.locale ?? "vi"].pill;
|
|
33
|
+
const elapsedMs = options.elapsedMs ?? 0;
|
|
34
|
+
switch (state) {
|
|
35
|
+
case "idle":
|
|
36
|
+
case "preparing":
|
|
37
|
+
return { label: copy.opening, color: PILL_COLORS.gray };
|
|
38
|
+
case "preview":
|
|
39
|
+
return { label: copy.ready, color: PILL_COLORS.green };
|
|
40
|
+
case "connecting":
|
|
41
|
+
return { label: copy.connecting, color: PILL_COLORS.amber };
|
|
42
|
+
case "standby":
|
|
43
|
+
if (elapsedMs > 0) return { label: copy.paused, color: PILL_COLORS.amber };
|
|
44
|
+
return options.courtBound
|
|
45
|
+
? { label: copy.court, color: PILL_COLORS.orange }
|
|
46
|
+
: { label: copy.standby, color: PILL_COLORS.orange };
|
|
47
|
+
case "live":
|
|
48
|
+
return { label: copy.live, color: PILL_COLORS.red };
|
|
49
|
+
case "reconnecting":
|
|
50
|
+
return { label: copy.reconnecting, color: PILL_COLORS.amber };
|
|
51
|
+
case "ending":
|
|
52
|
+
return { label: copy.ending, color: PILL_COLORS.gray };
|
|
53
|
+
case "error":
|
|
54
|
+
return { label: copy.error, color: PILL_COLORS.darkRed };
|
|
55
|
+
// "ended" thoáng qua rồi AutoPrepare đưa về preview.
|
|
56
|
+
default:
|
|
57
|
+
return { label: copy.ready, color: PILL_COLORS.green };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Đuôi sessionId để đối chiếu với URL /dashboard/<id> — đủ phân biệt, không tràn màn hình. */
|
|
62
|
+
export function shortSessionId(sessionId: string): string {
|
|
63
|
+
return sessionId.length <= 10 ? sessionId : `…${sessionId.slice(-8)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** mm:ss (hoặc h:mm:ss khi qua giờ) từ elapsedMs của engine — đếm thời gian PHÁT. */
|
|
67
|
+
export function formatElapsed(ms: number): string {
|
|
68
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
69
|
+
const hours = Math.floor(total / 3600);
|
|
70
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
71
|
+
const seconds = total % 60;
|
|
72
|
+
const mm = String(minutes).padStart(2, "0");
|
|
73
|
+
const ss = String(seconds).padStart(2, "0");
|
|
74
|
+
return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Nhãn lens từ label tiếng Anh của native ("Back Ultra Wide Camera", "Back
|
|
79
|
+
* Telephoto Camera", …) — load-bearing: native dựng nhãn đúng dạng này để UI
|
|
80
|
+
* dò được (native-contract test giữ hợp đồng).
|
|
81
|
+
*/
|
|
82
|
+
export function lensShortLabel(label: string): string {
|
|
83
|
+
if (/ultra\s*wide/i.test(label)) return "0.5x";
|
|
84
|
+
if (/telephoto/i.test(label)) return "Tele";
|
|
85
|
+
return "1x";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** previewTrack của engine ở mode device-rtmp là `{kind:"rtmp"}` — JS không có track để bind. */
|
|
89
|
+
export function isRtmpPreview(previewTrack: unknown): boolean {
|
|
90
|
+
return (
|
|
91
|
+
typeof previewTrack === "object" &&
|
|
92
|
+
previewTrack !== null &&
|
|
93
|
+
(previewTrack as { kind?: unknown }).kind === "rtmp"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Lens đang chọn: id engine chọn tường minh, không thì lens 1x mặc định của camera sau. */
|
|
98
|
+
export function activeLensId(
|
|
99
|
+
cameras: CameraDeviceInfo[],
|
|
100
|
+
selectedCameraId: string | null,
|
|
101
|
+
): string | null {
|
|
102
|
+
if (selectedCameraId) return selectedCameraId;
|
|
103
|
+
const backLenses = cameras.filter((camera) => camera.facing === "environment");
|
|
104
|
+
return backLenses.find((camera) => lensShortLabel(camera.label) === "1x")?.deviceId ?? null;
|
|
105
|
+
}
|
package/src/ui/hooks.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { LivestreamState } from "../contracts";
|
|
3
|
+
import { applyAntiFlicker, getCameraCapabilities, type CameraCapabilities } from "../camera-controls";
|
|
4
|
+
import type { AntiFlickerMode } from "../camera-settings";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bounds + cờ hỗ trợ của lens đang gắn — đọc lại khi `active` bật (mở sheet)
|
|
8
|
+
* và khi đổi lens. null = module/hàm chưa có hoặc camera chưa mở → UI ẩn control.
|
|
9
|
+
*/
|
|
10
|
+
export function useCameraCapabilities(
|
|
11
|
+
active: boolean,
|
|
12
|
+
cameraKey: string | null,
|
|
13
|
+
): CameraCapabilities | null {
|
|
14
|
+
const [capabilities, setCapabilities] = useState<CameraCapabilities | null>(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!active) return;
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
void getCameraCapabilities().then((value) => {
|
|
19
|
+
if (!cancelled) setCapabilities(value);
|
|
20
|
+
});
|
|
21
|
+
return () => {
|
|
22
|
+
cancelled = true;
|
|
23
|
+
};
|
|
24
|
+
}, [active, cameraKey]);
|
|
25
|
+
return capabilities;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tự tối màn hình tiết kiệm nhiệt/pin sau `afterMs` ở trạng thái LIVE; chạm để
|
|
30
|
+
* bật lại (setDimmed(false)). null = tắt tính năng.
|
|
31
|
+
*/
|
|
32
|
+
export function useThermalDim(
|
|
33
|
+
state: LivestreamState,
|
|
34
|
+
afterMs: number | null,
|
|
35
|
+
): [boolean, (dimmed: boolean) => void] {
|
|
36
|
+
const [dimmed, setDimmed] = useState(false);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (state !== "live" || afterMs === null) {
|
|
39
|
+
setDimmed(false);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const timer = setTimeout(() => setDimmed(true), afterMs);
|
|
43
|
+
return () => clearTimeout(timer);
|
|
44
|
+
// `dimmed` trong deps: chạm bật lại UI thì hẹn giờ lại từ đầu.
|
|
45
|
+
}, [state, afterMs, dimmed]);
|
|
46
|
+
return [dimmed, setDimmed];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Đẩy chế độ chống chớp xuống native khi mount + mỗi lần đổi. Gọi trước khi
|
|
51
|
+
* camera mở cũng không mất — native lưu giá trị và tự áp lại mỗi lần
|
|
52
|
+
* attachCamera (kể cả đổi lens). iOS/binary cũ thiếu hàm → no-op.
|
|
53
|
+
*/
|
|
54
|
+
export function useAntiFlicker(mode: AntiFlickerMode | undefined): void {
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!mode) return;
|
|
57
|
+
void applyAntiFlicker(mode);
|
|
58
|
+
}, [mode]);
|
|
59
|
+
}
|
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@pickleball/expo-sdk/ui` — màn hình camera + sheet cấu hình dựng sẵn (bố cục
|
|
3
|
+
* của app Picklive Camera). Subpath riêng để root entry không kéo theo
|
|
4
|
+
* react-native-safe-area-context/Modal cho app chỉ dùng engine.
|
|
5
|
+
*
|
|
6
|
+
* Yêu cầu: render trong PickleballLiveProvider; @pickleball/rtmp-native cài
|
|
7
|
+
* trong app để có khung hình (nạp lười — thiếu thì nền tối, không gãy import).
|
|
8
|
+
*/
|
|
9
|
+
export { PickleballCameraScreen } from "./camera-screen";
|
|
10
|
+
export type {
|
|
11
|
+
CameraScreenMode,
|
|
12
|
+
CameraScreenSheetRenderProps,
|
|
13
|
+
PickleballCameraScreenProps,
|
|
14
|
+
} from "./camera-screen";
|
|
15
|
+
export { CameraControlsSheet } from "./controls-sheet";
|
|
16
|
+
export type { CameraControlsSheetProps, CameraControlsSheetSections } from "./controls-sheet";
|
|
17
|
+
export { uiCopy } from "./copy";
|
|
18
|
+
export type { UiCopy, UiLocale } from "./copy";
|
|
19
|
+
export {
|
|
20
|
+
PILL_COLORS,
|
|
21
|
+
activeLensId,
|
|
22
|
+
formatElapsed,
|
|
23
|
+
isRtmpPreview,
|
|
24
|
+
lensShortLabel,
|
|
25
|
+
shortSessionId,
|
|
26
|
+
statusPillFor,
|
|
27
|
+
} from "./helpers";
|
|
28
|
+
export type { StatusPill as StatusPillValue, StatusPillOptions } from "./helpers";
|
|
29
|
+
export { useAntiFlicker, useCameraCapabilities, useThermalDim } from "./hooks";
|
|
30
|
+
export { RtmpPreview, getRtmpPreviewView, resetRtmpPreviewViewCache } from "./preview-view";
|
|
31
|
+
export {
|
|
32
|
+
ActionButton,
|
|
33
|
+
CameraOption,
|
|
34
|
+
ControlRow,
|
|
35
|
+
LensRow,
|
|
36
|
+
Note,
|
|
37
|
+
PresetButton,
|
|
38
|
+
Segmented,
|
|
39
|
+
StatusPill,
|
|
40
|
+
uiColors,
|
|
41
|
+
uiStyles,
|
|
42
|
+
} from "./primitives";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { type ComponentType } from "react";
|
|
2
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
|
|
4
|
+
declare const require: (name: string) => any;
|
|
5
|
+
|
|
6
|
+
type PreviewComponent = ComponentType<{ style?: StyleProp<ViewStyle> }>;
|
|
7
|
+
|
|
8
|
+
let cached: PreviewComponent | null | undefined;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* View native của @pickleball/rtmp-native, nạp LƯỜI: import tĩnh là app nào
|
|
12
|
+
* chưa cài module (hoặc static-render web của expo-router) gãy ngay lúc
|
|
13
|
+
* import subpath ./ui. Không có module → null, màn hình vẽ nền tối thay vì
|
|
14
|
+
* khung hình.
|
|
15
|
+
*
|
|
16
|
+
* BẮT BUỘC render view này khi camera đã mở, nếu không màn preview TỐI ĐEN:
|
|
17
|
+
* native vẫn encode bình thường nhưng không có đích để vẽ khung hình.
|
|
18
|
+
*/
|
|
19
|
+
export function getRtmpPreviewView(): PreviewComponent | null {
|
|
20
|
+
if (cached !== undefined) return cached;
|
|
21
|
+
try {
|
|
22
|
+
const loaded = require("@pickleball/rtmp-native");
|
|
23
|
+
const candidate = loaded?.PickleballRtmpPreviewView;
|
|
24
|
+
cached = candidate ? (candidate as PreviewComponent) : null;
|
|
25
|
+
} catch {
|
|
26
|
+
cached = null;
|
|
27
|
+
}
|
|
28
|
+
return cached;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Cho test: xoá cache để mock module khác. */
|
|
32
|
+
export function resetRtmpPreviewViewCache(): void {
|
|
33
|
+
cached = undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function RtmpPreview({ style }: { style?: StyleProp<ViewStyle> }) {
|
|
37
|
+
const Preview = getRtmpPreviewView();
|
|
38
|
+
return Preview ? <Preview style={style} /> : null;
|
|
39
|
+
}
|