@livo-tv/sdk 1.5.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.
@@ -4,6 +4,7 @@
4
4
  var react = require('react');
5
5
  var reactDom = require('react-dom');
6
6
  var lucideReact = require('lucide-react');
7
+ var video = require('@livo-tv/blocks/video');
7
8
  var sonner = require('sonner');
8
9
  var AlertDialogPrimitive = require('@radix-ui/react-alert-dialog');
9
10
  var clsx = require('clsx');
@@ -714,7 +715,14 @@ function parseStudioStreamSummary(value) {
714
715
  const status = readString(value, "status");
715
716
  if (!id || !title || !status) return null;
716
717
  const encoderLostAt = isRecord(value) ? typeof value.encoderLostAt === "number" ? value.encoderLostAt : value.encoderLostAt === null ? null : void 0 : void 0;
717
- return { id, title, status, encoderLostAt };
718
+ 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;
719
+ return {
720
+ id,
721
+ title,
722
+ status,
723
+ ...encoderLostAt !== void 0 ? { encoderLostAt } : {},
724
+ ...captionsEnabled !== void 0 ? { captionsEnabled } : {}
725
+ };
718
726
  }
719
727
  function parseStudioSession(value) {
720
728
  if (!isRecord(value)) return null;
@@ -726,7 +734,9 @@ function parseStudioSession(value) {
726
734
  const fromFull = isRecord(value.stream) ? parseStudioStreamSummary({
727
735
  id: value.stream.id,
728
736
  title: value.stream.title,
729
- status: value.stream.status
737
+ status: value.stream.status,
738
+ captionsEnabled: value.stream.captionsEnabled,
739
+ captionsUrl: value.stream.captionsUrl
730
740
  }) : null;
731
741
  const stream = fromSummary ?? fromFull;
732
742
  if (!stream) return null;
@@ -759,6 +769,14 @@ function parsePublicStreamStatus(value) {
759
769
  const status = readString(nested, "status");
760
770
  return status;
761
771
  }
772
+ function parsePublicStreamMeta(value) {
773
+ if (!isRecord(value)) return null;
774
+ const nested = isRecord(value.stream) ? value.stream : value;
775
+ const status = readString(nested, "status");
776
+ if (!status) return null;
777
+ const captionsEnabled = typeof nested.captionsEnabled === "boolean" ? nested.captionsEnabled : typeof nested.captionsUrl === "string" && nested.captionsUrl.length > 0;
778
+ return { status, captionsEnabled };
779
+ }
762
780
  function studioErrorCode(error) {
763
781
  if (!error || typeof error !== "object") return null;
764
782
  const body = "body" in error ? error.body : null;
@@ -1638,6 +1656,211 @@ async function playSpeakerTest(deviceId) {
1638
1656
  await ctx3.close();
1639
1657
  }
1640
1658
  }
1659
+
1660
+ // src/studio/lib/caption-prefs.ts
1661
+ var STUDIO_CAPTION_LOCALES = ["en", "es", "pt"];
1662
+ var DEFAULT_STUDIO_CAPTION_PREFS = {
1663
+ locale: "en",
1664
+ size: "m"
1665
+ };
1666
+ function isStudioCaptionLocale(value) {
1667
+ return STUDIO_CAPTION_LOCALES.includes(value);
1668
+ }
1669
+ var STORAGE_KEY = "livo-studio-captions";
1670
+ function defaultStorage2() {
1671
+ if (typeof window === "undefined") return null;
1672
+ try {
1673
+ return window.localStorage;
1674
+ } catch {
1675
+ return null;
1676
+ }
1677
+ }
1678
+ function isCaptionSize(value) {
1679
+ return value === "s" || value === "m" || value === "l";
1680
+ }
1681
+ function captionSizeClass(size) {
1682
+ switch (size) {
1683
+ case "s":
1684
+ return "text-[length:clamp(10px,2.1cqh,0.75rem)]";
1685
+ case "m":
1686
+ return "text-[length:clamp(10px,2.6cqh,0.875rem)]";
1687
+ case "l":
1688
+ return "text-[length:clamp(12px,3.2cqh,1.05rem)]";
1689
+ default: {
1690
+ const _exhaustive = size;
1691
+ return _exhaustive;
1692
+ }
1693
+ }
1694
+ }
1695
+ function readStudioCaptionPrefs(storage = defaultStorage2(), fallbackLocale = "en") {
1696
+ const raw = storage?.getItem(STORAGE_KEY);
1697
+ if (!raw) {
1698
+ return { ...DEFAULT_STUDIO_CAPTION_PREFS, locale: fallbackLocale };
1699
+ }
1700
+ try {
1701
+ const parsed = JSON.parse(raw);
1702
+ const locale = typeof parsed.locale === "string" && isStudioCaptionLocale(parsed.locale) ? parsed.locale : fallbackLocale;
1703
+ const size = typeof parsed.size === "string" && isCaptionSize(parsed.size) ? parsed.size : "m";
1704
+ return { locale, size };
1705
+ } catch {
1706
+ return { ...DEFAULT_STUDIO_CAPTION_PREFS, locale: fallbackLocale };
1707
+ }
1708
+ }
1709
+ function persistStudioCaptionPrefs(prefs, storage = defaultStorage2()) {
1710
+ storage?.setItem(STORAGE_KEY, JSON.stringify(prefs));
1711
+ }
1712
+ function studioCaptionsBaseUrl(apiUrl, streamId) {
1713
+ if (!apiUrl) return void 0;
1714
+ return `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}/captions`;
1715
+ }
1716
+
1717
+ // src/studio/lib/video-effects.ts
1718
+ var DEFAULT_STUDIO_VIDEO_EFFECTS = {
1719
+ background: "none",
1720
+ blurStrength: 50,
1721
+ filter: "none"
1722
+ };
1723
+ var FILTER_CSS = {
1724
+ none: "",
1725
+ grayscale: "grayscale(1)",
1726
+ sepia: "sepia(0.85)",
1727
+ warm: "sepia(0.35) saturate(1.2) hue-rotate(-10deg)",
1728
+ cool: "saturate(0.9) hue-rotate(15deg) brightness(1.05)",
1729
+ bright: "brightness(1.15) contrast(1.08)"
1730
+ };
1731
+ function defaultStudioEffectBackgrounds(cdnBase = "https://hls.livo.tv") {
1732
+ const root = cdnBase.replace(/\/$/, "");
1733
+ return [
1734
+ {
1735
+ id: "office",
1736
+ url: `${root}/env/backgrounds/office.jpg`,
1737
+ label: "Office"
1738
+ },
1739
+ {
1740
+ id: "lounge",
1741
+ url: `${root}/env/backgrounds/lounge.jpg`,
1742
+ label: "Lounge"
1743
+ },
1744
+ {
1745
+ id: "studio",
1746
+ url: `${root}/env/backgrounds/studio.jpg`,
1747
+ label: "Studio"
1748
+ },
1749
+ { id: "sky", url: `${root}/env/backgrounds/sky.jpg`, label: "Sky" },
1750
+ {
1751
+ id: "forest",
1752
+ url: `${root}/env/backgrounds/forest.jpg`,
1753
+ label: "Forest"
1754
+ },
1755
+ {
1756
+ id: "abstract",
1757
+ url: `${root}/env/backgrounds/abstract.jpg`,
1758
+ label: "Abstract"
1759
+ }
1760
+ ];
1761
+ }
1762
+ function createFilterVideoMiddleware(filter) {
1763
+ const css = FILTER_CSS[filter];
1764
+ if (!css) return null;
1765
+ return async () => {
1766
+ const scratch = document.createElement("canvas");
1767
+ return async (canvas, ctx3) => {
1768
+ if (scratch.width !== canvas.width || scratch.height !== canvas.height) {
1769
+ scratch.width = canvas.width;
1770
+ scratch.height = canvas.height;
1771
+ }
1772
+ const copy = scratch.getContext("2d");
1773
+ if (!copy) return;
1774
+ copy.clearRect(0, 0, scratch.width, scratch.height);
1775
+ copy.drawImage(canvas, 0, 0);
1776
+ ctx3.save();
1777
+ ctx3.filter = css;
1778
+ ctx3.drawImage(scratch, 0, 0);
1779
+ ctx3.restore();
1780
+ };
1781
+ };
1782
+ }
1783
+ async function loadBackgroundTransformer(meeting) {
1784
+ const mod = await import('@cloudflare/realtimekit-virtual-background');
1785
+ const Init = mod.default ?? mod.RealtimeKitVideoBackgroundTransformer ?? null;
1786
+ if (!Init) return null;
1787
+ return Init.init({ meeting });
1788
+ }
1789
+ async function applyStudioVideoEffects(options) {
1790
+ const self = options.self;
1791
+ if (!self?.removeAllVideoMiddlewares || !self.addVideoMiddleware) return;
1792
+ await self.removeAllVideoMiddlewares();
1793
+ const needsSegmentation = options.config.background !== "none";
1794
+ if (needsSegmentation) {
1795
+ await self.setVideoMiddlewareGlobalConfig?.({
1796
+ disablePerFrameCanvasRendering: true
1797
+ });
1798
+ try {
1799
+ const transformer = await loadBackgroundTransformer(options.meeting);
1800
+ if (transformer && options.config.background === "blur") {
1801
+ const strength = Math.min(
1802
+ 100,
1803
+ Math.max(0, options.config.blurStrength)
1804
+ );
1805
+ await self.addVideoMiddleware(
1806
+ await transformer.createBackgroundBlurVideoMiddleware(strength)
1807
+ );
1808
+ } else if (transformer && options.config.background === "image" && options.config.imageUrl) {
1809
+ await self.addVideoMiddleware(
1810
+ await transformer.createStaticBackgroundVideoMiddleware(
1811
+ options.config.imageUrl
1812
+ )
1813
+ );
1814
+ }
1815
+ } catch {
1816
+ }
1817
+ }
1818
+ const filter = createFilterVideoMiddleware(options.config.filter);
1819
+ if (filter) await self.addVideoMiddleware(filter);
1820
+ }
1821
+
1822
+ // src/studio/lib/effects-prefs.ts
1823
+ var STORAGE_KEY2 = "livo-studio-video-effects";
1824
+ function defaultStorage3() {
1825
+ if (typeof window === "undefined") return null;
1826
+ try {
1827
+ return window.localStorage;
1828
+ } catch {
1829
+ return null;
1830
+ }
1831
+ }
1832
+ var BACKGROUNDS = ["none", "blur", "image"];
1833
+ var FILTERS = [
1834
+ "none",
1835
+ "grayscale",
1836
+ "sepia",
1837
+ "warm",
1838
+ "cool",
1839
+ "bright"
1840
+ ];
1841
+ function isBackground(value) {
1842
+ return BACKGROUNDS.includes(value);
1843
+ }
1844
+ function isFilter(value) {
1845
+ return FILTERS.includes(value);
1846
+ }
1847
+ function readStudioVideoEffects(storage = defaultStorage3()) {
1848
+ const raw = storage?.getItem(STORAGE_KEY2);
1849
+ if (!raw) return { ...DEFAULT_STUDIO_VIDEO_EFFECTS };
1850
+ try {
1851
+ const parsed = JSON.parse(raw);
1852
+ const background = typeof parsed.background === "string" && isBackground(parsed.background) ? parsed.background : "none";
1853
+ const filter = typeof parsed.filter === "string" && isFilter(parsed.filter) ? parsed.filter : "none";
1854
+ const blurStrength = typeof parsed.blurStrength === "number" && Number.isFinite(parsed.blurStrength) ? Math.min(100, Math.max(0, parsed.blurStrength)) : 50;
1855
+ const imageUrl = typeof parsed.imageUrl === "string" && parsed.imageUrl ? parsed.imageUrl : void 0;
1856
+ return { background, blurStrength, imageUrl, filter };
1857
+ } catch {
1858
+ return { ...DEFAULT_STUDIO_VIDEO_EFFECTS };
1859
+ }
1860
+ }
1861
+ function persistStudioVideoEffects(config, storage = defaultStorage3()) {
1862
+ storage?.setItem(STORAGE_KEY2, JSON.stringify(config));
1863
+ }
1641
1864
  function StudioDeviceSelectGroup({
1642
1865
  devices,
1643
1866
  selected,
@@ -1716,7 +1939,11 @@ function StudioSettingsDialog({
1716
1939
  meeting,
1717
1940
  labels,
1718
1941
  reducedMotion,
1719
- cameraOn
1942
+ cameraOn,
1943
+ captionPrefs,
1944
+ onCaptionPrefsChange,
1945
+ captionsAvailable,
1946
+ effectBackgrounds
1720
1947
  }) {
1721
1948
  const devices = useMediaDevices(open ? 1 : 0, open);
1722
1949
  const [saved, setSaved] = react.useState(
@@ -1728,11 +1955,16 @@ function StudioSettingsDialog({
1728
1955
  const [displayName, setDisplayName] = react.useState("");
1729
1956
  const [savedDisplayName, setSavedDisplayName] = react.useState("");
1730
1957
  const [savingDisplayName, setSavingDisplayName] = react.useState(false);
1958
+ const [tab, setTab] = react.useState("general");
1959
+ const [effects, setEffects] = react.useState(
1960
+ () => meeting.videoEffects()
1961
+ );
1731
1962
  const { effective } = resolveDeviceSelection(saved, devices);
1732
1963
  react.useEffect(() => {
1733
1964
  if (!open) return;
1734
1965
  setSaved(readStudioDeviceSelection());
1735
- }, [open]);
1966
+ setEffects(meeting.videoEffects());
1967
+ }, [meeting, open]);
1736
1968
  react.useEffect(() => {
1737
1969
  if (!open) return;
1738
1970
  const selfName = meeting.participants().find((row) => row.isSelf)?.name?.trim() ?? "";
@@ -1785,76 +2017,306 @@ function StudioSettingsDialog({
1785
2017
  setTestingSpeaker(false);
1786
2018
  }
1787
2019
  }
1788
- return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(DialogContent, { className: "sm:max-w-md", children: [
2020
+ async function applyEffects(next) {
2021
+ setEffects(next);
2022
+ persistStudioVideoEffects(next);
2023
+ try {
2024
+ await meeting.setVideoEffects(next);
2025
+ } catch {
2026
+ }
2027
+ }
2028
+ const tabs = [
2029
+ { id: "general", label: labels.settingsGeneral },
2030
+ { id: "audio", label: labels.settingsAudio },
2031
+ { id: "video", label: labels.settingsVideo },
2032
+ { id: "effects", label: labels.settingsEffects },
2033
+ { id: "subtitles", label: labels.settingsSubtitles }
2034
+ ];
2035
+ return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(DialogContent, { className: "sm:max-w-2xl", children: [
1789
2036
  /* @__PURE__ */ jsxRuntime.jsxs(DialogHeader, { children: [
1790
2037
  /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: labels.settingsTitle }),
1791
2038
  /* @__PURE__ */ jsxRuntime.jsx(DialogDescription, { children: labels.settingsDescription })
1792
2039
  ] }),
1793
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1794
- /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "studio-display-name", children: labels.guestName }),
1795
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
2040
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4 sm:flex-row", children: [
2041
+ /* @__PURE__ */ jsxRuntime.jsx(
2042
+ "div",
2043
+ {
2044
+ role: "tablist",
2045
+ "aria-label": labels.settings,
2046
+ className: "flex shrink-0 flex-row gap-1 overflow-x-auto sm:w-40 sm:flex-col",
2047
+ children: tabs.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
2048
+ "button",
2049
+ {
2050
+ type: "button",
2051
+ role: "tab",
2052
+ "aria-selected": tab === item.id,
2053
+ className: cn(
2054
+ "rounded-md px-3 py-1.5 text-left text-sm",
2055
+ tab === item.id ? "bg-muted font-medium" : "text-muted-foreground hover:bg-muted/60"
2056
+ ),
2057
+ onClick: () => setTab(item.id),
2058
+ children: item.label
2059
+ },
2060
+ item.id
2061
+ ))
2062
+ }
2063
+ ),
2064
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1 space-y-4", children: [
2065
+ tab === "general" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2066
+ /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: "studio-display-name", children: labels.guestName }),
2067
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
2068
+ /* @__PURE__ */ jsxRuntime.jsx(
2069
+ Input,
2070
+ {
2071
+ id: "studio-display-name",
2072
+ value: displayName,
2073
+ maxLength: STUDIO_DISPLAY_NAME_MAX,
2074
+ onChange: (event) => setDisplayName(event.target.value)
2075
+ }
2076
+ ),
2077
+ /* @__PURE__ */ jsxRuntime.jsx(
2078
+ Button,
2079
+ {
2080
+ type: "button",
2081
+ variant: "outline",
2082
+ disabled: displayNameSaveDisabled,
2083
+ onClick: () => void onSaveDisplayName(),
2084
+ children: labels.displayNameSave
2085
+ }
2086
+ )
2087
+ ] })
2088
+ ] }) : null,
2089
+ tab === "audio" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2090
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2091
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sr-only", children: labels.micLevel }),
2092
+ /* @__PURE__ */ jsxRuntime.jsx(
2093
+ AudioWaveform,
2094
+ {
2095
+ track: audioTrack,
2096
+ reducedMotion,
2097
+ className: "w-full"
2098
+ }
2099
+ )
2100
+ ] }),
2101
+ /* @__PURE__ */ jsxRuntime.jsx(
2102
+ StudioDeviceSelectGroup,
2103
+ {
2104
+ devices: { ...devices, video: [] },
2105
+ selected: effective,
2106
+ labels,
2107
+ onChange: (kind, id) => void onChange(kind, id),
2108
+ speakerAction: /* @__PURE__ */ jsxRuntime.jsx(
2109
+ Button,
2110
+ {
2111
+ type: "button",
2112
+ size: "sm",
2113
+ variant: "outline",
2114
+ disabled: testingSpeaker,
2115
+ onClick: () => void onTestSpeaker(),
2116
+ children: labels.testSpeaker
2117
+ }
2118
+ )
2119
+ }
2120
+ )
2121
+ ] }) : null,
2122
+ tab === "video" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2123
+ /* @__PURE__ */ jsxRuntime.jsx(
2124
+ SettingsCameraPreview,
2125
+ {
2126
+ track: videoTrack,
2127
+ open,
2128
+ cameraOn,
2129
+ cameraOffLabel: labels.cameraOff
2130
+ }
2131
+ ),
2132
+ /* @__PURE__ */ jsxRuntime.jsx(
2133
+ StudioDeviceSelectGroup,
2134
+ {
2135
+ devices: { ...devices, audio: [], speaker: [] },
2136
+ selected: effective,
2137
+ labels,
2138
+ onChange: (kind, id) => void onChange(kind, id)
2139
+ }
2140
+ )
2141
+ ] }) : null,
2142
+ tab === "effects" ? /* @__PURE__ */ jsxRuntime.jsx(
2143
+ EffectsPanel,
2144
+ {
2145
+ labels,
2146
+ effects,
2147
+ backgrounds: effectBackgrounds,
2148
+ onApply: (next) => void applyEffects(next)
2149
+ }
2150
+ ) : null,
2151
+ tab === "subtitles" ? /* @__PURE__ */ jsxRuntime.jsx(
2152
+ SubtitlesPanel,
2153
+ {
2154
+ labels,
2155
+ prefs: captionPrefs,
2156
+ available: captionsAvailable,
2157
+ onChange: (next) => {
2158
+ persistStudioCaptionPrefs(next);
2159
+ onCaptionPrefsChange(next);
2160
+ }
2161
+ }
2162
+ ) : null
2163
+ ] })
2164
+ ] })
2165
+ ] }) });
2166
+ }
2167
+ function EffectsPanel({
2168
+ labels,
2169
+ effects,
2170
+ backgrounds,
2171
+ onApply
2172
+ }) {
2173
+ const filters = [
2174
+ { id: "none", label: labels.effectFilterNone },
2175
+ { id: "grayscale", label: labels.effectFilterGrayscale },
2176
+ { id: "sepia", label: labels.effectFilterSepia },
2177
+ { id: "warm", label: labels.effectFilterWarm },
2178
+ { id: "cool", label: labels.effectFilterCool },
2179
+ { id: "bright", label: labels.effectFilterBright }
2180
+ ];
2181
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
2182
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2183
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-sm font-medium", children: labels.settingsEffects }),
2184
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-2", children: [
1796
2185
  /* @__PURE__ */ jsxRuntime.jsx(
1797
- Input,
2186
+ EffectTile,
1798
2187
  {
1799
- id: "studio-display-name",
1800
- value: displayName,
1801
- maxLength: STUDIO_DISPLAY_NAME_MAX,
1802
- onChange: (event) => setDisplayName(event.target.value)
2188
+ label: labels.effectNone,
2189
+ active: effects.background === "none",
2190
+ onClick: () => onApply({ ...effects, background: "none" })
1803
2191
  }
1804
2192
  ),
1805
2193
  /* @__PURE__ */ jsxRuntime.jsx(
1806
- Button,
2194
+ EffectTile,
1807
2195
  {
1808
- type: "button",
1809
- variant: "outline",
1810
- disabled: displayNameSaveDisabled,
1811
- onClick: () => void onSaveDisplayName(),
1812
- children: labels.displayNameSave
2196
+ label: labels.effectBlur,
2197
+ active: effects.background === "blur" && effects.blurStrength <= 50,
2198
+ onClick: () => onApply({ ...effects, background: "blur", blurStrength: 40 })
2199
+ }
2200
+ ),
2201
+ /* @__PURE__ */ jsxRuntime.jsx(
2202
+ EffectTile,
2203
+ {
2204
+ label: labels.effectBlurStrong,
2205
+ active: effects.background === "blur" && effects.blurStrength > 50,
2206
+ onClick: () => onApply({ ...effects, background: "blur", blurStrength: 80 })
1813
2207
  }
1814
2208
  )
1815
2209
  ] })
