@livo-tv/sdk 1.2.3 → 1.2.4

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.
@@ -1899,6 +1899,7 @@ function StudioGuestWaiting({
1899
1899
  muted,
1900
1900
  labels,
1901
1901
  settingsOpen,
1902
+ statusLabel,
1902
1903
  onToggleCamera,
1903
1904
  onToggleMute,
1904
1905
  onOpenSettings,
@@ -1930,7 +1931,7 @@ function StudioGuestWaiting({
1930
1931
  reducedMotion
1931
1932
  }
1932
1933
  ),
1933
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-center text-sm", role: "status", children: labels.waitingHost }),
1934
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-center text-sm", role: "status", children: statusLabel ?? labels.waitingHost }),
1934
1935
  /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-center gap-2", children: [
1935
1936
  /* @__PURE__ */ jsxRuntime.jsx(
1936
1937
  ControlIcon,
@@ -3514,6 +3515,7 @@ function StudioToolbar({
3514
3515
  ] }) });
3515
3516
  }
3516
3517
  var STAGE_GRANT_VERIFY_MS = 4e3;
3518
+ var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
3517
3519
  function StudioRoom({
3518
3520
  session,
3519
3521
  meeting,
@@ -3549,6 +3551,7 @@ function StudioRoom({
3549
3551
  );
3550
3552
  const [egressError, setEgressError] = react.useState(null);
3551
3553
  const [shareGrant, setShareGrant] = react.useState("idle");
3554
+ const [connectTimedOut, setConnectTimedOut] = react.useState(false);
3552
3555
  const [screenPermission, setScreenPermission] = react.useState(
3553
3556
  () => meeting.screenSharePermission()
3554
3557
  );
@@ -3795,6 +3798,14 @@ function StudioRoom({
3795
3798
  [participants]
3796
3799
  );
3797
3800
  const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting, tick]);
3801
+ const connectionState = react.useMemo(
3802
+ () => meeting.connectionState(),
3803
+ [meeting, tick]
3804
+ );
3805
+ const lastMeetingError = react.useMemo(
3806
+ () => meeting.lastMeetingError(),
3807
+ [meeting, tick]
3808
+ );
3798
3809
  const canManageStage = meeting.canManageStage();
3799
3810
  const stageEnabled = meeting.stageEnabled();
3800
3811
  const stageRequests = react.useMemo(
@@ -3892,6 +3903,16 @@ function StudioRoom({
3892
3903
  }
3893
3904
  }
3894
3905
  }, [labels.knock, session.role, waitlisted]);
3906
+ react.useEffect(() => {
3907
+ if (session.role !== "guest" || connectionState !== "connecting") {
3908
+ setConnectTimedOut(false);
3909
+ return;
3910
+ }
3911
+ const id = window.setTimeout(() => {
3912
+ setConnectTimedOut(true);
3913
+ }, STUDIO_CONNECT_WATCHDOG_MS);
3914
+ return () => window.clearTimeout(id);
3915
+ }, [connectionState, session.role]);
3895
3916
  react.useEffect(() => {
3896
3917
  if (inWaitingRoom || screenNoticePrimed.current) return;
3897
3918
  if (!durableScreenBlock) return;
@@ -4207,7 +4228,20 @@ function StudioRoom({
4207
4228
  }
4208
4229
  ) });
4209
4230
  }
