@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;
|
|
2
2
|
|
|
3
3
|
var _chunk7OX2DSKIcjs = require('./chunk-7OX2DSKI.cjs');
|
|
4
4
|
|
|
@@ -186,7 +186,13 @@ function isSessionGoneError(error) {
|
|
|
186
186
|
|
|
187
187
|
// src/engine.ts
|
|
188
188
|
var PENDING_END_KEY = "@pickleball/expo-sdk/pending-end";
|
|
189
|
+
var CAPTURE_ORIENTATION_KEY = "@pickleball/expo-sdk/capture-orientation";
|
|
189
190
|
var DEFAULT_STANDBY_TIMEOUT_MS = 15 * 6e4;
|
|
191
|
+
var TOKEN_REFRESH_MARGIN_MS = 2 * 6e4;
|
|
192
|
+
var COMMAND_POLL_STANDBY_MS = 2e3;
|
|
193
|
+
var COMMAND_POLL_LIVE_MS = 4e3;
|
|
194
|
+
var COMMAND_POLL_MAX_BACKOFF_MS = 3e4;
|
|
195
|
+
var COMMAND_POLL_RECONCILE_MS = 3e4;
|
|
190
196
|
var activeStates = /* @__PURE__ */ new Set([
|
|
191
197
|
"standby",
|
|
192
198
|
"live",
|
|
@@ -214,7 +220,7 @@ var LEGAL_LIVESTREAM_TRANSITIONS = {
|
|
|
214
220
|
error: ["preparing", "reconnecting", "ending", "ended"]
|
|
215
221
|
};
|
|
216
222
|
var LivestreamEngine = (_class = class {
|
|
217
|
-
constructor(dependencies) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);_class.prototype.__init16.call(this);_class.prototype.__init17.call(this);_class.prototype.__init18.call(this);
|
|
223
|
+
constructor(dependencies) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);_class.prototype.__init16.call(this);_class.prototype.__init17.call(this);_class.prototype.__init18.call(this);_class.prototype.__init19.call(this);_class.prototype.__init20.call(this);_class.prototype.__init21.call(this);_class.prototype.__init22.call(this);
|
|
218
224
|
this.dependencies = dependencies;
|
|
219
225
|
this.now = _nullishCoalesce(dependencies.now, () => ( Date.now));
|
|
220
226
|
this.snapshot = {
|
|
@@ -234,7 +240,8 @@ var LivestreamEngine = (_class = class {
|
|
|
234
240
|
cameras: [],
|
|
235
241
|
selectedCameraId: null,
|
|
236
242
|
elapsedMs: 0,
|
|
237
|
-
isOnline: dependencies.network.isOnline()
|
|
243
|
+
isOnline: dependencies.network.isOnline(),
|
|
244
|
+
adoptableSessionId: null
|
|
238
245
|
};
|
|
239
246
|
}
|
|
240
247
|
|
|
@@ -261,27 +268,40 @@ var LivestreamEngine = (_class = class {
|
|
|
261
268
|
|
|
262
269
|
|
|
263
270
|
|
|
271
|
+
/** Đang trong vòng quay số lại — xem doRetry() để biết vì sao cần cờ này. */
|
|
272
|
+
__init9() {this.redialing = false}
|
|
264
273
|
|
|
265
274
|
|
|
266
275
|
|
|
267
276
|
|
|
268
277
|
|
|
278
|
+
/** Huỷ subscription websocket lệnh của phiên hiện tại (nếu đang mở). */
|
|
279
|
+
|
|
280
|
+
/** Khoá phiên+token đang subscribe — đổi thì resubscribe (token refresh/đổi phiên). */
|
|
281
|
+
|
|
282
|
+
/** Websocket đã đẩy được ít nhất một lần → coi là sống, cho phép hạ nhịp poll. */
|
|
283
|
+
__init10() {this.commandSubscriptionActive = false}
|
|
284
|
+
/** issuedAt của lệnh cuối đã xử lý — server chỉ trả lệnh mới hơn mốc này. */
|
|
285
|
+
__init11() {this.commandCursor = 0}
|
|
286
|
+
__init12() {this.commandPollBackoffMs = 0}
|
|
287
|
+
|
|
288
|
+
|
|
269
289
|
/** Đang publish track (live) hay chỉ ngồi trong phòng (standby). */
|
|
270
|
-
|
|
290
|
+
__init13() {this.publishing = false}
|
|
271
291
|
/** Tổng thời gian đã phát trước lần tạm dừng gần nhất. */
|
|
272
|
-
|
|
292
|
+
__init14() {this.pausedElapsedMs = 0}
|
|
273
293
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
294
|
+
__init15() {this.refreshRetryAttempt = 0}
|
|
295
|
+
__init16() {this.operationGeneration = 0}
|
|
296
|
+
__init17() {this.liveTimersStarted = false}
|
|
297
|
+
__init18() {this.keepAwakeActive = false}
|
|
278
298
|
|
|
279
299
|
|
|
280
|
-
|
|
300
|
+
__init19() {this.sessionTerminations = /* @__PURE__ */ new Map()}
|
|
281
301
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
302
|
+
__init20() {this.disposed = false}
|
|
303
|
+
__init21() {this.getSnapshot = () => this.snapshot}
|
|
304
|
+
__init22() {this.subscribe = (listener) => {
|
|
285
305
|
this.listeners.add(listener);
|
|
286
306
|
return () => this.listeners.delete(listener);
|
|
287
307
|
}}
|
|
@@ -348,12 +368,19 @@ var LivestreamEngine = (_class = class {
|
|
|
348
368
|
throw new UnsupportedSdkError();
|
|
349
369
|
}
|
|
350
370
|
this.update({ config: bootstrap.config });
|
|
351
|
-
const
|
|
371
|
+
const room = this.ensureRoom();
|
|
372
|
+
const permitted = room.requestPermissions ? await room.requestPermissions() : await this.dependencies.runtime.requestPermissions();
|
|
352
373
|
this.ensureOperation(generation);
|
|
353
374
|
if (!permitted) {
|
|
354
375
|
throw new PermissionDeniedError();
|
|
355
376
|
}
|
|
356
|
-
|
|
377
|
+
if (!this.snapshot.captureOrientation) {
|
|
378
|
+
const stored = await this.dependencies.storage.getItem(CAPTURE_ORIENTATION_KEY).catch(() => null);
|
|
379
|
+
this.ensureOperation(generation);
|
|
380
|
+
if (stored === "portrait" || stored === "landscape") {
|
|
381
|
+
this.update({ captureOrientation: stored });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
357
384
|
if (this.snapshot.captureOrientation) {
|
|
358
385
|
await _optionalChain([room, 'access', _ => _.setCaptureOrientation, 'optionalCall', _2 => _2(this.snapshot.captureOrientation)]);
|
|
359
386
|
}
|
|
@@ -415,7 +442,7 @@ var LivestreamEngine = (_class = class {
|
|
|
415
442
|
this.transition("connecting");
|
|
416
443
|
this.ensureBackgroundGraceTimer();
|
|
417
444
|
try {
|
|
418
|
-
await this.ensurePendingEndResolved();
|
|
445
|
+
await this.ensurePendingEndResolved(input.externalSessionId);
|
|
419
446
|
this.ensureOperation(generation);
|
|
420
447
|
} catch (error) {
|
|
421
448
|
if (!this.isOperationCurrent(generation) || error instanceof StaleOperationError) {
|
|
@@ -440,7 +467,12 @@ var LivestreamEngine = (_class = class {
|
|
|
440
467
|
);
|
|
441
468
|
}
|
|
442
469
|
this.ownedSessionId = grant.sessionId;
|
|
470
|
+
if (this.snapshot.adoptableSessionId === grant.sessionId) {
|
|
471
|
+
this.update({ adoptableSessionId: null });
|
|
472
|
+
}
|
|
443
473
|
this.grant = grant;
|
|
474
|
+
this.commandCursor = 0;
|
|
475
|
+
this.commandPollBackoffMs = 0;
|
|
444
476
|
this.liveTimersStarted = false;
|
|
445
477
|
this.keepAwakeActive = false;
|
|
446
478
|
this.publishing = autoPublish;
|
|
@@ -533,7 +565,11 @@ var LivestreamEngine = (_class = class {
|
|
|
533
565
|
if (!sessionId || !this.grant) throw new Error("Missing session grant");
|
|
534
566
|
const activeGrant = this.grant;
|
|
535
567
|
try {
|
|
536
|
-
|
|
568
|
+
const expiresAt = _nullishCoalesce(_optionalChain([this, 'access', _8 => _8.grant, 'optionalAccess', _9 => _9.tokenExpiresAt]), () => ( 0));
|
|
569
|
+
const tokenStillFresh = expiresAt > 0 && expiresAt - Date.now() >= TOKEN_REFRESH_MARGIN_MS;
|
|
570
|
+
if (!this.redialing || !tokenStillFresh) {
|
|
571
|
+
await this.refreshCredentials(generation);
|
|
572
|
+
}
|
|
537
573
|
this.ensureOperation(generation);
|
|
538
574
|
await this.requireRoom().connect({
|
|
539
575
|
serverUrl: this.grant.serverUrl,
|
|
@@ -549,6 +585,16 @@ var LivestreamEngine = (_class = class {
|
|
|
549
585
|
await this.abandonGrant(activeGrant);
|
|
550
586
|
return;
|
|
551
587
|
}
|
|
588
|
+
const httpStatus = error.httpStatus;
|
|
589
|
+
if (httpStatus === 404 || httpStatus === 409) {
|
|
590
|
+
this.stopRedialing();
|
|
591
|
+
void this.stop("user");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (this.redialing && this.snapshot.sessionId) {
|
|
595
|
+
this.update({ error: normalizeError(error) });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
552
598
|
this.fail(error);
|
|
553
599
|
}
|
|
554
600
|
}
|
|
@@ -588,10 +634,11 @@ var LivestreamEngine = (_class = class {
|
|
|
588
634
|
}
|
|
589
635
|
const generation = this.operationGeneration;
|
|
590
636
|
if (this.snapshot.previewTrack) {
|
|
591
|
-
await _optionalChain([this, 'access',
|
|
637
|
+
await _optionalChain([this, 'access', _10 => _10.requireRoom, 'call', _11 => _11(), 'access', _12 => _12.setCaptureOrientation, 'optionalCall', _13 => _13(orientation)]);
|
|
592
638
|
if (!this.isOperationCurrent(generation)) return;
|
|
593
639
|
}
|
|
594
640
|
this.update({ captureOrientation: orientation });
|
|
641
|
+
void this.dependencies.storage.setItem(CAPTURE_ORIENTATION_KEY, orientation).catch(() => void 0);
|
|
595
642
|
}
|
|
596
643
|
async setMicrophoneEnabled(enabled) {
|
|
597
644
|
const generation = this.operationGeneration;
|
|
@@ -604,25 +651,38 @@ var LivestreamEngine = (_class = class {
|
|
|
604
651
|
if (event.type === "command") {
|
|
605
652
|
if (event.action === "start") void this.startPublishing();
|
|
606
653
|
else if (event.action === "stop") void this.stopPublishing();
|
|
654
|
+
else if (event.action === "end") void this.stop("user");
|
|
607
655
|
return;
|
|
608
656
|
}
|
|
609
657
|
if (event.type === "reconnecting" && (this.snapshot.state === "live" || this.snapshot.state === "standby")) {
|
|
610
658
|
this.transition("reconnecting");
|
|
611
659
|
void this.sendTelemetry("reconnect", { phase: "started" });
|
|
612
|
-
|
|
660
|
+
if (_optionalChain([this, 'access', _14 => _14.room, 'optionalAccess', _15 => _15.needsManualReconnect])) {
|
|
661
|
+
this.clearReconnectTimer();
|
|
662
|
+
this.scheduleRedial(0);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const configured = _nullishCoalesce(_optionalChain([this, 'access', _16 => _16.snapshot, 'access', _17 => _17.config, 'optionalAccess', _18 => _18.reconnectTimeoutMs]), () => ( 3e4));
|
|
613
666
|
this.clearReconnectTimer();
|
|
614
667
|
this.reconnectTimer = setTimeout(() => {
|
|
615
668
|
void this.stop("network_timeout");
|
|
616
|
-
},
|
|
669
|
+
}, configured);
|
|
617
670
|
return;
|
|
618
671
|
}
|
|
619
672
|
if (event.type === "reconnected" && this.snapshot.state === "reconnecting") {
|
|
620
673
|
this.clearReconnectTimer();
|
|
674
|
+
this.stopRedialing();
|
|
621
675
|
this.transition(this.publishing ? "live" : "standby");
|
|
622
676
|
void this.sendTelemetry("reconnect", { phase: "recovered" });
|
|
623
677
|
return;
|
|
624
678
|
}
|
|
625
|
-
if (event.type === "media-error")
|
|
679
|
+
if (event.type === "media-error") {
|
|
680
|
+
if (_optionalChain([this, 'access', _19 => _19.room, 'optionalAccess', _20 => _20.needsManualReconnect]) && activeStates.has(this.snapshot.state)) {
|
|
681
|
+
this.handleRoomEvent({ type: "reconnecting" });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
this.recordError(event.error);
|
|
685
|
+
}
|
|
626
686
|
if (event.type === "disconnected" && activeStates.has(this.snapshot.state)) {
|
|
627
687
|
this.handleRoomEvent({ type: "reconnecting" });
|
|
628
688
|
}
|
|
@@ -643,7 +703,7 @@ var LivestreamEngine = (_class = class {
|
|
|
643
703
|
) || this.backgroundTimer) {
|
|
644
704
|
return;
|
|
645
705
|
}
|
|
646
|
-
const grace = _nullishCoalesce(_optionalChain([this, 'access',
|
|
706
|
+
const grace = _nullishCoalesce(_optionalChain([this, 'access', _21 => _21.snapshot, 'access', _22 => _22.config, 'optionalAccess', _23 => _23.backgroundGraceMs]), () => ( 15e3));
|
|
647
707
|
this.backgroundTimer = setTimeout(() => {
|
|
648
708
|
this.backgroundTimer = void 0;
|
|
649
709
|
if (this.latestAppState !== "active") {
|
|
@@ -651,20 +711,30 @@ var LivestreamEngine = (_class = class {
|
|
|
651
711
|
}
|
|
652
712
|
}, grace);
|
|
653
713
|
}
|
|
654
|
-
|
|
714
|
+
/**
|
|
715
|
+
* `keepSession` = engine bị tháo nhưng phiên phải sống tiếp để lần mở app sau
|
|
716
|
+
* nhận lại. Mặc định false: giữ NGUYÊN hành vi cũ cho mọi SDK đối tác.
|
|
717
|
+
*
|
|
718
|
+
* Lưu ý dispose KHÔNG BAO GIỜ là đường của "người dùng bấm dừng" — nút dừng đi
|
|
719
|
+
* qua doStop, mà doStop xoá ownedSessionId trước, nên tới đây sessionId đã là
|
|
720
|
+
* undefined. Vì vậy nhánh end ở đây luôn là teardown NGOÀI Ý MUỐN (app bị giết,
|
|
721
|
+
* component remount), đúng chỗ cần giữ phiên.
|
|
722
|
+
*/
|
|
723
|
+
dispose(options) {
|
|
655
724
|
if (this.disposePromise) return this.disposePromise;
|
|
656
725
|
if (this.disposed) return Promise.resolve();
|
|
657
726
|
this.disposed = true;
|
|
658
727
|
this.operationGeneration += 1;
|
|
659
|
-
const sessionId = this.ownedSessionId;
|
|
728
|
+
const sessionId = _optionalChain([options, 'optionalAccess', _24 => _24.keepSession]) === true ? void 0 : this.ownedSessionId;
|
|
660
729
|
let releaseEnd = () => void 0;
|
|
661
730
|
const endBarrier = new Promise((resolve) => {
|
|
662
731
|
releaseEnd = resolve;
|
|
663
732
|
});
|
|
664
733
|
const termination = sessionId ? this.ensureSessionTermination(sessionId, "error", endBarrier) : void 0;
|
|
665
|
-
const roomCleanup = this.releaseRoom(_optionalChain([termination, 'optionalAccess',
|
|
734
|
+
const roomCleanup = this.releaseRoom(_optionalChain([termination, 'optionalAccess', _25 => _25.markerReady]));
|
|
666
735
|
this.clearTimers();
|
|
667
|
-
_optionalChain([this, 'access',
|
|
736
|
+
_optionalChain([this, 'access', _26 => _26.dependencies, 'access', _27 => _27.telemetry, 'optionalAccess', _28 => _28.dispose, 'optionalCall', _29 => _29()]);
|
|
737
|
+
_optionalChain([this, 'access', _30 => _30.dependencies, 'access', _31 => _31.commandChannel, 'optionalAccess', _32 => _32.dispose, 'optionalCall', _33 => _33()]);
|
|
668
738
|
this.pendingCriticalTelemetry = void 0;
|
|
669
739
|
this.networkUnsubscribe();
|
|
670
740
|
this.listeners.clear();
|
|
@@ -672,13 +742,13 @@ var LivestreamEngine = (_class = class {
|
|
|
672
742
|
this.grant = void 0;
|
|
673
743
|
this.ownedSessionId = void 0;
|
|
674
744
|
this.disposePromise = (async () => {
|
|
675
|
-
await _optionalChain([termination, 'optionalAccess',
|
|
745
|
+
await _optionalChain([termination, 'optionalAccess', _34 => _34.markerReady]);
|
|
676
746
|
await Promise.allSettled([
|
|
677
747
|
roomCleanup,
|
|
678
748
|
this.dependencies.runtime.deactivateKeepAwake()
|
|
679
749
|
]);
|
|
680
750
|
releaseEnd();
|
|
681
|
-
await _optionalChain([termination, 'optionalAccess',
|
|
751
|
+
await _optionalChain([termination, 'optionalAccess', _35 => _35.completion]);
|
|
682
752
|
})();
|
|
683
753
|
return this.disposePromise;
|
|
684
754
|
}
|
|
@@ -701,10 +771,36 @@ var LivestreamEngine = (_class = class {
|
|
|
701
771
|
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
702
772
|
this.pendingReplayError = void 0;
|
|
703
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Phiên tưởng đáng nhận lại hoá ra đã chết phía server → BỎ dấu để máy arm
|
|
776
|
+
* phiên mới được.
|
|
777
|
+
*
|
|
778
|
+
* 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
|
|
779
|
+
* trò chống phiên song song của nó), mà lượt start duy nhất được cho qua lại là
|
|
780
|
+
* lượt nhận lại chính cái phiên đã chết — không bao giờ thành công. Đã dính
|
|
781
|
+
* thật 18/08/2026: lưới standby 30 phút kết thúc phiên trong lúc máy ngủ, mở
|
|
782
|
+
* lại thì app đứng ở "error" vĩnh viễn.
|
|
783
|
+
*/
|
|
784
|
+
async abandonAdoptableSession(sessionId) {
|
|
785
|
+
if (this.snapshot.adoptableSessionId !== sessionId) return;
|
|
786
|
+
try {
|
|
787
|
+
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
788
|
+
} catch (e3) {
|
|
789
|
+
}
|
|
790
|
+
this.update({ adoptableSessionId: null });
|
|
791
|
+
}
|
|
704
792
|
async recoverStartupSession() {
|
|
705
793
|
let pending = await this.readPendingEnd();
|
|
706
794
|
if (!pending || this.ownsInMemorySession()) return;
|
|
707
795
|
if (pending.pendingEnd === null) {
|
|
796
|
+
const keep = await _optionalChain([this, 'access', _36 => _36.dependencies, 'access', _37 => _37.keepSessionOnRestart, 'optionalCall', _38 => _38()]) === true;
|
|
797
|
+
console.warn(
|
|
798
|
+
`[picklive] khoi dong: phien ${pending.sessionId} chua duoc dung co chu y -> ${keep ? "GIU de nhan lai" : "ket thuc"}`
|
|
799
|
+
);
|
|
800
|
+
if (keep) {
|
|
801
|
+
this.update({ adoptableSessionId: pending.sessionId });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
708
804
|
pending = { sessionId: pending.sessionId, pendingEnd: "error" };
|
|
709
805
|
await this.dependencies.storage.setItem(
|
|
710
806
|
PENDING_END_KEY,
|
|
@@ -723,10 +819,21 @@ var LivestreamEngine = (_class = class {
|
|
|
723
819
|
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
724
820
|
this.pendingReplayError = void 0;
|
|
725
821
|
}
|
|
726
|
-
|
|
822
|
+
/**
|
|
823
|
+
* Dấu phiên cũ chặn thao tác mới cho tới khi nó được giải quyết.
|
|
824
|
+
*
|
|
825
|
+
* `externalSessionId` = id lượt start đang xin đi qua; bỏ trống nghĩa là
|
|
826
|
+
* prepare (mở camera), vốn phải luôn được phép khi đang định nhận lại — không
|
|
827
|
+
* 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 ở
|
|
828
|
+
* màn lỗi "The previous livestream is still pending end".
|
|
829
|
+
*/
|
|
830
|
+
async ensurePendingEndResolved(externalSessionId) {
|
|
727
831
|
await this.initialize();
|
|
728
832
|
const pending = await this.readPendingEnd();
|
|
729
833
|
if (!pending) return;
|
|
834
|
+
if (pending.pendingEnd === null && this.snapshot.adoptableSessionId === pending.sessionId && (externalSessionId === void 0 || externalSessionId === `resume-${pending.sessionId}`)) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
730
837
|
if (!this.dependencies.network.isOnline()) {
|
|
731
838
|
throw new NetworkUnavailableError(
|
|
732
839
|
"Connect to the network to finish the previous livestream"
|
|
@@ -778,7 +885,7 @@ var LivestreamEngine = (_class = class {
|
|
|
778
885
|
pendingEnd: value.pendingEnd
|
|
779
886
|
};
|
|
780
887
|
}
|
|
781
|
-
} catch (
|
|
888
|
+
} catch (e4) {
|
|
782
889
|
}
|
|
783
890
|
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
784
891
|
return null;
|
|
@@ -791,18 +898,18 @@ var LivestreamEngine = (_class = class {
|
|
|
791
898
|
this.liveTimersStarted = true;
|
|
792
899
|
const interval = Math.max(
|
|
793
900
|
1e4,
|
|
794
|
-
_nullishCoalesce(_optionalChain([this, 'access',
|
|
901
|
+
_nullishCoalesce(_optionalChain([this, 'access', _39 => _39.snapshot, 'access', _40 => _40.config, 'optionalAccess', _41 => _41.telemetryIntervalMs]), () => ( 6e4))
|
|
795
902
|
);
|
|
796
903
|
this.heartbeatTimer = setInterval(() => {
|
|
797
904
|
void this.sendHeartbeat();
|
|
798
905
|
}, interval);
|
|
799
|
-
_optionalChain([this, 'access',
|
|
906
|
+
_optionalChain([this, 'access', _42 => _42.heartbeatTimer, 'access', _43 => _43.unref, 'optionalCall', _44 => _44()]);
|
|
800
907
|
this.elapsedTimer = setInterval(() => {
|
|
801
908
|
if (this.startedAt !== void 0) {
|
|
802
909
|
this.update({ elapsedMs: this.now() - this.startedAt });
|
|
803
910
|
}
|
|
804
911
|
}, 1e3);
|
|
805
|
-
_optionalChain([this, 'access',
|
|
912
|
+
_optionalChain([this, 'access', _45 => _45.elapsedTimer, 'access', _46 => _46.unref, 'optionalCall', _47 => _47()]);
|
|
806
913
|
this.scheduleTokenRefresh();
|
|
807
914
|
this.scheduleMaxSessionDuration();
|
|
808
915
|
}
|
|
@@ -814,6 +921,8 @@ var LivestreamEngine = (_class = class {
|
|
|
814
921
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
815
922
|
if (this.maxSessionTimer) clearTimeout(this.maxSessionTimer);
|
|
816
923
|
this.clearReconnectTimer();
|
|
924
|
+
this.stopCommandPolling();
|
|
925
|
+
this.stopCommandSubscription();
|
|
817
926
|
this.backgroundTimer = void 0;
|
|
818
927
|
this.heartbeatTimer = void 0;
|
|
819
928
|
this.elapsedTimer = void 0;
|
|
@@ -822,16 +931,185 @@ var LivestreamEngine = (_class = class {
|
|
|
822
931
|
this.sessionDeadline = void 0;
|
|
823
932
|
this.liveTimersStarted = false;
|
|
824
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Quay số lại với backoff cho adapter cần reconnect thủ công. Mỗi nhịp gọi
|
|
936
|
+
* retry() (refresh credentials + room.connect + enterSession). Thành công →
|
|
937
|
+
* enterSession phát "publishing" → nhánh "reconnected" dọn cả hai timer.
|
|
938
|
+
* Thất bại → doRetry() đưa state về "error" (vẫn còn sessionId) và nhịp sau
|
|
939
|
+
* retry() lại kéo về "reconnecting" — lặp tới khi doom timer 30s kết liễu.
|
|
940
|
+
* Backoff thưa dần để không dội server khi ingest thật sự sập.
|
|
941
|
+
*/
|
|
942
|
+
scheduleRedial(attempt) {
|
|
943
|
+
this.clearRedialTimer();
|
|
944
|
+
this.redialing = true;
|
|
945
|
+
if (!this.dependencies.network.isOnline()) return;
|
|
946
|
+
const delays = [0, 1e3, 2e3, 4e3, 8e3, 15e3, 3e4];
|
|
947
|
+
const delay = delays[Math.min(attempt, delays.length - 1)];
|
|
948
|
+
this.redialTimer = setTimeout(() => {
|
|
949
|
+
if (this.disposed) return;
|
|
950
|
+
const s = this.snapshot.state;
|
|
951
|
+
if (s !== "reconnecting" && !(s === "error" && this.snapshot.sessionId)) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
void this.retry().catch(() => {
|
|
955
|
+
}).finally(() => {
|
|
956
|
+
if (this.disposed) return;
|
|
957
|
+
const after = this.snapshot.state;
|
|
958
|
+
if (after === "reconnecting" || after === "error" && this.snapshot.sessionId) {
|
|
959
|
+
this.scheduleRedial(attempt + 1);
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}, delay);
|
|
963
|
+
}
|
|
964
|
+
clearRedialTimer() {
|
|
965
|
+
if (this.redialTimer) clearTimeout(this.redialTimer);
|
|
966
|
+
this.redialTimer = void 0;
|
|
967
|
+
}
|
|
968
|
+
stopRedialing() {
|
|
969
|
+
this.redialing = false;
|
|
970
|
+
this.clearRedialTimer();
|
|
971
|
+
}
|
|
825
972
|
clearReconnectTimer() {
|
|
826
973
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
827
974
|
this.reconnectTimer = void 0;
|
|
828
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Poll lệnh chỉ khi phiên là device-rtmp (grant rtmp://) và đang standby/
|
|
978
|
+
* live. Ngoài hai trạng thái đó không có gì để điều khiển: connecting chưa
|
|
979
|
+
* có phiên chạy, reconnecting đã có reconnect timer lo, ending/ended thì thôi.
|
|
980
|
+
*/
|
|
981
|
+
commandPollingWanted() {
|
|
982
|
+
const grant = this.grant;
|
|
983
|
+
return Boolean(
|
|
984
|
+
this.dependencies.commandChannel && grant && /^rtmps?:\/\//i.test(grant.serverUrl) && // "reconnecting" PHẢI có trong danh sách này. Sau khi bỏ trần 75 giây,
|
|
985
|
+
// máy có thể quay số hàng giờ; nếu trong lúc đó trọng tài kết thúc trận
|
|
986
|
+
// hoặc nhả sân, đây là đường duy nhất để máy biết mà thôi quay số vào một
|
|
987
|
+
// phiên đã chết (poll trả status "ended" → nhánh ở pollSessionCommands).
|
|
988
|
+
// Trước đây chỉ poll ở standby/live nên máy mất mạng là mù hoàn toàn.
|
|
989
|
+
activeStates.has(this.snapshot.state)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
syncCommandPolling() {
|
|
993
|
+
if (this.disposed || !this.commandPollingWanted()) {
|
|
994
|
+
this.stopCommandPolling();
|
|
995
|
+
this.stopCommandSubscription();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
this.syncCommandSubscription();
|
|
999
|
+
if (!this.commandPollTimer) this.scheduleCommandPoll(0);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Mở/đổi websocket nhận lệnh cho phiên hiện tại. Chỉ máy first-party có
|
|
1003
|
+
* dependency này (đối tác qua proxy thì undefined → chỉ poll). Resubscribe
|
|
1004
|
+
* khi đổi phiên HOẶC token (refresh) vì cả hai đi vào khoá.
|
|
1005
|
+
*/
|
|
1006
|
+
syncCommandSubscription() {
|
|
1007
|
+
const subscription = this.dependencies.commandSubscription;
|
|
1008
|
+
const grant = this.grant;
|
|
1009
|
+
if (!subscription || !grant) return;
|
|
1010
|
+
const key = `${grant.sessionId}:${grant.telemetry.token}`;
|
|
1011
|
+
if (this.commandSubscriptionKey === key) return;
|
|
1012
|
+
this.stopCommandSubscription();
|
|
1013
|
+
this.commandSubscriptionKey = key;
|
|
1014
|
+
const sessionId = grant.sessionId;
|
|
1015
|
+
this.commandUnsubscribe = subscription.subscribe(
|
|
1016
|
+
grant.telemetry,
|
|
1017
|
+
sessionId,
|
|
1018
|
+
(commands) => {
|
|
1019
|
+
if (this.disposed || _optionalChain([this, 'access', _48 => _48.grant, 'optionalAccess', _49 => _49.sessionId]) !== sessionId) return;
|
|
1020
|
+
this.commandSubscriptionActive = true;
|
|
1021
|
+
this.applyCommands(commands);
|
|
1022
|
+
},
|
|
1023
|
+
() => {
|
|
1024
|
+
this.commandSubscriptionActive = false;
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
stopCommandSubscription() {
|
|
1029
|
+
if (this.commandUnsubscribe) {
|
|
1030
|
+
try {
|
|
1031
|
+
this.commandUnsubscribe();
|
|
1032
|
+
} catch (e5) {
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
this.commandUnsubscribe = void 0;
|
|
1036
|
+
this.commandSubscriptionKey = void 0;
|
|
1037
|
+
this.commandSubscriptionActive = false;
|
|
1038
|
+
}
|
|
1039
|
+
stopCommandPolling() {
|
|
1040
|
+
if (this.commandPollTimer) clearTimeout(this.commandPollTimer);
|
|
1041
|
+
this.commandPollTimer = void 0;
|
|
1042
|
+
this.commandPollBackoffMs = 0;
|
|
1043
|
+
}
|
|
1044
|
+
scheduleCommandPoll(delayMs) {
|
|
1045
|
+
this.commandPollTimer = setTimeout(() => {
|
|
1046
|
+
this.commandPollTimer = void 0;
|
|
1047
|
+
void this.pollSessionCommands();
|
|
1048
|
+
}, delayMs);
|
|
1049
|
+
_optionalChain([this, 'access', _50 => _50.commandPollTimer, 'access', _51 => _51.unref, 'optionalCall', _52 => _52()]);
|
|
1050
|
+
}
|
|
1051
|
+
commandPollInterval() {
|
|
1052
|
+
if (this.commandSubscriptionActive) return COMMAND_POLL_RECONCILE_MS;
|
|
1053
|
+
return this.snapshot.state === "live" ? COMMAND_POLL_LIVE_MS : COMMAND_POLL_STANDBY_MS;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Áp lệnh từ poll HOẶC subscription vào cùng một state machine. Subscription
|
|
1057
|
+
* trả cả lệnh CŨ (after:0) nên phải lọc theo con trỏ ở đây; poll đã lọc
|
|
1058
|
+
* server-side (after=cursor) nên bước lọc này vô hại với nó.
|
|
1059
|
+
*/
|
|
1060
|
+
applyCommands(commands) {
|
|
1061
|
+
for (const command of commands) {
|
|
1062
|
+
if (command.issuedAt <= this.commandCursor) continue;
|
|
1063
|
+
this.commandCursor = command.issuedAt;
|
|
1064
|
+
this.handleRoomEvent({ type: "command", action: command.action });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async pollSessionCommands() {
|
|
1068
|
+
if (!this.commandPollingWanted()) return;
|
|
1069
|
+
const channel = this.dependencies.commandChannel;
|
|
1070
|
+
const grant = this.grant;
|
|
1071
|
+
if (!channel || !grant) return;
|
|
1072
|
+
const sessionId = grant.sessionId;
|
|
1073
|
+
let result = null;
|
|
1074
|
+
try {
|
|
1075
|
+
result = await channel.poll(grant.telemetry, this.commandCursor);
|
|
1076
|
+
this.commandPollBackoffMs = 0;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
const status = _optionalChain([error, 'optionalAccess', _53 => _53.status]);
|
|
1079
|
+
if (status === 401) {
|
|
1080
|
+
void this.refreshCredentials().catch(() => void 0);
|
|
1081
|
+
}
|
|
1082
|
+
if (status === 409 && _optionalChain([this, 'access', _54 => _54.grant, 'optionalAccess', _55 => _55.sessionId]) === sessionId) {
|
|
1083
|
+
void this.stop("user");
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
this.commandPollBackoffMs = Math.min(
|
|
1087
|
+
COMMAND_POLL_MAX_BACKOFF_MS,
|
|
1088
|
+
Math.max(this.commandPollInterval(), this.commandPollBackoffMs * 2)
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
if (this.disposed || _optionalChain([this, 'access', _56 => _56.grant, 'optionalAccess', _57 => _57.sessionId]) !== sessionId) return;
|
|
1092
|
+
if (result) {
|
|
1093
|
+
this.applyCommands(result.commands);
|
|
1094
|
+
if (result.commands.length === 0) {
|
|
1095
|
+
if (result.status === "ended" && activeStates.has(this.snapshot.state)) {
|
|
1096
|
+
void this.stop("user");
|
|
1097
|
+
} else if (result.status === "live" && this.snapshot.state === "standby") {
|
|
1098
|
+
void this.startPublishing();
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (!this.commandPollingWanted() || this.commandPollTimer) return;
|
|
1103
|
+
this.scheduleCommandPoll(
|
|
1104
|
+
this.commandPollBackoffMs || this.commandPollInterval()
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
829
1107
|
/** Heartbeat kèm chỉ số thiết bị (nếu collector được cung cấp). */
|
|
830
1108
|
async sendHeartbeat() {
|
|
831
1109
|
let metrics = null;
|
|
832
1110
|
try {
|
|
833
|
-
metrics = await _asyncNullishCoalesce(await _optionalChain([this, 'access',
|
|
834
|
-
} catch (
|
|
1111
|
+
metrics = await _asyncNullishCoalesce(await _optionalChain([this, 'access', _58 => _58.dependencies, 'access', _59 => _59.deviceMetrics, 'optionalCall', _60 => _60()]), async () => ( null));
|
|
1112
|
+
} catch (e6) {
|
|
835
1113
|
metrics = null;
|
|
836
1114
|
}
|
|
837
1115
|
if (this.disposed) return;
|
|
@@ -864,19 +1142,19 @@ var LivestreamEngine = (_class = class {
|
|
|
864
1142
|
startTelemetrySend(pending) {
|
|
865
1143
|
const operation = (async () => {
|
|
866
1144
|
try {
|
|
867
|
-
await _optionalChain([this, 'access',
|
|
1145
|
+
await _optionalChain([this, 'access', _61 => _61.dependencies, 'access', _62 => _62.telemetry, 'optionalAccess', _63 => _63.send, 'call', _64 => _64(pending.credentials, [
|
|
868
1146
|
pending.event
|
|
869
1147
|
])]);
|
|
870
1148
|
} catch (error) {
|
|
871
|
-
if (_optionalChain([error, 'optionalAccess',
|
|
1149
|
+
if (_optionalChain([error, 'optionalAccess', _65 => _65.status]) === 401) {
|
|
872
1150
|
try {
|
|
873
1151
|
await this.refreshCredentials();
|
|
874
1152
|
if (this.grant) {
|
|
875
|
-
await _optionalChain([this, 'access',
|
|
1153
|
+
await _optionalChain([this, 'access', _66 => _66.dependencies, 'access', _67 => _67.telemetry, 'optionalAccess', _68 => _68.send, 'call', _69 => _69(this.grant.telemetry, [
|
|
876
1154
|
pending.event
|
|
877
1155
|
])]);
|
|
878
1156
|
}
|
|
879
|
-
} catch (
|
|
1157
|
+
} catch (e7) {
|
|
880
1158
|
}
|
|
881
1159
|
}
|
|
882
1160
|
}
|
|
@@ -912,6 +1190,7 @@ var LivestreamEngine = (_class = class {
|
|
|
912
1190
|
telemetry: _nullishCoalesce(refreshed.telemetry, () => ( this.grant.telemetry)),
|
|
913
1191
|
config: refreshed.config
|
|
914
1192
|
};
|
|
1193
|
+
this.syncCommandSubscription();
|
|
915
1194
|
this.update({ config: refreshed.config });
|
|
916
1195
|
this.refreshRetryAttempt = 0;
|
|
917
1196
|
if (this.liveTimersStarted) {
|
|
@@ -935,20 +1214,21 @@ var LivestreamEngine = (_class = class {
|
|
|
935
1214
|
this.update({ state, error: state === "error" ? this.snapshot.error : null });
|
|
936
1215
|
if (state === "standby") this.startStandbyTimer();
|
|
937
1216
|
else this.clearStandbyTimer();
|
|
1217
|
+
this.syncCommandPolling();
|
|
938
1218
|
if (this.snapshot.sessionId) {
|
|
939
1219
|
void this.sendTelemetry("state_changed", { from: previous, to: state });
|
|
940
1220
|
}
|
|
941
1221
|
}
|
|
942
1222
|
startStandbyTimer() {
|
|
943
1223
|
this.clearStandbyTimer();
|
|
944
|
-
const timeout = _nullishCoalesce(_optionalChain([this, 'access',
|
|
1224
|
+
const timeout = _nullishCoalesce(_optionalChain([this, 'access', _70 => _70.snapshot, 'access', _71 => _71.config, 'optionalAccess', _72 => _72.standbyTimeoutMs]), () => ( DEFAULT_STANDBY_TIMEOUT_MS));
|
|
945
1225
|
this.standbyTimer = setTimeout(() => {
|
|
946
1226
|
this.standbyTimer = void 0;
|
|
947
1227
|
if (this.snapshot.state === "standby") {
|
|
948
1228
|
void this.stop("standby_timeout");
|
|
949
1229
|
}
|
|
950
1230
|
}, timeout);
|
|
951
|
-
_optionalChain([this, 'access',
|
|
1231
|
+
_optionalChain([this, 'access', _73 => _73.standbyTimer, 'access', _74 => _74.unref, 'optionalCall', _75 => _75()]);
|
|
952
1232
|
}
|
|
953
1233
|
clearStandbyTimer() {
|
|
954
1234
|
if (this.standbyTimer) clearTimeout(this.standbyTimer);
|
|
@@ -1002,6 +1282,12 @@ var LivestreamEngine = (_class = class {
|
|
|
1002
1282
|
this.pendingReplayError = error;
|
|
1003
1283
|
});
|
|
1004
1284
|
}
|
|
1285
|
+
if (online && _optionalChain([this, 'access', _76 => _76.room, 'optionalAccess', _77 => _77.needsManualReconnect])) {
|
|
1286
|
+
const s = this.snapshot.state;
|
|
1287
|
+
if (s === "reconnecting" || s === "error" && this.snapshot.sessionId) {
|
|
1288
|
+
this.scheduleRedial(0);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1005
1291
|
});
|
|
1006
1292
|
}
|
|
1007
1293
|
requireRoom() {
|
|
@@ -1015,7 +1301,7 @@ var LivestreamEngine = (_class = class {
|
|
|
1015
1301
|
this.room = void 0;
|
|
1016
1302
|
try {
|
|
1017
1303
|
unsubscribe();
|
|
1018
|
-
} catch (
|
|
1304
|
+
} catch (e8) {
|
|
1019
1305
|
}
|
|
1020
1306
|
if (!room) return Promise.resolve();
|
|
1021
1307
|
return beforeDispose.then(() => room.dispose());
|
|
@@ -1044,7 +1330,10 @@ var LivestreamEngine = (_class = class {
|
|
|
1044
1330
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
1045
1331
|
this.tokenRefreshTimer = void 0;
|
|
1046
1332
|
if (!this.grant || !this.snapshot.sessionId) return;
|
|
1047
|
-
const
|
|
1333
|
+
const horizon = Math.min(this.grant.tokenExpiresAt, this.grant.telemetry.expiresAt);
|
|
1334
|
+
const remaining = horizon - this.now();
|
|
1335
|
+
if (delayOverride === void 0 && remaining <= 6e4) return;
|
|
1336
|
+
const delay = _nullishCoalesce(delayOverride, () => ( Math.max(1e3, remaining - 6e4)));
|
|
1048
1337
|
this.tokenRefreshTimer = setTimeout(() => {
|
|
1049
1338
|
void this.refreshCredentials().catch(() => {
|
|
1050
1339
|
if (this.disposed) return;
|
|
@@ -1056,7 +1345,7 @@ var LivestreamEngine = (_class = class {
|
|
|
1056
1345
|
this.scheduleTokenRefresh(retryDelay);
|
|
1057
1346
|
});
|
|
1058
1347
|
}, delay);
|
|
1059
|
-
_optionalChain([this, 'access',
|
|
1348
|
+
_optionalChain([this, 'access', _78 => _78.tokenRefreshTimer, 'access', _79 => _79.unref, 'optionalCall', _80 => _80()]);
|
|
1060
1349
|
}
|
|
1061
1350
|
scheduleMaxSessionDuration() {
|
|
1062
1351
|
if (this.startedAt === void 0 || !this.snapshot.config) return;
|
|
@@ -1067,18 +1356,18 @@ var LivestreamEngine = (_class = class {
|
|
|
1067
1356
|
this.maxSessionTimer = setTimeout(() => {
|
|
1068
1357
|
void this.stop("error");
|
|
1069
1358
|
}, delay);
|
|
1070
|
-
_optionalChain([this, 'access',
|
|
1359
|
+
_optionalChain([this, 'access', _81 => _81.maxSessionTimer, 'access', _82 => _82.unref, 'optionalCall', _83 => _83()]);
|
|
1071
1360
|
}
|
|
1072
1361
|
restartHeartbeat() {
|
|
1073
1362
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1074
1363
|
const interval = Math.max(
|
|
1075
1364
|
1e4,
|
|
1076
|
-
_nullishCoalesce(_optionalChain([this, 'access',
|
|
1365
|
+
_nullishCoalesce(_optionalChain([this, 'access', _84 => _84.snapshot, 'access', _85 => _85.config, 'optionalAccess', _86 => _86.telemetryIntervalMs]), () => ( 6e4))
|
|
1077
1366
|
);
|
|
1078
1367
|
this.heartbeatTimer = setInterval(() => {
|
|
1079
1368
|
void this.sendHeartbeat();
|
|
1080
1369
|
}, interval);
|
|
1081
|
-
_optionalChain([this, 'access',
|
|
1370
|
+
_optionalChain([this, 'access', _87 => _87.heartbeatTimer, 'access', _88 => _88.unref, 'optionalCall', _89 => _89()]);
|
|
1082
1371
|
}
|
|
1083
1372
|
async enterSession(generation, publish) {
|
|
1084
1373
|
this.ensureOperation(generation);
|
|
@@ -1170,14 +1459,14 @@ var LivestreamEngine = (_class = class {
|
|
|
1170
1459
|
)
|
|
1171
1460
|
).catch(() => {
|
|
1172
1461
|
});
|
|
1173
|
-
} catch (
|
|
1462
|
+
} catch (e9) {
|
|
1174
1463
|
markerReady = Promise.resolve();
|
|
1175
1464
|
}
|
|
1176
1465
|
const completion = Promise.all([markerReady, beforeEnd]).then(async () => {
|
|
1177
1466
|
try {
|
|
1178
1467
|
await this.dependencies.sessionProvider.end({ sessionId, reason });
|
|
1179
1468
|
await this.dependencies.storage.removeItem(PENDING_END_KEY);
|
|
1180
|
-
} catch (
|
|
1469
|
+
} catch (e10) {
|
|
1181
1470
|
}
|
|
1182
1471
|
});
|
|
1183
1472
|
const termination = { markerReady, completion };
|
|
@@ -1210,287 +1499,180 @@ var LivestreamEngine = (_class = class {
|
|
|
1210
1499
|
}
|
|
1211
1500
|
}, _class);
|
|
1212
1501
|
|
|
1213
|
-
// src/
|
|
1214
|
-
var
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
}
|
|
1241
|
-
]);
|
|
1242
|
-
}
|
|
1243
|
-
for (const [event, listener] of this.nativeListeners) {
|
|
1244
|
-
this.room.on(event, listener);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
__init19() {this.listeners = /* @__PURE__ */ new Set()}
|
|
1249
|
-
__init20() {this.tracks = []}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
// Khớp engine (isCameraFront: false) — xem ghi chú ở engine.ts.
|
|
1254
|
-
__init21() {this.isFront = false}
|
|
1255
|
-
__init22() {this.orientationOverride = null}
|
|
1256
|
-
__init23() {this.deviceOverride = null}
|
|
1257
|
-
__init24() {this.published = false}
|
|
1258
|
-
__init25() {this.disposed = false}
|
|
1259
|
-
__init26() {this.quality = 720}
|
|
1260
|
-
|
|
1261
|
-
async preparePreview(quality) {
|
|
1262
|
-
this.ensureNotDisposed();
|
|
1263
|
-
this.quality = quality;
|
|
1264
|
-
await this.ensurePreparedTracks();
|
|
1265
|
-
this.ensureNotDisposed();
|
|
1266
|
-
return this.preview;
|
|
1267
|
-
}
|
|
1268
|
-
async connect(input) {
|
|
1269
|
-
this.ensureNotDisposed();
|
|
1270
|
-
if (!/^wss?:\/\//i.test(input.serverUrl)) {
|
|
1271
|
-
throw new Error(
|
|
1272
|
-
`Mode LiveKit c\u1EA7n endpoint wss:// nh\u01B0ng phi\xEAn tr\u1EA3 v\u1EC1 "${input.serverUrl}". App tr\xEAn server \u0111ang \u0111\u1EB7t deliveryMode kh\xE1c \u2014 \u0111\u1ED5i mode trong app cho kh\u1EDBp.`
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
if (!this.preview) throw new Error("Preview must be prepared before connecting");
|
|
1276
|
-
if (this.room.state !== "disconnected") await this.disconnect(false);
|
|
1277
|
-
await this.ensurePreparedTracks();
|
|
1278
|
-
if (!this.videoTrack || !this.audioTrack || !this.preview) {
|
|
1279
|
-
throw new Error("Camera and microphone tracks are unavailable");
|
|
1280
|
-
}
|
|
1281
|
-
await this.bindings.startAudioSession();
|
|
1282
|
-
await this.room.connect(input.serverUrl, input.participantToken, {
|
|
1283
|
-
autoSubscribe: false
|
|
1284
|
-
});
|
|
1285
|
-
this.published = false;
|
|
1286
|
-
if (input.microphoneEnabled) await _optionalChain([this, 'access', _65 => _65.audioTrack, 'access', _66 => _66.unmute, 'optionalCall', _67 => _67()]);
|
|
1287
|
-
else await _optionalChain([this, 'access', _68 => _68.audioTrack, 'access', _69 => _69.mute, 'optionalCall', _70 => _70()]);
|
|
1288
|
-
if (input.publish !== false) {
|
|
1289
|
-
await this.setPublishing(true);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
/** Bật/tắt publish track khi đã trong phòng — standby ↔ live. */
|
|
1293
|
-
async setPublishing(enabled) {
|
|
1294
|
-
this.ensureNotDisposed();
|
|
1295
|
-
if (enabled === this.published) return;
|
|
1296
|
-
if (!this.videoTrack || !this.audioTrack || !this.preview) {
|
|
1297
|
-
throw new Error("Camera and microphone tracks are unavailable");
|
|
1298
|
-
}
|
|
1299
|
-
if (enabled) {
|
|
1300
|
-
const publication = await this.room.localParticipant.publishTrack(
|
|
1301
|
-
this.videoTrack,
|
|
1302
|
-
this.videoPublishOptions()
|
|
1303
|
-
);
|
|
1304
|
-
await this.room.localParticipant.publishTrack(this.audioTrack);
|
|
1305
|
-
this.preview.trackRef = {
|
|
1306
|
-
participant: this.room.localParticipant,
|
|
1307
|
-
publication,
|
|
1308
|
-
source: this.bindings.cameraSource
|
|
1309
|
-
};
|
|
1310
|
-
this.published = true;
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
await _optionalChain([this, 'access', _71 => _71.room, 'access', _72 => _72.localParticipant, 'access', _73 => _73.unpublishTrack, 'optionalCall', _74 => _74(this.videoTrack, false)]);
|
|
1314
|
-
await _optionalChain([this, 'access', _75 => _75.room, 'access', _76 => _76.localParticipant, 'access', _77 => _77.unpublishTrack, 'optionalCall', _78 => _78(this.audioTrack, false)]);
|
|
1315
|
-
delete this.preview.trackRef;
|
|
1316
|
-
this.published = false;
|
|
1317
|
-
}
|
|
1318
|
-
async disconnect(stopTracks = true) {
|
|
1319
|
-
this.ensureNotDisposed();
|
|
1320
|
-
await this.room.disconnect(stopTracks);
|
|
1321
|
-
this.published = false;
|
|
1322
|
-
if (this.preview) delete this.preview.trackRef;
|
|
1323
|
-
await this.bindings.stopAudioSession();
|
|
1324
|
-
if (stopTracks) this.releaseTracks(true);
|
|
1325
|
-
}
|
|
1326
|
-
captureResolution() {
|
|
1327
|
-
const [long, short] = this.quality === 1080 ? [1920, 1080] : [1280, 720];
|
|
1328
|
-
return { width: long, height: short, frameRate: 30 };
|
|
1329
|
-
}
|
|
1330
|
-
/**
|
|
1331
|
-
* Tùy chọn publish video. Mặc định của livekit-client (VP8, không encoding,
|
|
1332
|
-
* không degradation preference) khiến uplink yếu bóp bitrate xuống ~600kbps
|
|
1333
|
-
* mà VẪN giữ 1080 @ ~10fps → khối bể nát, tệ hơn 720p sạch.
|
|
1334
|
-
*
|
|
1335
|
-
* - videoCodec H.264: HW-encode trên Android, hiệu quả hơn VP8 ở bitrate thấp,
|
|
1336
|
-
* trùng preset egress (H264_*) nên bỏ được 1 lần transcode phía egress.
|
|
1337
|
-
* - simulcast tắt: viewer chỉ xem HLS do egress xuất; egress subscribe đúng
|
|
1338
|
-
* 1 lớp đã publish, nên simulcast chỉ thêm tải/bất ổn encoder (H.264 simulcast
|
|
1339
|
-
* kém tin cậy trên Android) mà không lợi gì cho người xem ở kiến trúc này.
|
|
1340
|
-
* - maintain-framerate: pickleball chuyển động nhanh; khi uplink không gánh nổi
|
|
1341
|
-
* mức đích thì HẠ ĐỘ PHÂN GIẢI (giữ nét-trên-điểm-ảnh) thay vì đóng băng fps.
|
|
1342
|
-
* - maxBitrate: trần theo mức chất lượng; BWE vẫn tự hạ dưới trần khi mạng yếu.
|
|
1343
|
-
*/
|
|
1344
|
-
videoPublishOptions() {
|
|
1345
|
-
return {
|
|
1346
|
-
source: this.bindings.cameraSource,
|
|
1347
|
-
simulcast: false,
|
|
1348
|
-
videoCodec: "h264",
|
|
1349
|
-
degradationPreference: "maintain-framerate",
|
|
1350
|
-
videoEncoding: {
|
|
1351
|
-
maxBitrate: this.quality === 1080 ? 45e5 : 25e5,
|
|
1352
|
-
maxFramerate: 30
|
|
1353
|
-
}
|
|
1354
|
-
};
|
|
1355
|
-
}
|
|
1356
|
-
async setCaptureOrientation(orientation) {
|
|
1357
|
-
this.ensureNotDisposed();
|
|
1358
|
-
this.orientationOverride = orientation;
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* livekit-client restart() chỉ giữ deviceId/facingMode khi gọi getUserMedia
|
|
1362
|
-
* (vứt width/height/frameRate) nên track mới rơi về format mặc định
|
|
1363
|
-
* 640x480. Native applyConstraints thì ngược lại: chỉ xử lý
|
|
1364
|
-
* width/height/frameRate. Sau mỗi restartTrack phải nắn lại format trên
|
|
1365
|
-
* track mới bằng applyConstraints.
|
|
1366
|
-
*/
|
|
1367
|
-
async reapplyCaptureFormat() {
|
|
1368
|
-
const nativeTrack = _optionalChain([this, 'access', _79 => _79.videoTrack, 'optionalAccess', _80 => _80.mediaStreamTrack]);
|
|
1369
|
-
if (_optionalChain([nativeTrack, 'optionalAccess', _81 => _81.applyConstraints])) {
|
|
1370
|
-
await nativeTrack.applyConstraints(this.captureResolution());
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
async switchCamera() {
|
|
1374
|
-
this.ensureNotDisposed();
|
|
1375
|
-
const facingMode = this.isFront ? "environment" : "user";
|
|
1376
|
-
const nativeTrack = _optionalChain([this, 'access', _82 => _82.videoTrack, 'optionalAccess', _83 => _83.mediaStreamTrack]);
|
|
1377
|
-
if (typeof _optionalChain([this, 'access', _84 => _84.videoTrack, 'optionalAccess', _85 => _85.restartTrack]) === "function") {
|
|
1378
|
-
await this.videoTrack.restartTrack({ facingMode });
|
|
1379
|
-
await this.reapplyCaptureFormat();
|
|
1380
|
-
} else if (typeof _optionalChain([nativeTrack, 'optionalAccess', _86 => _86._switchCamera]) === "function") {
|
|
1381
|
-
nativeTrack._switchCamera();
|
|
1382
|
-
} else if (_optionalChain([nativeTrack, 'optionalAccess', _87 => _87.applyConstraints])) {
|
|
1383
|
-
await nativeTrack.applyConstraints({ facingMode });
|
|
1384
|
-
} else {
|
|
1385
|
-
throw new Error("Camera switching is unavailable");
|
|
1386
|
-
}
|
|
1387
|
-
this.deviceOverride = null;
|
|
1388
|
-
this.isFront = !this.isFront;
|
|
1389
|
-
}
|
|
1390
|
-
async listCameras() {
|
|
1391
|
-
this.ensureNotDisposed();
|
|
1392
|
-
return await _asyncNullishCoalesce(await _optionalChain([this, 'access', _88 => _88.bindings, 'access', _89 => _89.listCameras, 'optionalCall', _90 => _90()]), async () => ( []));
|
|
1393
|
-
}
|
|
1394
|
-
async selectCamera(deviceId, facing) {
|
|
1395
|
-
this.ensureNotDisposed();
|
|
1396
|
-
if (typeof _optionalChain([this, 'access', _91 => _91.videoTrack, 'optionalAccess', _92 => _92.restartTrack]) !== "function") {
|
|
1397
|
-
throw new Error("Camera selection is unavailable");
|
|
1398
|
-
}
|
|
1399
|
-
await this.videoTrack.restartTrack({ deviceId });
|
|
1400
|
-
await this.reapplyCaptureFormat();
|
|
1401
|
-
this.deviceOverride = deviceId;
|
|
1402
|
-
if (facing !== "unknown") this.isFront = facing === "front";
|
|
1403
|
-
}
|
|
1404
|
-
async setMicrophoneEnabled(enabled) {
|
|
1405
|
-
this.ensureNotDisposed();
|
|
1406
|
-
if (!this.audioTrack) return;
|
|
1407
|
-
if (enabled) await _optionalChain([this, 'access', _93 => _93.audioTrack, 'access', _94 => _94.unmute, 'optionalCall', _95 => _95()]);
|
|
1408
|
-
else await _optionalChain([this, 'access', _96 => _96.audioTrack, 'access', _97 => _97.mute, 'optionalCall', _98 => _98()]);
|
|
1409
|
-
}
|
|
1410
|
-
subscribe(listener) {
|
|
1411
|
-
this.ensureNotDisposed();
|
|
1412
|
-
this.listeners.add(listener);
|
|
1413
|
-
return () => this.listeners.delete(listener);
|
|
1502
|
+
// src/uplink-check.ts
|
|
1503
|
+
var VIDEO_BITRATE_BPS = Object.freeze({
|
|
1504
|
+
720: 35e5,
|
|
1505
|
+
1080: 6e6
|
|
1506
|
+
});
|
|
1507
|
+
var AUDIO_BITRATE_BPS = 128e3;
|
|
1508
|
+
var DEFAULT_UPLOAD_URL = "https://speed.cloudflare.com/__up";
|
|
1509
|
+
var WARMUP_BYTES = 512 * 1024;
|
|
1510
|
+
function requiredMbpsFor(settings) {
|
|
1511
|
+
return (VIDEO_BITRATE_BPS[settings.resolution] + AUDIO_BITRATE_BPS) / 1e6;
|
|
1512
|
+
}
|
|
1513
|
+
function verdictFor(uploadMbps, requiredMbps) {
|
|
1514
|
+
if (uploadMbps >= requiredMbps * 1.8) return "good";
|
|
1515
|
+
if (uploadMbps >= requiredMbps * 1.25) return "ok";
|
|
1516
|
+
return "low";
|
|
1517
|
+
}
|
|
1518
|
+
function chunk(bytes) {
|
|
1519
|
+
const buffer = new Uint8Array(bytes);
|
|
1520
|
+
for (let i = 0; i < bytes; i += 977) buffer[i] = i & 255;
|
|
1521
|
+
return buffer;
|
|
1522
|
+
}
|
|
1523
|
+
function resolveFetch(explicit) {
|
|
1524
|
+
if (explicit) return explicit;
|
|
1525
|
+
try {
|
|
1526
|
+
const expoFetch = _optionalChain([_chunk7OX2DSKIcjs.__require.call(void 0, "expo/fetch"), 'optionalAccess', _90 => _90.fetch]);
|
|
1527
|
+
if (typeof expoFetch === "function") return expoFetch;
|
|
1528
|
+
} catch (e11) {
|
|
1414
1529
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1530
|
+
return globalThis.fetch;
|
|
1531
|
+
}
|
|
1532
|
+
async function runUplinkCheck(options) {
|
|
1533
|
+
const fetchImplementation = resolveFetch(options.fetch);
|
|
1534
|
+
const uploadUrl = _nullishCoalesce(options.uploadUrl, () => ( DEFAULT_UPLOAD_URL));
|
|
1535
|
+
const measureMs = _nullishCoalesce(options.measureMs, () => ( 6e3));
|
|
1536
|
+
const totalTimeoutMs = _nullishCoalesce(options.totalTimeoutMs, () => ( 25e3));
|
|
1537
|
+
const chunkBytes = _nullishCoalesce(options.chunkBytes, () => ( 2 * 1024 * 1024));
|
|
1538
|
+
async function post(body2, timeoutMs) {
|
|
1539
|
+
const abort = new AbortController();
|
|
1540
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
1541
|
+
try {
|
|
1542
|
+
const response = await fetchImplementation(uploadUrl, {
|
|
1543
|
+
method: "POST",
|
|
1544
|
+
body: body2,
|
|
1545
|
+
signal: abort.signal
|
|
1546
|
+
});
|
|
1547
|
+
await response.text().catch(() => void 0);
|
|
1548
|
+
} finally {
|
|
1549
|
+
clearTimeout(timer);
|
|
1420
1550
|
}
|
|
1421
|
-
await this.room.disconnect(true);
|
|
1422
|
-
await this.bindings.stopAudioSession();
|
|
1423
|
-
this.releaseTracks(true);
|
|
1424
|
-
this.listeners.clear();
|
|
1425
|
-
}
|
|
1426
|
-
releaseTracks(clearPreview) {
|
|
1427
|
-
for (const track of this.tracks) track.stop();
|
|
1428
|
-
this.tracks = [];
|
|
1429
|
-
this.videoTrack = void 0;
|
|
1430
|
-
this.audioTrack = void 0;
|
|
1431
|
-
if (clearPreview) this.preview = void 0;
|
|
1432
|
-
else if (this.preview) delete this.preview.trackRef;
|
|
1433
1551
|
}
|
|
1434
|
-
async
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1552
|
+
async function measurePing(url) {
|
|
1553
|
+
let best = null;
|
|
1554
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1555
|
+
const abort = new AbortController();
|
|
1556
|
+
const timer = setTimeout(() => abort.abort(), 4e3);
|
|
1557
|
+
const started2 = Date.now();
|
|
1558
|
+
try {
|
|
1559
|
+
await fetchImplementation(url, { method: "HEAD", signal: abort.signal });
|
|
1560
|
+
const elapsed = Date.now() - started2;
|
|
1561
|
+
if (best === null || elapsed < best) best = elapsed;
|
|
1562
|
+
} catch (e12) {
|
|
1563
|
+
} finally {
|
|
1564
|
+
clearTimeout(timer);
|
|
1445
1565
|
}
|
|
1446
|
-
});
|
|
1447
|
-
if (this.disposed) {
|
|
1448
|
-
for (const track of tracks) track.stop();
|
|
1449
|
-
throw new Error("LiveKit room adapter has been disposed");
|
|
1450
|
-
}
|
|
1451
|
-
this.tracks = tracks;
|
|
1452
|
-
this.videoTrack = this.tracks.find((track) => track.kind === "video");
|
|
1453
|
-
this.audioTrack = this.tracks.find((track) => track.kind === "audio");
|
|
1454
|
-
if (!this.videoTrack || !this.audioTrack) {
|
|
1455
|
-
this.releaseTracks(true);
|
|
1456
|
-
throw new Error("LiveKit did not create camera and microphone tracks");
|
|
1457
|
-
}
|
|
1458
|
-
if (this.preview) {
|
|
1459
|
-
this.preview.videoTrack = this.videoTrack;
|
|
1460
|
-
delete this.preview.trackRef;
|
|
1461
|
-
} else {
|
|
1462
|
-
this.preview = { videoTrack: this.videoTrack };
|
|
1463
1566
|
}
|
|
1567
|
+
return best;
|
|
1464
1568
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1569
|
+
const deadline = Date.now() + totalTimeoutMs;
|
|
1570
|
+
const requiredMbps = requiredMbpsFor(options);
|
|
1571
|
+
const isAbort = (error) => _optionalChain([error, 'optionalAccess', _91 => _91.name]) === "AbortError";
|
|
1572
|
+
const warmupBytes = Math.min(WARMUP_BYTES, chunkBytes);
|
|
1573
|
+
const warmupStarted = Date.now();
|
|
1574
|
+
try {
|
|
1575
|
+
await post(chunk(warmupBytes), _nullishCoalesce(options.warmupTimeoutMs, () => ( 1e4)));
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
if (!isAbort(error)) throw error;
|
|
1578
|
+
const seconds = Math.max(1, (Date.now() - warmupStarted) / 1e3);
|
|
1579
|
+
const uploadMbps2 = warmupBytes * 8 / seconds / 1e6;
|
|
1580
|
+
return { uploadMbps: uploadMbps2, pingMs: null, requiredMbps, verdict: "low" };
|
|
1581
|
+
}
|
|
1582
|
+
const body = chunk(chunkBytes);
|
|
1583
|
+
let sentBytes = 0;
|
|
1584
|
+
const started = Date.now();
|
|
1585
|
+
while (Date.now() - started < measureMs && Date.now() < deadline) {
|
|
1586
|
+
try {
|
|
1587
|
+
await post(body, Math.max(3e3, deadline - Date.now()));
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
if (!isAbort(error)) throw error;
|
|
1590
|
+
break;
|
|
1471
1591
|
}
|
|
1592
|
+
sentBytes += body.byteLength;
|
|
1472
1593
|
}
|
|
1473
|
-
|
|
1594
|
+
const elapsedSeconds = Math.max((Date.now() - started) / 1e3, 1e-3);
|
|
1595
|
+
const uploadMbps = sentBytes * 8 / elapsedSeconds / 1e6;
|
|
1596
|
+
return {
|
|
1597
|
+
uploadMbps,
|
|
1598
|
+
pingMs: options.pingUrl ? await measurePing(options.pingUrl) : null,
|
|
1599
|
+
requiredMbps,
|
|
1600
|
+
verdict: verdictFor(uploadMbps, requiredMbps)
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1474
1603
|
|
|
1475
1604
|
// src/rtmp-room-adapter.ts
|
|
1476
|
-
var VIDEO_BITRATE =
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
var
|
|
1481
|
-
|
|
1605
|
+
var VIDEO_BITRATE = VIDEO_BITRATE_BPS;
|
|
1606
|
+
var BITRATE_FLOOR = 8e5;
|
|
1607
|
+
var DEGRADE_FACTOR = 0.7;
|
|
1608
|
+
var RECOVER_FACTOR = 1.15;
|
|
1609
|
+
var RECOVER_AFTER_SUFFICIENT = 5;
|
|
1610
|
+
function nextBitrateState(state, event) {
|
|
1611
|
+
if (!event.sufficient) {
|
|
1612
|
+
return {
|
|
1613
|
+
...state,
|
|
1614
|
+
current: Math.max(BITRATE_FLOOR, Math.round(state.current * DEGRADE_FACTOR)),
|
|
1615
|
+
sufficientStreak: 0
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
const streak = state.sufficientStreak + 1;
|
|
1619
|
+
if (streak < RECOVER_AFTER_SUFFICIENT || state.current >= state.ceiling) {
|
|
1620
|
+
return { ...state, sufficientStreak: Math.min(streak, RECOVER_AFTER_SUFFICIENT) };
|
|
1621
|
+
}
|
|
1622
|
+
return {
|
|
1623
|
+
...state,
|
|
1624
|
+
current: Math.min(state.ceiling, Math.round(state.current * RECOVER_FACTOR)),
|
|
1625
|
+
sufficientStreak: 0
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
var RtmpRoomAdapter = (_class2 = class {
|
|
1629
|
+
constructor(bindings, options = {}) {;_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);
|
|
1482
1630
|
this.bindings = bindings;
|
|
1631
|
+
this.options = options;
|
|
1632
|
+
const request = _optionalChain([bindings, 'access', _92 => _92.requestPermissions, 'optionalAccess', _93 => _93.bind, 'call', _94 => _94(bindings)]);
|
|
1633
|
+
if (request) this.requestPermissions = request;
|
|
1483
1634
|
}
|
|
1484
|
-
|
|
1635
|
+
/**
|
|
1636
|
+
* RTMP không tự hồi: native chỉ phát hiện đứt (watchdog iOS / onDisconnect
|
|
1637
|
+
* Android) và báo "disconnected" — không tự quay số lại. Engine thấy cờ này
|
|
1638
|
+
* là chạy vòng redial backoff. Thiếu cờ: phiên ngồi chờ "reconnected" không
|
|
1639
|
+
* bao giờ tới rồi chết network_timeout sau 30s (prod 17/08/2026).
|
|
1640
|
+
*/
|
|
1641
|
+
__init23() {this.needsManualReconnect = true}
|
|
1642
|
+
__init24() {this.listeners = /* @__PURE__ */ new Set()}
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
__init25() {this.quality = 1080}
|
|
1646
|
+
__init26() {this.connected = false}
|
|
1647
|
+
__init27() {this.prepared = false}
|
|
1648
|
+
/** Khung người dùng chọn trên UI (9:16/16:9) — ưu tiên hơn hướng cầm máy. */
|
|
1485
1649
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Chỉ tồn tại khi bindings hỗ trợ — engine kiểm tra truthiness để quyết định
|
|
1653
|
+
* fallback về runtime.requestPermissions() (getUserMedia), nên KHÔNG được
|
|
1654
|
+
* khai báo method luôn-có rồi tự fallback bên trong.
|
|
1655
|
+
*/
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Phần tuỳ chọn của prepare(). Giữ dạng spread có điều kiện: truyền
|
|
1659
|
+
* `frameRate: undefined` xuống Expo Record là ghi đè mặc định của native
|
|
1660
|
+
* bằng null chứ không phải "bỏ qua trường".
|
|
1661
|
+
*/
|
|
1662
|
+
prepareOverrides() {
|
|
1663
|
+
return {
|
|
1664
|
+
...this.options.frameRate !== void 0 ? { frameRate: this.options.frameRate } : {},
|
|
1665
|
+
...this.options.cameraId !== void 0 ? { cameraId: this.options.cameraId } : {}
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1488
1668
|
async preparePreview(quality) {
|
|
1489
1669
|
this.quality = quality;
|
|
1490
1670
|
await this.bindings.prepare({
|
|
1491
1671
|
quality,
|
|
1492
|
-
landscape: _nullishCoalesce(_optionalChain([this, 'access',
|
|
1672
|
+
landscape: this.captureOrientation ? this.captureOrientation === "landscape" : _nullishCoalesce(_optionalChain([this, 'access', _95 => _95.bindings, 'access', _96 => _96.isLandscape, 'optionalCall', _97 => _97()]), () => ( true)),
|
|
1673
|
+
...this.prepareOverrides()
|
|
1493
1674
|
});
|
|
1675
|
+
this.prepared = true;
|
|
1494
1676
|
return { kind: "rtmp" };
|
|
1495
1677
|
}
|
|
1496
1678
|
async connect(input) {
|
|
@@ -1508,6 +1690,11 @@ var RtmpRoomAdapter = (_class3 = class {
|
|
|
1508
1690
|
publish: input.publish !== false
|
|
1509
1691
|
});
|
|
1510
1692
|
this.connected = true;
|
|
1693
|
+
this.bitrateState = {
|
|
1694
|
+
current: VIDEO_BITRATE[this.quality],
|
|
1695
|
+
ceiling: VIDEO_BITRATE[this.quality],
|
|
1696
|
+
sufficientStreak: 0
|
|
1697
|
+
};
|
|
1511
1698
|
}
|
|
1512
1699
|
async setPublishing(enabled) {
|
|
1513
1700
|
await this.bindings.setPublishing(enabled);
|
|
@@ -1515,6 +1702,7 @@ var RtmpRoomAdapter = (_class3 = class {
|
|
|
1515
1702
|
async disconnect(_stopTracks) {
|
|
1516
1703
|
if (!this.connected) return;
|
|
1517
1704
|
this.connected = false;
|
|
1705
|
+
this.bitrateState = void 0;
|
|
1518
1706
|
await this.bindings.disconnect();
|
|
1519
1707
|
}
|
|
1520
1708
|
async switchCamera() {
|
|
@@ -1538,7 +1726,15 @@ var RtmpRoomAdapter = (_class3 = class {
|
|
|
1538
1726
|
async setVideoBitrate(bitsPerSecond) {
|
|
1539
1727
|
await this.bindings.setVideoBitrate(bitsPerSecond);
|
|
1540
1728
|
}
|
|
1541
|
-
async setCaptureOrientation(
|
|
1729
|
+
async setCaptureOrientation(orientation) {
|
|
1730
|
+
this.captureOrientation = orientation;
|
|
1731
|
+
if (this.prepared && !this.connected) {
|
|
1732
|
+
await this.bindings.prepare({
|
|
1733
|
+
quality: this.quality,
|
|
1734
|
+
landscape: orientation === "landscape",
|
|
1735
|
+
...this.prepareOverrides()
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1542
1738
|
}
|
|
1543
1739
|
subscribe(listener) {
|
|
1544
1740
|
this.listeners.add(listener);
|
|
@@ -1548,21 +1744,38 @@ var RtmpRoomAdapter = (_class3 = class {
|
|
|
1548
1744
|
if (mapped) this.emit(mapped);
|
|
1549
1745
|
});
|
|
1550
1746
|
}
|
|
1747
|
+
if (!this.bandwidthSubscription && this.bindings.addBandwidthListener) {
|
|
1748
|
+
this.bandwidthSubscription = this.bindings.addBandwidthListener(
|
|
1749
|
+
(event) => this.handleBandwidth(event)
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1551
1752
|
return () => {
|
|
1552
1753
|
this.listeners.delete(listener);
|
|
1553
1754
|
};
|
|
1554
1755
|
}
|
|
1555
1756
|
async dispose() {
|
|
1556
|
-
_optionalChain([this, 'access',
|
|
1757
|
+
_optionalChain([this, 'access', _98 => _98.nativeSubscription, 'optionalAccess', _99 => _99.remove, 'call', _100 => _100()]);
|
|
1557
1758
|
this.nativeSubscription = void 0;
|
|
1759
|
+
_optionalChain([this, 'access', _101 => _101.bandwidthSubscription, 'optionalAccess', _102 => _102.remove, 'call', _103 => _103()]);
|
|
1760
|
+
this.bandwidthSubscription = void 0;
|
|
1558
1761
|
this.listeners.clear();
|
|
1559
1762
|
this.connected = false;
|
|
1763
|
+
this.bitrateState = void 0;
|
|
1560
1764
|
await this.bindings.dispose();
|
|
1561
1765
|
}
|
|
1766
|
+
handleBandwidth(event) {
|
|
1767
|
+
const state = this.bitrateState;
|
|
1768
|
+
if (!this.connected || !state) return;
|
|
1769
|
+
const next = nextBitrateState(state, event);
|
|
1770
|
+
this.bitrateState = next;
|
|
1771
|
+
if (next.current !== state.current) {
|
|
1772
|
+
void this.bindings.setVideoBitrate(next.current).catch(() => void 0);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1562
1775
|
emit(event) {
|
|
1563
1776
|
for (const listener of this.listeners) listener(event);
|
|
1564
1777
|
}
|
|
1565
|
-
},
|
|
1778
|
+
}, _class2);
|
|
1566
1779
|
function mapStatus(event) {
|
|
1567
1780
|
switch (event.status) {
|
|
1568
1781
|
case "error":
|
|
@@ -1580,74 +1793,55 @@ function mapStatus(event) {
|
|
|
1580
1793
|
}
|
|
1581
1794
|
|
|
1582
1795
|
// src/native-runtime.ts
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
const reactNativeLiveKit = _chunk7OX2DSKIcjs.__require.call(void 0, "@livekit/react-native");
|
|
1586
|
-
const liveKit = _chunk7OX2DSKIcjs.__require.call(void 0, "livekit-client");
|
|
1587
|
-
return {
|
|
1588
|
-
registerGlobals() {
|
|
1589
|
-
if (!globalsRegistered) {
|
|
1590
|
-
reactNativeLiveKit.registerGlobals();
|
|
1591
|
-
globalsRegistered = true;
|
|
1592
|
-
}
|
|
1593
|
-
},
|
|
1594
|
-
createRoom: () => new liveKit.Room({ adaptiveStream: false, dynacast: false }),
|
|
1595
|
-
createLocalTracks: liveKit.createLocalTracks,
|
|
1596
|
-
startAudioSession: () => reactNativeLiveKit.AudioSession.startAudioSession(),
|
|
1597
|
-
stopAudioSession: () => reactNativeLiveKit.AudioSession.stopAudioSession(),
|
|
1598
|
-
isLandscape() {
|
|
1599
|
-
const { width, height } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native").Dimensions.get("window");
|
|
1600
|
-
return width > height;
|
|
1601
|
-
},
|
|
1602
|
-
async listCameras() {
|
|
1603
|
-
const { mediaDevices } = _chunk7OX2DSKIcjs.__require.call(void 0, "@livekit/react-native-webrtc");
|
|
1604
|
-
const devices = await mediaDevices.enumerateDevices();
|
|
1605
|
-
return devices.filter((device) => device.kind === "videoinput" && device.deviceId).map((device) => ({
|
|
1606
|
-
deviceId: String(device.deviceId),
|
|
1607
|
-
label: String(_nullishCoalesce(device.label, () => ( ""))),
|
|
1608
|
-
facing: device.facing === "front" || device.facing === "environment" ? device.facing : "unknown"
|
|
1609
|
-
}));
|
|
1610
|
-
},
|
|
1611
|
-
cameraSource: liveKit.Track.Source.Camera,
|
|
1612
|
-
events: {
|
|
1613
|
-
reconnecting: liveKit.RoomEvent.Reconnecting,
|
|
1614
|
-
reconnected: liveKit.RoomEvent.Reconnected,
|
|
1615
|
-
disconnected: liveKit.RoomEvent.Disconnected,
|
|
1616
|
-
mediaDevicesError: liveKit.RoomEvent.MediaDevicesError,
|
|
1617
|
-
dataReceived: liveKit.RoomEvent.DataReceived
|
|
1618
|
-
}
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
function createDefaultRuntime() {
|
|
1622
|
-
const { NativeModules, Platform } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native");
|
|
1796
|
+
function createRtmpRuntime() {
|
|
1797
|
+
const { Platform, PermissionsAndroid } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native");
|
|
1623
1798
|
return {
|
|
1624
1799
|
assertSupported() {
|
|
1625
|
-
if (Platform.OS !== "ios" && Platform.OS !== "android"
|
|
1800
|
+
if (Platform.OS !== "ios" && Platform.OS !== "android") {
|
|
1626
1801
|
throw new UnsupportedRuntimeError();
|
|
1627
1802
|
}
|
|
1803
|
+
try {
|
|
1804
|
+
_chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native");
|
|
1805
|
+
} catch (e13) {
|
|
1806
|
+
throw new UnsupportedRuntimeError(
|
|
1807
|
+
"@pickleball/rtmp-native is required: install it and rebuild the Development Build"
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1628
1810
|
},
|
|
1629
1811
|
async requestPermissions() {
|
|
1812
|
+
if (Platform.OS === "android") {
|
|
1813
|
+
try {
|
|
1814
|
+
const granted = await PermissionsAndroid.requestMultiple([
|
|
1815
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
1816
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
|
|
1817
|
+
]);
|
|
1818
|
+
return granted[PermissionsAndroid.PERMISSIONS.CAMERA] === PermissionsAndroid.RESULTS.GRANTED && granted[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === PermissionsAndroid.RESULTS.GRANTED;
|
|
1819
|
+
} catch (e14) {
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1630
1823
|
try {
|
|
1631
|
-
const
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
} catch (
|
|
1636
|
-
return false;
|
|
1824
|
+
const native = _chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native").default;
|
|
1825
|
+
if (typeof _optionalChain([native, 'optionalAccess', _104 => _104.requestPermissions]) === "function") {
|
|
1826
|
+
return await native.requestPermissions() !== false;
|
|
1827
|
+
}
|
|
1828
|
+
} catch (e15) {
|
|
1637
1829
|
}
|
|
1830
|
+
return true;
|
|
1638
1831
|
},
|
|
1639
|
-
activateKeepAwake()
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
deactivateKeepAwake() {
|
|
1645
|
-
|
|
1646
|
-
"@pickleball/expo-sdk"
|
|
1647
|
-
);
|
|
1832
|
+
activateKeepAwake: () => Promise.resolve(
|
|
1833
|
+
_chunk7OX2DSKIcjs.__require.call(void 0, "expo-keep-awake").activateKeepAwakeAsync("@pickleball/expo-sdk")
|
|
1834
|
+
).catch(() => void 0),
|
|
1835
|
+
// deactivate trả promise và REJECT khi activity đã chết (sau crash/reload) —
|
|
1836
|
+
// không catch là thêm một "Uncaught (in promise)" gây nhiễu log.
|
|
1837
|
+
deactivateKeepAwake: () => {
|
|
1838
|
+
void Promise.resolve(
|
|
1839
|
+
_chunk7OX2DSKIcjs.__require.call(void 0, "expo-keep-awake").deactivateKeepAwake("@pickleball/expo-sdk")
|
|
1840
|
+
).catch(() => void 0);
|
|
1648
1841
|
}
|
|
1649
1842
|
};
|
|
1650
1843
|
}
|
|
1844
|
+
var createDefaultRuntime = createRtmpRuntime;
|
|
1651
1845
|
function createDefaultStorage() {
|
|
1652
1846
|
let installed = false;
|
|
1653
1847
|
const install = () => {
|
|
@@ -1724,12 +1918,90 @@ function createDefaultTelemetry(fetchImplementation = globalThis.fetch, timeoutM
|
|
|
1724
1918
|
};
|
|
1725
1919
|
}
|
|
1726
1920
|
function createDefaultRoomFactory() {
|
|
1727
|
-
return ()
|
|
1921
|
+
return createRtmpRoomFactory();
|
|
1922
|
+
}
|
|
1923
|
+
function createDefaultCommandChannel(fetchImplementation = globalThis.fetch, timeoutMs = 1e4) {
|
|
1924
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
1925
|
+
return {
|
|
1926
|
+
async poll(credentials, after) {
|
|
1927
|
+
const endpoint = credentials.endpoint.replace(/\/telemetry$/, "/commands");
|
|
1928
|
+
const controller = new AbortController();
|
|
1929
|
+
controllers.add(controller);
|
|
1930
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1931
|
+
try {
|
|
1932
|
+
const response = await fetchImplementation(endpoint, {
|
|
1933
|
+
method: "POST",
|
|
1934
|
+
headers: {
|
|
1935
|
+
"Content-Type": "application/json",
|
|
1936
|
+
Authorization: `Bearer ${credentials.token}`
|
|
1937
|
+
},
|
|
1938
|
+
body: JSON.stringify({ after }),
|
|
1939
|
+
signal: controller.signal,
|
|
1940
|
+
redirect: "error"
|
|
1941
|
+
});
|
|
1942
|
+
if (!response.ok) {
|
|
1943
|
+
if (typeof response.text === "function") await response.text();
|
|
1944
|
+
throw Object.assign(new Error("Command poll rejected"), {
|
|
1945
|
+
status: response.status
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
const envelope = await response.json();
|
|
1949
|
+
return parseCommandPollResult(_optionalChain([envelope, 'optionalAccess', _105 => _105.data]));
|
|
1950
|
+
} finally {
|
|
1951
|
+
clearTimeout(timeout);
|
|
1952
|
+
controllers.delete(controller);
|
|
1953
|
+
}
|
|
1954
|
+
},
|
|
1955
|
+
dispose() {
|
|
1956
|
+
for (const controller of controllers) controller.abort();
|
|
1957
|
+
controllers.clear();
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function parseCommandPollResult(value) {
|
|
1962
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1963
|
+
throw new Error("Command poll returned an invalid payload");
|
|
1964
|
+
}
|
|
1965
|
+
const record = value;
|
|
1966
|
+
const status = record.status;
|
|
1967
|
+
if (status !== "scheduled" && status !== "live" && status !== "ended") {
|
|
1968
|
+
throw new Error("Command poll returned an invalid status");
|
|
1969
|
+
}
|
|
1970
|
+
const commands = [];
|
|
1971
|
+
if (Array.isArray(record.commands)) {
|
|
1972
|
+
for (const item of record.commands) {
|
|
1973
|
+
if (item === null || typeof item !== "object") continue;
|
|
1974
|
+
const command = item;
|
|
1975
|
+
if (typeof command.id !== "string" || command.action !== "start" && command.action !== "stop" && command.action !== "end" || typeof command.issuedAt !== "number" || !Number.isFinite(command.issuedAt)) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
commands.push({
|
|
1979
|
+
id: command.id,
|
|
1980
|
+
action: command.action,
|
|
1981
|
+
issuedAt: command.issuedAt
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return { commands, status };
|
|
1728
1986
|
}
|
|
1729
1987
|
function createNativeRtmpBindings() {
|
|
1730
1988
|
const native = _chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native").default;
|
|
1989
|
+
const { Platform, PermissionsAndroid } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native");
|
|
1731
1990
|
return {
|
|
1732
1991
|
prepare: (options) => native.prepare(options),
|
|
1992
|
+
// Android: xin quyền THUẦN, không mở camera như getUserMedia của WebRTC.
|
|
1993
|
+
// Máy MediaTek (Xiaomi tanzanite) chỉ cho một camera mở cùng lúc — camera
|
|
1994
|
+
// WebRTC chưa nhả (~400ms) là HaishinKit dính "Too many users (-87)" và
|
|
1995
|
+
// preview đen. iOS không có giới hạn đó nên để engine fallback getUserMedia.
|
|
1996
|
+
...Platform.OS === "android" ? {
|
|
1997
|
+
async requestPermissions() {
|
|
1998
|
+
const granted = await PermissionsAndroid.requestMultiple([
|
|
1999
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
2000
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
|
|
2001
|
+
]);
|
|
2002
|
+
return granted[PermissionsAndroid.PERMISSIONS.CAMERA] === PermissionsAndroid.RESULTS.GRANTED && granted[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === PermissionsAndroid.RESULTS.GRANTED;
|
|
2003
|
+
}
|
|
2004
|
+
} : {},
|
|
1733
2005
|
isLandscape() {
|
|
1734
2006
|
const { width, height } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native").Dimensions.get("window");
|
|
1735
2007
|
return width > height;
|
|
@@ -1743,11 +2015,12 @@ function createNativeRtmpBindings() {
|
|
|
1743
2015
|
setMicrophoneEnabled: (enabled) => native.setMicrophoneEnabled(enabled),
|
|
1744
2016
|
setVideoBitrate: (bps) => native.setVideoBitrate(bps),
|
|
1745
2017
|
dispose: () => native.dispose(),
|
|
1746
|
-
addStatusListener: (listener) => native.addListener("onStatusChanged", listener)
|
|
2018
|
+
addStatusListener: (listener) => native.addListener("onStatusChanged", listener),
|
|
2019
|
+
addBandwidthListener: (listener) => native.addListener("onBandwidth", listener)
|
|
1747
2020
|
};
|
|
1748
2021
|
}
|
|
1749
|
-
function createRtmpRoomFactory() {
|
|
1750
|
-
return () => new RtmpRoomAdapter(createNativeRtmpBindings());
|
|
2022
|
+
function createRtmpRoomFactory(options) {
|
|
2023
|
+
return () => new RtmpRoomAdapter(createNativeRtmpBindings(), options);
|
|
1751
2024
|
}
|
|
1752
2025
|
function createDefaultDeviceMetrics() {
|
|
1753
2026
|
return async () => {
|
|
@@ -1770,7 +2043,7 @@ function createDefaultDeviceMetrics() {
|
|
|
1770
2043
|
let FileSystem;
|
|
1771
2044
|
try {
|
|
1772
2045
|
FileSystem = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-file-system/legacy");
|
|
1773
|
-
} catch (
|
|
2046
|
+
} catch (e16) {
|
|
1774
2047
|
FileSystem = _chunk7OX2DSKIcjs.__require.call(void 0, "expo-file-system");
|
|
1775
2048
|
}
|
|
1776
2049
|
if (FileSystem.getFreeDiskStorageAsync) {
|
|
@@ -1787,17 +2060,40 @@ function createDefaultDeviceMetrics() {
|
|
|
1787
2060
|
if (typeof Device.totalMemory === "number") {
|
|
1788
2061
|
metrics.ramTotalMb = Math.round(Device.totalMemory / 1048576);
|
|
1789
2062
|
}
|
|
1790
|
-
} catch (
|
|
2063
|
+
} catch (e17) {
|
|
2064
|
+
}
|
|
2065
|
+
try {
|
|
2066
|
+
const native = _chunk7OX2DSKIcjs.__require.call(void 0, "@pickleball/rtmp-native").default;
|
|
2067
|
+
const health = await native.getDeviceHealth();
|
|
2068
|
+
if (typeof _optionalChain([health, 'optionalAccess', _106 => _106.thermal]) === "number") metrics.thermal = health.thermal;
|
|
2069
|
+
if (typeof _optionalChain([health, 'optionalAccess', _107 => _107.batteryTempC]) === "number") {
|
|
2070
|
+
metrics.batteryTempC = Math.round(health.batteryTempC * 10) / 10;
|
|
2071
|
+
}
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
notes.push(`device health: ${String(error)}`);
|
|
1791
2074
|
}
|
|
1792
2075
|
try {
|
|
1793
2076
|
const NetInfo = _chunk7OX2DSKIcjs.__require.call(void 0, "@react-native-community/netinfo").default;
|
|
1794
2077
|
const status = await NetInfo.fetch();
|
|
1795
|
-
if (_optionalChain([status, 'optionalAccess',
|
|
2078
|
+
if (_optionalChain([status, 'optionalAccess', _108 => _108.type])) {
|
|
1796
2079
|
metrics.netType = status.type === "wifi" ? "WiFi" : status.type === "cellular" ? String(
|
|
1797
|
-
_nullishCoalesce(_optionalChain([status, 'access',
|
|
2080
|
+
_nullishCoalesce(_optionalChain([status, 'access', _109 => _109.details, 'optionalAccess', _110 => _110.cellularGeneration]), () => ( "cellular"))
|
|
1798
2081
|
).toUpperCase() : status.type;
|
|
1799
2082
|
}
|
|
1800
|
-
|
|
2083
|
+
const details = _optionalChain([status, 'optionalAccess', _111 => _111.details]);
|
|
2084
|
+
if (typeof _optionalChain([details, 'optionalAccess', _112 => _112.ipAddress]) === "string" && details.ipAddress) {
|
|
2085
|
+
metrics.localIp = details.ipAddress;
|
|
2086
|
+
}
|
|
2087
|
+
if (typeof _optionalChain([details, 'optionalAccess', _113 => _113.strength]) === "number" && Number.isFinite(details.strength)) {
|
|
2088
|
+
metrics.signalPct = Math.round(details.strength);
|
|
2089
|
+
}
|
|
2090
|
+
if (typeof _optionalChain([details, 'optionalAccess', _114 => _114.linkSpeed]) === "number" && Number.isFinite(details.linkSpeed)) {
|
|
2091
|
+
metrics.linkMbps = Math.round(details.linkSpeed);
|
|
2092
|
+
}
|
|
2093
|
+
if (typeof _optionalChain([details, 'optionalAccess', _115 => _115.frequency]) === "number" && Number.isFinite(details.frequency) && details.frequency > 0) {
|
|
2094
|
+
metrics.freqMhz = Math.round(details.frequency);
|
|
2095
|
+
}
|
|
2096
|
+
} catch (e18) {
|
|
1801
2097
|
}
|
|
1802
2098
|
if (notes.length > 0) console.log("[pickleball] metrics notes:", notes.join(" | "));
|
|
1803
2099
|
return Object.keys(metrics).length > 0 ? metrics : null;
|
|
@@ -1836,6 +2132,24 @@ function useLivestreamHost() {
|
|
|
1836
2132
|
var LivestreamContext = _react2.default.createContext(null);
|
|
1837
2133
|
function PickleballLiveProvider(props) {
|
|
1838
2134
|
const engineRef = _react.useRef.call(void 0, null);
|
|
2135
|
+
const keepSessionRef = _react.useRef.call(void 0, props.keepSessionOnRestart);
|
|
2136
|
+
keepSessionRef.current = props.keepSessionOnRestart;
|
|
2137
|
+
const resolvedKeepRef = _react.useRef.call(void 0, typeof props.keepSessionOnRestart === "function");
|
|
2138
|
+
_react.useEffect.call(void 0, () => {
|
|
2139
|
+
const value2 = props.keepSessionOnRestart;
|
|
2140
|
+
if (typeof value2 !== "function") {
|
|
2141
|
+
resolvedKeepRef.current = value2 === true;
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
resolvedKeepRef.current = true;
|
|
2145
|
+
let cancelled = false;
|
|
2146
|
+
void Promise.resolve().then(() => value2()).then((keep) => {
|
|
2147
|
+
if (!cancelled) resolvedKeepRef.current = keep === true;
|
|
2148
|
+
}).catch(() => void 0);
|
|
2149
|
+
return () => {
|
|
2150
|
+
cancelled = true;
|
|
2151
|
+
};
|
|
2152
|
+
}, [props.keepSessionOnRestart]);
|
|
1839
2153
|
const hostRef = _react.useRef.call(void 0, null);
|
|
1840
2154
|
const effectGenerationRef = _react.useRef.call(void 0, 0);
|
|
1841
2155
|
if (!engineRef.current) {
|
|
@@ -1847,8 +2161,17 @@ function PickleballLiveProvider(props) {
|
|
|
1847
2161
|
storage: _nullishCoalesce(props.storage, () => ( createDefaultStorage())),
|
|
1848
2162
|
network: _nullishCoalesce(props.network, () => ( createDefaultNetwork())),
|
|
1849
2163
|
telemetry: _nullishCoalesce(props.telemetry, () => ( createDefaultTelemetry())),
|
|
2164
|
+
commandChannel: _nullishCoalesce(props.commandChannel, () => ( createDefaultCommandChannel())),
|
|
2165
|
+
...props.commandSubscription ? { commandSubscription: props.commandSubscription } : {},
|
|
1850
2166
|
deviceMetrics,
|
|
1851
|
-
roomFactory: _nullishCoalesce(props.roomFactory, () => ( createDefaultRoomFactory()))
|
|
2167
|
+
roomFactory: _nullishCoalesce(props.roomFactory, () => ( createDefaultRoomFactory())),
|
|
2168
|
+
// Đọc qua ref để engine luôn thấy giá trị MỚI NHẤT: engine chỉ dựng một lần
|
|
2169
|
+
// (engineRef), còn prop thì đổi theo việc máy được gán/gỡ sân. Prop dạng
|
|
2170
|
+
// hàm được gọi xuyên qua (engine tự await) — đường cho app đọc thẳng đĩa.
|
|
2171
|
+
keepSessionOnRestart: () => {
|
|
2172
|
+
const value2 = keepSessionRef.current;
|
|
2173
|
+
return typeof value2 === "function" ? value2() : value2 === true;
|
|
2174
|
+
}
|
|
1852
2175
|
};
|
|
1853
2176
|
engineRef.current = new LivestreamEngine(dependencies);
|
|
1854
2177
|
hostRef.current = {
|
|
@@ -1875,29 +2198,38 @@ function PickleballLiveProvider(props) {
|
|
|
1875
2198
|
subscription.remove();
|
|
1876
2199
|
queueMicrotask(() => {
|
|
1877
2200
|
if (effectGenerationRef.current === effectGeneration) {
|
|
1878
|
-
void engine.dispose();
|
|
2201
|
+
void engine.dispose({ keepSession: resolvedKeepRef.current });
|
|
1879
2202
|
}
|
|
1880
2203
|
});
|
|
1881
2204
|
};
|
|
1882
2205
|
}, [engine]);
|
|
1883
2206
|
_react.useEffect.call(void 0, () => {
|
|
1884
|
-
if (snapshot.error) _optionalChain([props, 'access',
|
|
2207
|
+
if (snapshot.error) _optionalChain([props, 'access', _116 => _116.onError, 'optionalCall', _117 => _117(snapshot.error)]);
|
|
1885
2208
|
}, [props.onError, snapshot.error]);
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
2209
|
+
const actions = _react.useMemo.call(void 0,
|
|
2210
|
+
() => ({
|
|
2211
|
+
prepare: () => engine.prepare(),
|
|
2212
|
+
start: (input, options) => engine.start(input, options),
|
|
2213
|
+
startPublishing: () => engine.startPublishing(),
|
|
2214
|
+
stopPublishing: () => engine.stopPublishing(),
|
|
2215
|
+
stop: (reason) => engine.stop(reason),
|
|
2216
|
+
retry: () => engine.retry(),
|
|
2217
|
+
switchCamera: () => engine.switchCamera(),
|
|
2218
|
+
setMicrophoneEnabled: (enabled) => engine.setMicrophoneEnabled(enabled),
|
|
2219
|
+
setCaptureOrientation: (orientation) => engine.setCaptureOrientation(orientation),
|
|
2220
|
+
selectCamera: (deviceId) => engine.selectCamera(deviceId)
|
|
2221
|
+
}),
|
|
2222
|
+
[engine]
|
|
2223
|
+
);
|
|
2224
|
+
const locale = _nullishCoalesce(props.locale, () => ( "vi"));
|
|
2225
|
+
const theme = _react.useMemo.call(void 0,
|
|
2226
|
+
() => ({ ...defaultLivestreamTheme, ...props.theme }),
|
|
2227
|
+
[props.theme]
|
|
2228
|
+
);
|
|
2229
|
+
const value = _react.useMemo.call(void 0,
|
|
2230
|
+
() => ({ ...snapshot, locale, theme, ...actions }),
|
|
2231
|
+
[snapshot, locale, theme, actions]
|
|
2232
|
+
);
|
|
1901
2233
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, LivestreamHostContext.Provider, { value: hostRef.current, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, LivestreamContext.Provider, { value, children: props.children }) });
|
|
1902
2234
|
}
|
|
1903
2235
|
function useLivestream() {
|
|
@@ -1950,4 +2282,9 @@ function useLivestream() {
|
|
|
1950
2282
|
|
|
1951
2283
|
|
|
1952
2284
|
|
|
1953
|
-
|
|
2285
|
+
|
|
2286
|
+
|
|
2287
|
+
|
|
2288
|
+
|
|
2289
|
+
|
|
2290
|
+
exports.SDK_TELEMETRY_EVENT_NAMES = SDK_TELEMETRY_EVENT_NAMES; exports.isSdkRemoteConfig = isSdkRemoteConfig; exports.compareSemver = compareSemver; exports.isSdkVersionSupported = isSdkVersionSupported; exports.PickleballExpoError = PickleballExpoError; exports.PermissionDeniedError = PermissionDeniedError; exports.NetworkUnavailableError = NetworkUnavailableError; exports.TokenExpiredError = TokenExpiredError; exports.UnsupportedSdkError = UnsupportedSdkError; exports.UnsupportedRuntimeError = UnsupportedRuntimeError; exports.SessionConflictError = SessionConflictError; exports.RecordingFailureError = RecordingFailureError; exports.ConsentRequiredError = ConsentRequiredError; exports.SdkDisabledError = SdkDisabledError; exports.ExpoSdkConfigurationError = ExpoSdkConfigurationError; exports.InvalidResponseError = InvalidResponseError; exports.ExpoSdkTimeoutError = ExpoSdkTimeoutError; exports.UnknownSdkError = UnknownSdkError; exports.errorFromCode = errorFromCode; exports.normalizeError = normalizeError; exports.isSessionGoneError = isSessionGoneError; exports.PENDING_END_KEY = PENDING_END_KEY; exports.LEGAL_LIVESTREAM_TRANSITIONS = LEGAL_LIVESTREAM_TRANSITIONS; exports.LivestreamEngine = LivestreamEngine; exports.VIDEO_BITRATE_BPS = VIDEO_BITRATE_BPS; exports.AUDIO_BITRATE_BPS = AUDIO_BITRATE_BPS; exports.DEFAULT_UPLOAD_URL = DEFAULT_UPLOAD_URL; exports.requiredMbpsFor = requiredMbpsFor; exports.verdictFor = verdictFor; exports.runUplinkCheck = runUplinkCheck; exports.RtmpRoomAdapter = RtmpRoomAdapter; exports.createRtmpRuntime = createRtmpRuntime; exports.createDefaultRuntime = createDefaultRuntime; exports.createDefaultStorage = createDefaultStorage; exports.createDefaultNetwork = createDefaultNetwork; exports.createDefaultTelemetry = createDefaultTelemetry; exports.createDefaultRoomFactory = createDefaultRoomFactory; exports.createNativeRtmpBindings = createNativeRtmpBindings; exports.createRtmpRoomFactory = createRtmpRoomFactory; exports.createDefaultDeviceMetrics = createDefaultDeviceMetrics; exports.defaultLivestreamTheme = defaultLivestreamTheme; exports.useLivestreamHost = useLivestreamHost; exports.PickleballLiveProvider = PickleballLiveProvider; exports.useLivestream = useLivestream;
|