@livo-tv/sdk 1.2.0 → 1.2.2

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
  }
@@ -2212,7 +2305,7 @@ function BubbleGroup({ className, ...props }) {
2212
2305
  );
2213
2306
  }
2214
2307
  var bubbleVariants = classVarianceAuthority.cva(
2215
- "group/bubble relative flex w-fit max-w-[80%] min-w-0 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-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",
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-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed 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",
2353
+ "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",
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 w-fit max-w-full flex-col gap-1" : "flex w-fit max-w-full flex-col gap-1",
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
  }
@@ -2609,7 +2709,7 @@ function StudioSidePanel({
2609
2709
  const panelists = participants.filter((row) => isOnStage(row.stageStatus));
2610
2710
  const audience = participants.filter((row) => !isOnStage(row.stageStatus));
2611
2711
  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: [
2612
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex border-b", children: [
2712
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
2613
2713
  /* @__PURE__ */ jsxRuntime.jsxs(
2614
2714
  Button,
2615
2715
  {
@@ -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"
@@ -3263,14 +3365,24 @@ function StudioToolbar({
3263
3365
  children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
3264
3366
  }
3265
3367
  ) : null,
3266
- session.role === "guest" && stageEnabled && selfStageStatus !== "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(
3368
+ session.role === "guest" && stageEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
3267
3369
  ControlIcon,
3268
3370
  {
3269
- label: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
3371
+ label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
3270
3372
  pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
3271
- disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
3272
- onClick: onAskStage,
3273
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, {})
3373
+ disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
3374
+ onClick: () => {
3375
+ if (selfStageStatus === "ON_STAGE") {
3376
+ if (!canLeaveStage) {
3377
+ sonner.toast.message(labels.offAirLastVisible);
3378
+ return;
3379
+ }
3380
+ onLeaveStage();
3381
+ return;
3382
+ }
3383
+ onAskStage();
3384
+ },
3385
+ children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
3274
3386
  }
3275
3387
  ) : null,
3276
3388
  showScreenControl ? canShareFreely ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3445,6 +3557,12 @@ function StudioRoom({
3445
3557
  const seenWaitlisted = react.useRef(/* @__PURE__ */ new Set());
3446
3558
  const knockPrimed = react.useRef(false);
3447
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);
3448
3566
  const grantCheckTimers = react.useRef([]);
3449
3567
  const screenNoticePrimed = react.useRef(false);
3450
3568
  const lastSelfMedia = react.useRef({
@@ -3473,6 +3591,9 @@ function StudioRoom({
3473
3591
  const rejected = isStudioRejected(roomState);
3474
3592
  useBeforeUnload(active && !sessionOver);
3475
3593
  useWakeLock(active && !sessionOver);
3594
+ react.useEffect(() => {
3595
+ unlockStudioSounds();
3596
+ }, []);
3476
3597
  react.useEffect(() => {
3477
3598
  if (onAir) {
3478
3599
  setPublishing(false);
@@ -3698,9 +3819,57 @@ function StudioRoom({
3698
3819
  const incoming = chatMessages.filter((row) => row.userId !== selfUserId);
3699
3820
  const unread = sideTab === "chat" ? 0 : Math.max(0, incoming.length - seenIncoming);
3700
3821
  const peopleBadge = waitlisted.length + stageRequests.length;
3822
+ const selfStageStatus = self?.stageStatus ?? meeting.stageStatus();
3701
3823
  react.useEffect(() => {
3702
3824
  if (sideTab === "chat") setSeenIncoming(incoming.length);
3703
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]);
3704
3873
  react.useEffect(() => {
3705
3874
  if (session.role === "moderator" && waitlisted.length > 0 && !peopleAutoOpened.current) {
3706
3875
  peopleAutoOpened.current = true;
@@ -3717,7 +3886,10 @@ function StudioRoom({
3717
3886
  if (seenWaitlisted.current.has(guest.id)) continue;
3718
3887
  seenWaitlisted.current.add(guest.id);
3719
3888
  sonner.toast.message(labels.knock.replace("{name}", guest.name));
3720
- if (session.role === "moderator") setSideTab("people");
3889
+ if (session.role === "moderator") {
3890
+ playStudioSound("request");
3891
+ setSideTab("people");
3892
+ }
3721
3893
  }
3722
3894
  }, [labels.knock, session.role, waitlisted]);
3723
3895
  react.useEffect(() => {
@@ -3754,6 +3926,7 @@ function StudioRoom({
3754
3926
  }
3755
3927
  if (message.type === "share-request") {
3756
3928
  if (session.role !== "moderator") return;
3929
+ playStudioSound("request");
3757
3930
  sonner.toast.message(labels.shareRequestFrom.replace("{name}", message.name), {
3758
3931
  id: `share-request:${message.participantId}`,
3759
3932
  action: {
@@ -4129,10 +4302,13 @@ function StudioRoom({
4129
4302
  size: "icon-sm",
4130
4303
  "aria-label": labels.share,
4131
4304
  onClick: () => {
4132
- void navigator.clipboard.writeText(
4133
- playerWatchUrl(session.stream.id, playerOrigin)
4134
- );
4135
- 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
+ });
4136
4312
  },
4137
4313
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, {})
4138
4314
  }
@@ -4465,6 +4641,7 @@ function StudioDevicePreflight({
4465
4641
  setMicOk(Boolean(audio));
4466
4642
  setAudioTrack(audio);
4467
4643
  if (videoRef.current) {
4644
+ videoRef.current.setAttribute("webkit-playsinline", "");
4468
4645
  videoRef.current.srcObject = video ? next : null;
4469
4646
  }
4470
4647
  bindEnded(next, gen);
@@ -4494,7 +4671,7 @@ function StudioDevicePreflight({
4494
4671
  autoPlay: true,
4495
4672
  muted: true,
4496
4673
  playsInline: true,
4497
- 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"
4498
4675
  }
4499
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 }) }),
4500
4677
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4755,44 +4932,42 @@ function screenShareTrack(meeting, participantId) {
4755
4932
  }
4756
4933
  return findParticipant(meeting, participantId)?.screenShareTracks?.video ?? null;
4757
4934
  }
4758
- var pendingAudioPlay = /* @__PURE__ */ new Set();
4759
- function replayPendingAudio() {
4760
- for (const el of pendingAudioPlay) {
4761
- void el.play().then(
4762
- () => {
4763
- pendingAudioPlay.delete(el);
4764
- },
4765
- () => void 0
4766
- );
4935
+ var pendingMediaPlay = /* @__PURE__ */ new Set();
4936
+ function tryPlay(el) {
4937
+ if (typeof el.play !== "function") return;
4938
+ void el.play().then(
4939
+ () => {
4940
+ pendingMediaPlay.delete(el);
4941
+ },
4942
+ () => {
4943
+ pendingMediaPlay.add(el);
4944
+ }
4945
+ );
4946
+ }
4947
+ function replayPendingMedia() {
4948
+ for (const el of pendingMediaPlay) {
4949
+ tryPlay(el);
4767
4950
  }
4768
4951
  }
4769
4952
  if (typeof document !== "undefined") {
4770
- document.addEventListener("pointerdown", replayPendingAudio, {
4953
+ document.addEventListener("pointerdown", replayPendingMedia, {
4771
4954
  passive: true
4772
4955
  });
4773
4956
  }
4774
4957
  function bindTrackElement(el, track, kind) {
4775
4958
  if (!track) {
4776
4959
  el.srcObject = null;
4777
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4960
+ pendingMediaPlay.delete(el);
4778
4961
  return;
4779
4962
  }
4780
4963
  const current = el.srcObject;
4781
4964
  const currentTrack = current instanceof MediaStream ? kind === "video" ? current.getVideoTracks()[0] : current.getAudioTracks()[0] : void 0;
4782
4965
  if (currentTrack?.id === track.id) {
4966
+ if (el.paused) tryPlay(el);
4783
4967
  return;
4784
4968
  }
4785
4969
  el.srcObject = new MediaStream([track]);
4786
- if (typeof el.play === "function") {
4787
- void el.play().then(
4788
- () => {
4789
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4790
- },
4791
- () => {
4792
- if (el instanceof HTMLAudioElement) pendingAudioPlay.add(el);
4793
- }
4794
- );
4795
- }
4970
+ tryPlay(el);
4796
4971
  }
4797
4972
  function roomLeftState(payload, fallback) {
4798
4973
  if (typeof payload === "string" && payload) return payload;
@@ -5279,7 +5454,15 @@ function studioMeetingFromRtk(raw) {
5279
5454
  notifyStage();
5280
5455
  return;
5281
5456
  }
5282
- await meeting.stage?.join?.();
5457
+ try {
5458
+ await meeting.stage?.join?.();
5459
+ } catch {
5460
+ const userId = meeting.self?.userId;
5461
+ if (userId) {
5462
+ await meeting.stage?.grantAccess?.([userId]);
5463
+ }
5464
+ await meeting.stage?.join?.();
5465
+ }
5283
5466
  joinedStageLocally = true;
5284
5467
  await republishCameraIfOn();
5285
5468
  notifyStage();
@@ -5569,7 +5752,11 @@ function useRealtimeKitMeeting(authToken, generation = 0) {
5569
5752
  const [meeting, setMeeting] = react.useState(null);
5570
5753
  const [error, setError] = react.useState(null);
5571
5754
  react.useEffect(() => {
5572
- if (!authToken) return;
5755
+ if (!authToken) {
5756
+ setMeeting(null);
5757
+ setError(null);
5758
+ return;
5759
+ }
5573
5760
  let cancelled = false;
5574
5761
  let handle = null;
5575
5762
  setError(null);
@@ -5744,6 +5931,21 @@ async function getStudioJoinStatus(token, apiUrl = defaultApiUrl(), fetchFn = fe
5744
5931
  throw error;
5745
5932
  }
5746
5933
  }
5934
+ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
5935
+ try {
5936
+ const body = await publicJson(
5937
+ `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
5938
+ {},
5939
+ fetchFn
5940
+ );
5941
+ return parsePublicStreamStatus(body);
5942
+ } catch (error) {
5943
+ if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
5944
+ return "ended";
5945
+ }
5946
+ return null;
5947
+ }
5948
+ }
5747
5949
  var POLL_INTERVAL_MS = 5e3;
5748
5950
  function LivoHostStudio({
5749
5951
  token,
@@ -5838,6 +6040,7 @@ function LivoHostStudio({
5838
6040
  );
5839
6041
  }
5840
6042
  var POLL_INTERVAL_MS2 = 5e3;
6043
+ var PREVIEW_POLL_INTERVAL_MS = 2e3;
5841
6044
  function LivoGuestStudio({
5842
6045
  token,
5843
6046
  apiUrl,
@@ -5849,6 +6052,7 @@ function LivoGuestStudio({
5849
6052
  }) {
5850
6053
  const reducedMotion = usePrefersReducedMotion();
5851
6054
  const [session, setSession] = react.useState(null);
6055
+ const [status, setStatus] = react.useState(null);
5852
6056
  const [error, setError] = react.useState(null);
5853
6057
  const [displayName, setDisplayName] = react.useState("");
5854
6058
  const [guestId, setGuestId] = react.useState("");
@@ -5898,6 +6102,7 @@ function LivoGuestStudio({
5898
6102
  );
5899
6103
  setWaitingForHost(false);
5900
6104
  setRetryAfterMs(null);
6105
+ setStatus(null);
5901
6106
  setSession(next);
5902
6107
  } catch (err) {
5903
6108
  const code = studioErrorCode(err);
@@ -5914,9 +6119,9 @@ function LivoGuestStudio({
5914
6119
  setError(err instanceof Error ? err.message : lobbyLabels.joinError);
5915
6120
  setWaitingForHost(false);
5916
6121
  }
5917
- setJoining(false);
5918
6122
  } finally {
5919
6123
  joinInFlight.current = false;
6124
+ setJoining(false);
5920
6125
  }
5921
6126
  }, [apiUrl, displayName, guestId, lobbyLabels, token]);
5922
6127
  react.useEffect(() => {
@@ -5945,6 +6150,31 @@ function LivoGuestStudio({
5945
6150
  window.clearInterval(id);
5946
6151
  };
5947
6152
  }, [apiUrl, join, lobbyLabels.endedTitle, token, waitingForHost]);
6153
+ react.useEffect(() => {
6154
+ if (!session) return;
6155
+ const current = status ?? session.stream.status;
6156
+ if (current === "ended" || current === "error") return;
6157
+ let cancelled = false;
6158
+ const poll = async () => {
6159
+ const next = await getPublicStreamStatus(session.stream.id, apiUrl);
6160
+ if (!cancelled && next) setStatus(next);
6161
+ };
6162
+ void poll();
6163
+ const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
6164
+ const id = window.setInterval(() => void poll(), intervalMs);
6165
+ return () => {
6166
+ cancelled = true;
6167
+ window.clearInterval(id);
6168
+ };
6169
+ }, [apiUrl, session, status]);
6170
+ const liveSession = react.useMemo(() => {
6171
+ if (!session) return null;
6172
+ if (!status) return session;
6173
+ return {
6174
+ ...session,
6175
+ stream: { ...session.stream, status }
6176
+ };
6177
+ }, [session, status]);
5948
6178
  react.useEffect(() => {
5949
6179
  if (retryAfterMs == null) return;
5950
6180
  const id = window.setTimeout(() => {
@@ -5957,7 +6187,7 @@ function LivoGuestStudio({
5957
6187
  if (meetingError) {
5958
6188
  return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive p-6", role: "alert", children: meetingError });
5959
6189
  }
5960
- if (!session || !meeting) {
6190
+ if (!liveSession || !meeting) {
5961
6191
  return /* @__PURE__ */ jsxRuntime.jsx(
5962
6192
  StudioGuestLobby,
5963
6193
  {
@@ -5982,7 +6212,7 @@ function LivoGuestStudio({
5982
6212
  return /* @__PURE__ */ jsxRuntime.jsx(
5983
6213
  StudioRoom,
5984
6214
  {
5985
- session,
6215
+ session: liveSession,
5986
6216
  meeting,
5987
6217
  control: {
5988
6218
  livestream: async () => "starting",
@@ -5991,7 +6221,12 @@ function LivoGuestStudio({
5991
6221
  },
5992
6222
  onRejoin: () => {
5993
6223
  setSession(null);
6224
+ setStatus(null);
5994
6225
  setSessionGen((n) => n + 1);
6226
+ setJoining(false);
6227
+ setWaitingForHost(false);
6228
+ setError(null);
6229
+ joinInFlight.current = false;
5995
6230
  },
5996
6231
  reducedMotion,
5997
6232
  labels,
@@ -6133,6 +6368,8 @@ function studioRoomLabels(t) {
6133
6368
  admitAllAudience: t("admitAllAudience"),
6134
6369
  askToGoOnAir: t("askToGoOnAir"),
6135
6370
  stageRequested: t("stageRequested"),
6371
+ stageApproved: t("stageApproved"),
6372
+ stageDenied: t("stageDenied"),
6136
6373
  stageRequests: t("stageRequests"),
6137
6374
  people: t("people"),
6138
6375
  panelists: t("panelists"),
@@ -6184,6 +6421,7 @@ exports.applyEmbedTheme = applyEmbedTheme;
6184
6421
  exports.controlActionsFromClient = controlActionsFromClient;
6185
6422
  exports.createStudioControlClient = createStudioControlClient;
6186
6423
  exports.decodeEmbedTheme = decodeEmbedTheme;
6424
+ exports.getPublicStreamStatus = getPublicStreamStatus;
6187
6425
  exports.getStudioJoinStatus = getStudioJoinStatus;
6188
6426
  exports.joinStudioGuest = joinStudioGuest;
6189
6427
  exports.redeemStudioHost = redeemStudioHost;