4210
- if (inWaitingRoom && session.role === "guest") {
4231
+ const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
4232
+ if (guestConnectFailed) {
4233
+ const detail = [meeting.roomState(), lastMeetingError].filter((value) => Boolean(value)).join(" \u2014 ");
4234
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsxRuntime.jsx(
4235
+ EmptyState,
4236
+ {
4237
+ title: labels.connectFailed,
4238
+ description: detail || labels.connectFailed,
4239
+ actionLabel: onRejoin ? labels.retry : void 0,
4240
+ onAction: onRejoin
4241
+ }
4242
+ ) });
4243
+ }
4244
+ if (session.role === "guest" && (connectionState === "waitlisted" || connectionState === "connecting")) {
4211
4245
  return /* @__PURE__ */ jsxRuntime.jsx(
4212
4246
  StudioGuestWaiting,
4213
4247
  {
@@ -4217,6 +4251,7 @@ function StudioRoom({
4217
4251
  muted,
4218
4252
  labels,
4219
4253
  settingsOpen,
4254
+ statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
4220
4255
  onToggleCamera,
4221
4256
  onToggleMute: () => void onToggleMute(),
4222
4257
  onOpenSettings: () => setSettingsOpen(true),
@@ -4742,8 +4777,8 @@ function StudioGuestLobby({
4742
4777
  }
4743
4778
  }
4744
4779
  async function onTestSpeaker() {
4745
- const speakerId = effective.speaker || devices.speaker[0]?.deviceId || "";
4746
- if (!speakerId || testingSpeaker) return;
4780
+ if (testingSpeaker) return;
4781
+ const speakerId = effective.speaker || devices.speaker[0]?.deviceId || void 0;
4747
4782
  setTestingSpeaker(true);
4748
4783
  try {
4749
4784
  await playSpeakerTest(speakerId);
@@ -4811,7 +4846,7 @@ function StudioGuestLobby({
4811
4846
  type: "button",
4812
4847
  size: "sm",
4813
4848
  variant: "outline",
4814
- disabled: !(effective.speaker || devices.speaker[0]?.deviceId) || testingSpeaker,
4849
+ disabled: testingSpeaker,
4815
4850
  onClick: () => void onTestSpeaker(),
4816
4851
  children: deviceLabels.testSpeaker
4817
4852
  }
@@ -5053,6 +5088,38 @@ function isStageDisabledError(error) {
5053
5088
  const message = typeof row.message === "string" ? row.message : "";
5054
5089
  return message.includes("ERR2003") || message.toLowerCase().includes("stage is disabled");
5055
5090
  }
5091
+ function isStageBusyError(error) {
5092
+ if (!error || typeof error !== "object") return false;
5093
+ const row = error;
5094
+ if (row.name === "UnsupportedConcurrentMethodExecution") return true;
5095
+ if (row.code === "2006" || row.code === 2006) return true;
5096
+ const message = typeof row.message === "string" ? row.message : "";
5097
+ return message.includes("ERR2006") || message.includes("Unsupported concurrent calls") || message.includes("Action not permitted in current stage status");
5098
+ }
5099
+ function isRtkMeetingError(error) {
5100
+ if (!error || typeof error !== "object") return false;
5101
+ const row = error;
5102
+ const name = typeof row.name === "string" ? row.name : "";
5103
+ const message = typeof row.message === "string" ? row.message : "";
5104
+ return name === "ClientError" || name === "TransportConnectionError" || message.includes("ERR1608") || message.includes("No speaker found") || message.includes("Socket is not connected");
5105
+ }
5106
+ function meetingErrorText(error) {
5107
+ if (error instanceof Error && error.message.trim()) return error.message;
5108
+ if (error && typeof error === "object") {
5109
+ const message = error.message;
5110
+ if (typeof message === "string" && message.trim()) return message;
5111
+ }
5112
+ return "Studio failed";
5113
+ }
5114
+ async function settledDeviceList(load) {
5115
+ if (!load) return [];
5116
+ try {
5117
+ const [result] = await Promise.allSettled([load()]);
5118
+ return result?.status === "fulfilled" ? result.value : [];
5119
+ } catch {
5120
+ return [];
5121
+ }
5122
+ }
5056
5123
  function waitMs(ms) {
5057
5124
  return new Promise((resolve) => {
5058
5125
  setTimeout(resolve, ms);
@@ -5090,8 +5157,9 @@ function studioMeetingFromRtk(raw) {
5090
5157
  const pendingPanelists = /* @__PURE__ */ new Set();
5091
5158
  const chatSend = /* @__PURE__ */ new Map();
5092
5159
  const nameOverrides = /* @__PURE__ */ new Map();
5093
- let joiningStage = false;
5160
+ let joinStageInFlight = null;
5094
5161
  let joinedStageLocally = false;
5162
+ let lastMeetingError = null;
5095
5163
  const historyMessages = [];
5096
5164
  let speakerSinkId = null;
5097
5165
  let lastSpeakerId = typeof meeting.participants?.lastActiveSpeaker === "string" ? meeting.participants.lastActiveSpeaker : null;
@@ -5187,47 +5255,75 @@ function studioMeetingFromRtk(raw) {
5187
5255
  return [...byId.values()].sort((a, b) => a.time - b.time);
5188
5256
  }
5189
5257
  function currentStageStatus() {
5190
- return meeting.stage?.status ?? meeting.self?.stageStatus;
5258
+ return meeting.self?.stageStatus ?? meeting.stage?.status;
5259
+ }
5260
+ function resolveConnectionState() {
5261
+ const self = meeting.self;
5262
+ const roomState = self?.roomState ?? null;
5263
+ if (roomState === "waitlisted") return "waitlisted";
5264
+ if (roomState === "joined" || self?.roomJoined === true) return "joined";
5265
+ if (lastMeetingError) return "failed";
5266
+ return "connecting";
5267
+ }
5268
+ function recordMeetingError(error) {
5269
+ if (!isRtkMeetingError(error)) return;
5270
+ lastMeetingError = meetingErrorText(error);
5271
+ notifyParticipants();
5272
+ }
5273
+ function onUnhandledRejection(event) {
5274
+ recordMeetingError(event.reason);
5275
+ }
5276
+ if (typeof window !== "undefined") {
5277
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
5191
5278
  }
5192
5279
  async function republishCameraIfOn() {
5193
5280
  if (meeting.self?.videoEnabled !== true) return;
5194
5281
  await meeting.self.disableVideo?.();
5195
5282
  await meeting.self.enableVideo?.();
5196
5283
  }
5197
- async function joinStageWithRetry() {
5198
- if (joiningStage) return;
5199
- joiningStage = true;
5200
- try {
5201
- for (let attempt = 0; attempt < STAGE_JOIN_MAX_ATTEMPTS; attempt++) {
5202
- if (currentStageStatus() === "ON_STAGE") {
5284
+ async function runJoinStageAttempts() {
5285
+ let lastError;
5286
+ for (let attempt = 0; attempt < STAGE_JOIN_MAX_ATTEMPTS; attempt++) {
5287
+ if (currentStageStatus() === "ON_STAGE") {
5288
+ notifyStage();
5289
+ return;
5290
+ }
5291
+ try {
5292
+ await meeting.stage?.join?.();
5293
+ joinedStageLocally = true;
5294
+ await republishCameraIfOn();
5295
+ notifyStage();
5296
+ return;
5297
+ } catch (error) {
5298
+ lastError = error;
5299
+ if (isStageDisabledError(error)) {
5203
5300
  notifyStage();
5204
5301
  return;
5205
5302
  }
5206
- try {
5207
- await meeting.stage?.join?.();
5208
- joinedStageLocally = true;
5209
- await republishCameraIfOn();
5210
- notifyStage();
5211
- return;
5212
- } catch (error) {
5213
- if (isStageDisabledError(error)) {
5214
- notifyStage();
5215
- return;
5216
- }
5217
- console.warn("studio: stage join failed", error);
5218
- if (attempt === STAGE_JOIN_MAX_ATTEMPTS - 1) {
5219
- notifyStage();
5220
- return;
5221
- }
5303
+ if (isStageBusyError(error)) {
5222
5304
  await waitMs(STAGE_JOIN_RETRY_MS);
5223
- if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") {
5224
- notifyStage();
5225
- return;
5226
- }
5305
+ continue;
5227
5306
  }
5307
+ console.warn("studio: stage join failed", error);
5308
+ if (attempt === STAGE_JOIN_MAX_ATTEMPTS - 1) break;
5309
+ await waitMs(STAGE_JOIN_RETRY_MS);
5228
5310
  }
5311
+ }
5312
+ notifyStage();
5313
+ if (currentStageStatus() !== "ON_STAGE") {
5314
+ throw lastError instanceof Error ? lastError : new Error("stage join failed");
5315
+ }
5316
+ }
5317
+ async function joinStageWithRetry() {
5318
+ if (joinStageInFlight) {
5319
+ await joinStageInFlight;
5320
+ return;
5321
+ }
5322
+ joinStageInFlight = runJoinStageAttempts();
5323
+ try {
5324
+ await joinStageInFlight;
5229
5325
  } finally {
5230
- joiningStage = false;
5326
+ joinStageInFlight = null;
5231
5327
  }
5232
5328
  }
5233
5329
  async function autoJoinStage() {
@@ -5235,18 +5331,27 @@ function studioMeetingFromRtk(raw) {
5235
5331
  notifyStage();
5236
5332
  return;
5237
5333
  }
5238
- await joinStageWithRetry();
5334
+ try {
5335
+ await joinStageWithRetry();
5336
+ } catch {
5337
+ notifyStage();
5338
+ }
5239
5339
  }
5240
5340
  async function ensureStageJoined() {
5241
5341
  if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") return;
5242
- await joinStageWithRetry();
5342
+ try {
5343
+ await joinStageWithRetry();
5344
+ } catch {
5345
+ notifyStage();
5346
+ }
5243
5347
  }
5244
5348
  async function joinStageIfAllowed() {
5245
5349
  if (meeting.self?.permissions?.stageAccess !== "ALLOWED") return;
5246
5350
  if (currentStageStatus() === "ON_STAGE") return;
5247
5351
  try {
5248
- await meeting.stage?.join?.();
5352
+ await joinStageWithRetry();
5249
5353
  } catch {
5354
+ notifyStage();
5250
5355
  }
5251
5356
  }
5252
5357
  function queuePanelist(guest, fallbackId) {
@@ -5282,6 +5387,8 @@ function studioMeetingFromRtk(raw) {
5282
5387
  listen(meeting.self, "audioUpdate", rebindMedia);
5283
5388
  listen(meeting.self, "pinned", notifyPinned);
5284
5389
  listen(meeting.self, "unpinned", notifyPinned);
5390
+ listen(meeting.self, "waitlisted", notifyParticipants);
5391
+ listen(meeting.self, "roomJoined", notifyParticipants);
5285
5392
  const joined = meeting.participants?.joined;
5286
5393
  listen(joined, "participantJoined", (payload) => {
5287
5394
  void grantIfPending(payload);
@@ -5349,6 +5456,9 @@ function studioMeetingFromRtk(raw) {
5349
5456
  await joinStageIfAllowed();
5350
5457
  },
5351
5458
  async leave() {
5459
+ if (typeof window !== "undefined") {
5460
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
5461
+ }
5352
5462
  await meeting.leave?.();
5353
5463
  },
5354
5464
  async setAudioEnabled(enabled) {
@@ -5458,19 +5568,7 @@ function studioMeetingFromRtk(raw) {
5458
5568
  notifyStage();
5459
5569
  return;
5460
5570
  }
5461
- const userId = meeting.self?.userId;
5462
- if (userId) {
5463
- await meeting.stage?.grantAccess?.([userId]);
5464
- }
5465
- try {
5466
- await meeting.stage?.join?.();
5467
- } catch (error) {
5468
- notifyStage();
5469
- throw error;
5470
- }
5471
- joinedStageLocally = true;
5472
- await republishCameraIfOn();
5473
- notifyStage();
5571
+ await joinStageWithRetry();
5474
5572
  },
5475
5573
  async leaveStage() {
5476
5574
  await meeting.stage?.leave?.();
@@ -5478,7 +5576,7 @@ function studioMeetingFromRtk(raw) {
5478
5576
  notifyStage();
5479
5577
  },
5480
5578
  stageStatus() {
5481
- return meeting.stage?.status ?? meeting.self?.stageStatus ?? "ON_STAGE";
5579
+ return meeting.self?.stageStatus ?? meeting.stage?.status ?? "OFF_STAGE";
5482
5580
  },
5483
5581
  stageRequests() {
5484
5582
  if (meeting.self?.permissions?.acceptStageRequests !== true) {
@@ -5590,9 +5688,13 @@ function studioMeetingFromRtk(raw) {
5590
5688
  }));
5591
5689
  },
5592
5690
  inWaitingRoom() {
5593
- const self = meeting.self;
5594
- if (isStudioRejected(self?.roomState ?? null)) return false;
5595
- return self?.roomState === "waitlisted" || self?.roomJoined === false;
5691
+ return meeting.self?.roomState === "waitlisted";
5692
+ },
5693
+ connectionState() {
5694
+ return resolveConnectionState();
5695
+ },
5696
+ lastMeetingError() {
5697
+ return lastMeetingError;
5596
5698
  },
5597
5699
  moderatorIds() {
5598
5700
  const ids = [];
@@ -5712,9 +5814,9 @@ function studioMeetingFromRtk(raw) {
5712
5814
  async listDevices() {
5713
5815
  const self = meeting.self;
5714
5816
  const [audio, video, speaker] = await Promise.all([
5715
- self?.getAudioDevices?.() ?? [],
5716
- self?.getVideoDevices?.() ?? [],
5717
- self?.getSpeakerDevices?.() ?? []
5817
+ settledDeviceList(self?.getAudioDevices?.bind(self)),
5818
+ settledDeviceList(self?.getVideoDevices?.bind(self)),
5819
+ settledDeviceList(self?.getSpeakerDevices?.bind(self))
5718
5820
  ]);
5719
5821
  return {
5720
5822
  audio: asOptions(audio),
@@ -5727,6 +5829,10 @@ function studioMeetingFromRtk(raw) {
5727
5829
  if (kind === "speaker") {
5728
5830
  speakerSinkId = deviceId;
5729
5831
  for (const el of audioEls.values()) applySink(el, speakerSinkId);
5832
+ const speakers = await settledDeviceList(
5833
+ meeting.self?.getSpeakerDevices?.bind(meeting.self)
5834
+ );
5835
+ if (speakers.length === 0) return;
5730
5836
  }
5731
5837
  await meeting.self?.setDevice?.({
5732
5838
  deviceId,
@@ -6269,6 +6375,9 @@ function studioRoomLabels(t) {
6269
6375
  preview: t("preview"),
6270
6376
  waiting: t("waiting"),
6271
6377
  waitingHost: t("waitingHost"),
6378
+ connecting: t("connecting"),
6379
+ connectFailed: t("connectFailed"),
6380
+ retry: t("retry"),
6272
6381
  admit: t("admit"),
6273
6382
  deny: t("deny"),
6274
6383
  denied: t("denied"),