@livo-tv/sdk 1.2.2 → 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,
@@ -2275,13 +2276,13 @@ function BubbleGroup({ className, ...props }) {
2275
2276
  "div",
2276
2277
  {
2277
2278
  "data-slot": "bubble-group",
2278
- className: cn("flex min-w-0 flex-col gap-2", className),
2279
+ className: cn("flex min-w-min flex-col gap-2", className),
2279
2280
  ...props
2280
2281
  }
2281
2282
  );
2282
2283
  }
2283
2284
  var bubbleVariants = cva(
2284
- "group/bubble relative flex w-fit min-w-0 max-w-[80%] flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",
2285
+ "group/bubble relative flex w-fit min-w-min max-w-full flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",
2285
2286
  {
2286
2287
  variants: {
2287
2288
  variant: {
@@ -2326,7 +2327,7 @@ function BubbleContent({
2326
2327
  props: mergeProps(
2327
2328
  {
2328
2329
  className: cn(
2329
- "w-auto max-w-full rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed [overflow-wrap:break-word] group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50",
2330
+ "w-auto max-w-full whitespace-pre-wrap rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed [overflow-wrap:break-word] group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50",
2330
2331
  className
2331
2332
  )
2332
2333
  },
@@ -2510,7 +2511,7 @@ function StudioChatPanel({
2510
2511
  const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxs(
2511
2512
  "div",
2512
2513
  {
2513
- className: group.mine ? "ml-auto flex w-fit max-w-full flex-col gap-1" : "flex w-fit max-w-full flex-col gap-1",
2514
+ className: group.mine ? "ml-auto flex max-w-[80%] flex-col gap-1 self-end" : "flex max-w-[80%] flex-col gap-1 self-start",
2514
2515
  children: [
2515
2516
  /* @__PURE__ */ jsx(
2516
2517
  Bubble,
@@ -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
  }
@@ -4869,7 +4904,11 @@ function isRecorder(participant) {
4869
4904
  function resolvedName(participant, nameOverrides) {
4870
4905
  return nameOverrides.get(participant.id) ?? (participant.userId ? nameOverrides.get(participant.userId) : void 0) ?? participant.name?.trim() ?? "Guest";
4871
4906
  }
4872
- function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides) {
4907
+ function resolveTileStageStatus(participant, stageEnabled, fallback) {
4908
+ if (!stageEnabled) return "ON_STAGE";
4909
+ return participant.stageStatus ?? fallback ?? "OFF_STAGE";
4910
+ }
4911
+ function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides, stageStatus) {
4873
4912
  return {
4874
4913
  id: participant.id,
4875
4914
  userId: participant.userId ?? "",
@@ -4879,7 +4918,7 @@ function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides) {
4879
4918
  videoEnabled: participant.videoEnabled === true,
4880
4919
  screenShareEnabled: participant.screenShareEnabled === true,
4881
4920
  picture: participant.picture ?? null,
4882
- stageStatus: participant.stageStatus ?? "ON_STAGE",
4921
+ stageStatus,
4883
4922
  pinned: participant.isPinned === true || pinnedIds.has(participant.id),
4884
4923
  chatCanSend: chatSend.get(participant.id) ?? true
4885
4924
  };
@@ -5025,6 +5064,38 @@ function isStageDisabledError(error) {
5025
5064
  const message = typeof row.message === "string" ? row.message : "";
5026
5065
  return message.includes("ERR2003") || message.toLowerCase().includes("stage is disabled");
5027
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
+ }
5028
5099
  function waitMs(ms) {
5029
5100
  return new Promise((resolve) => {
5030
5101
  setTimeout(resolve, ms);
@@ -5062,8 +5133,9 @@ function studioMeetingFromRtk(raw) {
5062
5133
  const pendingPanelists = /* @__PURE__ */ new Set();
5063
5134
  const chatSend = /* @__PURE__ */ new Map();
5064
5135
  const nameOverrides = /* @__PURE__ */ new Map();
5065
- let joiningStage = false;
5136
+ let joinStageInFlight = null;
5066
5137
  let joinedStageLocally = false;
5138
+ let lastMeetingError = null;
5067
5139
  const historyMessages = [];
5068
5140
  let speakerSinkId = null;
5069
5141
  let lastSpeakerId = typeof meeting.participants?.lastActiveSpeaker === "string" ? meeting.participants.lastActiveSpeaker : null;
@@ -5159,47 +5231,75 @@ function studioMeetingFromRtk(raw) {
5159
5231
  return [...byId.values()].sort((a, b) => a.time - b.time);
5160
5232
  }
5161
5233
  function currentStageStatus() {
5162
- 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);
5163
5254
  }
5164
5255
  async function republishCameraIfOn() {
5165
5256
  if (meeting.self?.videoEnabled !== true) return;
5166
5257
  await meeting.self.disableVideo?.();
5167
5258
  await meeting.self.enableVideo?.();
5168
5259
  }
5169
- async function joinStageWithRetry() {
5170
- if (joiningStage) return;
5171
- joiningStage = true;
5172
- try {
5173
- for (let attempt = 0; attempt < STAGE_JOIN_MAX_ATTEMPTS; attempt++) {
5174
- 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)) {
5175
5276
  notifyStage();
5176
5277
  return;
5177
5278
  }
5178
- try {
5179
- await meeting.stage?.join?.();
5180
- joinedStageLocally = true;
5181
- await republishCameraIfOn();
5182
- notifyStage();
5183
- return;
5184
- } catch (error) {
5185
- if (isStageDisabledError(error)) {
5186
- notifyStage();
5187
- return;
5188
- }
5189
- console.warn("studio: stage join failed", error);
5190
- if (attempt === STAGE_JOIN_MAX_ATTEMPTS - 1) {
5191
- notifyStage();
5192
- return;
5193
- }
5279
+ if (isStageBusyError(error)) {
5194
5280
  await waitMs(STAGE_JOIN_RETRY_MS);
5195
- if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") {
5196
- notifyStage();
5197
- return;
5198
- }
5281
+ continue;
5199
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);
5200
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;
5201
5301
  } finally {
5202
- joiningStage = false;
5302
+ joinStageInFlight = null;
5203
5303
  }
5204
5304
  }
5205
5305
  async function autoJoinStage() {
@@ -5207,18 +5307,27 @@ function studioMeetingFromRtk(raw) {
5207
5307
  notifyStage();
5208
5308
  return;
5209
5309
  }
5210
- await joinStageWithRetry();
5310
+ try {
5311
+ await joinStageWithRetry();
5312
+ } catch {
5313
+ notifyStage();
5314
+ }
5211
5315
  }
5212
5316
  async function ensureStageJoined() {
5213
5317
  if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") return;
5214
- await joinStageWithRetry();
5318
+ try {
5319
+ await joinStageWithRetry();
5320
+ } catch {
5321
+ notifyStage();
5322
+ }
5215
5323
  }
5216
5324
  async function joinStageIfAllowed() {
5217
5325
  if (meeting.self?.permissions?.stageAccess !== "ALLOWED") return;
5218
5326
  if (currentStageStatus() === "ON_STAGE") return;
5219
5327
  try {
5220
- await meeting.stage?.join?.();
5328
+ await joinStageWithRetry();
5221
5329
  } catch {
5330
+ notifyStage();
5222
5331
  }
5223
5332
  }
5224
5333
  function queuePanelist(guest, fallbackId) {
@@ -5254,6 +5363,8 @@ function studioMeetingFromRtk(raw) {
5254
5363
  listen(meeting.self, "audioUpdate", rebindMedia);
5255
5364
  listen(meeting.self, "pinned", notifyPinned);
5256
5365
  listen(meeting.self, "unpinned", notifyPinned);
5366
+ listen(meeting.self, "waitlisted", notifyParticipants);
5367
+ listen(meeting.self, "roomJoined", notifyParticipants);
5257
5368
  const joined = meeting.participants?.joined;
5258
5369
  listen(joined, "participantJoined", (payload) => {
5259
5370
  void grantIfPending(payload);
@@ -5321,6 +5432,9 @@ function studioMeetingFromRtk(raw) {
5321
5432
  await joinStageIfAllowed();
5322
5433
  },
5323
5434
  async leave() {
5435
+ if (typeof window !== "undefined") {
5436
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
5437
+ }
5324
5438
  await meeting.leave?.();
5325
5439
  },
5326
5440
  async setAudioEnabled(enabled) {
@@ -5421,7 +5535,7 @@ function studioMeetingFromRtk(raw) {
5421
5535
  notifyStage();
5422
5536
  },
5423
5537
  async joinStage() {
5424
- if (currentStageStatus() === "ON_STAGE") {
5538
+ if (meeting.self?.stageStatus === "ON_STAGE") {
5425
5539
  notifyStage();
5426
5540
  return;
5427
5541
  }
@@ -5430,25 +5544,15 @@ function studioMeetingFromRtk(raw) {
5430
5544
  notifyStage();
5431
5545
  return;
5432
5546
  }
5433
- try {
5434
- await meeting.stage?.join?.();
5435
- } catch {
5436
- const userId = meeting.self?.userId;
5437
- if (userId) {
5438
- await meeting.stage?.grantAccess?.([userId]);
5439
- }
5440
- await meeting.stage?.join?.();
5441
- }
5442
- joinedStageLocally = true;
5443
- await republishCameraIfOn();
5444
- notifyStage();
5547
+ await joinStageWithRetry();
5445
5548
  },
5446
5549
  async leaveStage() {
5447
5550
  await meeting.stage?.leave?.();
5551
+ joinedStageLocally = false;
5448
5552
  notifyStage();
5449
5553
  },
5450
5554
  stageStatus() {
5451
- return meeting.stage?.status ?? meeting.self?.stageStatus ?? "ON_STAGE";
5555
+ return meeting.self?.stageStatus ?? meeting.stage?.status ?? "OFF_STAGE";
5452
5556
  },
5453
5557
  stageRequests() {
5454
5558
  if (meeting.self?.permissions?.acceptStageRequests !== true) {
@@ -5526,10 +5630,27 @@ function studioMeetingFromRtk(raw) {
5526
5630
  participants() {
5527
5631
  const self = meeting.self;
5528
5632
  const pinned = pinnedIdSet();
5529
- const joinedRows = asList(meeting.participants?.joined).filter((row) => row.id !== self?.id && !isRecorder(row)).map((row) => tileFrom(row, self?.id, pinned, chatSend, nameOverrides));
5633
+ const stageEnabled = self?.permissions?.stageEnabled !== false;
5634
+ const joinedRows = asList(meeting.participants?.joined).filter((row) => row.id !== self?.id && !isRecorder(row)).map(
5635
+ (row) => tileFrom(
5636
+ row,
5637
+ self?.id,
5638
+ pinned,
5639
+ chatSend,
5640
+ nameOverrides,
5641
+ resolveTileStageStatus(row, stageEnabled)
5642
+ )
5643
+ );
5530
5644
  if (self?.id && !isRecorder(self)) {
5531
5645
  joinedRows.unshift(
5532
- tileFrom(self, self.id, pinned, chatSend, nameOverrides)
5646
+ tileFrom(
5647
+ self,
5648
+ self.id,
5649
+ pinned,
5650
+ chatSend,
5651
+ nameOverrides,
5652
+ resolveTileStageStatus(self, stageEnabled, meeting.stage?.status)
5653
+ )
5533
5654
  );
5534
5655
  }
5535
5656
  return joinedRows;
@@ -5543,9 +5664,13 @@ function studioMeetingFromRtk(raw) {
5543
5664
  }));
5544
5665
  },
5545
5666
  inWaitingRoom() {
5546
- const self = meeting.self;
5547
- if (isStudioRejected(self?.roomState ?? null)) return false;
5548
- 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;
5549
5674
  },
5550
5675
  moderatorIds() {
5551
5676
  const ids = [];
@@ -5665,9 +5790,9 @@ function studioMeetingFromRtk(raw) {
5665
5790
  async listDevices() {
5666
5791
  const self = meeting.self;
5667
5792
  const [audio, video, speaker] = await Promise.all([
5668
- self?.getAudioDevices?.() ?? [],
5669
- self?.getVideoDevices?.() ?? [],
5670
- self?.getSpeakerDevices?.() ?? []
5793
+ settledDeviceList(self?.getAudioDevices?.bind(self)),
5794
+ settledDeviceList(self?.getVideoDevices?.bind(self)),
5795
+ settledDeviceList(self?.getSpeakerDevices?.bind(self))
5671
5796
  ]);
5672
5797
  return {
5673
5798
  audio: asOptions(audio),
@@ -5680,6 +5805,10 @@ function studioMeetingFromRtk(raw) {
5680
5805
  if (kind === "speaker") {
5681
5806
  speakerSinkId = deviceId;
5682
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;
5683
5812
  }
5684
5813
  await meeting.self?.setDevice?.({
5685
5814
  deviceId,
@@ -6222,6 +6351,9 @@ function studioRoomLabels(t) {
6222
6351
  preview: t("preview"),
6223
6352
  waiting: t("waiting"),
6224
6353
  waitingHost: t("waitingHost"),
6354
+ connecting: t("connecting"),
6355
+ connectFailed: t("connectFailed"),
6356
+ retry: t("retry"),
6225
6357
  admit: t("admit"),
6226
6358
  deny: t("deny"),
6227
6359
  denied: t("denied"),