@livo-tv/sdk 1.2.1 → 1.2.3

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.
@@ -764,6 +764,12 @@ function parseStudioJoinStatus(value) {
764
764
  if (value.streamStatus !== null && streamStatus === null) return null;
765
765
  return { ready: value.ready, streamStatus };
766
766
  }
767
+ function parsePublicStreamStatus(value) {
768
+ if (!isRecord(value)) return null;
769
+ const nested = isRecord(value.stream) ? value.stream : value;
770
+ const status = readString(nested, "status");
771
+ return status;
772
+ }
767
773
  function studioErrorCode(error) {
768
774
  if (!error || typeof error !== "object") return null;
769
775
  const body = "body" in error ? error.body : null;
@@ -771,6 +777,90 @@ function studioErrorCode(error) {
771
777
  return body.error;
772
778
  }
773
779
 
780
+ // src/studio/lib/studio-sounds.ts
781
+ var CHAT_DEBOUNCE_MS = 400;
782
+ var CHAT_HZ = 880;
783
+ var CHAT_MS = 80;
784
+ var REQUEST_LOW_HZ = 660;
785
+ var REQUEST_HIGH_HZ = 880;
786
+ var REQUEST_NOTE_MS = 80;
787
+ var GAIN = 0.1;
788
+ var ctx = null;
789
+ var lastChatAt = 0;
790
+ function audioContextCtor() {
791
+ if (typeof window === "undefined") return null;
792
+ return window.AudioContext || window.webkitAudioContext || null;
793
+ }
794
+ function prefersReducedMotion() {
795
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
796
+ return false;
797
+ }
798
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
799
+ }
800
+ function ensureContext() {
801
+ const AudioCtx = audioContextCtor();
802
+ if (!AudioCtx) return null;
803
+ if (!ctx || ctx.state === "closed") {
804
+ ctx = new AudioCtx();
805
+ }
806
+ return ctx;
807
+ }
808
+ function unlockStudioSounds() {
809
+ const context = ensureContext();
810
+ if (!context) return;
811
+ void context.resume();
812
+ }
813
+ function playTone(context, frequency, startAt, durationSec) {
814
+ const oscillator = context.createOscillator();
815
+ const gain = context.createGain();
816
+ oscillator.type = "sine";
817
+ oscillator.frequency.value = frequency;
818
+ gain.gain.setValueAtTime(GAIN, startAt);
819
+ gain.gain.exponentialRampToValueAtTime(1e-3, startAt + durationSec);
820
+ oscillator.connect(gain);
821
+ gain.connect(context.destination);
822
+ oscillator.start(startAt);
823
+ oscillator.stop(startAt + durationSec);
824
+ }
825
+ function playNow(context, kind) {
826
+ const now = context.currentTime;
827
+ if (kind === "chat") {
828
+ playTone(context, CHAT_HZ, now, CHAT_MS / 1e3);
829
+ return;
830
+ }
831
+ playTone(context, REQUEST_LOW_HZ, now, REQUEST_NOTE_MS / 1e3);
832
+ playTone(
833
+ context,
834
+ REQUEST_HIGH_HZ,
835
+ now + REQUEST_NOTE_MS / 1e3,
836
+ REQUEST_NOTE_MS / 1e3
837
+ );
838
+ }
839
+ function playStudioSound(kind) {
840
+ if (prefersReducedMotion()) return;
841
+ if (kind === "chat") {
842
+ const at = Date.now();
843
+ if (at - lastChatAt < CHAT_DEBOUNCE_MS) return;
844
+ lastChatAt = at;
845
+ }
846
+ const context = ensureContext();
847
+ if (!context) return;
848
+ const run = () => {
849
+ if (context.state !== "running") return;
850
+ playNow(context, kind);
851
+ };
852
+ if (context.state === "running") {
853
+ run();
854
+ return;
855
+ }
856
+ void context.resume().then(run, () => void 0);
857
+ }
858
+ if (typeof document !== "undefined") {
859
+ document.addEventListener("pointerdown", unlockStudioSounds, {
860
+ passive: true
861
+ });
862
+ }
863
+
774
864
  // src/studio/lib/bridge.ts
