@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +155 -101
  3. package/dist/advanced-DcbHBCGb.d.ts +300 -0
  4. package/dist/advanced-DeQwg7MK.d.cts +300 -0
  5. package/dist/advanced.cjs +14 -2
  6. package/dist/advanced.d.cts +2 -2
  7. package/dist/advanced.d.ts +2 -2
  8. package/dist/advanced.js +17 -5
  9. package/dist/camera-controls-By8tHigY.d.cts +97 -0
  10. package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
  11. package/dist/chunk-62YV3PCD.cjs +139 -0
  12. package/dist/chunk-6F3HN6US.js +50 -0
  13. package/dist/chunk-6YE5BL3A.cjs +50 -0
  14. package/dist/chunk-7Z7TN5N3.js +318 -0
  15. package/dist/{chunk-TRLASNPQ.cjs → chunk-GXQYMPHE.cjs} +1 -1
  16. package/dist/{chunk-TBPR2QND.cjs → chunk-IUIRVKJI.cjs} +736 -399
  17. package/dist/chunk-TBUEXRFQ.cjs +318 -0
  18. package/dist/{chunk-FODE2W2H.js → chunk-XGP2MN5R.js} +688 -351
  19. package/dist/{chunk-6JOMESNT.js → chunk-XL4CISVQ.js} +1 -1
  20. package/dist/chunk-ZSKSXOCZ.js +139 -0
  21. package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
  22. package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
  23. package/dist/device-agent/index.cjs +973 -44
  24. package/dist/device-agent/index.d.cts +339 -3
  25. package/dist/device-agent/index.d.ts +339 -3
  26. package/dist/device-agent/index.js +940 -11
  27. package/dist/{engine-CCSSqPbZ.d.cts → engine-BSvSdCSE.d.cts} +227 -2
  28. package/dist/{engine-CCSSqPbZ.d.ts → engine-BSvSdCSE.d.ts} +227 -2
  29. package/dist/index.cjs +419 -937
  30. package/dist/index.d.cts +315 -32
  31. package/dist/index.d.ts +315 -32
  32. package/dist/index.js +423 -941
  33. package/dist/plugin.cjs +11 -13
  34. package/dist/plugin.d.cts +15 -1
  35. package/dist/plugin.d.ts +15 -1
  36. package/dist/plugin.js +7 -9
  37. package/dist/realtime/index.cjs +31 -0
  38. package/dist/realtime/index.d.cts +15 -0
  39. package/dist/realtime/index.d.ts +15 -0
  40. package/dist/realtime/index.js +31 -0
  41. package/dist/ui/index.cjs +1297 -0
  42. package/dist/ui/index.d.cts +493 -0
  43. package/dist/ui/index.d.ts +493 -0
  44. package/dist/ui/index.js +1297 -0
  45. package/dist/version.cjs +2 -2
  46. package/dist/version.js +1 -1
  47. package/package.json +31 -15
  48. package/src/advanced.ts +11 -9
  49. package/src/auto-resume.tsx +259 -0
  50. package/src/camera-controls.ts +99 -0
  51. package/src/camera-grant-provider.ts +392 -0
  52. package/src/camera-settings.ts +133 -0
  53. package/src/contracts.ts +73 -0
  54. package/src/device-agent/agent.ts +262 -9
  55. package/src/device-agent/beat-policy.ts +46 -0
  56. package/src/device-agent/component.tsx +11 -0
  57. package/src/device-agent/convex-beat-transport.ts +140 -0
  58. package/src/device-agent/convex-command-subscription.ts +101 -0
  59. package/src/device-agent/convex-control-channel.ts +7 -1
  60. package/src/device-agent/http-transport.ts +22 -1
  61. package/src/device-agent/index.ts +22 -0
  62. package/src/device-agent/mqtt-channel.ts +379 -0
  63. package/src/device-agent/mqtt-codec.ts +347 -0
  64. package/src/device-agent/tournament.ts +352 -0
  65. package/src/engine.ts +580 -15
  66. package/src/http-session-provider.ts +21 -189
  67. package/src/index.ts +84 -5
  68. package/src/local-video-quality.ts +43 -0
  69. package/src/native-runtime.ts +234 -78
  70. package/src/plugin.ts +19 -6
  71. package/src/provider.tsx +94 -18
  72. package/src/realtime/index.ts +56 -0
  73. package/src/resume-policy.ts +85 -0
  74. package/src/resume.ts +132 -0
  75. package/src/rtmp-room-adapter.ts +164 -12
  76. package/src/session-grant.ts +256 -0
  77. package/src/ui/camera-screen.tsx +484 -0
  78. package/src/ui/controls-sheet.tsx +445 -0
  79. package/src/ui/copy.ts +326 -0
  80. package/src/ui/helpers.ts +105 -0
  81. package/src/ui/hooks.ts +59 -0
  82. package/src/ui/index.ts +42 -0
  83. package/src/ui/preview-view.tsx +39 -0
  84. package/src/ui/primitives.tsx +304 -0
  85. package/src/uplink-check.ts +173 -0
  86. package/dist/advanced-BXDrDl1h.d.ts +0 -316
  87. package/dist/advanced-BdAa2Erv.d.cts +0 -316
  88. package/src/livekit-room-adapter.ts +0 -390
  89. 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; var _class3;
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
- __init9() {this.publishing = false}
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
- __init10() {this.pausedElapsedMs = 0}
292
+ __init14() {this.pausedElapsedMs = 0}
273
293
 
