@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.
@@ -740,6 +740,12 @@ function parseStudioJoinStatus(value) {
740
740
  if (value.streamStatus !== null && streamStatus === null) return null;
741
741
  return { ready: value.ready, streamStatus };
742
742
  }
743
+ function parsePublicStreamStatus(value) {
744
+ if (!isRecord(value)) return null;
745
+ const nested = isRecord(value.stream) ? value.stream : value;
746
+ const status = readString(nested, "status");
747
+ return status;
748
+ }
743
749
  function studioErrorCode(error) {
744
750
  if (!error || typeof error !== "object") return null;
745
751
  const body = "body" in error ? error.body : null;
@@ -747,6 +753,90 @@ function studioErrorCode(error) {
747
753
  return body.error;
748
754
  }
749
755
 
756
+ // src/studio/lib/studio-sounds.ts
757
+ var CHAT_DEBOUNCE_MS = 400;
758
+ var CHAT_HZ = 880;
759
+ var CHAT_MS = 80;
760
+ var REQUEST_LOW_HZ = 660;
761
+ var REQUEST_HIGH_HZ = 880;
762
+ var REQUEST_NOTE_MS = 80;
763
+ var GAIN = 0.1;
764
+ var ctx = null;
765
+ var lastChatAt = 0;
766
+ function audioContextCtor() {
767
+ if (typeof window === "undefined") return null;
768
+ return window.AudioContext || window.webkitAudioContext || null;
769
+ }
770
+ function prefersReducedMotion() {
771
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
772
+ return false;
773
+ }
774
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
775
+ }
776
+ function ensureContext() {
777
+ const AudioCtx = audioContextCtor();
778
+ if (!AudioCtx) return null;
779
+ if (!ctx || ctx.state === "closed") {
780
+ ctx = new AudioCtx();
781
+ }
782
+ return ctx;
783
+ }
784
+ function unlockStudioSounds() {
785
+ const context = ensureContext();
786
+ if (!context) return;
787
+ void context.resume();
788
+ }
789
+ function playTone(context, frequency, startAt, durationSec) {
790
+ const oscillator = context.createOscillator();
791
+ const gain = context.createGain();
792
+ oscillator.type = "sine";
793
+ oscillator.frequency.value = frequency;
794
+ gain.gain.setValueAtTime(GAIN, startAt);
795
+ gain.gain.exponentialRampToValueAtTime(1e-3, startAt + durationSec);
796
+ oscillator.connect(gain);
797
+ gain.connect(context.destination);
798
+ oscillator.start(startAt);
799
+ oscillator.stop(startAt + durationSec);
800
+ }
801
+ function playNow(context, kind) {
802
+ const now = context.currentTime;
803
+ if (kind === "chat") {
804
+ playTone(context, CHAT_HZ, now, CHAT_MS / 1e3);
805
+ return;
806
+ }
807
+ playTone(context, REQUEST_LOW_HZ, now, REQUEST_NOTE_MS / 1e3);
808
+ playTone(
809
+ context,
810
+ REQUEST_HIGH_HZ,
811
+ now + REQUEST_NOTE_MS / 1e3,
812
+ REQUEST_NOTE_MS / 1e3
813
+ );
814
+ }
815
+ function playStudioSound(kind) {
816
+ if (prefersReducedMotion()) return;
817
+ if (kind === "chat") {
818
+ const at = Date.now();
819
+ if (at - lastChatAt < CHAT_DEBOUNCE_MS) return;
820
+ lastChatAt = at;
821
+ }
822
+ const context = ensureContext();
823
+ if (!context) return;
824
+ const run = () => {
825
+ if (context.state !== "running") return;
826
+ playNow(context, kind);
827
+ };
828
+ if (context.state === "running") {
829
+ run();
830
+ return;
831
+ }
832
+ void context.resume().then(run, () => void 0);
833
+ }
834
+ if (typeof document !== "undefined") {
835
+ document.addEventListener("pointerdown", unlockStudioSounds, {
836
+ passive: true
837
+ });
838
+ }
839
+
750
840
  // src/studio/lib/bridge.ts
