@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
|
@@ -2,9 +2,22 @@ type LivestreamState = "idle" | "preparing" | "preview" | "connecting"
|
|
|
2
2
|
/** Đã vào phòng, chưa phát — chờ lệnh phát từ app hoặc dashboard. */
|
|
3
3
|
| "standby" | "live" | "reconnecting" | "ending" | "ended" | "error";
|
|
4
4
|
type SdkErrorCode = "PERMISSION_DENIED" | "NETWORK_UNAVAILABLE" | "TOKEN_EXPIRED" | "SDK_UPGRADE_REQUIRED" | "SESSION_CONFLICT" | "RECORDING_FAILED" | "CONSENT_REQUIRED" | "SDK_DISABLED" | "UNKNOWN";
|
|
5
|
+
/**
|
|
6
|
+
* Vì sao 409 SESSION_CONFLICT bị nhận — ba ca cần app hành xử KHÁC NHAU mà
|
|
7
|
+
* trước đây chỉ phân biệt được bằng đọc chuỗi tiếng Anh trong `message`:
|
|
8
|
+
* - `superseded`: grant này đã bị grant mới thay (đối tác cấp lại camera-grant)
|
|
9
|
+
* → app DỪNG tại chỗ, không `end`, không quay số lại.
|
|
10
|
+
* - `ended`: phiên đã kết thúc → dọn state, không thử lại.
|
|
11
|
+
* - `expired`: phiên đã quá trần thời lượng credential → xin grant/phiên mới.
|
|
12
|
+
*/
|
|
13
|
+
type SdkConflictReason = "superseded" | "ended" | "expired";
|
|
5
14
|
interface SdkError {
|
|
6
15
|
code: SdkErrorCode;
|
|
7
16
|
message: string;
|
|
17
|
+
/** Chỉ có ở 409 SESSION_CONFLICT của namespace /api/v2/camera. */
|
|
18
|
+
reason?: SdkConflictReason;
|
|
19
|
+
/** Chỉ có ở 429 — cùng số với header `Retry-After` (giây, làm tròn lên). */
|
|
20
|
+
retryAfterMs?: number;
|
|
8
21
|
}
|
|
9
22
|
interface BootstrapInput {
|
|
10
23
|
sdkVersion: string;
|
|
@@ -43,6 +56,14 @@ interface StartLivestreamInput {
|
|
|
43
56
|
* Engine tự đặt cờ này theo options.autoPublish; app không cần truyền.
|
|
44
57
|
*/
|
|
45
58
|
standby?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Độ phân giải app THỰC SỰ quay, để server ghi đúng vào phiên. Chỉ app biết
|
|
61
|
+
* con số này: nó là lựa chọn local và app còn override `config.videoQuality`
|
|
62
|
+
* mà server trả về. Không khai thì server dùng mặc định của nó (1080) và bản
|
|
63
|
+
* ghi phiên nói dối — đã cắn 18/08/2026, dashboard báo 1080 trong khi máy
|
|
64
|
+
* phát 720 suốt.
|
|
65
|
+
*/
|
|
66
|
+
quality?: 720 | 1080;
|
|
46
67
|
}
|
|
47
68
|
interface SdkTelemetryCredentials {
|
|
48
69
|
endpoint: string;
|
|
@@ -64,6 +85,36 @@ interface SdkSessionRefresh {
|
|
|
64
85
|
telemetry?: SdkTelemetryCredentials;
|
|
65
86
|
config: SdkRemoteConfig;
|
|
66
87
|
}
|
|
88
|
+
type CameraGrantStatus = "scheduled" | "live";
|
|
89
|
+
/**
|
|
90
|
+
* Camera grant — điện thoại của user ĐỐI TÁC lên sóng (ADR-0002). Backend đối
|
|
91
|
+
* tác nhận từ REST v1 (POST /api/v1/live-sessions {cameraGrant:true} hoặc
|
|
92
|
+
* POST /api/v1/live-sessions/{id}/camera-grant) và chuyển NGUYÊN TRẠNG cho app;
|
|
93
|
+
* app dùng `telemetry.token` làm Bearer với namespace /api/v2/camera/sessions.
|
|
94
|
+
*
|
|
95
|
+
* Superset nghiêm ngặt của SdkSessionGrant: `serverUrl` là RTMP ingest
|
|
96
|
+
* (`rtmp://host:1935/`), `participantToken` là stream key — SDK 0.2 đã nhận
|
|
97
|
+
* hình dạng này qua isSessionGrant nên không cần validator mới ở lớp engine.
|
|
98
|
+
*/
|
|
99
|
+
interface CameraGrant extends SdkSessionGrant {
|
|
100
|
+
/** Không bao giờ "ended" — endpoint trả 409 thay vì grant. */
|
|
101
|
+
status: CameraGrantStatus;
|
|
102
|
+
matchRef: string | null;
|
|
103
|
+
courtRef: string | null;
|
|
104
|
+
watchUrl: string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Origin websocket Convex (https://<dep>.convex.cloud) để SDK subscribe lệnh
|
|
107
|
+
* thay vì poll HTTP 2s; null khi deployment không lộ origin chuẩn → SDK poll.
|
|
108
|
+
*/
|
|
109
|
+
convexUrl: string | null;
|
|
110
|
+
issuedAt: number;
|
|
111
|
+
/**
|
|
112
|
+
* Hạn dùng của grant. Phiên chờ mà app CHƯA từng kết nối bị dọn sau 30 phút
|
|
113
|
+
* (RTMP_STANDBY_SILENT_MS) — xin grant đúng lúc user mở camera, đừng cấp sớm
|
|
114
|
+
* hàng giờ trước trận.
|
|
115
|
+
*/
|
|
116
|
+
expiresAt: number;
|
|
117
|
+
}
|
|
67
118
|
interface EndLivestreamInput {
|
|
68
119
|
sessionId: string;
|
|
69
120
|
reason: "user" | "background_timeout" | "network_timeout" | "standby_timeout" | "error";
|
|
@@ -77,6 +128,22 @@ interface SdkTelemetryEvent {
|
|
|
77
128
|
name: SdkTelemetryEventName;
|
|
78
129
|
data?: Record<string, unknown>;
|
|
79
130
|
}
|
|
131
|
+
/** Lệnh điều khiển phiên giao qua kênh poll (mode device-rtmp). */
|
|
132
|
+
type SessionCommandAction = "start" | "stop" | "end";
|
|
133
|
+
interface SessionCommandRecord {
|
|
134
|
+
id: string;
|
|
135
|
+
action: SessionCommandAction;
|
|
136
|
+
/** Con trỏ tăng nghiêm ngặt theo phiên — client gửi lại làm `after`. */
|
|
137
|
+
issuedAt: number;
|
|
138
|
+
}
|
|
139
|
+
interface SessionCommandPollResult {
|
|
140
|
+
commands: SessionCommandRecord[];
|
|
141
|
+
/**
|
|
142
|
+
* Trạng thái phiên phía server — lưới an toàn khi lệnh rơi: "live" mà máy
|
|
143
|
+
* còn standby thì tự phát, "ended" thì tự kết thúc.
|
|
144
|
+
*/
|
|
145
|
+
status: "scheduled" | "live" | "ended";
|
|
146
|
+
}
|
|
80
147
|
interface LivestreamSessionProvider {
|
|
81
148
|
bootstrap(input: BootstrapInput): Promise<SdkBootstrap>;
|
|
82
149
|
start(input: StartLivestreamInput): Promise<SdkSessionGrant>;
|
|
@@ -318,6 +385,20 @@ type RoomEvent = {
|
|
|
318
385
|
action: string;
|
|
319
386
|
};
|
|
320
387
|
interface RoomAdapter {
|
|
388
|
+
/**
|
|
389
|
+
* Xin quyền camera/mic theo cách của mode. Không có thì engine dùng
|
|
390
|
+
* runtime.requestPermissions() (getUserMedia của WebRTC — mở camera thật
|
|
391
|
+
* ~400ms). Mode device-rtmp PHẢI tự xin bằng API quyền thuần: máy Android
|
|
392
|
+
* MediaTek chỉ cho một camera mở, camera WebRTC chưa nhả là HaishinKit dính
|
|
393
|
+
* "Too many users (-87)" và preview đen.
|
|
394
|
+
*/
|
|
395
|
+
requestPermissions?(): Promise<boolean>;
|
|
396
|
+
/**
|
|
397
|
+
* true = adapter KHÔNG tự nối lại khi rớt (RTMP: native chỉ báo
|
|
398
|
+
* "disconnected" rồi đứng im) — engine phải chủ động quay số lại qua retry().
|
|
399
|
+
* LiveKit bỏ trống: SDK của nó tự hồi và tự bắn "reconnected".
|
|
400
|
+
*/
|
|
401
|
+
readonly needsManualReconnect?: boolean;
|
|
321
402
|
/** Bật/tắt publish track khi đã ở trong phòng (standby ↔ live). */
|
|
322
403
|
setPublishing?(enabled: boolean): Promise<void>;
|
|
323
404
|
setCaptureOrientation?(orientation: CaptureOrientation): Promise<void>;
|
|
@@ -346,18 +427,66 @@ interface SdkNetwork {
|
|
|
346
427
|
isOnline(): boolean;
|
|
347
428
|
subscribe(listener: (online: boolean) => void): () => void;
|
|
348
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Khi app khởi động lại và thấy dấu của một phiên chưa được dừng có chủ ý, hàm
|
|
432
|
+
* này quyết định: GIỮ phiên đó để nhận lại, hay kết thúc nó như trước.
|
|
433
|
+
*
|
|
434
|
+
* Mặc định (không truyền) = kết thúc, giữ nguyên hành vi cho mọi SDK đối tác.
|
|
435
|
+
* App first-party trả true khi máy đang gán sân: lúc đó máy sụp nguồn giữa trận
|
|
436
|
+
* là chuyện của thiết bị gác sân, và nối lại đúng phiên cũ mới giữ được link xem
|
|
437
|
+
* và VOD liền mạch. Máy không gán sân thì vẫn dọn sạch như cũ.
|
|
438
|
+
*/
|
|
439
|
+
/**
|
|
440
|
+
* Được phép async có chủ đích: câu trả lời đúng nằm TRÊN ĐĨA (sân đã gán có
|
|
441
|
+
* persist), còn React state lúc engine khởi động thường CHƯA restore xong.
|
|
442
|
+
* Bản sync-đọc-state đã gây lỗi thật (18/08/2026): recover chạy trước restore
|
|
443
|
+
* → keep=false oan → app tự kết án phiên đáng giữ rồi arm phiên chờ THỨ HAI.
|
|
444
|
+
*/
|
|
445
|
+
type KeepSessionOnRestart = () => boolean | Promise<boolean>;
|
|
349
446
|
interface TelemetrySink {
|
|
350
447
|
send(credentials: SdkTelemetryCredentials, events: readonly SdkTelemetryEvent[]): Promise<void>;
|
|
351
448
|
dispose?(): void;
|
|
352
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Kênh nhận lệnh điều khiển cho mode device-rtmp: RTMP không có data channel
|
|
452
|
+
* nên start/stop/end từ server (match-start của trọng tài, dashboard) về qua
|
|
453
|
+
* HTTP poll, auth bằng chính telemetry token trong grant.
|
|
454
|
+
*/
|
|
455
|
+
interface SessionCommandChannel {
|
|
456
|
+
poll(credentials: SdkTelemetryCredentials, after: number): Promise<SessionCommandPollResult>;
|
|
457
|
+
dispose?(): void;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Kênh nhận lệnh device-rtmp qua WEBSOCKET (Convex subscription) — thay poll
|
|
461
|
+
* HTTP 2s cho máy first-party nối thẳng Convex. Lệnh về tức thì, lúc rảnh gần
|
|
462
|
+
* như 0 function call (poll 2s ở 200 máy là ~518M call/tháng). Có kênh này thì
|
|
463
|
+
* engine hạ poll về nhịp reconcile chậm làm lưới an toàn; thiếu thì poll như cũ.
|
|
464
|
+
*/
|
|
465
|
+
interface SessionCommandSubscription {
|
|
466
|
+
/**
|
|
467
|
+
* Mở subscription cho phiên; `onCommands` được gọi mỗi lần hàng lệnh đổi
|
|
468
|
+
* (kể cả lần đầu). `onError` báo websocket/token hỏng để engine tăng lại nhịp
|
|
469
|
+
* poll. Trả hàm huỷ subscription (KHÔNG đóng client — tái dùng qua phiên).
|
|
470
|
+
*/
|
|
471
|
+
subscribe(credentials: SdkTelemetryCredentials, sessionId: string, onCommands: (commands: SessionCommandRecord[]) => void, onError?: (error: Error) => void): () => void;
|
|
472
|
+
dispose?(): void;
|
|
473
|
+
}
|
|
353
474
|
interface EngineDependencies {
|
|
354
475
|
sessionProvider: LivestreamSessionProvider;
|
|
355
476
|
bootstrapInput: BootstrapInput;
|
|
356
477
|
roomFactory: () => RoomAdapter;
|
|
478
|
+
keepSessionOnRestart?: KeepSessionOnRestart;
|
|
357
479
|
runtime: SdkRuntime;
|
|
358
480
|
storage: SdkStorage;
|
|
359
481
|
network: SdkNetwork;
|
|
360
482
|
telemetry?: TelemetrySink;
|
|
483
|
+
/** Kênh poll lệnh cho phiên device-rtmp; thiếu thì mode này điếc lệnh server. */
|
|
484
|
+
commandChannel?: SessionCommandChannel;
|
|
485
|
+
/**
|
|
486
|
+
* Kênh nhận lệnh qua websocket (first-party). Có thì lệnh về tức thì và poll
|
|
487
|
+
* tụt về nhịp reconcile; thiếu (máy đối tác qua proxy) thì chỉ dùng poll.
|
|
488
|
+
*/
|
|
489
|
+
commandSubscription?: SessionCommandSubscription;
|
|
361
490
|
/** Thu chỉ số thiết bị; gọi mỗi nhịp heartbeat khi đang phát. */
|
|
362
491
|
deviceMetrics?: () => Promise<DeviceMetricsSample | null>;
|
|
363
492
|
now?: () => number;
|
|
@@ -378,6 +507,16 @@ interface DeviceMetricsSample {
|
|
|
378
507
|
storageFreeGb?: number;
|
|
379
508
|
netType?: string;
|
|
380
509
|
linkMbps?: number;
|
|
510
|
+
/** Cường độ sóng WiFi 0-100% — CHỈ Android (iOS không mở cho app). */
|
|
511
|
+
signalPct?: number;
|
|
512
|
+
/** IP nội bộ — phân biệt máy đang bám router nào, không cần quyền vị trí. */
|
|
513
|
+
localIp?: string;
|
|
514
|
+
/**
|
|
515
|
+
* Tần số kênh WiFi (MHz) — CHỈ Android. 2412–2484 = băng 2.4 GHz, ≥ 5000 =
|
|
516
|
+
* 5 GHz. Thay cho SSID (bị khoá sau quyền vị trí): 22/08/2026 một máy rơi từ
|
|
517
|
+
* AP 5 GHz của giải sang WiFi quán 2.4 GHz mà heartbeat không phân biệt được.
|
|
518
|
+
*/
|
|
519
|
+
freqMhz?: number;
|
|
381
520
|
rssi?: number;
|
|
382
521
|
connQuality?: string;
|
|
383
522
|
charging?: boolean;
|
|
@@ -400,6 +539,14 @@ interface LivestreamSnapshot {
|
|
|
400
539
|
selectedCameraId: string | null;
|
|
401
540
|
elapsedMs: number;
|
|
402
541
|
isOnline: boolean;
|
|
542
|
+
/**
|
|
543
|
+
* Phiên cũ còn sống mà app vừa khởi động lại có thể NHẬN LẠI (thay vì tạo phiên
|
|
544
|
+
* mới). Chỉ khác null khi engine tìm thấy dấu của một phiên chưa được dừng có
|
|
545
|
+
* chủ ý VÀ caller nói phiên đó đáng giữ (keepSessionOnRestart). Tầng trên
|
|
546
|
+
* (AutoResume) đọc trường này để nối lại đúng phiên cũ — giữ nguyên link xem và
|
|
547
|
+
* VOD liền một mạch.
|
|
548
|
+
*/
|
|
549
|
+
adoptableSessionId: string | null;
|
|
403
550
|
}
|
|
404
551
|
declare const PENDING_END_KEY = "@pickleball/expo-sdk/pending-end";
|
|
405
552
|
declare const LEGAL_LIVESTREAM_TRANSITIONS: Readonly<Record<LivestreamState, readonly LivestreamState[]>>;
|
|
@@ -428,10 +575,23 @@ declare class LivestreamEngine {
|
|
|
428
575
|
private backgroundTimer?;
|
|
429
576
|
private standbyTimer?;
|
|
430
577
|
private reconnectTimer?;
|
|
578
|
+
private redialTimer?;
|
|
579
|
+
/** Đang trong vòng quay số lại — xem doRetry() để biết vì sao cần cờ này. */
|
|
580
|
+
private redialing;
|
|
431
581
|
private heartbeatTimer?;
|
|
432
582
|
private elapsedTimer?;
|
|
433
583
|
private tokenRefreshTimer?;
|
|
434
584
|
private maxSessionTimer?;
|
|
585
|
+
private commandPollTimer?;
|
|
586
|
+
/** Huỷ subscription websocket lệnh của phiên hiện tại (nếu đang mở). */
|
|
587
|
+
private commandUnsubscribe?;
|
|
588
|
+
/** Khoá phiên+token đang subscribe — đổi thì resubscribe (token refresh/đổi phiên). */
|
|
589
|
+
private commandSubscriptionKey?;
|
|
590
|
+
/** Websocket đã đẩy được ít nhất một lần → coi là sống, cho phép hạ nhịp poll. */
|
|
591
|
+
private commandSubscriptionActive;
|
|
592
|
+
/** issuedAt của lệnh cuối đã xử lý — server chỉ trả lệnh mới hơn mốc này. */
|
|
593
|
+
private commandCursor;
|
|
594
|
+
private commandPollBackoffMs;
|
|
435
595
|
private grant?;
|
|
436
596
|
private startedAt?;
|
|
437
597
|
/** Đang publish track (live) hay chỉ ngồi trong phòng (standby). */
|
|
@@ -471,16 +631,81 @@ declare class LivestreamEngine {
|
|
|
471
631
|
handleRoomEvent(event: RoomEvent): void;
|
|
472
632
|
handleAppState(state: "active" | "background" | "inactive"): void;
|
|
473
633
|
private ensureBackgroundGraceTimer;
|
|
474
|
-
|
|
634
|
+
/**
|
|
635
|
+
* `keepSession` = engine bị tháo nhưng phiên phải sống tiếp để lần mở app sau
|
|
636
|
+
* nhận lại. Mặc định false: giữ NGUYÊN hành vi cũ cho mọi SDK đối tác.
|
|
637
|
+
*
|
|
638
|
+
* Lưu ý dispose KHÔNG BAO GIỜ là đường của "người dùng bấm dừng" — nút dừng đi
|
|
639
|
+
* qua doStop, mà doStop xoá ownedSessionId trước, nên tới đây sessionId đã là
|
|
640
|
+
* undefined. Vì vậy nhánh end ở đây luôn là teardown NGOÀI Ý MUỐN (app bị giết,
|
|
641
|
+
* component remount), đúng chỗ cần giữ phiên.
|
|
642
|
+
*/
|
|
643
|
+
dispose(options?: {
|
|
644
|
+
keepSession?: boolean;
|
|
645
|
+
}): Promise<void>;
|
|
475
646
|
private replayPendingEnd;
|
|
476
647
|
private doReplayPendingEnd;
|
|
648
|
+
/**
|
|
649
|
+
* Phiên tưởng đáng nhận lại hoá ra đã chết phía server → BỎ dấu để máy arm
|
|
650
|
+
* phiên mới được.
|
|
651
|
+
*
|
|
652
|
+
* Thiếu hàm này là máy kẹt cứng: dấu vẫn nằm đó chặn mọi lượt start (đúng vai
|
|
653
|
+
* trò chống phiên song song của nó), mà lượt start duy nhất được cho qua lại là
|
|
654
|
+
* lượt nhận lại chính cái phiên đã chết — không bao giờ thành công. Đã dính
|
|
655
|
+
* thật 18/08/2026: lưới standby 30 phút kết thúc phiên trong lúc máy ngủ, mở
|
|
656
|
+
* lại thì app đứng ở "error" vĩnh viễn.
|
|
657
|
+
*/
|
|
658
|
+
abandonAdoptableSession(sessionId: string): Promise<void>;
|
|
477
659
|
private recoverStartupSession;
|
|
660
|
+
/**
|
|
661
|
+
* Dấu phiên cũ chặn thao tác mới cho tới khi nó được giải quyết.
|
|
662
|
+
*
|
|
663
|
+
* `externalSessionId` = id lượt start đang xin đi qua; bỏ trống nghĩa là
|
|
664
|
+
* prepare (mở camera), vốn phải luôn được phép khi đang định nhận lại — không
|
|
665
|
+
* mở được camera thì không nhận lại được, và đó chính là bẫy làm máy kẹt cứng ở
|
|
666
|
+
* màn lỗi "The previous livestream is still pending end".
|
|
667
|
+
*/
|
|
478
668
|
private ensurePendingEndResolved;
|
|
479
669
|
private readPendingEnd;
|
|
480
670
|
private ownsInMemorySession;
|
|
481
671
|
private startTimers;
|
|
482
672
|
private clearTimers;
|
|
673
|
+
/**
|
|
674
|
+
* Quay số lại với backoff cho adapter cần reconnect thủ công. Mỗi nhịp gọi
|
|
675
|
+
* retry() (refresh credentials + room.connect + enterSession). Thành công →
|
|
676
|
+
* enterSession phát "publishing" → nhánh "reconnected" dọn cả hai timer.
|
|
677
|
+
* Thất bại → doRetry() đưa state về "error" (vẫn còn sessionId) và nhịp sau
|
|
678
|
+
* retry() lại kéo về "reconnecting" — lặp tới khi doom timer 30s kết liễu.
|
|
679
|
+
* Backoff thưa dần để không dội server khi ingest thật sự sập.
|
|
680
|
+
*/
|
|
681
|
+
private scheduleRedial;
|
|
682
|
+
private clearRedialTimer;
|
|
683
|
+
private stopRedialing;
|
|
483
684
|
private clearReconnectTimer;
|
|
685
|
+
/**
|
|
686
|
+
* Poll lệnh chỉ khi phiên là device-rtmp (grant rtmp://) và đang standby/
|
|
687
|
+
* live. Ngoài hai trạng thái đó không có gì để điều khiển: connecting chưa
|
|
688
|
+
* có phiên chạy, reconnecting đã có reconnect timer lo, ending/ended thì thôi.
|
|
689
|
+
*/
|
|
690
|
+
private commandPollingWanted;
|
|
691
|
+
private syncCommandPolling;
|
|
692
|
+
/**
|
|
693
|
+
* Mở/đổi websocket nhận lệnh cho phiên hiện tại. Chỉ máy first-party có
|
|
694
|
+
* dependency này (đối tác qua proxy thì undefined → chỉ poll). Resubscribe
|
|
695
|
+
* khi đổi phiên HOẶC token (refresh) vì cả hai đi vào khoá.
|
|
696
|
+
*/
|
|
697
|
+
private syncCommandSubscription;
|
|
698
|
+
private stopCommandSubscription;
|
|
699
|
+
private stopCommandPolling;
|
|
700
|
+
private scheduleCommandPoll;
|
|
701
|
+
private commandPollInterval;
|
|
702
|
+
/**
|
|
703
|
+
* Áp lệnh từ poll HOẶC subscription vào cùng một state machine. Subscription
|
|
704
|
+
* trả cả lệnh CŨ (after:0) nên phải lọc theo con trỏ ở đây; poll đã lọc
|
|
705
|
+
* server-side (after=cursor) nên bước lọc này vô hại với nó.
|
|
706
|
+
*/
|
|
707
|
+
private applyCommands;
|
|
708
|
+
private pollSessionCommands;
|
|
484
709
|
/** Heartbeat kèm chỉ số thiết bị (nếu collector được cung cấp). */
|
|
485
710
|
private sendHeartbeat;
|
|
486
711
|
private sendTelemetry;
|
|
@@ -513,4 +738,4 @@ declare class LivestreamEngine {
|
|
|
513
738
|
private ensureOperation;
|
|
514
739
|
}
|
|
515
740
|
|
|
516
|
-
export {
|
|
741
|
+
export { normalizeError as $, type SdkTelemetryEvent as A, type BootstrapInput as B, type CameraGrant as C, type DeviceMetricsSample as D, type EndLivestreamInput as E, type SdkTelemetryEventName as F, type SessionCommandAction as G, type SessionCommandChannel as H, InvalidResponseError as I, type SessionCommandPollResult as J, type SessionCommandRecord as K, type LivestreamSessionProvider as L, SessionConflictError as M, NetworkUnavailableError as N, TokenExpiredError as O, PermissionDeniedError as P, UnsupportedRuntimeError as Q, type RoomAdapter as R, type SessionCommandSubscription as S, type TelemetrySink as T, UnknownSdkError as U, UnsupportedSdkError as V, compareSemver as W, errorFromCode as X, isSdkRemoteConfig as Y, isSdkVersionSupported as Z, isSessionGoneError as _, type SdkStorage as a, LivestreamEngine as a0, type RoomConnection as a1, type RoomEvent as a2, type EngineDependencies as a3, LEGAL_LIVESTREAM_TRANSITIONS as a4, PENDING_END_KEY as a5, type SdkSessionGrant as b, type StartLivestreamInput as c, type SdkRemoteConfig as d, type CameraDeviceInfo as e, type CameraGrantStatus as f, type CaptureOrientation as g, ConsentRequiredError as h, type EndReason as i, ExpoSdkConfigurationError as j, type ExpoSdkErrorCode as k, ExpoSdkTimeoutError as l, type LivestreamSnapshot as m, type LivestreamState as n, PickleballExpoError as o, RecordingFailureError as p, SDK_TELEMETRY_EVENT_NAMES as q, type SdkBootstrap as r, type SdkConflictReason as s, SdkDisabledError as t, type SdkError as u, type SdkErrorCode as v, type SdkNetwork as w, type SdkRuntime as x, type SdkSessionRefresh as y, type SdkTelemetryCredentials as z };
|
|
@@ -2,9 +2,22 @@ type LivestreamState = "idle" | "preparing" | "preview" | "connecting"
|
|
|
2
2
|
/** Đã vào phòng, chưa phát — chờ lệnh phát từ app hoặc dashboard. */
|
|
3
3
|
| "standby" | "live" | "reconnecting" | "ending" | "ended" | "error";
|
|
4
4
|
type SdkErrorCode = "PERMISSION_DENIED" | "NETWORK_UNAVAILABLE" | "TOKEN_EXPIRED" | "SDK_UPGRADE_REQUIRED" | "SESSION_CONFLICT" | "RECORDING_FAILED" | "CONSENT_REQUIRED" | "SDK_DISABLED" | "UNKNOWN";
|
|
5
|
+
/**
|
|
6
|
+
* Vì sao 409 SESSION_CONFLICT bị nhận — ba ca cần app hành xử KHÁC NHAU mà
|
|
7
|
+
* trước đây chỉ phân biệt được bằng đọc chuỗi tiếng Anh trong `message`:
|
|
8
|
+
* - `superseded`: grant này đã bị grant mới thay (đối tác cấp lại camera-grant)
|
|
9
|
+
* → app DỪNG tại chỗ, không `end`, không quay số lại.
|
|
10
|
+
* - `ended`: phiên đã kết thúc → dọn state, không thử lại.
|
|
11
|
+
* - `expired`: phiên đã quá trần thời lượng credential → xin grant/phiên mới.
|
|
12
|
+
*/
|
|
13
|
+
type SdkConflictReason = "superseded" | "ended" | "expired";
|
|
5
14
|
interface SdkError {
|
|
6
15
|
code: SdkErrorCode;
|
|
7
16
|
message: string;
|
|
17
|
+
/** Chỉ có ở 409 SESSION_CONFLICT của namespace /api/v2/camera. */
|
|
18
|
+
reason?: SdkConflictReason;
|
|
19
|
+
/** Chỉ có ở 429 — cùng số với header `Retry-After` (giây, làm tròn lên). */
|
|
20
|
+
retryAfterMs?: number;
|
|
8
21
|
}
|
|
9
22
|
interface BootstrapInput {
|
|
10
23
|
sdkVersion: string;
|
|
@@ -43,6 +56,14 @@ interface StartLivestreamInput {
|
|
|
43
56
|
* Engine tự đặt cờ này theo options.autoPublish; app không cần truyền.
|
|
44
57
|
*/
|
|
45
58
|
standby?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Độ phân giải app THỰC SỰ quay, để server ghi đúng vào phiên. Chỉ app biết
|
|
61
|
+
* con số này: nó là lựa chọn local và app còn override `config.videoQuality`
|
|
62
|
+
* mà server trả về. Không khai thì server dùng mặc định của nó (1080) và bản
|
|
63
|
+
* ghi phiên nói dối — đã cắn 18/08/2026, dashboard báo 1080 trong khi máy
|
|
64
|
+
* phát 720 suốt.
|
|
65
|
+
*/
|
|
66
|
+
quality?: 720 | 1080;
|
|
46
67
|
}
|
|
47
68
|
interface SdkTelemetryCredentials {
|
|
48
69
|
endpoint: string;
|
|
@@ -64,6 +85,36 @@ interface SdkSessionRefresh {
|
|
|
64
85
|
telemetry?: SdkTelemetryCredentials;
|
|
65
86
|
config: SdkRemoteConfig;
|
|
66
87
|
}
|
|
88
|
+
type CameraGrantStatus = "scheduled" | "live";
|
|
89
|
+
/**
|
|
90
|
+
* Camera grant — điện thoại của user ĐỐI TÁC lên sóng (ADR-0002). Backend đối
|
|
91
|
+
* tác nhận từ REST v1 (POST /api/v1/live-sessions {cameraGrant:true} hoặc
|
|
92
|
+
* POST /api/v1/live-sessions/{id}/camera-grant) và chuyển NGUYÊN TRẠNG cho app;
|
|
93
|
+
* app dùng `telemetry.token` làm Bearer với namespace /api/v2/camera/sessions.
|
|
94
|
+
*
|
|
95
|
+
* Superset nghiêm ngặt của SdkSessionGrant: `serverUrl` là RTMP ingest
|
|
96
|
+
* (`rtmp://host:1935/`), `participantToken` là stream key — SDK 0.2 đã nhận
|
|
97
|
+
* hình dạng này qua isSessionGrant nên không cần validator mới ở lớp engine.
|
|
98
|
+
*/
|
|
99
|
+
interface CameraGrant extends SdkSessionGrant {
|
|
100
|
+
/** Không bao giờ "ended" — endpoint trả 409 thay vì grant. */
|
|
101
|
+
status: CameraGrantStatus;
|
|
102
|
+
matchRef: string | null;
|
|
103
|
+
courtRef: string | null;
|
|
104
|
+
watchUrl: string | null;
|
|
105
|
+
/**
|
|
106
|
+
* Origin websocket Convex (https://<dep>.convex.cloud) để SDK subscribe lệnh
|
|
107
|
+
* thay vì poll HTTP 2s; null khi deployment không lộ origin chuẩn → SDK poll.
|
|
108
|
+
*/
|
|
109
|
+
convexUrl: string | null;
|
|
110
|
+
issuedAt: number;
|
|
111
|
+
/**
|
|
112
|
+
* Hạn dùng của grant. Phiên chờ mà app CHƯA từng kết nối bị dọn sau 30 phút
|
|
113
|
+
* (RTMP_STANDBY_SILENT_MS) — xin grant đúng lúc user mở camera, đừng cấp sớm
|
|
114
|
+
* hàng giờ trước trận.
|
|
115
|
+
*/
|
|
116
|
+
expiresAt: number;
|
|
117
|
+
}
|
|
67
118
|
interface EndLivestreamInput {
|
|
68
119
|
sessionId: string;
|
|
69
120
|
reason: "user" | "background_timeout" | "network_timeout" | "standby_timeout" | "error";
|
|
@@ -77,6 +128,22 @@ interface SdkTelemetryEvent {
|
|
|
77
128
|
name: SdkTelemetryEventName;
|
|
78
129
|
data?: Record<string, unknown>;
|
|
79
130
|
}
|
|
131
|
+
/** Lệnh điều khiển phiên giao qua kênh poll (mode device-rtmp). */
|
|
132
|
+
type SessionCommandAction = "start" | "stop" | "end";
|
|
133
|
+
interface SessionCommandRecord {
|
|
134
|
+
id: string;
|
|
135
|
+
action: SessionCommandAction;
|
|
136
|
+
/** Con trỏ tăng nghiêm ngặt theo phiên — client gửi lại làm `after`. */
|
|
137
|
+
issuedAt: number;
|
|
138
|
+
}
|
|
139
|
+
interface SessionCommandPollResult {
|
|
140
|
+
commands: SessionCommandRecord[];
|
|
141
|
+
/**
|
|
142
|
+
* Trạng thái phiên phía server — lưới an toàn khi lệnh rơi: "live" mà máy
|
|
143
|
+
* còn standby thì tự phát, "ended" thì tự kết thúc.
|
|
144
|
+
*/
|
|
145
|
+
status: "scheduled" | "live" | "ended";
|
|
146
|
+
}
|
|
80
147
|
interface LivestreamSessionProvider {
|
|
81
148
|
bootstrap(input: BootstrapInput): Promise<SdkBootstrap>;
|
|
82
149
|
start(input: StartLivestreamInput): Promise<SdkSessionGrant>;
|
|
@@ -318,6 +385,20 @@ type RoomEvent = {
|
|
|
318
385
|
action: string;
|
|
319
386
|
};
|
|
320
387
|
interface RoomAdapter {
|
|
388
|
+
/**
|
|
389
|
+
* Xin quyền camera/mic theo cách của mode. Không có thì engine dùng
|
|
390
|
+
* runtime.requestPermissions() (getUserMedia của WebRTC — mở camera thật
|
|
391
|
+
* ~400ms). Mode device-rtmp PHẢI tự xin bằng API quyền thuần: máy Android
|
|
392
|
+
* MediaTek chỉ cho một camera mở, camera WebRTC chưa nhả là HaishinKit dính
|
|
393
|
+
* "Too many users (-87)" và preview đen.
|
|
394
|
+
*/
|
|
395
|
+
requestPermissions?(): Promise<boolean>;
|
|
396
|
+
/**
|
|
397
|
+
* true = adapter KHÔNG tự nối lại khi rớt (RTMP: native chỉ báo
|
|
398
|
+
* "disconnected" rồi đứng im) — engine phải chủ động quay số lại qua retry().
|
|
399
|
+
* LiveKit bỏ trống: SDK của nó tự hồi và tự bắn "reconnected".
|
|
400
|
+
*/
|
|
401
|
+
readonly needsManualReconnect?: boolean;
|
|
321
402
|
/** Bật/tắt publish track khi đã ở trong phòng (standby ↔ live). */
|
|
322
403
|
setPublishing?(enabled: boolean): Promise<void>;
|
|
323
404
|
setCaptureOrientation?(orientation: CaptureOrientation): Promise<void>;
|
|
@@ -346,18 +427,66 @@ interface SdkNetwork {
|
|
|
346
427
|
isOnline(): boolean;
|
|
347
428
|
subscribe(listener: (online: boolean) => void): () => void;
|
|
348
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Khi app khởi động lại và thấy dấu của một phiên chưa được dừng có chủ ý, hàm
|
|
432
|
+
* này quyết định: GIỮ phiên đó để nhận lại, hay kết thúc nó như trước.
|
|
433
|
+
*
|
|
434
|
+
* Mặc định (không truyền) = kết thúc, giữ nguyên hành vi cho mọi SDK đối tác.
|
|
435
|
+
* App first-party trả true khi máy đang gán sân: lúc đó máy sụp nguồn giữa trận
|
|
436
|
+
* là chuyện của thiết bị gác sân, và nối lại đúng phiên cũ mới giữ được link xem
|
|
437
|
+
* và VOD liền mạch. Máy không gán sân thì vẫn dọn sạch như cũ.
|
|
438
|
+
*/
|
|
439
|
+
/**
|
|
440
|
+
* Được phép async có chủ đích: câu trả lời đúng nằm TRÊN ĐĨA (sân đã gán có
|
|
441
|
+
* persist), còn React state lúc engine khởi động thường CHƯA restore xong.
|
|
442
|
+
* Bản sync-đọc-state đã gây lỗi thật (18/08/2026): recover chạy trước restore
|
|
443
|
+
* → keep=false oan → app tự kết án phiên đáng giữ rồi arm phiên chờ THỨ HAI.
|
|
444
|
+
*/
|
|
445
|
+
type KeepSessionOnRestart = () => boolean | Promise<boolean>;
|
|
349
446
|
interface TelemetrySink {
|
|
350
447
|
send(credentials: SdkTelemetryCredentials, events: readonly SdkTelemetryEvent[]): Promise<void>;
|
|
351
448
|
dispose?(): void;
|
|
352
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Kênh nhận lệnh điều khiển cho mode device-rtmp: RTMP không có data channel
|
|
452
|
+
* nên start/stop/end từ server (match-start của trọng tài, dashboard) về qua
|
|
453
|
+
* HTTP poll, auth bằng chính telemetry token trong grant.
|
|
454
|
+
*/
|
|
455
|
+
interface SessionCommandChannel {
|
|
456
|
+
poll(credentials: SdkTelemetryCredentials, after: number): Promise<SessionCommandPollResult>;
|
|
457
|
+
dispose?(): void;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Kênh nhận lệnh device-rtmp qua WEBSOCKET (Convex subscription) — thay poll
|
|
461
|
+
* HTTP 2s cho máy first-party nối thẳng Convex. Lệnh về tức thì, lúc rảnh gần
|
|
462
|
+
* như 0 function call (poll 2s ở 200 máy là ~518M call/tháng). Có kênh này thì
|
|
463
|
+
* engine hạ poll về nhịp reconcile chậm làm lưới an toàn; thiếu thì poll như cũ.
|
|
464
|
+
*/
|
|
465
|
+
interface SessionCommandSubscription {
|
|
466
|
+
/**
|
|
467
|
+
* Mở subscription cho phiên; `onCommands` được gọi mỗi lần hàng lệnh đổi
|
|
468
|
+
* (kể cả lần đầu). `onError` báo websocket/token hỏng để engine tăng lại nhịp
|
|
469
|
+
* poll. Trả hàm huỷ subscription (KHÔNG đóng client — tái dùng qua phiên).
|
|
470
|
+
*/
|
|
471
|
+
subscribe(credentials: SdkTelemetryCredentials, sessionId: string, onCommands: (commands: SessionCommandRecord[]) => void, onError?: (error: Error) => void): () => void;
|
|
472
|
+
dispose?(): void;
|
|
473
|
+
}
|
|
353
474
|
interface EngineDependencies {
|
|
354
475
|
sessionProvider: LivestreamSessionProvider;
|
|
355
476
|
bootstrapInput: BootstrapInput;
|
|
356
477
|
roomFactory: () => RoomAdapter;
|
|
478
|
+
keepSessionOnRestart?: KeepSessionOnRestart;
|
|
357
479
|
runtime: SdkRuntime;
|
|
358
480
|
storage: SdkStorage;
|
|
359
481
|
network: SdkNetwork;
|
|
360
482
|
telemetry?: TelemetrySink;
|
|
483
|
+
/** Kênh poll lệnh cho phiên device-rtmp; thiếu thì mode này điếc lệnh server. */
|
|
484
|
+
commandChannel?: SessionCommandChannel;
|
|
485
|
+
/**
|
|
486
|
+
* Kênh nhận lệnh qua websocket (first-party). Có thì lệnh về tức thì và poll
|
|
487
|
+
* tụt về nhịp reconcile; thiếu (máy đối tác qua proxy) thì chỉ dùng poll.
|
|
488
|
+
*/
|
|
489
|
+
commandSubscription?: SessionCommandSubscription;
|
|
361
490
|
/** Thu chỉ số thiết bị; gọi mỗi nhịp heartbeat khi đang phát. */
|
|
362
491
|
deviceMetrics?: () => Promise<DeviceMetricsSample | null>;
|
|
363
492
|
now?: () => number;
|
|
@@ -378,6 +507,16 @@ interface DeviceMetricsSample {
|
|
|
378
507
|
storageFreeGb?: number;
|
|
379
508
|
netType?: string;
|
|
380
509
|
linkMbps?: number;
|
|
510
|
+
/** Cường độ sóng WiFi 0-100% — CHỈ Android (iOS không mở cho app). */
|
|
511
|
+
signalPct?: number;
|
|
512
|
+
/** IP nội bộ — phân biệt máy đang bám router nào, không cần quyền vị trí. */
|
|
513
|
+
localIp?: string;
|
|
514
|
+
/**
|
|
515
|
+
* Tần số kênh WiFi (MHz) — CHỈ Android. 2412–2484 = băng 2.4 GHz, ≥ 5000 =
|
|
516
|
+
* 5 GHz. Thay cho SSID (bị khoá sau quyền vị trí): 22/08/2026 một máy rơi từ
|
|
517
|
+
* AP 5 GHz của giải sang WiFi quán 2.4 GHz mà heartbeat không phân biệt được.
|
|
518
|
+
*/
|
|
519
|
+
freqMhz?: number;
|
|
381
520
|
rssi?: number;
|
|
382
521
|
connQuality?: string;
|
|
383
522
|
charging?: boolean;
|
|
@@ -400,6 +539,14 @@ interface LivestreamSnapshot {
|
|
|
400
539
|
selectedCameraId: string | null;
|
|
401
540
|
elapsedMs: number;
|
|
402
541
|
isOnline: boolean;
|
|
542
|
+
/**
|
|
543
|
+
* Phiên cũ còn sống mà app vừa khởi động lại có thể NHẬN LẠI (thay vì tạo phiên
|
|
544
|
+
* mới). Chỉ khác null khi engine tìm thấy dấu của một phiên chưa được dừng có
|
|
545
|
+
* chủ ý VÀ caller nói phiên đó đáng giữ (keepSessionOnRestart). Tầng trên
|
|
546
|
+
* (AutoResume) đọc trường này để nối lại đúng phiên cũ — giữ nguyên link xem và
|
|
547
|
+
* VOD liền một mạch.
|
|
548
|
+
*/
|
|
549
|
+
adoptableSessionId: string | null;
|
|
403
550
|
}
|
|
404
551
|
declare const PENDING_END_KEY = "@pickleball/expo-sdk/pending-end";
|
|
405
552
|
declare const LEGAL_LIVESTREAM_TRANSITIONS: Readonly<Record<LivestreamState, readonly LivestreamState[]>>;
|
|
@@ -428,10 +575,23 @@ declare class LivestreamEngine {
|
|
|
428
575
|
private backgroundTimer?;
|
|
429
576
|
private standbyTimer?;
|
|
430
577
|
private reconnectTimer?;
|
|
578
|
+
private redialTimer?;
|
|
579
|
+
/** Đang trong vòng quay số lại — xem doRetry() để biết vì sao cần cờ này. */
|
|
580
|
+
private redialing;
|
|
431
581
|
private heartbeatTimer?;
|
|
432
582
|
private elapsedTimer?;
|
|
433
583
|
private tokenRefreshTimer?;
|
|
434
584
|
private maxSessionTimer?;
|
|
585
|
+
private commandPollTimer?;
|
|
586
|
+
/** Huỷ subscription websocket lệnh của phiên hiện tại (nếu đang mở). */
|
|
587
|
+
private commandUnsubscribe?;
|
|
588
|
+
/** Khoá phiên+token đang subscribe — đổi thì resubscribe (token refresh/đổi phiên). */
|
|
589
|
+
private commandSubscriptionKey?;
|
|
590
|
+
/** Websocket đã đẩy được ít nhất một lần → coi là sống, cho phép hạ nhịp poll. */
|
|
591
|
+
private commandSubscriptionActive;
|
|
592
|
+
/** issuedAt của lệnh cuối đã xử lý — server chỉ trả lệnh mới hơn mốc này. */
|
|
593
|
+
private commandCursor;
|
|
594
|
+
private commandPollBackoffMs;
|
|
435
595
|
private grant?;
|
|
436
596
|
private startedAt?;
|
|
437
597
|
/** Đang publish track (live) hay chỉ ngồi trong phòng (standby). */
|
|
@@ -471,16 +631,81 @@ declare class LivestreamEngine {
|
|
|
471
631
|
handleRoomEvent(event: RoomEvent): void;
|
|
472
632
|
handleAppState(state: "active" | "background" | "inactive"): void;
|
|
473
633
|
private ensureBackgroundGraceTimer;
|
|
474
|
-
|
|
634
|
+
/**
|
|
635
|
+
* `keepSession` = engine bị tháo nhưng phiên phải sống tiếp để lần mở app sau
|
|
636
|
+
* nhận lại. Mặc định false: giữ NGUYÊN hành vi cũ cho mọi SDK đối tác.
|
|
637
|
+
*
|
|
638
|
+
* Lưu ý dispose KHÔNG BAO GIỜ là đường của "người dùng bấm dừng" — nút dừng đi
|
|
639
|
+
* qua doStop, mà doStop xoá ownedSessionId trước, nên tới đây sessionId đã là
|
|
640
|
+
* undefined. Vì vậy nhánh end ở đây luôn là teardown NGOÀI Ý MUỐN (app bị giết,
|
|
641
|
+
* component remount), đúng chỗ cần giữ phiên.
|
|
642
|
+
*/
|
|
643
|
+
dispose(options?: {
|
|
644
|
+
keepSession?: boolean;
|
|
645
|
+
}): Promise<void>;
|
|
475
646
|
private replayPendingEnd;
|
|
476
647
|
private doReplayPendingEnd;
|
|
648
|
+
/**
|
|
649
|
+
* Phiên tưởng đáng nhận lại hoá ra đã chết phía server → BỎ dấu để máy arm
|
|
650
|
+
* phiên mới được.
|
|
651
|
+
*
|
|
652
|
+
* Thiếu hàm này là máy kẹt cứng: dấu vẫn nằm đó chặn mọi lượt start (đúng vai
|
|
653
|
+
* trò chống phiên song song của nó), mà lượt start duy nhất được cho qua lại là
|
|
654
|
+
* lượt nhận lại chính cái phiên đã chết — không bao giờ thành công. Đã dính
|
|
655
|
+
* thật 18/08/2026: lưới standby 30 phút kết thúc phiên trong lúc máy ngủ, mở
|
|
656
|
+
* lại thì app đứng ở "error" vĩnh viễn.
|
|
657
|
+
*/
|
|
658
|
+
abandonAdoptableSession(sessionId: string): Promise<void>;
|
|
477
659
|
private recoverStartupSession;
|
|
660
|
+
/**
|
|
661
|
+
* Dấu phiên cũ chặn thao tác mới cho tới khi nó được giải quyết.
|
|
662
|
+
*
|
|
663
|
+
* `externalSessionId` = id lượt start đang xin đi qua; bỏ trống nghĩa là
|
|
664
|
+
* prepare (mở camera), vốn phải luôn được phép khi đang định nhận lại — không
|
|
665
|
+
* mở được camera thì không nhận lại được, và đó chính là bẫy làm máy kẹt cứng ở
|
|
666
|
+
* màn lỗi "The previous livestream is still pending end".
|
|
667
|
+
*/
|
|
478
668
|
private ensurePendingEndResolved;
|
|
479
669
|
private readPendingEnd;
|
|
480
670
|
private ownsInMemorySession;
|
|
481
671
|
private startTimers;
|
|
482
672
|
private clearTimers;
|
|
673
|
+
/**
|
|
674
|
+
* Quay số lại với backoff cho adapter cần reconnect thủ công. Mỗi nhịp gọi
|
|
675
|
+
* retry() (refresh credentials + room.connect + enterSession). Thành công →
|
|
676
|
+
* enterSession phát "publishing" → nhánh "reconnected" dọn cả hai timer.
|
|
677
|
+
* Thất bại → doRetry() đưa state về "error" (vẫn còn sessionId) và nhịp sau
|
|
678
|
+
* retry() lại kéo về "reconnecting" — lặp tới khi doom timer 30s kết liễu.
|
|
679
|
+
* Backoff thưa dần để không dội server khi ingest thật sự sập.
|
|
680
|
+
*/
|
|
681
|
+
private scheduleRedial;
|
|
682
|
+
private clearRedialTimer;
|
|
683
|
+
private stopRedialing;
|
|
483
684
|
private clearReconnectTimer;
|
|
685
|
+
/**
|
|
686
|
+
* Poll lệnh chỉ khi phiên là device-rtmp (grant rtmp://) và đang standby/
|
|
687
|
+
* live. Ngoài hai trạng thái đó không có gì để điều khiển: connecting chưa
|
|
688
|
+
* có phiên chạy, reconnecting đã có reconnect timer lo, ending/ended thì thôi.
|
|
689
|
+
*/
|
|
690
|
+
private commandPollingWanted;
|
|
691
|
+
private syncCommandPolling;
|
|
692
|
+
/**
|
|
693
|
+
* Mở/đổi websocket nhận lệnh cho phiên hiện tại. Chỉ máy first-party có
|
|
694
|
+
* dependency này (đối tác qua proxy thì undefined → chỉ poll). Resubscribe
|
|
695
|
+
* khi đổi phiên HOẶC token (refresh) vì cả hai đi vào khoá.
|
|
696
|
+
*/
|
|
697
|
+
private syncCommandSubscription;
|
|
698
|
+
private stopCommandSubscription;
|
|
699
|
+
private stopCommandPolling;
|
|
700
|
+
private scheduleCommandPoll;
|
|
701
|
+
private commandPollInterval;
|
|
702
|
+
/**
|
|
703
|
+
* Áp lệnh từ poll HOẶC subscription vào cùng một state machine. Subscription
|
|
704
|
+
* trả cả lệnh CŨ (after:0) nên phải lọc theo con trỏ ở đây; poll đã lọc
|
|
705
|
+
* server-side (after=cursor) nên bước lọc này vô hại với nó.
|
|
706
|
+
*/
|
|
707
|
+
private applyCommands;
|
|
708
|
+
private pollSessionCommands;
|
|
484
709
|
/** Heartbeat kèm chỉ số thiết bị (nếu collector được cung cấp). */
|
|
485
710
|
private sendHeartbeat;
|
|
486
711
|
private sendTelemetry;
|
|
@@ -513,4 +738,4 @@ declare class LivestreamEngine {
|
|
|
513
738
|
private ensureOperation;
|
|
514
739
|
}
|
|
515
740
|
|
|
516
|
-
export {
|
|
741
|
+
export { normalizeError as $, type SdkTelemetryEvent as A, type BootstrapInput as B, type CameraGrant as C, type DeviceMetricsSample as D, type EndLivestreamInput as E, type SdkTelemetryEventName as F, type SessionCommandAction as G, type SessionCommandChannel as H, InvalidResponseError as I, type SessionCommandPollResult as J, type SessionCommandRecord as K, type LivestreamSessionProvider as L, SessionConflictError as M, NetworkUnavailableError as N, TokenExpiredError as O, PermissionDeniedError as P, UnsupportedRuntimeError as Q, type RoomAdapter as R, type SessionCommandSubscription as S, type TelemetrySink as T, UnknownSdkError as U, UnsupportedSdkError as V, compareSemver as W, errorFromCode as X, isSdkRemoteConfig as Y, isSdkVersionSupported as Z, isSessionGoneError as _, type SdkStorage as a, LivestreamEngine as a0, type RoomConnection as a1, type RoomEvent as a2, type EngineDependencies as a3, LEGAL_LIVESTREAM_TRANSITIONS as a4, PENDING_END_KEY as a5, type SdkSessionGrant as b, type StartLivestreamInput as c, type SdkRemoteConfig as d, type CameraDeviceInfo as e, type CameraGrantStatus as f, type CaptureOrientation as g, ConsentRequiredError as h, type EndReason as i, ExpoSdkConfigurationError as j, type ExpoSdkErrorCode as k, ExpoSdkTimeoutError as l, type LivestreamSnapshot as m, type LivestreamState as n, PickleballExpoError as o, RecordingFailureError as p, SDK_TELEMETRY_EVENT_NAMES as q, type SdkBootstrap as r, type SdkConflictReason as s, SdkDisabledError as t, type SdkError as u, type SdkErrorCode as v, type SdkNetwork as w, type SdkRuntime as x, type SdkSessionRefresh as y, type SdkTelemetryCredentials as z };
|