@pickleball/rtmp-native 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pickleball/rtmp-native",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Hardware-encoded RTMP publisher for Pickleball Live (device-rtmp delivery mode)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,9 +31,9 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/react": "~19.2.0",
34
- "expo": "57.0.4",
34
+ "expo": "~57.0.14",
35
35
  "react": "19.2.0",
36
- "react-native": "0.86.0",
36
+ "react-native": "0.86.2",
37
37
  "typescript": "^5.8.0"
38
38
  },
39
39
  "scripts": {
@@ -17,11 +17,53 @@ export interface NativeRtmpBandwidth {
17
17
  readonly videoBitrate: number;
18
18
  }
19
19
 
20
+ /**
21
+ * Nhiệt/pin của máy — trùng tên trường với `deviceMetrics` phía server.
22
+ *
23
+ * `thermal` dùng THANG CỦA ANDROID (`PowerManager.THERMAL_STATUS_*`): 0 none ·
24
+ * 1 light · 2 moderate · 3 severe · 4 critical · 5 emergency · 6 shutdown.
25
+ * iOS chỉ có 4 bậc nên native đã quy đổi sang thang này (serious→3,
26
+ * critical→4) để ngưỡng "máy quá nóng ≥ 3" của dashboard đúng trên cả hai nền.
27
+ *
28
+ * Cả hai optional: Android < API 29 không có `thermal`; iOS KHÔNG có
29
+ * `batteryTempC` (không API công khai nào cho nhiệt độ pin).
30
+ */
31
+ export interface NativeDeviceHealth {
32
+ readonly thermal?: number;
33
+ readonly batteryTempC?: number;
34
+ }
35
+
36
+ /**
37
+ * Một lần tiến trình app chết trước đó, theo ApplicationExitInfo của Android 11+.
38
+ * `reasonName` là tên hằng REASON_* ("PACKAGE_UPDATED", "CRASH", "ANR", "OTHER"…)
39
+ * — native đã quy đổi để JS/dashboard không giữ bảng mã. `description` là chuỗi
40
+ * hệ thống tự do, VD "stop com.google.android.webview due to installPackageLI"
41
+ * hay "SwipeUpClean" (HyperOS). iOS luôn trả mảng rỗng.
42
+ */
43
+ export interface NativeProcessExit {
44
+ /** epoch ms lúc tiến trình chết. */
45
+ readonly at: number;
46
+ readonly pid: number;
47
+ readonly reason: number;
48
+ readonly reasonName: string;
49
+ readonly description?: string;
50
+ /** RunningAppProcessInfo.IMPORTANCE_* lúc chết — 100 = đang ở tiền cảnh. */
51
+ readonly importance: number;
52
+ readonly status: number;
53
+ }
54
+
20
55
  export interface NativeRtmpStats {
21
56
  readonly isConnected: boolean;
22
57
  readonly isPublishing: boolean;
23
58
  readonly videoBitrate: number;
24
59
  readonly currentFps: number;
60
+ /**
61
+ * Tổng byte đã RỜI máy qua socket RTMP; -1 = chưa có số liệu. iOS nuôi từ
62
+ * NetworkMonitor (~1s/nhịp) — watchdog native tự cắt khi số này đứng yên 30s
63
+ * lúc đang publish (reason "media_stalled") để engine quay số lại. Android
64
+ * chưa trả trường này.
65
+ */
66
+ readonly totalBytesOut?: number;
25
67
  }
26
68
 
27
69
  export interface RtmpPrepareOptions {
@@ -34,6 +76,11 @@ export interface RtmpPrepareOptions {
34
76
  * HaishinKit nướng hướng thẳng vào ảnh đã mã hoá nên phải biết trước.
35
77
  */
36
78
  readonly landscape?: boolean;
79
+ /**
80
+ * Camera mở ngay ở lần prepare này. Thiếu = native giữ camera sau mặc định.
81
+ * Id không tồn tại trên máy sẽ bị native bỏ qua thay vì ném lỗi.
82
+ */
83
+ readonly cameraId?: string;
37
84
  }
38
85
 
39
86
  export interface RtmpConnectOptions {
@@ -57,3 +104,96 @@ export interface NativeCameraInfo {
57
104
  /** "environment" = camera sau — khớp cách đặt tên của WebRTC. */
58
105
  readonly facing: "front" | "environment";
59
106
  }
107
+
108
+ /**
109
+ * Khả năng điều khiển của ống kính ĐANG gắn — refresh sau mỗi lần đổi lens.
110
+ *
111
+ * `supported:false` = control không dùng được trên máy/lens này (hoặc reflection
112
+ * vào HaishinKit Android không đào được) → UI ẨN control tương ứng, không hiện
113
+ * nút chết. Degrade từng control, không phải cả sheet.
114
+ */
115
+ export interface CameraCapabilities {
116
+ /**
117
+ * Các mức fps ống kính ĐANG gắn thật sự chạy được, lọc từ {24, 30, 60}.
118
+ *
119
+ * Native lọc theo đúng tiêu chí lúc chạy (iOS: có format cùng kích thước với
120
+ * activeFormat phủ được fps; Android: có AE target range phủ được fps) — cả
121
+ * hai đều bỏ qua trong im lặng nếu không khớp, nên UI chỉ được chào những
122
+ * mức có trong đây. Không bao giờ rỗng: thiếu dữ liệu thì trả [30].
123
+ */
124
+ readonly supportedFrameRates: readonly number[];
125
+ readonly zoom: { readonly min: number; readonly max: number; readonly supported: boolean };
126
+ readonly exposureBias: {
127
+ readonly min: number;
128
+ readonly max: number;
129
+ readonly step: number;
130
+ readonly supported: boolean;
131
+ };
132
+ readonly torch: { readonly supported: boolean };
133
+ readonly focusLock: { readonly supported: boolean };
134
+ readonly exposureLock: { readonly supported: boolean };
135
+ }
136
+
137
+ /**
138
+ * Ô đặt logo trên khung hình, tính bằng PIXEL của độ phân giải đang encode,
139
+ * gốc trên-trái. Dựng bằng `logoRect()` của @pickleball/shared.
140
+ */
141
+ export interface RtmpOverlayLogo {
142
+ /** File ảnh đã tải về đĩa máy (app cache theo URL). */
143
+ filePath: string;
144
+ x: number;
145
+ y: number;
146
+ width: number;
147
+ height: number;
148
+ /** 0-1. Native nướng thẳng vào pixel vì ScreenObject không có opacity. */
149
+ opacity: number;
150
+ }
151
+
152
+ /**
153
+ * Một lớp overlay ĐỘNG (gif chạy khung / video lặp) — vẽ chồng lên logo tĩnh.
154
+ * Cùng hệ pixel với RtmpOverlayLogo. `opacity`: gif nướng vào từng khung lúc
155
+ * decode; video bị ép về 1 (AssetScreenObject không có opacity).
156
+ */
157
+ export interface RtmpOverlayAnimatedItem {
158
+ /** Bất biến theo lớp — native tái dùng object/animator theo id. */
159
+ id: string;
160
+ kind: "gif" | "video";
161
+ filePath: string;
162
+ x: number;
163
+ y: number;
164
+ width: number;
165
+ height: number;
166
+ opacity: number;
167
+ }
168
+
169
+ /**
170
+ * Một lớp CHỮ đã điền sẵn (JS điền biến từ tỉ số/metadata — native chỉ VẼ,
171
+ * không biết template). Cùng hệ pixel với RtmpOverlayLogo; `x` là MỎ NEO theo
172
+ * `align` (left = mép trái chữ tại x, center = tâm, right = mép phải), `y` là
173
+ * mép TRÊN của nền (không nền thì của chữ).
174
+ *
175
+ * Chữ đổi ~2 lần/phút (mỗi pha bóng) — native cập nhật layer TẠI CHỖ theo `id`,
176
+ * tuyệt đối không rebuild cả dàn layer (Android: rebuild là restart MediaPlayer
177
+ * của lớp video — churn đã đo).
178
+ */
179
+ export interface RtmpOverlayTextItem {
180
+ /** Bất biến theo lớp — native tái dùng texture/ScreenObject theo id. */
181
+ id: string;
182
+ text: string;
183
+ x: number;
184
+ y: number;
185
+ fontSizePx: number;
186
+ align: "left" | "center" | "right";
187
+ weight: "regular" | "bold";
188
+ /** #rrggbb. */
189
+ color: string;
190
+ /** 0-1 cho CẢ lớp (chữ + nền) — nướng vào pixel lúc vẽ. */
191
+ opacity: number;
192
+ /**
193
+ * Nền sau chữ — field PHẲNG (Record hai nền tảng không phải lồng object).
194
+ * Không có bgColor = không nền. Padding/bo góc suy từ fontSizePx theo
195
+ * OVERLAY_TEXT_BG của shared (0.45/0.25/0.22 × fontSize).
196
+ */
197
+ bgColor?: string;
198
+ bgOpacity?: number;
199
+ }
@@ -1,13 +1,27 @@
1
1
  import { NativeModule, requireNativeModule } from "expo";
2
2
  import type {
3
+ CameraCapabilities,
3
4
  NativeCameraInfo,
5
+ NativeDeviceHealth,
6
+ NativeProcessExit,
4
7
  NativeRtmpStats,
5
8
  PickleballRtmpNativeEvents,
6
9
  RtmpConnectOptions,
10
+ RtmpOverlayAnimatedItem,
11
+ RtmpOverlayLogo,
12
+ RtmpOverlayTextItem,
7
13
  RtmpPrepareOptions,
8
14
  } from "./PickleballRtmpNative.types";
9
15
 
10
16
  declare class PickleballRtmpNativeModule extends NativeModule<PickleballRtmpNativeEvents> {
17
+ /**
18
+ * Xin quyền camera + mic (iOS, ≥ 0.2.0): SDK 1.0 không còn getUserMedia của
19
+ * WebRTC để "mượn" hộp thoại quyền, nên hỏi qua AVFoundation TRƯỚC khi
20
+ * prepare — không thì camera đen im lặng ở lần mở đầu tiên. Android dùng
21
+ * PermissionsAndroid ở JS (không có hàm này); binary cũ cũng thiếu → JS
22
+ * PHẢI guard `typeof` trước khi gọi. false = một trong hai quyền bị từ chối.
23
+ */
24
+ requestPermissions(): Promise<boolean>;
11
25
  /** Mở camera + mic và bắt đầu capture (chưa gửi đi đâu). */
12
26
  prepare(options: RtmpPrepareOptions): Promise<void>;
13
27
  /** Kết nối tới server RTMP; publish ngay nếu options.publish. */
@@ -22,7 +36,62 @@ declare class PickleballRtmpNativeModule extends NativeModule<PickleballRtmpNati
22
36
  setMicrophoneEnabled(enabled: boolean): Promise<void>;
23
37
  /** Hạ/nâng bitrate khi mạng đổi — thay cho BWE của WebRTC. */
24
38
  setVideoBitrate(bitsPerSecond: number): Promise<void>;
39
+ /** Bounds + cờ hỗ trợ của lens đang gắn — UI ẩn control có supported:false. */
40
+ getCameraCapabilities(): Promise<CameraCapabilities>;
41
+ setZoom(factor: number): Promise<void>;
42
+ setExposureBias(ev: number): Promise<void>;
43
+ setFocusLocked(locked: boolean): Promise<void>;
44
+ setExposureLocked(locked: boolean): Promise<void>;
45
+ setTorch(enabled: boolean): Promise<void>;
46
+ /**
47
+ * Chống chớp đèn (antibanding 50/60Hz). Android-only — iOS không có API
48
+ * tương đương (sau này đi đường shutter-priority riêng), và build cũ cũng
49
+ * thiếu hàm: JS PHẢI guard `typeof` trước khi gọi. Không ném khi chưa có
50
+ * camera — native lưu giá trị và tự áp lại mỗi lần mở camera/đổi lens.
51
+ */
52
+ setAntiFlicker(mode: "auto" | "50hz" | "60hz"): Promise<void>;
53
+ /**
54
+ * Logo nướng vào khung hình trước encoder; `null` = gỡ.
55
+ *
56
+ * Ô đặt logo tính SẴN ở JS bằng `logoRect()` của @pickleball/shared — cùng
57
+ * công thức dashboard vẽ preview, nên "thấy sao ra vậy". Native cố ý không
58
+ * tính lại: hai bản công thức lệch nhau được, mà cái lệch bị nướng vĩnh viễn
59
+ * vào video.
60
+ *
61
+ * Có logo thì mixer chuyển sang render offscreen (tốn thêm một vòng vẽ mỗi
62
+ * khung); `null` trả về passthrough như cũ.
63
+ */
64
+ setOverlayLogo(options: RtmpOverlayLogo | null): Promise<Record<string, unknown> | void>;
65
+ /**
66
+ * Các lớp overlay ĐỘNG (gif/video) — `null` = gỡ hết. Vẽ CHỒNG LÊN logo/
67
+ * composite tĩnh theo thứ tự mảng. Build cũ không có hàm này — JS phải guard
68
+ * `typeof` trước khi gọi. iOS trước; Android chưa có (trả lỗi/undefined).
69
+ */
70
+ setOverlayAnimated(items: RtmpOverlayAnimatedItem[] | null): Promise<Record<string, unknown> | void>;
71
+ /**
72
+ * Các lớp CHỮ đã điền sẵn (tỉ số/tên đội) — `null` = gỡ hết. Trên cùng mọi
73
+ * lớp khác. Native cập nhật TẠI CHỖ theo `id` khi chỉ có chuỗi đổi — không
74
+ * rebuild dàn layer. Build cũ không có hàm này — JS guard `typeof`.
75
+ */
76
+ setOverlayTexts(items: RtmpOverlayTextItem[] | null): Promise<Record<string, unknown> | void>;
25
77
  getStats(): NativeRtmpStats;
78
+ /**
79
+ * Nhiệt/pin của máy cho heartbeat. Không package Expo nào đọc được hai giá
80
+ * trị này nên phải đi đường native. Mọi trường optional — máy không báo thì
81
+ * thiếu trường, KHÔNG ném lỗi (đang chạy trong nhịp heartbeat).
82
+ */
83
+ getDeviceHealth(): Promise<NativeDeviceHealth>;
84
+ /**
85
+ * Hộp đen: các lần tiến trình chết gần đây, mới nhất trước (tối đa `max`,
86
+ * kẹp 1–16). Android 11+; máy cũ hơn và iOS trả []. Không bao giờ ném.
87
+ */
88
+ getProcessExitReasons(max: number): Promise<NativeProcessExit[]>;
89
+ /**
90
+ * Chụp 1 ảnh JPEG từ khung hình đang compose → chuỗi base64 (không kèm tiền tố
91
+ * data:). Dùng cho xem trước từ xa trên dashboard khi previewActive. Ném lỗi
92
+ * nếu camera chưa chạy (chưa prepare / chưa có frame).
93
+ */
94
+ snapshot(): Promise<string>;
26
95
  dispose(): Promise<void>;
27
96
  }
28
97