@livo-tv/sdk 1.2.3 → 1.3.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.
@@ -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,
@@ -2703,11 +2704,15 @@ function StudioSidePanel({
2703
2704
  onGrantStage,
2704
2705
  onDenyStage,
2705
2706
  onTakeOffAir,
2707
+ onKick,
2706
2708
  guestUrl,
2707
2709
  onShareGuestLink
2708
2710
  }) {
2709
2711
  const panelists = participants.filter((row) => isOnStage(row.stageStatus));
2710
2712
  const audience = participants.filter((row) => !isOnStage(row.stageStatus));
2713
+ const [pendingKick, setPendingKick] = react.useState(
2714
+ null
2715
+ );
2711
2716
  return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "bg-background flex min-h-0 w-full flex-1 flex-col border-t md:w-80 md:flex-none md:border-l md:border-t-0", children: [
2712
2717
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
2713
2718
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2824,23 +2829,37 @@ function StudioSidePanel({
2824
2829
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: labels.panelists }),
2825
2830
  panelists.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-sm", children: labels.noPanelists }) : /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-2", children: panelists.map((row) => {
2826
2831
  const userId = row.userId;
2832
+ const showKick = isModerator2 && !row.isSelf && Boolean(row.id);
2827
2833
  return /* @__PURE__ */ jsxRuntime.jsxs(
2828
2834
  "li",
2829
2835
  {
2830
2836
  className: "space-y-2 rounded-md border p-2",
2831
2837
  children: [
2832
2838
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-sm", children: row.name }),
2833
- !row.isSelf && userId ? /* @__PURE__ */ jsxRuntime.jsx(
2834
- Button,
2835
- {
2836
- type: "button",
2837
- size: "xs",
2838
- variant: "outline",
2839
- className: "w-full",
2840
- onClick: () => onTakeOffAir(userId),
2841
- children: labels.offAir
2842
- }
2843
- ) : null
2839
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-2", children: [
2840
+ !row.isSelf && userId ? /* @__PURE__ */ jsxRuntime.jsx(
2841
+ Button,
2842
+ {
2843
+ type: "button",
2844
+ size: "xs",
2845
+ variant: "outline",
2846
+ className: "w-full",
2847
+ onClick: () => onTakeOffAir(userId),
2848
+ children: labels.offAir
2849
+ }
2850
+ ) : null,
2851
+ showKick ? /* @__PURE__ */ jsxRuntime.jsx(
2852
+ Button,
2853
+ {
2854
+ type: "button",
2855
+ size: "xs",
2856
+ variant: "ghost",
2857
+ className: "w-full",
2858
+ onClick: () => setPendingKick(row),
2859
+ children: labels.kick
2860
+ }
2861
+ ) : null
2862
+ ] })
2844
2863
  ]
2845
2864
  },
2846
2865
  row.id
@@ -2851,29 +2870,61 @@ function StudioSidePanel({
2851
2870
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: labels.audience }),
2852
2871
  audience.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-sm", children: labels.noAudience }) : /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-2", children: audience.map((row) => {
2853
2872
  const userId = row.userId;
2873
+ const showKick = isModerator2 && !row.isSelf && Boolean(row.id);
2854
2874
  return /* @__PURE__ */ jsxRuntime.jsxs(
2855
2875
  "li",
2856
2876
  {
2857
2877
  className: "space-y-2 rounded-md border p-2",
2858
2878
  children: [
2859
2879
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-sm", children: row.name }),
2860
- !row.isSelf && userId ? /* @__PURE__ */ jsxRuntime.jsx(
2861
- Button,
2862
- {
2863
- type: "button",
2864
- size: "xs",
2865
- variant: "outline",
2866
- className: "w-full",
2867
- onClick: () => onGrantStage(userId),
2868
- children: labels.bringOnAir
2869
- }
2870
- ) : null
2880
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-2", children: [
2881
+ !row.isSelf && userId ? /* @__PURE__ */ jsxRuntime.jsx(
2882
+ Button,
2883
+ {
2884
+ type: "button",
2885
+ size: "xs",
2886
+ variant: "outline",
2887
+ className: "w-full",
2888
+ onClick: () => onGrantStage(userId),
2889
+ children: labels.bringOnAir
2890
+ }
2891
+ ) : null,
2892
+ showKick ? /* @__PURE__ */ jsxRuntime.jsx(
2893
+ Button,
2894
+ {
2895
+ type: "button",
2896
+ size: "xs",
2897
+ variant: "ghost",
2898
+ className: "w-full",
2899
+ onClick: () => setPendingKick(row),
2900
+ children: labels.kick
2901
+ }
2902
+ ) : null
2903
+ ] })
2871
2904
  ]
2872
2905
  },
2873
2906
  row.id
2874
2907
  );
2875
2908
  }) })
