@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/dist/ui/index.js
ADDED
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PickleballAutoPrepare,
|
|
3
|
+
applyAntiFlicker,
|
|
4
|
+
getCameraCapabilities,
|
|
5
|
+
setExposureBias,
|
|
6
|
+
setExposureLocked,
|
|
7
|
+
setFocusLocked,
|
|
8
|
+
setTorch,
|
|
9
|
+
setZoom,
|
|
10
|
+
useCameraSettings
|
|
11
|
+
} from "../chunk-7Z7TN5N3.js";
|
|
12
|
+
import {
|
|
13
|
+
runUplinkCheck,
|
|
14
|
+
useLivestream
|
|
15
|
+
} from "../chunk-XGP2MN5R.js";
|
|
16
|
+
import {
|
|
17
|
+
__require
|
|
18
|
+
} from "../chunk-KFQGP6VL.js";
|
|
19
|
+
|
|
20
|
+
// src/ui/camera-screen.tsx
|
|
21
|
+
import { useState as useState3 } from "react";
|
|
22
|
+
import { Pressable as Pressable3, StyleSheet as StyleSheet3, Text as Text3, View as View3 } from "react-native";
|
|
23
|
+
import { SafeAreaView as SafeAreaView2 } from "react-native-safe-area-context";
|
|
24
|
+
|
|
25
|
+
// src/ui/controls-sheet.tsx
|
|
26
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
27
|
+
import {
|
|
28
|
+
Modal,
|
|
29
|
+
Platform,
|
|
30
|
+
Pressable as Pressable2,
|
|
31
|
+
ScrollView,
|
|
32
|
+
StyleSheet as StyleSheet2,
|
|
33
|
+
Switch,
|
|
34
|
+
Text as Text2,
|
|
35
|
+
View as View2,
|
|
36
|
+
useWindowDimensions
|
|
37
|
+
} from "react-native";
|
|
38
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
39
|
+
|
|
40
|
+
// src/ui/copy.ts
|
|
41
|
+
var vi = {
|
|
42
|
+
pill: {
|
|
43
|
+
opening: "\u0110ANG M\u1EDE CAMERA\u2026",
|
|
44
|
+
ready: "S\u1EB4N S\xC0NG",
|
|
45
|
+
connecting: "\u0110ANG K\u1EBET N\u1ED0I\u2026",
|
|
46
|
+
paused: "T\u1EA0M D\u1EEANG",
|
|
47
|
+
court: "TR\u1EF0C S\xC2N",
|
|
48
|
+
standby: "S\u1EB4N S\xC0NG PH\xC1T",
|
|
49
|
+
live: "LIVE",
|
|
50
|
+
reconnecting: "M\u1EA4T K\u1EBET N\u1ED0I\u2026",
|
|
51
|
+
ending: "\u0110ANG D\u1EEANG\u2026",
|
|
52
|
+
error: "L\u1ED6I"
|
|
53
|
+
},
|
|
54
|
+
hint: {
|
|
55
|
+
casualPreview: "B\u1EA5m \u201CB\u1EAFt \u0111\u1EA7u ph\xE1t\u201D \u0111\u1EC3 l\xEAn s\xF3ng",
|
|
56
|
+
casualStandby: "Phi\xEAn \u0111\xE3 s\u1EB5n s\xE0ng \u2014 b\u1EA5m \u201CPh\xE1t ngay\u201D",
|
|
57
|
+
paused: "\u0110ang t\u1EA1m d\u1EEBng \u2014 b\u1EA5m \u201CPh\xE1t ti\u1EBFp\u201D \u0111\u1EC3 ti\u1EBFp t\u1EE5c",
|
|
58
|
+
courtPreview: "\u0110ang chu\u1EA9n b\u1ECB camera\u2026",
|
|
59
|
+
courtStandby: (court) => `\u0110ang g\xE1c ${court} \u2014 tr\u1ECDng t\xE0i b\u1EA5m b\u1EAFt \u0111\u1EA7u l\xE0 t\u1EF1 ph\xE1t`,
|
|
60
|
+
courtStandbyNoCourt: "\u0110ang tr\u1EF1c \u2014 ch\u1EDD l\u1EC7nh ph\xE1t",
|
|
61
|
+
connecting: "\u0110ang k\u1EBFt n\u1ED1i t\u1EDBi m\xE1y ch\u1EE7\u2026",
|
|
62
|
+
reconnecting: "M\u1EA5t k\u1EBFt n\u1ED1i \u2014 \u0111ang th\u1EED l\u1EA1i\u2026",
|
|
63
|
+
ending: "\u0110ang d\u1EEBng ph\xE1t\u2026",
|
|
64
|
+
errorFallback: "C\xF3 l\u1ED7i \u2014 th\u1EED l\u1EA1i",
|
|
65
|
+
cameraOpening: "\u0110ang m\u1EDF camera\u2026",
|
|
66
|
+
cameraError: "Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c camera"
|
|
67
|
+
},
|
|
68
|
+
button: {
|
|
69
|
+
goLive: "B\u1EAFt \u0111\u1EA7u ph\xE1t",
|
|
70
|
+
publishNow: "Ph\xE1t ngay",
|
|
71
|
+
resume: "Ph\xE1t ti\u1EBFp",
|
|
72
|
+
stop: "D\u1EEBng",
|
|
73
|
+
stopQuestion: "D\u1EEBng ph\xE1t?",
|
|
74
|
+
confirmStop: "X\xE1c nh\u1EADn d\u1EEBng",
|
|
75
|
+
cancel: "Hu\u1EF7",
|
|
76
|
+
retry: "Th\u1EED l\u1EA1i",
|
|
77
|
+
connecting: "\u0110ang k\u1EBFt n\u1ED1i\u2026",
|
|
78
|
+
ending: "\u0110ang d\u1EEBng\u2026"
|
|
79
|
+
},
|
|
80
|
+
lens: { front: "Tr\u01B0\u1EDBc" },
|
|
81
|
+
thermal: {
|
|
82
|
+
title: "Ti\u1EBFt ki\u1EC7m nhi\u1EC7t & pin",
|
|
83
|
+
subtitle: "Ch\u1EA1m v\xE0o m\xE0n h\xECnh \u0111\u1EC3 b\u1EADt l\u1EA1i giao di\u1EC7n"
|
|
84
|
+
},
|
|
85
|
+
errors: {
|
|
86
|
+
title: {
|
|
87
|
+
PERMISSION_DENIED: "C\u1EA7n quy\u1EC1n Camera v\xE0 Micr\xF4",
|
|
88
|
+
NETWORK_UNAVAILABLE: "Kh\xF4ng c\xF3 k\u1EBFt n\u1ED1i m\u1EA1ng",
|
|
89
|
+
TOKEN_EXPIRED: "Phi\xEAn x\xE1c th\u1EF1c \u0111\xE3 h\u1EBFt h\u1EA1n",
|
|
90
|
+
SDK_UPGRADE_REQUIRED: "C\u1EA7n c\u1EADp nh\u1EADt \u1EE9ng d\u1EE5ng",
|
|
91
|
+
UNSUPPORTED_RUNTIME: "M\xF4i tr\u01B0\u1EDDng ch\u1EA1y kh\xF4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3",
|
|
92
|
+
SESSION_CONFLICT: "\u0110ang c\xF3 phi\xEAn ph\xE1t kh\xE1c",
|
|
93
|
+
RECORDING_FAILED: "Kh\xF4ng th\u1EC3 ghi h\xECnh",
|
|
94
|
+
CONSENT_REQUIRED: "C\u1EA7n \u0111\u1ED3ng \xFD ghi h\xECnh",
|
|
95
|
+
SDK_DISABLED: "T\xEDnh n\u0103ng ph\xE1t tr\u1EF1c ti\u1EBFp \u0111ang t\u1EAFt",
|
|
96
|
+
CONFIGURATION_ERROR: "C\u1EA5u h\xECnh SDK ch\u01B0a \u0111\xFAng",
|
|
97
|
+
INVALID_RESPONSE: "Ph\u1EA3n h\u1ED3i m\xE1y ch\u1EE7 kh\xF4ng h\u1EE3p l\u1EC7",
|
|
98
|
+
TIMEOUT: "Y\xEAu c\u1EA7u qu\xE1 th\u1EDDi gian",
|
|
99
|
+
UNKNOWN: "\u0110\xE3 x\u1EA3y ra l\u1ED7i"
|
|
100
|
+
},
|
|
101
|
+
remediation: {
|
|
102
|
+
PERMISSION_DENIED: "M\u1EDF C\xE0i \u0111\u1EB7t v\xE0 cho ph\xE9p Camera c\xF9ng Micr\xF4, sau \u0111\xF3 th\u1EED l\u1EA1i.",
|
|
103
|
+
NETWORK_UNAVAILABLE: "Ki\u1EC3m tra Wi-Fi ho\u1EB7c d\u1EEF li\u1EC7u di \u0111\u1ED9ng r\u1ED3i th\u1EED l\u1EA1i.",
|
|
104
|
+
TOKEN_EXPIRED: "Phi\xEAn x\xE1c th\u1EF1c \u0111\xE3 h\u1EBFt h\u1EA1n. H\xE3y k\u1EBFt n\u1ED1i m\u1EA1ng v\xE0 th\u1EED l\u1EA1i.",
|
|
105
|
+
SDK_UPGRADE_REQUIRED: "C\u1EADp nh\u1EADt \u1EE9ng d\u1EE5ng l\xEAn b\u1EA3n m\u1EDBi nh\u1EA5t v\xE0 c\xE0i l\u1EA1i Development Build.",
|
|
106
|
+
UNSUPPORTED_RUNTIME: "C\xE0i Development Build tr\xEAn iOS ho\u1EB7c Android; Expo Go v\xE0 web kh\xF4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3.",
|
|
107
|
+
SESSION_CONFLICT: "K\u1EBFt th\xFAc phi\xEAn ph\xE1t \u0111ang ho\u1EA1t \u0111\u1ED9ng tr\u01B0\u1EDBc khi b\u1EAFt \u0111\u1EA7u phi\xEAn m\u1EDBi.",
|
|
108
|
+
RECORDING_FAILED: "Kh\xF4ng th\u1EC3 t\u1EA1o b\u1EA3n ghi m\xE1y ch\u1EE7. H\xE3y th\u1EED l\u1EA1i ho\u1EB7c li\xEAn h\u1EC7 h\u1ED7 tr\u1EE3.",
|
|
109
|
+
CONSENT_REQUIRED: "\u0110\u1ED3ng \xFD ghi h\xECnh tr\u01B0\u1EDBc khi b\u1EAFt \u0111\u1EA7u ph\xE1t tr\u1EF1c ti\u1EBFp.",
|
|
110
|
+
SDK_DISABLED: "T\xEDnh n\u0103ng \u0111ang b\u1ECB t\u1EAFt cho \u1EE9ng d\u1EE5ng n\xE0y. H\xE3y li\xEAn h\u1EC7 qu\u1EA3n tr\u1ECB vi\xEAn.",
|
|
111
|
+
CONFIGURATION_ERROR: "Ki\u1EC3m tra c\u1EA5u h\xECnh Provider v\xE0 config plugin, sau \u0111\xF3 rebuild \u1EE9ng d\u1EE5ng.",
|
|
112
|
+
INVALID_RESPONSE: "M\xE1y ch\u1EE7 tr\u1EA3 v\u1EC1 d\u1EEF li\u1EC7u kh\xF4ng h\u1EE3p l\u1EC7. H\xE3y th\u1EED l\u1EA1i ho\u1EB7c li\xEAn h\u1EC7 h\u1ED7 tr\u1EE3.",
|
|
113
|
+
TIMEOUT: "Y\xEAu c\u1EA7u qu\xE1 th\u1EDDi gian. Ki\u1EC3m tra k\u1EBFt n\u1ED1i r\u1ED3i th\u1EED l\u1EA1i.",
|
|
114
|
+
UNKNOWN: "Th\u1EED l\u1EA1i. N\u1EBFu l\u1ED7i ti\u1EBFp di\u1EC5n, h\xE3y li\xEAn h\u1EC7 h\u1ED7 tr\u1EE3."
|
|
115
|
+
},
|
|
116
|
+
technicalDetail: "Chi ti\u1EBFt k\u1EF9 thu\u1EADt"
|
|
117
|
+
},
|
|
118
|
+
sheet: {
|
|
119
|
+
title: "C\u1EA5u h\xECnh camera",
|
|
120
|
+
close: "\u0110\xF3ng",
|
|
121
|
+
closeBackdrop: "\u0110\xF3ng c\u1EA5u h\xECnh",
|
|
122
|
+
resolution: "\u0110\u1ED9 ph\xE2n gi\u1EA3i",
|
|
123
|
+
fps: "FPS",
|
|
124
|
+
fps60Note: "60fps g\u1EA5p \u0111\xF4i bitrate v\xE0 to\u1EA3 nhi\u1EC7t \u2014 ch\u1EC9 n\xEAn d\xF9ng khi s\xE2n \u0111\u1EE7 s\xE1ng v\xE0 m\u1EA1ng kho\u1EBB.",
|
|
125
|
+
antiFlicker: "Ch\u1ED1ng ch\u1EDBp \u0111\xE8n",
|
|
126
|
+
antiFlickerNote: "\u0110i\u1EC7n VN 50Hz \u2014 \u0111\xE8n trong nh\xE0 th\u1EA5y v\u1EB1n s\xE1ng t\u1ED1i hay h\xECnh ph\u1EADp ph\u1ED3ng th\xEC gi\u1EEF 50Hz.",
|
|
127
|
+
auto: "Auto",
|
|
128
|
+
lockedNote: "\u0110ang trong phi\xEAn \u2014 \u0111\u1ED5i \u0111\u1ED9 ph\xE2n gi\u1EA3i/FPS/camera sau khi d\u1EEBng ph\xE1t.",
|
|
129
|
+
defaultCamera: "Camera m\u1EB7c \u0111\u1ECBnh",
|
|
130
|
+
autoCamera: "T\u1EF1 \u0111\u1ED9ng",
|
|
131
|
+
autoCameraHint: "Camera sau m\u1EB7c \u0111\u1ECBnh c\u1EE7a m\xE1y",
|
|
132
|
+
frontCamera: "Camera tr\u01B0\u1EDBc",
|
|
133
|
+
backCamera: "Camera sau",
|
|
134
|
+
cameraIdHint: (id) => `id ${id}`,
|
|
135
|
+
zoom: "Zoom",
|
|
136
|
+
ev: "EV",
|
|
137
|
+
focusLock: "Kho\xE1 n\xE9t",
|
|
138
|
+
exposureLock: "Kho\xE1 s\xE1ng",
|
|
139
|
+
torch: "\u0110\xE8n pin",
|
|
140
|
+
network: "M\u1EA1ng",
|
|
141
|
+
runCheck: "Ki\u1EC3m tra t\u1ED1c \u0111\u1ED9 m\u1EA1ng",
|
|
142
|
+
running: "\u0110ang \u0111o t\u1EA3i l\xEAn\u2026 (~10 gi\xE2y)",
|
|
143
|
+
busyNote: "\u0110ang ph\xE1t \u2014 \u0111o l\xFAc n\xE0y s\u1EBD gi\xE0nh b\u0103ng th\xF4ng v\u1EDBi s\xF3ng. D\u1EEBng ph\xE1t r\u1ED3i \u0111o.",
|
|
144
|
+
resultLine: ({ uploadMbps, requiredMbps, resolution, fps, pingMs }) => `T\u1EA3i l\xEAn ${uploadMbps.toFixed(1)} Mbps \xB7 c\u1EA7n ~${requiredMbps.toFixed(1)} Mbps cho ${resolution}p${fps}` + (pingMs !== null ? ` \xB7 ping ${pingMs}ms` : ""),
|
|
145
|
+
good: "\u0110\u1EE6 T\u1ED0T \u2014 d\u01B0 b\u0103ng th\xF4ng cho c\u1EA5u h\xECnh n\xE0y.",
|
|
146
|
+
ok: "T\u1EA0M \u0110\u1EE6 \u2014 m\u1EA1ng ch\xF9ng l\xE0 ch\u1EA5t l\u01B0\u1EE3ng ph\u1EA3i h\u1EA1 theo.",
|
|
147
|
+
low: "KH\xD4NG \u0110\u1EE6 \u2014 h\u1EA1 \u0111\u1ED9 ph\xE2n gi\u1EA3i ho\u1EB7c \u0111\u1ED5i m\u1EA1ng tr\u01B0\u1EDBc khi l\xEAn s\xF3ng.",
|
|
148
|
+
checkError: "Kh\xF4ng \u0111o \u0111\u01B0\u1EE3c \u2014 ki\u1EC3m tra k\u1EBFt n\u1ED1i m\u1EA1ng r\u1ED3i th\u1EED l\u1EA1i."
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var en = {
|
|
152
|
+
pill: {
|
|
153
|
+
opening: "OPENING CAMERA\u2026",
|
|
154
|
+
ready: "READY",
|
|
155
|
+
connecting: "CONNECTING\u2026",
|
|
156
|
+
paused: "PAUSED",
|
|
157
|
+
court: "ON COURT",
|
|
158
|
+
standby: "READY TO GO LIVE",
|
|
159
|
+
live: "LIVE",
|
|
160
|
+
reconnecting: "RECONNECTING\u2026",
|
|
161
|
+
ending: "STOPPING\u2026",
|
|
162
|
+
error: "ERROR"
|
|
163
|
+
},
|
|
164
|
+
hint: {
|
|
165
|
+
casualPreview: "Tap \u201CGo live\u201D to start streaming",
|
|
166
|
+
casualStandby: "Session ready \u2014 tap \u201CGo live now\u201D",
|
|
167
|
+
paused: "Paused \u2014 tap \u201CResume\u201D to continue",
|
|
168
|
+
courtPreview: "Preparing the camera\u2026",
|
|
169
|
+
courtStandby: (court) => `Standing by on ${court} \u2014 the referee's start button goes live automatically`,
|
|
170
|
+
courtStandbyNoCourt: "Standing by \u2014 waiting for the start command",
|
|
171
|
+
connecting: "Connecting to the server\u2026",
|
|
172
|
+
reconnecting: "Connection lost \u2014 retrying\u2026",
|
|
173
|
+
ending: "Stopping the stream\u2026",
|
|
174
|
+
errorFallback: "Something went wrong \u2014 retry",
|
|
175
|
+
cameraOpening: "Opening the camera\u2026",
|
|
176
|
+
cameraError: "The camera could not be opened"
|
|
177
|
+
},
|
|
178
|
+
button: {
|
|
179
|
+
goLive: "Go live",
|
|
180
|
+
publishNow: "Go live now",
|
|
181
|
+
resume: "Resume",
|
|
182
|
+
stop: "Stop",
|
|
183
|
+
stopQuestion: "Stop streaming?",
|
|
184
|
+
confirmStop: "Yes, stop",
|
|
185
|
+
cancel: "Cancel",
|
|
186
|
+
retry: "Retry",
|
|
187
|
+
connecting: "Connecting\u2026",
|
|
188
|
+
ending: "Stopping\u2026"
|
|
189
|
+
},
|
|
190
|
+
lens: { front: "Front" },
|
|
191
|
+
thermal: {
|
|
192
|
+
title: "Saving heat & battery",
|
|
193
|
+
subtitle: "Tap the screen to show the controls again"
|
|
194
|
+
},
|
|
195
|
+
errors: {
|
|
196
|
+
title: {
|
|
197
|
+
PERMISSION_DENIED: "Camera and Microphone access required",
|
|
198
|
+
NETWORK_UNAVAILABLE: "No network connection",
|
|
199
|
+
TOKEN_EXPIRED: "Authentication expired",
|
|
200
|
+
SDK_UPGRADE_REQUIRED: "App update required",
|
|
201
|
+
UNSUPPORTED_RUNTIME: "Unsupported runtime",
|
|
202
|
+
SESSION_CONFLICT: "Another livestream is active",
|
|
203
|
+
RECORDING_FAILED: "Recording failed",
|
|
204
|
+
CONSENT_REQUIRED: "Recording consent required",
|
|
205
|
+
SDK_DISABLED: "Livestreaming is disabled",
|
|
206
|
+
CONFIGURATION_ERROR: "SDK configuration error",
|
|
207
|
+
INVALID_RESPONSE: "Invalid server response",
|
|
208
|
+
TIMEOUT: "Request timed out",
|
|
209
|
+
UNKNOWN: "Something went wrong"
|
|
210
|
+
},
|
|
211
|
+
remediation: {
|
|
212
|
+
PERMISSION_DENIED: "Open Settings and allow Camera and Microphone, then try again.",
|
|
213
|
+
NETWORK_UNAVAILABLE: "Check Wi-Fi or mobile data, then try again.",
|
|
214
|
+
TOKEN_EXPIRED: "Your authentication has expired. Connect to the network and try again.",
|
|
215
|
+
SDK_UPGRADE_REQUIRED: "Update the app to the latest version and install a new Development Build.",
|
|
216
|
+
UNSUPPORTED_RUNTIME: "Install an iOS or Android Development Build; Expo Go and web are unsupported.",
|
|
217
|
+
SESSION_CONFLICT: "End the active livestream before starting another one.",
|
|
218
|
+
RECORDING_FAILED: "The server recording could not be created. Try again or contact support.",
|
|
219
|
+
CONSENT_REQUIRED: "Accept recording consent before going live.",
|
|
220
|
+
SDK_DISABLED: "Livestreaming is disabled for this app. Contact an administrator.",
|
|
221
|
+
CONFIGURATION_ERROR: "Check the Provider and config plugin setup, then rebuild the app.",
|
|
222
|
+
INVALID_RESPONSE: "The server returned invalid data. Try again or contact support.",
|
|
223
|
+
TIMEOUT: "The request timed out. Check your connection and try again.",
|
|
224
|
+
UNKNOWN: "Try again. If the problem continues, contact support."
|
|
225
|
+
},
|
|
226
|
+
technicalDetail: "Technical details"
|
|
227
|
+
},
|
|
228
|
+
sheet: {
|
|
229
|
+
title: "Camera settings",
|
|
230
|
+
close: "Close",
|
|
231
|
+
closeBackdrop: "Close settings",
|
|
232
|
+
resolution: "Resolution",
|
|
233
|
+
fps: "FPS",
|
|
234
|
+
fps60Note: "60 fps doubles the bitrate and heat \u2014 only for bright courts on a strong network.",
|
|
235
|
+
antiFlicker: "Anti-flicker",
|
|
236
|
+
antiFlickerNote: "Match the local mains frequency (VN: 50 Hz). Banding or pulsing indoor light means the wrong setting.",
|
|
237
|
+
auto: "Auto",
|
|
238
|
+
lockedNote: "In a session \u2014 change resolution/FPS/camera after stopping.",
|
|
239
|
+
defaultCamera: "Default camera",
|
|
240
|
+
autoCamera: "Automatic",
|
|
241
|
+
autoCameraHint: "The device's default back camera",
|
|
242
|
+
frontCamera: "Front camera",
|
|
243
|
+
backCamera: "Back camera",
|
|
244
|
+
cameraIdHint: (id) => `id ${id}`,
|
|
245
|
+
zoom: "Zoom",
|
|
246
|
+
ev: "EV",
|
|
247
|
+
focusLock: "Lock focus",
|
|
248
|
+
exposureLock: "Lock exposure",
|
|
249
|
+
torch: "Torch",
|
|
250
|
+
network: "Network",
|
|
251
|
+
runCheck: "Check upload speed",
|
|
252
|
+
running: "Measuring upload\u2026 (~10 s)",
|
|
253
|
+
busyNote: "Streaming \u2014 measuring now would compete with the stream. Stop first.",
|
|
254
|
+
resultLine: ({ uploadMbps, requiredMbps, resolution, fps, pingMs }) => `Upload ${uploadMbps.toFixed(1)} Mbps \xB7 needs ~${requiredMbps.toFixed(1)} Mbps for ${resolution}p${fps}` + (pingMs !== null ? ` \xB7 ping ${pingMs} ms` : ""),
|
|
255
|
+
good: "GOOD \u2014 plenty of headroom for this configuration.",
|
|
256
|
+
ok: "MARGINAL \u2014 quality will drop when the network dips.",
|
|
257
|
+
low: "NOT ENOUGH \u2014 lower the resolution or switch networks before going live.",
|
|
258
|
+
checkError: "Could not measure \u2014 check the connection and try again."
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var uiCopy = { vi, en };
|
|
262
|
+
|
|
263
|
+
// src/ui/hooks.ts
|
|
264
|
+
import { useEffect, useState } from "react";
|
|
265
|
+
function useCameraCapabilities(active, cameraKey) {
|
|
266
|
+
const [capabilities, setCapabilities] = useState(null);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (!active) return;
|
|
269
|
+
let cancelled = false;
|
|
270
|
+
void getCameraCapabilities().then((value) => {
|
|
271
|
+
if (!cancelled) setCapabilities(value);
|
|
272
|
+
});
|
|
273
|
+
return () => {
|
|
274
|
+
cancelled = true;
|
|
275
|
+
};
|
|
276
|
+
}, [active, cameraKey]);
|
|
277
|
+
return capabilities;
|
|
278
|
+
}
|
|
279
|
+
function useThermalDim(state, afterMs) {
|
|
280
|
+
const [dimmed, setDimmed] = useState(false);
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (state !== "live" || afterMs === null) {
|
|
283
|
+
setDimmed(false);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const timer = setTimeout(() => setDimmed(true), afterMs);
|
|
287
|
+
return () => clearTimeout(timer);
|
|
288
|
+
}, [state, afterMs, dimmed]);
|
|
289
|
+
return [dimmed, setDimmed];
|
|
290
|
+
}
|
|
291
|
+
function useAntiFlicker(mode) {
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!mode) return;
|
|
294
|
+
void applyAntiFlicker(mode);
|
|
295
|
+
}, [mode]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/ui/primitives.tsx
|
|
299
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
300
|
+
|
|
301
|
+
// src/ui/helpers.ts
|
|
302
|
+
var PILL_COLORS = Object.freeze({
|
|
303
|
+
gray: "#6b7280",
|
|
304
|
+
green: "#22c55e",
|
|
305
|
+
amber: "#f59e0b",
|
|
306
|
+
orange: "#f97316",
|
|
307
|
+
red: "#ef4444",
|
|
308
|
+
darkRed: "#b91c1c"
|
|
309
|
+
});
|
|
310
|
+
function statusPillFor(state, options = {}) {
|
|
311
|
+
const copy = uiCopy[options.locale ?? "vi"].pill;
|
|
312
|
+
const elapsedMs = options.elapsedMs ?? 0;
|
|
313
|
+
switch (state) {
|
|
314
|
+
case "idle":
|
|
315
|
+
case "preparing":
|
|
316
|
+
return { label: copy.opening, color: PILL_COLORS.gray };
|
|
317
|
+
case "preview":
|
|
318
|
+
return { label: copy.ready, color: PILL_COLORS.green };
|
|
319
|
+
case "connecting":
|
|
320
|
+
return { label: copy.connecting, color: PILL_COLORS.amber };
|
|
321
|
+
case "standby":
|
|
322
|
+
if (elapsedMs > 0) return { label: copy.paused, color: PILL_COLORS.amber };
|
|
323
|
+
return options.courtBound ? { label: copy.court, color: PILL_COLORS.orange } : { label: copy.standby, color: PILL_COLORS.orange };
|
|
324
|
+
case "live":
|
|
325
|
+
return { label: copy.live, color: PILL_COLORS.red };
|
|
326
|
+
case "reconnecting":
|
|
327
|
+
return { label: copy.reconnecting, color: PILL_COLORS.amber };
|
|
328
|
+
case "ending":
|
|
329
|
+
return { label: copy.ending, color: PILL_COLORS.gray };
|
|
330
|
+
case "error":
|
|
331
|
+
return { label: copy.error, color: PILL_COLORS.darkRed };
|
|
332
|
+
// "ended" thoáng qua rồi AutoPrepare đưa về preview.
|
|
333
|
+
default:
|
|
334
|
+
return { label: copy.ready, color: PILL_COLORS.green };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function shortSessionId(sessionId) {
|
|
338
|
+
return sessionId.length <= 10 ? sessionId : `\u2026${sessionId.slice(-8)}`;
|
|
339
|
+
}
|
|
340
|
+
function formatElapsed(ms) {
|
|
341
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
342
|
+
const hours = Math.floor(total / 3600);
|
|
343
|
+
const minutes = Math.floor(total % 3600 / 60);
|
|
344
|
+
const seconds = total % 60;
|
|
345
|
+
const mm = String(minutes).padStart(2, "0");
|
|
346
|
+
const ss = String(seconds).padStart(2, "0");
|
|
347
|
+
return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;
|
|
348
|
+
}
|
|
349
|
+
function lensShortLabel(label) {
|
|
350
|
+
if (/ultra\s*wide/i.test(label)) return "0.5x";
|
|
351
|
+
if (/telephoto/i.test(label)) return "Tele";
|
|
352
|
+
return "1x";
|
|
353
|
+
}
|
|
354
|
+
function isRtmpPreview(previewTrack) {
|
|
355
|
+
return typeof previewTrack === "object" && previewTrack !== null && previewTrack.kind === "rtmp";
|
|
356
|
+
}
|
|
357
|
+
function activeLensId(cameras, selectedCameraId) {
|
|
358
|
+
if (selectedCameraId) return selectedCameraId;
|
|
359
|
+
const backLenses = cameras.filter((camera) => camera.facing === "environment");
|
|
360
|
+
return backLenses.find((camera) => lensShortLabel(camera.label) === "1x")?.deviceId ?? null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/ui/primitives.tsx
|
|
364
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
365
|
+
var uiColors = Object.freeze({
|
|
366
|
+
ink: "#0a0a0b",
|
|
367
|
+
text: "#ffffff",
|
|
368
|
+
textSecondary: "#e5e7eb",
|
|
369
|
+
textMuted: "#d1d5db",
|
|
370
|
+
textDim: "#9ca3af",
|
|
371
|
+
border: "#3f3f46",
|
|
372
|
+
panel: "rgba(15,15,19,0.93)",
|
|
373
|
+
chip: "rgba(0,0,0,0.55)",
|
|
374
|
+
chipStrong: "rgba(0,0,0,0.72)",
|
|
375
|
+
surface: "rgba(255,255,255,0.06)",
|
|
376
|
+
hairline: "rgba(255,255,255,0.08)",
|
|
377
|
+
good: "#4ade80",
|
|
378
|
+
ok: "#facc15",
|
|
379
|
+
low: "#f87171"
|
|
380
|
+
});
|
|
381
|
+
function StatusPill({
|
|
382
|
+
label,
|
|
383
|
+
color,
|
|
384
|
+
style
|
|
385
|
+
}) {
|
|
386
|
+
return /* @__PURE__ */ jsx(View, { style: [uiStyles.pill, { backgroundColor: color }, style], accessibilityRole: "text", children: /* @__PURE__ */ jsx(Text, { style: uiStyles.pillText, children: label }) });
|
|
387
|
+
}
|
|
388
|
+
function ControlRow({ label, children }) {
|
|
389
|
+
return /* @__PURE__ */ jsxs(View, { style: uiStyles.row, children: [
|
|
390
|
+
/* @__PURE__ */ jsx(Text, { style: uiStyles.rowLabel, children: label }),
|
|
391
|
+
children
|
|
392
|
+
] });
|
|
393
|
+
}
|
|
394
|
+
function Note({ children }) {
|
|
395
|
+
return /* @__PURE__ */ jsx(Text, { style: uiStyles.note, children });
|
|
396
|
+
}
|
|
397
|
+
function ActionButton({
|
|
398
|
+
label,
|
|
399
|
+
onPress,
|
|
400
|
+
disabled,
|
|
401
|
+
accessibilityLabel
|
|
402
|
+
}) {
|
|
403
|
+
return /* @__PURE__ */ jsx(
|
|
404
|
+
Pressable,
|
|
405
|
+
{
|
|
406
|
+
accessibilityRole: "button",
|
|
407
|
+
accessibilityLabel: accessibilityLabel ?? label,
|
|
408
|
+
disabled,
|
|
409
|
+
onPress,
|
|
410
|
+
style: [uiStyles.actionButton, disabled && uiStyles.disabled],
|
|
411
|
+
children: /* @__PURE__ */ jsx(Text, { style: uiStyles.actionButtonText, children: label })
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
function CameraOption({
|
|
416
|
+
label,
|
|
417
|
+
hint,
|
|
418
|
+
active,
|
|
419
|
+
disabled,
|
|
420
|
+
onPress
|
|
421
|
+
}) {
|
|
422
|
+
return /* @__PURE__ */ jsxs(
|
|
423
|
+
Pressable,
|
|
424
|
+
{
|
|
425
|
+
accessibilityRole: "button",
|
|
426
|
+
accessibilityState: { selected: active, disabled: disabled === true },
|
|
427
|
+
onPress,
|
|
428
|
+
disabled,
|
|
429
|
+
style: [uiStyles.cameraOption, active && uiStyles.cameraOptionActive],
|
|
430
|
+
children: [
|
|
431
|
+
/* @__PURE__ */ jsx(Text, { style: [uiStyles.cameraOptionText, active && uiStyles.cameraOptionTextActive], children: label }),
|
|
432
|
+
/* @__PURE__ */ jsx(Text, { style: [uiStyles.cameraOptionHint, active && uiStyles.cameraOptionTextActive], children: hint })
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
function Segmented({
|
|
438
|
+
options,
|
|
439
|
+
value,
|
|
440
|
+
disabled,
|
|
441
|
+
onChange
|
|
442
|
+
}) {
|
|
443
|
+
return /* @__PURE__ */ jsx(View, { style: [uiStyles.segmented, disabled && uiStyles.disabled], children: options.map((option) => {
|
|
444
|
+
const active = option.value === value;
|
|
445
|
+
return /* @__PURE__ */ jsx(
|
|
446
|
+
Pressable,
|
|
447
|
+
{
|
|
448
|
+
accessibilityRole: "button",
|
|
449
|
+
accessibilityState: { selected: active, disabled: disabled === true },
|
|
450
|
+
disabled,
|
|
451
|
+
onPress: () => onChange(option.value),
|
|
452
|
+
style: [uiStyles.segment, active && uiStyles.segmentActive],
|
|
453
|
+
children: /* @__PURE__ */ jsx(Text, { style: [uiStyles.segmentText, active && uiStyles.segmentTextActive], children: option.label })
|
|
454
|
+
},
|
|
455
|
+
String(option.value)
|
|
456
|
+
);
|
|
457
|
+
}) });
|
|
458
|
+
}
|
|
459
|
+
function PresetButton({
|
|
460
|
+
label,
|
|
461
|
+
active,
|
|
462
|
+
onPress
|
|
463
|
+
}) {
|
|
464
|
+
return /* @__PURE__ */ jsx(
|
|
465
|
+
Pressable,
|
|
466
|
+
{
|
|
467
|
+
accessibilityRole: "button",
|
|
468
|
+
accessibilityState: { selected: active },
|
|
469
|
+
onPress,
|
|
470
|
+
style: [uiStyles.preset, active && uiStyles.presetActive],
|
|
471
|
+
children: /* @__PURE__ */ jsx(Text, { style: [uiStyles.presetText, active && uiStyles.presetTextActive], children: label })
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
function LensRow({
|
|
476
|
+
cameras,
|
|
477
|
+
selectedCameraId,
|
|
478
|
+
onSelect,
|
|
479
|
+
locale = "vi"
|
|
480
|
+
}) {
|
|
481
|
+
const backLenses = cameras.filter((camera) => camera.facing === "environment");
|
|
482
|
+
const frontLens = cameras.find((camera) => camera.facing === "front");
|
|
483
|
+
const active = activeLensId(cameras, selectedCameraId);
|
|
484
|
+
if (backLenses.length === 0 && !frontLens) return null;
|
|
485
|
+
return /* @__PURE__ */ jsxs(View, { style: uiStyles.lensRow, children: [
|
|
486
|
+
backLenses.map((camera) => {
|
|
487
|
+
const isActive = camera.deviceId === active;
|
|
488
|
+
return /* @__PURE__ */ jsx(
|
|
489
|
+
Pressable,
|
|
490
|
+
{
|
|
491
|
+
accessibilityRole: "button",
|
|
492
|
+
accessibilityState: { selected: isActive },
|
|
493
|
+
onPress: () => onSelect(camera.deviceId),
|
|
494
|
+
style: [uiStyles.lensButton, isActive && uiStyles.lensButtonActive],
|
|
495
|
+
children: /* @__PURE__ */ jsx(Text, { style: [uiStyles.lensText, isActive && uiStyles.lensTextActive], children: lensShortLabel(camera.label) })
|
|
496
|
+
},
|
|
497
|
+
camera.deviceId
|
|
498
|
+
);
|
|
499
|
+
}),
|
|
500
|
+
frontLens ? /* @__PURE__ */ jsx(
|
|
501
|
+
Pressable,
|
|
502
|
+
{
|
|
503
|
+
accessibilityRole: "button",
|
|
504
|
+
accessibilityState: { selected: frontLens.deviceId === selectedCameraId },
|
|
505
|
+
onPress: () => onSelect(frontLens.deviceId),
|
|
506
|
+
style: [
|
|
507
|
+
uiStyles.lensButton,
|
|
508
|
+
frontLens.deviceId === selectedCameraId && uiStyles.lensButtonActive
|
|
509
|
+
],
|
|
510
|
+
children: /* @__PURE__ */ jsx(
|
|
511
|
+
Text,
|
|
512
|
+
{
|
|
513
|
+
style: [
|
|
514
|
+
uiStyles.lensText,
|
|
515
|
+
frontLens.deviceId === selectedCameraId && uiStyles.lensTextActive
|
|
516
|
+
],
|
|
517
|
+
children: uiCopy[locale].lens.front
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
},
|
|
521
|
+
frontLens.deviceId
|
|
522
|
+
) : null
|
|
523
|
+
] });
|
|
524
|
+
}
|
|
525
|
+
var uiStyles = StyleSheet.create({
|
|
526
|
+
pill: { borderRadius: 999, paddingHorizontal: 10, paddingVertical: 5 },
|
|
527
|
+
pillText: {
|
|
528
|
+
color: uiColors.text,
|
|
529
|
+
fontWeight: "800",
|
|
530
|
+
fontSize: 11,
|
|
531
|
+
letterSpacing: 0.5,
|
|
532
|
+
fontVariant: ["tabular-nums"]
|
|
533
|
+
},
|
|
534
|
+
row: {
|
|
535
|
+
flexDirection: "row",
|
|
536
|
+
alignItems: "center",
|
|
537
|
+
justifyContent: "space-between",
|
|
538
|
+
gap: 10,
|
|
539
|
+
minHeight: 32
|
|
540
|
+
},
|
|
541
|
+
rowLabel: { color: uiColors.textSecondary, fontSize: 13, fontWeight: "600" },
|
|
542
|
+
note: { color: uiColors.textDim, fontSize: 11, lineHeight: 15 },
|
|
543
|
+
actionButton: {
|
|
544
|
+
borderRadius: 9,
|
|
545
|
+
backgroundColor: uiColors.surface,
|
|
546
|
+
borderWidth: 1,
|
|
547
|
+
borderColor: uiColors.border,
|
|
548
|
+
paddingHorizontal: 12,
|
|
549
|
+
paddingVertical: 8,
|
|
550
|
+
alignSelf: "flex-start"
|
|
551
|
+
},
|
|
552
|
+
actionButtonText: { color: uiColors.textSecondary, fontSize: 12, fontWeight: "700" },
|
|
553
|
+
cameraOption: {
|
|
554
|
+
flexDirection: "row",
|
|
555
|
+
alignItems: "center",
|
|
556
|
+
justifyContent: "space-between",
|
|
557
|
+
gap: 8,
|
|
558
|
+
backgroundColor: uiColors.surface,
|
|
559
|
+
borderRadius: 9,
|
|
560
|
+
paddingHorizontal: 12,
|
|
561
|
+
paddingVertical: 8
|
|
562
|
+
},
|
|
563
|
+
cameraOptionActive: { backgroundColor: uiColors.text },
|
|
564
|
+
cameraOptionText: { color: uiColors.textMuted, fontSize: 12, fontWeight: "700" },
|
|
565
|
+
cameraOptionHint: { color: uiColors.textDim, fontSize: 11, flexShrink: 1 },
|
|
566
|
+
cameraOptionTextActive: { color: uiColors.ink },
|
|
567
|
+
segmented: {
|
|
568
|
+
flexDirection: "row",
|
|
569
|
+
backgroundColor: uiColors.surface,
|
|
570
|
+
borderRadius: 999,
|
|
571
|
+
padding: 3
|
|
572
|
+
},
|
|
573
|
+
segment: { borderRadius: 999, paddingHorizontal: 13, paddingVertical: 6 },
|
|
574
|
+
segmentActive: { backgroundColor: uiColors.text },
|
|
575
|
+
segmentText: { color: uiColors.textMuted, fontSize: 12, fontWeight: "700" },
|
|
576
|
+
segmentTextActive: { color: uiColors.ink },
|
|
577
|
+
disabled: { opacity: 0.4 },
|
|
578
|
+
presetRow: { flexDirection: "row", gap: 6, flexWrap: "wrap" },
|
|
579
|
+
preset: {
|
|
580
|
+
borderRadius: 999,
|
|
581
|
+
borderWidth: 1,
|
|
582
|
+
borderColor: uiColors.border,
|
|
583
|
+
paddingHorizontal: 12,
|
|
584
|
+
paddingVertical: 6
|
|
585
|
+
},
|
|
586
|
+
presetActive: { backgroundColor: uiColors.text, borderColor: uiColors.text },
|
|
587
|
+
presetText: { color: uiColors.textMuted, fontSize: 12, fontWeight: "700" },
|
|
588
|
+
presetTextActive: { color: uiColors.ink },
|
|
589
|
+
lensRow: {
|
|
590
|
+
flexDirection: "row",
|
|
591
|
+
gap: 8,
|
|
592
|
+
backgroundColor: uiColors.chip,
|
|
593
|
+
borderRadius: 999,
|
|
594
|
+
padding: 4
|
|
595
|
+
},
|
|
596
|
+
lensButton: { borderRadius: 999, paddingHorizontal: 14, paddingVertical: 7 },
|
|
597
|
+
lensButtonActive: { backgroundColor: uiColors.text },
|
|
598
|
+
lensText: { color: uiColors.textSecondary, fontSize: 13, fontWeight: "700" },
|
|
599
|
+
lensTextActive: { color: uiColors.ink }
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// src/ui/controls-sheet.tsx
|
|
603
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
604
|
+
var ZOOM_PRESETS = [1, 2, 3, 5];
|
|
605
|
+
var EV_PRESETS = [-2, -1, 0, 1, 2];
|
|
606
|
+
var FPS_CHOICES = [
|
|
607
|
+
{ value: 24, label: "24" },
|
|
608
|
+
{ value: 30, label: "30" },
|
|
609
|
+
{ value: 60, label: "60" }
|
|
610
|
+
];
|
|
611
|
+
var IN_SESSION = /* @__PURE__ */ new Set(["connecting", "standby", "live", "reconnecting", "ending"]);
|
|
612
|
+
var UPLINK_BUSY = /* @__PURE__ */ new Set(["connecting", "live", "reconnecting", "ending"]);
|
|
613
|
+
function CameraControlsSheet(props) {
|
|
614
|
+
const { visible, onClose, settings, uplink, footer } = props;
|
|
615
|
+
const sections = props.sections ?? {};
|
|
616
|
+
const show = (key) => sections[key] !== false;
|
|
617
|
+
const live = useLivestream();
|
|
618
|
+
const locale = props.locale ?? live.locale;
|
|
619
|
+
const copy = uiCopy[locale].sheet;
|
|
620
|
+
const config = useCameraSettings(settings);
|
|
621
|
+
const { width, height } = useWindowDimensions();
|
|
622
|
+
const side = props.layout === "side" || props.layout !== "bottom" && width > height;
|
|
623
|
+
const caps = useCameraCapabilities(visible, live.selectedCameraId);
|
|
624
|
+
const [zoom, setZoomValue] = useState2(1);
|
|
625
|
+
const [ev, setEvValue] = useState2(0);
|
|
626
|
+
const [focusLocked, setFocusLockedValue] = useState2(false);
|
|
627
|
+
const [exposureLocked, setExposureLockedValue] = useState2(false);
|
|
628
|
+
const [torch, setTorchValue] = useState2(false);
|
|
629
|
+
const [net, setNet] = useState2({ running: false, result: null, error: null });
|
|
630
|
+
const configLocked = IN_SESSION.has(live.state);
|
|
631
|
+
const netBusy = UPLINK_BUSY.has(live.state);
|
|
632
|
+
const supportedFps = caps?.supportedFrameRates ?? [24, 30];
|
|
633
|
+
const fpsOptions = FPS_CHOICES.filter((choice) => supportedFps.includes(choice.value));
|
|
634
|
+
useEffect2(() => {
|
|
635
|
+
if (configLocked || !caps) return;
|
|
636
|
+
if (!supportedFps.includes(config.fps)) settings.set({ fps: 30 });
|
|
637
|
+
}, [caps, config.fps, configLocked, settings, supportedFps]);
|
|
638
|
+
async function runNetworkCheck() {
|
|
639
|
+
setNet({ running: true, result: null, error: null });
|
|
640
|
+
try {
|
|
641
|
+
const result = await runUplinkCheck({
|
|
642
|
+
resolution: config.resolution,
|
|
643
|
+
...uplink?.uploadUrl ? { uploadUrl: uplink.uploadUrl } : {},
|
|
644
|
+
...uplink?.pingUrl ? { pingUrl: uplink.pingUrl } : {}
|
|
645
|
+
});
|
|
646
|
+
setNet({ running: false, result, error: null });
|
|
647
|
+
} catch {
|
|
648
|
+
setNet({ running: false, result: null, error: copy.checkError });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return (
|
|
652
|
+
// supportedOrientations đủ cả dọc lẫn ngang: mặc định của Modal là
|
|
653
|
+
// ["portrait"], app khoá landscape (Info.plist chỉ có LandscapeLeft/Right)
|
|
654
|
+
// → iOS không tìm được hướng chung và ABORT ngay khi mount modal
|
|
655
|
+
// ("Supported orientations has no common orientation with the application").
|
|
656
|
+
/* @__PURE__ */ jsx2(
|
|
657
|
+
Modal,
|
|
658
|
+
{
|
|
659
|
+
visible,
|
|
660
|
+
transparent: true,
|
|
661
|
+
animationType: "fade",
|
|
662
|
+
onRequestClose: onClose,
|
|
663
|
+
statusBarTranslucent: true,
|
|
664
|
+
navigationBarTranslucent: true,
|
|
665
|
+
supportedOrientations: [
|
|
666
|
+
"portrait",
|
|
667
|
+
"portrait-upside-down",
|
|
668
|
+
"landscape",
|
|
669
|
+
"landscape-left",
|
|
670
|
+
"landscape-right"
|
|
671
|
+
],
|
|
672
|
+
children: /* @__PURE__ */ jsxs2(View2, { style: side ? styles.overlaySide : styles.overlayBottom, children: [
|
|
673
|
+
/* @__PURE__ */ jsx2(
|
|
674
|
+
Pressable2,
|
|
675
|
+
{
|
|
676
|
+
style: styles.backdrop,
|
|
677
|
+
onPress: onClose,
|
|
678
|
+
accessibilityRole: "button",
|
|
679
|
+
accessibilityLabel: copy.closeBackdrop
|
|
680
|
+
}
|
|
681
|
+
),
|
|
682
|
+
/* @__PURE__ */ jsxs2(
|
|
683
|
+
SafeAreaView,
|
|
684
|
+
{
|
|
685
|
+
edges: side ? ["top", "right", "bottom"] : ["bottom", "left", "right"],
|
|
686
|
+
style: side ? styles.panelSide : styles.panelBottom,
|
|
687
|
+
children: [
|
|
688
|
+
/* @__PURE__ */ jsxs2(View2, { style: styles.panelHeader, children: [
|
|
689
|
+
/* @__PURE__ */ jsx2(Text2, { style: styles.heading, children: props.title ?? copy.title }),
|
|
690
|
+
/* @__PURE__ */ jsx2(
|
|
691
|
+
Pressable2,
|
|
692
|
+
{
|
|
693
|
+
onPress: onClose,
|
|
694
|
+
style: styles.closeX,
|
|
695
|
+
accessibilityRole: "button",
|
|
696
|
+
accessibilityLabel: copy.close,
|
|
697
|
+
children: /* @__PURE__ */ jsx2(Text2, { style: styles.closeXText, children: "\u2715" })
|
|
698
|
+
}
|
|
699
|
+
)
|
|
700
|
+
] }),
|
|
701
|
+
/* @__PURE__ */ jsxs2(ScrollView, { contentContainerStyle: styles.content, showsVerticalScrollIndicator: false, children: [
|
|
702
|
+
show("quality") ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
703
|
+
/* @__PURE__ */ jsx2(ControlRow, { label: copy.resolution, children: /* @__PURE__ */ jsx2(
|
|
704
|
+
Segmented,
|
|
705
|
+
{
|
|
706
|
+
options: [
|
|
707
|
+
{ value: 720, label: "720p" },
|
|
708
|
+
{ value: 1080, label: "1080p" }
|
|
709
|
+
],
|
|
710
|
+
value: config.resolution,
|
|
711
|
+
disabled: configLocked,
|
|
712
|
+
onChange: (value) => settings.set({ resolution: value })
|
|
713
|
+
}
|
|
714
|
+
) }),
|
|
715
|
+
/* @__PURE__ */ jsx2(ControlRow, { label: copy.fps, children: /* @__PURE__ */ jsx2(
|
|
716
|
+
Segmented,
|
|
717
|
+
{
|
|
718
|
+
options: fpsOptions,
|
|
719
|
+
value: config.fps,
|
|
720
|
+
disabled: configLocked,
|
|
721
|
+
onChange: (value) => settings.set({ fps: value })
|
|
722
|
+
}
|
|
723
|
+
) }),
|
|
724
|
+
config.fps === 60 ? /* @__PURE__ */ jsx2(Note, { children: copy.fps60Note }) : null
|
|
725
|
+
] }) : null,
|
|
726
|
+
show("antiFlicker") && Platform.OS === "android" ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
727
|
+
/* @__PURE__ */ jsx2(ControlRow, { label: copy.antiFlicker, children: /* @__PURE__ */ jsx2(
|
|
728
|
+
Segmented,
|
|
729
|
+
{
|
|
730
|
+
options: [
|
|
731
|
+
{ value: "50hz", label: "50Hz" },
|
|
732
|
+
{ value: "60hz", label: "60Hz" },
|
|
733
|
+
{ value: "auto", label: copy.auto }
|
|
734
|
+
],
|
|
735
|
+
value: config.antiFlicker,
|
|
736
|
+
onChange: (value) => settings.set({ antiFlicker: value })
|
|
737
|
+
}
|
|
738
|
+
) }),
|
|
739
|
+
/* @__PURE__ */ jsx2(Note, { children: copy.antiFlickerNote })
|
|
740
|
+
] }) : null,
|
|
741
|
+
configLocked && (show("quality") || show("camera")) ? /* @__PURE__ */ jsx2(Note, { children: copy.lockedNote }) : null,
|
|
742
|
+
show("camera") ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
743
|
+
/* @__PURE__ */ jsx2(ControlRow, { label: copy.defaultCamera }),
|
|
744
|
+
/* @__PURE__ */ jsxs2(View2, { style: [styles.cameraList, configLocked && uiStyles.disabled], children: [
|
|
745
|
+
/* @__PURE__ */ jsx2(
|
|
746
|
+
CameraOption,
|
|
747
|
+
{
|
|
748
|
+
label: copy.autoCamera,
|
|
749
|
+
hint: copy.autoCameraHint,
|
|
750
|
+
active: config.cameraId === null,
|
|
751
|
+
disabled: configLocked,
|
|
752
|
+
onPress: () => settings.set({ cameraId: null })
|
|
753
|
+
}
|
|
754
|
+
),
|
|
755
|
+
live.cameras.map((camera) => /* @__PURE__ */ jsx2(
|
|
756
|
+
CameraOption,
|
|
757
|
+
{
|
|
758
|
+
label: camera.facing === "front" ? copy.frontCamera : copy.backCamera,
|
|
759
|
+
hint: copy.cameraIdHint(camera.deviceId),
|
|
760
|
+
active: config.cameraId === camera.deviceId,
|
|
761
|
+
disabled: configLocked,
|
|
762
|
+
onPress: () => settings.set({ cameraId: camera.deviceId })
|
|
763
|
+
},
|
|
764
|
+
camera.deviceId
|
|
765
|
+
))
|
|
766
|
+
] })
|
|
767
|
+
] }) : null,
|
|
768
|
+
show("zoom") && caps?.zoom.supported ? /* @__PURE__ */ jsx2(ControlRow, { label: copy.zoom, children: /* @__PURE__ */ jsx2(View2, { style: uiStyles.presetRow, children: ZOOM_PRESETS.filter(
|
|
769
|
+
(preset) => preset >= caps.zoom.min && preset <= caps.zoom.max
|
|
770
|
+
).map((preset) => /* @__PURE__ */ jsx2(
|
|
771
|
+
PresetButton,
|
|
772
|
+
{
|
|
773
|
+
label: `${preset}x`,
|
|
774
|
+
active: zoom === preset,
|
|
775
|
+
onPress: () => {
|
|
776
|
+
setZoomValue(preset);
|
|
777
|
+
void setZoom(preset);
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
preset
|
|
781
|
+
)) }) }) : null,
|
|
782
|
+
show("exposure") && caps?.exposureBias.supported ? /* @__PURE__ */ jsx2(ControlRow, { label: copy.ev, children: /* @__PURE__ */ jsx2(View2, { style: uiStyles.presetRow, children: EV_PRESETS.filter(
|
|
783
|
+
(preset) => preset >= caps.exposureBias.min && preset <= caps.exposureBias.max
|
|
784
|
+
).map((preset) => /* @__PURE__ */ jsx2(
|
|
785
|
+
PresetButton,
|
|
786
|
+
{
|
|
787
|
+
label: preset > 0 ? `+${preset}` : `${preset}`,
|
|
788
|
+
active: ev === preset,
|
|
789
|
+
onPress: () => {
|
|
790
|
+
setEvValue(preset);
|
|
791
|
+
void setExposureBias(preset);
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
preset
|
|
795
|
+
)) }) }) : null,
|
|
796
|
+
show("locks") && caps?.focusLock.supported ? /* @__PURE__ */ jsx2(ControlRow, { label: copy.focusLock, children: /* @__PURE__ */ jsx2(
|
|
797
|
+
Switch,
|
|
798
|
+
{
|
|
799
|
+
value: focusLocked,
|
|
800
|
+
accessibilityLabel: copy.focusLock,
|
|
801
|
+
onValueChange: (value) => {
|
|
802
|
+
setFocusLockedValue(value);
|
|
803
|
+
void setFocusLocked(value);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
) }) : null,
|
|
807
|
+
show("locks") && caps?.exposureLock.supported ? /* @__PURE__ */ jsx2(ControlRow, { label: copy.exposureLock, children: /* @__PURE__ */ jsx2(
|
|
808
|
+
Switch,
|
|
809
|
+
{
|
|
810
|
+
value: exposureLocked,
|
|
811
|
+
accessibilityLabel: copy.exposureLock,
|
|
812
|
+
onValueChange: (value) => {
|
|
813
|
+
setExposureLockedValue(value);
|
|
814
|
+
void setExposureLocked(value);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
) }) : null,
|
|
818
|
+
show("torch") && caps?.torch.supported ? /* @__PURE__ */ jsx2(ControlRow, { label: copy.torch, children: /* @__PURE__ */ jsx2(
|
|
819
|
+
Switch,
|
|
820
|
+
{
|
|
821
|
+
value: torch,
|
|
822
|
+
accessibilityLabel: copy.torch,
|
|
823
|
+
onValueChange: (value) => {
|
|
824
|
+
setTorchValue(value);
|
|
825
|
+
void setTorch(value);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
) }) : null,
|
|
829
|
+
show("uplink") ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
830
|
+
/* @__PURE__ */ jsx2(ControlRow, { label: copy.network }),
|
|
831
|
+
/* @__PURE__ */ jsx2(
|
|
832
|
+
Pressable2,
|
|
833
|
+
{
|
|
834
|
+
accessibilityRole: "button",
|
|
835
|
+
disabled: net.running || netBusy,
|
|
836
|
+
onPress: () => void runNetworkCheck(),
|
|
837
|
+
style: [uiStyles.actionButton, (net.running || netBusy) && uiStyles.disabled],
|
|
838
|
+
children: /* @__PURE__ */ jsx2(Text2, { style: uiStyles.actionButtonText, children: net.running ? copy.running : copy.runCheck })
|
|
839
|
+
}
|
|
840
|
+
),
|
|
841
|
+
netBusy ? /* @__PURE__ */ jsx2(Note, { children: copy.busyNote }) : null,
|
|
842
|
+
net.result ? /* @__PURE__ */ jsxs2(
|
|
843
|
+
Text2,
|
|
844
|
+
{
|
|
845
|
+
style: [
|
|
846
|
+
styles.netResult,
|
|
847
|
+
net.result.verdict === "good" ? styles.netGood : net.result.verdict === "ok" ? styles.netOk : styles.netLow
|
|
848
|
+
],
|
|
849
|
+
children: [
|
|
850
|
+
copy.resultLine({
|
|
851
|
+
uploadMbps: net.result.uploadMbps,
|
|
852
|
+
requiredMbps: net.result.requiredMbps,
|
|
853
|
+
resolution: config.resolution,
|
|
854
|
+
fps: config.fps,
|
|
855
|
+
pingMs: net.result.pingMs
|
|
856
|
+
}),
|
|
857
|
+
"\n",
|
|
858
|
+
net.result.verdict === "good" ? copy.good : net.result.verdict === "ok" ? copy.ok : copy.low
|
|
859
|
+
]
|
|
860
|
+
}
|
|
861
|
+
) : null,
|
|
862
|
+
net.error ? /* @__PURE__ */ jsx2(Note, { children: net.error }) : null
|
|
863
|
+
] }) : null,
|
|
864
|
+
footer
|
|
865
|
+
] })
|
|
866
|
+
]
|
|
867
|
+
}
|
|
868
|
+
)
|
|
869
|
+
] })
|
|
870
|
+
}
|
|
871
|
+
)
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
var styles = StyleSheet2.create({
|
|
875
|
+
overlaySide: { flex: 1, flexDirection: "row" },
|
|
876
|
+
overlayBottom: { flex: 1, flexDirection: "column" },
|
|
877
|
+
// Phần còn lại: mờ NHẸ để vẫn canh được khung hình trong lúc chỉnh; bấm là đóng.
|
|
878
|
+
backdrop: { flex: 1, backgroundColor: "rgba(0,0,0,0.3)" },
|
|
879
|
+
// Màn ngang: panel NỬA PHẢI — label và nút đứng gần nhau, nửa trái vẫn thấy
|
|
880
|
+
// khung hình. minWidth giữ dùng được trên màn hẹp, maxWidth để tablet không
|
|
881
|
+
// thành tấm bảng quá khổ.
|
|
882
|
+
panelSide: {
|
|
883
|
+
width: "50%",
|
|
884
|
+
minWidth: 340,
|
|
885
|
+
maxWidth: 480,
|
|
886
|
+
backgroundColor: uiColors.panel,
|
|
887
|
+
borderLeftWidth: 1,
|
|
888
|
+
borderLeftColor: uiColors.hairline
|
|
889
|
+
},
|
|
890
|
+
// Màn dọc: bottom-sheet tối đa 70% chiều cao.
|
|
891
|
+
panelBottom: {
|
|
892
|
+
maxHeight: "70%",
|
|
893
|
+
backgroundColor: uiColors.panel,
|
|
894
|
+
borderTopLeftRadius: 18,
|
|
895
|
+
borderTopRightRadius: 18,
|
|
896
|
+
borderTopWidth: 1,
|
|
897
|
+
borderTopColor: uiColors.hairline
|
|
898
|
+
},
|
|
899
|
+
panelHeader: {
|
|
900
|
+
flexDirection: "row",
|
|
901
|
+
alignItems: "center",
|
|
902
|
+
justifyContent: "space-between",
|
|
903
|
+
paddingHorizontal: 14,
|
|
904
|
+
paddingVertical: 10,
|
|
905
|
+
borderBottomWidth: 1,
|
|
906
|
+
borderBottomColor: uiColors.hairline
|
|
907
|
+
},
|
|
908
|
+
closeX: {
|
|
909
|
+
width: 30,
|
|
910
|
+
height: 30,
|
|
911
|
+
borderRadius: 999,
|
|
912
|
+
backgroundColor: "rgba(255,255,255,0.1)",
|
|
913
|
+
alignItems: "center",
|
|
914
|
+
justifyContent: "center"
|
|
915
|
+
},
|
|
916
|
+
closeXText: { color: uiColors.textSecondary, fontSize: 14, fontWeight: "700" },
|
|
917
|
+
content: { padding: 14, paddingBottom: 24, gap: 10 },
|
|
918
|
+
heading: { color: uiColors.text, fontSize: 15, fontWeight: "800" },
|
|
919
|
+
netResult: { fontSize: 12, fontWeight: "700", lineHeight: 17 },
|
|
920
|
+
netGood: { color: uiColors.good },
|
|
921
|
+
netOk: { color: uiColors.ok },
|
|
922
|
+
netLow: { color: uiColors.low },
|
|
923
|
+
cameraList: { gap: 5 }
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// src/ui/preview-view.tsx
|
|
927
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
928
|
+
var cached;
|
|
929
|
+
function getRtmpPreviewView() {
|
|
930
|
+
if (cached !== void 0) return cached;
|
|
931
|
+
try {
|
|
932
|
+
const loaded = __require("@pickleball/rtmp-native");
|
|
933
|
+
const candidate = loaded?.PickleballRtmpPreviewView;
|
|
934
|
+
cached = candidate ? candidate : null;
|
|
935
|
+
} catch {
|
|
936
|
+
cached = null;
|
|
937
|
+
}
|
|
938
|
+
return cached;
|
|
939
|
+
}
|
|
940
|
+
function resetRtmpPreviewViewCache() {
|
|
941
|
+
cached = void 0;
|
|
942
|
+
}
|
|
943
|
+
function RtmpPreview({ style }) {
|
|
944
|
+
const Preview = getRtmpPreviewView();
|
|
945
|
+
return Preview ? /* @__PURE__ */ jsx3(Preview, { style }) : null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// src/ui/camera-screen.tsx
|
|
949
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
950
|
+
function PickleballCameraScreen(props) {
|
|
951
|
+
const {
|
|
952
|
+
mode = "casual",
|
|
953
|
+
settings,
|
|
954
|
+
onGoLive,
|
|
955
|
+
onStop,
|
|
956
|
+
title,
|
|
957
|
+
courtLabel,
|
|
958
|
+
renderTopRight,
|
|
959
|
+
renderBottom,
|
|
960
|
+
children,
|
|
961
|
+
controls,
|
|
962
|
+
controlsProps,
|
|
963
|
+
lensRow = true,
|
|
964
|
+
autoPrepare = true,
|
|
965
|
+
captureOrientation,
|
|
966
|
+
thermalDimAfterMs = 3e4,
|
|
967
|
+
thermalDimCaption,
|
|
968
|
+
style
|
|
969
|
+
} = props;
|
|
970
|
+
const live = useLivestream();
|
|
971
|
+
const locale = props.locale ?? live.locale;
|
|
972
|
+
const copy = uiCopy[locale];
|
|
973
|
+
const config = useCameraSettings(settings ?? NO_SETTINGS);
|
|
974
|
+
useAntiFlicker(settings ? config.antiFlicker : void 0);
|
|
975
|
+
const [sheetOpen, setSheetOpen] = useState3(false);
|
|
976
|
+
const [confirmingStop, setConfirmingStop] = useState3(false);
|
|
977
|
+
const [busy, setBusy] = useState3(false);
|
|
978
|
+
const [thermalDimmed, setThermalDimmed] = useThermalDim(live.state, thermalDimAfterMs);
|
|
979
|
+
const showGoLiveButton = props.showGoLiveButton ?? mode === "casual";
|
|
980
|
+
const courtBound = mode === "court";
|
|
981
|
+
const paused = live.state === "standby" && live.elapsedMs > 0;
|
|
982
|
+
const pill = props.statusPill === void 0 ? statusPillFor(live.state, { elapsedMs: live.elapsedMs, courtBound, locale }) : props.statusPill;
|
|
983
|
+
const pillLabel = pill && live.state === "live" && props.statusPill === void 0 ? `${pill.label} ${formatElapsed(live.elapsedMs)}` : pill?.label;
|
|
984
|
+
const hint = props.statusHint === void 0 ? defaultHint() : props.statusHint;
|
|
985
|
+
function defaultHint() {
|
|
986
|
+
switch (live.state) {
|
|
987
|
+
case "preview":
|
|
988
|
+
return courtBound ? copy.hint.courtPreview : onGoLive ? copy.hint.casualPreview : null;
|
|
989
|
+
case "standby":
|
|
990
|
+
if (paused) return copy.hint.paused;
|
|
991
|
+
if (courtBound) {
|
|
992
|
+
return courtLabel ? copy.hint.courtStandby(courtLabel) : copy.hint.courtStandbyNoCourt;
|
|
993
|
+
}
|
|
994
|
+
return copy.hint.casualStandby;
|
|
995
|
+
case "connecting":
|
|
996
|
+
return copy.hint.connecting;
|
|
997
|
+
case "reconnecting":
|
|
998
|
+
return copy.hint.reconnecting;
|
|
999
|
+
case "ending":
|
|
1000
|
+
return copy.hint.ending;
|
|
1001
|
+
case "error":
|
|
1002
|
+
return live.error ? copy.errors.remediation[live.error.code] : copy.hint.errorFallback;
|
|
1003
|
+
default:
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const errorTitle = live.error ? copy.errors.title[live.error.code] : null;
|
|
1008
|
+
async function run(action) {
|
|
1009
|
+
setBusy(true);
|
|
1010
|
+
try {
|
|
1011
|
+
await action();
|
|
1012
|
+
} catch {
|
|
1013
|
+
} finally {
|
|
1014
|
+
setBusy(false);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const mainAction = (() => {
|
|
1018
|
+
if (!showGoLiveButton) return null;
|
|
1019
|
+
if (busy) return { label: copy.button.connecting, onPress: () => void 0, disabled: true, tone: "muted" };
|
|
1020
|
+
switch (live.state) {
|
|
1021
|
+
case "preview":
|
|
1022
|
+
return onGoLive ? { label: copy.button.goLive, onPress: () => void run(onGoLive), tone: "go" } : null;
|
|
1023
|
+
case "standby":
|
|
1024
|
+
return {
|
|
1025
|
+
label: paused ? copy.button.resume : copy.button.publishNow,
|
|
1026
|
+
onPress: () => void run(() => live.startPublishing()),
|
|
1027
|
+
tone: "go"
|
|
1028
|
+
};
|
|
1029
|
+
case "live":
|
|
1030
|
+
return confirmingStop ? {
|
|
1031
|
+
label: copy.button.confirmStop,
|
|
1032
|
+
onPress: () => {
|
|
1033
|
+
setConfirmingStop(false);
|
|
1034
|
+
void run(onStop ?? (() => live.stop("user")));
|
|
1035
|
+
},
|
|
1036
|
+
tone: "stop"
|
|
1037
|
+
} : { label: copy.button.stop, onPress: () => setConfirmingStop(true), tone: "stop" };
|
|
1038
|
+
case "error":
|
|
1039
|
+
return {
|
|
1040
|
+
label: copy.button.retry,
|
|
1041
|
+
onPress: () => void run(() => live.sessionId ? live.retry() : live.prepare()),
|
|
1042
|
+
tone: "go"
|
|
1043
|
+
};
|
|
1044
|
+
case "connecting":
|
|
1045
|
+
case "reconnecting":
|
|
1046
|
+
return { label: copy.button.connecting, onPress: () => void 0, disabled: true, tone: "muted" };
|
|
1047
|
+
case "ending":
|
|
1048
|
+
return { label: copy.button.ending, onPress: () => void 0, disabled: true, tone: "muted" };
|
|
1049
|
+
default:
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
})();
|
|
1053
|
+
const infoLine = props.infoLine === void 0 ? settings ? `${config.resolution}p \u2022 ${config.fps} fps \u2022 H.264` : null : props.infoLine;
|
|
1054
|
+
const showPreview = isRtmpPreview(live.previewTrack);
|
|
1055
|
+
const sheetEnabled = controls !== false && (controls !== void 0 || settings !== void 0);
|
|
1056
|
+
return /* @__PURE__ */ jsxs3(View3, { style: [styles2.root, style], children: [
|
|
1057
|
+
autoPrepare ? /* @__PURE__ */ jsx4(PickleballAutoPrepare, { captureOrientation }) : null,
|
|
1058
|
+
showPreview ? /* @__PURE__ */ jsx4(RtmpPreview, { style: StyleSheet3.absoluteFill }) : /* @__PURE__ */ jsx4(View3, { style: [StyleSheet3.absoluteFill, styles2.center], children: live.state === "error" ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1059
|
+
/* @__PURE__ */ jsx4(Text3, { style: styles2.errorTitle, accessibilityRole: "alert", children: errorTitle ?? copy.hint.cameraError }),
|
|
1060
|
+
live.error ? /* @__PURE__ */ jsxs3(Text3, { style: styles2.errorDetail, selectable: true, children: [
|
|
1061
|
+
copy.errors.technicalDetail,
|
|
1062
|
+
": ",
|
|
1063
|
+
live.error.message
|
|
1064
|
+
] }) : null
|
|
1065
|
+
] }) : /* @__PURE__ */ jsx4(Text3, { style: styles2.mutedText, children: copy.hint.cameraOpening }) }),
|
|
1066
|
+
/* @__PURE__ */ jsxs3(SafeAreaView2, { style: styles2.overlay, pointerEvents: "box-none", children: [
|
|
1067
|
+
/* @__PURE__ */ jsxs3(View3, { style: styles2.topArea, pointerEvents: "box-none", children: [
|
|
1068
|
+
/* @__PURE__ */ jsxs3(View3, { style: styles2.topRow, pointerEvents: "box-none", children: [
|
|
1069
|
+
/* @__PURE__ */ jsxs3(View3, { style: styles2.statusCard, pointerEvents: "none", children: [
|
|
1070
|
+
pill && pillLabel ? /* @__PURE__ */ jsx4(StatusPill, { label: pillLabel, color: pill.color }) : null,
|
|
1071
|
+
live.sessionId ? /* @__PURE__ */ jsxs3(View3, { style: styles2.sessionInfo, children: [
|
|
1072
|
+
title ? /* @__PURE__ */ jsx4(Text3, { style: styles2.sessionTitleText, numberOfLines: 1, children: title }) : null,
|
|
1073
|
+
/* @__PURE__ */ jsx4(Text3, { style: styles2.sessionIdText, numberOfLines: 1, children: shortSessionId(live.sessionId) })
|
|
1074
|
+
] }) : null
|
|
1075
|
+
] }),
|
|
1076
|
+
renderTopRight?.(),
|
|
1077
|
+
sheetEnabled ? /* @__PURE__ */ jsx4(
|
|
1078
|
+
Pressable3,
|
|
1079
|
+
{
|
|
1080
|
+
onPress: () => setSheetOpen(true),
|
|
1081
|
+
style: styles2.moreButton,
|
|
1082
|
+
accessibilityRole: "button",
|
|
1083
|
+
accessibilityLabel: copy.sheet.title,
|
|
1084
|
+
children: /* @__PURE__ */ jsx4(Text3, { style: styles2.moreButtonText, children: "\u2022\u2022\u2022" })
|
|
1085
|
+
}
|
|
1086
|
+
) : null
|
|
1087
|
+
] }),
|
|
1088
|
+
hint && live.state !== "live" ? /* @__PURE__ */ jsxs3(View3, { style: styles2.stepHintChip, pointerEvents: "none", children: [
|
|
1089
|
+
/* @__PURE__ */ jsx4(
|
|
1090
|
+
View3,
|
|
1091
|
+
{
|
|
1092
|
+
style: [
|
|
1093
|
+
styles2.stepHintDot,
|
|
1094
|
+
{ backgroundColor: props.statusHintDotColor ?? uiColors.textDim }
|
|
1095
|
+
]
|
|
1096
|
+
}
|
|
1097
|
+
),
|
|
1098
|
+
/* @__PURE__ */ jsx4(Text3, { style: styles2.stepHintText, children: hint })
|
|
1099
|
+
] }) : null
|
|
1100
|
+
] }),
|
|
1101
|
+
children ? /* @__PURE__ */ jsx4(View3, { style: styles2.centerSlot, pointerEvents: "box-none", children }) : null,
|
|
1102
|
+
/* @__PURE__ */ jsxs3(View3, { style: styles2.bottomArea, pointerEvents: "box-none", children: [
|
|
1103
|
+
mainAction ? /* @__PURE__ */ jsxs3(View3, { style: styles2.mainRow, children: [
|
|
1104
|
+
confirmingStop && live.state === "live" ? /* @__PURE__ */ jsx4(
|
|
1105
|
+
Pressable3,
|
|
1106
|
+
{
|
|
1107
|
+
accessibilityRole: "button",
|
|
1108
|
+
onPress: () => setConfirmingStop(false),
|
|
1109
|
+
style: [styles2.mainButton, styles2.mainMuted],
|
|
1110
|
+
children: /* @__PURE__ */ jsx4(Text3, { style: styles2.mainButtonText, children: copy.button.cancel })
|
|
1111
|
+
}
|
|
1112
|
+
) : null,
|
|
1113
|
+
/* @__PURE__ */ jsx4(
|
|
1114
|
+
Pressable3,
|
|
1115
|
+
{
|
|
1116
|
+
accessibilityRole: "button",
|
|
1117
|
+
accessibilityState: { disabled: mainAction.disabled === true },
|
|
1118
|
+
disabled: mainAction.disabled,
|
|
1119
|
+
onPress: mainAction.onPress,
|
|
1120
|
+
style: [
|
|
1121
|
+
styles2.mainButton,
|
|
1122
|
+
mainAction.tone === "go" && styles2.mainGo,
|
|
1123
|
+
mainAction.tone === "stop" && styles2.mainStop,
|
|
1124
|
+
mainAction.tone === "muted" && styles2.mainMuted
|
|
1125
|
+
],
|
|
1126
|
+
children: /* @__PURE__ */ jsx4(
|
|
1127
|
+
Text3,
|
|
1128
|
+
{
|
|
1129
|
+
style: [styles2.mainButtonText, mainAction.tone === "go" && styles2.mainGoText],
|
|
1130
|
+
children: mainAction.label
|
|
1131
|
+
}
|
|
1132
|
+
)
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
] }) : null,
|
|
1136
|
+
lensRow ? /* @__PURE__ */ jsx4(
|
|
1137
|
+
LensRow,
|
|
1138
|
+
{
|
|
1139
|
+
cameras: live.cameras,
|
|
1140
|
+
selectedCameraId: live.selectedCameraId,
|
|
1141
|
+
onSelect: (deviceId) => void live.selectCamera(deviceId).catch(() => void 0),
|
|
1142
|
+
locale
|
|
1143
|
+
}
|
|
1144
|
+
) : null,
|
|
1145
|
+
infoLine ? /* @__PURE__ */ jsx4(Text3, { style: styles2.infoLine, children: infoLine }) : null,
|
|
1146
|
+
renderBottom?.()
|
|
1147
|
+
] })
|
|
1148
|
+
] }),
|
|
1149
|
+
sheetEnabled ? controls ? controls({ visible: sheetOpen, onClose: () => setSheetOpen(false) }) : settings ? /* @__PURE__ */ jsx4(
|
|
1150
|
+
CameraControlsSheet,
|
|
1151
|
+
{
|
|
1152
|
+
...controlsProps,
|
|
1153
|
+
visible: sheetOpen,
|
|
1154
|
+
onClose: () => setSheetOpen(false),
|
|
1155
|
+
settings,
|
|
1156
|
+
locale: controlsProps?.locale ?? locale
|
|
1157
|
+
}
|
|
1158
|
+
) : null : null,
|
|
1159
|
+
thermalDimmed ? /* @__PURE__ */ jsx4(
|
|
1160
|
+
Pressable3,
|
|
1161
|
+
{
|
|
1162
|
+
onPress: () => setThermalDimmed(false),
|
|
1163
|
+
style: styles2.thermalOverlay,
|
|
1164
|
+
accessibilityRole: "button",
|
|
1165
|
+
accessibilityLabel: copy.thermal.subtitle,
|
|
1166
|
+
children: /* @__PURE__ */ jsxs3(View3, { style: styles2.thermalCard, children: [
|
|
1167
|
+
/* @__PURE__ */ jsx4(StatusPill, { label: `${copy.pill.live} ${formatElapsed(live.elapsedMs)}`, color: "#ef4444" }),
|
|
1168
|
+
/* @__PURE__ */ jsx4(Text3, { style: styles2.thermalTitle, children: copy.thermal.title }),
|
|
1169
|
+
thermalDimCaption ? /* @__PURE__ */ jsx4(Text3, { style: styles2.thermalCaption, children: thermalDimCaption }) : null,
|
|
1170
|
+
/* @__PURE__ */ jsx4(Text3, { style: styles2.thermalSubtitle, children: copy.thermal.subtitle })
|
|
1171
|
+
] })
|
|
1172
|
+
}
|
|
1173
|
+
) : null
|
|
1174
|
+
] });
|
|
1175
|
+
}
|
|
1176
|
+
var NO_SETTINGS_VALUE = Object.freeze({
|
|
1177
|
+
resolution: 720,
|
|
1178
|
+
fps: 30,
|
|
1179
|
+
antiFlicker: "50hz",
|
|
1180
|
+
cameraId: null
|
|
1181
|
+
});
|
|
1182
|
+
var NO_SETTINGS = {
|
|
1183
|
+
key: "none",
|
|
1184
|
+
get: () => NO_SETTINGS_VALUE,
|
|
1185
|
+
set: () => void 0,
|
|
1186
|
+
subscribe: () => () => void 0,
|
|
1187
|
+
restore: async () => void 0,
|
|
1188
|
+
isRestored: () => true
|
|
1189
|
+
};
|
|
1190
|
+
var styles2 = StyleSheet3.create({
|
|
1191
|
+
root: { flex: 1, backgroundColor: uiColors.ink },
|
|
1192
|
+
center: { alignItems: "center", justifyContent: "center", padding: 24 },
|
|
1193
|
+
overlay: { flex: 1, justifyContent: "space-between" },
|
|
1194
|
+
topArea: { gap: 8 },
|
|
1195
|
+
topRow: {
|
|
1196
|
+
flexDirection: "row",
|
|
1197
|
+
justifyContent: "space-between",
|
|
1198
|
+
alignItems: "center",
|
|
1199
|
+
gap: 8,
|
|
1200
|
+
paddingHorizontal: 16,
|
|
1201
|
+
paddingTop: 8
|
|
1202
|
+
},
|
|
1203
|
+
statusCard: {
|
|
1204
|
+
flexDirection: "row",
|
|
1205
|
+
alignItems: "center",
|
|
1206
|
+
gap: 8,
|
|
1207
|
+
flexShrink: 1,
|
|
1208
|
+
minWidth: 0
|
|
1209
|
+
},
|
|
1210
|
+
stepHintChip: {
|
|
1211
|
+
flexDirection: "row",
|
|
1212
|
+
alignItems: "center",
|
|
1213
|
+
gap: 6,
|
|
1214
|
+
alignSelf: "flex-start",
|
|
1215
|
+
marginLeft: 16,
|
|
1216
|
+
backgroundColor: uiColors.chip,
|
|
1217
|
+
borderRadius: 999,
|
|
1218
|
+
paddingHorizontal: 10,
|
|
1219
|
+
paddingVertical: 5,
|
|
1220
|
+
maxWidth: 340
|
|
1221
|
+
},
|
|
1222
|
+
stepHintDot: { width: 6, height: 6, borderRadius: 3 },
|
|
1223
|
+
stepHintText: { color: uiColors.textMuted, fontSize: 11 },
|
|
1224
|
+
sessionInfo: { flexShrink: 1, minWidth: 0 },
|
|
1225
|
+
sessionTitleText: { color: uiColors.text, fontSize: 11, fontWeight: "700" },
|
|
1226
|
+
sessionIdText: { color: uiColors.textMuted, fontSize: 9, fontVariant: ["tabular-nums"] },
|
|
1227
|
+
moreButton: {
|
|
1228
|
+
borderRadius: 999,
|
|
1229
|
+
backgroundColor: uiColors.chip,
|
|
1230
|
+
paddingHorizontal: 12,
|
|
1231
|
+
paddingVertical: 7
|
|
1232
|
+
},
|
|
1233
|
+
moreButtonText: { color: uiColors.text, fontSize: 13, fontWeight: "700" },
|
|
1234
|
+
centerSlot: { alignItems: "center", justifyContent: "center" },
|
|
1235
|
+
bottomArea: { alignItems: "center", gap: 10, paddingBottom: 12 },
|
|
1236
|
+
mainRow: { flexDirection: "row", gap: 10 },
|
|
1237
|
+
mainButton: {
|
|
1238
|
+
borderRadius: 999,
|
|
1239
|
+
paddingHorizontal: 26,
|
|
1240
|
+
paddingVertical: 12,
|
|
1241
|
+
minWidth: 150,
|
|
1242
|
+
alignItems: "center",
|
|
1243
|
+
backgroundColor: uiColors.chip
|
|
1244
|
+
},
|
|
1245
|
+
mainGo: { backgroundColor: "#b4ff39" },
|
|
1246
|
+
mainStop: { backgroundColor: "#ef4444" },
|
|
1247
|
+
mainMuted: { backgroundColor: uiColors.chip },
|
|
1248
|
+
mainButtonText: { color: uiColors.text, fontSize: 15, fontWeight: "800", letterSpacing: 0.3 },
|
|
1249
|
+
mainGoText: { color: uiColors.ink },
|
|
1250
|
+
infoLine: { color: uiColors.textDim, fontSize: 11, fontVariant: ["tabular-nums"] },
|
|
1251
|
+
mutedText: { color: uiColors.textDim, fontSize: 14, textAlign: "center" },
|
|
1252
|
+
errorTitle: { color: uiColors.text, fontSize: 16, fontWeight: "800", textAlign: "center" },
|
|
1253
|
+
errorDetail: { color: uiColors.textDim, fontSize: 11, textAlign: "center", marginTop: 8 },
|
|
1254
|
+
thermalOverlay: {
|
|
1255
|
+
position: "absolute",
|
|
1256
|
+
top: 0,
|
|
1257
|
+
left: 0,
|
|
1258
|
+
right: 0,
|
|
1259
|
+
bottom: 0,
|
|
1260
|
+
backgroundColor: "rgba(0,0,0,0.92)",
|
|
1261
|
+
alignItems: "center",
|
|
1262
|
+
justifyContent: "center",
|
|
1263
|
+
zIndex: 99
|
|
1264
|
+
},
|
|
1265
|
+
thermalCard: { alignItems: "center", gap: 12, padding: 24 },
|
|
1266
|
+
thermalTitle: { color: uiColors.text, fontSize: 18, fontWeight: "700" },
|
|
1267
|
+
thermalCaption: { color: uiColors.textMuted, fontSize: 16, fontWeight: "800", letterSpacing: 0.5 },
|
|
1268
|
+
thermalSubtitle: { color: uiColors.textDim, fontSize: 13 }
|
|
1269
|
+
});
|
|
1270
|
+
export {
|
|
1271
|
+
ActionButton,
|
|
1272
|
+
CameraControlsSheet,
|
|
1273
|
+
CameraOption,
|
|
1274
|
+
ControlRow,
|
|
1275
|
+
LensRow,
|
|
1276
|
+
Note,
|
|
1277
|
+
PILL_COLORS,
|
|
1278
|
+
PickleballCameraScreen,
|
|
1279
|
+
PresetButton,
|
|
1280
|
+
RtmpPreview,
|
|
1281
|
+
Segmented,
|
|
1282
|
+
StatusPill,
|
|
1283
|
+
activeLensId,
|
|
1284
|
+
formatElapsed,
|
|
1285
|
+
getRtmpPreviewView,
|
|
1286
|
+
isRtmpPreview,
|
|
1287
|
+
lensShortLabel,
|
|
1288
|
+
resetRtmpPreviewViewCache,
|
|
1289
|
+
shortSessionId,
|
|
1290
|
+
statusPillFor,
|
|
1291
|
+
uiColors,
|
|
1292
|
+
uiCopy,
|
|
1293
|
+
uiStyles,
|
|
1294
|
+
useAntiFlicker,
|
|
1295
|
+
useCameraCapabilities,
|
|
1296
|
+
useThermalDim
|
|
1297
|
+
};
|