@livo-tv/sdk 1.4.0 → 1.6.0

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,8 @@
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, MonitorUp, MessageSquare, Users, 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, Captions, MoreVertical, Maximize, PictureInPicture2, MessageSquare, Users, PhoneOff, Undo2, Volume2, ChevronUp, XIcon, Pin, MessageSquareOff } from 'lucide-react';
5
+ import { CaptionOverlay, EMPTY_LIVE_CAPTION_PLAYOUT, liveCaptionsUrl, loadLiveCaptions, mergeLiveCaptionCues, advanceLiveCaptionPlayout } from '@livo-tv/blocks/video';
5
6
  import { toast } from 'sonner';
6
7
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
7
8
  import { clsx } from 'clsx';
@@ -518,52 +519,38 @@ function canTakeOffAir(tiles, participantId, onAir) {
518
519
  (tile) => tile.id !== participantId && isOnStage(tile.stageStatus)
519
520
  );
520
521
  }
522
+ function pipSpotlightFrom(tile, kind) {
523
+ return {
524
+ participantId: tile.id,
525
+ name: tile.name,
526
+ kind,
527
+ picture: tile.picture
528
+ };
529
+ }
521
530
  function resolveStudioPipSpotlight(tiles, activeSpeakerId) {
522
531
  const visible = tiles.filter((tile) => isOnStage(tile.stageStatus));
523
532
  const pinned = visible.find((tile) => tile.pinned === true);
524
533
  if (pinned) {
525
- return {
526
- participantId: pinned.id,
527
- name: pinned.name,
528
- kind: pinned.screenShareEnabled ? "screen" : pinned.videoEnabled ? "camera" : "idle",
529
- picture: pinned.picture
530
- };
534
+ return pipSpotlightFrom(
535
+ pinned,
536
+ pinned.screenShareEnabled ? "screen" : pinned.videoEnabled ? "camera" : "idle"
537
+ );
538
+ }
539
+ const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId) : void 0;
540
+ if (speaker) {
541
+ return pipSpotlightFrom(speaker, speaker.videoEnabled ? "camera" : "idle");
531
542
  }
532
543
  const sharing = visible.find((tile) => tile.screenShareEnabled);
533
544
  if (sharing) {
534
- return {
535
- participantId: sharing.id,
536
- name: sharing.name,
537
- kind: "screen",
538
- picture: sharing.picture
539
- };
540
- }
541
- const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId && tile.videoEnabled) : void 0;
542
- if (speaker) {
543
- return {
544
- participantId: speaker.id,
545
- name: speaker.name,
546
- kind: "camera",
547
- picture: speaker.picture
548
- };
545
+ return pipSpotlightFrom(sharing, "screen");
549
546
  }
550
547
  const remoteCam = visible.find((tile) => !tile.isSelf && tile.videoEnabled);
551
548
  if (remoteCam) {
552
- return {
553
- participantId: remoteCam.id,
554
- name: remoteCam.name,
555
- kind: "camera",
556
- picture: remoteCam.picture
557
- };
549
+ return pipSpotlightFrom(remoteCam, "camera");
558
550
  }
559
551
  const self = visible.find((tile) => tile.isSelf);
560
552
  if (!self) return null;
561
- return {
562
- participantId: self.id,
563
- name: self.name,
564
- kind: self.videoEnabled ? "camera" : "idle",
565
- picture: self.picture
566
- };
553
+ return pipSpotlightFrom(self, self.videoEnabled ? "camera" : "idle");
567
554
  }
