@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.
@@ -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';
@@ -690,7 +691,14 @@ function parseStudioStreamSummary(value) {
690
691
  const status = readString(value, "status");
691
692
  if (!id || !title || !status) return null;
692
693
  const encoderLostAt = isRecord(value) ? typeof value.encoderLostAt === "number" ? value.encoderLostAt : value.encoderLostAt === null ? null : void 0 : void 0;
693
- 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
+ };
694
702
  }
695
703
  function parseStudioSession(value) {
696
704
  if (!isRecord(value)) return null;
@@ -702,7 +710,9 @@ function parseStudioSession(value) {
702
710
  const fromFull = isRecord(value.stream) ? parseStudioStreamSummary({
703
711
  id: value.stream.id,
704
712
  title: value.stream.title,
705
- status: value.stream.status
713
+ status: value.stream.status,
714
+ captionsEnabled: value.stream.captionsEnabled,
715
+ captionsUrl: value.stream.captionsUrl
706
716
  }) : null;
707
717
  const stream = fromSummary ?? fromFull;
708
718
  if (!stream) return null;
@@ -735,6 +745,14 @@ function parsePublicStreamStatus(value) {
735
745
  const status = readString(nested, "status");
736
746
  return status;
737
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
+ }
738
756
  function studioErrorCode(error) {
739
757
  if (!error || typeof error !== "object") return null;
740
758
  const body = "body" in error ? error.body : null;
@@ -1614,6 +1632,211 @@ async function playSpeakerTest(deviceId) {
1614
1632
  await ctx3.close();
1615
1633
  }
1616
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
+ }
1617
1840
  function StudioDeviceSelectGroup({
1618
1841
  devices,
1619
1842
  selected,
@@ -1692,7 +1915,11 @@ function StudioSettingsDialog({
1692
1915
  meeting,
1693
1916
  labels,
1694
1917
  reducedMotion,
1695
- cameraOn
1918
+ cameraOn,
1919
+ captionPrefs,
1920
+ onCaptionPrefsChange,
1921
+ captionsAvailable,
1922
+ effectBackgrounds
1696
1923
  }) {
1697
1924
  const devices = useMediaDevices(open ? 1 : 0, open);
1698
1925
  const [saved, setSaved] = useState(
@@ -1704,11 +1931,16 @@ function StudioSettingsDialog({
1704
1931
  const [displayName, setDisplayName] = useState("");
1705
1932
  const [savedDisplayName, setSavedDisplayName] = useState("");
1706
1933
  const [savingDisplayName, setSavingDisplayName] = useState(false);
1934
+ const [tab, setTab] = useState("general");
1935
+ const [effects, setEffects] = useState(
1936
+ () => meeting.videoEffects()
1937
+ );
1707
1938
  const { effective } = resolveDeviceSelection(saved, devices);
1708
1939
  useEffect(() => {
1709
1940
  if (!open) return;
1710
1941
  setSaved(readStudioDeviceSelection());
1711
- }, [open]);
1942
+ setEffects(meeting.videoEffects());
1943
+ }, [meeting, open]);
1712
1944
  useEffect(() => {
1713
1945
  if (!open) return;
1714
1946
  const selfName = meeting.participants().find((row) => row.isSelf)?.name?.trim() ?? "";
@@ -1761,76 +1993,306 @@ function StudioSettingsDialog({
1761
1993
  setTestingSpeaker(false);
1762
1994
  }
1763
1995
  }
1764
- 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: [
1765
2012
  /* @__PURE__ */ jsxs(DialogHeader, { children: [
1766
2013
  /* @__PURE__ */ jsx(DialogTitle, { children: labels.settingsTitle }),
1767
2014
  /* @__PURE__ */ jsx(DialogDescription, { children: labels.settingsDescription })
1768
2015
  ] }),
1769
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1770
- /* @__PURE__ */ jsx(Label, { htmlFor: "studio-display-name", children: labels.guestName }),
1771
- /* @__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: [
1772
2161
  /* @__PURE__ */ jsx(
1773
- Input,
2162
+ EffectTile,
1774
2163
  {
1775
- id: "studio-display-name",
1776
- value: displayName,
1777
- maxLength: STUDIO_DISPLAY_NAME_MAX,
1778
- onChange: (event) => setDisplayName(event.target.value)
2164
+ label: labels.effectNone,
2165
+ active: effects.background === "none",
2166
+ onClick: () => onApply({ ...effects, background: "none" })
1779
2167
  }
1780
2168
  ),
1781
2169
  /* @__PURE__ */ jsx(
1782
- Button,
2170
+ EffectTile,
1783
2171
  {
1784
- type: "button",
1785
- variant: "outline",
1786
- disabled: displayNameSaveDisabled,
1787
- onClick: () => void onSaveDisplayName(),
1788
- 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 })
1789
2183
  }
1790
2184
  )
1791
2185
  ] })
1792
2186
  ] }),
1793
- /* @__PURE__ */ jsx(
1794
- SettingsCameraPreview,
1795
- {
1796
- track: videoTrack,
1797
- open,
1798
- cameraOn,
1799
- cameraOffLabel: labels.cameraOff
1800
- }
1801
- ),
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,
1802
2204
  /* @__PURE__ */ jsxs("div", { children: [
1803
- /* @__PURE__ */ jsx("p", { className: "sr-only", children: labels.micLevel }),
1804
- /* @__PURE__ */ jsx(
1805
- 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,
1806
2208
  {
1807
- track: audioTrack,
1808
- reducedMotion,
1809
- className: "w-full"
1810
- }
1811
- )
1812
- ] }),
1813
- /* @__PURE__ */ jsx(
1814
- StudioDeviceSelectGroup,
1815
- {
1816
- devices,
1817
- selected: effective,
1818
- labels,
1819
- onChange: (kind, id) => void onChange(kind, id),
1820
- speakerAction: /* @__PURE__ */ jsx(
1821
- 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",
1822
2239
  {
1823
- type: "button",
1824
- size: "sm",
1825
- variant: "outline",
1826
- disabled: testingSpeaker,
1827
- onClick: () => void onTestSpeaker(),
1828
- children: labels.testSpeaker
2240
+ className: "bg-muted aspect-video w-full bg-cover bg-center",
2241
+ style: previewUrl ? { backgroundImage: `url(${previewUrl})` } : void 0
1829
2242
  }
1830
- )
1831
- }
1832
- )
1833
- ] }) });
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
+ ] });
1834
2296
  }