274
- __init11() {this.refreshRetryAttempt = 0}
275
- __init12() {this.operationGeneration = 0}
276
- __init13() {this.liveTimersStarted = false}
277
- __init14() {this.keepAwakeActive = false}
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
- __init15() {this.sessionTerminations = /* @__PURE__ */ new Map()}
300
+ __init19() {this.sessionTerminations = /* @__PURE__ */ new Map()}
281
301
 
282
- __init16() {this.disposed = false}
283
- __init17() {this.getSnapshot = () => this.snapshot}
284
- __init18() {this.subscribe = (listener) => {
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 permitted = await this.dependencies.runtime.requestPermissions();
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
- const room = this.ensureRoom();
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
- await this.refreshCredentials(generation);
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', _8 => _8.requireRoom, 'call', _9 => _9(), 'access', _10 => _10.setCaptureOrientation, 'optionalCall', _11 => _11(orientation)]);
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
- const timeout = _nullishCoalesce(_optionalChain([this, 'access', _12 => _12.snapshot, 'access', _13 => _13.config, 'optionalAccess', _14 => _14.reconnectTimeoutMs]), () => ( 3e4));
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
- }, timeout);
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") this.recordError(event.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', _15 => _15.snapshot, 'access', _16 => _16.config, 'optionalAccess', _17 => _17.backgroundGraceMs]), () => ( 15e3));
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
- dispose() {
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', _18 => _18.markerReady]));
734
+ const roomCleanup = this.releaseRoom(_optionalChain([termination, 'optionalAccess', _25 => _25.markerReady]));
666
735
  this.clearTimers();
667
- _optionalChain([this, 'access', _19 => _19.dependencies, 'access', _20 => _20.telemetry, 'optionalAccess', _21 => _21.dispose, 'optionalCall', _22 => _22()]);
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', _23 => _23.markerReady]);
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', _24 => _24.completion]);
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
- async ensurePendingEndResolved() {
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 (e3) {
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', _25 => _25.snapshot, 'access', _26 => _26.config, 'optionalAccess', _27 => _27.telemetryIntervalMs]), () => ( 6e4))
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', _28 => _28.heartbeatTimer, 'access', _29 => _29.unref, 'optionalCall', _30 => _30()]);
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', _31 => _31.elapsedTimer, 'access', _32 => _32.unref, 'optionalCall', _33 => _33()]);
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', _34 => _34.dependencies, 'access', _35 => _35.deviceMetrics, 'optionalCall', _36 => _36()]), async () => ( null));
834
- } catch (e4) {
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', _37 => _37.dependencies, 'access', _38 => _38.telemetry, 'optionalAccess', _39 => _39.send, 'call', _40 => _40(pending.credentials, [
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', _41 => _41.status]) === 401) {
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', _42 => _42.dependencies, 'access', _43 => _43.telemetry, 'optionalAccess', _44 => _44.send, 'call', _45 => _45(this.grant.telemetry, [
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 (e5) {
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', _46 => _46.snapshot, 'access', _47 => _47.config, 'optionalAccess', _48 => _48.standbyTimeoutMs]), () => ( DEFAULT_STANDBY_TIMEOUT_MS));
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', _49 => _49.standbyTimer, 'access', _50 => _50.unref, 'optionalCall', _51 => _51()]);
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 (e6) {
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 delay = _nullishCoalesce(delayOverride, () => ( Math.max(1e3, this.grant.tokenExpiresAt - this.now() - 6e4)));
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', _52 => _52.tokenRefreshTimer, 'access', _53 => _53.unref, 'optionalCall', _54 => _54()]);
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', _55 => _55.maxSessionTimer, 'access', _56 => _56.unref, 'optionalCall', _57 => _57()]);
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', _58 => _58.snapshot, 'access', _59 => _59.config, 'optionalAccess', _60 => _60.telemetryIntervalMs]), () => ( 6e4))
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', _61 => _61.heartbeatTimer, 'access', _62 => _62.unref, 'optionalCall', _63 => _63()]);
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 (e7) {
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 (e8) {
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/livekit-room-adapter.ts
1214
- var LiveKitRoomAdapter = (_class2 = class {
1215
- constructor(bindings) {;_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);
1216
- this.bindings = bindings;
1217
- bindings.registerGlobals();
1218
- this.room = bindings.createRoom();
1219
- this.nativeListeners = [
1220
- [bindings.events.reconnecting, () => this.emit({ type: "reconnecting" })],
1221
- [bindings.events.reconnected, () => this.emit({ type: "reconnected" })],
1222
- [bindings.events.disconnected, (reason) => {
1223
- this.published = false;
1224
- this.emit({ type: "disconnected", reason: String(_nullishCoalesce(reason, () => ( ""))) });
1225
- }],
1226
- [bindings.events.mediaDevicesError, (error) => this.emit({ type: "media-error", error })]
1227
- ];
1228
- if (bindings.events.dataReceived) {
1229
- this.nativeListeners.push([
1230
- bindings.events.dataReceived,
1231
- (payload) => {
1232
- try {
1233
- const text = new TextDecoder().decode(payload);
1234
- const parsed = JSON.parse(text);
1235
- if (_optionalChain([parsed, 'optionalAccess', _64 => _64.type]) === "cmd" && typeof parsed.action === "string") {
1236
- this.emit({ type: "command", action: parsed.action });
1237
- }
1238
- } catch (e9) {
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
- async dispose() {
1416
- if (this.disposed) return;
1417
- this.disposed = true;
1418
- for (const [event, listener] of this.nativeListeners) {
1419
- this.room.off(event, listener);
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 ensurePreparedTracks() {
1435
- this.ensureNotDisposed();
1436
- const tracksReady = _optionalChain([this, 'access', _99 => _99.videoTrack, 'optionalAccess', _100 => _100.mediaStreamTrack, 'optionalAccess', _101 => _101.readyState]) !== "ended" && _optionalChain([this, 'access', _102 => _102.audioTrack, 'optionalAccess', _103 => _103.mediaStreamTrack, 'optionalAccess', _104 => _104.readyState]) !== "ended" && Boolean(this.videoTrack && this.audioTrack);
1437
- if (tracksReady) return;
1438
- if (this.tracks.length) this.releaseTracks(false);
1439
- const resolution = this.captureResolution();
1440
- const tracks = await this.bindings.createLocalTracks({
1441
- audio: true,
1442
- video: {
1443
- ...this.deviceOverride ? { deviceId: this.deviceOverride } : { facingMode: this.isFront ? "user" : "environment" },
1444
- resolution
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
- emit(event) {
1466
- for (const listener of this.listeners) listener(event);
1467
- }
1468
- ensureNotDisposed() {
1469
- if (this.disposed) {
1470
- throw new Error("LiveKit room adapter has been disposed");
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
- }, _class2);
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
- 720: 35e5,
1478
- 1080: 6e6
1479
- };
1480
- var RtmpRoomAdapter = (_class3 = class {
1481
- constructor(bindings) {;_class3.prototype.__init27.call(this);_class3.prototype.__init28.call(this);_class3.prototype.__init29.call(this);
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
- __init27() {this.listeners = /* @__PURE__ */ new Set()}
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
- __init28() {this.quality = 1080}
1487
- __init29() {this.connected = false}
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', _105 => _105.bindings, 'access', _106 => _106.isLandscape, 'optionalCall', _107 => _107()]), () => ( true))
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(_orientation) {
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', _108 => _108.nativeSubscription, 'optionalAccess', _109 => _109.remove, 'call', _110 => _110()]);
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
- }, _class3);
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
- var globalsRegistered = false;
1584
- function createNativeLiveKitBindings() {
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" || !NativeModules.LivekitReactNativeModule) {
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 { mediaDevices } = _chunk7OX2DSKIcjs.__require.call(void 0, "@livekit/react-native-webrtc");
1632
- const stream = await mediaDevices.getUserMedia({ audio: true, video: true });
1633
- for (const track of stream.getTracks()) track.stop();
1634
- return true;
1635
- } catch (e10) {
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
- return _chunk7OX2DSKIcjs.__require.call(void 0, "expo-keep-awake").activateKeepAwakeAsync(
1641
- "@pickleball/expo-sdk"
1642
- );
1643
- },
1644
- deactivateKeepAwake() {
1645
- return _chunk7OX2DSKIcjs.__require.call(void 0, "expo-keep-awake").deactivateKeepAwake(
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 () => new LiveKitRoomAdapter(createNativeLiveKitBindings());
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 (e11) {
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 (e12) {
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', _111 => _111.type])) {
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', _112 => _112.details, 'optionalAccess', _113 => _113.cellularGeneration]), () => ( "cellular"))
2080
+ _nullishCoalesce(_optionalChain([status, 'access', _109 => _109.details, 'optionalAccess', _110 => _110.cellularGeneration]), () => ( "cellular"))
1798
2081
  ).toUpperCase() : status.type;
1799
2082
  }
1800
- } catch (e13) {
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', _114 => _114.onError, 'optionalCall', _115 => _115(snapshot.error)]);
2207
+ if (snapshot.error) _optionalChain([props, 'access', _116 => _116.onError, 'optionalCall', _117 => _117(snapshot.error)]);
1885
2208
  }, [props.onError, snapshot.error]);
1886
- const value = {
1887
- ...snapshot,
1888
- locale: _nullishCoalesce(props.locale, () => ( "vi")),
1889
- theme: { ...defaultLivestreamTheme, ...props.theme },
1890
- prepare: () => engine.prepare(),
1891
- start: (input, options) => engine.start(input, options),
1892
- startPublishing: () => engine.startPublishing(),
1893
- stopPublishing: () => engine.stopPublishing(),
1894
- stop: (reason) => engine.stop(reason),
1895
- retry: () => engine.retry(),
1896
- switchCamera: () => engine.switchCamera(),
1897
- setMicrophoneEnabled: (enabled) => engine.setMicrophoneEnabled(enabled),
1898
- setCaptureOrientation: (orientation) => engine.setCaptureOrientation(orientation),
1899
- selectCamera: (deviceId) => engine.selectCamera(deviceId)
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
- 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.LiveKitRoomAdapter = LiveKitRoomAdapter; exports.RtmpRoomAdapter = RtmpRoomAdapter; exports.createNativeLiveKitBindings = createNativeLiveKitBindings; 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;
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;