@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.
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { createContext, useContext, useState, useRef, useEffect, useCallback, useMemo } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- import { Loader2, Share2, MicOff, Mic, Video, VideoOff, Settings, X, Link2, EyeOff, Eye, Users, MonitorUp, MessageSquare, Maximize, PictureInPicture2, PhoneOff, Undo2, Volume2, XIcon, Pin, MessageSquareOff, MoreVertical } from 'lucide-react';
4
+ import { Loader2, Share2, MicOff, Mic, Video, VideoOff, Settings, X, Link2, EyeOff, Eye, MonitorUp, MessageSquare, Users, Maximize, PictureInPicture2, PhoneOff, Undo2, Volume2, XIcon, Pin, MessageSquareOff, MoreVertical } from 'lucide-react';
5
5
  import { toast } from 'sonner';
6
6
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
7
7
  import { clsx } from 'clsx';
@@ -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-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",
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-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",
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
  }
@@ -2585,7 +2685,7 @@ function StudioSidePanel({
2585
2685
  const panelists = participants.filter((row) => isOnStage(row.stageStatus));
2586
2686
  const audience = participants.filter((row) => !isOnStage(row.stageStatus));
2587
2687
  return /* @__PURE__ */ jsxs("aside", { className: "bg-background flex min-h-0 w-full flex-1 flex-col border-t md:w-80 md:flex-none md:border-l md:border-t-0", children: [
2588
- /* @__PURE__ */ jsxs("div", { className: "flex border-b", children: [
2688
+ /* @__PURE__ */ jsxs("div", { className: "flex", children: [
2589
2689
  /* @__PURE__ */ jsxs(
2590
2690
  Button,
2591
2691
  {
@@ -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"
@@ -3239,14 +3341,24 @@ function StudioToolbar({
3239
3341
  children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
3240
3342
  }
3241
3343
  ) : null,
3242
- session.role === "guest" && stageEnabled && selfStageStatus !== "ON_STAGE" ? /* @__PURE__ */ jsx(
3344
+ session.role === "guest" && stageEnabled ? /* @__PURE__ */ jsx(
3243
3345
  ControlIcon,
3244
3346
  {
3245
- label: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
3347
+ label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
3246
3348
  pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
3247
- disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
3248
- onClick: onAskStage,
3249
- children: /* @__PURE__ */ jsx(Users, {})
3349
+ disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
3350
+ onClick: () => {
3351
+ if (selfStageStatus === "ON_STAGE") {
3352
+ if (!canLeaveStage) {
3353
+ toast.message(labels.offAirLastVisible);
3354
+ return;
3355
+ }
3356
+ onLeaveStage();
3357
+ return;
3358
+ }
3359
+ onAskStage();
3360
+ },
3361
+ children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
3250
3362
  }
3251
3363
  ) : null,
3252
3364
  showScreenControl ? canShareFreely ? /* @__PURE__ */ jsx(
@@ -3421,6 +3533,12 @@ function StudioRoom({
3421
3533
  const seenWaitlisted = useRef(/* @__PURE__ */ new Set());
3422
3534
  const knockPrimed = useRef(false);
3423
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);
3424
3542
  const grantCheckTimers = useRef([]);
3425
3543
  const screenNoticePrimed = useRef(false);
3426
3544
  const lastSelfMedia = useRef({
@@ -3449,6 +3567,9 @@ function StudioRoom({
3449
3567
  const rejected = isStudioRejected(roomState);
3450
3568
  useBeforeUnload(active && !sessionOver);
3451
3569
  useWakeLock(active && !sessionOver);
3570
+ useEffect(() => {
3571
+ unlockStudioSounds();
3572
+ }, []);
3452
3573
  useEffect(() => {
3453
3574
  if (onAir) {
3454
3575
  setPublishing(false);
@@ -3674,9 +3795,57 @@ function StudioRoom({
3674
3795
  const incoming = chatMessages.filter((row) => row.userId !== selfUserId);
3675
3796
  const unread = sideTab === "chat" ? 0 : Math.max(0, incoming.length - seenIncoming);
3676
3797
  const peopleBadge = waitlisted.length + stageRequests.length;
3798
+ const selfStageStatus = self?.stageStatus ?? meeting.stageStatus();
3677
3799
  useEffect(() => {
3678
3800
  if (sideTab === "chat") setSeenIncoming(incoming.length);
3679
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]);
3680
3849
  useEffect(() => {
3681
3850
  if (session.role === "moderator" && waitlisted.length > 0 && !peopleAutoOpened.current) {
3682
3851
  peopleAutoOpened.current = true;
@@ -3693,7 +3862,10 @@ function StudioRoom({
3693
3862
  if (seenWaitlisted.current.has(guest.id)) continue;
3694
3863
  seenWaitlisted.current.add(guest.id);
3695
3864
  toast.message(labels.knock.replace("{name}", guest.name));
3696
- if (session.role === "moderator") setSideTab("people");
3865
+ if (session.role === "moderator") {
3866
+ playStudioSound("request");
3867
+ setSideTab("people");
3868
+ }
3697
3869
  }
3698
3870
  }, [labels.knock, session.role, waitlisted]);
3699
3871
  useEffect(() => {
@@ -3730,6 +3902,7 @@ function StudioRoom({
3730
3902
  }
3731
3903
  if (message.type === "share-request") {
3732
3904
  if (session.role !== "moderator") return;
3905
+ playStudioSound("request");
3733
3906
  toast.message(labels.shareRequestFrom.replace("{name}", message.name), {
3734
3907
  id: `share-request:${message.participantId}`,
3735
3908
  action: {
@@ -4105,10 +4278,13 @@ function StudioRoom({
4105
4278
  size: "icon-sm",
4106
4279
  "aria-label": labels.share,
4107
4280
  onClick: () => {
4108
- void navigator.clipboard.writeText(
4109
- playerWatchUrl(session.stream.id, playerOrigin)
4110
- );
4111
- 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
+ });
4112
4288
  },
4113
4289
  children: /* @__PURE__ */ jsx(Share2, {})
4114
4290
  }
@@ -4441,6 +4617,7 @@ function StudioDevicePreflight({
4441
4617
  setMicOk(Boolean(audio));
4442
4618
  setAudioTrack(audio);
4443
4619
  if (videoRef.current) {
4620
+ videoRef.current.setAttribute("webkit-playsinline", "");
4444
4621
  videoRef.current.srcObject = video ? next : null;
4445
4622
  }
4446
4623
  bindEnded(next, gen);
@@ -4470,7 +4647,7 @@ function StudioDevicePreflight({
4470
4647
  autoPlay: true,
4471
4648
  muted: true,
4472
4649
  playsInline: true,
4473
- 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"
4474
4651
  }
4475
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 }) }),
4476
4653
  /* @__PURE__ */ jsx(
@@ -4731,44 +4908,42 @@ function screenShareTrack(meeting, participantId) {
4731
4908
  }
4732
4909
  return findParticipant(meeting, participantId)?.screenShareTracks?.video ?? null;
4733
4910
  }
4734
- var pendingAudioPlay = /* @__PURE__ */ new Set();
4735
- function replayPendingAudio() {
4736
- for (const el of pendingAudioPlay) {
4737
- void el.play().then(
4738
- () => {
4739
- pendingAudioPlay.delete(el);
4740
- },
4741
- () => void 0
4742
- );
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);
4743
4926
  }
4744
4927
  }
4745
4928
  if (typeof document !== "undefined") {
4746
- document.addEventListener("pointerdown", replayPendingAudio, {
4929
+ document.addEventListener("pointerdown", replayPendingMedia, {
4747
4930
  passive: true
4748
4931
  });
4749
4932
  }
4750
4933
  function bindTrackElement(el, track, kind) {
4751
4934
  if (!track) {
4752
4935
  el.srcObject = null;
4753
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4936
+ pendingMediaPlay.delete(el);
4754
4937
  return;
4755
4938
  }
4756
4939
  const current = el.srcObject;
4757
4940
  const currentTrack = current instanceof MediaStream ? kind === "video" ? current.getVideoTracks()[0] : current.getAudioTracks()[0] : void 0;
4758
4941
  if (currentTrack?.id === track.id) {
4942
+ if (el.paused) tryPlay(el);
4759
4943
  return;
4760
4944
  }
4761
4945
  el.srcObject = new MediaStream([track]);
4762
- if (typeof el.play === "function") {
4763
- void el.play().then(
4764
- () => {
4765
- if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
4766
- },
4767
- () => {
4768
- if (el instanceof HTMLAudioElement) pendingAudioPlay.add(el);
4769
- }
4770
- );
4771
- }
4946
+ tryPlay(el);
4772
4947
  }
4773
4948
  function roomLeftState(payload, fallback) {
4774
4949
  if (typeof payload === "string" && payload) return payload;
@@ -5255,7 +5430,15 @@ function studioMeetingFromRtk(raw) {
5255
5430
  notifyStage();
5256
5431
  return;
5257
5432
  }
5258
- await meeting.stage?.join?.();
5433
+ try {
5434
+ await meeting.stage?.join?.();
5435
+ } catch {
5436
+ const userId = meeting.self?.userId;
5437
+ if (userId) {
5438
+ await meeting.stage?.grantAccess?.([userId]);
5439
+ }
5440
+ await meeting.stage?.join?.();
5441
+ }
5259
5442
  joinedStageLocally = true;
5260
5443
  await republishCameraIfOn();
5261
5444
  notifyStage();
@@ -5545,7 +5728,11 @@ function useRealtimeKitMeeting(authToken, generation = 0) {
5545
5728
  const [meeting, setMeeting] = useState(null);
5546
5729
  const [error, setError] = useState(null);
5547
5730
  useEffect(() => {
5548
- if (!authToken) return;
5731
+ if (!authToken) {
5732
+ setMeeting(null);
5733
+ setError(null);
5734
+ return;
5735
+ }
5549
5736
  let cancelled = false;
5550
5737
  let handle = null;
5551
5738
  setError(null);
@@ -5720,6 +5907,21 @@ async function getStudioJoinStatus(token, apiUrl = defaultApiUrl(), fetchFn = fe
5720
5907
  throw error;
5721
5908
  }
5722
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
+ }
5723
5925
  var POLL_INTERVAL_MS = 5e3;
5724
5926
  function LivoHostStudio({
5725
5927
  token,
@@ -5814,6 +6016,7 @@ function LivoHostStudio({
5814
6016
  );
5815
6017
  }
5816
6018
  var POLL_INTERVAL_MS2 = 5e3;
6019
+ var PREVIEW_POLL_INTERVAL_MS = 2e3;
5817
6020
  function LivoGuestStudio({
5818
6021
  token,
5819
6022
  apiUrl,
@@ -5825,6 +6028,7 @@ function LivoGuestStudio({
5825
6028
  }) {
5826
6029
  const reducedMotion = usePrefersReducedMotion();
5827
6030
  const [session, setSession] = useState(null);
6031
+ const [status, setStatus] = useState(null);
5828
6032
  const [error, setError] = useState(null);
5829
6033
  const [displayName, setDisplayName] = useState("");
5830
6034
  const [guestId, setGuestId] = useState("");
@@ -5874,6 +6078,7 @@ function LivoGuestStudio({
5874
6078
  );
5875
6079
  setWaitingForHost(false);
5876
6080
  setRetryAfterMs(null);
6081
+ setStatus(null);
5877
6082
  setSession(next);
5878
6083
  } catch (err) {
5879
6084
  const code = studioErrorCode(err);
@@ -5890,9 +6095,9 @@ function LivoGuestStudio({
5890
6095
  setError(err instanceof Error ? err.message : lobbyLabels.joinError);
5891
6096
  setWaitingForHost(false);
5892
6097
  }
5893
- setJoining(false);
5894
6098
  } finally {
5895
6099
  joinInFlight.current = false;
6100
+ setJoining(false);
5896
6101
  }
5897
6102
  }, [apiUrl, displayName, guestId, lobbyLabels, token]);
5898
6103
  useEffect(() => {
@@ -5921,6 +6126,31 @@ function LivoGuestStudio({
5921
6126
  window.clearInterval(id);
5922
6127
  };
5923
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]);
5924
6154
  useEffect(() => {
5925
6155
  if (retryAfterMs == null) return;
5926
6156
  const id = window.setTimeout(() => {
@@ -5933,7 +6163,7 @@ function LivoGuestStudio({
5933
6163
  if (meetingError) {
5934
6164
  return /* @__PURE__ */ jsx("p", { className: "text-destructive p-6", role: "alert", children: meetingError });
5935
6165
  }
5936
- if (!session || !meeting) {
6166
+ if (!liveSession || !meeting) {
5937
6167
  return /* @__PURE__ */ jsx(
5938
6168
  StudioGuestLobby,
5939
6169
  {
@@ -5958,7 +6188,7 @@ function LivoGuestStudio({
5958
6188
  return /* @__PURE__ */ jsx(
5959
6189
  StudioRoom,
5960
6190
  {
5961
- session,
6191
+ session: liveSession,
5962
6192
  meeting,
5963
6193
  control: {
5964
6194
  livestream: async () => "starting",
@@ -5967,7 +6197,12 @@ function LivoGuestStudio({
5967
6197
  },
5968
6198
  onRejoin: () => {
5969
6199
  setSession(null);
6200
+ setStatus(null);
5970
6201
  setSessionGen((n) => n + 1);
6202
+ setJoining(false);
6203
+ setWaitingForHost(false);
6204
+ setError(null);
6205
+ joinInFlight.current = false;
5971
6206
  },
5972
6207
  reducedMotion,
5973
6208
  labels,
@@ -6109,6 +6344,8 @@ function studioRoomLabels(t) {
6109
6344
  admitAllAudience: t("admitAllAudience"),
6110
6345
  askToGoOnAir: t("askToGoOnAir"),
6111
6346
  stageRequested: t("stageRequested"),
6347
+ stageApproved: t("stageApproved"),
6348
+ stageDenied: t("stageDenied"),
6112
6349
  stageRequests: t("stageRequests"),
6113
6350
  people: t("people"),
6114
6351
  panelists: t("panelists"),
@@ -6146,6 +6383,6 @@ function studioRoomLabels(t) {
6146
6383
  };
6147
6384
  }
6148
6385
 
6149
- 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 };
6150
6387
  //# sourceMappingURL=index.js.map
6151
6388
  //# sourceMappingURL=index.js.map