1835
2297
  function SettingsCameraPreview({
1836
2298
  track,
@@ -1881,7 +2343,11 @@ function StudioGuestWaiting({
1881
2343
  onToggleCamera,
1882
2344
  onToggleMute,
1883
2345
  onOpenSettings,
1884
- onCloseSettings
2346
+ onCloseSettings,
2347
+ captionPrefs,
2348
+ onCaptionPrefsChange,
2349
+ captionsAvailable,
2350
+ effectBackgrounds
1885
2351
  }) {
1886
2352
  const [audioTrack, setAudioTrack] = useState(null);
1887
2353
  const [videoTrack, setVideoTrack] = useState(null);
@@ -1948,7 +2414,11 @@ function StudioGuestWaiting({
1948
2414
  meeting,
1949
2415
  labels,
1950
2416
  reducedMotion,
1951
- cameraOn
2417
+ cameraOn,
2418
+ captionPrefs,
2419
+ onCaptionPrefsChange,
2420
+ captionsAvailable,
2421
+ effectBackgrounds
1952
2422
  }
1953
2423
  )
1954
2424
  ] });
@@ -3573,6 +4043,12 @@ function StudioToolbar({
3573
4043
  stageEnabled,
3574
4044
  canLeaveStage,
3575
4045
  reducedMotion,
4046
+ captionsOn,
4047
+ captionsAvailable,
4048
+ captionsReady,
4049
+ captionPrefs,
4050
+ onCaptionPrefsChange,
4051
+ effectBackgrounds,
3576
4052
  onToggleMute,
3577
4053
  onToggleCamera,
3578
4054
  onToggleScreen,
@@ -3586,204 +4062,360 @@ function StudioToolbar({
3586
4062
  onCloseSettings,
3587
4063
  onOpenPip,
3588
4064
  onClosePip,
4065
+ onToggleCaptions,
3589
4066
  onLeave
3590
4067
  }) {
3591
- 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";
3592
4072
  async function enterFullscreen() {
3593
4073
  try {
3594
4074
  await document.documentElement.requestFullscreen();
3595
4075
  } catch {
3596
4076
  }
3597
4077
  }
3598
- 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: [
3599
- showMediaControls ? /* @__PURE__ */ jsxs(Fragment, { children: [
3600
- /* @__PURE__ */ jsx(
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(
3601
4120
  ControlIcon,
3602
4121
  {
3603
- label: muted ? labels.unmute : labels.mute,
3604
- pressed: muted,
3605
- tone: muted ? "danger" : "default",
3606
- onClick: onToggleMute,
3607
- children: muted ? /* @__PURE__ */ jsx(MicOff, {}) : /* @__PURE__ */ jsx(Mic, {})
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, {})
3608
4137
  }
3609
- ),
3610
- /* @__PURE__ */ jsx(
4138
+ ) : null,
4139
+ showStageGuest ? /* @__PURE__ */ jsx(
3611
4140
  ControlIcon,
3612
4141
  {
3613
- label: labels.camera,
3614
- pressed: cameraOn,
3615
- onClick: onToggleCamera,
3616
- children: cameraOn ? /* @__PURE__ */ jsx(Video, {}) : /* @__PURE__ */ jsx(VideoOff, {})
3617
- }
3618
- )
3619
- ] }) : null,
3620
- session.role === "moderator" && canManageStage ? /* @__PURE__ */ jsx(
3621
- ControlIcon,
3622
- {
3623
- label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : labels.goOnAir,
3624
- pressed: selfStageStatus !== "ON_STAGE",
3625
- disabled: selfStageStatus === "ON_STAGE" && !canLeaveStage,
3626
- onClick: () => {
3627
- if (selfStageStatus === "ON_STAGE") {
3628
- if (!canLeaveStage) {
3629
- toast.message(labels.offAirLastVisible);
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();
3630
4152
  return;
3631
4153
  }
3632
- onLeaveStage();
3633
- return;
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, {})
3634
4178
  }
3635
- onAskStage();
3636
- },
3637
- children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
3638
- }
3639
- ) : null,
3640
- session.role === "guest" && stageEnabled ? /* @__PURE__ */ jsx(
3641
- ControlIcon,
3642
- {
3643
- label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
3644
- pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
3645
- disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
3646
- onClick: () => {
3647
- if (selfStageStatus === "ON_STAGE") {
3648
- if (!canLeaveStage) {
3649
- toast.message(labels.offAirLastVisible);
3650
- return;
3651
- }
3652
- onLeaveStage();
3653
- return;
4179
+ ),
4180
+ /* @__PURE__ */ jsx(
4181
+ ControlIcon,
4182
+ {
4183
+ label: labels.cancelShareRequest,
4184
+ onClick: onCancelShareRequest,
4185
+ children: /* @__PURE__ */ jsx(X, {})
3654
4186
  }
3655
- onAskStage();
3656
- },
3657
- children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {})
3658
- }
3659
- ) : null,
3660
- showScreenControl ? canShareFreely ? /* @__PURE__ */ jsx(
3661
- ControlIcon,
3662
- {
3663
- label: labels.screen,
3664
- pressed: screenOn,
3665
- disabled: screenPending || !screenAvailable && !screenOn,
3666
- pending: screenPending,
3667
- onClick: onToggleScreen,
3668
- children: /* @__PURE__ */ jsx(MonitorUp, {})
3669
- }
3670
- ) : shareGrant === "requested" ? /* @__PURE__ */ jsxs(Fragment, { children: [
4187
+ )
4188
+ ] }) : /* @__PURE__ */ jsx(ControlIcon, { label: labels.askToShare, onClick: onAskToShare, children: /* @__PURE__ */ jsx(MonitorUp, {}) }) : null,
3671
4189
  /* @__PURE__ */ jsx(
3672
4190
  ControlIcon,
3673
4191
  {
3674
- label: labels.shareRequested,
3675
- pressed: true,
3676
- disabled: true,
3677
- onClick: () => void 0,
3678
- children: /* @__PURE__ */ jsx(MonitorUp, {})
4192
+ label: captionsLabel,
4193
+ pressed: captionsOn,
4194
+ disabled: !captionsAvailable,
4195
+ onClick: onToggleCaptions,
4196
+ children: /* @__PURE__ */ jsx(Captions, {})
3679
4197
  }
3680
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: [
3681
4241
  /* @__PURE__ */ jsx(
3682
4242
  ControlIcon,
3683
4243
  {
3684
- label: labels.cancelShareRequest,
3685
- onClick: onCancelShareRequest,
3686
- children: /* @__PURE__ */ jsx(X, {})
4244
+ label: unreadChat > 0 ? `${labels.chat} (${unreadChat})` : labels.chat,
4245
+ pressed: chatOpen,
4246
+ badge: unreadChat,
4247
+ onClick: onToggleChat,
4248
+ children: /* @__PURE__ */ jsx(MessageSquare, {})
3687
4249
  }
3688
- )
3689
- ] }) : /* @__PURE__ */ jsx(ControlIcon, { label: labels.askToShare, onClick: onAskToShare, children: /* @__PURE__ */ jsx(MonitorUp, {}) }) : null,
3690
- /* @__PURE__ */ jsx(
3691
- ControlIcon,
3692
- {
3693
- label: unreadChat > 0 ? `${labels.chat} (${unreadChat})` : labels.chat,
3694
- pressed: chatOpen,
3695
- badge: unreadChat,
3696
- onClick: onToggleChat,
3697
- children: /* @__PURE__ */ jsx(MessageSquare, {})
3698
- }
3699
- ),
3700
- session.role === "moderator" ? /* @__PURE__ */ jsx(
3701
- ControlIcon,
3702
- {
3703
- label: peopleBadge > 0 ? `${labels.people} (${peopleBadge})` : labels.people,
3704
- pressed: peopleOpen,
3705
- badge: peopleBadge,
3706
- onClick: onTogglePeople,
3707
- children: /* @__PURE__ */ jsx(Users, {})
3708
- }
3709
- ) : null,
3710
- fullscreenSupported ? /* @__PURE__ */ jsx(
3711
- ControlIcon,
3712
- {
3713
- label: labels.fullscreen,
3714
- onClick: () => void enterFullscreen(),
3715
- children: /* @__PURE__ */ jsx(Maximize, {})
3716
- }
3717
- ) : null,
3718
- pipSupported ? /* @__PURE__ */ jsx(
3719
- ControlIcon,
3720
- {
3721
- label: pipOpen ? labels.closePip : labels.openPip,
3722
- pressed: pipOpen,
3723
- onClick: () => {
3724
- if (pipOpen) onClosePip();
3725
- else onOpenPip();
3726
- },
3727
- children: /* @__PURE__ */ jsx(PictureInPicture2, {})
3728
- }
3729
- ) : null,
4250
+ ),
4251
+ session.role === "moderator" ? /* @__PURE__ */ jsx(
4252
+ ControlIcon,
4253
+ {
4254
+ label: peopleBadge > 0 ? `${labels.people} (${peopleBadge})` : labels.people,
4255
+ pressed: peopleOpen,
4256
+ badge: peopleBadge,
4257
+ onClick: onTogglePeople,
4258
+ children: /* @__PURE__ */ jsx(Users, {})
4259
+ }
4260
+ ) : null,
4261
+ /* @__PURE__ */ jsx(
4262
+ ControlIcon,
4263
+ {
4264
+ label: labels.settings,
4265
+ pressed: settingsOpen,
4266
+ onClick: onOpenSettings,
4267
+ children: /* @__PURE__ */ jsx(Settings, {})
4268
+ }
4269
+ ),
4270
+ /* @__PURE__ */ jsx(
4271
+ StudioSettingsDialog,
4272
+ {
4273
+ open: settingsOpen,
4274
+ onOpenChange: onCloseSettings,
4275
+ meeting,
4276
+ reducedMotion,
4277
+ cameraOn,
4278
+ labels,
4279
+ captionPrefs,
4280
+ onCaptionPrefsChange,
4281
+ captionsAvailable,
4282
+ effectBackgrounds
4283
+ }
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: [
3730
4322
  /* @__PURE__ */ jsx(
3731
4323
  ControlIcon,
3732
4324
  {
3733
- label: labels.settings,
3734
- pressed: settingsOpen,
3735
- onClick: onOpenSettings,
3736
- children: /* @__PURE__ */ jsx(Settings, {})
4325
+ label,
4326
+ pressed,
4327
+ tone,
4328
+ onClick,
4329
+ children
3737
4330
  }
3738
4331
  ),
3739
- /* @__PURE__ */ jsx(
3740
- StudioSettingsDialog,
3741
- {
3742
- open: settingsOpen,
3743
- onOpenChange: onCloseSettings,
3744
- meeting,
3745
- reducedMotion,
3746
- cameraOn,
3747
- labels: {
3748
- settingsTitle: labels.settingsTitle,
3749
- settingsDescription: labels.settingsDescription,
3750
- guestName: labels.guestName,
3751
- displayNameSave: labels.displayNameSave,
3752
- displayNameSaved: labels.displayNameSaved,
3753
- displayNameError: labels.displayNameError,
3754
- cameraDevice: labels.cameraDevice,
3755
- micDevice: labels.micDevice,
3756
- speakerDevice: labels.speakerDevice,
3757
- testSpeaker: labels.testSpeaker,
3758
- testSpeakerError: labels.testSpeakerError,
3759
- cameraOff: labels.cameraOff,
3760
- 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" })
3761
4347
  }
3762
- }
3763
- ),
3764
- session.role === "guest" ? /* @__PURE__ */ jsx(
3765
- ConfirmAction,
3766
- {
3767
- triggerLabel: labels.leave,
3768
- title: labels.leaveTitle,
3769
- description: labels.leaveDescription,
3770
- confirmLabel: labels.leaveConfirm,
3771
- pending,
3772
- onConfirm: onLeave,
3773
- trigger: /* @__PURE__ */ jsx(
3774
- Button,
3775
- {
3776
- type: "button",
3777
- variant: "destructive",
3778
- size: "icon-lg",
3779
- disabled: pending,
3780
- "aria-label": labels.leave,
3781
- children: /* @__PURE__ */ jsx(PhoneOff, {})
3782
- }
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
3783
4411
  )
3784
- }
3785
- ) : null
3786
- ] }) });
4412
+ );
4413
+ };
4414
+ tick2();
4415
+ const timer = window.setInterval(tick2, PLAYOUT_MS);
4416
+ return () => window.clearInterval(timer);
4417
+ }, [baseUrl, enabled, streamStatus]);
4418
+ return playout;
3787
4419
  }
