@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.
@@ -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;
@@ -359,6 +365,7 @@ declare function studioRoomLabels(t: (key: string, values?: Record<string, strin
359
365
  unpinnedToast: string;
360
366
  pinError: string;
361
367
  kickTitle: string;
368
+ kickTitleNamed: string;
362
369
  kickDescription: string;
363
370
  kickConfirm: string;
364
371
  kickError: 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;
@@ -359,6 +365,7 @@ declare function studioRoomLabels(t: (key: string, values?: Record<string, strin
359
365
  unpinnedToast: string;
360
366
  pinError: string;
361
367
  kickTitle: string;
368
+ kickTitleNamed: string;
362
369
  kickDescription: string;
363
370
  kickConfirm: string;
364
371
  kickError: 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,
@@ -2679,11 +2680,15 @@ function StudioSidePanel({
2679
2680
  onGrantStage,
2680
2681
  onDenyStage,
2681
2682
  onTakeOffAir,
2683
+ onKick,
2682
2684
  guestUrl,
2683
2685
  onShareGuestLink
2684
2686
  }) {
2685
2687
  const panelists = participants.filter((row) => isOnStage(row.stageStatus));
2686
2688
  const audience = participants.filter((row) => !isOnStage(row.stageStatus));
2689
+ const [pendingKick, setPendingKick] = useState(
2690
+ null
2691
+ );
2687
2692
  return /* @__PURE__ */ 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: [
2688
2693
  /* @__PURE__ */ jsxs("div", { className: "flex", children: [
2689
2694
  /* @__PURE__ */ jsxs(
@@ -2800,23 +2805,37 @@ function StudioSidePanel({
2800
2805
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: labels.panelists }),
2801
2806
  panelists.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: labels.noPanelists }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: panelists.map((row) => {
2802
2807
  const userId = row.userId;
2808
+ const showKick = isModerator2 && !row.isSelf && Boolean(row.id);
2803
2809
  return /* @__PURE__ */ jsxs(
2804
2810
  "li",
2805
2811
  {
2806
2812
  className: "space-y-2 rounded-md border p-2",
2807
2813
  children: [
2808
2814
  /* @__PURE__ */ jsx("span", { className: "block truncate text-sm", children: row.name }),
2809
- !row.isSelf && userId ? /* @__PURE__ */ jsx(
2810
- Button,
2811
- {
2812
- type: "button",
2813
- size: "xs",
2814
- variant: "outline",
2815
- className: "w-full",
2816
- onClick: () => onTakeOffAir(userId),
2817
- children: labels.offAir
2818
- }
2819
- ) : null
2815
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
2816
+ !row.isSelf && userId ? /* @__PURE__ */ jsx(
2817
+ Button,
2818
+ {
2819
+ type: "button",
2820
+ size: "xs",
2821
+ variant: "outline",
2822
+ className: "w-full",
2823
+ onClick: () => onTakeOffAir(userId),
2824
+ children: labels.offAir
2825
+ }
2826
+ ) : null,
2827
+ showKick ? /* @__PURE__ */ jsx(
2828
+ Button,
2829
+ {
2830
+ type: "button",
2831
+ size: "xs",
2832
+ variant: "ghost",
2833
+ className: "w-full",
2834
+ onClick: () => setPendingKick(row),
2835
+ children: labels.kick
2836
+ }
2837
+ ) : null
2838
+ ] })
2820
2839
  ]
2821
2840
  },
2822
2841
  row.id
@@ -2827,29 +2846,61 @@ function StudioSidePanel({
2827
2846
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: labels.audience }),
2828
2847
  audience.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: labels.noAudience }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: audience.map((row) => {
2829
2848
  const userId = row.userId;
2849
+ const showKick = isModerator2 && !row.isSelf && Boolean(row.id);
2830
2850
  return /* @__PURE__ */ jsxs(
2831
2851
  "li",
2832
2852
  {
2833
2853
  className: "space-y-2 rounded-md border p-2",
2834
2854
  children: [
2835
2855
  /* @__PURE__ */ jsx("span", { className: "block truncate text-sm", children: row.name }),
2836
- !row.isSelf && userId ? /* @__PURE__ */ jsx(
2837
- Button,
2838
- {
2839
- type: "button",
2840
- size: "xs",
2841
- variant: "outline",
2842
- className: "w-full",
2843
- onClick: () => onGrantStage(userId),
2844
- children: labels.bringOnAir
2845
- }
2846
- ) : null
2856
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
2857
+ !row.isSelf && userId ? /* @__PURE__ */ jsx(
2858
+ Button,
2859
+ {
2860
+ type: "button",
2861
+ size: "xs",
2862
+ variant: "outline",
2863
+ className: "w-full",
2864
+ onClick: () => onGrantStage(userId),
2865
+ children: labels.bringOnAir
2866
+ }
2867
+ ) : null,
2868
+ showKick ? /* @__PURE__ */ jsx(
2869
+ Button,
2870
+ {
2871
+ type: "button",
2872
+ size: "xs",
2873
+ variant: "ghost",
2874
+ className: "w-full",
2875
+ onClick: () => setPendingKick(row),
2876
+ children: labels.kick
2877
+ }
2878
+ ) : null
2879
+ ] })
2847
2880
  ]
2848
2881
  },
2849
2882
  row.id
2850
2883
  );
2851
2884
  }) })
2852
- ] })
2885
+ ] }),
2886
+ /* @__PURE__ */ jsx(
2887
+ ConfirmAction,
2888
+ {
2889
+ open: pendingKick !== null,
2890
+ onOpenChange: (open) => {
2891
+ if (!open) setPendingKick(null);
2892
+ },
2893
+ triggerLabel: labels.kick,
2894
+ title: pendingKick ? labels.kickTitleNamed.replace("{name}", pendingKick.name) : labels.kickTitle,
2895
+ description: labels.kickDescription,
2896
+ confirmLabel: labels.kickConfirm,
2897
+ onConfirm: () => {
2898
+ const target = pendingKick;
2899
+ setPendingKick(null);
2900
+ if (target) onKick(target.id);
2901
+ }
2902
+ }
2903
+ )
2853
2904
  ] })