568
555
  function resolveStudioPipSelfView(tiles, spotlight) {
569
556
  const self = tiles.find((tile) => tile.isSelf);
@@ -682,6 +669,9 @@ function screenShareBlock(captureSupported, permission) {
682
669
  if (permission === "NOT_ALLOWED") return "permission";
683
670
  return null;
684
671
  }
672
+ function showScreenShareControl(block) {
673
+ return block !== "permission";
674
+ }
685
675
 
686
676
  // src/studio/lib/parse.ts
687
677
  function isRecord(value) {
@@ -701,7 +691,14 @@ function parseStudioStreamSummary(value) {
701
691
  const status = readString(value, "status");
702
692
  if (!id || !title || !status) return null;
703
693
  const encoderLostAt = isRecord(value) ? typeof value.encoderLostAt === "number" ? value.encoderLostAt : value.encoderLostAt === null ? null : void 0 : void 0;
704
- return { id, title, status, encoderLostAt };
694
+ const captionsEnabled = isRecord(value) ? typeof value.captionsEnabled === "boolean" ? value.captionsEnabled : typeof value.captionsUrl === "string" && value.captionsUrl.length > 0 ? true : value.captionsUrl === null ? false : void 0 : void 0;
695
+ return {
696
+ id,
697
+ title,
698
+ status,
699
+ ...encoderLostAt !== void 0 ? { encoderLostAt } : {},
700
+ ...captionsEnabled !== void 0 ? { captionsEnabled } : {}
701
+ };
705
702
  }
706
703
  function parseStudioSession(value) {
707
704
  if (!isRecord(value)) return null;
@@ -713,7 +710,9 @@ function parseStudioSession(value) {
713
710
  const fromFull = isRecord(value.stream) ? parseStudioStreamSummary({
714
711
  id: value.stream.id,
715
712
  title: value.stream.title,
716
- status: value.stream.status
713
+ status: value.stream.status,
714
+ captionsEnabled: value.stream.captionsEnabled,
715
+ captionsUrl: value.stream.captionsUrl
717
716
  }) : null;
718
717
  const stream = fromSummary ?? fromFull;
719
718
  if (!stream) return null;
@@ -746,6 +745,14 @@ function parsePublicStreamStatus(value) {
746
745
  const status = readString(nested, "status");
747
746
  return status;
748
747
  }
748
+ function parsePublicStreamMeta(value) {
749
+ if (!isRecord(value)) return null;
750
+ const nested = isRecord(value.stream) ? value.stream : value;
751
+ const status = readString(nested, "status");
752
+ if (!status) return null;
753
+ const captionsEnabled = typeof nested.captionsEnabled === "boolean" ? nested.captionsEnabled : typeof nested.captionsUrl === "string" && nested.captionsUrl.length > 0;
754
+ return { status, captionsEnabled };
755
+ }
749
756
  function studioErrorCode(error) {
750
757
  if (!error || typeof error !== "object") return null;
751
758
  const body = "body" in error ? error.body : null;
@@ -1464,6 +1471,19 @@ function DialogHeader({ className, ...props }) {
1464
1471
  }
1465
1472
  );
1466
1473
  }
1474
+ function DialogFooter({ className, ...props }) {
1475
+ return /* @__PURE__ */ jsx(
1476
+ "div",
1477
+ {
1478
+ "data-slot": "dialog-footer",
1479
+ className: cn(
1480
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1481
+ className
1482
+ ),
1483
+ ...props
1484
+ }
1485
+ );
1486
+ }
1467
1487
  function DialogTitle({
1468
1488
  className,
1469
1489
  ...props
@@ -1612,6 +1632,211 @@ async function playSpeakerTest(deviceId) {
1612
1632
  await ctx3.close();
1613
1633
  }
1614
1634
  }
1635
+
1636
+ // src/studio/lib/caption-prefs.ts
1637
+ var STUDIO_CAPTION_LOCALES = ["en", "es", "pt"];
1638
+ var DEFAULT_STUDIO_CAPTION_PREFS = {
1639
+ locale: "en",
1640
+ size: "m"
1641
+ };
1642
+ function isStudioCaptionLocale(value) {
1643
+ return STUDIO_CAPTION_LOCALES.includes(value);
1644
+ }
1645
+ var STORAGE_KEY = "livo-studio-captions";
1646
+ function defaultStorage2() {
1647
+ if (typeof window === "undefined") return null;
1648
+ try {
1649
+ return window.localStorage;
1650
+ } catch {
1651
+ return null;
1652
+ }
1653
+ }
1654
+ function isCaptionSize(value) {
1655
+ return value === "s" || value === "m" || value === "l";
1656
+ }
1657
+ function captionSizeClass(size) {
1658
+ switch (size) {
1659
+ case "s":
1660
+ return "text-[length:clamp(10px,2.1cqh,0.75rem)]";
1661
+ case "m":
1662
+ return "text-[length:clamp(10px,2.6cqh,0.875rem)]";
1663
+ case "l":
1664
+ return "text-[length:clamp(12px,3.2cqh,1.05rem)]";
1665
+ default: {
1666
+ const _exhaustive = size;
1667
+ return _exhaustive;
1668
+ }
1669
+ }
1670
+ }
1671
+ function readStudioCaptionPrefs(storage = defaultStorage2(), fallbackLocale = "en") {
1672
+ const raw = storage?.getItem(STORAGE_KEY);
1673
+ if (!raw) {
1674
+ return { ...DEFAULT_STUDIO_CAPTION_PREFS, locale: fallbackLocale };
1675
+ }
1676
+ try {
1677
+ const parsed = JSON.parse(raw);
1678
+ const locale = typeof parsed.locale === "string" && isStudioCaptionLocale(parsed.locale) ? parsed.locale : fallbackLocale;
1679
+ const size = typeof parsed.size === "string" && isCaptionSize(parsed.size) ? parsed.size : "m";
1680
+ return { locale, size };
1681
+ } catch {
1682
+ return { ...DEFAULT_STUDIO_CAPTION_PREFS, locale: fallbackLocale };
1683
+ }
1684
+ }
1685
+ function persistStudioCaptionPrefs(prefs, storage = defaultStorage2()) {
1686
+ storage?.setItem(STORAGE_KEY, JSON.stringify(prefs));
1687
+ }
1688
+ function studioCaptionsBaseUrl(apiUrl, streamId) {
1689
+ if (!apiUrl) return void 0;
1690
+ return `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}/captions`;
1691
+ }
1692
+
1693
+ // src/studio/lib/video-effects.ts
1694
+ var DEFAULT_STUDIO_VIDEO_EFFECTS = {
1695
+ background: "none",
1696
+ blurStrength: 50,
1697
+ filter: "none"
1698
+ };
1699
+ var FILTER_CSS = {
1700
+ none: "",
1701
+ grayscale: "grayscale(1)",
1702
+ sepia: "sepia(0.85)",
1703
+ warm: "sepia(0.35) saturate(1.2) hue-rotate(-10deg)",
1704
+ cool: "saturate(0.9) hue-rotate(15deg) brightness(1.05)",
1705
+ bright: "brightness(1.15) contrast(1.08)"
1706
+ };
1707
+ function defaultStudioEffectBackgrounds(cdnBase = "https://hls.livo.tv") {
1708
+ const root = cdnBase.replace(/\/$/, "");
1709
+ return [
1710
+ {
1711
+ id: "office",
1712
+ url: `${root}/env/backgrounds/office.jpg`,
1713
+ label: "Office"
1714
+ },
1715
+ {
1716
+ id: "lounge",
1717
+ url: `${root}/env/backgrounds/lounge.jpg`,
1718
+ label: "Lounge"
1719
+ },
1720
+ {
1721
+ id: "studio",
1722
+ url: `${root}/env/backgrounds/studio.jpg`,
1723
+ label: "Studio"
1724
+ },
1725
+ { id: "sky", url: `${root}/env/backgrounds/sky.jpg`, label: "Sky" },
1726
+ {
1727
+ id: "forest",
1728
+ url: `${root}/env/backgrounds/forest.jpg`,
1729
+ label: "Forest"
1730
+ },
1731
+ {
1732
+ id: "abstract",
1733
+ url: `${root}/env/backgrounds/abstract.jpg`,
1734
+ label: "Abstract"
1735
+ }
1736
+ ];
1737
+ }
1738
+ function createFilterVideoMiddleware(filter) {
1739
+ const css = FILTER_CSS[filter];
1740
+ if (!css) return null;
1741
+ return async () => {
1742
+ const scratch = document.createElement("canvas");
1743
+ return async (canvas, ctx3) => {
1744
+ if (scratch.width !== canvas.width || scratch.height !== canvas.height) {
1745
+ scratch.width = canvas.width;
1746
+ scratch.height = canvas.height;
1747
+ }
1748
+ const copy = scratch.getContext("2d");
1749
+ if (!copy) return;
1750
+ copy.clearRect(0, 0, scratch.width, scratch.height);
1751
+ copy.drawImage(canvas, 0, 0);
1752
+ ctx3.save();
1753
+ ctx3.filter = css;
1754
+ ctx3.drawImage(scratch, 0, 0);
1755
+ ctx3.restore();
1756
+ };
1757
+ };
1758
+ }
1759
+ async function loadBackgroundTransformer(meeting) {
1760
+ const mod = await import('@cloudflare/realtimekit-virtual-background');
1761
+ const Init = mod.default ?? mod.RealtimeKitVideoBackgroundTransformer ?? null;
1762
+ if (!Init) return null;
1763
+ return Init.init({ meeting });
1764
+ }
1765
+ async function applyStudioVideoEffects(options) {
1766
+ const self = options.self;
1767
+ if (!self?.removeAllVideoMiddlewares || !self.addVideoMiddleware) return;
1768
+ await self.removeAllVideoMiddlewares();
1769
+ const needsSegmentation = options.config.background !== "none";
1770
+ if (needsSegmentation) {
1771
+ await self.setVideoMiddlewareGlobalConfig?.({
1772
+ disablePerFrameCanvasRendering: true
1773
+ });
1774
+ try {
1775
+ const transformer = await loadBackgroundTransformer(options.meeting);
1776
+ if (transformer && options.config.background === "blur") {
1777
+ const strength = Math.min(
1778
+ 100,
1779
+ Math.max(0, options.config.blurStrength)
1780
+ );
1781
+ await self.addVideoMiddleware(
1782
+ await transformer.createBackgroundBlurVideoMiddleware(strength)
1783
+ );
1784
+ } else if (transformer && options.config.background === "image" && options.config.imageUrl) {
1785
+ await self.addVideoMiddleware(
1786
+ await transformer.createStaticBackgroundVideoMiddleware(
1787
+ options.config.imageUrl
1788
+ )
1789
+ );
1790
+ }
1791
+ } catch {
1792
+ }
1793
+ }
1794
+ const filter = createFilterVideoMiddleware(options.config.filter);
1795
+ if (filter) await self.addVideoMiddleware(filter);
1796
+ }
1797
+
1798
+ // src/studio/lib/effects-prefs.ts
1799
+ var STORAGE_KEY2 = "livo-studio-video-effects";
1800
+ function defaultStorage3() {
1801
+ if (typeof window === "undefined") return null;
1802
+ try {
1803
+ return window.localStorage;
1804
+ } catch {
1805
+ return null;
1806
+ }
1807
+ }
1808
+ var BACKGROUNDS = ["none", "blur", "image"];
1809
+ var FILTERS = [
1810
+ "none",
1811
+ "grayscale",
1812
+ "sepia",
1813
+ "warm",
1814
+ "cool",
1815
+ "bright"
1816
+ ];
1817
+ function isBackground(value) {
1818
+ return BACKGROUNDS.includes(value);
1819
+ }
1820
+ function isFilter(value) {
1821
+ return FILTERS.includes(value);
1822
+ }
1823
+ function readStudioVideoEffects(storage = defaultStorage3()) {
1824
+ const raw = storage?.getItem(STORAGE_KEY2);
1825
+ if (!raw) return { ...DEFAULT_STUDIO_VIDEO_EFFECTS };
1826
+ try {
1827
+ const parsed = JSON.parse(raw);
1828
+ const background = typeof parsed.background === "string" && isBackground(parsed.background) ? parsed.background : "none";
1829
+ const filter = typeof parsed.filter === "string" && isFilter(parsed.filter) ? parsed.filter : "none";
1830
+ const blurStrength = typeof parsed.blurStrength === "number" && Number.isFinite(parsed.blurStrength) ? Math.min(100, Math.max(0, parsed.blurStrength)) : 50;
1831
+ const imageUrl = typeof parsed.imageUrl === "string" && parsed.imageUrl ? parsed.imageUrl : void 0;
1832
+ return { background, blurStrength, imageUrl, filter };
1833
+ } catch {
1834
+ return { ...DEFAULT_STUDIO_VIDEO_EFFECTS };
1835
+ }
1836
+ }
1837
+ function persistStudioVideoEffects(config, storage = defaultStorage3()) {
1838
+ storage?.setItem(STORAGE_KEY2, JSON.stringify(config));
1839
+ }
1615
1840
  function StudioDeviceSelectGroup({
1616
1841
  devices,
1617
1842
  selected,
@@ -1690,7 +1915,11 @@ function StudioSettingsDialog({
1690
1915
  meeting,
1691
1916
  labels,
1692
1917
  reducedMotion,
1693
- cameraOn
1918
+ cameraOn,
1919
+ captionPrefs,
1920
+ onCaptionPrefsChange,
1921
+ captionsAvailable,
1922
+ effectBackgrounds
1694
1923
  }) {
1695
1924
  const devices = useMediaDevices(open ? 1 : 0, open);
1696
1925
  const [saved, setSaved] = useState(
@@ -1702,11 +1931,16 @@ function StudioSettingsDialog({
1702
1931
  const [displayName, setDisplayName] = useState("");
1703
1932
  const [savedDisplayName, setSavedDisplayName] = useState("");
1704
1933
  const [savingDisplayName, setSavingDisplayName] = useState(false);
1934
+ const [tab, setTab] = useState("general");
1935
+ const [effects, setEffects] = useState(
1936
+ () => meeting.videoEffects()
1937
+ );
1705
1938
  const { effective } = resolveDeviceSelection(saved, devices);
1706
1939
  useEffect(() => {
1707
1940
  if (!open) return;
1708
1941
  setSaved(readStudioDeviceSelection());
1709
- }, [open]);
1942
+ setEffects(meeting.videoEffects());
1943
+ }, [meeting, open]);
1710
1944
  useEffect(() => {
1711
1945
  if (!open) return;
1712
1946
  const selfName = meeting.participants().find((row) => row.isSelf)?.name?.trim() ?? "";
@@ -1759,76 +1993,306 @@ function StudioSettingsDialog({
1759
1993
  setTestingSpeaker(false);
1760
1994
  }
1761
1995
  }
1762
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
1996
+ async function applyEffects(next) {
1997
+ setEffects(next);
1998
+ persistStudioVideoEffects(next);
1999
+ try {
2000
+ await meeting.setVideoEffects(next);
2001
+ } catch {
2002
+ }
2003
+ }
2004
+ const tabs = [
2005
+ { id: "general", label: labels.settingsGeneral },
2006
+ { id: "audio", label: labels.settingsAudio },
2007
+ { id: "video", label: labels.settingsVideo },
2008
+ { id: "effects", label: labels.settingsEffects },
2009
+ { id: "subtitles", label: labels.settingsSubtitles }
2010
+ ];
2011
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-2xl", children: [
1763
2012
  /* @__PURE__ */ jsxs(DialogHeader, { children: [
1764
2013
  /* @__PURE__ */ jsx(DialogTitle, { children: labels.settingsTitle }),
1765
2014
  /* @__PURE__ */ jsx(DialogDescription, { children: labels.settingsDescription })
1766
2015
  ] }),
1767
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1768
- /* @__PURE__ */ jsx(Label, { htmlFor: "studio-display-name", children: labels.guestName }),
1769
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2016
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 sm:flex-row", children: [
2017
+ /* @__PURE__ */ jsx(
2018
+ "div",
2019
+ {
2020
+ role: "tablist",
2021
+ "aria-label": labels.settings,
2022
+ className: "flex shrink-0 flex-row gap-1 overflow-x-auto sm:w-40 sm:flex-col",
2023
+ children: tabs.map((item) => /* @__PURE__ */ jsx(
2024
+ "button",
2025
+ {
2026
+ type: "button",
2027
+ role: "tab",
2028
+ "aria-selected": tab === item.id,
2029
+ className: cn(
2030
+ "rounded-md px-3 py-1.5 text-left text-sm",
2031
+ tab === item.id ? "bg-muted font-medium" : "text-muted-foreground hover:bg-muted/60"
2032
+ ),
2033
+ onClick: () => setTab(item.id),
2034
+ children: item.label
2035
+ },
2036
+ item.id
2037
+ ))
2038
+ }
2039
+ ),
2040
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-4", children: [
2041
+ tab === "general" ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2042
+ /* @__PURE__ */ jsx(Label, { htmlFor: "studio-display-name", children: labels.guestName }),
2043
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2044
+ /* @__PURE__ */ jsx(
2045
+ Input,
2046
+ {
2047
+ id: "studio-display-name",
2048
+ value: displayName,
2049
+ maxLength: STUDIO_DISPLAY_NAME_MAX,
2050
+ onChange: (event) => setDisplayName(event.target.value)
2051
+ }
2052
+ ),
2053
+ /* @__PURE__ */ jsx(
2054
+ Button,
2055
+ {
2056
+ type: "button",
2057
+ variant: "outline",
2058
+ disabled: displayNameSaveDisabled,
2059
+ onClick: () => void onSaveDisplayName(),
2060
+ children: labels.displayNameSave
2061
+ }
2062
+ )
2063
+ ] })
2064
+ ] }) : null,
2065
+ tab === "audio" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2066
+ /* @__PURE__ */ jsxs("div", { children: [
2067
+ /* @__PURE__ */ jsx("p", { className: "sr-only", children: labels.micLevel }),
2068
+ /* @__PURE__ */ jsx(
2069
+ AudioWaveform,
2070
+ {
2071
+ track: audioTrack,
2072
+ reducedMotion,
2073
+ className: "w-full"
2074
+ }
2075
+ )
2076
+ ] }),
2077
+ /* @__PURE__ */ jsx(
2078
+ StudioDeviceSelectGroup,
2079
+ {
2080
+ devices: { ...devices, video: [] },
2081
+ selected: effective,
2082
+ labels,
2083
+ onChange: (kind, id) => void onChange(kind, id),
2084
+ speakerAction: /* @__PURE__ */ jsx(
2085
+ Button,
2086
+ {
2087
+ type: "button",
2088
+ size: "sm",
2089
+ variant: "outline",
2090
+ disabled: testingSpeaker,
2091
+ onClick: () => void onTestSpeaker(),
2092
+ children: labels.testSpeaker
2093
+ }
2094
+ )
2095
+ }
2096
+ )
2097
+ ] }) : null,
2098
+ tab === "video" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2099
+ /* @__PURE__ */ jsx(
2100
+ SettingsCameraPreview,
2101
+ {
2102
+ track: videoTrack,
2103
+ open,
2104
+ cameraOn,
2105
+ cameraOffLabel: labels.cameraOff
2106
+ }
2107
+ ),
2108
+ /* @__PURE__ */ jsx(
2109
+ StudioDeviceSelectGroup,
2110
+ {
2111
+ devices: { ...devices, audio: [], speaker: [] },
2112
+ selected: effective,
2113
+ labels,
2114
+ onChange: (kind, id) => void onChange(kind, id)
2115
+ }
2116
+ )
2117
+ ] }) : null,
2118
+ tab === "effects" ? /* @__PURE__ */ jsx(
2119
+ EffectsPanel,
2120
+ {
2121
+ labels,
2122
+ effects,
2123
+ backgrounds: effectBackgrounds,
2124
+ onApply: (next) => void applyEffects(next)
2125
+ }
2126
+ ) : null,
2127
+ tab === "subtitles" ? /* @__PURE__ */ jsx(
2128
+ SubtitlesPanel,
2129
+ {
2130
+ labels,
2131
+ prefs: captionPrefs,
2132
+ available: captionsAvailable,
2133
+ onChange: (next) => {
2134
+ persistStudioCaptionPrefs(next);
2135
+ onCaptionPrefsChange(next);
2136
+ }
2137
+ }
2138
+ ) : null
2139
+ ] })
2140
+ ] })
2141
+ ] }) });
2142
+ }
2143
+ function EffectsPanel({
2144
+ labels,
2145
+ effects,
2146
+ backgrounds,
2147
+ onApply
2148
+ }) {
2149
+ const filters = [
2150
+ { id: "none", label: labels.effectFilterNone },
2151
+ { id: "grayscale", label: labels.effectFilterGrayscale },
2152
+ { id: "sepia", label: labels.effectFilterSepia },
2153
+ { id: "warm", label: labels.effectFilterWarm },
2154
+ { id: "cool", label: labels.effectFilterCool },
2155
+ { id: "bright", label: labels.effectFilterBright }
2156
+ ];
2157
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2158
+ /* @__PURE__ */ jsxs("div", { children: [
2159
+ /* @__PURE__ */ jsx("p", { className: "mb-2 text-sm font-medium", children: labels.settingsEffects }),
2160
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-2", children: [
1770
2161
  /* @__PURE__ */ jsx(
1771
- Input,
2162
+ EffectTile,
1772
2163
  {
1773
- id: "studio-display-name",
1774
- value: displayName,
1775
- maxLength: STUDIO_DISPLAY_NAME_MAX,
1776
- onChange: (event) => setDisplayName(event.target.value)
2164
+ label: labels.effectNone,
2165
+ active: effects.background === "none",
2166
+ onClick: () => onApply({ ...effects, background: "none" })
1777
2167
  }
1778
2168
  ),
1779
2169
  /* @__PURE__ */ jsx(
1780
- Button,
2170
+ EffectTile,
1781
2171
  {
1782
- type: "button",
1783
- variant: "outline",
1784
- disabled: displayNameSaveDisabled,
1785
- onClick: () => void onSaveDisplayName(),
1786
- children: labels.displayNameSave
2172
+ label: labels.effectBlur,
2173
+ active: effects.background === "blur" && effects.blurStrength <= 50,
2174
+ onClick: () => onApply({ ...effects, background: "blur", blurStrength: 40 })
2175
+ }
2176
+ ),
2177
+ /* @__PURE__ */ jsx(
2178
+ EffectTile,
2179
+ {
2180
+ label: labels.effectBlurStrong,
2181
+ active: effects.background === "blur" && effects.blurStrength > 50,
2182
+ onClick: () => onApply({ ...effects, background: "blur", blurStrength: 80 })
1787
2183
  }
1788
2184
  )
1789
2185
  ] })
1790
2186
  ] }),