3788
4420
  var STAGE_GRANT_VERIFY_MS = 4e3;
3789
4421
  var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
@@ -3802,7 +4434,10 @@ function StudioRoom({
3802
4434
  initialCameraOn = true,
3803
4435
  initialMuted = false,
3804
4436
  initialDevices,
3805
- appDownloadUrl
4437
+ appDownloadUrl,
4438
+ captionsBaseUrl,
4439
+ apiUrl,
4440
+ cdnBase
3806
4441
  }) {
3807
4442
  const [cameraOn, setCameraOn] = useState(initialCameraOn);
3808
4443
  const [screenOn, setScreenOn] = useState(false);
@@ -3824,6 +4459,11 @@ function StudioRoom({
3824
4459
  const [egressError, setEgressError] = useState(null);
3825
4460
  const [shareGrant, setShareGrant] = useState("idle");
3826
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);
3827
4467
  const [connectTimedOut, setConnectTimedOut] = useState(false);
3828
4468
  const [screenPermission, setScreenPermission] = useState(
3829
4469
  () => meeting.screenSharePermission()
@@ -3849,6 +4489,19 @@ function StudioRoom({
3849
4489
  const pip = useDocumentPictureInPicture();
3850
4490
  const gateFromContext = useStudioWindowGate();
3851
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
+ );
3852
4505
  const onAir = isLiveStudioStatus(session.stream.status);
3853
4506
  const active = isStudioActiveStatus(session.stream.status);
3854
4507
  if (onAir && liveStartedAtRef.current === null) {
@@ -4533,6 +5186,10 @@ function StudioRoom({
4533
5186
  labels,
4534
5187
  settingsOpen,
4535
5188
  statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
5189
+ captionPrefs,
5190
+ onCaptionPrefsChange: setCaptionPrefs,
5191
+ captionsAvailable,
5192
+ effectBackgrounds,
4536
5193
  onToggleCamera,
4537
5194
  onToggleMute: () => void onToggleMute(),
4538
5195
  onOpenSettings: () => setSettingsOpen(true),
@@ -4640,27 +5297,37 @@ function StudioRoom({
4640
5297
  }
4641
5298
  ) : null,
4642
5299
  /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col md:flex-row", children: [
4643
- /* @__PURE__ */ jsx(
4644
- StudioStage,
4645
- {
4646
- participants: stageParticipants,
4647
- meeting,
4648
- reducedMotion,
4649
- canModerate: session.role === "moderator",
4650
- canManageStage,
4651
- onAir,
4652
- labels,
4653
- onKick: (id) => void onKick(id),
4654
- onMute: (id) => void onHostMedia(id, "audio"),
4655
- onStopCamera: (id) => void onHostMedia(id, "video"),
4656
- onStopScreen: (id) => void onHostMedia(id, "screen"),
4657
- onToggleStage: (tile) => void onSetStage(tile, !isOnStage(tile.stageStatus)),
4658
- onTogglePin: (tile) => void onTogglePin(tile),
4659
- onToggleChat: (tile) => void onToggleChat(tile),
4660
- canTakeOff: (id) => canTakeOffAir(participants, id, onAir),
4661
- onOpenPeople: () => setSideTab("people")
4662
- }
4663
- ),
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
+ ] }),
4664
5331
  sideTab ? /* @__PURE__ */ jsx(
4665
5332
  StudioSidePanel,
4666
5333
  {
@@ -4773,6 +5440,12 @@ function StudioRoom({
4773
5440
  stageEnabled,
4774
5441
  canLeaveStage: selfId != null && canTakeOffAir(participants, selfId, onAir),
4775
5442
  reducedMotion,
5443
+ captionsOn,
5444
+ captionsAvailable,
5445
+ captionsReady,
5446
+ captionPrefs,
5447
+ onCaptionPrefsChange: setCaptionPrefs,
5448
+ effectBackgrounds,
4776
5449
  onToggleMute: () => void onToggleMute(),
4777
5450
  onToggleCamera,
4778
5451
  onToggleScreen,
@@ -4800,6 +5473,16 @@ function StudioRoom({
4800
5473
  onCloseSettings: setSettingsOpen,
4801
5474
  onOpenPip,
4802
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
+ },
4803
5486
  onLeave: () => void onLeave()
4804
5487
  }
4805
5488
  ),
@@ -5194,16 +5877,16 @@ function usePrefersReducedMotion() {
5194
5877
  // src/studio/lib/guest-identity.ts
5195
5878
  var NAME_KEY = "livo-studio-display-name";
5196
5879
  var ID_KEY = "livo-studio-guest-id";
5197
- function readStudioGuestIdentity(storage = defaultStorage2()) {
5880
+ function readStudioGuestIdentity(storage = defaultStorage4()) {
5198
5881
  const guestId = storage?.getItem(ID_KEY)?.trim() || crypto.randomUUID();
5199
5882
  storage?.setItem(ID_KEY, guestId);
5200
5883
  const displayName = storage?.getItem(NAME_KEY)?.trim() ?? "";
5201
5884
  return { displayName, guestId };
5202
5885
  }
5203
- function persistStudioGuestName(displayName, storage = defaultStorage2()) {
5886
+ function persistStudioGuestName(displayName, storage = defaultStorage4()) {
5204
5887
  storage?.setItem(NAME_KEY, displayName.trim());
5205
5888
  }
5206
- function defaultStorage2() {
5889
+ function defaultStorage4() {
5207
5890
  if (typeof window === "undefined") return null;
5208
5891
  try {
5209
5892
  return window.localStorage;
@@ -5572,10 +6255,19 @@ function studioMeetingFromRtk(raw) {
5572
6255
  if (typeof window !== "undefined") {
5573
6256
  window.addEventListener("unhandledrejection", onUnhandledRejection);
5574
6257
  }
6258
+ let videoEffects = readStudioVideoEffects();
6259
+ async function applyCurrentVideoEffects() {
6260
+ await applyStudioVideoEffects({
6261
+ self: meeting.self,
6262
+ meeting,
6263
+ config: videoEffects
6264
+ });
6265
+ }
5575
6266
  async function republishCameraIfOn() {
5576
6267
  if (meeting.self?.videoEnabled !== true) return;
5577
6268
  await meeting.self.disableVideo?.();
5578
6269
  await meeting.self.enableVideo?.();
6270
+ await applyCurrentVideoEffects();
5579
6271
  }
5580
6272
  async function acceptAllowedStageIfOffAir() {
5581
6273
  if (currentStageStatus() !== "OFF_STAGE") return;
@@ -5763,6 +6455,7 @@ function studioMeetingFromRtk(raw) {
5763
6455
  async join() {
5764
6456
  await meeting.join?.();
5765
6457
  await joinStageIfAllowed();
6458
+ await applyCurrentVideoEffects();
5766
6459
  },
5767
6460
  async leave() {
5768
6461
  if (typeof window !== "undefined") {
@@ -5775,8 +6468,10 @@ function studioMeetingFromRtk(raw) {
5775
6468
  else await meeting.self?.disableAudio?.();
5776
6469
  },
5777
6470
  async setVideoEnabled(enabled) {
5778
- if (enabled) await meeting.self?.enableVideo?.();
5779
- else await meeting.self?.disableVideo?.();
6471
+ if (enabled) {
6472
+ await meeting.self?.enableVideo?.();
6473
+ await applyCurrentVideoEffects();
6474
+ } else await meeting.self?.disableVideo?.();
5780
6475
  },
5781
6476
  async setScreenShareEnabled(enabled) {
5782
6477
  if (enabled) await meeting.self?.enableScreenShare?.();
@@ -6160,6 +6855,14 @@ function studioMeetingFromRtk(raw) {
6160
6855
  }
6161
6856
  });
6162
6857
  },
6858
+ async setVideoEffects(config) {
6859
+ videoEffects = config;
6860
+ persistStudioVideoEffects(config);
6861
+ await applyCurrentVideoEffects();
6862
+ },
6863
+ videoEffects() {
6864
+ return videoEffects;
6865
+ },
6163
6866
  async setDisplayName(name) {
6164
6867
  const trimmed = name.trim().slice(0, STUDIO_DISPLAY_NAME_MAX);
6165
6868
  if (!trimmed) return;
@@ -6391,6 +7094,21 @@ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn
6391
7094
  return null;
6392
7095
  }
6393
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
+ }
6394
7112
  var POLL_INTERVAL_MS = 5e3;
6395
7113
  function LivoHostStudio({
6396
7114
  token,
@@ -6406,6 +7124,9 @@ function LivoHostStudio({
6406
7124
  const reducedMotion = usePrefersReducedMotion();
6407
7125
  const [session, setSession] = useState(null);
6408
7126
  const [status, setStatus] = useState(null);
7127
+ const [captionsEnabled, setCaptionsEnabled] = useState(
7128
+ void 0
7129
+ );
6409
7130
  const [error, setError] = useState(null);
6410
7131
  const { meeting, error: meetingError } = useRealtimeKitMeeting(
6411
7132
  session?.authToken ?? null
@@ -6454,6 +7175,20 @@ function LivoHostStudio({
6454
7175
  window.clearInterval(id);
6455
7176
  };
6456
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]);
6457
7192
  const liveSession = useMemo(() => {
6458
7193
  if (!session) return null;
6459
7194
  if (!status) return session;
@@ -6461,10 +7196,11 @@ function LivoHostStudio({
6461
7196
  ...session,
6462
7197
  stream: {
6463
7198
  ...session.stream,
6464
- status
7199
+ status,
7200
+ captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
6465
7201
  }
6466
7202
  };
6467
- }, [session, status]);
7203
+ }, [captionsEnabled, session, status]);
6468
7204
  if (error || meetingError) {
6469
7205
  return /* @__PURE__ */ jsx("p", { className: "text-destructive p-6", role: "alert", children: error ?? meetingError });
6470
7206
  }
@@ -6482,7 +7218,8 @@ function LivoHostStudio({
6482
7218
  embed,
6483
7219
  chrome,
6484
7220
  playerOrigin,
6485
- appDownloadUrl
7221
+ appDownloadUrl,
7222
+ apiUrl
6486
7223
  }
6487
7224
  );
6488
7225
  }
@@ -6501,6 +7238,9 @@ function LivoGuestStudio({
6501
7238
  const reducedMotion = usePrefersReducedMotion();
6502
7239
  const [session, setSession] = useState(null);
6503
7240
  const [status, setStatus] = useState(null);
7241
+ const [captionsEnabled, setCaptionsEnabled] = useState(
7242
+ void 0
7243
+ );
6504
7244
  const [error, setError] = useState(null);
6505
7245
  const [displayName, setDisplayName] = useState("");
6506
7246
  const [guestId, setGuestId] = useState("");
@@ -6604,8 +7344,11 @@ function LivoGuestStudio({
6604
7344
  if (current === "ended" || current === "error") return;
6605
7345
  let cancelled = false;
6606
7346
  const poll = async () => {
6607
- const next = await getPublicStreamStatus(session.stream.id, apiUrl);
6608
- 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
+ }
6609
7352
  };
6610
7353
  void poll();
6611
7354
  const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
@@ -6620,9 +7363,13 @@ function LivoGuestStudio({
6620
7363
  if (!status) return session;
6621
7364
  return {
6622
7365
  ...session,
6623
- stream: { ...session.stream, status }
7366
+ stream: {
7367
+ ...session.stream,
7368
+ status,
7369
+ captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
7370
+ }
6624
7371
  };
6625
- }, [session, status]);
7372
+ }, [captionsEnabled, session, status]);
6626
7373
  useEffect(() => {
6627
7374
  if (retryAfterMs == null) return;
6628
7375
  const id = window.setTimeout(() => {
@@ -6683,7 +7430,8 @@ function LivoGuestStudio({
6683
7430
  initialCameraOn: cameraOn,
6684
7431
  initialMuted: muted,
6685
7432
  initialDevices: deviceSelection,
6686
- appDownloadUrl
7433
+ appDownloadUrl,
7434
+ apiUrl
6687
7435
  }
6688
7436
  );
6689
7437
  }
@@ -6859,10 +7607,40 @@ function studioRoomLabels(t) {
6859
7607
  chatPin: t("chatPin"),
6860
7608
  chatUnpin: t("chatUnpin"),
6861
7609
  chatSave: t("chatSave"),
6862
- 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")
6863
7641
  };
6864
7642
  }
6865
7643
 
6866
- 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 };
6867
7645
  //# sourceMappingURL=index.js.map
6868
7646
  //# sourceMappingURL=index.js.map