@livo-tv/sdk 1.2.1 → 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.
@@ -341,6 +341,8 @@ declare function studioRoomLabels(t: (key: string, values?: Record<string, strin
341
341
  admitAllAudience: string;
342
342
  askToGoOnAir: string;
343
343
  stageRequested: string;
344
+ stageApproved: string;
345
+ stageDenied: string;
344
346
  stageRequests: string;
345
347
  people: string;
346
348
  panelists: string;
@@ -515,5 +517,6 @@ declare function getStudioJoinStatus(token: string, apiUrl?: string, fetchFn?: t
515
517
  ready: boolean;
516
518
  streamStatus: string | null;
517
519
  } | "ended">;
520
+ declare function getPublicStreamStatus(streamId: string, apiUrl?: string, fetchFn?: typeof fetch): Promise<string | null>;
518
521
 
519
- export { LivoGuestStudio, LivoHostStudio, StudioApiError, type StudioControlActions, StudioControlClient, type StudioControlClientOptions, StudioControlError, StudioGuestLobby, type StudioLobbyLabels, type StudioMeetingHandle, type StudioRole, StudioRoom, type StudioRoomLabels, type StudioSession, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, controlActionsFromClient, createStudioControlClient, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
522
+ export { LivoGuestStudio, LivoHostStudio, StudioApiError, type StudioControlActions, StudioControlClient, type StudioControlClientOptions, StudioControlError, StudioGuestLobby, type StudioLobbyLabels, type StudioMeetingHandle, type StudioRole, StudioRoom, type StudioRoomLabels, type StudioSession, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, controlActionsFromClient, createStudioControlClient, getPublicStreamStatus, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
@@ -341,6 +341,8 @@ declare function studioRoomLabels(t: (key: string, values?: Record<string, strin
341
341
  admitAllAudience: string;
342
342
  askToGoOnAir: string;
343
343
  stageRequested: string;
344
+ stageApproved: string;
345
+ stageDenied: string;
344
346
  stageRequests: string;
345
347
  people: string;
346
348
  panelists: string;
@@ -515,5 +517,6 @@ declare function getStudioJoinStatus(token: string, apiUrl?: string, fetchFn?: t
515
517
  ready: boolean;
516
518
  streamStatus: string | null;
517
519
  } | "ended">;
520
+ declare function getPublicStreamStatus(streamId: string, apiUrl?: string, fetchFn?: typeof fetch): Promise<string | null>;
518
521
 
519
- export { LivoGuestStudio, LivoHostStudio, StudioApiError, type StudioControlActions, StudioControlClient, type StudioControlClientOptions, StudioControlError, StudioGuestLobby, type StudioLobbyLabels, type StudioMeetingHandle, type StudioRole, StudioRoom, type StudioRoomLabels, type StudioSession, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, controlActionsFromClient, createStudioControlClient, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
522
+ export { LivoGuestStudio, LivoHostStudio, StudioApiError, type StudioControlActions, StudioControlClient, type StudioControlClientOptions, StudioControlError, StudioGuestLobby, type StudioLobbyLabels, type StudioMeetingHandle, type StudioRole, StudioRoom, type StudioRoomLabels, type StudioSession, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, controlActionsFromClient, createStudioControlClient, getPublicStreamStatus, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
@@ -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
  }
@@ -2188,7 +2281,7 @@ function BubbleGroup({ className, ...props }) {
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-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",
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 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 w-fit max-w-full flex-col gap-1" : "flex w-fit max-w-full flex-col gap-1",
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(
@@ -4741,44 +4908,42 @@ function screenShareTrack(meeting, participantId) {
4741
4908
  }
4742
4909
  return findParticipant(meeting, participantId)?.screenShareTracks?.video ?? null;
4743
4910
  }
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
- );
4911
+ var pendingMediaPlay = /* @__PURE__ */ new Set();
4912
+ function tryPlay(el) {
4913
+ if (typeof el.play !== "function") return;
4914
+ void el.play().then(
4915
+ () => {
4916
+ pendingMediaPlay.delete(el);
4917
+ },
4918
+ () => {
4919
+ pendingMediaPlay.add(el);
4920
+ }
4921
+ );
4922
+ }
4923
+ function replayPendingMedia() {
4924
+ for (const el of pendingMediaPlay) {
4925
+ tryPlay(el);
4753
4926
  }
4754
4927
  }
4755
4928
  if (typeof document !== "undefined") {
4756
- document.addEventListener("pointerdown", replayPendingAudio, {
4929
+ document.addEventListener("pointerdown", replayPendingMedia, {
4757
4930
  passive: true
4758
4931
  });
4759
4932
  }
4760
4933
  function bindTrackElement(el, track, kind) {
4761
4934
  if (!track) {
4762
4935
  el.srcObject = null;
4763
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4936
+ pendingMediaPlay.delete(el);
4764
4937
  return;
4765
4938
  }
4766
4939
  const current = el.srcObject;
4767
4940
  const currentTrack = current instanceof MediaStream ? kind === "video" ? current.getVideoTracks()[0] : current.getAudioTracks()[0] : void 0;
4768
4941
  if (currentTrack?.id === track.id) {
4942
+ if (el.paused) tryPlay(el);
4769
4943
  return;
4770
4944
  }
4771
4945
  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
- }
4946
+ tryPlay(el);
4782
4947
  }
4783
4948
  function roomLeftState(payload, fallback) {
4784
4949
  if (typeof payload === "string" && payload) return payload;
@@ -5742,6 +5907,21 @@ async function getStudioJoinStatus(token, apiUrl = defaultApiUrl(), fetchFn = fe
5742
5907
  throw error;
5743
5908
  }
5744
5909
  }
5910
+ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
5911
+ try {
5912
+ const body = await publicJson(
5913
+ `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
5914
+ {},
5915
+ fetchFn
5916
+ );
5917
+ return parsePublicStreamStatus(body);
5918
+ } catch (error) {
5919
+ if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
5920
+ return "ended";
5921
+ }
5922
+ return null;
5923
+ }
5924
+ }
5745
5925
  var POLL_INTERVAL_MS = 5e3;
5746
5926
  function LivoHostStudio({
5747
5927
  token,
@@ -5836,6 +6016,7 @@ function LivoHostStudio({
5836
6016
  );
5837
6017
  }
5838
6018
  var POLL_INTERVAL_MS2 = 5e3;
6019
+ var PREVIEW_POLL_INTERVAL_MS = 2e3;
5839
6020
  function LivoGuestStudio({
5840
6021
  token,
5841
6022
  apiUrl,
@@ -5847,6 +6028,7 @@ function LivoGuestStudio({
5847
6028
  }) {
5848
6029
  const reducedMotion = usePrefersReducedMotion();
5849
6030
  const [session, setSession] = useState(null);
6031
+ const [status, setStatus] = useState(null);
5850
6032
  const [error, setError] = useState(null);
5851
6033
  const [displayName, setDisplayName] = useState("");
5852
6034
  const [guestId, setGuestId] = useState("");
@@ -5896,6 +6078,7 @@ function LivoGuestStudio({
5896
6078
  );
5897
6079
  setWaitingForHost(false);
5898
6080
  setRetryAfterMs(null);
6081
+ setStatus(null);
5899
6082
  setSession(next);
5900
6083
  } catch (err) {
5901
6084
  const code = studioErrorCode(err);
@@ -5943,6 +6126,31 @@ function LivoGuestStudio({
5943
6126
  window.clearInterval(id);
5944
6127
  };
5945
6128
  }, [apiUrl, join, lobbyLabels.endedTitle, token, waitingForHost]);
6129
+ useEffect(() => {
6130
+ if (!session) return;
6131
+ const current = status ?? session.stream.status;
6132
+ if (current === "ended" || current === "error") return;
6133
+ let cancelled = false;
6134
+ const poll = async () => {
6135
+ const next = await getPublicStreamStatus(session.stream.id, apiUrl);
6136
+ if (!cancelled && next) setStatus(next);
6137
+ };
6138
+ void poll();
6139
+ const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
6140
+ const id = window.setInterval(() => void poll(), intervalMs);
6141
+ return () => {
6142
+ cancelled = true;
6143
+ window.clearInterval(id);
6144
+ };
6145
+ }, [apiUrl, session, status]);
6146
+ const liveSession = useMemo(() => {
6147
+ if (!session) return null;
6148
+ if (!status) return session;
6149
+ return {
6150
+ ...session,
6151
+ stream: { ...session.stream, status }
6152
+ };
6153
+ }, [session, status]);
5946
6154
  useEffect(() => {
5947
6155
  if (retryAfterMs == null) return;
5948
6156
  const id = window.setTimeout(() => {
@@ -5955,7 +6163,7 @@ function LivoGuestStudio({
5955
6163
  if (meetingError) {
5956
6164
  return /* @__PURE__ */ jsx("p", { className: "text-destructive p-6", role: "alert", children: meetingError });
5957
6165
  }
5958
- if (!session || !meeting) {
6166
+ if (!liveSession || !meeting) {
5959
6167
  return /* @__PURE__ */ jsx(
5960
6168
  StudioGuestLobby,
5961
6169
  {
@@ -5980,7 +6188,7 @@ function LivoGuestStudio({
5980
6188
  return /* @__PURE__ */ jsx(
5981
6189
  StudioRoom,
5982
6190
  {
5983
- session,
6191
+ session: liveSession,
5984
6192
  meeting,
5985
6193
  control: {
5986
6194
  livestream: async () => "starting",
@@ -5989,6 +6197,7 @@ function LivoGuestStudio({
5989
6197
  },
5990
6198
  onRejoin: () => {
5991
6199
  setSession(null);
6200
+ setStatus(null);
5992
6201
  setSessionGen((n) => n + 1);
5993
6202
  setJoining(false);
5994
6203
  setWaitingForHost(false);
@@ -6135,6 +6344,8 @@ function studioRoomLabels(t) {
6135
6344
  admitAllAudience: t("admitAllAudience"),
6136
6345
  askToGoOnAir: t("askToGoOnAir"),
6137
6346
  stageRequested: t("stageRequested"),
6347
+ stageApproved: t("stageApproved"),
6348
+ stageDenied: t("stageDenied"),
6138
6349
  stageRequests: t("stageRequests"),
6139
6350
  people: t("people"),
6140
6351
  panelists: t("panelists"),
@@ -6172,6 +6383,6 @@ function studioRoomLabels(t) {
6172
6383
  };
6173
6384
  }
6174
6385
 
6175
- export { LivoGuestStudio, LivoHostStudio, StudioApiError, StudioControlClient, StudioControlError, StudioGuestLobby, StudioRoom, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, applyEmbedTheme, controlActionsFromClient, createStudioControlClient, decodeEmbedTheme, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
6386
+ 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
6387
  //# sourceMappingURL=index.js.map
6177
6388
  //# sourceMappingURL=index.js.map