1816
2210
  ] }),
1817
- /* @__PURE__ */ jsxRuntime.jsx(
1818
- SettingsCameraPreview,
1819
- {
1820
- track: videoTrack,
1821
- open,
1822
- cameraOn,
1823
- cameraOffLabel: labels.cameraOff
1824
- }
1825
- ),
2211
+ backgrounds.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2212
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-sm font-medium", children: labels.effectBackgrounds }),
2213
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-3 gap-2", children: backgrounds.map((bg) => /* @__PURE__ */ jsxRuntime.jsx(
2214
+ EffectTile,
2215
+ {
2216
+ label: bg.label,
2217
+ active: effects.background === "image" && effects.imageUrl === bg.url,
2218
+ previewUrl: bg.url,
2219
+ onClick: () => onApply({
2220
+ ...effects,
2221
+ background: "image",
2222
+ imageUrl: bg.url
2223
+ })
2224
+ },
2225
+ bg.id
2226
+ )) })
2227
+ ] }) : null,
1826
2228
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1827
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sr-only", children: labels.micLevel }),
1828
- /* @__PURE__ */ jsxRuntime.jsx(
1829
- AudioWaveform,
2229
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 text-sm font-medium", children: labels.effectFilters }),
2230
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: filters.map((filter) => /* @__PURE__ */ jsxRuntime.jsx(
2231
+ Button,
1830
2232
  {
1831
- track: audioTrack,
1832
- reducedMotion,
1833
- className: "w-full"
1834
- }
1835
- )
1836
- ] }),
1837
- /* @__PURE__ */ jsxRuntime.jsx(
1838
- StudioDeviceSelectGroup,
1839
- {
1840
- devices,
1841
- selected: effective,
1842
- labels,
1843
- onChange: (kind, id) => void onChange(kind, id),
1844
- speakerAction: /* @__PURE__ */ jsxRuntime.jsx(
1845
- Button,
2233
+ type: "button",
2234
+ size: "sm",
2235
+ variant: effects.filter === filter.id ? "default" : "outline",
2236
+ onClick: () => onApply({ ...effects, filter: filter.id }),
2237
+ children: filter.label
2238
+ },
2239
+ filter.id
2240
+ )) })
2241
+ ] })
2242
+ ] });
2243
+ }
2244
+ function EffectTile({
2245
+ label,
2246
+ active,
2247
+ previewUrl,
2248
+ onClick
2249
+ }) {
2250
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2251
+ "button",
2252
+ {
2253
+ type: "button",
2254
+ "aria-pressed": active,
2255
+ onClick,
2256
+ className: cn(
2257
+ "overflow-hidden rounded-lg border text-left text-xs",
2258
+ active ? "ring-primary ring-2" : "border-border"
2259
+ ),
2260
+ children: [
2261
+ /* @__PURE__ */ jsxRuntime.jsx(
2262
+ "div",
1846
2263
  {
1847
- type: "button",
1848
- size: "sm",
1849
- variant: "outline",
1850
- disabled: testingSpeaker,
1851
- onClick: () => void onTestSpeaker(),
1852
- children: labels.testSpeaker
2264
+ className: "bg-muted aspect-video w-full bg-cover bg-center",
2265
+ style: previewUrl ? { backgroundImage: `url(${previewUrl})` } : void 0
1853
2266
  }
1854
- )
1855
- }
1856
- )
1857
- ] }) });
2267
+ ),
2268
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate px-2 py-1", children: label })
2269
+ ]
2270
+ }
2271
+ );
2272
+ }
2273
+ function SubtitlesPanel({
2274
+ labels,
2275
+ prefs,
2276
+ available,
2277
+ onChange
2278
+ }) {
2279
+ const localeLabel = {
2280
+ en: labels.localeEn,
2281
+ es: labels.localeEs,
2282
+ pt: labels.localePt
2283
+ };
2284
+ const sizes = [
2285
+ { id: "s", label: labels.captionsSizeS },
2286
+ { id: "m", label: labels.captionsSizeM },
2287
+ { id: "l", label: labels.captionsSizeL }
2288
+ ];
2289
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
2290
+ available ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-sm", children: labels.captionsDelayed }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-sm", children: labels.captionsNotReady }),
2291
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2292
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: labels.captionsLanguage }),
2293
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: STUDIO_CAPTION_LOCALES.map((locale) => /* @__PURE__ */ jsxRuntime.jsx(
2294
+ Button,
2295
+ {
2296
+ type: "button",
2297
+ size: "sm",
2298
+ variant: prefs.locale === locale ? "default" : "outline",
2299
+ onClick: () => onChange({ ...prefs, locale }),
2300
+ children: localeLabel[locale]
2301
+ },
2302
+ locale
2303
+ )) })
2304
+ ] }),
2305
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2306
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: labels.captionsSize }),
2307
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: sizes.map((size) => /* @__PURE__ */ jsxRuntime.jsx(
2308
+ Button,
2309
+ {
2310
+ type: "button",
2311
+ size: "sm",
2312
+ variant: prefs.size === size.id ? "default" : "outline",
2313
+ onClick: () => onChange({ ...prefs, size: size.id }),
2314
+ children: size.label
2315
+ },
2316
+ size.id
2317
+ )) })
2318
+ ] })
2319
+ ] });
1858
2320
  }