2854
2905
  ] });
2855
2906
  }
@@ -3490,6 +3541,7 @@ function StudioToolbar({
3490
3541
  ] }) });
3491
3542
  }
3492
3543
  var STAGE_GRANT_VERIFY_MS = 4e3;
3544
+ var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
3493
3545
  function StudioRoom({
3494
3546
  session,
3495
3547
  meeting,
@@ -3525,6 +3577,7 @@ function StudioRoom({
3525
3577
  );
3526
3578
  const [egressError, setEgressError] = useState(null);
3527
3579
  const [shareGrant, setShareGrant] = useState("idle");
3580
+ const [connectTimedOut, setConnectTimedOut] = useState(false);
3528
3581
  const [screenPermission, setScreenPermission] = useState(
3529
3582
  () => meeting.screenSharePermission()
3530
3583
  );
@@ -3771,6 +3824,14 @@ function StudioRoom({
3771
3824
  [participants]
3772
3825
  );
3773
3826
  const waitlisted = useMemo(() => meeting.waitlisted(), [meeting, tick]);
3827
+ const connectionState = useMemo(
3828
+ () => meeting.connectionState(),
3829
+ [meeting, tick]
3830
+ );
3831
+ const lastMeetingError = useMemo(
3832
+ () => meeting.lastMeetingError(),
3833
+ [meeting, tick]
3834
+ );
3774
3835
  const canManageStage = meeting.canManageStage();
3775
3836
  const stageEnabled = meeting.stageEnabled();
3776
3837
  const stageRequests = useMemo(
@@ -3868,6 +3929,16 @@ function StudioRoom({
3868
3929
  }
3869
3930
  }
3870
3931
  }, [labels.knock, session.role, waitlisted]);
3932
+ useEffect(() => {
3933
+ if (session.role !== "guest" || connectionState !== "connecting") {
3934
+ setConnectTimedOut(false);
3935
+ return;
3936
+ }
3937
+ const id = window.setTimeout(() => {
3938
+ setConnectTimedOut(true);
3939
+ }, STUDIO_CONNECT_WATCHDOG_MS);
3940
+ return () => window.clearTimeout(id);
3941
+ }, [connectionState, session.role]);
3871
3942
  useEffect(() => {
3872
3943
  if (inWaitingRoom || screenNoticePrimed.current) return;
3873
3944
  if (!durableScreenBlock) return;
@@ -4183,7 +4254,20 @@ function StudioRoom({
4183
4254
  }
4184
4255
  ) });
4185
4256
  }
4186
- if (inWaitingRoom && session.role === "guest") {
4257
+ const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
4258
+ if (guestConnectFailed) {
4259
+ const detail = [meeting.roomState(), lastMeetingError].filter((value) => Boolean(value)).join(" \u2014 ");
4260
+ return /* @__PURE__ */ jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsx(
4261
+ EmptyState,
4262
+ {
4263
+ title: labels.connectFailed,
4264
+ description: detail || labels.connectFailed,
4265
+ actionLabel: onRejoin ? labels.retry : void 0,
4266
+ onAction: onRejoin
4267
+ }
4268
+ ) });
4269
+ }
4270
+ if (session.role === "guest" && (connectionState === "waitlisted" || connectionState === "connecting")) {
4187
4271
  return /* @__PURE__ */ jsx(
4188
4272
  StudioGuestWaiting,
4189
4273
  {
@@ -4193,6 +4277,7 @@ function StudioRoom({
4193
4277
  muted,
4194
4278
  labels,
4195
4279
  settingsOpen,
4280
+ statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
4196
4281
  onToggleCamera,
4197
4282
  onToggleMute: () => void onToggleMute(),
4198
4283
  onOpenSettings: () => setSettingsOpen(true),
@@ -4377,6 +4462,7 @@ function StudioRoom({
4377
4462
  const tile = participants.find((row) => row.userId === userId);
4378
4463
  if (tile) void onSetStage(tile, false);
4379
4464
  },
4465
+ onKick: (id) => void onKick(id),
4380
4466
  guestUrl: session.guestUrl,
4381
4467
  onShareGuestLink: () => {
4382
4468
  const url = session.guestUrl;
@@ -4718,8 +4804,8 @@ function StudioGuestLobby({
4718
4804
  }
4719
4805
  }
4720
4806
  async function onTestSpeaker() {
4721
- const speakerId = effective.speaker || devices.speaker[0]?.deviceId || "";
4722
- if (!speakerId || testingSpeaker) return;
4807
+ if (testingSpeaker) return;
4808
+ const speakerId = effective.speaker || devices.speaker[0]?.deviceId || void 0;
4723
4809
  setTestingSpeaker(true);
4724
4810
  try {
4725
4811
  await playSpeakerTest(speakerId);
@@ -4787,7 +4873,7 @@ function StudioGuestLobby({
4787
4873
  type: "button",
4788
4874
  size: "sm",
4789
4875
  variant: "outline",
4790
- disabled: !(effective.speaker || devices.speaker[0]?.deviceId) || testingSpeaker,
4876
+ disabled: testingSpeaker,
4791
4877
  onClick: () => void onTestSpeaker(),
4792
4878
  children: deviceLabels.testSpeaker
4793
4879
  }
@@ -5029,6 +5115,38 @@ function isStageDisabledError(error) {
5029
5115
  const message = typeof row.message === "string" ? row.message : "";
5030
5116
  return message.includes("ERR2003") || message.toLowerCase().includes("stage is disabled");
5031
5117
  }
5118
+ function isStageBusyError(error) {
5119
+ if (!error || typeof error !== "object") return false;
5120
+ const row = error;
5121
+ if (row.name === "UnsupportedConcurrentMethodExecution") return true;
5122
+ if (row.code === "2006" || row.code === 2006) return true;
5123
+ const message = typeof row.message === "string" ? row.message : "";
5124
+ return message.includes("ERR2006") || message.includes("Unsupported concurrent calls") || message.includes("Action not permitted in current stage status");
5125
+ }
5126
+ function isRtkMeetingError(error) {
5127
+ if (!error || typeof error !== "object") return false;
5128
+ const row = error;
5129
+ const name = typeof row.name === "string" ? row.name : "";
5130
+ const message = typeof row.message === "string" ? row.message : "";
5131
+ return name === "ClientError" || name === "TransportConnectionError" || message.includes("ERR1608") || message.includes("No speaker found") || message.includes("Socket is not connected");
5132
+ }
5133
+ function meetingErrorText(error) {
5134
+ if (error instanceof Error && error.message.trim()) return error.message;
5135
+ if (error && typeof error === "object") {
5136
+ const message = error.message;
5137
+ if (typeof message === "string" && message.trim()) return message;
5138
+ }
5139
+ return "Studio failed";
5140
+ }
5141
+ async function settledDeviceList(load) {
5142
+ if (!load) return [];
5143
+ try {
5144
+ const [result] = await Promise.allSettled([load()]);
5145
+ return result?.status === "fulfilled" ? result.value : [];
5146
+ } catch {
5147
+ return [];
5148
+ }
5149
+ }
5032
5150
  function waitMs(ms) {
5033
5151
  return new Promise((resolve) => {
5034
5152
  setTimeout(resolve, ms);
@@ -5066,8 +5184,9 @@ function studioMeetingFromRtk(raw) {
5066
5184
  const pendingPanelists = /* @__PURE__ */ new Set();
5067
5185
  const chatSend = /* @__PURE__ */ new Map();
5068
5186
  const nameOverrides = /* @__PURE__ */ new Map();
5069
- let joiningStage = false;
5187
+ let joinStageInFlight = null;
5070
5188
  let joinedStageLocally = false;
5189
+ let lastMeetingError = null;
5071
5190
  const historyMessages = [];
5072
5191
  let speakerSinkId = null;
5073
5192
  let lastSpeakerId = typeof meeting.participants?.lastActiveSpeaker === "string" ? meeting.participants.lastActiveSpeaker : null;
@@ -5163,47 +5282,75 @@ function studioMeetingFromRtk(raw) {
5163
5282
  return [...byId.values()].sort((a, b) => a.time - b.time);
5164
5283
  }
5165
5284
  function currentStageStatus() {
5166
- return meeting.stage?.status ?? meeting.self?.stageStatus;
5285
+ return meeting.self?.stageStatus ?? meeting.stage?.status;
5286
+ }
5287
+ function resolveConnectionState() {
5288
+ const self = meeting.self;
5289
+ const roomState = self?.roomState ?? null;
5290
+ if (roomState === "waitlisted") return "waitlisted";
5291
+ if (roomState === "joined" || self?.roomJoined === true) return "joined";
5292
+ if (lastMeetingError) return "failed";
5293
+ return "connecting";
5294
+ }
5295
+ function recordMeetingError(error) {
5296
+ if (!isRtkMeetingError(error)) return;
5297
+ lastMeetingError = meetingErrorText(error);
5298
+ notifyParticipants();
5299
+ }
5300
+ function onUnhandledRejection(event) {
5301
+ recordMeetingError(event.reason);
5302
+ }
5303
+ if (typeof window !== "undefined") {
5304
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
5167
5305
  }
5168
5306
  async function republishCameraIfOn() {
5169
5307
  if (meeting.self?.videoEnabled !== true) return;
5170
5308
  await meeting.self.disableVideo?.();
5171
5309
  await meeting.self.enableVideo?.();
5172
5310
  }
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") {
5311
+ async function runJoinStageAttempts() {
5312
+ let lastError;
5313
+ for (let attempt = 0; attempt < STAGE_JOIN_MAX_ATTEMPTS; attempt++) {
5314
+ if (currentStageStatus() === "ON_STAGE") {
5315
+ notifyStage();
5316
+ return;
5317
+ }
5318
+ try {
5319
+ await meeting.stage?.join?.();
5320
+ joinedStageLocally = true;
5321
+ await republishCameraIfOn();
5322
+ notifyStage();
5323
+ return;
5324
+ } catch (error) {
5325
+ lastError = error;
5326
+ if (isStageDisabledError(error)) {
5179
5327
  notifyStage();
5180
5328
  return;
5181
5329
  }
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
- }
5330
+ if (isStageBusyError(error)) {
5198
5331
  await waitMs(STAGE_JOIN_RETRY_MS);
5199
- if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") {
5200
- notifyStage();
5201
- return;
5202
- }
5332
+ continue;
5203
5333
  }
5334
+ console.warn("studio: stage join failed", error);
5335
+ if (attempt === STAGE_JOIN_MAX_ATTEMPTS - 1) break;
5336
+ await waitMs(STAGE_JOIN_RETRY_MS);
5204
5337
  }
5338
+ }
5339
+ notifyStage();
5340
+ if (currentStageStatus() !== "ON_STAGE") {
5341
+ throw lastError instanceof Error ? lastError : new Error("stage join failed");
5342
+ }
5343
+ }
5344
+ async function joinStageWithRetry() {
5345
+ if (joinStageInFlight) {
5346
+ await joinStageInFlight;
5347
+ return;
5348
+ }
5349
+ joinStageInFlight = runJoinStageAttempts();
5350
+ try {
5351
+ await joinStageInFlight;
5205
5352
  } finally {
5206
- joiningStage = false;
5353
+ joinStageInFlight = null;
5207
5354
  }
5208
5355
  }
5209
5356
  async function autoJoinStage() {
@@ -5211,18 +5358,27 @@ function studioMeetingFromRtk(raw) {
5211
5358
  notifyStage();
5212
5359
  return;
5213
5360
  }
5214
- await joinStageWithRetry();
5361
+ try {
5362
+ await joinStageWithRetry();
5363
+ } catch {
5364
+ notifyStage();
5365
+ }
5215
5366
  }
5216
5367
  async function ensureStageJoined() {
5217
5368
  if (currentStageStatus() !== "ACCEPTED_TO_JOIN_STAGE") return;
5218
- await joinStageWithRetry();
5369
+ try {
5370
+ await joinStageWithRetry();
5371
+ } catch {
5372
+ notifyStage();
5373
+ }
5219
5374
  }
5220
5375
  async function joinStageIfAllowed() {
5221
5376
  if (meeting.self?.permissions?.stageAccess !== "ALLOWED") return;
5222
5377
  if (currentStageStatus() === "ON_STAGE") return;
5223
5378
  try {
5224
- await meeting.stage?.join?.();
5379
+ await joinStageWithRetry();
5225
5380
  } catch {
5381
+ notifyStage();
5226
5382
  }
5227
5383
  }
5228
5384
  function queuePanelist(guest, fallbackId) {
@@ -5258,6 +5414,8 @@ function studioMeetingFromRtk(raw) {
5258
5414
  listen(meeting.self, "audioUpdate", rebindMedia);
5259
5415
  listen(meeting.self, "pinned", notifyPinned);
5260
5416
  listen(meeting.self, "unpinned", notifyPinned);
5417
+ listen(meeting.self, "waitlisted", notifyParticipants);
5418
+ listen(meeting.self, "roomJoined", notifyParticipants);
5261
5419
  const joined = meeting.participants?.joined;
5262
5420
  listen(joined, "participantJoined", (payload) => {
5263
5421
  void grantIfPending(payload);
@@ -5325,6 +5483,9 @@ function studioMeetingFromRtk(raw) {
5325
5483
  await joinStageIfAllowed();
5326
5484
  },
5327
5485
  async leave() {
5486
+ if (typeof window !== "undefined") {
5487
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
5488
+ }
5328
5489
  await meeting.leave?.();
5329
5490
  },
5330
5491
  async setAudioEnabled(enabled) {
@@ -5434,19 +5595,7 @@ function studioMeetingFromRtk(raw) {
5434
5595
  notifyStage();
5435
5596
  return;
5436
5597
  }
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();
5598
+ await joinStageWithRetry();
5450
5599
  },
5451
5600
  async leaveStage() {
5452
5601
  await meeting.stage?.leave?.();
@@ -5454,7 +5603,7 @@ function studioMeetingFromRtk(raw) {
5454
5603
  notifyStage();
5455
5604
  },
5456
5605
  stageStatus() {
5457
- return meeting.stage?.status ?? meeting.self?.stageStatus ?? "ON_STAGE";
5606
+ return meeting.self?.stageStatus ?? meeting.stage?.status ?? "OFF_STAGE";
5458
5607
  },
5459
5608
  stageRequests() {
5460
5609
  if (meeting.self?.permissions?.acceptStageRequests !== true) {
@@ -5566,9 +5715,13 @@ function studioMeetingFromRtk(raw) {
5566
5715
  }));
5567
5716
  },
5568
5717
  inWaitingRoom() {
5569
- const self = meeting.self;
5570
- if (isStudioRejected(self?.roomState ?? null)) return false;
5571
- return self?.roomState === "waitlisted" || self?.roomJoined === false;
5718
+ return meeting.self?.roomState === "waitlisted";
5719
+ },
5720
+ connectionState() {
5721
+ return resolveConnectionState();
5722
+ },
5723
+ lastMeetingError() {
5724
+ return lastMeetingError;
5572
5725
  },
5573
5726
  moderatorIds() {
5574
5727
  const ids = [];
@@ -5688,9 +5841,9 @@ function studioMeetingFromRtk(raw) {
5688
5841
  async listDevices() {
5689
5842
  const self = meeting.self;
5690
5843
  const [audio, video, speaker] = await Promise.all([
5691
- self?.getAudioDevices?.() ?? [],
5692
- self?.getVideoDevices?.() ?? [],
5693
- self?.getSpeakerDevices?.() ?? []
5844
+ settledDeviceList(self?.getAudioDevices?.bind(self)),
5845
+ settledDeviceList(self?.getVideoDevices?.bind(self)),
5846
+ settledDeviceList(self?.getSpeakerDevices?.bind(self))
5694
5847
  ]);
5695
5848
  return {
5696
5849
  audio: asOptions(audio),
@@ -5703,6 +5856,10 @@ function studioMeetingFromRtk(raw) {
5703
5856
  if (kind === "speaker") {
5704
5857
  speakerSinkId = deviceId;
5705
5858
  for (const el of audioEls.values()) applySink(el, speakerSinkId);
5859
+ const speakers = await settledDeviceList(
5860
+ meeting.self?.getSpeakerDevices?.bind(meeting.self)
5861
+ );
5862
+ if (speakers.length === 0) return;
5706
5863
  }
5707
5864
  await meeting.self?.setDevice?.({
5708
5865
  deviceId,
@@ -6245,6 +6402,9 @@ function studioRoomLabels(t) {
6245
6402
  preview: t("preview"),
6246
6403
  waiting: t("waiting"),
6247
6404
  waitingHost: t("waitingHost"),
6405
+ connecting: t("connecting"),
6406
+ connectFailed: t("connectFailed"),
6407
+ retry: t("retry"),
6248
6408
  admit: t("admit"),
6249
6409
  deny: t("deny"),
6250
6410
  denied: t("denied"),
@@ -6385,6 +6545,7 @@ function studioRoomLabels(t) {
6385
6545
  unpinnedToast: t("unpinnedToast"),
6386
6546
  pinError: t("pinError"),
6387
6547
  kickTitle: t("kickTitle"),
6548
+ kickTitleNamed: t("kickTitleNamed", { name: "{name}" }),
6388
6549
  kickDescription: t("kickDescription"),
6389
6550
  kickConfirm: t("kickConfirm"),
6390
6551
  kickError: t("kickError"),