1791
- /* @__PURE__ */ jsx(
1792
- SettingsCameraPreview,
1793
- {
1794
- track: videoTrack,
1795
- open,
1796
- cameraOn,
1797
- cameraOffLabel: labels.cameraOff
1798
- }
1799
- ),
2187
+ backgrounds.length > 0 ? /* @__PURE__ */ jsxs("div", { children: [
2188
+ /* @__PURE__ */ jsx("p", { className: "mb-2 text-sm font-medium", children: labels.effectBackgrounds }),
2189
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: backgrounds.map((bg) => /* @__PURE__ */ jsx(
2190
+ EffectTile,
2191
+ {
2192
+ label: bg.label,
2193
+ active: effects.background === "image" && effects.imageUrl === bg.url,
2194
+ previewUrl: bg.url,
2195
+ onClick: () => onApply({
2196
+ ...effects,
2197
+ background: "image",
2198
+ imageUrl: bg.url
2199
+ })
2200
+ },
2201
+ bg.id
2202
+ )) })
2203
+ ] }) : null,
1800
2204
  /* @__PURE__ */ jsxs("div", { children: [
1801
- /* @__PURE__ */ jsx("p", { className: "sr-only", children: labels.micLevel }),
1802
- /* @__PURE__ */ jsx(
1803
- AudioWaveform,
2205
+ /* @__PURE__ */ jsx("p", { className: "mb-2 text-sm font-medium", children: labels.effectFilters }),
2206
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: filters.map((filter) => /* @__PURE__ */ jsx(
2207
+ Button,
1804
2208
  {
1805
- track: audioTrack,
1806
- reducedMotion,
1807
- className: "w-full"
1808
- }
1809
- )
1810
- ] }),
1811
- /* @__PURE__ */ jsx(
1812
- StudioDeviceSelectGroup,
1813
- {
1814
- devices,
1815
- selected: effective,
1816
- labels,
1817
- onChange: (kind, id) => void onChange(kind, id),
1818
- speakerAction: /* @__PURE__ */ jsx(
1819
- Button,
2209
+ type: "button",
2210
+ size: "sm",
2211
+ variant: effects.filter === filter.id ? "default" : "outline",
2212
+ onClick: () => onApply({ ...effects, filter: filter.id }),
2213
+ children: filter.label
2214
+ },
2215
+ filter.id
2216
+ )) })
2217
+ ] })
2218
+ ] });
2219
+ }
2220
+ function EffectTile({
2221
+ label,
2222
+ active,
2223
+ previewUrl,
2224
+ onClick
2225
+ }) {
2226
+ return /* @__PURE__ */ jsxs(
2227
+ "button",
2228
+ {
2229
+ type: "button",
2230
+ "aria-pressed": active,
2231
+ onClick,
2232
+ className: cn(
2233
+ "overflow-hidden rounded-lg border text-left text-xs",
2234
+ active ? "ring-primary ring-2" : "border-border"
2235
+ ),
2236
+ children: [
2237
+ /* @__PURE__ */ jsx(
2238
+ "div",
1820
2239
  {
1821
- type: "button",
1822
- size: "sm",
1823
- variant: "outline",
1824
- disabled: testingSpeaker,
1825
- onClick: () => void onTestSpeaker(),
1826
- children: labels.testSpeaker
2240
+ className: "bg-muted aspect-video w-full bg-cover bg-center",
2241
+ style: previewUrl ? { backgroundImage: `url(${previewUrl})` } : void 0
1827
2242
  }
1828
- )
1829
- }
1830
- )
1831
- ] }) });
2243
+ ),
2244
+ /* @__PURE__ */ jsx("p", { className: "truncate px-2 py-1", children: label })
2245
+ ]
2246
+ }
2247
+ );
2248
+ }
2249
+ function SubtitlesPanel({
2250
+ labels,
2251
+ prefs,
2252
+ available,
2253
+ onChange
2254
+ }) {
2255
+ const localeLabel = {
2256
+ en: labels.localeEn,
2257
+ es: labels.localeEs,
2258
+ pt: labels.localePt
2259
+ };
2260
+ const sizes = [
2261
+ { id: "s", label: labels.captionsSizeS },
2262
+ { id: "m", label: labels.captionsSizeM },
2263
+ { id: "l", label: labels.captionsSizeL }
2264
+ ];
2265
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2266
+ available ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: labels.captionsDelayed }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: labels.captionsNotReady }),
2267
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2268
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: labels.captionsLanguage }),
2269
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: STUDIO_CAPTION_LOCALES.map((locale) => /* @__PURE__ */ jsx(
2270
+ Button,
2271
+ {
2272
+ type: "button",
2273
+ size: "sm",
2274
+ variant: prefs.locale === locale ? "default" : "outline",
2275
+ onClick: () => onChange({ ...prefs, locale }),
2276
+ children: localeLabel[locale]
2277
+ },
2278
+ locale
2279
+ )) })
2280
+ ] }),
2281
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2282
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: labels.captionsSize }),
2283
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: sizes.map((size) => /* @__PURE__ */ jsx(
2284
+ Button,
2285
+ {
2286
+ type: "button",
2287
+ size: "sm",
2288
+ variant: prefs.size === size.id ? "default" : "outline",
2289
+ onClick: () => onChange({ ...prefs, size: size.id }),
2290
+ children: size.label
2291
+ },
2292
+ size.id
2293
+ )) })
2294
+ ] })
2295
+ ] });
1832
2296
  }
1833
2297
  function SettingsCameraPreview({
1834
2298
  track,
@@ -1879,7 +2343,11 @@ function StudioGuestWaiting({
1879
2343
  onToggleCamera,
1880
2344
  onToggleMute,
1881
2345
  onOpenSettings,
1882
- onCloseSettings
2346
+ onCloseSettings,
2347
+ captionPrefs,
2348
+ onCaptionPrefsChange,
2349
+ captionsAvailable,
2350
+ effectBackgrounds
1883
2351
  }) {
1884
2352
  const [audioTrack, setAudioTrack] = useState(null);
1885
2353
  const [videoTrack, setVideoTrack] = useState(null);
@@ -1946,7 +2414,11 @@ function StudioGuestWaiting({
1946
2414
  meeting,
1947
2415
  labels,
1948
2416
  reducedMotion,
1949
- cameraOn
2417
+ cameraOn,
2418
+ captionPrefs,
2419
+ onCaptionPrefsChange,
2420
+ captionsAvailable,
2421
+ effectBackgrounds
1950
2422
  }
1951
2423
  )
1952
2424
  ] });