1859
2321
  function SettingsCameraPreview({
1860
2322
  track,
@@ -1905,7 +2367,11 @@ function StudioGuestWaiting({
1905
2367
  onToggleCamera,
1906
2368
  onToggleMute,
1907
2369
  onOpenSettings,
1908
- onCloseSettings
2370
+ onCloseSettings,
2371
+ captionPrefs,
2372
+ onCaptionPrefsChange,
2373
+ captionsAvailable,
2374
+ effectBackgrounds
1909
2375
  }) {
1910
2376
  const [audioTrack, setAudioTrack] = react.useState(null);
1911
2377
  const [videoTrack, setVideoTrack] = react.useState(null);
@@ -1972,7 +2438,11 @@ function StudioGuestWaiting({
1972
2438
  meeting,
1973
2439
  labels,
1974
2440
  reducedMotion,
1975
- cameraOn
2441
+ cameraOn,
2442
+ captionPrefs,
2443
+ onCaptionPrefsChange,
2444
+ captionsAvailable,
2445
+ effectBackgrounds
1976
2446
  }
1977
2447
  )
1978
2448
  ] });
@@ -3597,6 +4067,12 @@ function StudioToolbar({
3597
4067
  stageEnabled,
3598
4068
  canLeaveStage,
3599
4069
  reducedMotion,
4070
+ captionsOn,
4071
+ captionsAvailable,
4072
+ captionsReady,
4073
+ captionPrefs,
4074
+ onCaptionPrefsChange,
4075
+ effectBackgrounds,
3600
4076
  onToggleMute,
3601
4077
  onToggleCamera,
3602
4078
  onToggleScreen,
@@ -3610,204 +4086,360 @@ function StudioToolbar({
3610
4086
  onCloseSettings,
3611
4087
  onOpenPip,
3612
4088
  onClosePip,
4089
+ onToggleCaptions,
3613
4090
  onLeave
3614
4091
  }) {
3615
- const fullscreenSupported = document.fullscreenEnabled === true && typeof document.documentElement.requestFullscreen === "function";
4092
+ const devices = useMediaDevices(1, true);
4093
+ const saved = readStudioDeviceSelection();
4094
+ const { effective } = resolveDeviceSelection(saved, devices);
4095
+ const fullscreenSupported = typeof document !== "undefined" && document.fullscreenEnabled === true && typeof document.documentElement.requestFullscreen === "function";
3616
4096
  async function enterFullscreen() {
3617
4097
  try {
3618
4098
  await document.documentElement.requestFullscreen();
3619
4099
  } catch {
3620
4100
  }
3621
4101
  }
3622
- return /* @__PURE__ */ jsxRuntime.jsx("footer", { className: "flex flex-wrap items-center justify-center gap-2 border-t px-3 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs(TooltipProvider, { children: [
3623
- showMediaControls ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3624
- /* @__PURE__ */ jsxRuntime.jsx(
4102
+ async function onDeviceChange(kind, deviceId) {
4103
+ persistStudioDeviceSelection(kind, deviceId);
4104
+ try {
4105
+ await meeting.setDevice(kind, deviceId);
4106
+ } catch {
4107
+ }
4108
+ }
4109
+ const showStageModerator = session.role === "moderator" && canManageStage;
4110
+ const showStageGuest = session.role === "guest" && stageEnabled;
4111
+ const captionsLabel = !captionsAvailable ? labels.captions : !captionsReady ? labels.captionsNotReady : captionsOn ? labels.captionsOn : labels.captionsOff;
4112
+ return /* @__PURE__ */ jsxRuntime.jsx("footer", { className: "flex flex-wrap items-center justify-center gap-3 border-t px-3 py-3 sm:justify-between", children: /* @__PURE__ */ jsxRuntime.jsxs(TooltipProvider, { children: [
4113
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
4114
+ showMediaControls ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4115
+ /* @__PURE__ */ jsxRuntime.jsx(
4116
+ MediaSplit,
4117
+ {
4118
+ label: muted ? labels.unmute : labels.mute,
4119
+ pressed: muted,
4120
+ tone: muted ? "danger" : "default",
4121
+ onClick: onToggleMute,
4122
+ pickerLabel: labels.micDevice,
4123
+ options: devices.audio,
4124
+ selected: effective.audio,
4125
+ onSelect: (id) => void onDeviceChange("audio", id),
4126
+ children: muted ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, {})
4127
+ }
4128
+ ),
4129
+ /* @__PURE__ */ jsxRuntime.jsx(
4130
+ MediaSplit,
4131
+ {
4132
+ label: labels.camera,
4133
+ pressed: cameraOn,
4134
+ onClick: onToggleCamera,
4135
+ pickerLabel: labels.cameraDevice,
4136
+ options: devices.video,
4137
+ selected: effective.video,
4138
+ onSelect: (id) => void onDeviceChange("video", id),
4139
+ children: cameraOn ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.VideoOff, {})
4140
+ }
4141
+ )
4142
+ ] }) : null,
4143
+ showStageModerator ? /* @__PURE__ */ jsxRuntime.jsx(
3625
4144
  ControlIcon,
3626
4145
  {
3627
- label: muted ? labels.unmute : labels.mute,
3628
- pressed: muted,
3629
- tone: muted ? "danger" : "default",
3630
- onClick: onToggleMute,
3631
- children: muted ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, {})
4146
+ label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : labels.goOnAir,
4147
+ pressed: selfStageStatus !== "ON_STAGE",
4148
+ disabled: selfStageStatus === "ON_STAGE" && !canLeaveStage,
4149
+ onClick: () => {
4150
+ if (selfStageStatus === "ON_STAGE") {
4151
+ if (!canLeaveStage) {
4152
+ sonner.toast.message(labels.offAirLastVisible);
4153
+ return;
4154
+ }
4155
+ onLeaveStage();
4156
+ return;
4157
+ }
4158
+ onAskStage();
4159
+ },
4160
+ children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
3632
4161
  }
3633
- ),
3634
- /* @__PURE__ */ jsxRuntime.jsx(
4162
+ ) : null,
4163
+ showStageGuest ? /* @__PURE__ */ jsxRuntime.jsx(
3635
4164
  ControlIcon,
3636
4165
  {
3637
- label: labels.camera,
3638
- pressed: cameraOn,
3639
- onClick: onToggleCamera,
3640
- children: cameraOn ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.VideoOff, {})
3641
- }
3642
- )
3643
- ] }) : null,
3644
- session.role === "moderator" && canManageStage ? /* @__PURE__ */ jsxRuntime.jsx(
3645
- ControlIcon,
3646
- {
3647
- label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : labels.goOnAir,
3648
- pressed: selfStageStatus !== "ON_STAGE",
3649
- disabled: selfStageStatus === "ON_STAGE" && !canLeaveStage,
3650
- onClick: () => {
3651
- if (selfStageStatus === "ON_STAGE") {
3652
- if (!canLeaveStage) {
3653
- sonner.toast.message(labels.offAirLastVisible);
4166
+ label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
4167
+ pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
4168
+ disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
4169
+ onClick: () => {
4170
+ if (selfStageStatus === "ON_STAGE") {
4171
+ if (!canLeaveStage) {
4172
+ sonner.toast.message(labels.offAirLastVisible);
4173
+ return;
4174
+ }
4175
+ onLeaveStage();
3654
4176
  return;
3655
4177
  }
3656
- onLeaveStage();
3657
- return;
4178
+ onAskStage();
4179
+ },
4180
+ children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
4181
+ }
4182
+ ) : null,
4183
+ showScreenControl ? canShareFreely ? /* @__PURE__ */ jsxRuntime.jsx(
4184
+ ControlIcon,
4185
+ {
4186
+ label: labels.screen,
4187
+ pressed: screenOn,
4188
+ disabled: screenPending || !screenAvailable && !screenOn,
4189
+ pending: screenPending,
4190
+ onClick: onToggleScreen,
4191
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {})
4192
+ }
4193
+ ) : shareGrant === "requested" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4194
+ /* @__PURE__ */ jsxRuntime.jsx(
4195
+ ControlIcon,
4196
+ {
4197
+ label: labels.shareRequested,
4198
+ pressed: true,
4199
+ disabled: true,
4200
+ onClick: () => void 0,
4201
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {})
3658
4202
  }
3659
- onAskStage();
3660
- },
3661
- children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
3662
- }
3663
- ) : null,
3664
- session.role === "guest" && stageEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
3665
- ControlIcon,
3666
- {
3667
- label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
3668
- pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
3669
- disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
3670
- onClick: () => {
3671
- if (selfStageStatus === "ON_STAGE") {
3672
- if (!canLeaveStage) {
3673
- sonner.toast.message(labels.offAirLastVisible);
3674
- return;
3675
- }
3676
- onLeaveStage();
3677
- return;
4203
+ ),
4204
+ /* @__PURE__ */ jsxRuntime.jsx(
4205
+ ControlIcon,
4206
+ {
4207
+ label: labels.cancelShareRequest,
4208
+ onClick: onCancelShareRequest,
4209
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, {})
3678
4210
  }
3679
- onAskStage();
3680
- },
3681
- children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
3682
- }
3683
- ) : null,
3684
- showScreenControl ? canShareFreely ? /* @__PURE__ */ jsxRuntime.jsx(
3685
- ControlIcon,
3686
- {
3687
- label: labels.screen,
3688
- pressed: screenOn,
3689
- disabled: screenPending || !screenAvailable && !screenOn,
3690
- pending: screenPending,
3691
- onClick: onToggleScreen,
3692
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {})
3693
- }
3694
- ) : shareGrant === "requested" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4211
+ )
4212
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ControlIcon, { label: labels.askToShare, onClick: onAskToShare, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {}) }) : null,
3695
4213
  /* @__PURE__ */ jsxRuntime.jsx(
3696
4214
  ControlIcon,
3697
4215
  {
3698
- label: labels.shareRequested,
3699
- pressed: true,
3700
- disabled: true,
3701
- onClick: () => void 0,
3702
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {})
4216
+ label: captionsLabel,
4217
+ pressed: captionsOn,
4218
+ disabled: !captionsAvailable,
4219
+ onClick: onToggleCaptions,
4220
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Captions, {})
3703
4221
  }