751
841
  function postLivoEvent(type, payload) {
752
842
  if (typeof window === "undefined") return;
@@ -1160,9 +1250,9 @@ function AudioWaveform({
1160
1250
  if (!canvas || !track || reducedMotion) return;
1161
1251
  const AudioCtx = window.AudioContext || window.webkitAudioContext;
1162
1252
  if (!AudioCtx) return;
1163
- const ctx = new AudioCtx();
1164
- const source = ctx.createMediaStreamSource(new MediaStream([track]));
1165
- const analyser = ctx.createAnalyser();
1253
+ const ctx2 = new AudioCtx();
1254
+ const source = ctx2.createMediaStreamSource(new MediaStream([track]));
1255
+ const analyser = ctx2.createAnalyser();
1166
1256
  analyser.fftSize = 64;
1167
1257
  source.connect(analyser);
1168
1258
  const data = new Uint8Array(analyser.frequencyBinCount);
@@ -1191,7 +1281,7 @@ function AudioWaveform({
1191
1281
  frame = requestAnimationFrame(draw);
1192
1282
  return () => {
1193
1283
  cancelAnimationFrame(frame);
1194
- void ctx.close();
1284
+ void ctx2.close();
1195
1285
  };
1196
1286
  }, [track, reducedMotion]);
1197
1287
  if (reducedMotion) {
@@ -1477,10 +1567,10 @@ async function playSpeakerTest(deviceId) {
1477
1567
  if (!AudioCtx) {
1478
1568
  throw new Error("AudioContext unavailable");
1479
1569
  }
1480
- const ctx = new AudioCtx();
1481
- void ctx.resume();
1482
- const oscillator = ctx.createOscillator();
1483
- const gain = ctx.createGain();
1570
+ const ctx2 = new AudioCtx();
1571
+ void ctx2.resume();
1572
+ const oscillator = ctx2.createOscillator();
1573
+ const gain = ctx2.createGain();
1484
1574
  oscillator.type = "sine";
1485
1575
  oscillator.frequency.value = TEST_HZ;
1486
1576
  gain.gain.value = 0.18;
@@ -1490,7 +1580,7 @@ async function playSpeakerTest(deviceId) {
1490
1580
  const probe = document.createElement("audio");
1491
1581
  const canRoute = Boolean(deviceId) && typeof probe.setSinkId === "function";
1492
1582
  if (canRoute && deviceId) {
1493
- const destination = ctx.createMediaStreamDestination();
1583
+ const destination = ctx2.createMediaStreamDestination();
1494
1584
  gain.connect(destination);
1495
1585
  audio = probe;
1496
1586
  audio.autoplay = true;
@@ -1501,13 +1591,13 @@ async function playSpeakerTest(deviceId) {
1501
1591
  await audio.play();
1502
1592
  } catch {
1503
1593
  gain.disconnect();
1504
- gain.connect(ctx.destination);
1594
+ gain.connect(ctx2.destination);
1505
1595
  audio.srcObject = null;
1506
1596
  audio.remove();
1507
1597
  audio = null;
1508
1598
  }
1509
1599
  } else {
1510
- gain.connect(ctx.destination);
1600
+ gain.connect(ctx2.destination);
1511
1601
  }
1512
1602
  oscillator.start();
1513
1603
  await new Promise((resolve) => {
@@ -1519,7 +1609,7 @@ async function playSpeakerTest(deviceId) {
1519
1609
  audio.srcObject = null;
1520
1610
  audio.remove();
1521
1611
  }
1522
- await ctx.close();
1612
+ await ctx2.close();
1523
1613
  }
1524
1614
  }
1525
1615
  function StudioDeviceSelectGroup({
@@ -1754,6 +1844,7 @@ function SettingsCameraPreview({
1754
1844
  el.srcObject = null;
1755
1845
  return;
1756
1846
  }
1847
+ el.setAttribute("webkit-playsinline", "");
1757
1848
  el.srcObject = new MediaStream([track]);
1758
1849
  void el.play().catch(() => void 0);
1759
1850
  return () => {
@@ -1773,7 +1864,7 @@ function SettingsCameraPreview({
1773
1864
  autoPlay: true,
1774
1865
  muted: true,
1775
1866
  playsInline: true,
1776
- className: "absolute inset-0 size-full -scale-x-100 object-cover"
1867
+ className: "livo-studio-inline-video absolute inset-0 size-full -scale-x-100 object-cover"
1777
1868
  }
1778
1869
  ) });
1779
1870
  }
@@ -1872,6 +1963,7 @@ function WaitingCameraPreview({
1872
1963
  el.srcObject = null;
1873
1964
  return;
1874
1965
  }
1966
+ el.setAttribute("webkit-playsinline", "");
1875
1967
  el.srcObject = new MediaStream([track]);
1876
1968
  void el.play().catch(() => void 0);
1877
1969
  return () => {
@@ -1891,7 +1983,7 @@ function WaitingCameraPreview({
1891
1983
  autoPlay: true,
1892
1984
  muted: true,
1893
1985
  playsInline: true,
1894
- className: "absolute inset-0 size-full -scale-x-100 object-cover"
1986
+ className: "livo-studio-inline-video absolute inset-0 size-full -scale-x-100 object-cover"
1895
1987
  }
1896
1988
  ) });
1897
1989
  }
@@ -2078,6 +2170,7 @@ function PipTrackVideo({
2078
2170
  el.srcObject = null;
2079
2171
  return;
2080
2172
  }
2173
+ el.setAttribute("webkit-playsinline", "");
2081
2174
  el.srcObject = new MediaStream([track]);
2082
2175
  void el.play().catch(() => void 0);
2083
2176
  return () => {
@@ -2092,7 +2185,7 @@ function PipTrackVideo({
2092
2185
  playsInline: true,
2093
2186
  muted: true,
2094
2187
  className: cn(
2095
- "absolute inset-0 size-full",
2188
+ "livo-studio-inline-video absolute inset-0 size-full",
2096
2189
  fit === "contain" ? "object-contain" : "object-cover"
2097
2190
  )
2098
2191
  }
@@ -2182,13 +2275,13 @@ function BubbleGroup({ className, ...props }) {
2182
2275
  "div",
2183
2276
  {
2184
2277
  "data-slot": "bubble-group",
2185
- className: cn("flex min-w-0 flex-col gap-2", className),
2278
+ className: cn("flex min-w-min flex-col gap-2", className),
2186
2279
  ...props
2187
2280
  }
2188
2281
  );
2189
2282
  }
2190
2283
  var bubbleVariants = cva(
2191
- "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",
2284
+ "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",
2192
2285
  {
2193
2286
  variants: {
2194
2287
  variant: {
@@ -2233,7 +2326,7 @@ function BubbleContent({
2233
2326
  props: mergeProps(
2234
2327
  {
2235
2328
  className: cn(
2236
- "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",
2329
+ "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",
2237
2330
  className
2238
2331
  )
2239
2332
  },
@@ -2414,17 +2507,24 @@ function StudioChatPanel({
2414
2507
  }
2415
2508
  ),
2416
2509
  groups.map((group) => {
2417
- const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
2418
- /* @__PURE__ */ jsx(
2419
- Bubble,
2420
- {
2421
- align: group.mine ? "end" : "start",
2422
- variant: group.mine ? "default" : "secondary",
2423
- children: /* @__PURE__ */ jsx(BubbleContent, { children: row.text })
2424
- }
2425
- ),
2426
- messageActions(row, group.mine)
2427
- ] }, row.id));
2510
+ const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxs(
2511
+ "div",
2512
+ {
2513
+ className: group.mine ? "ml-auto flex max-w-[80%] flex-col gap-1 self-end" : "flex max-w-[80%] flex-col gap-1 self-start",
2514
+ children: [
2515
+ /* @__PURE__ */ jsx(
2516
+ Bubble,
2517
+ {
2518
+ align: group.mine ? "end" : "start",
2519
+ variant: group.mine ? "default" : "secondary",
2520
+ children: /* @__PURE__ */ jsx(BubbleContent, { children: row.text })
2521
+ }
2522
+ ),
2523
+ messageActions(row, group.mine)
2524
+ ]
2525
+ },
2526
+ row.id
2527
+ ));
2428
2528
  if (group.mine) {
2429
2529
  return /* @__PURE__ */ jsx(BubbleGroup, { className: "items-end", children: bubbles }, group.rows[0].id);
2430
2530
  }
@@ -3036,7 +3136,9 @@ function StudioTileVideo({
3036
3136
  }) {
3037
3137
  const ref = useRef(null);
3038
3138
  useEffect(() => {
3039
- meeting.attachVideo(participantId, ref.current, kind);
3139
+ const el = ref.current;
3140
+ el?.setAttribute("webkit-playsinline", "");
3141
+ meeting.attachVideo(participantId, el, kind);
3040
3142
  return () => meeting.attachVideo(participantId, null, kind);
3041
3143
  }, [kind, meeting, participantId]);
3042
3144
  return /* @__PURE__ */ jsx(
@@ -3049,7 +3151,7 @@ function StudioTileVideo({
3049
3151
  disablePictureInPicture: true,
3050
3152
  controlsList: "nodownload noplaybackrate noremoteplayback",
3051
3153
  className: cn(
3052
- "absolute inset-0 size-full",
3154
+ "livo-studio-tile-video absolute inset-0 size-full",
3053
3155
  "pointer-events-none",
3054
3156
  contain ? "object-contain" : "object-cover",
3055
3157
  mirror && "-scale-x-100"
@@ -3431,6 +3533,12 @@ function StudioRoom({
3431
3533
  const seenWaitlisted = useRef(/* @__PURE__ */ new Set());
3432
3534
  const knockPrimed = useRef(false);
3433
3535
  const peopleAutoOpened = useRef(false);
3536
+ const seenChatIds = useRef(/* @__PURE__ */ new Set());
3537
+ const chatSoundPrimed = useRef(false);
3538
+ const seenStageRequests = useRef(/* @__PURE__ */ new Set());
3539
+ const stageRequestPrimed = useRef(false);
3540
+ const awaitingStageDecision = useRef(false);
3541
+ const prevStageStatus = useRef(null);
3434
3542
  const grantCheckTimers = useRef([]);
3435
3543
  const screenNoticePrimed = useRef(false);
3436
3544
  const lastSelfMedia = useRef({
@@ -3459,6 +3567,9 @@ function StudioRoom({
3459
3567
  const rejected = isStudioRejected(roomState);
3460
3568
  useBeforeUnload(active && !sessionOver);
3461
3569
  useWakeLock(active && !sessionOver);
3570
+ useEffect(() => {
3571
+ unlockStudioSounds();
3572
+ }, []);
3462
3573
  useEffect(() => {
3463
3574
  if (onAir) {
3464
3575
  setPublishing(false);
@@ -3684,9 +3795,57 @@ function StudioRoom({
3684
3795
  const incoming = chatMessages.filter((row) => row.userId !== selfUserId);
3685
3796
  const unread = sideTab === "chat" ? 0 : Math.max(0, incoming.length - seenIncoming);
3686
3797
  const peopleBadge = waitlisted.length + stageRequests.length;
3798
+ const selfStageStatus = self?.stageStatus ?? meeting.stageStatus();
3687
3799
  useEffect(() => {
3688
3800
  if (sideTab === "chat") setSeenIncoming(incoming.length);
3689
3801
  }, [incoming.length, sideTab]);
3802
+ useEffect(() => {
3803
+ if (!chatSoundPrimed.current) {
3804
+ for (const row of incoming) seenChatIds.current.add(row.id);
3805
+ chatSoundPrimed.current = true;
3806
+ return;
3807
+ }
3808
+ for (const row of incoming) {
3809
+ if (seenChatIds.current.has(row.id)) continue;
3810
+ seenChatIds.current.add(row.id);
3811
+ playStudioSound("chat");
3812
+ }
3813
+ }, [incoming]);
3814
+ useEffect(() => {
3815
+ if (session.role !== "moderator") return;
3816
+ if (!stageRequestPrimed.current) {
3817
+ for (const row of stageRequests) {
3818
+ seenStageRequests.current.add(row.userId);
3819
+ }
3820
+ stageRequestPrimed.current = true;
3821
+ return;
3822
+ }
3823
+ for (const row of stageRequests) {
3824
+ if (seenStageRequests.current.has(row.userId)) continue;
3825
+ seenStageRequests.current.add(row.userId);
3826
+ playStudioSound("request");
3827
+ }
3828
+ }, [session.role, stageRequests]);
3829
+ useEffect(() => {
3830
+ const next = selfStageStatus;
3831
+ const prev = prevStageStatus.current;
3832
+ prevStageStatus.current = next;
3833
+ if (session.role !== "guest") return;
3834
+ if (next === "REQUESTED_TO_JOIN_STAGE") {
3835
+ awaitingStageDecision.current = true;
3836
+ return;
3837
+ }
3838
+ if (!awaitingStageDecision.current) return;
3839
+ if (next === "ACCEPTED_TO_JOIN_STAGE" || next === "ON_STAGE") {
3840
+ awaitingStageDecision.current = false;
3841
+ toast.success(labels.stageApproved);
3842
+ return;
3843
+ }
3844
+ if (next === "OFF_STAGE" && prev === "REQUESTED_TO_JOIN_STAGE") {
3845
+ awaitingStageDecision.current = false;
3846
+ toast.message(labels.stageDenied);
3847
+ }
3848
+ }, [labels.stageApproved, labels.stageDenied, selfStageStatus, session.role]);
3690
3849
  useEffect(() => {
3691
3850
  if (session.role === "moderator" && waitlisted.length > 0 && !peopleAutoOpened.current) {
3692
3851
  peopleAutoOpened.current = true;
@@ -3703,7 +3862,10 @@ function StudioRoom({
3703
3862
  if (seenWaitlisted.current.has(guest.id)) continue;
3704
3863
  seenWaitlisted.current.add(guest.id);
3705
3864
  toast.message(labels.knock.replace("{name}", guest.name));
3706
- if (session.role === "moderator") setSideTab("people");
3865
+ if (session.role === "moderator") {
3866
+ playStudioSound("request");
3867
+ setSideTab("people");
3868
+ }
3707
3869
  }
3708
3870
  }, [labels.knock, session.role, waitlisted]);
3709
3871
  useEffect(() => {
@@ -3740,6 +3902,7 @@ function StudioRoom({
3740
3902
  }
3741
3903
  if (message.type === "share-request") {
3742
3904
  if (session.role !== "moderator") return;
3905
+ playStudioSound("request");
3743
3906
  toast.message(labels.shareRequestFrom.replace("{name}", message.name), {
3744
3907
  id: `share-request:${message.participantId}`,
3745
3908
  action: {
@@ -4115,10 +4278,13 @@ function StudioRoom({
4115
4278
  size: "icon-sm",
4116
4279
  "aria-label": labels.share,
4117
4280
  onClick: () => {
4118
- void navigator.clipboard.writeText(
4119
- playerWatchUrl(session.stream.id, playerOrigin)
4120
- );
4121
- toast.success(labels.shareCopied);
4281
+ void shareOrCopyLink({
4282
+ url: playerWatchUrl(session.stream.id, playerOrigin),
4283
+ title: session.stream.title
4284
+ }).then((result) => {
4285
+ if (result === "copied") toast.success(labels.shareCopied);
4286
+ if (result === "failed") toast.error(labels.shareFailed);
4287
+ });
4122
4288
  },
4123
4289
  children: /* @__PURE__ */ jsx(Share2, {})
4124
4290
  }
@@ -4451,6 +4617,7 @@ function StudioDevicePreflight({
4451
4617
  setMicOk(Boolean(audio));
4452
4618
  setAudioTrack(audio);
4453
4619
  if (videoRef.current) {
4620
+ videoRef.current.setAttribute("webkit-playsinline", "");
4454
4621
  videoRef.current.srcObject = video ? next : null;
4455
4622
  }
4456
4623
  bindEnded(next, gen);
@@ -4480,7 +4647,7 @@ function StudioDevicePreflight({
4480
4647
  autoPlay: true,
4481
4648
  muted: true,
4482
4649
  playsInline: true,
4483
- className: "bg-muted aspect-video w-full rounded-xl object-cover"
4650
+ className: "livo-studio-inline-video bg-muted aspect-video w-full rounded-xl object-cover"
4484
4651
  }
4485
4652
  ) : /* @__PURE__ */ jsx("div", { className: "bg-muted flex aspect-video w-full items-center justify-center rounded-xl", children: /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: cameraOffLabel }) }),
4486
4653
  /* @__PURE__ */ jsx(
@@ -4702,7 +4869,11 @@ function isRecorder(participant) {
4702
4869
  function resolvedName(participant, nameOverrides) {
4703
4870
  return nameOverrides.get(participant.id) ?? (participant.userId ? nameOverrides.get(participant.userId) : void 0) ?? participant.name?.trim() ?? "Guest";
4704
4871
  }
4705
- function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides) {
4872
+ function resolveTileStageStatus(participant, stageEnabled, fallback) {
4873
+ if (!stageEnabled) return "ON_STAGE";
4874
+ return participant.stageStatus ?? fallback ?? "OFF_STAGE";
4875
+ }
4876
+ function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides, stageStatus) {
4706
4877
  return {
4707
4878
  id: participant.id,
4708
4879
  userId: participant.userId ?? "",
@@ -4712,7 +4883,7 @@ function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides) {
4712
4883
  videoEnabled: participant.videoEnabled === true,
4713
4884
  screenShareEnabled: participant.screenShareEnabled === true,
4714
4885
  picture: participant.picture ?? null,
4715
- stageStatus: participant.stageStatus ?? "ON_STAGE",
4886
+ stageStatus,
4716
4887
  pinned: participant.isPinned === true || pinnedIds.has(participant.id),
4717
4888
  chatCanSend: chatSend.get(participant.id) ?? true
4718
4889
  };
@@ -4741,44 +4912,42 @@ function screenShareTrack(meeting, participantId) {
4741
4912
  }
4742
4913
  return findParticipant(meeting, participantId)?.screenShareTracks?.video ?? null;
4743
4914
  }
4744
- var pendingAudioPlay = /* @__PURE__ */ new Set();
4745
- function replayPendingAudio() {
4746
- for (const el of pendingAudioPlay) {
4747
- void el.play().then(
4748
- () => {
4749
- pendingAudioPlay.delete(el);
4750
- },
4751
- () => void 0
4752
- );
4915
+ var pendingMediaPlay = /* @__PURE__ */ new Set();
4916
+ function tryPlay(el) {
4917
+ if (typeof el.play !== "function") return;
4918
+ void el.play().then(
4919
+ () => {
4920
+ pendingMediaPlay.delete(el);
4921
+ },
4922
+ () => {
4923
+ pendingMediaPlay.add(el);
4924
+ }
4925
+ );
4926
+ }
4927
+ function replayPendingMedia() {
4928
+ for (const el of pendingMediaPlay) {
4929
+ tryPlay(el);
4753
4930
  }
4754
4931
  }
4755
4932
  if (typeof document !== "undefined") {
4756
- document.addEventListener("pointerdown", replayPendingAudio, {
4933
+ document.addEventListener("pointerdown", replayPendingMedia, {
4757
4934
  passive: true
4758
4935
  });
4759
4936
  }
4760
4937
  function bindTrackElement(el, track, kind) {
4761
4938
  if (!track) {
4762
4939
  el.srcObject = null;
4763
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4940
+ pendingMediaPlay.delete(el);
4764
4941
  return;
4765
4942
  }
4766
4943
  const current = el.srcObject;
4767
4944
  const currentTrack = current instanceof MediaStream ? kind === "video" ? current.getVideoTracks()[0] : current.getAudioTracks()[0] : void 0;
4768
4945
  if (currentTrack?.id === track.id) {
4946
+ if (el.paused) tryPlay(el);
4769
4947
  return;
4770
4948
  }
4771
4949
  el.srcObject = new MediaStream([track]);
4772
- if (typeof el.play === "function") {
4773
- void el.play().then(
4774
- () => {
4775
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4776
- },
4777
- () => {
4778
- if (el instanceof HTMLAudioElement) pendingAudioPlay.add(el);
4779
- }
4780
- );
4781
- }
4950
+ tryPlay(el);
4782
4951
  }
4783
4952
  function roomLeftState(payload, fallback) {
4784
4953
  if (typeof payload === "string" && payload) return payload;
@@ -5256,7 +5425,7 @@ function studioMeetingFromRtk(raw) {
5256
5425
  notifyStage();
5257
5426
  },
5258
5427
  async joinStage() {
5259
- if (currentStageStatus() === "ON_STAGE") {
5428
+ if (meeting.self?.stageStatus === "ON_STAGE") {
5260
5429
  notifyStage();
5261
5430
  return;
5262
5431
  }
@@ -5265,14 +5434,15 @@ function studioMeetingFromRtk(raw) {
5265
5434
  notifyStage();
5266
5435
  return;
5267
5436
  }
5437
+ const userId = meeting.self?.userId;
5438
+ if (userId) {
5439
+ await meeting.stage?.grantAccess?.([userId]);
5440
+ }
5268
5441
  try {
5269
5442
  await meeting.stage?.join?.();
5270
- } catch {
5271
- const userId = meeting.self?.userId;
5272
- if (userId) {
5273
- await meeting.stage?.grantAccess?.([userId]);
5274
- }
5275
- await meeting.stage?.join?.();
5443
+ } catch (error) {
5444
+ notifyStage();
5445
+ throw error;
5276
5446
  }
5277
5447
  joinedStageLocally = true;
5278
5448
  await republishCameraIfOn();
@@ -5280,6 +5450,7 @@ function studioMeetingFromRtk(raw) {
5280
5450
  },
5281
5451
  async leaveStage() {
5282
5452
  await meeting.stage?.leave?.();
5453
+ joinedStageLocally = false;
5283
5454
  notifyStage();
5284
5455
  },
5285
5456
  stageStatus() {
@@ -5361,10 +5532,27 @@ function studioMeetingFromRtk(raw) {
5361
5532
  participants() {
5362
5533
  const self = meeting.self;
5363
5534
  const pinned = pinnedIdSet();
5364
- const joinedRows = asList(meeting.participants?.joined).filter((row) => row.id !== self?.id && !isRecorder(row)).map((row) => tileFrom(row, self?.id, pinned, chatSend, nameOverrides));
5535
+ const stageEnabled = self?.permissions?.stageEnabled !== false;
5536
+ const joinedRows = asList(meeting.participants?.joined).filter((row) => row.id !== self?.id && !isRecorder(row)).map(
5537
+ (row) => tileFrom(
5538
+ row,
5539
+ self?.id,
5540
+ pinned,
5541
+ chatSend,
5542
+ nameOverrides,
5543
+ resolveTileStageStatus(row, stageEnabled)
5544
+ )
5545
+ );
5365
5546
  if (self?.id && !isRecorder(self)) {
5366
5547
  joinedRows.unshift(
5367
- tileFrom(self, self.id, pinned, chatSend, nameOverrides)
5548
+ tileFrom(
5549
+ self,
5550
+ self.id,
5551
+ pinned,
5552
+ chatSend,
5553
+ nameOverrides,
5554
+ resolveTileStageStatus(self, stageEnabled, meeting.stage?.status)
5555
+ )
5368
5556
  );
5369
5557
  }
5370
5558
  return joinedRows;
@@ -5742,6 +5930,21 @@ async function getStudioJoinStatus(token, apiUrl = defaultApiUrl(), fetchFn = fe
5742
5930
  throw error;
5743
5931
  }
5744
5932
  }
5933
+ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
5934
+ try {
5935
+ const body = await publicJson(
5936
+ `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
5937
+ {},
5938
+ fetchFn
5939
+ );
5940
+ return parsePublicStreamStatus(body);
5941
+ } catch (error) {
5942
+ if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
5943
+ return "ended";
5944
+ }
5945
+ return null;
5946
+ }
5947
+ }
5745
5948
  var POLL_INTERVAL_MS = 5e3;
5746
5949
  function LivoHostStudio({
5747
5950
  token,
@@ -5836,6 +6039,7 @@ function LivoHostStudio({
5836
6039
  );
5837
6040
  }
5838
6041
  var POLL_INTERVAL_MS2 = 5e3;
6042
+ var PREVIEW_POLL_INTERVAL_MS = 2e3;
5839
6043
  function LivoGuestStudio({
5840
6044
  token,
5841
6045
  apiUrl,
@@ -5847,6 +6051,7 @@ function LivoGuestStudio({
5847
6051
  }) {
5848
6052
  const reducedMotion = usePrefersReducedMotion();
5849
6053
  const [session, setSession] = useState(null);
6054
+ const [status, setStatus] = useState(null);
5850
6055
  const [error, setError] = useState(null);
5851
6056
  const [displayName, setDisplayName] = useState("");
5852
6057
  const [guestId, setGuestId] = useState("");
@@ -5896,6 +6101,7 @@ function LivoGuestStudio({
5896
6101
  );
5897
6102
  setWaitingForHost(false);
5898
6103
  setRetryAfterMs(null);
6104
+ setStatus(null);
5899
6105
  setSession(next);
5900
6106
  } catch (err) {
5901
6107
  const code = studioErrorCode(err);
@@ -5943,6 +6149,31 @@ function LivoGuestStudio({
5943
6149
  window.clearInterval(id);
5944
6150
  };
5945
6151
  }, [apiUrl, join, lobbyLabels.endedTitle, token, waitingForHost]);
6152
+ useEffect(() => {
6153
+ if (!session) return;
6154
+ const current = status ?? session.stream.status;
6155
+ if (current === "ended" || current === "error") return;
6156
+ let cancelled = false;
6157
+ const poll = async () => {
6158
+ const next = await getPublicStreamStatus(session.stream.id, apiUrl);
6159
+ if (!cancelled && next) setStatus(next);
6160
+ };
6161
+ void poll();
6162
+ const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
6163
+ const id = window.setInterval(() => void poll(), intervalMs);
6164
+ return () => {
6165
+ cancelled = true;
6166
+ window.clearInterval(id);
6167
+ };
6168
+ }, [apiUrl, session, status]);
6169
+ const liveSession = useMemo(() => {
6170
+ if (!session) return null;
6171
+ if (!status) return session;
6172
+ return {
6173
+ ...session,
6174
+ stream: { ...session.stream, status }
6175
+ };
6176
+ }, [session, status]);
5946
6177
  useEffect(() => {
5947
6178
  if (retryAfterMs == null) return;
5948
6179
  const id = window.setTimeout(() => {
@@ -5955,7 +6186,7 @@ function LivoGuestStudio({
5955
6186
  if (meetingError) {
5956
6187
  return /* @__PURE__ */ jsx("p", { className: "text-destructive p-6", role: "alert", children: meetingError });
5957
6188
  }
5958
- if (!session || !meeting) {
6189
+ if (!liveSession || !meeting) {
5959
6190
  return /* @__PURE__ */ jsx(
5960
6191
  StudioGuestLobby,
5961
6192
  {
@@ -5980,7 +6211,7 @@ function LivoGuestStudio({
5980
6211
  return /* @__PURE__ */ jsx(
5981
6212
  StudioRoom,
5982
6213
  {
5983
- session,
6214
+ session: liveSession,
5984
6215
  meeting,
5985
6216
  control: {
5986
6217
  livestream: async () => "starting",
@@ -5989,6 +6220,7 @@ function LivoGuestStudio({
5989
6220
  },
5990
6221
  onRejoin: () => {
5991
6222
  setSession(null);
6223
+ setStatus(null);
5992
6224
  setSessionGen((n) => n + 1);
5993
6225
  setJoining(false);
5994
6226
  setWaitingForHost(false);
@@ -6135,6 +6367,8 @@ function studioRoomLabels(t) {
6135
6367
  admitAllAudience: t("admitAllAudience"),
6136
6368
  askToGoOnAir: t("askToGoOnAir"),
6137
6369
  stageRequested: t("stageRequested"),
6370
+ stageApproved: t("stageApproved"),
6371
+ stageDenied: t("stageDenied"),
6138
6372
  stageRequests: t("stageRequests"),
6139
6373
  people: t("people"),
6140
6374
  panelists: t("panelists"),
@@ -6172,6 +6406,6 @@ function studioRoomLabels(t) {
6172
6406
  };
6173
6407
  }
6174
6408
 
6175
- export { LivoGuestStudio, LivoHostStudio, StudioApiError, StudioControlClient, StudioControlError, StudioGuestLobby, StudioRoom, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, applyEmbedTheme, controlActionsFromClient, createStudioControlClient, decodeEmbedTheme, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
6409
+ export { LivoGuestStudio, LivoHostStudio, StudioApiError, StudioControlClient, StudioControlError, StudioGuestLobby, StudioRoom, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, applyEmbedTheme, controlActionsFromClient, createStudioControlClient, decodeEmbedTheme, getPublicStreamStatus, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
6176
6410
  //# sourceMappingURL=index.js.map
6177
6411
  //# sourceMappingURL=index.js.map