@@ -2023,6 +2495,19 @@ var badgeVariants = cva(
2023
2495
  function Badge({ className, variant, ...props }) {
2024
2496
  return /* @__PURE__ */ jsx("span", { className: cn(badgeVariants({ variant }), className), ...props });
2025
2497
  }
2498
+
2499
+ // src/studio/lib/studio-video-style.ts
2500
+ var STUDIO_VIDEO_MEDIA_STYLE = `
2501
+ .livo-studio-tile-video::-webkit-media-controls-start-playback-button,
2502
+ .livo-studio-inline-video::-webkit-media-controls-start-playback-button {
2503
+ display: none !important;
2504
+ -webkit-appearance: none;
2505
+ }
2506
+ .livo-studio-tile-video::-webkit-media-controls,
2507
+ .livo-studio-inline-video::-webkit-media-controls {
2508
+ display: none !important;
2509
+ }
2510
+ `;
2026
2511
  function StudioPipPanel({
2027
2512
  meeting,
2028
2513
  labels,
@@ -2077,7 +2562,7 @@ function StudioPipPanel({
2077
2562
  /* @__PURE__ */ jsx(StudioLiveBadge, { children: labels.live }),
2078
2563
  elapsed ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground tabular-nums text-xs", children: elapsed }) : null
2079
2564
  ] }) : null,
2080
- selfView ? /* @__PURE__ */ jsx(
2565
+ selfView && meeting.localVideoTrack() ? /* @__PURE__ */ jsx(
2081
2566
  "div",
2082
2567
  {
2083
2568
  "data-testid": "pip-self-view",
@@ -2134,7 +2619,8 @@ function PipSpotlight({
2134
2619
  spotlight,
2135
2620
  cameraOffLabel
2136
2621
  }) {
2137
- if (!spotlight || spotlight.kind === "idle") {
2622
+ const track = spotlight && spotlight.kind !== "idle" ? spotlight.kind === "screen" ? meeting.participantScreenTrack(spotlight.participantId) : meeting.participantVideoTrack(spotlight.participantId) : null;
2623
+ if (!spotlight || spotlight.kind === "idle" || !track) {
2138
2624
  return /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-2", children: [
2139
2625
  spotlight?.picture ? /* @__PURE__ */ jsx(
2140
2626
  "img",
@@ -2147,7 +2633,6 @@ function PipSpotlight({
2147
2633
  /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: spotlight?.name ?? cameraOffLabel })
2148
2634
  ] });
2149
2635
  }
2150
- const track = spotlight.kind === "screen" ? meeting.participantScreenTrack(spotlight.participantId) : meeting.participantVideoTrack(spotlight.participantId);
2151
2636
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2152
2637
  /* @__PURE__ */ jsx(
2153
2638
  PipTrackVideo,
@@ -2166,11 +2651,7 @@ function PipTrackVideo({
2166
2651
  const ref = useRef(null);
2167
2652
  useEffect(() => {
2168
2653
  const el = ref.current;
2169
- if (!el) return;
2170
- if (!track) {
2171
- el.srcObject = null;
2172
- return;
2173
- }
2654
+ if (!el || !track) return;
2174
2655
  el.setAttribute("webkit-playsinline", "");
2175
2656
  el.srcObject = new MediaStream([track]);
2176
2657
  void el.play().catch(() => void 0);
@@ -2178,19 +2659,24 @@ function PipTrackVideo({
2178
2659
  el.srcObject = null;
2179
2660
  };
2180
2661
  }, [track]);
2181
- return /* @__PURE__ */ jsx(
2182
- "video",
2183
- {
2184
- ref,
2185
- autoPlay: true,
2186
- playsInline: true,
2187
- muted: true,
2188
- className: cn(
2189
- "livo-studio-inline-video absolute inset-0 size-full",
2190
- fit === "contain" ? "object-contain" : "object-cover"
2191
- )
2192
- }
2193
- );
2662
+ if (!track) return null;
2663
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2664
+ /* @__PURE__ */ jsx("style", { children: STUDIO_VIDEO_MEDIA_STYLE }),
2665
+ /* @__PURE__ */ jsx(
2666
+ "video",
2667
+ {
2668
+ ref,
2669
+ autoPlay: true,
2670
+ playsInline: true,
2671
+ muted: true,
2672
+ disableRemotePlayback: true,
2673
+ className: cn(
2674
+ "livo-studio-inline-video absolute inset-0 size-full",
2675
+ fit === "contain" ? "object-contain" : "object-cover"
2676
+ )
2677
+ }
2678
+ )
2679
+ ] });
2194
2680
  }
2195
2681
  function pipScreenControls({
2196
2682
  labels,
@@ -3217,6 +3703,7 @@ function StudioTile({
3217
3703
  const onStage = isOnStage(tile.stageStatus);
3218
3704
  const [confirm, setConfirm] = useState(null);
3219
3705
  const showMenu = (canModerate || canManageStage) && !tile.isSelf;
3706
+ const hasVideoTrack = tileHasVideoTrack(meeting, tile);
3220
3707
  const glowRef = useSpeakingGlow({
3221
3708
  track: tile.kind === "screen" || !tile.audioEnabled ? null : meeting.participantAudioTrack(tile.id),
3222
3709
  enabled: onStage,
@@ -3232,23 +3719,14 @@ function StudioTile({
3232
3719
  compact ? "aspect-video w-40 shrink-0 md:w-full" : tile.kind === "screen" ? "min-h-0 flex-1 md:min-h-48" : fill ? "size-full min-h-0 md:aspect-video md:h-auto" : "aspect-video"
3233
3720
  ),
3234
3721
  children: [
3235
- tile.kind === "idle" ? /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3", children: [
3236
- tile.picture ? /* @__PURE__ */ jsx(
3237
- "img",
3238
- {
3239
- src: tile.picture,
3240
- alt: "",
3241
- className: "size-16 rounded-full object-cover"
3242
- }
3243
- ) : /* @__PURE__ */ jsx("div", { className: "bg-background flex size-16 items-center justify-center rounded-full text-xl", children: tile.name.slice(0, 1).toUpperCase() }),
3244
- /* @__PURE__ */ jsx(
3245
- AudioWaveform,
3246
- {
3247
- track: tile.isSelf ? meeting.localAudioTrack() : null,
3248
- reducedMotion
3249
- }
3250
- )
3251
- ] }) : /* @__PURE__ */ jsx(
3722
+ tile.kind === "idle" || !hasVideoTrack ? /* @__PURE__ */ jsx(
3723
+ IdlePersonTile,
3724
+ {
3725
+ tile,
3726
+ meeting,
3727
+ reducedMotion
3728
+ }
3729
+ ) : /* @__PURE__ */ jsx(
3252
3730
  StudioTileVideo,
3253
3731
  {
3254
3732
  meeting,
@@ -3347,6 +3825,37 @@ function TileStatusIcons({
3347
3825
  tile.chatCanSend === false ? /* @__PURE__ */ jsx(StatusIcon, { label: labels.muteChat, children: /* @__PURE__ */ jsx(MessageSquareOff, {}) }) : null
3348
3826
  ] });
3349
3827
  }
3828
+ function tileHasVideoTrack(meeting, tile) {
3829
+ if (tile.kind === "screen") {
3830
+ return meeting.participantScreenTrack(tile.id) != null;
3831
+ }
3832
+ if (tile.kind === "idle") return false;
3833
+ if (tile.isSelf) return meeting.localVideoTrack() != null;
3834
+ return meeting.participantVideoTrack(tile.id) != null;
3835
+ }
3836
+ function IdlePersonTile({
3837
+ tile,
3838
+ meeting,
3839
+ reducedMotion
3840
+ }) {
3841
+ return /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3", children: [
3842
+ tile.picture ? /* @__PURE__ */ jsx(
3843
+ "img",
3844
+ {
3845
+ src: tile.picture,
3846
+ alt: "",
3847
+ className: "size-16 rounded-full object-cover"
3848
+ }
3849
+ ) : /* @__PURE__ */ jsx("div", { className: "bg-background flex size-16 items-center justify-center rounded-full text-xl", children: tile.name.slice(0, 1).toUpperCase() }),
3850
+ /* @__PURE__ */ jsx(
3851
+ AudioWaveform,
3852
+ {
3853
+ track: tile.isSelf ? meeting.localAudioTrack() : null,
3854
+ reducedMotion
3855
+ }
3856
+ )
3857
+ ] });
3858
+ }
3350
3859
  function StatusIcon({
3351
3860
  label,
3352
3861
  children
@@ -3370,23 +3879,27 @@ function StudioTileVideo({
3370
3879
  meeting.attachVideo(participantId, el, kind);
3371
3880
  return () => meeting.attachVideo(participantId, null, kind);
3372
3881
  }, [kind, meeting, participantId]);
3373
- return /* @__PURE__ */ jsx(
3374
- "video",
3375
- {
3376
- ref,
3377
- autoPlay: true,
3378
- playsInline: true,
3379
- muted: true,
3380
- disablePictureInPicture: true,
3381
- controlsList: "nodownload noplaybackrate noremoteplayback",
3382
- className: cn(
3383
- "livo-studio-tile-video absolute inset-0 size-full",
3384
- "pointer-events-none",
3385
- contain ? "object-contain" : "object-cover",
3386
- mirror && "-scale-x-100"
3387
- )
3388
- }
3389
- );
3882
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3883
+ /* @__PURE__ */ jsx("style", { children: STUDIO_VIDEO_MEDIA_STYLE }),
3884
+ /* @__PURE__ */ jsx(
3885
+ "video",
3886
+ {
3887
+ ref,
3888
+ autoPlay: true,
3889
+ playsInline: true,
3890
+ muted: true,
3891
+ disablePictureInPicture: true,
3892
+ disableRemotePlayback: true,
3893
+ controlsList: "nodownload noplaybackrate noremoteplayback",
3894
+ className: cn(
3895
+ "livo-studio-tile-video absolute inset-0 size-full",
3896
+ "pointer-events-none",
3897
+ contain ? "object-contain" : "object-cover",
3898
+ mirror && "-scale-x-100"
3899
+ )
3900
+ }
3901
+ )
3902
+ ] });
3390
3903
  }
3391
3904
  function StudioStage({
3392
3905
  participants,
@@ -3478,6 +3991,32 @@ function StudioStage({
3478
3991
  }
3479
3992
  );
3480
3993
  }
3994
+ function StudioDownloadAppDialog({
3995
+ open,
3996
+ onOpenChange,
3997
+ downloadUrl,
3998
+ labels
3999
+ }) {
4000
+ const href = downloadUrl?.trim() ?? "";
4001
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { children: [
4002
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
4003
+ /* @__PURE__ */ jsx(DialogTitle, { children: labels.screenDownloadAppTitle }),
4004
+ /* @__PURE__ */ jsx(DialogDescription, { children: labels.screenUnsupportedDevice })
4005
+ ] }),
4006
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
4007
+ /* @__PURE__ */ jsx(
4008
+ Button,
4009
+ {
4010
+ type: "button",
4011
+ variant: "outline",
4012
+ onClick: () => onOpenChange(false),
4013
+ children: labels.screenDownloadAppDismiss
4014
+ }
4015
+ ),
4016
+ href ? /* @__PURE__ */ jsx(Button, { render: /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noreferrer" }), children: labels.screenDownloadAppAction }) : /* @__PURE__ */ jsx(Button, { type: "button", disabled: true, children: labels.screenDownloadAppAction })
4017
+ ] })
4018
+ ] }) });
4019
+ }
3481
4020
  function StudioToolbar({
3482
4021
  session,
3483
4022
  meeting,
@@ -3504,6 +4043,12 @@ function StudioToolbar({
3504
4043
  stageEnabled,
3505
4044
  canLeaveStage,
3506
4045
  reducedMotion,
4046
+ captionsOn,
4047
+ captionsAvailable,
4048
+ captionsReady,
4049
+ captionPrefs,
4050
+ onCaptionPrefsChange,
4051
+ effectBackgrounds,
3507
4052
  onToggleMute,
3508
4053
  onToggleCamera,
3509
4054
  onToggleScreen,
@@ -3517,204 +4062,360 @@ function StudioToolbar({
3517
4062
  onCloseSettings,
3518
4063
  onOpenPip,
3519
4064
  onClosePip,
4065
+ onToggleCaptions,
3520
4066
  onLeave
3521
4067
  }) {
3522
- const fullscreenSupported = document.fullscreenEnabled === true && typeof document.documentElement.requestFullscreen === "function";
4068
+ const devices = useMediaDevices(1, true);
4069
+ const saved = readStudioDeviceSelection();
4070
+ const { effective } = resolveDeviceSelection(saved, devices);
4071
+ const fullscreenSupported = typeof document !== "undefined" && document.fullscreenEnabled === true && typeof document.documentElement.requestFullscreen === "function";
3523
4072
  async function enterFullscreen() {
3524
4073
  try {
3525
4074
  await document.documentElement.requestFullscreen();
3526
4075
  } catch {
3527
4076
  }
3528
4077
  }
3529
- return /* @__PURE__ */ jsx("footer", { className: "flex flex-wrap items-center justify-center gap-2 border-t px-3 py-3", children: /* @__PURE__ */ jsxs(TooltipProvider, { children: [
3530
- showMediaControls ? /* @__PURE__ */ jsxs(Fragment, { children: [
4078
+ async function onDeviceChange(kind, deviceId) {
4079
+ persistStudioDeviceSelection(kind, deviceId);
4080
+ try {
4081
+ await meeting.setDevice(kind, deviceId);
4082
+ } catch {
4083
+ }
4084
+ }
4085
+ const showStageModerator = session.role === "moderator" && canManageStage;
4086
+ const showStageGuest = session.role === "guest" && stageEnabled;
4087
+ const captionsLabel = !captionsAvailable ? labels.captions : !captionsReady ? labels.captionsNotReady : captionsOn ? labels.captionsOn : labels.captionsOff;
4088
+ return /* @__PURE__ */ jsx("footer", { className: "flex flex-wrap items-center justify-center gap-3 border-t px-3 py-3 sm:justify-between", children: /* @__PURE__ */ jsxs(TooltipProvider, { children: [
4089
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
4090
+ showMediaControls ? /* @__PURE__ */ jsxs(Fragment, { children: [
4091
+ /* @__PURE__ */ jsx(
4092
+ MediaSplit,
4093
+ {
4094
+ label: muted ? labels.unmute : labels.mute,
4095
+ pressed: muted,
4096
+ tone: muted ? "danger" : "default",
4097
+ onClick: onToggleMute,
4098
+ pickerLabel: labels.micDevice,
4099
+ options: devices.audio,
4100
+ selected: effective.audio,
4101
+ onSelect: (id) => void onDeviceChange("audio", id),
4102
+ children: muted ? /* @__PURE__ */ jsx(MicOff, {}) : /* @__PURE__ */ jsx(Mic, {})
4103
+ }
4104
+ ),
4105
+ /* @__PURE__ */ jsx(
4106
+ MediaSplit,
4107
+ {
4108
+ label: labels.camera,
4109
+ pressed: cameraOn,
4110
+ onClick: onToggleCamera,
4111
+ pickerLabel: labels.cameraDevice,
4112
+ options: devices.video,
4113
+ selected: effective.video,
4114
+ onSelect: (id) => void onDeviceChange("video", id),
4115
+ children: cameraOn ? /* @__PURE__ */ jsx(Video, {}) : /* @__PURE__ */ jsx(VideoOff, {})
4116
+ }
4117
+ )
4118
+ ] }) : null,
4119
+ showStageModerator ? /* @__PURE__ */ jsx(
4120
+ ControlIcon,
4121
+ {
4122
+ label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : labels.goOnAir,
4123
+ pressed: selfStageStatus !== "ON_STAGE",
4124
+ disabled: selfStageStatus === "ON_STAGE" && !canLeaveStage,
4125
+ onClick: () => {
4126
+ if (selfStageStatus === "ON_STAGE") {
4127
+ if (!canLeaveStage) {
4128
+ toast.message(labels.offAirLastVisible);
4129
+ return;
4130
+ }
4131
+ onLeaveStage();
4132
+ return;
4133
+ }
4134
+ onAskStage();
4135
+ },
4136
+ children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
4137
+ }
4138
+ ) : null,
4139
+ showStageGuest ? /* @__PURE__ */ jsx(
4140
+ ControlIcon,
4141
+ {
4142
+ label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
4143
+ pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
4144
+ disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
4145
+ onClick: () => {
4146
+ if (selfStageStatus === "ON_STAGE") {
4147
+ if (!canLeaveStage) {
4148
+ toast.message(labels.offAirLastVisible);
4149
+ return;
4150
+ }
4151
+ onLeaveStage();
4152
+ return;
4153
+ }
4154
+ onAskStage();
4155
+ },
4156
+ children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
4157
+ }
4158
+ ) : null,
4159
+ showScreenControl ? canShareFreely ? /* @__PURE__ */ jsx(
4160
+ ControlIcon,
4161
+ {
4162
+ label: labels.screen,
4163
+ pressed: screenOn,
4164
+ disabled: screenPending || !screenAvailable && !screenOn,
4165
+ pending: screenPending,
4166
+ onClick: onToggleScreen,
4167
+ children: /* @__PURE__ */ jsx(MonitorUp, {})
4168
+ }
4169
+ ) : shareGrant === "requested" ? /* @__PURE__ */ jsxs(Fragment, { children: [
4170
+ /* @__PURE__ */ jsx(
4171
+ ControlIcon,
4172
+ {
4173
+ label: labels.shareRequested,
4174
+ pressed: true,
4175
+ disabled: true,
4176
+ onClick: () => void 0,
4177
+ children: /* @__PURE__ */ jsx(MonitorUp, {})
4178
+ }
4179
+ ),
4180
+ /* @__PURE__ */ jsx(
4181
+ ControlIcon,
4182
+ {
4183
+ label: labels.cancelShareRequest,
4184
+ onClick: onCancelShareRequest,
4185
+ children: /* @__PURE__ */ jsx(X, {})
4186
+ }
4187
+ )
4188
+ ] }) : /* @__PURE__ */ jsx(ControlIcon, { label: labels.askToShare, onClick: onAskToShare, children: /* @__PURE__ */ jsx(MonitorUp, {}) }) : null,
4189
+ /* @__PURE__ */ jsx(
4190
+ ControlIcon,
4191
+ {
4192
+ label: captionsLabel,
4193
+ pressed: captionsOn,
4194
+ disabled: !captionsAvailable,
4195
+ onClick: onToggleCaptions,
4196
+ children: /* @__PURE__ */ jsx(Captions, {})
4197
+ }
4198
+ ),
4199
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
4200
+ /* @__PURE__ */ jsx(
4201
+ DropdownMenuTrigger,
4202
+ {
4203
+ render: /* @__PURE__ */ jsx(
4204
+ Button,
4205
+ {
4206
+ type: "button",
4207
+ size: "icon-lg",
4208
+ variant: "secondary",
4209
+ "aria-label": labels.more
4210
+ }
4211
+ ),
4212
+ children: /* @__PURE__ */ jsx(MoreVertical, {})
4213
+ }
4214
+ ),
4215
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "center", className: "min-w-44", children: [
4216
+ fullscreenSupported ? /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => void enterFullscreen(), children: [
4217
+ /* @__PURE__ */ jsx(Maximize, {}),
4218
+ labels.fullscreen
4219
+ ] }) : null,
4220
+ pipSupported ? /* @__PURE__ */ jsxs(
4221
+ DropdownMenuItem,
4222
+ {
4223
+ onClick: () => {
4224
+ if (pipOpen) onClosePip();
4225
+ else onOpenPip();
4226
+ },
4227
+ children: [
4228
+ /* @__PURE__ */ jsx(PictureInPicture2, {}),
4229
+ pipOpen ? labels.closePip : labels.openPip
4230
+ ]
4231
+ }
4232
+ ) : null,
4233
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: onOpenSettings, children: [
4234
+ /* @__PURE__ */ jsx(Settings, {}),
4235
+ labels.settings
4236
+ ] })
4237
+ ] })
4238
+ ] })
4239
+ ] }),
4240
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
3531
4241
  /* @__PURE__ */ jsx(
3532
4242
  ControlIcon,
3533
4243
  {
3534
- label: muted ? labels.unmute : labels.mute,
3535
- pressed: muted,
3536
- tone: muted ? "danger" : "default",
3537
- onClick: onToggleMute,
3538
- children: muted ? /* @__PURE__ */ jsx(MicOff, {}) : /* @__PURE__ */ jsx(Mic, {})
4244
+ label: unreadChat > 0 ? `${labels.chat} (${unreadChat})` : labels.chat,
4245
+ pressed: chatOpen,
4246
+ badge: unreadChat,
4247
+ onClick: onToggleChat,
4248
+ children: /* @__PURE__ */ jsx(MessageSquare, {})
3539
4249
  }
3540
4250
  ),
3541
- /* @__PURE__ */ jsx(
4251
+ session.role === "moderator" ? /* @__PURE__ */ jsx(
3542
4252
  ControlIcon,
3543
4253
  {
3544
- label: labels.camera,
3545
- pressed: cameraOn,
3546
- onClick: onToggleCamera,
3547
- children: cameraOn ? /* @__PURE__ */ jsx(Video, {}) : /* @__PURE__ */ jsx(VideoOff, {})
4254
+ label: peopleBadge > 0 ? `${labels.people} (${peopleBadge})` : labels.people,
4255
+ pressed: peopleOpen,
4256
+ badge: peopleBadge,
4257
+ onClick: onTogglePeople,
4258
+ children: /* @__PURE__ */ jsx(Users, {})
3548
4259
  }
3549
- )
3550
- ] }) : null,
3551
- session.role === "moderator" && canManageStage ? /* @__PURE__ */ jsx(
3552
- ControlIcon,
3553
- {
3554
- label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : labels.goOnAir,
3555
- pressed: selfStageStatus !== "ON_STAGE",
3556
- disabled: selfStageStatus === "ON_STAGE" && !canLeaveStage,
3557
- onClick: () => {
3558
- if (selfStageStatus === "ON_STAGE") {
3559
- if (!canLeaveStage) {
3560
- toast.message(labels.offAirLastVisible);
3561
- return;
3562
- }
3563
- onLeaveStage();
3564
- return;
3565
- }
3566
- onAskStage();
3567
- },
3568
- children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
3569
- }
3570
- ) : null,
3571
- session.role === "guest" && stageEnabled ? /* @__PURE__ */ jsx(
3572
- ControlIcon,
3573
- {
3574
- label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
3575
- pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
3576
- disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
3577
- onClick: () => {
3578
- if (selfStageStatus === "ON_STAGE") {
3579
- if (!canLeaveStage) {
3580
- toast.message(labels.offAirLastVisible);
3581
- return;
3582
- }
3583
- onLeaveStage();
3584
- return;
3585
- }
3586
- onAskStage();
3587
- },
3588
- children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
3589
- }
3590
- ) : null,
3591
- showScreenControl ? canShareFreely ? /* @__PURE__ */ jsx(
3592
- ControlIcon,
3593
- {
3594
- label: labels.screen,
3595
- pressed: screenOn,
3596
- disabled: screenPending || !screenAvailable && !screenOn,
3597
- pending: screenPending,
3598
- onClick: onToggleScreen,
3599
- children: /* @__PURE__ */ jsx(MonitorUp, {})
3600
- }
3601
- ) : shareGrant === "requested" ? /* @__PURE__ */ jsxs(Fragment, { children: [
4260
+ ) : null,
3602
4261
  /* @__PURE__ */ jsx(
3603
4262
  ControlIcon,
3604
4263
  {
3605
- label: labels.shareRequested,
3606
- pressed: true,
3607
- disabled: true,
3608
- onClick: () => void 0,
3609
- children: /* @__PURE__ */ jsx(MonitorUp, {})
4264
+ label: labels.settings,
4265
+ pressed: settingsOpen,
4266
+ onClick: onOpenSettings,
4267
+ children: /* @__PURE__ */ jsx(Settings, {})
3610
4268
  }
3611
4269
  ),
3612
4270
  /* @__PURE__ */ jsx(
3613
- ControlIcon,
4271
+ StudioSettingsDialog,
3614
4272
  {
3615
- label: labels.cancelShareRequest,
3616
- onClick: onCancelShareRequest,
3617
- children: /* @__PURE__ */ jsx(X, {})
4273
+ open: settingsOpen,
4274
+ onOpenChange: onCloseSettings,
4275
+ meeting,
4276
+ reducedMotion,
4277
+ cameraOn,
4278
+ labels,
4279
+ captionPrefs,
4280
+ onCaptionPrefsChange,
4281
+ captionsAvailable,
4282
+ effectBackgrounds
3618
4283
  }
3619
- )
3620
- ] }) : /* @__PURE__ */ jsx(ControlIcon, { label: labels.askToShare, onClick: onAskToShare, children: /* @__PURE__ */ jsx(MonitorUp, {}) }) : null,
3621
- /* @__PURE__ */ jsx(
3622
- ControlIcon,
3623
- {
3624
- label: unreadChat > 0 ? `${labels.chat} (${unreadChat})` : labels.chat,
3625
- pressed: chatOpen,
3626
- badge: unreadChat,
3627
- onClick: onToggleChat,
3628
- children: /* @__PURE__ */ jsx(MessageSquare, {})
3629
- }
3630
- ),
3631
- session.role === "moderator" ? /* @__PURE__ */ jsx(
3632
- ControlIcon,
3633
- {
3634
- label: peopleBadge > 0 ? `${labels.people} (${peopleBadge})` : labels.people,
3635
- pressed: peopleOpen,
3636
- badge: peopleBadge,
3637
- onClick: onTogglePeople,
3638
- children: /* @__PURE__ */ jsx(Users, {})
3639
- }
3640
- ) : null,
3641
- fullscreenSupported ? /* @__PURE__ */ jsx(
3642
- ControlIcon,
3643
- {
3644
- label: labels.fullscreen,
3645
- onClick: () => void enterFullscreen(),
3646
- children: /* @__PURE__ */ jsx(Maximize, {})
3647
- }
3648
- ) : null,
3649
- pipSupported ? /* @__PURE__ */ jsx(
3650
- ControlIcon,
3651
- {
3652
- label: pipOpen ? labels.closePip : labels.openPip,
3653
- pressed: pipOpen,
3654
- onClick: () => {
3655
- if (pipOpen) onClosePip();
3656
- else onOpenPip();
3657
- },
3658
- children: /* @__PURE__ */ jsx(PictureInPicture2, {})
3659
- }
3660
- ) : null,
4284
+ ),
4285
+ session.role === "guest" ? /* @__PURE__ */ jsx(
4286
+ ConfirmAction,
4287
+ {
4288
+ triggerLabel: labels.leave,
4289
+ title: labels.leaveTitle,
4290
+ description: labels.leaveDescription,
4291
+ confirmLabel: labels.leaveConfirm,
4292
+ pending,
4293
+ onConfirm: onLeave,
4294
+ trigger: /* @__PURE__ */ jsx(
4295
+ Button,
4296
+ {
4297
+ type: "button",
4298
+ variant: "destructive",
4299
+ size: "icon-lg",
4300
+ disabled: pending,
4301
+ "aria-label": labels.leave,
4302
+ children: /* @__PURE__ */ jsx(PhoneOff, {})
4303
+ }
4304
+ )
4305
+ }
4306
+ ) : null
4307
+ ] })
4308
+ ] }) });
4309
+ }
4310
+ function MediaSplit({
4311
+ label,
4312
+ pressed,
4313
+ tone = "default",
4314
+ onClick,
4315
+ pickerLabel,
4316
+ options,
4317
+ selected,
4318
+ onSelect,
4319
+ children
4320
+ }) {
4321
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center", children: [
3661
4322
  /* @__PURE__ */ jsx(
3662
4323
  ControlIcon,
3663
4324
  {
3664
- label: labels.settings,
3665
- pressed: settingsOpen,
3666
- onClick: onOpenSettings,
3667
- children: /* @__PURE__ */ jsx(Settings, {})
4325
+ label,
4326
+ pressed,
4327
+ tone,
4328
+ onClick,
4329
+ children
3668
4330
  }
3669
4331
  ),
3670
- /* @__PURE__ */ jsx(
3671
- StudioSettingsDialog,
3672
- {
3673
- open: settingsOpen,
3674
- onOpenChange: onCloseSettings,
3675
- meeting,
3676
- reducedMotion,
3677
- cameraOn,
3678
- labels: {
3679
- settingsTitle: labels.settingsTitle,
3680
- settingsDescription: labels.settingsDescription,
3681
- guestName: labels.guestName,
3682
- displayNameSave: labels.displayNameSave,
3683
- displayNameSaved: labels.displayNameSaved,
3684
- displayNameError: labels.displayNameError,
3685
- cameraDevice: labels.cameraDevice,
3686
- micDevice: labels.micDevice,
3687
- speakerDevice: labels.speakerDevice,
3688
- testSpeaker: labels.testSpeaker,
3689
- testSpeakerError: labels.testSpeakerError,
3690
- cameraOff: labels.cameraOff,
3691
- micLevel: labels.micLevel
4332
+ options.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
4333
+ /* @__PURE__ */ jsx(
4334
+ DropdownMenuTrigger,
4335
+ {
4336
+ render: /* @__PURE__ */ jsx(
4337
+ Button,
4338
+ {
4339
+ type: "button",
4340
+ size: "icon-xs",
4341
+ variant: "secondary",
4342
+ className: "-ml-1",
4343
+ "aria-label": pickerLabel
4344
+ }
4345
+ ),
4346
+ children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-3.5" })
3692
4347
  }
3693
- }
3694
- ),
3695
- session.role === "guest" ? /* @__PURE__ */ jsx(
3696
- ConfirmAction,
3697
- {
3698
- triggerLabel: labels.leave,
3699
- title: labels.leaveTitle,
3700
- description: labels.leaveDescription,
3701
- confirmLabel: labels.leaveConfirm,
3702
- pending,
3703
- onConfirm: onLeave,
3704
- trigger: /* @__PURE__ */ jsx(
3705
- Button,
3706
- {
3707
- type: "button",
3708
- variant: "destructive",
3709
- size: "icon-lg",
3710
- disabled: pending,
3711
- "aria-label": labels.leave,
3712
- children: /* @__PURE__ */ jsx(PhoneOff, {})
3713
- }
4348
+ ),
4349
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "start", side: "top", className: "min-w-52", children: options.map((option) => /* @__PURE__ */ jsxs(
4350
+ DropdownMenuItem,
4351
+ {
4352
+ onClick: () => onSelect(option.deviceId),
4353
+ children: [
4354
+ option.deviceId === selected ? "\u2713 " : "",
4355
+ option.label
4356
+ ]
4357
+ },
4358
+ option.deviceId
4359
+ )) })
4360
+ ] }) : null
4361
+ ] });
4362
+ }
4363
+ var POLL_MS = 2e3;
4364
+ var PLAYOUT_MS = 250;
4365
+ var STUDIO_EXTRA_LAG_MS = 200;
4366
+ function useStudioCaptions({
4367
+ enabled,
4368
+ baseUrl,
4369
+ locale,
4370
+ streamStatus
4371
+ }) {
4372
+ const [playout, setPlayout] = useState(
4373
+ EMPTY_LIVE_CAPTION_PLAYOUT
4374
+ );
4375
+ const cuesRef = useRef([]);
4376
+ useEffect(() => {
4377
+ if (!enabled) {
4378
+ cuesRef.current = [];
4379
+ setPlayout(EMPTY_LIVE_CAPTION_PLAYOUT);
4380
+ return;
4381
+ }
4382
+ if (!baseUrl || !isStudioActiveStatus(streamStatus)) return;
4383
+ let cancelled = false;
4384
+ const url = liveCaptionsUrl(baseUrl, locale);
4385
+ const poll = async () => {
4386
+ const loaded = await loadLiveCaptions(url);
4387
+ if (cancelled || !loaded.ok) return;
4388
+ cuesRef.current = mergeLiveCaptionCues(cuesRef.current, loaded.cues);
4389
+ };
4390
+ void poll();
4391
+ const timer = window.setInterval(() => void poll(), POLL_MS);
4392
+ return () => {
4393
+ cancelled = true;
4394
+ window.clearInterval(timer);
4395
+ };
4396
+ }, [baseUrl, enabled, locale, streamStatus]);
4397
+ useEffect(() => {
4398
+ if (!enabled || !baseUrl || !isStudioActiveStatus(streamStatus)) {
4399
+ setPlayout(EMPTY_LIVE_CAPTION_PLAYOUT);
4400
+ return;
4401
+ }
4402
+ const tick2 = () => {
4403
+ setPlayout(
4404
+ (prev) => advanceLiveCaptionPlayout(
4405
+ prev,
4406
+ cuesRef.current,
4407
+ Date.now(),
4408
+ 0,
4409
+ void 0,
4410
+ STUDIO_EXTRA_LAG_MS
3714
4411
  )
3715
- }
3716
- ) : null
3717
- ] }) });
4412
+ );
4413
+ };
4414
+ tick2();
4415
+ const timer = window.setInterval(tick2, PLAYOUT_MS);
4416
+ return () => window.clearInterval(timer);
4417
+ }, [baseUrl, enabled, streamStatus]);
4418
+ return playout;
3718
4419
  }