3704
4222
  ),
4223
+ /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
4224
+ /* @__PURE__ */ jsxRuntime.jsx(
4225
+ DropdownMenuTrigger,
4226
+ {
4227
+ render: /* @__PURE__ */ jsxRuntime.jsx(
4228
+ Button,
4229
+ {
4230
+ type: "button",
4231
+ size: "icon-lg",
4232
+ variant: "secondary",
4233
+ "aria-label": labels.more
4234
+ }
4235
+ ),
4236
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreVertical, {})
4237
+ }
4238
+ ),
4239
+ /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuContent, { align: "center", className: "min-w-44", children: [
4240
+ fullscreenSupported ? /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuItem, { onClick: () => void enterFullscreen(), children: [
4241
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize, {}),
4242
+ labels.fullscreen
4243
+ ] }) : null,
4244
+ pipSupported ? /* @__PURE__ */ jsxRuntime.jsxs(
4245
+ DropdownMenuItem,
4246
+ {
4247
+ onClick: () => {
4248
+ if (pipOpen) onClosePip();
4249
+ else onOpenPip();
4250
+ },
4251
+ children: [
4252
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PictureInPicture2, {}),
4253
+ pipOpen ? labels.closePip : labels.openPip
4254
+ ]
4255
+ }
4256
+ ) : null,
4257
+ /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuItem, { onClick: onOpenSettings, children: [
4258
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, {}),
4259
+ labels.settings
4260
+ ] })
4261
+ ] })
4262
+ ] })
4263
+ ] }),
4264
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
3705
4265
  /* @__PURE__ */ jsxRuntime.jsx(
3706
4266
  ControlIcon,
3707
4267
  {
3708
- label: labels.cancelShareRequest,
3709
- onClick: onCancelShareRequest,
3710
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, {})
4268
+ label: unreadChat > 0 ? `${labels.chat} (${unreadChat})` : labels.chat,
4269
+ pressed: chatOpen,
4270
+ badge: unreadChat,
4271
+ onClick: onToggleChat,
4272
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquare, {})
3711
4273
  }