2876
- ] })
2909
+ ] }),
2910
+ /* @__PURE__ */ jsxRuntime.jsx(
2911
+ ConfirmAction,
2912
+ {
2913
+ open: pendingKick !== null,
2914
+ onOpenChange: (open) => {
2915
+ if (!open) setPendingKick(null);
2916
+ },
2917
+ triggerLabel: labels.kick,
2918
+ title: pendingKick ? labels.kickTitleNamed.replace("{name}", pendingKick.name) : labels.kickTitle,
2919
+ description: labels.kickDescription,
2920
+ confirmLabel: labels.kickConfirm,
2921
+ onConfirm: () => {
2922
+ const target = pendingKick;
2923
+ setPendingKick(null);
2924
+ if (target) onKick(target.id);
2925
+ }
2926
+ }
2927
+ )
2877
2928
  ] })
2878
2929
  ] });
2879
2930
  }
@@ -3514,6 +3565,7 @@ function StudioToolbar({
3514
3565
  ] }) });
3515
3566
  }
3516
3567
  var STAGE_GRANT_VERIFY_MS = 4e3;
3568
+ var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
3517
3569
  function StudioRoom({
3518
3570
  session,
3519
3571
  meeting,
@@ -3549,6 +3601,7 @@ function StudioRoom({
3549
3601
  );
3550
3602
  const [egressError, setEgressError] = react.useState(null);
3551
3603
  const [shareGrant, setShareGrant] = react.useState("idle");
3604
+ const [connectTimedOut, setConnectTimedOut] = react.useState(false);
3552
3605
  const [screenPermission, setScreenPermission] = react.useState(
3553
3606
  () => meeting.screenSharePermission()
3554
3607
  );
@@ -3795,6 +3848,14 @@ function StudioRoom({
3795
3848
  [participants]
3796
3849
  );
3797
3850
  const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting, tick]);
3851
+ const connectionState = react.useMemo(
3852
+ () => meeting.connectionState(),
3853
+ [meeting, tick]
3854
+ );
3855
+ const lastMeetingError = react.useMemo(
3856
+ () => meeting.lastMeetingError(),
3857
+ [meeting, tick]
3858
+ );
3798
3859
  const canManageStage = meeting.canManageStage();
3799
3860
  const stageEnabled = meeting.stageEnabled();
3800
3861
  const stageRequests = react.useMemo(
@@ -3892,6 +3953,16 @@ function StudioRoom({
3892
3953
  }
3893
3954
  }
3894
3955
  }, [labels.knock, session.role, waitlisted]);
3956
+ react.useEffect(() => {
3957
+ if (session.role !== "guest" || connectionState !== "connecting") {
3958
+ setConnectTimedOut(false);
3959
+ return;
3960
+ }
3961
+ const id = window.setTimeout(() => {
3962
+ setConnectTimedOut(true);
3963
+ }, STUDIO_CONNECT_WATCHDOG_MS);
3964
+ return () => window.clearTimeout(id);
3965
+ }, [connectionState, session.role]);
3895
3966
  react.useEffect(() => {
3896
3967
  if (inWaitingRoom || screenNoticePrimed.current) return;
3897
3968
  if (!durableScreenBlock) return;
@@ -4207,7 +4278,20 @@ function StudioRoom({
4207
4278
  }
4208
4279
  ) });
4209
4280
  }