775
865
  function postLivoEvent(type, payload) {
776
866
  if (typeof window === "undefined") return;
@@ -1184,9 +1274,9 @@ function AudioWaveform({
1184
1274
  if (!canvas || !track || reducedMotion) return;
1185
1275
  const AudioCtx = window.AudioContext || window.webkitAudioContext;
1186
1276
  if (!AudioCtx) return;
1187
- const ctx = new AudioCtx();
1188
- const source = ctx.createMediaStreamSource(new MediaStream([track]));
1189
- const analyser = ctx.createAnalyser();
1277
+ const ctx2 = new AudioCtx();
1278
+ const source = ctx2.createMediaStreamSource(new MediaStream([track]));
1279
+ const analyser = ctx2.createAnalyser();
1190
1280
  analyser.fftSize = 64;
1191
1281
  source.connect(analyser);
1192
1282
  const data = new Uint8Array(analyser.frequencyBinCount);
@@ -1215,7 +1305,7 @@ function AudioWaveform({
1215
1305
  frame = requestAnimationFrame(draw);
1216
1306
  return () => {
1217
1307
  cancelAnimationFrame(frame);
1218
- void ctx.close();
1308
+ void ctx2.close();
1219
1309
  };
1220
1310
  }, [track, reducedMotion]);
1221
1311
  if (reducedMotion) {
@@ -1501,10 +1591,10 @@ async function playSpeakerTest(deviceId) {
1501
1591
  if (!AudioCtx) {
1502
1592
  throw new Error("AudioContext unavailable");
1503
1593
  }
1504
- const ctx = new AudioCtx();
1505
- void ctx.resume();
1506
- const oscillator = ctx.createOscillator();
1507
- const gain = ctx.createGain();
1594
+ const ctx2 = new AudioCtx();
1595
+ void ctx2.resume();
1596
+ const oscillator = ctx2.createOscillator();
1597
+ const gain = ctx2.createGain();
1508
1598
  oscillator.type = "sine";
1509
1599
  oscillator.frequency.value = TEST_HZ;
1510
1600
  gain.gain.value = 0.18;
@@ -1514,7 +1604,7 @@ async function playSpeakerTest(deviceId) {
1514
1604
  const probe = document.createElement("audio");
1515
1605
  const canRoute = Boolean(deviceId) && typeof probe.setSinkId === "function";
1516
1606
  if (canRoute && deviceId) {
1517
- const destination = ctx.createMediaStreamDestination();
1607
+ const destination = ctx2.createMediaStreamDestination();
1518
1608
  gain.connect(destination);
1519
1609
  audio = probe;
1520
1610
  audio.autoplay = true;
@@ -1525,13 +1615,13 @@ async function playSpeakerTest(deviceId) {
1525
1615
  await audio.play();
1526
1616
  } catch {
1527
1617
  gain.disconnect();
1528
- gain.connect(ctx.destination);
1618
+ gain.connect(ctx2.destination);
1529
1619
  audio.srcObject = null;
1530
1620
  audio.remove();
1531
1621
  audio = null;
1532
1622
  }
1533
1623
  } else {
1534
- gain.connect(ctx.destination);
1624
+ gain.connect(ctx2.destination);
1535
1625
  }
1536
1626
  oscillator.start();
1537
1627
  await new Promise((resolve) => {
@@ -1543,7 +1633,7 @@ async function playSpeakerTest(deviceId) {
1543
1633
  audio.srcObject = null;
1544
1634
  audio.remove();
1545
1635
  }
1546
- await ctx.close();
1636
+ await ctx2.close();
1547
1637
  }
1548
1638
  }
1549
1639
  function StudioDeviceSelectGroup({
@@ -1778,6 +1868,7 @@ function SettingsCameraPreview({
1778
1868
  el.srcObject = null;
1779
1869
  return;
1780
1870
  }
1871
+ el.setAttribute("webkit-playsinline", "");
1781
1872
  el.srcObject = new MediaStream([track]);
1782
1873
  void el.play().catch(() => void 0);
1783
1874
  return () => {
@@ -1797,7 +1888,7 @@ function SettingsCameraPreview({
1797
1888
  autoPlay: true,
1798
1889
  muted: true,
1799
1890
  playsInline: true,
1800
- className: "absolute inset-0 size-full -scale-x-100 object-cover"
1891
+ className: "livo-studio-inline-video absolute inset-0 size-full -scale-x-100 object-cover"
1801
1892
  }
1802
1893
  ) });
1803
1894
  }
@@ -1896,6 +1987,7 @@ function WaitingCameraPreview({
1896
1987
  el.srcObject = null;
1897
1988
  return;
1898
1989
  }
1990
+ el.setAttribute("webkit-playsinline", "");
1899
1991
  el.srcObject = new MediaStream([track]);
1900
1992
  void el.play().catch(() => void 0);
1901
1993
  return () => {
@@ -1915,7 +2007,7 @@ function WaitingCameraPreview({
1915
2007
  autoPlay: true,
1916
2008
  muted: true,
1917
2009
  playsInline: true,
1918
- className: "absolute inset-0 size-full -scale-x-100 object-cover"
2010
+ className: "livo-studio-inline-video absolute inset-0 size-full -scale-x-100 object-cover"
1919
2011
  }
1920
2012
  ) });
1921
2013
  }
@@ -2102,6 +2194,7 @@ function PipTrackVideo({
2102
2194
  el.srcObject = null;
2103
2195
  return;
2104
2196
  }
2197
+ el.setAttribute("webkit-playsinline", "");
2105
2198
  el.srcObject = new MediaStream([track]);
2106
2199
  void el.play().catch(() => void 0);
2107
2200
  return () => {
@@ -2116,7 +2209,7 @@ function PipTrackVideo({
2116
2209
  playsInline: true,
2117
2210
  muted: true,
2118
2211
  className: cn(
2119
- "absolute inset-0 size-full",
2212
+ "livo-studio-inline-video absolute inset-0 size-full",
2120
2213
  fit === "contain" ? "object-contain" : "object-cover"
2121
2214
  )
2122
2215
  }
@@ -2206,13 +2299,13 @@ function BubbleGroup({ className, ...props }) {
2206
2299
  "div",
2207
2300
  {
2208
2301
  "data-slot": "bubble-group",
2209
- className: cn("flex min-w-0 flex-col gap-2", className),
2302
+ className: cn("flex min-w-min flex-col gap-2", className),
2210
2303
  ...props
2211
2304
  }
2212
2305
  );
2213
2306
  }
2214
2307
  var bubbleVariants = classVarianceAuthority.cva(
2215
- "group/bubble relative flex w-max 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",
2308
+ "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",
2216
2309
  {
2217
2310
  variants: {
2218
2311
  variant: {
@@ -2257,7 +2350,7 @@ function BubbleContent({
2257
2350
  props: mergeProps.mergeProps(
2258
2351
  {
2259
2352
  className: cn(
2260
- "w-max max-w-full overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed break-words 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",
2353
+ "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",
2261
2354
  className
2262
2355
  )
2263
2356
  },
@@ -2438,17 +2531,24 @@ function StudioChatPanel({
2438
2531
  }
2439
2532
  ),
2440
2533
  groups.map((group) => {
2441
- const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
2442
- /* @__PURE__ */ jsxRuntime.jsx(
2443
- Bubble,
2444
- {
2445
- align: group.mine ? "end" : "start",
2446
- variant: group.mine ? "default" : "secondary",
2447
- children: /* @__PURE__ */ jsxRuntime.jsx(BubbleContent, { children: row.text })
2448
- }
2449
- ),
2450
- messageActions(row, group.mine)
2451
- ] }, row.id));
2534
+ const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
2535
+ "div",
2536
+ {
2537
+ 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",
2538
+ children: [
2539
+ /* @__PURE__ */ jsxRuntime.jsx(
2540
+ Bubble,
2541
+ {
2542
+ align: group.mine ? "end" : "start",
2543
+ variant: group.mine ? "default" : "secondary",
2544
+ children: /* @__PURE__ */ jsxRuntime.jsx(BubbleContent, { children: row.text })
2545
+ }
2546
+ ),
2547
+ messageActions(row, group.mine)
2548
+ ]
2549
+ },
2550
+ row.id
2551
+ ));
2452
2552
  if (group.mine) {
2453
2553
  return /* @__PURE__ */ jsxRuntime.jsx(BubbleGroup, { className: "items-end", children: bubbles }, group.rows[0].id);
2454
2554
  }
@@ -3060,7 +3160,9 @@ function StudioTileVideo({
3060
3160
  }) {
3061
3161
  const ref = react.useRef(null);
3062
3162
  react.useEffect(() => {
3063
- meeting.attachVideo(participantId, ref.current, kind);
3163
+ const el = ref.current;
3164
+ el?.setAttribute("webkit-playsinline", "");
3165
+ meeting.attachVideo(participantId, el, kind);
3064
3166
  return () => meeting.attachVideo(participantId, null, kind);
3065
3167
  }, [kind, meeting, participantId]);
3066
3168
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -3073,7 +3175,7 @@ function StudioTileVideo({
3073
3175
  disablePictureInPicture: true,
3074
3176
  controlsList: "nodownload noplaybackrate noremoteplayback",
3075
3177
  className: cn(
3076
- "absolute inset-0 size-full",
3178
+ "livo-studio-tile-video absolute inset-0 size-full",
3077
3179
  "pointer-events-none",
3078
3180
  contain ? "object-contain" : "object-cover",
3079
3181
  mirror && "-scale-x-100"
@@ -3455,6 +3557,12 @@ function StudioRoom({
3455
3557
  const seenWaitlisted = react.useRef(/* @__PURE__ */ new Set());
3456
3558
  const knockPrimed = react.useRef(false);
3457
3559
  const peopleAutoOpened = react.useRef(false);
3560
+ const seenChatIds = react.useRef(/* @__PURE__ */ new Set());
3561
+ const chatSoundPrimed = react.useRef(false);
3562
+ const seenStageRequests = react.useRef(/* @__PURE__ */ new Set());
3563
+ const stageRequestPrimed = react.useRef(false);
3564
+ const awaitingStageDecision = react.useRef(false);
3565
+ const prevStageStatus = react.useRef(null);
3458
3566
  const grantCheckTimers = react.useRef([]);
3459
3567
  const screenNoticePrimed = react.useRef(false);
3460
3568
  const lastSelfMedia = react.useRef({
@@ -3483,6 +3591,9 @@ function StudioRoom({
3483
3591
  const rejected = isStudioRejected(roomState);
3484
3592
  useBeforeUnload(active && !sessionOver);
3485
3593
  useWakeLock(active && !sessionOver);
3594
+ react.useEffect(() => {
3595
+ unlockStudioSounds();
3596
+ }, []);
3486
3597
  react.useEffect(() => {
3487
3598
  if (onAir) {
3488
3599
  setPublishing(false);
@@ -3708,9 +3819,57 @@ function StudioRoom({
3708
3819
  const incoming = chatMessages.filter((row) => row.userId !== selfUserId);
3709
3820
  const unread = sideTab === "chat" ? 0 : Math.max(0, incoming.length - seenIncoming);
3710
3821
  const peopleBadge = waitlisted.length + stageRequests.length;
3822
+ const selfStageStatus = self?.stageStatus ?? meeting.stageStatus();
3711
3823
  react.useEffect(() => {
3712
3824
  if (sideTab === "chat") setSeenIncoming(incoming.length);
3713
3825
  }, [incoming.length, sideTab]);
3826
+ react.useEffect(() => {
3827
+ if (!chatSoundPrimed.current) {
3828
+ for (const row of incoming) seenChatIds.current.add(row.id);
3829
+ chatSoundPrimed.current = true;
3830
+ return;
3831
+ }
3832
+ for (const row of incoming) {
3833
+ if (seenChatIds.current.has(row.id)) continue;
3834
+ seenChatIds.current.add(row.id);
3835
+ playStudioSound("chat");
3836
+ }
3837
+ }, [incoming]);
3838
+ react.useEffect(() => {
3839
+ if (session.role !== "moderator") return;
3840
+ if (!stageRequestPrimed.current) {
3841
+ for (const row of stageRequests) {
3842
+ seenStageRequests.current.add(row.userId);
3843
+ }
3844
+ stageRequestPrimed.current = true;
3845
+ return;
3846
+ }
3847
+ for (const row of stageRequests) {
3848
+ if (seenStageRequests.current.has(row.userId)) continue;
3849
+ seenStageRequests.current.add(row.userId);
3850
+ playStudioSound("request");
3851
+ }
3852
+ }, [session.role, stageRequests]);
3853
+ react.useEffect(() => {
3854
+ const next = selfStageStatus;
3855
+ const prev = prevStageStatus.current;
3856
+ prevStageStatus.current = next;
3857
+ if (session.role !== "guest") return;
3858
+ if (next === "REQUESTED_TO_JOIN_STAGE") {
3859
+ awaitingStageDecision.current = true;
3860
+ return;
3861
+ }
3862
+ if (!awaitingStageDecision.current) return;
3863
+ if (next === "ACCEPTED_TO_JOIN_STAGE" || next === "ON_STAGE") {
3864
+ awaitingStageDecision.current = false;
3865
+ sonner.toast.success(labels.stageApproved);
3866
+ return;
3867
+ }
3868
+ if (next === "OFF_STAGE" && prev === "REQUESTED_TO_JOIN_STAGE") {
3869
+ awaitingStageDecision.current = false;
3870
+ sonner.toast.message(labels.stageDenied);
3871
+ }
3872
+ }, [labels.stageApproved, labels.stageDenied, selfStageStatus, session.role]);
3714
3873
  react.useEffect(() => {
3715
3874
  if (session.role === "moderator" && waitlisted.length > 0 && !peopleAutoOpened.current) {
3716
3875
  peopleAutoOpened.current = true;
@@ -3727,7 +3886,10 @@ function StudioRoom({
3727
3886
  if (seenWaitlisted.current.has(guest.id)) continue;
3728
3887
  seenWaitlisted.current.add(guest.id);
3729
3888
  sonner.toast.message(labels.knock.replace("{name}", guest.name));
3730
- if (session.role === "moderator") setSideTab("people");
3889
+ if (session.role === "moderator") {
3890
+ playStudioSound("request");
3891
+ setSideTab("people");
3892
+ }
3731
3893
  }
3732
3894
  }, [labels.knock, session.role, waitlisted]);
3733
3895
  react.useEffect(() => {
@@ -3764,6 +3926,7 @@ function StudioRoom({
3764
3926
  }
3765
3927
  if (message.type === "share-request") {
3766
3928
  if (session.role !== "moderator") return;
3929
+ playStudioSound("request");
3767
3930
  sonner.toast.message(labels.shareRequestFrom.replace("{name}", message.name), {
3768
3931
  id: `share-request:${message.participantId}`,
3769
3932
  action: {
@@ -4139,10 +4302,13 @@ function StudioRoom({
4139
4302
  size: "icon-sm",
4140
4303
  "aria-label": labels.share,
4141
4304
  onClick: () => {
4142
- void navigator.clipboard.writeText(
4143
- playerWatchUrl(session.stream.id, playerOrigin)
4144
- );
4145
- sonner.toast.success(labels.shareCopied);
4305
+ void shareOrCopyLink({
4306
+ url: playerWatchUrl(session.stream.id, playerOrigin),
4307
+ title: session.stream.title
4308
+ }).then((result) => {
4309
+ if (result === "copied") sonner.toast.success(labels.shareCopied);
4310
+ if (result === "failed") sonner.toast.error(labels.shareFailed);
4311
+ });
4146
4312
  },
4147
4313
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, {})
4148
4314
  }
@@ -4475,6 +4641,7 @@ function StudioDevicePreflight({
4475
4641
  setMicOk(Boolean(audio));
4476
4642
  setAudioTrack(audio);
4477
4643
  if (videoRef.current) {
4644
+ videoRef.current.setAttribute("webkit-playsinline", "");
4478
4645
  videoRef.current.srcObject = video ? next : null;
4479
4646
  }
4480
4647
  bindEnded(next, gen);
@@ -4504,7 +4671,7 @@ function StudioDevicePreflight({
4504
4671
  autoPlay: true,
4505
4672
  muted: true,
4506
4673
  playsInline: true,
4507
- className: "bg-muted aspect-video w-full rounded-xl object-cover"
4674
+ className: "livo-studio-inline-video bg-muted aspect-video w-full rounded-xl object-cover"
4508
4675
  }
4509
4676
  ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-muted flex aspect-video w-full items-center justify-center rounded-xl", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-sm", children: cameraOffLabel }) }),
4510
4677
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4726,7 +4893,11 @@ function isRecorder(participant) {
4726
4893
  function resolvedName(participant, nameOverrides) {
4727
4894
  return nameOverrides.get(participant.id) ?? (participant.userId ? nameOverrides.get(participant.userId) : void 0) ?? participant.name?.trim() ?? "Guest";
4728
4895
  }
4729
- function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides) {
4896
+ function resolveTileStageStatus(participant, stageEnabled, fallback) {
4897
+ if (!stageEnabled) return "ON_STAGE";
4898
+ return participant.stageStatus ?? fallback ?? "OFF_STAGE";
4899
+ }
4900
+ function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides, stageStatus) {
4730
4901
  return {
4731
4902
  id: participant.id,
4732
4903
  userId: participant.userId ?? "",
@@ -4736,7 +4907,7 @@ function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides) {
4736
4907
  videoEnabled: participant.videoEnabled === true,
4737
4908
  screenShareEnabled: participant.screenShareEnabled === true,
4738
4909
  picture: participant.picture ?? null,
4739
- stageStatus: participant.stageStatus ?? "ON_STAGE",
4910
+ stageStatus,
4740
4911
  pinned: participant.isPinned === true || pinnedIds.has(participant.id),
4741
4912
  chatCanSend: chatSend.get(participant.id) ?? true
4742
4913
  };
@@ -4765,44 +4936,42 @@ function screenShareTrack(meeting, participantId) {
4765
4936
  }
4766
4937
  return findParticipant(meeting, participantId)?.screenShareTracks?.video ?? null;
4767
4938
  }
4768
- var pendingAudioPlay = /* @__PURE__ */ new Set();
4769
- function replayPendingAudio() {
4770
- for (const el of pendingAudioPlay) {
4771
- void el.play().then(
4772
- () => {
4773
- pendingAudioPlay.delete(el);
4774
- },
4775
- () => void 0
4776
- );
4939
+ var pendingMediaPlay = /* @__PURE__ */ new Set();
4940
+ function tryPlay(el) {
4941
+ if (typeof el.play !== "function") return;
4942
+ void el.play().then(
4943
+ () => {
4944
+ pendingMediaPlay.delete(el);
4945
+ },
4946
+ () => {
4947
+ pendingMediaPlay.add(el);
4948
+ }
4949
+ );
4950
+ }
4951
+ function replayPendingMedia() {
4952
+ for (const el of pendingMediaPlay) {
4953
+ tryPlay(el);
4777
4954
  }
4778
4955
  }
4779
4956
  if (typeof document !== "undefined") {
4780
- document.addEventListener("pointerdown", replayPendingAudio, {
4957
+ document.addEventListener("pointerdown", replayPendingMedia, {
4781
4958
  passive: true
4782
4959
  });
4783
4960
  }
4784
4961
  function bindTrackElement(el, track, kind) {
4785
4962
  if (!track) {
4786
4963
  el.srcObject = null;
4787
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4964
+ pendingMediaPlay.delete(el);
4788
4965
  return;
4789
4966
  }
4790
4967
  const current = el.srcObject;
4791
4968
  const currentTrack = current instanceof MediaStream ? kind === "video" ? current.getVideoTracks()[0] : current.getAudioTracks()[0] : void 0;
4792
4969
  if (currentTrack?.id === track.id) {
4970
+ if (el.paused) tryPlay(el);
4793
4971
  return;
4794
4972
  }
4795
4973
  el.srcObject = new MediaStream([track]);
4796
- if (typeof el.play === "function") {
4797
- void el.play().then(
4798
- () => {
4799
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4800
- },
4801
- () => {
4802
- if (el instanceof HTMLAudioElement) pendingAudioPlay.add(el);
4803
- }
4804
- );
4805
- }
4974
+ tryPlay(el);
4806
4975
  }
4807
4976
  function roomLeftState(payload, fallback) {
4808
4977
  if (typeof payload === "string" && payload) return payload;
@@ -5280,7 +5449,7 @@ function studioMeetingFromRtk(raw) {
5280
5449
  notifyStage();
5281
5450
  },
5282
5451
  async joinStage() {
5283
- if (currentStageStatus() === "ON_STAGE") {
5452
+ if (meeting.self?.stageStatus === "ON_STAGE") {
5284
5453
  notifyStage();
5285
5454
  return;
5286
5455
  }
@@ -5289,14 +5458,15 @@ function studioMeetingFromRtk(raw) {
5289
5458
  notifyStage();
5290
5459
  return;
5291
5460
  }
5461
+ const userId = meeting.self?.userId;
5462
+ if (userId) {
5463
+ await meeting.stage?.grantAccess?.([userId]);
5464
+ }
5292
5465
  try {
5293
5466
  await meeting.stage?.join?.();
5294
- } catch {
5295
- const userId = meeting.self?.userId;
5296
- if (userId) {
5297
- await meeting.stage?.grantAccess?.([userId]);
5298
- }
5299
- await meeting.stage?.join?.();
5467
+ } catch (error) {
5468
+ notifyStage();
5469
+ throw error;
5300
5470
  }
5301
5471
  joinedStageLocally = true;
5302
5472
  await republishCameraIfOn();
@@ -5304,6 +5474,7 @@ function studioMeetingFromRtk(raw) {
5304
5474
  },
5305
5475
  async leaveStage() {
5306
5476
  await meeting.stage?.leave?.();
5477
+ joinedStageLocally = false;
5307
5478
  notifyStage();
5308
5479
  },
5309
5480
  stageStatus() {
@@ -5385,10 +5556,27 @@ function studioMeetingFromRtk(raw) {
5385
5556
  participants() {
5386
5557
  const self = meeting.self;
5387
5558
  const pinned = pinnedIdSet();
5388
- const joinedRows = asList(meeting.participants?.joined).filter((row) => row.id !== self?.id && !isRecorder(row)).map((row) => tileFrom(row, self?.id, pinned, chatSend, nameOverrides));
5559
+ const stageEnabled = self?.permissions?.stageEnabled !== false;
5560
+ const joinedRows = asList(meeting.participants?.joined).filter((row) => row.id !== self?.id && !isRecorder(row)).map(
5561
+ (row) => tileFrom(
5562
+ row,
5563
+ self?.id,
5564
+ pinned,
5565
+ chatSend,
5566
+ nameOverrides,
5567
+ resolveTileStageStatus(row, stageEnabled)
5568
+ )
5569
+ );
5389
5570
  if (self?.id && !isRecorder(self)) {
5390
5571
  joinedRows.unshift(
5391
- tileFrom(self, self.id, pinned, chatSend, nameOverrides)
5572
+ tileFrom(
5573
+ self,
5574
+ self.id,
5575
+ pinned,
5576
+ chatSend,
5577
+ nameOverrides,
5578
+ resolveTileStageStatus(self, stageEnabled, meeting.stage?.status)
5579
+ )
5392
5580
  );
5393
5581
  }
5394
5582
  return joinedRows;
@@ -5766,6 +5954,21 @@ async function getStudioJoinStatus(token, apiUrl = defaultApiUrl(), fetchFn = fe
5766
5954
  throw error;
5767
5955
  }
5768
5956
  }
5957
+ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
5958
+ try {
5959
+ const body = await publicJson(
5960
+ `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
5961
+ {},
5962
+ fetchFn
5963
+ );
5964
+ return parsePublicStreamStatus(body);
5965
+ } catch (error) {
5966
+ if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
5967
+ return "ended";
5968
+ }
5969
+ return null;
5970
+ }
5971
+ }
5769
5972
  var POLL_INTERVAL_MS = 5e3;
5770
5973
  function LivoHostStudio({
5771
5974
  token,
@@ -5860,6 +6063,7 @@ function LivoHostStudio({
5860
6063
  );
5861
6064
  }
5862
6065
  var POLL_INTERVAL_MS2 = 5e3;
6066
+ var PREVIEW_POLL_INTERVAL_MS = 2e3;
5863
6067
  function LivoGuestStudio({
5864
6068
  token,
5865
6069
  apiUrl,
@@ -5871,6 +6075,7 @@ function LivoGuestStudio({
5871
6075
  }) {
5872
6076
  const reducedMotion = usePrefersReducedMotion();
5873
6077
  const [session, setSession] = react.useState(null);
6078
+ const [status, setStatus] = react.useState(null);
5874
6079
  const [error, setError] = react.useState(null);
5875
6080
  const [displayName, setDisplayName] = react.useState("");
5876
6081
  const [guestId, setGuestId] = react.useState("");
@@ -5920,6 +6125,7 @@ function LivoGuestStudio({
5920
6125
  );
5921
6126
  setWaitingForHost(false);
5922
6127
  setRetryAfterMs(null);
6128
+ setStatus(null);
5923
6129
  setSession(next);
5924
6130
  } catch (err) {
5925
6131
  const code = studioErrorCode(err);
@@ -5967,6 +6173,31 @@ function LivoGuestStudio({
5967
6173
  window.clearInterval(id);
5968
6174
  };
5969
6175
  }, [apiUrl, join, lobbyLabels.endedTitle, token, waitingForHost]);
6176
+ react.useEffect(() => {
6177
+ if (!session) return;
6178
+ const current = status ?? session.stream.status;
6179
+ if (current === "ended" || current === "error") return;
6180
+ let cancelled = false;
6181
+ const poll = async () => {
6182
+ const next = await getPublicStreamStatus(session.stream.id, apiUrl);
6183
+ if (!cancelled && next) setStatus(next);
6184
+ };
6185
+ void poll();
6186
+ const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
6187
+ const id = window.setInterval(() => void poll(), intervalMs);
6188
+ return () => {
6189
+ cancelled = true;
6190
+ window.clearInterval(id);
6191
+ };
6192
+ }, [apiUrl, session, status]);
6193
+ const liveSession = react.useMemo(() => {
6194
+ if (!session) return null;
6195
+ if (!status) return session;
6196
+ return {
6197
+ ...session,
6198
+ stream: { ...session.stream, status }
6199
+ };
6200
+ }, [session, status]);
5970
6201
  react.useEffect(() => {
5971
6202
  if (retryAfterMs == null) return;
5972
6203
  const id = window.setTimeout(() => {
@@ -5979,7 +6210,7 @@ function LivoGuestStudio({
5979
6210
  if (meetingError) {
5980
6211
  return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive p-6", role: "alert", children: meetingError });
5981
6212
  }
5982
- if (!session || !meeting) {
6213
+ if (!liveSession || !meeting) {
5983
6214
  return /* @__PURE__ */ jsxRuntime.jsx(
5984
6215
  StudioGuestLobby,
5985
6216
  {
@@ -6004,7 +6235,7 @@ function LivoGuestStudio({
6004
6235
  return /* @__PURE__ */ jsxRuntime.jsx(
6005
6236
  StudioRoom,
6006
6237
  {
6007
- session,
6238
+ session: liveSession,
6008
6239
  meeting,
6009
6240
  control: {
6010
6241
  livestream: async () => "starting",
@@ -6013,6 +6244,7 @@ function LivoGuestStudio({
6013
6244
  },
6014
6245
  onRejoin: () => {
6015
6246
  setSession(null);
6247
+ setStatus(null);
6016
6248
  setSessionGen((n) => n + 1);
6017
6249
  setJoining(false);
6018
6250
  setWaitingForHost(false);
@@ -6159,6 +6391,8 @@ function studioRoomLabels(t) {
6159
6391
  admitAllAudience: t("admitAllAudience"),
6160
6392
  askToGoOnAir: t("askToGoOnAir"),
6161
6393
  stageRequested: t("stageRequested"),
6394
+ stageApproved: t("stageApproved"),
6395
+ stageDenied: t("stageDenied"),
6162
6396
  stageRequests: t("stageRequests"),
6163
6397
  people: t("people"),
6164
6398
  panelists: t("panelists"),
@@ -6210,6 +6444,7 @@ exports.applyEmbedTheme = applyEmbedTheme;
6210
6444
  exports.controlActionsFromClient = controlActionsFromClient;
6211
6445
  exports.createStudioControlClient = createStudioControlClient;
6212
6446
  exports.decodeEmbedTheme = decodeEmbedTheme;
6447
+ exports.getPublicStreamStatus = getPublicStreamStatus;
6213
6448
  exports.getStudioJoinStatus = getStudioJoinStatus;
6214
6449
  exports.joinStudioGuest = joinStudioGuest;
6215
6450
  exports.redeemStudioHost = redeemStudioHost;