3712
- )
3713
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(ControlIcon, { label: labels.askToShare, onClick: onAskToShare, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {}) }) : null,
3714
- /* @__PURE__ */ jsxRuntime.jsx(
3715
- ControlIcon,
3716
- {
3717
- label: unreadChat > 0 ? `${labels.chat} (${unreadChat})` : labels.chat,
3718
- pressed: chatOpen,
3719
- badge: unreadChat,
3720
- onClick: onToggleChat,
3721
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquare, {})
3722
- }
3723
- ),
3724
- session.role === "moderator" ? /* @__PURE__ */ jsxRuntime.jsx(
3725
- ControlIcon,
3726
- {
3727
- label: peopleBadge > 0 ? `${labels.people} (${peopleBadge})` : labels.people,
3728
- pressed: peopleOpen,
3729
- badge: peopleBadge,
3730
- onClick: onTogglePeople,
3731
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, {})
3732
- }
3733
- ) : null,
3734
- fullscreenSupported ? /* @__PURE__ */ jsxRuntime.jsx(
3735
- ControlIcon,
3736
- {
3737
- label: labels.fullscreen,
3738
- onClick: () => void enterFullscreen(),
3739
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize, {})
3740
- }
3741
- ) : null,
3742
- pipSupported ? /* @__PURE__ */ jsxRuntime.jsx(
3743
- ControlIcon,
3744
- {
3745
- label: pipOpen ? labels.closePip : labels.openPip,
3746
- pressed: pipOpen,
3747
- onClick: () => {
3748
- if (pipOpen) onClosePip();
3749
- else onOpenPip();
3750
- },
3751
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PictureInPicture2, {})
3752
- }
3753
- ) : null,
4274
+ ),
4275
+ session.role === "moderator" ? /* @__PURE__ */ jsxRuntime.jsx(
4276
+ ControlIcon,
4277
+ {
4278
+ label: peopleBadge > 0 ? `${labels.people} (${peopleBadge})` : labels.people,
4279
+ pressed: peopleOpen,
4280
+ badge: peopleBadge,
4281
+ onClick: onTogglePeople,
4282
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, {})
4283
+ }
4284
+ ) : null,
4285
+ /* @__PURE__ */ jsxRuntime.jsx(
4286
+ ControlIcon,
4287
+ {
4288
+ label: labels.settings,
4289
+ pressed: settingsOpen,
4290
+ onClick: onOpenSettings,
4291
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, {})
4292
+ }
4293
+ ),
4294
+ /* @__PURE__ */ jsxRuntime.jsx(
4295
+ StudioSettingsDialog,
4296
+ {
4297
+ open: settingsOpen,
4298
+ onOpenChange: onCloseSettings,
4299
+ meeting,
4300
+ reducedMotion,
4301
+ cameraOn,
4302
+ labels,
4303
+ captionPrefs,
4304
+ onCaptionPrefsChange,
4305
+ captionsAvailable,
4306
+ effectBackgrounds
4307
+ }
4308
+ ),
4309
+ session.role === "guest" ? /* @__PURE__ */ jsxRuntime.jsx(
4310
+ ConfirmAction,
4311
+ {
4312
+ triggerLabel: labels.leave,
4313
+ title: labels.leaveTitle,
4314
+ description: labels.leaveDescription,
4315
+ confirmLabel: labels.leaveConfirm,
4316
+ pending,
4317
+ onConfirm: onLeave,
4318
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(
4319
+ Button,
4320
+ {
4321
+ type: "button",
4322
+ variant: "destructive",
4323
+ size: "icon-lg",
4324
+ disabled: pending,
4325
+ "aria-label": labels.leave,
4326
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PhoneOff, {})
4327
+ }
4328
+ )
4329
+ }
4330
+ ) : null
4331
+ ] })
4332
+ ] }) });
4333
+ }
4334
+ function MediaSplit({
4335
+ label,
4336
+ pressed,
4337
+ tone = "default",
4338
+ onClick,
4339
+ pickerLabel,
4340
+ options,
4341
+ selected,
4342
+ onSelect,
4343
+ children
4344
+ }) {
4345
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center", children: [
3754
4346
  /* @__PURE__ */ jsxRuntime.jsx(
3755
4347
  ControlIcon,
3756
4348
  {
3757
- label: labels.settings,
3758
- pressed: settingsOpen,
3759
- onClick: onOpenSettings,
3760
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, {})
4349
+ label,
4350
+ pressed,
4351
+ tone,
4352
+ onClick,
4353
+ children
3761
4354
  }
3762
4355
  ),
