@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.
@@ -126,6 +126,7 @@ type StudioDevices = {
126
126
  type StudioVideoKind = "camera" | "screen";
127
127
  type StudioHostMediaKind = "audio" | "video" | "screen";
128
128
  type StudioScreenSharePermission = "ALLOWED" | "NOT_ALLOWED" | "CAN_REQUEST";
129
+ type StudioConnectionState = "joined" | "waitlisted" | "connecting" | "failed";
129
130
  type StudioBroadcast = {
130
131
  type: "host-media";
131
132
  kind: StudioHostMediaKind;
@@ -190,6 +191,8 @@ type StudioMeetingHandle = {
190
191
  participants: () => StudioParticipantTile[];
191
192
  waitlisted: () => StudioWaitlistedGuest[];
192
193
  inWaitingRoom: () => boolean;
194
+ connectionState: () => StudioConnectionState;
195
+ lastMeetingError: () => string | null;
193
196
  moderatorIds: () => string[];
194
197
  localAudioTrack: () => MediaStreamTrack | null;
195
198
  localVideoTrack: () => MediaStreamTrack | null;
@@ -219,6 +222,9 @@ declare function studioRoomLabels(t: (key: string, values?: Record<string, strin
219
222
  preview: string;
220
223
  waiting: string;
221
224
  waitingHost: string;
225
+ connecting: string;
226
+ connectFailed: string;
227
+ retry: string;
222
228
  admit: string;
223
229
  deny: string;
224
230
  denied: string;
@@ -126,6 +126,7 @@ type StudioDevices = {
126
126
  type StudioVideoKind = "camera" | "screen";
127
127
  type StudioHostMediaKind = "audio" | "video" | "screen";
128
128
  type StudioScreenSharePermission = "ALLOWED" | "NOT_ALLOWED" | "CAN_REQUEST";
129
+ type StudioConnectionState = "joined" | "waitlisted" | "connecting" | "failed";
129
130
  type StudioBroadcast = {
130
131
  type: "host-media";
131
132
  kind: StudioHostMediaKind;
@@ -190,6 +191,8 @@ type StudioMeetingHandle = {
190
191
  participants: () => StudioParticipantTile[];
191
192
  waitlisted: () => StudioWaitlistedGuest[];
192
193
  inWaitingRoom: () => boolean;
194
+ connectionState: () => StudioConnectionState;
195
+ lastMeetingError: () => string | null;
193
196
  moderatorIds: () => string[];
194
197
  localAudioTrack: () => MediaStreamTrack | null;
195
198
  localVideoTrack: () => MediaStreamTrack | null;
@@ -219,6 +222,9 @@ declare function studioRoomLabels(t: (key: string, values?: Record<string, strin
219
222
  preview: string;
220
223
  waiting: string;
221
224
  waitingHost: string;
225
+ connecting: string;
226
+ connectFailed: string;
227
+ retry: string;
222
228
  admit: string;
223
229
  deny: string;
224
230
  denied: string;
@@ -1875,6 +1875,7 @@ function StudioGuestWaiting({
1875
1875
  muted,
1876
1876
  labels,
1877
1877
  settingsOpen,
1878
+ statusLabel,
1878
1879
  onToggleCamera,
1879
1880
  onToggleMute,
1880
1881
  onOpenSettings,
@@ -1906,7 +1907,7 @@ function StudioGuestWaiting({
1906
1907
  reducedMotion
1907
1908
  }
1908
1909
  ),
1909
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", role: "status", children: labels.waitingHost }),
1910
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-center text-sm", role: "status", children: statusLabel ?? labels.waitingHost }),
1910
1911
  /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs("div", { className: "flex justify-center gap-2", children: [
1911
1912
  /* @__PURE__ */ jsx(
1912
1913
  ControlIcon,
@@ -3490,6 +3491,7 @@ function StudioToolbar({
3490
3491
  ] }) });
3491
3492
  }
3492
3493
  var STAGE_GRANT_VERIFY_MS = 4e3;
3494
+ var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
3493
3495
  function StudioRoom({
3494
3496
  session,
3495
3497
  meeting,
@@ -3525,6 +3527,7 @@ function StudioRoom({
3525
3527
  );
3526
3528
  const [egressError, setEgressError] = useState(null);
3527
3529
  const [shareGrant, setShareGrant] = useState("idle");
3530
+ const [connectTimedOut, setConnectTimedOut] = useState(false);
3528
3531
  const [screenPermission, setScreenPermission] = useState(
3529
3532
  () => meeting.screenSharePermission()
3530
3533
  );
@@ -3771,6 +3774,14 @@ function StudioRoom({
3771
3774
  [participants]
3772
3775
  );
3773
3776
  const waitlisted = useMemo(() => meeting.waitlisted(), [meeting, tick]);
3777
+ const connectionState = useMemo(
3778
+ () => meeting.connectionState(),
3779
+ [meeting, tick]
3780
+ );
3781
+ const lastMeetingError = useMemo(
3782
+ () => meeting.lastMeetingError(),
3783
+ [meeting, tick]
3784
+ );
3774
3785
  const canManageStage = meeting.canManageStage();
3775
3786
  const stageEnabled = meeting.stageEnabled();
3776
3787
  const stageRequests = useMemo(
@@ -3868,6 +3879,16 @@ function StudioRoom({
3868
3879
  }
3869
3880
  }
3870
3881
  }, [labels.knock, session.role, waitlisted]);
3882
+ useEffect(() => {
3883
+ if (session.role !== "guest" || connectionState !== "connecting") {
3884
+ setConnectTimedOut(false);
3885
+ return;
3886
+ }
3887
+ const id = window.setTimeout(() => {
3888
+ setConnectTimedOut(true);
3889
+ }, STUDIO_CONNECT_WATCHDOG_MS);
3890
+ return () => window.clearTimeout(id);
3891
+ }, [connectionState, session.role]);
3871
3892
  useEffect(() => {
3872
3893
  if (inWaitingRoom || screenNoticePrimed.current) return;
3873
3894
  if (!durableScreenBlock) return;
@@ -4183,7 +4204,20 @@ function StudioRoom({
4183
4204
  }
4184
4205
  ) });
4185
4206
  }
4186
- if (inWaitingRoom && session.role === "guest") {
4207
+ const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
4208
+ if (guestConnectFailed) {
4209
+ const detail = [meeting.roomState(), lastMeetingError].filter((value) => Boolean(value)).join(" \u2014 ");
4210
+ return /* @__PURE__ */ jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsx(
4211
+ EmptyState,
4212
+ {
4213
+ title: labels.connectFailed,
4214
+ description: detail || labels.connectFailed,
4215
+ actionLabel: onRejoin ? labels.retry : void 0,
4216
+ onAction: onRejoin
4217
+ }
4218
+ ) });
4219
+ }
4220
+ if (session.role === "guest" && (connectionState === "waitlisted" || connectionState === "connecting")) {
4187
4221
  return /* @__PURE__ */ jsx(
4188
4222
  StudioGuestWaiting,
4189
4223
  {
@@ -4193,6 +4227,7 @@ function StudioRoom({
4193
4227
  muted,
4194
4228
  labels,
4195
4229
  settingsOpen,
4230
+ statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
4196
4231
  onToggleCamera,
4197
4232
  onToggleMute: () => void onToggleMute(),
4198
4233
  onOpenSettings: () => setSettingsOpen(true),
@@ -4718,8 +4753,8 @@ function StudioGuestLobby({
4718
4753
  }
4719
4754
  }
4720
4755
  async function onTestSpeaker() {
4721
- const speakerId = effective.speaker || devices.speaker[0]?.deviceId || "";
4722
- if (!speakerId || testingSpeaker) return;
4756
+ if (testingSpeaker) return;
4757
+ const speakerId = effective.speaker || devices.speaker[0]?.deviceId || void 0;
4723
4758
  setTestingSpeaker(true);
4724
4759
  try {
4725
4760
  await playSpeakerTest(speakerId);
@@ -4787,7 +4822,7 @@ function StudioGuestLobby({
4787
4822
  type: "button",
4788
4823
  size: "sm",
4789
4824
  variant: "outline",
4790
- disabled: !(effective.speaker || devices.speaker[0]?.deviceId) || testingSpeaker,
4825
+ disabled: testingSpeaker,
4791
4826
  onClick: () => void onTestSpeaker(),
4792
4827
  children: deviceLabels.testSpeaker
4793
4828
  }
@@ -5029,6 +5064,38 @@ function isStageDisabledError(error) {
5029
5064
  const message = typeof row.message === "string" ? row.message : "";
5030
5065
  return message.includes("ERR2003") || message.toLowerCase().includes("stage is disabled");
5031
5066
  }
5067
+ function isStageBusyError(error) {
5068
+ if (!error || typeof error !== "object") return false;
5069
+ const row = error;
5070
+ if (row.name === "UnsupportedConcurrentMethodExecution") return true;
5071
+ if (row.code === "2006" || row.code === 2006) return true;
5072
+ const message = typeof row.message === "string" ? row.message : "";
5073
+ return message.includes("ERR2006") || message.includes("Unsupported concurrent calls") || message.includes("Action not permitted in current stage status");
5074
+ }
5075
+ function isRtkMeetingError(error) {
5076
+ if (!error || typeof error !== "object") return false;
5077
+ const row = error;
5078
+ const name = typeof row.name === "string" ? row.name : "";
5079
+ const message = typeof row.message === "string" ? row.message : "";
5080
+ return name === "ClientError" || name === "TransportConnectionError" || message.includes("ERR1608") || message.includes("No speaker found") || message.includes("Socket is not connected");
5081
+ }
5082
+ function meetingErrorText(error) {
5083
+ if (error instanceof Error && error.message.trim()) return error.message;
5084
+ if (error && typeof error === "object") {
5085
+ const message = error.message;
5086
+ if (typeof message === "string" && message.trim()) return message;
5087
+ }
5088
+ return "Studio failed";
5089
+ }
5090
+ async function settledDeviceList(load) {
5091
+ if (!load) return [];
5092
+ try {
5093
+ const [result] = await Promise.allSettled([load()]);
5094
+ return result?.status === "fulfilled" ? result.value : [];
5095
+ } catch {
5096
+ return [];
5097
+ }
5098
+ }
5032
5099
  function waitMs(ms) {
5033
5100
  return new Promise((resolve) => {
5034
5101
  setTimeout(resolve, ms);
@@ -5066,8 +5133,9 @@ function studioMeetingFromRtk(raw) {
5066
5133
  const pendingPanelists = /* @__PURE__ */ new Set();
5067
5134
  const chatSend = /* @__PURE__ */ new Map();
5068
5135
  const nameOverrides = /* @__PURE__ */ new Map();
5069
- let joiningStage = false;
5136
+ let joinStageInFlight = null;
5070
5137
  let joinedStageLocally = false;
5138
+ let lastMeetingError = null;
5071
5139
  const historyMessages = [];
5072
5140
  let speakerSinkId = null;
5073
5141
  let lastSpeakerId = typeof meeting.participants?.lastActiveSpeaker === "string" ? meeting.participants.lastActiveSpeaker : null;
@@ -5163,47 +5231,75 @@ function studioMeetingFromRtk(raw) {
5163
5231
  return [...byId.values()].sort((a, b) => a.time - b.time);
5164
5232
  }
5165
5233
  function currentStageStatus() {
5166
- return meeting.stage?.status ?? meeting.self?.stageStatus;
5234
+ return meeting.self?.stageStatus ?? meeting.stage?.status;
5235
+ }
5236
+ function resolveConnectionState() {
5237
+ const self = meeting.self;
5238
+ const roomState = self?.roomState ?? null;
5239
+ if (roomState === "waitlisted") return "waitlisted";
5240
+ if (roomState === "joined" || self?.roomJoined === true) return "joined";
5241
+ if (lastMeetingError) return "failed";
5242
+ return "connecting";
5243
+ }
5244
+ function recordMeetingError(error) {
5245
+ if (!isRtkMeetingError(error)) return;
5246
+ lastMeetingError = meetingErrorText(error);
5247
+ notifyParticipants();
5248
+ }
5249
+ function onUnhandledRejection(event) {
5250
+ recordMeetingError(event.reason);
5251
+ }
5252
+ if (typeof window !== "undefined") {
5253
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
5167
5254
  }
5168
5255
  async function republishCameraIfOn() {
5169
5256
  if (meeting.self?.videoEnabled !== true) return;
5170
5257
  await meeting.self.disableVideo?.();
5171
5258
  await meeting.self.enableVideo?.();
5172
5259
  }
5173
- async function joinStageWithRetry() {
5174
- if (joiningStage) return;
5175
- joiningStage = true;
5176
- try {
5177
- for (let attempt = 0; attempt < STAGE_JOIN_MAX_ATTEMPTS; attempt++) {
5178
- if (currentStageStatus() === "ON_STAGE") {
5260
+ async function runJoinStageAttempts() {
5261
+ let lastError;
5262
+ for (let attempt = 0; attempt < STAGE_JOIN_MAX_ATTEMPTS; attempt++) {
5263
+ if (currentStageStatus() === "ON_STAGE") {
5264
+ notifyStage();
5265
+ return;
5266
+ }
5267
+ try {
5268
+ await meeting.stage?.join?.();
5269
+ joinedStageLocally = true;
5270
+ await republishCameraIfOn();
5271
+ notifyStage();
5272
+ return;
5273
+ } catch (error) {
5274
+ lastError = error;
5275
+ if (isStageDisabledError(error)) {
5179
5276
  notifyStage();
5180
5277
  return;
5181
5278
  }
5182
- try {
5183
- await meeting.stage?.join?.();
5184
- joinedStageLocally = true;
5185
- await republishCameraIfOn();
5186
- notifyStage();
5187
- return;
5188
- } catch (error) {
5189
- if (isStageDisabledError(error)) {
5190
- notifyStage();
5191
- return;
5192
- }
5193
- console.warn("studio: stage join failed", error);
5194
- if (attempt === STAGE_JOIN_MAX_ATTEMPTS - 1) {
5195
- notifyStage();
5196
- return;
5197
- }
5279
+ if (isStageBusyError(error)) {
5198
5280
  await waitMs(STAGE_JOIN_RETRY_MS);
5199
- if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") {
5200
- notifyStage();
5201
- return;
5202
- }
5281
+ continue;
5203
5282
  }
5283
+ console.warn("studio: stage join failed", error);
5284
+ if (attempt === STAGE_JOIN_MAX_ATTEMPTS - 1) break;
5285
+ await waitMs(STAGE_JOIN_RETRY_MS);
5204
5286
  }
5287
+ }
5288
+ notifyStage();
5289
+ if (currentStageStatus() !== "ON_STAGE") {
5290
+ throw lastError instanceof Error ? lastError : new Error("stage join failed");
5291
+ }
5292
+ }
5293
+ async function joinStageWithRetry() {
5294
+ if (joinStageInFlight) {
5295
+ await joinStageInFlight;
5296
+ return;
5297
+ }
5298
+ joinStageInFlight = runJoinStageAttempts();
5299
+ try {
5300
+ await joinStageInFlight;
5205
5301
  } finally {
5206
- joiningStage = false;
5302
+ joinStageInFlight = null;
5207
5303
  }
5208
5304
  }
5209
5305
  async function autoJoinStage() {
@@ -5211,18 +5307,27 @@ function studioMeetingFromRtk(raw) {
5211
5307
  notifyStage();
5212
5308
  return;
5213
5309
  }
5214
- await joinStageWithRetry();
5310
+ try {
5311
+ await joinStageWithRetry();
5312
+ } catch {
5313
+ notifyStage();
5314
+ }
5215
5315
  }
5216
5316
  async function ensureStageJoined() {
5217
5317
  if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") return;
5218
- await joinStageWithRetry();
5318
+ try {
5319
+ await joinStageWithRetry();
5320
+ } catch {
5321
+ notifyStage();
5322
+ }
5219
5323
  }
5220
5324
  async function joinStageIfAllowed() {
5221
5325
  if (meeting.self?.permissions?.stageAccess !== "ALLOWED") return;
5222
5326
  if (currentStageStatus() === "ON_STAGE") return;
5223
5327
  try {
5224
- await meeting.stage?.join?.();
5328
+ await joinStageWithRetry();
5225
5329
  } catch {
5330
+ notifyStage();
5226
5331
  }
5227
5332
  }
5228
5333
  function queuePanelist(guest, fallbackId) {
@@ -5258,6 +5363,8 @@ function studioMeetingFromRtk(raw) {
5258
5363
  listen(meeting.self, "audioUpdate", rebindMedia);
5259
5364
  listen(meeting.self, "pinned", notifyPinned);
5260
5365
  listen(meeting.self, "unpinned", notifyPinned);
5366
+ listen(meeting.self, "waitlisted", notifyParticipants);
5367
+ listen(meeting.self, "roomJoined", notifyParticipants);
5261
5368
  const joined = meeting.participants?.joined;
5262
5369
  listen(joined, "participantJoined", (payload) => {
5263
5370
  void grantIfPending(payload);
@@ -5325,6 +5432,9 @@ function studioMeetingFromRtk(raw) {
5325
5432
  await joinStageIfAllowed();
5326
5433
  },
5327
5434
  async leave() {
5435
+ if (typeof window !== "undefined") {
5436
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
5437
+ }
5328
5438
  await meeting.leave?.();
5329
5439
  },
5330
5440
  async setAudioEnabled(enabled) {
@@ -5434,19 +5544,7 @@ function studioMeetingFromRtk(raw) {
5434
5544
  notifyStage();
5435
5545
  return;
5436
5546
  }
5437
- const userId = meeting.self?.userId;
5438
- if (userId) {
5439
- await meeting.stage?.grantAccess?.([userId]);
5440
- }
5441
- try {
5442
- await meeting.stage?.join?.();
5443
- } catch (error) {
5444
- notifyStage();
5445
- throw error;
5446
- }
5447
- joinedStageLocally = true;
5448
- await republishCameraIfOn();
5449
- notifyStage();
5547
+ await joinStageWithRetry();
5450
5548
  },
5451
5549
  async leaveStage() {
5452
5550
  await meeting.stage?.leave?.();
@@ -5454,7 +5552,7 @@ function studioMeetingFromRtk(raw) {
5454
5552
  notifyStage();
5455
5553
  },
5456
5554
  stageStatus() {
5457
- return meeting.stage?.status ?? meeting.self?.stageStatus ?? "ON_STAGE";
5555
+ return meeting.self?.stageStatus ?? meeting.stage?.status ?? "OFF_STAGE";
5458
5556
  },
5459
5557
  stageRequests() {
5460
5558
  if (meeting.self?.permissions?.acceptStageRequests !== true) {
@@ -5566,9 +5664,13 @@ function studioMeetingFromRtk(raw) {
5566
5664
  }));
5567
5665
  },
5568
5666
  inWaitingRoom() {
5569
- const self = meeting.self;
5570
- if (isStudioRejected(self?.roomState ?? null)) return false;
5571
- return self?.roomState === "waitlisted" || self?.roomJoined === false;
5667
+ return meeting.self?.roomState === "waitlisted";
5668
+ },
5669
+ connectionState() {
5670
+ return resolveConnectionState();
5671
+ },
5672
+ lastMeetingError() {
5673
+ return lastMeetingError;
5572
5674
  },
5573
5675
  moderatorIds() {
5574
5676
  const ids = [];
@@ -5688,9 +5790,9 @@ function studioMeetingFromRtk(raw) {
5688
5790
  async listDevices() {
5689
5791
  const self = meeting.self;
5690
5792
  const [audio, video, speaker] = await Promise.all([
5691
- self?.getAudioDevices?.() ?? [],
5692
- self?.getVideoDevices?.() ?? [],
5693
- self?.getSpeakerDevices?.() ?? []
5793
+ settledDeviceList(self?.getAudioDevices?.bind(self)),
5794
+ settledDeviceList(self?.getVideoDevices?.bind(self)),
5795
+ settledDeviceList(self?.getSpeakerDevices?.bind(self))
5694
5796
  ]);
5695
5797
  return {
5696
5798
  audio: asOptions(audio),
@@ -5703,6 +5805,10 @@ function studioMeetingFromRtk(raw) {
5703
5805
  if (kind === "speaker") {
5704
5806
  speakerSinkId = deviceId;
5705
5807
  for (const el of audioEls.values()) applySink(el, speakerSinkId);
5808
+ const speakers = await settledDeviceList(
5809
+ meeting.self?.getSpeakerDevices?.bind(meeting.self)
5810
+ );
5811
+ if (speakers.length === 0) return;
5706
5812
  }
5707
5813
  await meeting.self?.setDevice?.({
5708
5814
  deviceId,
@@ -6245,6 +6351,9 @@ function studioRoomLabels(t) {
6245
6351
  preview: t("preview"),
6246
6352
  waiting: t("waiting"),
6247
6353
  waitingHost: t("waitingHost"),
6354
+ connecting: t("connecting"),
6355
+ connectFailed: t("connectFailed"),
6356
+ retry: t("retry"),
6248
6357
  admit: t("admit"),
6249
6358
  deny: t("deny"),
6250
6359
  denied: t("denied"),