4210
- if (inWaitingRoom && session.role === "guest") {
4281
+ const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
4282
+ if (guestConnectFailed) {
4283
+ const detail = [meeting.roomState(), lastMeetingError].filter((value) => Boolean(value)).join(" \u2014 ");
4284
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsxRuntime.jsx(
4285
+ EmptyState,
4286
+ {
4287
+ title: labels.connectFailed,
4288
+ description: detail || labels.connectFailed,
4289
+ actionLabel: onRejoin ? labels.retry : void 0,
4290
+ onAction: onRejoin
4291
+ }
4292
+ ) });
4293
+ }
4294
+ if (session.role === "guest" && (connectionState === "waitlisted" || connectionState === "connecting")) {
4211
4295
  return /* @__PURE__ */ jsxRuntime.jsx(
4212
4296
  StudioGuestWaiting,
4213
4297
  {
@@ -4217,6 +4301,7 @@ function StudioRoom({
4217
4301
  muted,
4218
4302
  labels,
4219
4303
  settingsOpen,
4304
+ statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
4220
4305
  onToggleCamera,
4221
4306
  onToggleMute: () => void onToggleMute(),
4222
4307
  onOpenSettings: () => setSettingsOpen(true),
@@ -4401,6 +4486,7 @@ function StudioRoom({
4401
4486
  const tile = participants.find((row) => row.userId === userId);
4402
4487
  if (tile) void onSetStage(tile, false);
4403
4488
  },
4489
+ onKick: (id) => void onKick(id),
4404
4490
  guestUrl: session.guestUrl,
4405
4491
  onShareGuestLink: () => {
4406
4492
  const url = session.guestUrl;
@@ -4742,8 +4828,8 @@ function StudioGuestLobby({
4742
4828
  }
4743
4829
  }
4744
4830
  async function onTestSpeaker() {
4745
- const speakerId = effective.speaker || devices.speaker[0]?.deviceId || "";
4746
- if (!speakerId || testingSpeaker) return;
4831
+ if (testingSpeaker) return;
4832
+ const speakerId = effective.speaker || devices.speaker[0]?.deviceId || void 0;
4747
4833
  setTestingSpeaker(true);
4748
4834
  try {
4749
4835
  await playSpeakerTest(speakerId);
@@ -4811,7 +4897,7 @@ function StudioGuestLobby({
4811
4897
  type: "button",
4812
4898
  size: "sm",
4813
4899
  variant: "outline",
4814
- disabled: !(effective.speaker || devices.speaker[0]?.deviceId) || testingSpeaker,
4900
+ disabled: testingSpeaker,
4815
4901
  onClick: () => void onTestSpeaker(),
4816
4902
  children: deviceLabels.testSpeaker
4817
4903
  }
@@ -5053,6 +5139,38 @@ function isStageDisabledError(error) {
5053
5139
  const message = typeof row.message === "string" ? row.message : "";
5054
5140
  return message.includes("ERR2003") || message.toLowerCase().includes("stage is disabled");
5055
5141
  }
5142
+ function isStageBusyError(error) {
5143
+ if (!error || typeof error !== "object") return false;
5144
+ const row = error;
5145
+ if (row.name === "UnsupportedConcurrentMethodExecution") return true;
5146
+ if (row.code === "2006" || row.code === 2006) return true;
5147
+ const message = typeof row.message === "string" ? row.message : "";
5148
+ return message.includes("ERR2006") || message.includes("Unsupported concurrent calls") || message.includes("Action not permitted in current stage status");
5149
+ }
5150
+ function isRtkMeetingError(error) {
5151
+ if (!error || typeof error !== "object") return false;
5152
+ const row = error;
5153
+ const name = typeof row.name === "string" ? row.name : "";
5154
+ const message = typeof row.message === "string" ? row.message : "";
5155
+ return name === "ClientError" || name === "TransportConnectionError" || message.includes("ERR1608") || message.includes("No speaker found") || message.includes("Socket is not connected");
5156
+ }
5157
+ function meetingErrorText(error) {
5158
+ if (error instanceof Error && error.message.trim()) return error.message;
5159
+ if (error && typeof error === "object") {
5160
+ const message = error.message;
5161
+ if (typeof message === "string" && message.trim()) return message;
5162
+ }
5163
+ return "Studio failed";
5164
+ }
5165
+ async function settledDeviceList(load) {
5166
+ if (!load) return [];
5167
+ try {
5168
+ const [result] = await Promise.allSettled([load()]);
5169
+ return result?.status === "fulfilled" ? result.value : [];
5170
+ } catch {
5171
+ return [];
5172
+ }
5173
+ }
5056
5174
  function waitMs(ms) {
5057
5175
  return new Promise((resolve) => {
5058
5176
  setTimeout(resolve, ms);
@@ -5090,8 +5208,9 @@ function studioMeetingFromRtk(raw) {
5090
5208
  const pendingPanelists = /* @__PURE__ */ new Set();
5091
5209
  const chatSend = /* @__PURE__ */ new Map();
5092
5210
  const nameOverrides = /* @__PURE__ */ new Map();
5093
- let joiningStage = false;
5211
+ let joinStageInFlight = null;
5094
5212
  let joinedStageLocally = false;
5213
+ let lastMeetingError = null;
5095
5214
  const historyMessages = [];
5096
5215
  let speakerSinkId = null;
5097
5216
  let lastSpeakerId = typeof meeting.participants?.lastActiveSpeaker === "string" ? meeting.participants.lastActiveSpeaker : null;
@@ -5187,47 +5306,75 @@ function studioMeetingFromRtk(raw) {
5187
5306
  return [...byId.values()].sort((a, b) => a.time - b.time);
5188
5307
  }
5189
5308
  function currentStageStatus() {
5190
- return meeting.stage?.status ?? meeting.self?.stageStatus;
5309
+ return meeting.self?.stageStatus ?? meeting.stage?.status;
5310
+ }
5311
+ function resolveConnectionState() {
5312
+ const self = meeting.self;
5313
+ const roomState = self?.roomState ?? null;
5314
+ if (roomState === "waitlisted") return "waitlisted";
5315
+ if (roomState === "joined" || self?.roomJoined === true) return "joined";
5316
+ if (lastMeetingError) return "failed";
5317
+ return "connecting";
5318
+ }
5319
+ function recordMeetingError(error) {
5320
+ if (!isRtkMeetingError(error)) return;
5321
+ lastMeetingError = meetingErrorText(error);
5322
+ notifyParticipants();
5323
+ }
5324
+ function onUnhandledRejection(event) {
5325
+ recordMeetingError(event.reason);
5326
+ }
5327
+ if (typeof window !== "undefined") {
5328
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
5191
5329
  }
5192
5330
  async function republishCameraIfOn() {
5193
5331
  if (meeting.self?.videoEnabled !== true) return;
5194
5332
  await meeting.self.disableVideo?.();
5195
5333
  await meeting.self.enableVideo?.();
5196
5334
  }
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") {
5335
+ async function runJoinStageAttempts() {
5336
+ let lastError;
5337
+ for (let attempt = 0; attempt < STAGE_JOIN_MAX_ATTEMPTS; attempt++) {
5338
+ if (currentStageStatus() === "ON_STAGE") {
5339
+ notifyStage();
5340
+ return;
5341
+ }
5342
+ try {
5343
+ await meeting.stage?.join?.();
5344
+ joinedStageLocally = true;
5345
+ await republishCameraIfOn();
5346
+ notifyStage();
5347
+ return;
5348
+ } catch (error) {
5349
+ lastError = error;
5350
+ if (isStageDisabledError(error)) {
5203
5351
  notifyStage();
5204
5352
  return;
5205
5353
  }
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
- }
5354
+ if (isStageBusyError(error)) {
5222
5355
  await waitMs(STAGE_JOIN_RETRY_MS);
5223
- if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") {
5224
- notifyStage();
5225
- return;
5226
- }
5356
+ continue;
5227
5357
  }
5358
+ console.warn("studio: stage join failed", error);
5359
+ if (attempt === STAGE_JOIN_MAX_ATTEMPTS - 1) break;
5360
+ await waitMs(STAGE_JOIN_RETRY_MS);
5228
5361
  }
5362
+ }
5363
+ notifyStage();
5364
+ if (currentStageStatus() !== "ON_STAGE") {
5365
+ throw lastError instanceof Error ? lastError : new Error("stage join failed");
5366
+ }
5367
+ }
5368
+ async function joinStageWithRetry() {
5369
+ if (joinStageInFlight) {
5370
+ await joinStageInFlight;
5371
+ return;
5372
+ }
5373
+ joinStageInFlight = runJoinStageAttempts();
5374
+ try {
5375
+ await joinStageInFlight;
5229
5376
  } finally {
5230
- joiningStage = false;
5377
+ joinStageInFlight = null;
5231
5378
  }
5232
5379
  }
5233
5380
  async function autoJoinStage() {
@@ -5235,18 +5382,27 @@ function studioMeetingFromRtk(raw) {
5235
5382
  notifyStage();
5236
5383
  return;
5237
5384
  }
5238
- await joinStageWithRetry();
5385
+ try {
5386
+ await joinStageWithRetry();
5387
+ } catch {
5388
+ notifyStage();
5389
+ }
5239
5390
  }
5240
5391
  async function ensureStageJoined() {
5241
5392
  if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") return;
5242
- await joinStageWithRetry();
5393
+ try {
5394
+ await joinStageWithRetry();
5395
+ } catch {
5396
+ notifyStage();
5397
+ }
5243
5398
  }
5244
5399
  async function joinStageIfAllowed() {
5245
5400
  if (meeting.self?.permissions?.stageAccess !== "ALLOWED") return;
5246
5401
  if (currentStageStatus() === "ON_STAGE") return;
5247
5402
  try {
5248
- await meeting.stage?.join?.();
5403
+ await joinStageWithRetry();
5249
5404
  } catch {
5405
+ notifyStage();
5250
5406
  }
5251
5407
  }
5252
5408
  function queuePanelist(guest, fallbackId) {
@@ -5282,6 +5438,8 @@ function studioMeetingFromRtk(raw) {
5282
5438
  listen(meeting.self, "audioUpdate", rebindMedia);
5283
5439
  listen(meeting.self, "pinned", notifyPinned);
5284
5440
  listen(meeting.self, "unpinned", notifyPinned);
5441
+ listen(meeting.self, "waitlisted", notifyParticipants);
5442
+ listen(meeting.self, "roomJoined", notifyParticipants);
5285
5443
  const joined = meeting.participants?.joined;
5286
5444
  listen(joined, "participantJoined", (payload) => {
5287
5445
  void grantIfPending(payload);
@@ -5349,6 +5507,9 @@ function studioMeetingFromRtk(raw) {
5349
5507
  await joinStageIfAllowed();
5350
5508
  },
5351
5509
  async leave() {
5510
+ if (typeof window !== "undefined") {
5511
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
5512
+ }
5352
5513
  await meeting.leave?.();
5353
5514
  },
5354
5515
  async setAudioEnabled(enabled) {
@@ -5458,19 +5619,7 @@ function studioMeetingFromRtk(raw) {
5458
5619
  notifyStage();
5459
5620
  return;
5460
5621
  }
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();
5622
+ await joinStageWithRetry();
5474
5623
  },
5475
5624
  async leaveStage() {
5476
5625
  await meeting.stage?.leave?.();
@@ -5478,7 +5627,7 @@ function studioMeetingFromRtk(raw) {
5478
5627
  notifyStage();
5479
5628
  },
5480
5629
  stageStatus() {
5481
- return meeting.stage?.status ?? meeting.self?.stageStatus ?? "ON_STAGE";
5630
+ return meeting.self?.stageStatus ?? meeting.stage?.status ?? "OFF_STAGE";
5482
5631
  },
5483
5632
  stageRequests() {
5484
5633
  if (meeting.self?.permissions?.acceptStageRequests !== true) {
@@ -5590,9 +5739,13 @@ function studioMeetingFromRtk(raw) {
5590
5739
  }));
5591
5740
  },
5592
5741
  inWaitingRoom() {
5593
- const self = meeting.self;
5594
- if (isStudioRejected(self?.roomState ?? null)) return false;
5595
- return self?.roomState === "waitlisted" || self?.roomJoined === false;
5742
+ return meeting.self?.roomState === "waitlisted";
5743
+ },
5744
+ connectionState() {
5745
+ return resolveConnectionState();
5746
+ },
5747
+ lastMeetingError() {
5748
+ return lastMeetingError;
5596
5749
  },
5597
5750
  moderatorIds() {
5598
5751
  const ids = [];
@@ -5712,9 +5865,9 @@ function studioMeetingFromRtk(raw) {
5712
5865
  async listDevices() {
5713
5866
  const self = meeting.self;
5714
5867
  const [audio, video, speaker] = await Promise.all([
5715
- self?.getAudioDevices?.() ?? [],
5716
- self?.getVideoDevices?.() ?? [],
5717
- self?.getSpeakerDevices?.() ?? []
5868
+ settledDeviceList(self?.getAudioDevices?.bind(self)),
5869
+ settledDeviceList(self?.getVideoDevices?.bind(self)),
5870
+ settledDeviceList(self?.getSpeakerDevices?.bind(self))
5718
5871
  ]);
5719
5872
  return {
5720
5873
  audio: asOptions(audio),
@@ -5727,6 +5880,10 @@ function studioMeetingFromRtk(raw) {
5727
5880
  if (kind === "speaker") {
5728
5881
  speakerSinkId = deviceId;
5729
5882
  for (const el of audioEls.values()) applySink(el, speakerSinkId);
5883
+ const speakers = await settledDeviceList(
5884
+ meeting.self?.getSpeakerDevices?.bind(meeting.self)
5885
+ );
5886
+ if (speakers.length === 0) return;
5730
5887
  }
5731
5888
  await meeting.self?.setDevice?.({
5732
5889
  deviceId,
@@ -6269,6 +6426,9 @@ function studioRoomLabels(t) {
6269
6426
  preview: t("preview"),
6270
6427
  waiting: t("waiting"),
6271
6428
  waitingHost: t("waitingHost"),
6429
+ connecting: t("connecting"),
6430
+ connectFailed: t("connectFailed"),
6431
+ retry: t("retry"),
6272
6432
  admit: t("admit"),
6273
6433
  deny: t("deny"),
6274
6434
  denied: t("denied"),
@@ -6409,6 +6569,7 @@ function studioRoomLabels(t) {
6409
6569
  unpinnedToast: t("unpinnedToast"),
6410
6570
  pinError: t("pinError"),
6411
6571
  kickTitle: t("kickTitle"),
6572
+ kickTitleNamed: t("kickTitleNamed", { name: "{name}" }),
6412
6573
  kickDescription: t("kickDescription"),
6413
6574
  kickConfirm: t("kickConfirm"),
6414
6575
  kickError: t("kickError"),