@pickleball/expo-sdk 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +163 -40
- package/dist/advanced-DcbHBCGb.d.ts +300 -0
- package/dist/advanced-DeQwg7MK.d.cts +300 -0
- package/dist/advanced.cjs +47 -0
- package/dist/advanced.d.cts +3 -0
- package/dist/advanced.d.ts +3 -0
- package/dist/advanced.js +47 -0
- package/dist/camera-controls-By8tHigY.d.cts +97 -0
- package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
- package/dist/chunk-2JHULJZ7.js +265 -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-7OX2DSKI.cjs +33 -0
- package/dist/chunk-7Z7TN5N3.js +318 -0
- package/dist/chunk-DHDX3ZOO.js +800 -0
- package/dist/chunk-GXQYMPHE.cjs +9 -0
- package/dist/chunk-IUIRVKJI.cjs +2290 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-LCWGNGEN.cjs +265 -0
- package/dist/chunk-TBUEXRFQ.cjs +318 -0
- package/dist/chunk-XGP2MN5R.js +2290 -0
- package/dist/chunk-XL4CISVQ.js +9 -0
- package/dist/chunk-YGSQ25UA.cjs +800 -0
- 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 +1427 -0
- package/dist/device-agent/index.d.cts +493 -0
- package/dist/device-agent/index.d.ts +493 -0
- package/dist/device-agent/index.js +1427 -0
- package/dist/engine-BSvSdCSE.d.cts +741 -0
- package/dist/engine-BSvSdCSE.d.ts +741 -0
- package/dist/index.cjs +426 -6174
- package/dist/index.d.cts +299 -1400
- package/dist/index.d.ts +299 -1400
- package/dist/index.js +413 -6063
- package/dist/match/index.cjs +2456 -0
- package/dist/match/index.d.cts +253 -0
- package/dist/match/index.d.ts +253 -0
- package/dist/match/index.js +2456 -0
- package/dist/match/remote/index.cjs +21 -0
- package/dist/match/remote/index.d.cts +67 -0
- package/dist/match/remote/index.d.ts +67 -0
- package/dist/match/remote/index.js +21 -0
- package/dist/match/voice/index.cjs +23 -0
- package/dist/match/voice/index.d.cts +114 -0
- package/dist/match/voice/index.d.ts +114 -0
- package/dist/match/voice/index.js +23 -0
- package/dist/plugin.cjs +52 -71
- package/dist/plugin.d.cts +28 -1
- package/dist/plugin.d.ts +28 -1
- package/dist/plugin.js +26 -23
- 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/types-CrmcX408.d.cts +176 -0
- package/dist/types-CrmcX408.d.ts +176 -0
- package/dist/types-DiKWjwvN.d.cts +96 -0
- package/dist/types-DiKWjwvN.d.ts +96 -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 +4 -32
- package/dist/version.js +2 -1
- package/package.json +95 -15
- package/src/advanced.ts +49 -0
- 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 +696 -0
- package/src/device-agent/beat-policy.ts +46 -0
- package/src/device-agent/component.tsx +60 -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 +88 -0
- package/src/device-agent/http-transport.ts +164 -0
- package/src/device-agent/index.ts +26 -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 +601 -20
- package/src/errors.ts +16 -0
- package/src/http-session-provider.ts +21 -183
- package/src/index.ts +117 -14
- package/src/local-video-quality.ts +43 -0
- package/src/match/index.ts +29 -0
- package/src/match/provider.tsx +4 -2
- package/src/match/remote/hid-gesture.ts +10 -2
- package/src/match/remote/types.ts +6 -4
- package/src/match/voice/scheduler.ts +6 -1
- package/src/match/voice/service.ts +16 -4
- package/src/native-runtime.ts +283 -77
- package/src/plugin.ts +54 -20
- package/src/provider.tsx +129 -22
- 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 +348 -0
- 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/chunk-VHLUJQG5.js +0 -17
- package/src/livekit-room-adapter.ts +0 -353
- package/src/livestream-screen.tsx +0 -980
package/src/native-runtime.ts
CHANGED
|
@@ -1,99 +1,110 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
EngineDependencies,
|
|
3
|
+
SdkNetwork,
|
|
4
|
+
SdkRuntime,
|
|
5
|
+
SdkStorage,
|
|
6
|
+
SessionCommandChannel,
|
|
7
|
+
TelemetrySink,
|
|
8
|
+
} from "./engine";
|
|
9
|
+
import type {
|
|
10
|
+
SessionCommandPollResult,
|
|
11
|
+
SessionCommandRecord,
|
|
12
|
+
} from "./contracts";
|
|
13
|
+
import { RtmpRoomAdapter, type NativeRtmpBindings } from "./rtmp-room-adapter";
|
|
3
14
|
import { UnsupportedRuntimeError } from "./errors";
|
|
4
15
|
|
|
5
16
|
declare const require: (name: string) => any;
|
|
6
17
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
globalsRegistered = true;
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
createRoom: () => new liveKit.Room({ adaptiveStream: false, dynacast: false }),
|
|
20
|
-
createLocalTracks: liveKit.createLocalTracks,
|
|
21
|
-
startAudioSession: () => reactNativeLiveKit.AudioSession.startAudioSession(),
|
|
22
|
-
stopAudioSession: () => reactNativeLiveKit.AudioSession.stopAudioSession(),
|
|
23
|
-
isLandscape() {
|
|
24
|
-
const { width, height } = require("react-native").Dimensions.get("window");
|
|
25
|
-
return width > height;
|
|
26
|
-
},
|
|
27
|
-
async listCameras() {
|
|
28
|
-
const { mediaDevices } = require("@livekit/react-native-webrtc");
|
|
29
|
-
const devices: Array<{
|
|
30
|
-
kind?: string;
|
|
31
|
-
deviceId?: string;
|
|
32
|
-
label?: string;
|
|
33
|
-
facing?: string;
|
|
34
|
-
}> = await mediaDevices.enumerateDevices();
|
|
35
|
-
return devices
|
|
36
|
-
.filter((device) => device.kind === "videoinput" && device.deviceId)
|
|
37
|
-
.map((device) => ({
|
|
38
|
-
deviceId: String(device.deviceId),
|
|
39
|
-
label: String(device.label ?? ""),
|
|
40
|
-
facing:
|
|
41
|
-
device.facing === "front" || device.facing === "environment"
|
|
42
|
-
? device.facing
|
|
43
|
-
: ("unknown" as const),
|
|
44
|
-
}));
|
|
45
|
-
},
|
|
46
|
-
cameraSource: liveKit.Track.Source.Camera,
|
|
47
|
-
events: {
|
|
48
|
-
reconnecting: liveKit.RoomEvent.Reconnecting,
|
|
49
|
-
reconnected: liveKit.RoomEvent.Reconnected,
|
|
50
|
-
disconnected: liveKit.RoomEvent.Disconnected,
|
|
51
|
-
mediaDevicesError: liveKit.RoomEvent.MediaDevicesError,
|
|
52
|
-
dataReceived: liveKit.RoomEvent.DataReceived,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function createDefaultRuntime(): SdkRuntime {
|
|
58
|
-
const { NativeModules, Platform } = require("react-native");
|
|
18
|
+
/**
|
|
19
|
+
* Runtime cho app RTMP-only (KHÔNG có LiveKit/WebRTC): assert bằng module
|
|
20
|
+
* @pickleball/rtmp-native, xin quyền bằng API quyền thuần (Android) hoặc hàm
|
|
21
|
+
* native `requestPermissions` (iOS, rtmp-native ≥ 0.2; thiếu thì AVFoundation
|
|
22
|
+
* hỏi lúc mở camera). Chuyển từ apps/device-streamer (createRtmpOnlyRuntime).
|
|
23
|
+
* Không dùng getUserMedia: máy MediaTek chỉ cho một camera mở cùng lúc.
|
|
24
|
+
*/
|
|
25
|
+
export function createRtmpRuntime(): SdkRuntime {
|
|
26
|
+
const { Platform, PermissionsAndroid } = require("react-native");
|
|
59
27
|
return {
|
|
60
28
|
assertSupported() {
|
|
61
|
-
if (
|
|
62
|
-
(Platform.OS !== "ios" && Platform.OS !== "android") ||
|
|
63
|
-
!NativeModules.LivekitReactNativeModule
|
|
64
|
-
) {
|
|
29
|
+
if (Platform.OS !== "ios" && Platform.OS !== "android") {
|
|
65
30
|
throw new UnsupportedRuntimeError();
|
|
66
31
|
}
|
|
32
|
+
try {
|
|
33
|
+
require("@pickleball/rtmp-native");
|
|
34
|
+
} catch {
|
|
35
|
+
throw new UnsupportedRuntimeError(
|
|
36
|
+
"@pickleball/rtmp-native is required: install it and rebuild the Development Build",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
67
39
|
},
|
|
68
40
|
async requestPermissions() {
|
|
41
|
+
if (Platform.OS === "android") {
|
|
42
|
+
try {
|
|
43
|
+
const granted = await PermissionsAndroid.requestMultiple([
|
|
44
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
45
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
46
|
+
]);
|
|
47
|
+
return (
|
|
48
|
+
granted[PermissionsAndroid.PERMISSIONS.CAMERA] ===
|
|
49
|
+
PermissionsAndroid.RESULTS.GRANTED &&
|
|
50
|
+
granted[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] ===
|
|
51
|
+
PermissionsAndroid.RESULTS.GRANTED
|
|
52
|
+
);
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
69
57
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
58
|
+
const native = require("@pickleball/rtmp-native").default;
|
|
59
|
+
if (typeof native?.requestPermissions === "function") {
|
|
60
|
+
return (await native.requestPermissions()) !== false;
|
|
61
|
+
}
|
|
74
62
|
} catch {
|
|
75
|
-
|
|
63
|
+
// Binary cũ chưa có hàm → AVFoundation tự hỏi lúc prepare.
|
|
76
64
|
}
|
|
65
|
+
return true;
|
|
77
66
|
},
|
|
78
|
-
activateKeepAwake()
|
|
79
|
-
|
|
80
|
-
"@pickleball/expo-sdk",
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
67
|
+
activateKeepAwake: () =>
|
|
68
|
+
Promise.resolve(
|
|
69
|
+
require("expo-keep-awake").activateKeepAwakeAsync("@pickleball/expo-sdk"),
|
|
70
|
+
).catch(() => undefined),
|
|
71
|
+
// deactivate trả promise và REJECT khi activity đã chết (sau crash/reload) —
|
|
72
|
+
// không catch là thêm một "Uncaught (in promise)" gây nhiễu log.
|
|
73
|
+
deactivateKeepAwake: () => {
|
|
74
|
+
void Promise.resolve(
|
|
75
|
+
require("expo-keep-awake").deactivateKeepAwake("@pickleball/expo-sdk"),
|
|
76
|
+
).catch(() => undefined);
|
|
87
77
|
},
|
|
88
78
|
};
|
|
89
79
|
}
|
|
90
80
|
|
|
81
|
+
/** Runtime mặc định của SDK 1.0 = RTMP (LiveKit đã gỡ khỏi SDK — ADR-0002). */
|
|
82
|
+
export const createDefaultRuntime = createRtmpRuntime;
|
|
83
|
+
|
|
91
84
|
export function createDefaultStorage(): SdkStorage {
|
|
92
|
-
|
|
85
|
+
// Cài localStorage của expo-sqlite ở LẦN DÙNG ĐẦU, không phải lúc gọi factory:
|
|
86
|
+
// app thường tạo storage ở module scope (root layout), và static-render web
|
|
87
|
+
// của expo-router nạp module đó trong Node nơi expo-sqlite không resolve được.
|
|
88
|
+
// Không có lời gọi get/set/remove nào khi render tĩnh nên require không chạy.
|
|
89
|
+
let installed = false;
|
|
90
|
+
const install = () => {
|
|
91
|
+
if (installed) return;
|
|
92
|
+
require("expo-sqlite/localStorage/install");
|
|
93
|
+
installed = true;
|
|
94
|
+
};
|
|
93
95
|
return {
|
|
94
|
-
getItem: async (key) =>
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
getItem: async (key) => {
|
|
97
|
+
install();
|
|
98
|
+
return globalThis.localStorage.getItem(key);
|
|
99
|
+
},
|
|
100
|
+
setItem: async (key, value) => {
|
|
101
|
+
install();
|
|
102
|
+
globalThis.localStorage.setItem(key, value);
|
|
103
|
+
},
|
|
104
|
+
removeItem: async (key) => {
|
|
105
|
+
install();
|
|
106
|
+
globalThis.localStorage.removeItem(key);
|
|
107
|
+
},
|
|
97
108
|
};
|
|
98
109
|
}
|
|
99
110
|
|
|
@@ -155,8 +166,152 @@ export function createDefaultTelemetry(
|
|
|
155
166
|
};
|
|
156
167
|
}
|
|
157
168
|
|
|
169
|
+
/** roomFactory mặc định của SDK 1.0 = RTMP (fps/camera mặc định của native). */
|
|
158
170
|
export function createDefaultRoomFactory(): EngineDependencies["roomFactory"] {
|
|
159
|
-
return ()
|
|
171
|
+
return createRtmpRoomFactory();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Kênh poll lệnh cho phiên device-rtmp. URL suy từ telemetry endpoint
|
|
176
|
+
* (.../telemetry → .../commands) — cùng origin, cùng Bearer token, nên không
|
|
177
|
+
* cần thêm bất kỳ trường mới nào đi qua proxy của đối tác.
|
|
178
|
+
*/
|
|
179
|
+
export function createDefaultCommandChannel(
|
|
180
|
+
fetchImplementation = globalThis.fetch,
|
|
181
|
+
timeoutMs = 10_000,
|
|
182
|
+
): SessionCommandChannel {
|
|
183
|
+
const controllers = new Set<AbortController>();
|
|
184
|
+
return {
|
|
185
|
+
async poll(credentials, after): Promise<SessionCommandPollResult> {
|
|
186
|
+
const endpoint = credentials.endpoint.replace(/\/telemetry$/, "/commands");
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
controllers.add(controller);
|
|
189
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
190
|
+
try {
|
|
191
|
+
const response = await fetchImplementation(endpoint, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: {
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
Authorization: `Bearer ${credentials.token}`,
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({ after }),
|
|
198
|
+
signal: controller.signal,
|
|
199
|
+
redirect: "error",
|
|
200
|
+
});
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
if (typeof response.text === "function") await response.text();
|
|
203
|
+
throw Object.assign(new Error("Command poll rejected"), {
|
|
204
|
+
status: response.status,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const envelope = (await response.json()) as { data?: unknown };
|
|
208
|
+
return parseCommandPollResult(envelope?.data);
|
|
209
|
+
} finally {
|
|
210
|
+
clearTimeout(timeout);
|
|
211
|
+
controllers.delete(controller);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
dispose() {
|
|
215
|
+
for (const controller of controllers) controller.abort();
|
|
216
|
+
controllers.clear();
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseCommandPollResult(value: unknown): SessionCommandPollResult {
|
|
222
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
223
|
+
throw new Error("Command poll returned an invalid payload");
|
|
224
|
+
}
|
|
225
|
+
const record = value as Record<string, unknown>;
|
|
226
|
+
const status = record.status;
|
|
227
|
+
if (status !== "scheduled" && status !== "live" && status !== "ended") {
|
|
228
|
+
throw new Error("Command poll returned an invalid status");
|
|
229
|
+
}
|
|
230
|
+
const commands: SessionCommandRecord[] = [];
|
|
231
|
+
if (Array.isArray(record.commands)) {
|
|
232
|
+
for (const item of record.commands) {
|
|
233
|
+
if (item === null || typeof item !== "object") continue;
|
|
234
|
+
const command = item as Record<string, unknown>;
|
|
235
|
+
if (
|
|
236
|
+
typeof command.id !== "string" ||
|
|
237
|
+
(command.action !== "start" &&
|
|
238
|
+
command.action !== "stop" &&
|
|
239
|
+
command.action !== "end") ||
|
|
240
|
+
typeof command.issuedAt !== "number" ||
|
|
241
|
+
!Number.isFinite(command.issuedAt)
|
|
242
|
+
) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
commands.push({
|
|
246
|
+
id: command.id,
|
|
247
|
+
action: command.action,
|
|
248
|
+
issuedAt: command.issuedAt,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { commands, status };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Bindings native RTMP. `require` động: app chưa cài @pickleball/rtmp-native
|
|
257
|
+
* chỉ lỗi khi thật sự mở camera (UnsupportedRuntimeError rõ ràng), không gãy
|
|
258
|
+
* lúc import SDK — static-render web của expo-router vẫn nạp được module.
|
|
259
|
+
*/
|
|
260
|
+
export function createNativeRtmpBindings(): NativeRtmpBindings {
|
|
261
|
+
const native = require("@pickleball/rtmp-native").default;
|
|
262
|
+
const { Platform, PermissionsAndroid } = require("react-native");
|
|
263
|
+
return {
|
|
264
|
+
prepare: (options) => native.prepare(options),
|
|
265
|
+
// Android: xin quyền THUẦN, không mở camera như getUserMedia của WebRTC.
|
|
266
|
+
// Máy MediaTek (Xiaomi tanzanite) chỉ cho một camera mở cùng lúc — camera
|
|
267
|
+
// WebRTC chưa nhả (~400ms) là HaishinKit dính "Too many users (-87)" và
|
|
268
|
+
// preview đen. iOS không có giới hạn đó nên để engine fallback getUserMedia.
|
|
269
|
+
...(Platform.OS === "android"
|
|
270
|
+
? {
|
|
271
|
+
async requestPermissions() {
|
|
272
|
+
const granted = await PermissionsAndroid.requestMultiple([
|
|
273
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
274
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
275
|
+
]);
|
|
276
|
+
return (
|
|
277
|
+
granted[PermissionsAndroid.PERMISSIONS.CAMERA] ===
|
|
278
|
+
PermissionsAndroid.RESULTS.GRANTED &&
|
|
279
|
+
granted[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] ===
|
|
280
|
+
PermissionsAndroid.RESULTS.GRANTED
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
: {}),
|
|
285
|
+
isLandscape() {
|
|
286
|
+
const { width, height } = require("react-native").Dimensions.get("window");
|
|
287
|
+
return width > height;
|
|
288
|
+
},
|
|
289
|
+
connect: (options) => native.connect(options),
|
|
290
|
+
setPublishing: (enabled) => native.setPublishing(enabled),
|
|
291
|
+
disconnect: () => native.disconnect(),
|
|
292
|
+
switchCamera: () => native.switchCamera(),
|
|
293
|
+
listCameras: () => native.listCameras(),
|
|
294
|
+
selectCamera: (deviceId, facing) => native.selectCamera(deviceId, facing),
|
|
295
|
+
setMicrophoneEnabled: (enabled) => native.setMicrophoneEnabled(enabled),
|
|
296
|
+
setVideoBitrate: (bps) => native.setVideoBitrate(bps),
|
|
297
|
+
dispose: () => native.dispose(),
|
|
298
|
+
addStatusListener: (listener) => native.addListener("onStatusChanged", listener),
|
|
299
|
+
addBandwidthListener: (listener) => native.addListener("onBandwidth", listener),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** roomFactory cho mode device-rtmp (thiết bị đẩy RTMP, không dùng LiveKit). */
|
|
304
|
+
export function createRtmpRoomFactory(options?: {
|
|
305
|
+
/**
|
|
306
|
+
* fps capture + encode (24 tiết kiệm băng thông, 30 mặc định của native).
|
|
307
|
+
* 60 chỉ nên truyền khi `getCameraCapabilities().supportedFrameRates` có nó —
|
|
308
|
+
* native bỏ qua trong im lặng nếu ống kính đang gắn không chạy được.
|
|
309
|
+
*/
|
|
310
|
+
frameRate?: 24 | 30 | 60;
|
|
311
|
+
/** Camera mở sẵn lúc prepare — thiếu = camera sau mặc định của native. */
|
|
312
|
+
cameraId?: string;
|
|
313
|
+
}): EngineDependencies["roomFactory"] {
|
|
314
|
+
return () => new RtmpRoomAdapter(createNativeRtmpBindings(), options);
|
|
160
315
|
}
|
|
161
316
|
|
|
162
317
|
/**
|
|
@@ -214,6 +369,21 @@ export function createDefaultDeviceMetrics(): NonNullable<
|
|
|
214
369
|
/* expo-device chưa được cài */
|
|
215
370
|
}
|
|
216
371
|
|
|
372
|
+
// Nhiệt máy + nhiệt pin: chỉ có đường native (không package Expo nào đọc
|
|
373
|
+
// được). Guard require như các nguồn khác — app không cài rtmp-native thì
|
|
374
|
+
// bỏ qua hai trường này thay vì gãy cả collector.
|
|
375
|
+
try {
|
|
376
|
+
const native = require("@pickleball/rtmp-native").default;
|
|
377
|
+
const health = await native.getDeviceHealth();
|
|
378
|
+
if (typeof health?.thermal === "number") metrics.thermal = health.thermal;
|
|
379
|
+
if (typeof health?.batteryTempC === "number") {
|
|
380
|
+
// Làm tròn 1 số lẻ: pin báo theo 1/10 °C, giữ nguyên là đủ chính xác.
|
|
381
|
+
metrics.batteryTempC = Math.round(health.batteryTempC * 10) / 10;
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
notes.push(`device health: ${String(error)}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
217
387
|
try {
|
|
218
388
|
const NetInfo = require("@react-native-community/netinfo").default;
|
|
219
389
|
const status = await NetInfo.fetch();
|
|
@@ -227,6 +397,42 @@ export function createDefaultDeviceMetrics(): NonNullable<
|
|
|
227
397
|
).toUpperCase()
|
|
228
398
|
: status.type;
|
|
229
399
|
}
|
|
400
|
+
// Chi tiết mạng cho việc truy vết sự cố tại sân: máy còn bám router của
|
|
401
|
+
// giải hay đã rớt sang 4G, sóng yếu tới mức nào.
|
|
402
|
+
//
|
|
403
|
+
// CỐ Ý không đọc `details.ssid`/`bssid`: hai field đó bị khoá sau quyền
|
|
404
|
+
// VỊ TRÍ (Android cần ACCESS_FINE_LOCATION + bật GPS; iOS cần entitlement
|
|
405
|
+
// Access WiFi Information). Xin quyền vị trí cho mỗi máy đặt ở sân chỉ để
|
|
406
|
+
// hiện tên WiFi là cái giá quá đắt — IP nội bộ dưới đây phân biệt được
|
|
407
|
+
// router mà không cần quyền nào.
|
|
408
|
+
const details = status?.details as
|
|
409
|
+
| {
|
|
410
|
+
ipAddress?: unknown;
|
|
411
|
+
strength?: unknown;
|
|
412
|
+
linkSpeed?: unknown;
|
|
413
|
+
frequency?: unknown;
|
|
414
|
+
}
|
|
415
|
+
| undefined;
|
|
416
|
+
if (typeof details?.ipAddress === "string" && details.ipAddress) {
|
|
417
|
+
metrics.localIp = details.ipAddress;
|
|
418
|
+
}
|
|
419
|
+
// strength: 0-100 (%). CHỈ Android — iOS không mở cường độ sóng cho app.
|
|
420
|
+
if (typeof details?.strength === "number" && Number.isFinite(details.strength)) {
|
|
421
|
+
metrics.signalPct = Math.round(details.strength);
|
|
422
|
+
}
|
|
423
|
+
if (typeof details?.linkSpeed === "number" && Number.isFinite(details.linkSpeed)) {
|
|
424
|
+
metrics.linkMbps = Math.round(details.linkSpeed);
|
|
425
|
+
}
|
|
426
|
+
// Tần số kênh (Android) — phân biệt 2.4/5 GHz không cần quyền vị trí.
|
|
427
|
+
// Chỉ đi vào telemetry PHIÊN (dataJson tự do); heartbeat thiết bị lọc
|
|
428
|
+
// theo METRIC_FIELDS nên không đụng validator server.
|
|
429
|
+
if (
|
|
430
|
+
typeof details?.frequency === "number" &&
|
|
431
|
+
Number.isFinite(details.frequency) &&
|
|
432
|
+
details.frequency > 0
|
|
433
|
+
) {
|
|
434
|
+
metrics.freqMhz = Math.round(details.frequency);
|
|
435
|
+
}
|
|
230
436
|
} catch {
|
|
231
437
|
/* netinfo không khả dụng */
|
|
232
438
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -4,38 +4,71 @@ import {
|
|
|
4
4
|
withAndroidManifest,
|
|
5
5
|
withDangerousMod,
|
|
6
6
|
withInfoPlist,
|
|
7
|
-
withPlugins,
|
|
8
7
|
type ConfigPlugin,
|
|
9
8
|
} from "expo/config-plugins";
|
|
10
9
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
11
10
|
import { join } from "node:path";
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Version của SCHEMA plugin — tách khỏi version package có chủ ý. Giá trị này
|
|
14
|
+
* được inline vào dist/plugin.cjs, mà @expo/fingerprint băm file đó làm
|
|
15
|
+
* runtimeVersion của app fleet: gắn theo version package là MỖI lần bump SDK
|
|
16
|
+
* (kể cả patch JS-only) đổi fingerprint và toàn bộ máy ngoài sân mất OTA. Chỉ
|
|
17
|
+
* nâng khi plugin đổi hành vi native (permission, mod).
|
|
18
|
+
*/
|
|
19
|
+
export const PLUGIN_SCHEMA_VERSION = "1";
|
|
13
20
|
|
|
14
21
|
export interface PickleballLivePluginProps {
|
|
15
22
|
cameraPermission?: string;
|
|
16
23
|
microphonePermission?: string;
|
|
17
24
|
bluetoothPermission?: string;
|
|
18
25
|
consumeHidVolumeKeys?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Tắt phần cấu hình native của module không dùng để app gọn hơn:
|
|
28
|
+
* - remote: false → bỏ permission Bluetooth (Android + iOS usage string) và
|
|
29
|
+
* không patch MainActivity nhận phím HID. Dùng khi app không import
|
|
30
|
+
* "@pickleball/expo-sdk/match/remote".
|
|
31
|
+
* - voice: false → hiện chưa gate gì trong plugin (mic thuộc livestream);
|
|
32
|
+
* nhận trước để tương thích về sau. Muốn loại hẳn native khỏi binary, thêm
|
|
33
|
+
* expo.autolinking.exclude trong package.json của app (xem README).
|
|
34
|
+
* - livekit: KHÔNG còn tác dụng từ 1.0 (SDK RTMP-only, không compose plugin
|
|
35
|
+
* LiveKit nữa). Vẫn nhận để app.config cũ của fleet không phải sửa — sửa
|
|
36
|
+
* là đổi fingerprint OTA.
|
|
37
|
+
*/
|
|
38
|
+
modules?: {
|
|
39
|
+
remote?: boolean;
|
|
40
|
+
voice?: boolean;
|
|
41
|
+
/** @deprecated bị bỏ qua từ 1.0 */
|
|
42
|
+
livekit?: boolean;
|
|
43
|
+
};
|
|
44
|
+
/** @deprecated bị bỏ qua từ 1.0 (SDK không còn LiveKit). */
|
|
19
45
|
liveKit?: {
|
|
20
46
|
android?: { audioType?: string; enableScreenShareService?: boolean };
|
|
21
47
|
ios?: { enableMultitaskingCameraAccess?: boolean };
|
|
22
48
|
};
|
|
23
49
|
}
|
|
24
50
|
|
|
25
|
-
const
|
|
51
|
+
const CORE_ANDROID_PERMISSIONS = [
|
|
26
52
|
"android.permission.ACCESS_NETWORK_STATE",
|
|
27
|
-
"android.permission.BLUETOOTH",
|
|
28
|
-
"android.permission.BLUETOOTH_ADMIN",
|
|
29
|
-
"android.permission.BLUETOOTH_SCAN",
|
|
30
|
-
"android.permission.BLUETOOTH_CONNECT",
|
|
31
|
-
"android.permission.ACCESS_FINE_LOCATION",
|
|
32
53
|
"android.permission.CAMERA",
|
|
54
|
+
"android.permission.FOREGROUND_SERVICE",
|
|
55
|
+
"android.permission.FOREGROUND_SERVICE_CAMERA",
|
|
56
|
+
"android.permission.FOREGROUND_SERVICE_MICROPHONE",
|
|
33
57
|
"android.permission.INTERNET",
|
|
34
58
|
"android.permission.MODIFY_AUDIO_SETTINGS",
|
|
35
59
|
"android.permission.RECORD_AUDIO",
|
|
36
60
|
"android.permission.WAKE_LOCK",
|
|
37
61
|
] as const;
|
|
38
62
|
|
|
63
|
+
// Chỉ cần cho remote BLE/HID (@pickleball/expo-sdk/match/remote)
|
|
64
|
+
const REMOTE_ANDROID_PERMISSIONS = [
|
|
65
|
+
"android.permission.BLUETOOTH",
|
|
66
|
+
"android.permission.BLUETOOTH_ADMIN",
|
|
67
|
+
"android.permission.BLUETOOTH_SCAN",
|
|
68
|
+
"android.permission.BLUETOOTH_CONNECT",
|
|
69
|
+
"android.permission.ACCESS_FINE_LOCATION",
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
39
72
|
export function injectHidVolumeMainActivity(source: string): string {
|
|
40
73
|
if (source.includes("PickleballRemoteHidDispatcher.dispatch(event)")) return source;
|
|
41
74
|
if (!/class\s+MainActivity\s*:\s*ReactActivity\(\)\s*\{/.test(source)) {
|
|
@@ -76,20 +109,21 @@ const withPickleballLive: ConfigPlugin<PickleballLivePluginProps | undefined> =
|
|
|
76
109
|
const bluetoothPermission =
|
|
77
110
|
props?.bluetoothPermission ??
|
|
78
111
|
"Cho phép $(PRODUCT_NAME) dùng Bluetooth để nhận thao tác từ nút điều khiển trên sân.";
|
|
112
|
+
const remoteEnabled = props?.modules?.remote !== false;
|
|
113
|
+
const androidPermissions = remoteEnabled
|
|
114
|
+
? [...CORE_ANDROID_PERMISSIONS, ...REMOTE_ANDROID_PERMISSIONS]
|
|
115
|
+
: [...CORE_ANDROID_PERMISSIONS];
|
|
79
116
|
|
|
80
|
-
config = withPlugins(config, [
|
|
81
|
-
["@livekit/react-native-expo-plugin", props?.liveKit],
|
|
82
|
-
]);
|
|
83
117
|
config.ios = { ...config.ios, bitcode: false };
|
|
84
118
|
config = withInfoPlist(config, (mod) => {
|
|
85
119
|
mod.modResults.NSCameraUsageDescription = cameraPermission;
|
|
86
120
|
mod.modResults.NSMicrophoneUsageDescription = microphonePermission;
|
|
87
|
-
|
|
121
|
+
if (remoteEnabled) {
|
|
122
|
+
mod.modResults.NSBluetoothAlwaysUsageDescription = bluetoothPermission;
|
|
123
|
+
}
|
|
88
124
|
return mod;
|
|
89
125
|
});
|
|
90
|
-
config = AndroidConfig.Permissions.withPermissions(config,
|
|
91
|
-
...ANDROID_PERMISSIONS,
|
|
92
|
-
]);
|
|
126
|
+
config = AndroidConfig.Permissions.withPermissions(config, androidPermissions);
|
|
93
127
|
config = withAndroidManifest(config, (mod) => {
|
|
94
128
|
const manifest = mod.modResults.manifest;
|
|
95
129
|
const existing = manifest["uses-permission"] ?? [];
|
|
@@ -106,7 +140,7 @@ const withPickleballLive: ConfigPlugin<PickleballLivePluginProps | undefined> =
|
|
|
106
140
|
const name = entry.$?.["android:name"];
|
|
107
141
|
if (name && !byName.has(name)) byName.set(name, entry);
|
|
108
142
|
}
|
|
109
|
-
for (const permission of
|
|
143
|
+
for (const permission of androidPermissions) {
|
|
110
144
|
const entry = byName.get(permission) ?? { $: { "android:name": permission } };
|
|
111
145
|
entry.$ = {
|
|
112
146
|
"android:name": permission,
|
|
@@ -114,17 +148,17 @@ const withPickleballLive: ConfigPlugin<PickleballLivePluginProps | undefined> =
|
|
|
114
148
|
};
|
|
115
149
|
byName.set(permission, entry);
|
|
116
150
|
}
|
|
117
|
-
const requiredNames = new Set<string>(
|
|
151
|
+
const requiredNames = new Set<string>(androidPermissions);
|
|
118
152
|
manifest["uses-permission"] = [
|
|
119
153
|
...existing.filter((entry) => {
|
|
120
154
|
const name = entry.$?.["android:name"];
|
|
121
155
|
return !name || !requiredNames.has(name);
|
|
122
156
|
}),
|
|
123
|
-
...
|
|
157
|
+
...androidPermissions.map((name) => byName.get(name)!),
|
|
124
158
|
];
|
|
125
159
|
return mod;
|
|
126
160
|
});
|
|
127
|
-
if (props?.consumeHidVolumeKeys !== false) {
|
|
161
|
+
if (remoteEnabled && props?.consumeHidVolumeKeys !== false) {
|
|
128
162
|
config = withDangerousMod(config, ["android", async (mod) => {
|
|
129
163
|
const root = join(mod.modRequest.platformProjectRoot, "app", "src", "main", "java");
|
|
130
164
|
const path = await findMainActivity(root);
|
|
@@ -141,5 +175,5 @@ const withPickleballLive: ConfigPlugin<PickleballLivePluginProps | undefined> =
|
|
|
141
175
|
export default createRunOncePlugin(
|
|
142
176
|
withPickleballLive,
|
|
143
177
|
"@pickleball/expo-sdk",
|
|
144
|
-
|
|
178
|
+
PLUGIN_SCHEMA_VERSION,
|
|
145
179
|
);
|