3763
- /* @__PURE__ */ jsxRuntime.jsx(
3764
- StudioSettingsDialog,
3765
- {
3766
- open: settingsOpen,
3767
- onOpenChange: onCloseSettings,
3768
- meeting,
3769
- reducedMotion,
3770
- cameraOn,
3771
- labels: {
3772
- settingsTitle: labels.settingsTitle,
3773
- settingsDescription: labels.settingsDescription,
3774
- guestName: labels.guestName,
3775
- displayNameSave: labels.displayNameSave,
3776
- displayNameSaved: labels.displayNameSaved,
3777
- displayNameError: labels.displayNameError,
3778
- cameraDevice: labels.cameraDevice,
3779
- micDevice: labels.micDevice,
3780
- speakerDevice: labels.speakerDevice,
3781
- testSpeaker: labels.testSpeaker,
3782
- testSpeakerError: labels.testSpeakerError,
3783
- cameraOff: labels.cameraOff,
3784
- micLevel: labels.micLevel
4356
+ options.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
4357
+ /* @__PURE__ */ jsxRuntime.jsx(
4358
+ DropdownMenuTrigger,
4359
+ {
4360
+ render: /* @__PURE__ */ jsxRuntime.jsx(
4361
+ Button,
4362
+ {
4363
+ type: "button",
4364
+ size: "icon-xs",
4365
+ variant: "secondary",
4366
+ className: "-ml-1",
4367
+ "aria-label": pickerLabel
4368
+ }
4369
+ ),
4370
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronUp, { className: "size-3.5" })
3785
4371
  }
3786
- }
3787
- ),
3788
- session.role === "guest" ? /* @__PURE__ */ jsxRuntime.jsx(
3789
- ConfirmAction,
3790
- {
3791
- triggerLabel: labels.leave,
3792
- title: labels.leaveTitle,
3793
- description: labels.leaveDescription,
3794
- confirmLabel: labels.leaveConfirm,
3795
- pending,
3796
- onConfirm: onLeave,
3797
- trigger: /* @__PURE__ */ jsxRuntime.jsx(
3798
- Button,
3799
- {
3800
- type: "button",
3801
- variant: "destructive",
3802
- size: "icon-lg",
3803
- disabled: pending,
3804
- "aria-label": labels.leave,
3805
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PhoneOff, {})
3806
- }
4372
+ ),
4373
+ /* @__PURE__ */ jsxRuntime.jsx(DropdownMenuContent, { align: "start", side: "top", className: "min-w-52", children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsxs(
4374
+ DropdownMenuItem,
4375
+ {
4376
+ onClick: () => onSelect(option.deviceId),
4377
+ children: [
4378
+ option.deviceId === selected ? "\u2713 " : "",
4379
+ option.label
4380
+ ]
4381
+ },
4382
+ option.deviceId
4383
+ )) })
4384
+ ] }) : null
4385
+ ] });
4386
+ }
4387
+ var POLL_MS = 2e3;
4388
+ var PLAYOUT_MS = 250;
4389
+ var STUDIO_EXTRA_LAG_MS = 200;
4390
+ function useStudioCaptions({
4391
+ enabled,
4392
+ baseUrl,
4393
+ locale,
4394
+ streamStatus
4395
+ }) {
4396
+ const [playout, setPlayout] = react.useState(
4397
+ video.EMPTY_LIVE_CAPTION_PLAYOUT
4398
+ );
4399
+ const cuesRef = react.useRef([]);
4400
+ react.useEffect(() => {
4401
+ if (!enabled) {
4402
+ cuesRef.current = [];
4403
+ setPlayout(video.EMPTY_LIVE_CAPTION_PLAYOUT);
4404
+ return;
4405
+ }
4406
+ if (!baseUrl || !isStudioActiveStatus(streamStatus)) return;
4407
+ let cancelled = false;
4408
+ const url = video.liveCaptionsUrl(baseUrl, locale);
4409
+ const poll = async () => {
4410
+ const loaded = await video.loadLiveCaptions(url);
4411
+ if (cancelled || !loaded.ok) return;
4412
+ cuesRef.current = video.mergeLiveCaptionCues(cuesRef.current, loaded.cues);
4413
+ };
4414
+ void poll();
4415
+ const timer = window.setInterval(() => void poll(), POLL_MS);
4416
+ return () => {
4417
+ cancelled = true;
4418
+ window.clearInterval(timer);
4419
+ };
4420
+ }, [baseUrl, enabled, locale, streamStatus]);
4421
+ react.useEffect(() => {
4422
+ if (!enabled || !baseUrl || !isStudioActiveStatus(streamStatus)) {
4423
+ setPlayout(video.EMPTY_LIVE_CAPTION_PLAYOUT);
4424
+ return;
4425
+ }
4426
+ const tick2 = () => {
4427
+ setPlayout(
4428
+ (prev) => video.advanceLiveCaptionPlayout(
4429
+ prev,
4430
+ cuesRef.current,
4431
+ Date.now(),
4432
+ 0,
4433
+ void 0,
4434
+ STUDIO_EXTRA_LAG_MS
3807
4435
  )
3808
- }
3809
- ) : null
3810
- ] }) });
4436
+ );
4437
+ };
4438
+ tick2();
4439
+ const timer = window.setInterval(tick2, PLAYOUT_MS);
4440
+ return () => window.clearInterval(timer);
4441
+ }, [baseUrl, enabled, streamStatus]);
4442
+ return playout;
3811
4443
  }
3812
4444
  var STAGE_GRANT_VERIFY_MS = 4e3;
3813
4445
  var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