3719
4420
  var STAGE_GRANT_VERIFY_MS = 4e3;
3720
4421
  var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
@@ -3732,7 +4433,11 @@ function StudioRoom({
3732
4433
  windowGate,
3733
4434
  initialCameraOn = true,
3734
4435
  initialMuted = false,
3735
- initialDevices
4436
+ initialDevices,
4437
+ appDownloadUrl,
4438
+ captionsBaseUrl,
4439
+ apiUrl,
4440
+ cdnBase
3736
4441
  }) {
3737
4442
  const [cameraOn, setCameraOn] = useState(initialCameraOn);
3738
4443
  const [screenOn, setScreenOn] = useState(false);
@@ -3753,6 +4458,12 @@ function StudioRoom({
3753
4458
  );
3754
4459
  const [egressError, setEgressError] = useState(null);
3755
4460
  const [shareGrant, setShareGrant] = useState("idle");
4461
+ const [downloadAppOpen, setDownloadAppOpen] = useState(false);
4462
+ const [captionsOn, setCaptionsOn] = useState(false);
4463
+ const [captionPrefs, setCaptionPrefs] = useState(
4464
+ () => readStudioCaptionPrefs()
4465
+ );
4466
+ const captionHintShown = useRef(false);
3756
4467
  const [connectTimedOut, setConnectTimedOut] = useState(false);
3757
4468
  const [screenPermission, setScreenPermission] = useState(
3758
4469
  () => meeting.screenSharePermission()
@@ -3778,6 +4489,19 @@ function StudioRoom({
3778
4489
  const pip = useDocumentPictureInPicture();
3779
4490
  const gateFromContext = useStudioWindowGate();
3780
4491
  const { reportDuplicate } = windowGate ?? gateFromContext;
4492
+ const resolvedCaptionsBaseUrl = captionsBaseUrl ?? studioCaptionsBaseUrl(apiUrl, session.stream.id);
4493
+ const captionsAvailable = session.stream.captionsEnabled !== false;
4494
+ const captionsReady = isStudioActiveStatus(session.stream.status);
4495
+ const captionPlayout = useStudioCaptions({
4496
+ enabled: captionsOn && captionsAvailable,
4497
+ baseUrl: resolvedCaptionsBaseUrl,
4498
+ locale: captionPrefs.locale,
4499
+ streamStatus: session.stream.status
4500
+ });
4501
+ const effectBackgrounds = useMemo(
4502
+ () => defaultStudioEffectBackgrounds(cdnBase),
4503
+ [cdnBase]
4504
+ );
3781
4505
  const onAir = isLiveStudioStatus(session.stream.status);
3782
4506
  const active = isStudioActiveStatus(session.stream.status);
3783
4507
  if (onAir && liveStartedAtRef.current === null) {
@@ -3794,6 +4518,11 @@ function StudioRoom({
3794
4518
  const roomState = leaveState ?? meeting.roomState();
3795
4519
  const sessionOver = isStudioSessionOver(session.stream.status, roomState);
3796
4520
  const rejected = isStudioRejected(roomState);
4521
+ const connectionState = useMemo(
4522
+ () => meeting.connectionState(),
4523
+ [meeting, tick2]
4524
+ );
4525
+ const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
3797
4526
  useBeforeUnload(active && !sessionOver);
3798
4527
  useWakeLock(active && !sessionOver);
3799
4528
  useEffect(() => {
@@ -3957,12 +4686,13 @@ function StudioRoom({
3957
4686
  void meeting.setScreenShareEnabled(false);
3958
4687
  }, [guestOffStage, meeting]);
3959
4688
  useEffect(() => {
3960
- if (!sessionOver && !rejected) return;
4689
+ if (!sessionOver && !rejected && !guestConnectFailed) return;
4690
+ pip.close();
3961
4691
  setShareGrant("idle");
3962
4692
  void meeting.setAudioEnabled(false);
3963
4693
  void meeting.setVideoEnabled(false);
3964
4694
  void meeting.setScreenShareEnabled(false);
3965
- }, [meeting, rejected, sessionOver]);
4695
+ }, [guestConnectFailed, meeting, pip.close, rejected, sessionOver]);
3966
4696
  useEffect(() => {
3967
4697
  return meeting.subscribeMedia(() => {
3968
4698
  const self2 = meeting.participants().find((row) => row.isSelf);
@@ -4000,10 +4730,6 @@ function StudioRoom({
4000
4730
  [participants]
4001
4731
  );
4002
4732
  const waitlisted = useMemo(() => meeting.waitlisted(), [meeting, tick2]);
4003
- const connectionState = useMemo(
4004
- () => meeting.connectionState(),
4005
- [meeting, tick2]
4006
- );
4007
4733
  const lastMeetingError = useMemo(
4008
4734
  () => meeting.lastMeetingError(),
4009
4735
  [meeting, tick2]
@@ -4021,7 +4747,7 @@ function StudioRoom({
4021
4747
  captureSupported,
4022
4748
  screenPermission
4023
4749
  );
4024
- const showScreenControl = durableScreenBlock === null;
4750
+ const showScreenControl = showScreenShareControl(durableScreenBlock);
4025
4751
  const canGoLive = session.role === "moderator" && canPublish(session.stream.status);
4026
4752
  const showPublishButton = session.role === "moderator" && !onAir && session.stream.status !== "ended" && session.stream.status !== "error";
4027
4753
  const self = participants.find((row) => row.isSelf);
@@ -4117,17 +4843,10 @@ function StudioRoom({
4117
4843
  }, [connectionState, session.role]);
4118
4844
  useEffect(() => {
4119
4845
  if (inWaitingRoom || screenNoticePrimed.current) return;
4120
- if (!durableScreenBlock) return;
4846
+ if (durableScreenBlock !== "permission") return;
4121
4847
  screenNoticePrimed.current = true;
4122
- toast.message(
4123
- durableScreenBlock === "device" ? labels.screenUnsupportedDevice : labels.screenNotAllowed
4124
- );
4125
- }, [
4126
- durableScreenBlock,
4127
- inWaitingRoom,
4128
- labels.screenNotAllowed,
4129
- labels.screenUnsupportedDevice
4130
- ]);
4848
+ toast.message(labels.screenNotAllowed);
4849
+ }, [durableScreenBlock, inWaitingRoom, labels.screenNotAllowed]);
4131
4850
  useEffect(() => {
4132
4851
  return meeting.subscribeBroadcast((message) => {
4133
4852
  if (message.type === "host-media") {
@@ -4236,6 +4955,10 @@ function StudioRoom({
4236
4955
  setCameraOn((current) => !current);
4237
4956
  }, []);
4238
4957
  const onToggleScreen = useCallback(() => {
4958
+ if (durableScreenBlock === "device") {
4959
+ setDownloadAppOpen(true);
4960
+ return;
4961
+ }
4239
4962
  if (!canShareFreely || screenRequestPending) return;
4240
4963
  if (!screenOn && !canStartScreenShare(meeting.participants())) {
4241
4964
  toast.message(labels.screenBusy);
@@ -4245,12 +4968,17 @@ function StudioRoom({
4245
4968
  setScreenOn((current) => !current);
4246
4969
  }, [
4247
4970
  canShareFreely,
4971
+ durableScreenBlock,
4248
4972
  labels.screenBusy,
4249
4973
  meeting,
4250
4974
  screenOn,
4251
4975
  screenRequestPending
4252
4976
  ]);
4253
4977
  const onAskToShare = useCallback(async () => {
4978
+ if (durableScreenBlock === "device") {
4979
+ setDownloadAppOpen(true);
4980
+ return;
4981
+ }
4254
4982
  const local = meeting.participants().find((row) => row.isSelf);
4255
4983
  if (!local) return;
4256
4984
  try {
@@ -4267,7 +4995,12 @@ function StudioRoom({
4267
4995
  } catch {
4268
4996
  toast.error(labels.shareRequestError);
4269
4997
  }
4270
- }, [labels.shareRequestError, labels.shareRequestSent, meeting]);
4998
+ }, [
4999
+ durableScreenBlock,
5000
+ labels.shareRequestError,
5001
+ labels.shareRequestSent,
5002
+ meeting
5003
+ ]);
4271
5004
  const verifyGrantTookEffect = useCallback(
4272
5005
  (userId, retried = false) => {
4273
5006
  const id = window.setTimeout(() => {
@@ -4430,7 +5163,6 @@ function StudioRoom({
4430
5163
  }
4431
5164
  ) });
4432
5165
  }
4433
- const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
4434
5166
  if (guestConnectFailed) {
4435
5167
  const detail = [meeting.roomState(), lastMeetingError].filter((value) => Boolean(value)).join(" \u2014 ");
4436
5168
  return /* @__PURE__ */ jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsx(
@@ -4454,6 +5186,10 @@ function StudioRoom({
4454
5186
  labels,
4455
5187
  settingsOpen,
4456
5188
  statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
5189
+ captionPrefs,
5190
+ onCaptionPrefsChange: setCaptionPrefs,
5191
+ captionsAvailable,
5192
+ effectBackgrounds,
4457
5193
  onToggleCamera,
4458
5194
  onToggleMute: () => void onToggleMute(),
4459
5195
  onOpenSettings: () => setSettingsOpen(true),
@@ -4561,27 +5297,37 @@ function StudioRoom({
4561
5297
  }
4562
5298
  ) : null,
4563
5299
  /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col md:flex-row", children: [
4564
- /* @__PURE__ */ jsx(
4565
- StudioStage,
4566
- {
4567
- participants: stageParticipants,
4568
- meeting,
4569
- reducedMotion,
4570
- canModerate: session.role === "moderator",
4571
- canManageStage,
4572
- onAir,
4573
- labels,
4574
- onKick: (id) => void onKick(id),
4575
- onMute: (id) => void onHostMedia(id, "audio"),
4576
- onStopCamera: (id) => void onHostMedia(id, "video"),
4577
- onStopScreen: (id) => void onHostMedia(id, "screen"),
4578
- onToggleStage: (tile) => void onSetStage(tile, !isOnStage(tile.stageStatus)),
4579
- onTogglePin: (tile) => void onTogglePin(tile),
4580
- onToggleChat: (tile) => void onToggleChat(tile),
4581
- canTakeOff: (id) => canTakeOffAir(participants, id, onAir),
4582
- onOpenPeople: () => setSideTab("people")
4583
- }
4584
- ),
5300
+ /* @__PURE__ */ jsxs("div", { className: "relative flex min-h-0 min-w-0 flex-1 flex-col", children: [
5301
+ /* @__PURE__ */ jsx(
5302
+ StudioStage,
5303
+ {
5304
+ participants: stageParticipants,
5305
+ meeting,
5306
+ reducedMotion,
5307
+ canModerate: session.role === "moderator",
5308
+ canManageStage,
5309
+ onAir,
5310
+ labels,
5311
+ onKick: (id) => void onKick(id),
5312
+ onMute: (id) => void onHostMedia(id, "audio"),
5313
+ onStopCamera: (id) => void onHostMedia(id, "video"),
5314
+ onStopScreen: (id) => void onHostMedia(id, "screen"),
5315
+ onToggleStage: (tile) => void onSetStage(tile, !isOnStage(tile.stageStatus)),
5316
+ onTogglePin: (tile) => void onTogglePin(tile),
5317
+ onToggleChat: (tile) => void onToggleChat(tile),
5318
+ canTakeOff: (id) => canTakeOffAir(participants, id, onAir),
5319
+ onOpenPeople: () => setSideTab("people")
5320
+ }
5321
+ ),
5322
+ captionsOn ? /* @__PURE__ */ jsx(
5323
+ CaptionOverlay,
5324
+ {
5325
+ screen: captionPlayout.screen,
5326
+ className: "bottom-3",
5327
+ textClassName: captionSizeClass(captionPrefs.size)
5328
+ }
5329
+ ) : null
5330
+ ] }),
4585
5331
  sideTab ? /* @__PURE__ */ jsx(
4586
5332
  StudioSidePanel,
4587
5333
  {
@@ -4694,6 +5440,12 @@ function StudioRoom({
4694
5440
  stageEnabled,
4695
5441
  canLeaveStage: selfId != null && canTakeOffAir(participants, selfId, onAir),
4696
5442
  reducedMotion,
5443
+ captionsOn,
5444
+ captionsAvailable,
5445
+ captionsReady,
5446
+ captionPrefs,
5447
+ onCaptionPrefsChange: setCaptionPrefs,
5448
+ effectBackgrounds,
4697
5449
  onToggleMute: () => void onToggleMute(),
4698
5450
  onToggleCamera,
4699
5451
  onToggleScreen,
@@ -4721,6 +5473,16 @@ function StudioRoom({
4721
5473
  onCloseSettings: setSettingsOpen,
4722
5474
  onOpenPip,
4723
5475
  onClosePip: pip.close,
5476
+ onToggleCaptions: () => {
5477
+ if (!captionsAvailable) return;
5478
+ if (!captionsOn && !captionHintShown.current) {
5479
+ captionHintShown.current = true;
5480
+ toast.message(
5481
+ captionsReady ? labels.captionsDelayed : labels.captionsNotReady
5482
+ );
5483
+ }
5484
+ setCaptionsOn((current) => !current);
5485
+ },
4724
5486
  onLeave: () => void onLeave()
4725
5487
  }
4726
5488
  ),
@@ -4764,7 +5526,21 @@ function StudioRoom({
4764
5526
  }
4765
5527
  ),
4766
5528
  pip.pipWindow.document.body
4767
- ) : null
5529
+ ) : null,
5530
+ /* @__PURE__ */ jsx(
5531
+ StudioDownloadAppDialog,
5532
+ {
5533
+ open: downloadAppOpen,
5534
+ onOpenChange: setDownloadAppOpen,
5535
+ downloadUrl: appDownloadUrl,
5536
+ labels: {
5537
+ screenDownloadAppTitle: labels.screenDownloadAppTitle,
5538
+ screenUnsupportedDevice: labels.screenUnsupportedDevice,
5539
+ screenDownloadAppAction: labels.screenDownloadAppAction,
5540
+ screenDownloadAppDismiss: labels.screenDownloadAppDismiss
5541
+ }
5542
+ }
5543
+ )
4768
5544
  ] });
4769
5545
  }
4770
5546
  function isOverconstrained(error) {
@@ -5101,16 +5877,16 @@ function usePrefersReducedMotion() {
5101
5877
  // src/studio/lib/guest-identity.ts
5102
5878
  var NAME_KEY = "livo-studio-display-name";
5103
5879
  var ID_KEY = "livo-studio-guest-id";
5104
- function readStudioGuestIdentity(storage = defaultStorage2()) {
5880
+ function readStudioGuestIdentity(storage = defaultStorage4()) {
5105
5881
  const guestId = storage?.getItem(ID_KEY)?.trim() || crypto.randomUUID();
5106
5882
  storage?.setItem(ID_KEY, guestId);
5107
5883
  const displayName = storage?.getItem(NAME_KEY)?.trim() ?? "";
5108
5884
  return { displayName, guestId };
5109
5885
  }
5110
- function persistStudioGuestName(displayName, storage = defaultStorage2()) {
5886
+ function persistStudioGuestName(displayName, storage = defaultStorage4()) {
5111
5887
  storage?.setItem(NAME_KEY, displayName.trim());
5112
5888
  }
5113
- function defaultStorage2() {
5889
+ function defaultStorage4() {
5114
5890
  if (typeof window === "undefined") return null;
5115
5891
  try {
5116
5892
  return window.localStorage;
@@ -5479,10 +6255,19 @@ function studioMeetingFromRtk(raw) {
5479
6255
  if (typeof window !== "undefined") {
5480
6256
  window.addEventListener("unhandledrejection", onUnhandledRejection);
5481
6257
  }
6258
+ let videoEffects = readStudioVideoEffects();
6259
+ async function applyCurrentVideoEffects() {
6260
+ await applyStudioVideoEffects({
6261
+ self: meeting.self,
6262
+ meeting,
6263
+ config: videoEffects
6264
+ });
6265
+ }
5482
6266
  async function republishCameraIfOn() {
5483
6267
  if (meeting.self?.videoEnabled !== true) return;
5484
6268
  await meeting.self.disableVideo?.();
5485
6269
  await meeting.self.enableVideo?.();
6270
+ await applyCurrentVideoEffects();
5486
6271
  }
5487
6272
  async function acceptAllowedStageIfOffAir() {
5488
6273
  if (currentStageStatus() !== "OFF_STAGE") return;
@@ -5670,6 +6455,7 @@ function studioMeetingFromRtk(raw) {
5670
6455
  async join() {
5671
6456
  await meeting.join?.();
5672
6457
  await joinStageIfAllowed();
6458
+ await applyCurrentVideoEffects();
5673
6459
  },
5674
6460
  async leave() {
5675
6461
  if (typeof window !== "undefined") {
@@ -5682,8 +6468,10 @@ function studioMeetingFromRtk(raw) {
5682
6468
  else await meeting.self?.disableAudio?.();
5683
6469
  },
5684
6470
  async setVideoEnabled(enabled) {
5685
- if (enabled) await meeting.self?.enableVideo?.();
5686
- else await meeting.self?.disableVideo?.();
6471
+ if (enabled) {
6472
+ await meeting.self?.enableVideo?.();
6473
+ await applyCurrentVideoEffects();
6474
+ } else await meeting.self?.disableVideo?.();
5687
6475
  },
5688
6476
  async setScreenShareEnabled(enabled) {
5689
6477
  if (enabled) await meeting.self?.enableScreenShare?.();
@@ -6067,6 +6855,14 @@ function studioMeetingFromRtk(raw) {
6067
6855
  }
6068
6856
  });
6069
6857
  },
6858
+ async setVideoEffects(config) {
6859
+ videoEffects = config;
6860
+ persistStudioVideoEffects(config);
6861
+ await applyCurrentVideoEffects();
6862
+ },
6863
+ videoEffects() {
6864
+ return videoEffects;
6865
+ },
6070
6866
  async setDisplayName(name) {
6071
6867
  const trimmed = name.trim().slice(0, STUDIO_DISPLAY_NAME_MAX);
6072
6868
  if (!trimmed) return;
@@ -6298,6 +7094,21 @@ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn
6298
7094
  return null;
6299
7095
  }
6300
7096
  }
7097
+ async function getPublicStreamMeta(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
7098
+ try {
7099
+ const body = await publicJson(
7100
+ `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
7101
+ {},
7102
+ fetchFn
7103
+ );
7104
+ return parsePublicStreamMeta(body);
7105
+ } catch (error) {
7106
+ if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
7107
+ return { status: "ended", captionsEnabled: false };
7108
+ }
7109
+ return null;
7110
+ }
7111
+ }
6301
7112
  var POLL_INTERVAL_MS = 5e3;
6302
7113
  function LivoHostStudio({
6303
7114
  token,
@@ -6307,11 +7118,15 @@ function LivoHostStudio({
6307
7118
  embed = false,
6308
7119
  chrome,
6309
7120
  playerOrigin,
6310
- theme
7121
+ theme,
7122
+ appDownloadUrl
6311
7123
  }) {
6312
7124
  const reducedMotion = usePrefersReducedMotion();
6313
7125
  const [session, setSession] = useState(null);
6314
7126
  const [status, setStatus] = useState(null);
7127
+ const [captionsEnabled, setCaptionsEnabled] = useState(
7128
+ void 0
7129
+ );
6315
7130
  const [error, setError] = useState(null);
6316
7131
  const { meeting, error: meetingError } = useRealtimeKitMeeting(
6317
7132
  session?.authToken ?? null
@@ -6360,6 +7175,20 @@ function LivoHostStudio({
6360
7175
  window.clearInterval(id);
6361
7176
  };
6362
7177
  }, [client, status]);
7178
+ useEffect(() => {
7179
+ if (!session) return;
7180
+ let cancelled = false;
7181
+ const poll = async () => {
7182
+ const next = await getPublicStreamMeta(session.stream.id, apiUrl);
7183
+ if (!cancelled && next) setCaptionsEnabled(next.captionsEnabled);
7184
+ };
7185
+ void poll();
7186
+ const id = window.setInterval(() => void poll(), POLL_INTERVAL_MS);
7187
+ return () => {
7188
+ cancelled = true;
7189
+ window.clearInterval(id);
7190
+ };
7191
+ }, [apiUrl, session]);
6363
7192
  const liveSession = useMemo(() => {
6364
7193
  if (!session) return null;
6365
7194
  if (!status) return session;
@@ -6367,10 +7196,11 @@ function LivoHostStudio({
6367
7196
  ...session,
6368
7197
  stream: {
6369
7198
  ...session.stream,
6370
- status
7199
+ status,
7200
+ captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
6371
7201
  }
6372
7202
  };
6373
- }, [session, status]);
7203
+ }, [captionsEnabled, session, status]);
6374
7204
  if (error || meetingError) {
6375
7205
  return /* @__PURE__ */ jsx("p", { className: "text-destructive p-6", role: "alert", children: error ?? meetingError });
6376
7206
  }
@@ -6387,7 +7217,9 @@ function LivoHostStudio({
6387
7217
  labels,
6388
7218
  embed,
6389
7219
  chrome,
6390
- playerOrigin
7220
+ playerOrigin,
7221
+ appDownloadUrl,
7222
+ apiUrl
6391
7223
  }
6392
7224
  );
6393
7225
  }
@@ -6400,11 +7232,15 @@ function LivoGuestStudio({
6400
7232
  lobbyLabels,
6401
7233
  embed = false,
6402
7234
  playerOrigin,
6403
- theme
7235
+ theme,
7236
+ appDownloadUrl
6404
7237
  }) {
6405
7238
  const reducedMotion = usePrefersReducedMotion();
6406
7239
  const [session, setSession] = useState(null);
6407
7240
  const [status, setStatus] = useState(null);
7241
+ const [captionsEnabled, setCaptionsEnabled] = useState(
7242
+ void 0
7243
+ );
6408
7244
  const [error, setError] = useState(null);
6409
7245
  const [displayName, setDisplayName] = useState("");
6410
7246
  const [guestId, setGuestId] = useState("");
@@ -6508,8 +7344,11 @@ function LivoGuestStudio({
6508
7344
  if (current === "ended" || current === "error") return;
6509
7345
  let cancelled = false;
6510
7346
  const poll = async () => {
6511
- const next = await getPublicStreamStatus(session.stream.id, apiUrl);
6512
- if (!cancelled && next) setStatus(next);
7347
+ const next = await getPublicStreamMeta(session.stream.id, apiUrl);
7348
+ if (!cancelled && next) {
7349
+ setStatus(next.status);
7350
+ setCaptionsEnabled(next.captionsEnabled);
7351
+ }
6513
7352
  };
6514
7353
  void poll();
6515
7354
  const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
@@ -6524,9 +7363,13 @@ function LivoGuestStudio({
6524
7363
  if (!status) return session;
6525
7364
  return {
6526
7365
  ...session,
6527
- stream: { ...session.stream, status }
7366
+ stream: {
7367
+ ...session.stream,
7368
+ status,
7369
+ captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
7370
+ }
6528
7371
  };
6529
- }, [session, status]);
7372
+ }, [captionsEnabled, session, status]);
6530
7373
  useEffect(() => {
6531
7374
  if (retryAfterMs == null) return;
6532
7375
  const id = window.setTimeout(() => {
@@ -6586,7 +7429,9 @@ function LivoGuestStudio({
6586
7429
  playerOrigin,
6587
7430
  initialCameraOn: cameraOn,
6588
7431
  initialMuted: muted,
6589
- initialDevices: deviceSelection
7432
+ initialDevices: deviceSelection,
7433
+ appDownloadUrl,
7434
+ apiUrl
6590
7435
  }
6591
7436
  );
6592
7437
  }
@@ -6691,6 +7536,9 @@ function studioRoomLabels(t) {
6691
7536
  pipTitle: t("pipTitle"),
6692
7537
  pipError: t("pipError"),
6693
7538
  screenUnsupportedDevice: t("screenUnsupportedDevice"),
7539
+ screenDownloadAppTitle: t("screenDownloadAppTitle"),
7540
+ screenDownloadAppAction: t("screenDownloadAppAction"),
7541
+ screenDownloadAppDismiss: t("screenDownloadAppDismiss"),
6694
7542
  screenNotAllowed: t("screenNotAllowed"),
6695
7543
  askToShare: t("askToShare"),
6696
7544
  shareRequested: t("shareRequested"),
@@ -6759,10 +7607,40 @@ function studioRoomLabels(t) {
6759
7607
  chatPin: t("chatPin"),
6760
7608
  chatUnpin: t("chatUnpin"),
6761
7609
  chatSave: t("chatSave"),
6762
- waitingSkeleton: t("waitingSkeleton")
7610
+ waitingSkeleton: t("waitingSkeleton"),
7611
+ more: t("more"),
7612
+ captions: t("captions"),
7613
+ captionsOn: t("captionsOn"),
7614
+ captionsOff: t("captionsOff"),
7615
+ captionsNotReady: t("captionsNotReady"),
7616
+ captionsDelayed: t("captionsDelayed"),
7617
+ captionsLanguage: t("captionsLanguage"),
7618
+ captionsSize: t("captionsSize"),
7619
+ captionsSizeS: t("captionsSizeS"),
7620
+ captionsSizeM: t("captionsSizeM"),
7621
+ captionsSizeL: t("captionsSizeL"),
7622
+ settingsGeneral: t("settingsGeneral"),
7623
+ settingsAudio: t("settingsAudio"),
7624
+ settingsVideo: t("settingsVideo"),
7625
+ settingsEffects: t("settingsEffects"),
7626
+ settingsSubtitles: t("settingsSubtitles"),
7627
+ effectNone: t("effectNone"),
7628
+ effectBlur: t("effectBlur"),
7629
+ effectBlurStrong: t("effectBlurStrong"),
7630
+ effectBackgrounds: t("effectBackgrounds"),
7631
+ effectFilters: t("effectFilters"),
7632
+ effectFilterNone: t("effectFilterNone"),
7633
+ effectFilterGrayscale: t("effectFilterGrayscale"),
7634
+ effectFilterSepia: t("effectFilterSepia"),
7635
+ effectFilterWarm: t("effectFilterWarm"),
7636
+ effectFilterCool: t("effectFilterCool"),
7637
+ effectFilterBright: t("effectFilterBright"),
7638
+ localeEn: t("localeEn"),
7639
+ localeEs: t("localeEs"),
7640
+ localePt: t("localePt")
6763
7641
  };
6764
7642
  }
6765
7643
 
6766
- export { LivoGuestStudio, LivoHostStudio, StudioApiError, StudioControlClient, StudioControlError, StudioGuestLobby, StudioRoom, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, applyEmbedTheme, controlActionsFromClient, createStudioControlClient, decodeEmbedTheme, getPublicStreamStatus, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
7644
+ export { LivoGuestStudio, LivoHostStudio, StudioApiError, StudioControlClient, StudioControlError, StudioGuestLobby, StudioRoom, StudioSkeleton, StudioWindowGate, StudioWindowGateContext, applyEmbedTheme, controlActionsFromClient, createStudioControlClient, decodeEmbedTheme, getPublicStreamMeta, getPublicStreamStatus, getStudioJoinStatus, joinStudioGuest, redeemStudioHost, studioErrorCode, studioRoomLabels, usePrefersReducedMotion, useRealtimeKitMeeting, useStudioWindowGate };
6767
7645
  //# sourceMappingURL=index.js.map
6768
7646
  //# sourceMappingURL=index.js.map