@@ -3826,7 +4458,10 @@ function StudioRoom({
3826
4458
  initialCameraOn = true,
3827
4459
  initialMuted = false,
3828
4460
  initialDevices,
3829
- appDownloadUrl
4461
+ appDownloadUrl,
4462
+ captionsBaseUrl,
4463
+ apiUrl,
4464
+ cdnBase
3830
4465
  }) {
3831
4466
  const [cameraOn, setCameraOn] = react.useState(initialCameraOn);
3832
4467
  const [screenOn, setScreenOn] = react.useState(false);
@@ -3848,6 +4483,11 @@ function StudioRoom({
3848
4483
  const [egressError, setEgressError] = react.useState(null);
3849
4484
  const [shareGrant, setShareGrant] = react.useState("idle");
3850
4485
  const [downloadAppOpen, setDownloadAppOpen] = react.useState(false);
4486
+ const [captionsOn, setCaptionsOn] = react.useState(false);
4487
+ const [captionPrefs, setCaptionPrefs] = react.useState(
4488
+ () => readStudioCaptionPrefs()
4489
+ );
4490
+ const captionHintShown = react.useRef(false);
3851
4491
  const [connectTimedOut, setConnectTimedOut] = react.useState(false);
3852
4492
  const [screenPermission, setScreenPermission] = react.useState(
3853
4493
  () => meeting.screenSharePermission()
@@ -3873,6 +4513,19 @@ function StudioRoom({
3873
4513
  const pip = useDocumentPictureInPicture();
3874
4514
  const gateFromContext = useStudioWindowGate();
3875
4515
  const { reportDuplicate } = windowGate ?? gateFromContext;
4516
+ const resolvedCaptionsBaseUrl = captionsBaseUrl ?? studioCaptionsBaseUrl(apiUrl, session.stream.id);
4517
+ const captionsAvailable = session.stream.captionsEnabled !== false;
4518
+ const captionsReady = isStudioActiveStatus(session.stream.status);
4519
+ const captionPlayout = useStudioCaptions({
4520
+ enabled: captionsOn && captionsAvailable,
4521
+ baseUrl: resolvedCaptionsBaseUrl,
4522
+ locale: captionPrefs.locale,
4523
+ streamStatus: session.stream.status
4524
+ });
4525
+ const effectBackgrounds = react.useMemo(
4526
+ () => defaultStudioEffectBackgrounds(cdnBase),
4527
+ [cdnBase]
4528
+ );
3876
4529
  const onAir = isLiveStudioStatus(session.stream.status);
3877
4530
  const active = isStudioActiveStatus(session.stream.status);
3878
4531
  if (onAir && liveStartedAtRef.current === null) {
@@ -4557,6 +5210,10 @@ function StudioRoom({
4557
5210
  labels,
4558
5211
  settingsOpen,
4559
5212
  statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
5213
+ captionPrefs,
5214
+ onCaptionPrefsChange: setCaptionPrefs,
5215
+ captionsAvailable,
5216
+ effectBackgrounds,
4560
5217
  onToggleCamera,
4561
5218
  onToggleMute: () => void onToggleMute(),
4562
5219
  onOpenSettings: () => setSettingsOpen(true),
@@ -4664,27 +5321,37 @@ function StudioRoom({
4664
5321
  }
4665
5322
  ) : null,
4666
5323
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col md:flex-row", children: [
4667
- /* @__PURE__ */ jsxRuntime.jsx(
4668
- StudioStage,
4669
- {
4670
- participants: stageParticipants,
4671
- meeting,
4672
- reducedMotion,
4673
- canModerate: session.role === "moderator",
4674
- canManageStage,
4675
- onAir,
4676
- labels,
4677
- onKick: (id) => void onKick(id),
4678
- onMute: (id) => void onHostMedia(id, "audio"),
4679
- onStopCamera: (id) => void onHostMedia(id, "video"),
4680
- onStopScreen: (id) => void onHostMedia(id, "screen"),
4681
- onToggleStage: (tile) => void onSetStage(tile, !isOnStage(tile.stageStatus)),
4682
- onTogglePin: (tile) => void onTogglePin(tile),
4683
- onToggleChat: (tile) => void onToggleChat(tile),
4684
- canTakeOff: (id) => canTakeOffAir(participants, id, onAir),
4685
- onOpenPeople: () => setSideTab("people")
4686
- }
4687
- ),
5324
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex min-h-0 min-w-0 flex-1 flex-col", children: [
5325
+ /* @__PURE__ */ jsxRuntime.jsx(
5326
+ StudioStage,
5327
+ {
5328
+ participants: stageParticipants,
5329
+ meeting,
5330
+ reducedMotion,
5331
+ canModerate: session.role === "moderator",
5332
+ canManageStage,
5333
+ onAir,
5334
+ labels,
5335
+ onKick: (id) => void onKick(id),
5336
+ onMute: (id) => void onHostMedia(id, "audio"),
5337
+ onStopCamera: (id) => void onHostMedia(id, "video"),
5338
+ onStopScreen: (id) => void onHostMedia(id, "screen"),
5339
+ onToggleStage: (tile) => void onSetStage(tile, !isOnStage(tile.stageStatus)),
5340
+ onTogglePin: (tile) => void onTogglePin(tile),
5341
+ onToggleChat: (tile) => void onToggleChat(tile),
5342
+ canTakeOff: (id) => canTakeOffAir(participants, id, onAir),
5343
+ onOpenPeople: () => setSideTab("people")
5344
+ }
5345
+ ),
5346
+ captionsOn ? /* @__PURE__ */ jsxRuntime.jsx(
5347
+ video.CaptionOverlay,
5348
+ {
5349
+ screen: captionPlayout.screen,
5350
+ className: "bottom-3",
5351
+ textClassName: captionSizeClass(captionPrefs.size)
5352
+ }
5353
+ ) : null
5354
+ ] }),
4688
5355
  sideTab ? /* @__PURE__ */ jsxRuntime.jsx(
4689
5356
  StudioSidePanel,
4690
5357
  {
@@ -4797,6 +5464,12 @@ function StudioRoom({
4797
5464
  stageEnabled,
4798
5465
  canLeaveStage: selfId != null && canTakeOffAir(participants, selfId, onAir),
4799
5466
  reducedMotion,
5467
+ captionsOn,
5468
+ captionsAvailable,
5469
+ captionsReady,
5470
+ captionPrefs,
5471
+ onCaptionPrefsChange: setCaptionPrefs,
5472
+ effectBackgrounds,
4800
5473
  onToggleMute: () => void onToggleMute(),
4801
5474
  onToggleCamera,
4802
5475
  onToggleScreen,
@@ -4824,6 +5497,16 @@ function StudioRoom({
4824
5497
  onCloseSettings: setSettingsOpen,
4825
5498
  onOpenPip,
4826
5499
  onClosePip: pip.close,
5500
+ onToggleCaptions: () => {
5501
+ if (!captionsAvailable) return;
5502
+ if (!captionsOn && !captionHintShown.current) {
5503
+ captionHintShown.current = true;
5504
+ sonner.toast.message(
5505
+ captionsReady ? labels.captionsDelayed : labels.captionsNotReady
5506
+ );
5507
+ }
5508
+ setCaptionsOn((current) => !current);
5509
+ },
4827
5510
  onLeave: () => void onLeave()
4828
5511
  }
4829
5512
  ),
@@ -5218,16 +5901,16 @@ function usePrefersReducedMotion() {
5218
5901
  // src/studio/lib/guest-identity.ts
5219
5902
  var NAME_KEY = "livo-studio-display-name";
5220
5903
  var ID_KEY = "livo-studio-guest-id";
5221
- function readStudioGuestIdentity(storage = defaultStorage2()) {
5904
+ function readStudioGuestIdentity(storage = defaultStorage4()) {
5222
5905
  const guestId = storage?.getItem(ID_KEY)?.trim() || crypto.randomUUID();
5223
5906
  storage?.setItem(ID_KEY, guestId);
5224
5907
  const displayName = storage?.getItem(NAME_KEY)?.trim() ?? "";
5225
5908
  return { displayName, guestId };
5226
5909
  }
5227
- function persistStudioGuestName(displayName, storage = defaultStorage2()) {
5910
+ function persistStudioGuestName(displayName, storage = defaultStorage4()) {
5228
5911
  storage?.setItem(NAME_KEY, displayName.trim());
5229
5912
  }
5230
- function defaultStorage2() {
5913
+ function defaultStorage4() {
5231
5914
  if (typeof window === "undefined") return null;
5232
5915
  try {
5233
5916
  return window.localStorage;
@@ -5596,10 +6279,19 @@ function studioMeetingFromRtk(raw) {
5596
6279
  if (typeof window !== "undefined") {
5597
6280
  window.addEventListener("unhandledrejection", onUnhandledRejection);
5598
6281
  }
6282
+ let videoEffects = readStudioVideoEffects();
6283
+ async function applyCurrentVideoEffects() {
6284
+ await applyStudioVideoEffects({
6285
+ self: meeting.self,
6286
+ meeting,
6287
+ config: videoEffects
6288
+ });
6289
+ }
5599
6290
  async function republishCameraIfOn() {
5600
6291
  if (meeting.self?.videoEnabled !== true) return;
5601
6292
  await meeting.self.disableVideo?.();
5602
6293
  await meeting.self.enableVideo?.();
6294
+ await applyCurrentVideoEffects();
5603
6295
  }
5604
6296
  async function acceptAllowedStageIfOffAir() {
5605
6297
  if (currentStageStatus() !== "OFF_STAGE") return;
@@ -5787,6 +6479,7 @@ function studioMeetingFromRtk(raw) {
5787
6479
  async join() {
5788
6480
  await meeting.join?.();
5789
6481
  await joinStageIfAllowed();
6482
+ await applyCurrentVideoEffects();
5790
6483
  },
5791
6484
  async leave() {
5792
6485
  if (typeof window !== "undefined") {
@@ -5799,8 +6492,10 @@ function studioMeetingFromRtk(raw) {
5799
6492
  else await meeting.self?.disableAudio?.();
5800
6493
  },
5801
6494
  async setVideoEnabled(enabled) {
5802
- if (enabled) await meeting.self?.enableVideo?.();
5803
- else await meeting.self?.disableVideo?.();
6495
+ if (enabled) {
6496
+ await meeting.self?.enableVideo?.();
6497
+ await applyCurrentVideoEffects();
6498
+ } else await meeting.self?.disableVideo?.();
5804
6499
  },
5805
6500
  async setScreenShareEnabled(enabled) {
5806
6501
  if (enabled) await meeting.self?.enableScreenShare?.();
@@ -6184,6 +6879,14 @@ function studioMeetingFromRtk(raw) {
6184
6879
  }
6185
6880
  });
6186
6881
  },
6882
+ async setVideoEffects(config) {
6883
+ videoEffects = config;
6884
+ persistStudioVideoEffects(config);
6885
+ await applyCurrentVideoEffects();
6886
+ },
6887
+ videoEffects() {
6888
+ return videoEffects;
6889
+ },
6187
6890
  async setDisplayName(name) {
6188
6891
  const trimmed = name.trim().slice(0, STUDIO_DISPLAY_NAME_MAX);
6189
6892
  if (!trimmed) return;
@@ -6415,6 +7118,21 @@ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn
6415
7118
  return null;
6416
7119
  }
6417
7120
  }
7121
+ async function getPublicStreamMeta(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
7122
+ try {
7123
+ const body = await publicJson(
7124
+ `${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
7125
+ {},
7126
+ fetchFn
7127
+ );
7128
+ return parsePublicStreamMeta(body);
7129
+ } catch (error) {
7130
+ if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
7131
+ return { status: "ended", captionsEnabled: false };
7132
+ }
7133
+ return null;
7134
+ }
7135
+ }
6418
7136
  var POLL_INTERVAL_MS = 5e3;
6419
7137
  function LivoHostStudio({
6420
7138
  token,
@@ -6430,6 +7148,9 @@ function LivoHostStudio({
6430
7148
  const reducedMotion = usePrefersReducedMotion();
6431
7149
  const [session, setSession] = react.useState(null);
6432
7150
  const [status, setStatus] = react.useState(null);
7151
+ const [captionsEnabled, setCaptionsEnabled] = react.useState(
7152
+ void 0
7153
+ );
6433
7154
  const [error, setError] = react.useState(null);
6434
7155
  const { meeting, error: meetingError } = useRealtimeKitMeeting(
6435
7156
  session?.authToken ?? null
@@ -6478,6 +7199,20 @@ function LivoHostStudio({
6478
7199
  window.clearInterval(id);
6479
7200
  };
6480
7201
  }, [client, status]);
7202
+ react.useEffect(() => {
7203
+ if (!session) return;
7204
+ let cancelled = false;
7205
+ const poll = async () => {
7206
+ const next = await getPublicStreamMeta(session.stream.id, apiUrl);
7207
+ if (!cancelled && next) setCaptionsEnabled(next.captionsEnabled);
7208
+ };
7209
+ void poll();
7210
+ const id = window.setInterval(() => void poll(), POLL_INTERVAL_MS);
7211
+ return () => {
7212
+ cancelled = true;
7213
+ window.clearInterval(id);
7214
+ };
7215
+ }, [apiUrl, session]);
6481
7216
  const liveSession = react.useMemo(() => {
6482
7217
  if (!session) return null;
6483
7218
  if (!status) return session;
@@ -6485,10 +7220,11 @@ function LivoHostStudio({
6485
7220
  ...session,
6486
7221
  stream: {
6487
7222
  ...session.stream,
6488
- status
7223
+ status,
7224
+ captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
6489
7225
  }
6490
7226
  };
6491
- }, [session, status]);
7227
+ }, [captionsEnabled, session, status]);
6492
7228
  if (error || meetingError) {
6493
7229
  return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive p-6", role: "alert", children: error ?? meetingError });
6494
7230
  }
@@ -6506,7 +7242,8 @@ function LivoHostStudio({
6506
7242
  embed,
6507
7243
  chrome,
6508
7244
  playerOrigin,
6509
- appDownloadUrl
7245
+ appDownloadUrl,
7246
+ apiUrl
6510
7247
  }
6511
7248
  );
6512
7249
  }
@@ -6525,6 +7262,9 @@ function LivoGuestStudio({
6525
7262
  const reducedMotion = usePrefersReducedMotion();
6526
7263
  const [session, setSession] = react.useState(null);
6527
7264
  const [status, setStatus] = react.useState(null);
7265
+ const [captionsEnabled, setCaptionsEnabled] = react.useState(
7266
+ void 0
7267
+ );
6528
7268
  const [error, setError] = react.useState(null);
6529
7269
  const [displayName, setDisplayName] = react.useState("");
6530
7270
  const [guestId, setGuestId] = react.useState("");
@@ -6628,8 +7368,11 @@ function LivoGuestStudio({
6628
7368
  if (current === "ended" || current === "error") return;
6629
7369
  let cancelled = false;
6630
7370
  const poll = async () => {
6631
- const next = await getPublicStreamStatus(session.stream.id, apiUrl);
6632
- if (!cancelled && next) setStatus(next);
7371
+ const next = await getPublicStreamMeta(session.stream.id, apiUrl);
7372
+ if (!cancelled && next) {
7373
+ setStatus(next.status);
7374
+ setCaptionsEnabled(next.captionsEnabled);
7375
+ }
6633
7376
  };
6634
7377
  void poll();
6635
7378
  const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
@@ -6644,9 +7387,13 @@ function LivoGuestStudio({
6644
7387
  if (!status) return session;
6645
7388
  return {
6646
7389
  ...session,
6647
- stream: { ...session.stream, status }
7390
+ stream: {
7391
+ ...session.stream,
7392
+ status,
7393
+ captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
7394
+ }
6648
7395
  };
6649
- }, [session, status]);
7396
+ }, [captionsEnabled, session, status]);
6650
7397
  react.useEffect(() => {
6651
7398
  if (retryAfterMs == null) return;
6652
7399
  const id = window.setTimeout(() => {
@@ -6707,7 +7454,8 @@ function LivoGuestStudio({
6707
7454
  initialCameraOn: cameraOn,
6708
7455
  initialMuted: muted,
6709
7456
  initialDevices: deviceSelection,
6710
- appDownloadUrl
7457
+ appDownloadUrl,
7458
+ apiUrl
6711
7459
  }
6712
7460
  );
6713
7461
  }
@@ -6883,7 +7631,37 @@ function studioRoomLabels(t) {
6883
7631
  chatPin: t("chatPin"),
6884
7632
  chatUnpin: t("chatUnpin"),
6885
7633
  chatSave: t("chatSave"),
6886
- waitingSkeleton: t("waitingSkeleton")
7634
+ waitingSkeleton: t("waitingSkeleton"),
7635
+ more: t("more"),
7636
+ captions: t("captions"),
7637
+ captionsOn: t("captionsOn"),
7638
+ captionsOff: t("captionsOff"),
7639
+ captionsNotReady: t("captionsNotReady"),
7640
+ captionsDelayed: t("captionsDelayed"),
7641
+ captionsLanguage: t("captionsLanguage"),
7642
+ captionsSize: t("captionsSize"),
7643
+ captionsSizeS: t("captionsSizeS"),
7644
+ captionsSizeM: t("captionsSizeM"),
7645
+ captionsSizeL: t("captionsSizeL"),
7646
+ settingsGeneral: t("settingsGeneral"),
7647
+ settingsAudio: t("settingsAudio"),
7648
+ settingsVideo: t("settingsVideo"),
7649
+ settingsEffects: t("settingsEffects"),
7650
+ settingsSubtitles: t("settingsSubtitles"),
7651
+ effectNone: t("effectNone"),
7652
+ effectBlur: t("effectBlur"),
7653
+ effectBlurStrong: t("effectBlurStrong"),
7654
+ effectBackgrounds: t("effectBackgrounds"),
7655
+ effectFilters: t("effectFilters"),
7656
+ effectFilterNone: t("effectFilterNone"),
7657
+ effectFilterGrayscale: t("effectFilterGrayscale"),
7658
+ effectFilterSepia: t("effectFilterSepia"),
7659
+ effectFilterWarm: t("effectFilterWarm"),
7660
+ effectFilterCool: t("effectFilterCool"),
7661
+ effectFilterBright: t("effectFilterBright"),
7662
+ localeEn: t("localeEn"),
7663
+ localeEs: t("localeEs"),
7664
+ localePt: t("localePt")
6887
7665
  };
6888
7666
  }
6889
7667
 
@@ -6901,6 +7679,7 @@ exports.applyEmbedTheme = applyEmbedTheme;
6901
7679
  exports.controlActionsFromClient = controlActionsFromClient;
6902
7680
  exports.createStudioControlClient = createStudioControlClient;
6903
7681
  exports.decodeEmbedTheme = decodeEmbedTheme;
7682
+ exports.getPublicStreamMeta = getPublicStreamMeta;
6904
7683
  exports.getPublicStreamStatus = getPublicStreamStatus;
6905
7684
  exports.getStudioJoinStatus = getStudioJoinStatus;
6906
7685
  exports.joinStudioGuest = joinStudioGuest;