@livo-tv/sdk 1.5.0 → 1.6.1
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.
- package/dist/studio/index.cjs +935 -136
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.cts +51 -2
- package/dist/studio/index.d.ts +51 -2
- package/dist/studio/index.js +936 -138
- package/dist/studio/index.js.map +1 -1
- package/package.json +3 -2
package/dist/studio/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
1794
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1795
|
-
|
|
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
|
-
|
|
2186
|
+
EffectTile,
|
|
1798
2187
|
{
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
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
|
-
|
|
2194
|
+
EffectTile,
|
|
1807
2195
|
{
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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.
|
|
1818
|
-
|
|
1819
|
-
{
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
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: "
|
|
1828
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1829
|
-
|
|
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
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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
|
-
|
|
1848
|
-
|
|
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,38 +4086,60 @@ function StudioToolbar({
|
|
|
3610
4086
|
onCloseSettings,
|
|
3611
4087
|
onOpenPip,
|
|
3612
4088
|
onClosePip,
|
|
4089
|
+
onToggleCaptions,
|
|
3613
4090
|
onLeave
|
|
3614
4091
|
}) {
|
|
3615
|
-
const
|
|
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
|
-
|
|
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: "border-t px-3 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
|
|
3623
4113
|
showMediaControls ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3624
4114
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3625
|
-
|
|
4115
|
+
MediaSplit,
|
|
3626
4116
|
{
|
|
3627
4117
|
label: muted ? labels.unmute : labels.mute,
|
|
3628
4118
|
pressed: muted,
|
|
3629
4119
|
tone: muted ? "danger" : "default",
|
|
3630
4120
|
onClick: onToggleMute,
|
|
4121
|
+
pickerLabel: labels.micDevice,
|
|
4122
|
+
options: devices.audio,
|
|
4123
|
+
selected: effective.audio,
|
|
4124
|
+
onSelect: (id) => void onDeviceChange("audio", id),
|
|
3631
4125
|
children: muted ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, {})
|
|
3632
4126
|
}
|
|
3633
4127
|
),
|
|
3634
4128
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3635
|
-
|
|
4129
|
+
MediaSplit,
|
|
3636
4130
|
{
|
|
3637
4131
|
label: labels.camera,
|
|
3638
4132
|
pressed: cameraOn,
|
|
3639
4133
|
onClick: onToggleCamera,
|
|
4134
|
+
pickerLabel: labels.cameraDevice,
|
|
4135
|
+
options: devices.video,
|
|
4136
|
+
selected: effective.video,
|
|
4137
|
+
onSelect: (id) => void onDeviceChange("video", id),
|
|
3640
4138
|
children: cameraOn ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.VideoOff, {})
|
|
3641
4139
|
}
|
|
3642
4140
|
)
|
|
3643
4141
|
] }) : null,
|
|
3644
|
-
|
|
4142
|
+
showStageModerator ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3645
4143
|
ControlIcon,
|
|
3646
4144
|
{
|
|
3647
4145
|
label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : labels.goOnAir,
|
|
@@ -3661,7 +4159,7 @@ function StudioToolbar({
|
|
|
3661
4159
|
children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
|
|
3662
4160
|
}
|
|
3663
4161
|
) : null,
|
|
3664
|
-
|
|
4162
|
+
showStageGuest ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3665
4163
|
ControlIcon,
|
|
3666
4164
|
{
|
|
3667
4165
|
label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
|
|
@@ -3711,6 +4209,56 @@ function StudioToolbar({
|
|
|
3711
4209
|
}
|
|
3712
4210
|
)
|
|
3713
4211
|
] }) : /* @__PURE__ */ jsxRuntime.jsx(ControlIcon, { label: labels.askToShare, onClick: onAskToShare, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {}) }) : null,
|
|
4212
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4213
|
+
ControlIcon,
|
|
4214
|
+
{
|
|
4215
|
+
label: captionsLabel,
|
|
4216
|
+
pressed: captionsOn,
|
|
4217
|
+
disabled: !captionsAvailable,
|
|
4218
|
+
onClick: onToggleCaptions,
|
|
4219
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Captions, {})
|
|
4220
|
+
}
|
|
4221
|
+
),
|
|
4222
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
4223
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4224
|
+
DropdownMenuTrigger,
|
|
4225
|
+
{
|
|
4226
|
+
render: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4227
|
+
Button,
|
|
4228
|
+
{
|
|
4229
|
+
type: "button",
|
|
4230
|
+
size: "icon-lg",
|
|
4231
|
+
variant: "secondary",
|
|
4232
|
+
"aria-label": labels.more
|
|
4233
|
+
}
|
|
4234
|
+
),
|
|
4235
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreVertical, {})
|
|
4236
|
+
}
|
|
4237
|
+
),
|
|
4238
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuContent, { align: "center", className: "min-w-44", children: [
|
|
4239
|
+
fullscreenSupported ? /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuItem, { onClick: () => void enterFullscreen(), children: [
|
|
4240
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize, {}),
|
|
4241
|
+
labels.fullscreen
|
|
4242
|
+
] }) : null,
|
|
4243
|
+
pipSupported ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4244
|
+
DropdownMenuItem,
|
|
4245
|
+
{
|
|
4246
|
+
onClick: () => {
|
|
4247
|
+
if (pipOpen) onClosePip();
|
|
4248
|
+
else onOpenPip();
|
|
4249
|
+
},
|
|
4250
|
+
children: [
|
|
4251
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.PictureInPicture2, {}),
|
|
4252
|
+
pipOpen ? labels.closePip : labels.openPip
|
|
4253
|
+
]
|
|
4254
|
+
}
|
|
4255
|
+
) : null,
|
|
4256
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuItem, { onClick: onOpenSettings, children: [
|
|
4257
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, {}),
|
|
4258
|
+
labels.settings
|
|
4259
|
+
] })
|
|
4260
|
+
] })
|
|
4261
|
+
] }),
|
|
3714
4262
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3715
4263
|
ControlIcon,
|
|
3716
4264
|
{
|
|
@@ -3731,26 +4279,6 @@ function StudioToolbar({
|
|
|
3731
4279
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, {})
|
|
3732
4280
|
}
|
|
3733
4281
|
) : 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,
|
|
3754
4282
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3755
4283
|
ControlIcon,
|
|
3756
4284
|
{
|
|
@@ -3768,21 +4296,11 @@ function StudioToolbar({
|
|
|
3768
4296
|
meeting,
|
|
3769
4297
|
reducedMotion,
|
|
3770
4298
|
cameraOn,
|
|
3771
|
-
labels
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
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
|
|
3785
|
-
}
|
|
4299
|
+
labels,
|
|
4300
|
+
captionPrefs,
|
|
4301
|
+
onCaptionPrefsChange,
|
|
4302
|
+
captionsAvailable,
|
|
4303
|
+
effectBackgrounds
|
|
3786
4304
|
}
|
|
3787
4305
|
),
|
|
3788
4306
|
session.role === "guest" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3807,7 +4325,141 @@ function StudioToolbar({
|
|
|
3807
4325
|
)
|
|
3808
4326
|
}
|
|
3809
4327
|
) : null
|
|
3810
|
-
] }) });
|
|
4328
|
+
] }) }) });
|
|
4329
|
+
}
|
|
4330
|
+
function MediaSplit({
|
|
4331
|
+
label,
|
|
4332
|
+
pressed,
|
|
4333
|
+
tone = "default",
|
|
4334
|
+
onClick,
|
|
4335
|
+
pickerLabel,
|
|
4336
|
+
options,
|
|
4337
|
+
selected,
|
|
4338
|
+
onSelect,
|
|
4339
|
+
children
|
|
4340
|
+
}) {
|
|
4341
|
+
if (options.length === 0) {
|
|
4342
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4343
|
+
ControlIcon,
|
|
4344
|
+
{
|
|
4345
|
+
label,
|
|
4346
|
+
pressed,
|
|
4347
|
+
tone,
|
|
4348
|
+
onClick,
|
|
4349
|
+
children
|
|
4350
|
+
}
|
|
4351
|
+
);
|
|
4352
|
+
}
|
|
4353
|
+
const iconVariant = tone === "danger" ? "destructive" : pressed ? "default" : "secondary";
|
|
4354
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-secondary inline-flex h-10 items-center rounded-full p-0.5", children: [
|
|
4355
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
4356
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4357
|
+
DropdownMenuTrigger,
|
|
4358
|
+
{
|
|
4359
|
+
render: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4360
|
+
Button,
|
|
4361
|
+
{
|
|
4362
|
+
type: "button",
|
|
4363
|
+
size: "icon-xs",
|
|
4364
|
+
variant: "ghost",
|
|
4365
|
+
className: "text-muted-foreground hover:text-foreground h-9 w-8 rounded-full hover:bg-transparent",
|
|
4366
|
+
"aria-label": pickerLabel
|
|
4367
|
+
}
|
|
4368
|
+
),
|
|
4369
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronUp, { className: "size-3.5" })
|
|
4370
|
+
}
|
|
4371
|
+
),
|
|
4372
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuContent, { align: "start", side: "top", className: "min-w-52", children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4373
|
+
DropdownMenuItem,
|
|
4374
|
+
{
|
|
4375
|
+
onClick: () => onSelect(option.deviceId),
|
|
4376
|
+
children: [
|
|
4377
|
+
option.deviceId === selected ? "\u2713 " : "",
|
|
4378
|
+
option.label
|
|
4379
|
+
]
|
|
4380
|
+
},
|
|
4381
|
+
option.deviceId
|
|
4382
|
+
)) })
|
|
4383
|
+
] }),
|
|
4384
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Tooltip, { children: [
|
|
4385
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4386
|
+
TooltipTrigger,
|
|
4387
|
+
{
|
|
4388
|
+
render: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4389
|
+
Button,
|
|
4390
|
+
{
|
|
4391
|
+
type: "button",
|
|
4392
|
+
size: "icon-lg",
|
|
4393
|
+
variant: iconVariant,
|
|
4394
|
+
className: iconVariant === "secondary" ? "rounded-full bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_10%)] hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_16%)]" : "rounded-full",
|
|
4395
|
+
"aria-label": label,
|
|
4396
|
+
"aria-pressed": pressed,
|
|
4397
|
+
onClick
|
|
4398
|
+
}
|
|
4399
|
+
),
|
|
4400
|
+
children
|
|
4401
|
+
}
|
|
4402
|
+
),
|
|
4403
|
+
/* @__PURE__ */ jsxRuntime.jsx(TooltipContent, { children: label })
|
|
4404
|
+
] })
|
|
4405
|
+
] });
|
|
4406
|
+
}
|
|
4407
|
+
var POLL_MS = 2e3;
|
|
4408
|
+
var PLAYOUT_MS = 250;
|
|
4409
|
+
var STUDIO_EXTRA_LAG_MS = 200;
|
|
4410
|
+
function useStudioCaptions({
|
|
4411
|
+
enabled,
|
|
4412
|
+
baseUrl,
|
|
4413
|
+
locale,
|
|
4414
|
+
streamStatus
|
|
4415
|
+
}) {
|
|
4416
|
+
const [playout, setPlayout] = react.useState(
|
|
4417
|
+
video.EMPTY_LIVE_CAPTION_PLAYOUT
|
|
4418
|
+
);
|
|
4419
|
+
const cuesRef = react.useRef([]);
|
|
4420
|
+
react.useEffect(() => {
|
|
4421
|
+
if (!enabled) {
|
|
4422
|
+
cuesRef.current = [];
|
|
4423
|
+
setPlayout(video.EMPTY_LIVE_CAPTION_PLAYOUT);
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
if (!baseUrl || !isStudioActiveStatus(streamStatus)) return;
|
|
4427
|
+
let cancelled = false;
|
|
4428
|
+
const url = video.liveCaptionsUrl(baseUrl, locale);
|
|
4429
|
+
const poll = async () => {
|
|
4430
|
+
const loaded = await video.loadLiveCaptions(url);
|
|
4431
|
+
if (cancelled || !loaded.ok) return;
|
|
4432
|
+
cuesRef.current = video.mergeLiveCaptionCues(cuesRef.current, loaded.cues);
|
|
4433
|
+
};
|
|
4434
|
+
void poll();
|
|
4435
|
+
const timer = window.setInterval(() => void poll(), POLL_MS);
|
|
4436
|
+
return () => {
|
|
4437
|
+
cancelled = true;
|
|
4438
|
+
window.clearInterval(timer);
|
|
4439
|
+
};
|
|
4440
|
+
}, [baseUrl, enabled, locale, streamStatus]);
|
|
4441
|
+
react.useEffect(() => {
|
|
4442
|
+
if (!enabled || !baseUrl || !isStudioActiveStatus(streamStatus)) {
|
|
4443
|
+
setPlayout(video.EMPTY_LIVE_CAPTION_PLAYOUT);
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
const tick2 = () => {
|
|
4447
|
+
setPlayout(
|
|
4448
|
+
(prev) => video.advanceLiveCaptionPlayout(
|
|
4449
|
+
prev,
|
|
4450
|
+
cuesRef.current,
|
|
4451
|
+
Date.now(),
|
|
4452
|
+
0,
|
|
4453
|
+
void 0,
|
|
4454
|
+
STUDIO_EXTRA_LAG_MS
|
|
4455
|
+
)
|
|
4456
|
+
);
|
|
4457
|
+
};
|
|
4458
|
+
tick2();
|
|
4459
|
+
const timer = window.setInterval(tick2, PLAYOUT_MS);
|
|
4460
|
+
return () => window.clearInterval(timer);
|
|
4461
|
+
}, [baseUrl, enabled, streamStatus]);
|
|
4462
|
+
return playout;
|
|
3811
4463
|
}
|
|
3812
4464
|
var STAGE_GRANT_VERIFY_MS = 4e3;
|
|
3813
4465
|
var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
|
|
@@ -3826,7 +4478,10 @@ function StudioRoom({
|
|
|
3826
4478
|
initialCameraOn = true,
|
|
3827
4479
|
initialMuted = false,
|
|
3828
4480
|
initialDevices,
|
|
3829
|
-
appDownloadUrl
|
|
4481
|
+
appDownloadUrl,
|
|
4482
|
+
captionsBaseUrl,
|
|
4483
|
+
apiUrl,
|
|
4484
|
+
cdnBase
|
|
3830
4485
|
}) {
|
|
3831
4486
|
const [cameraOn, setCameraOn] = react.useState(initialCameraOn);
|
|
3832
4487
|
const [screenOn, setScreenOn] = react.useState(false);
|
|
@@ -3848,6 +4503,11 @@ function StudioRoom({
|
|
|
3848
4503
|
const [egressError, setEgressError] = react.useState(null);
|
|
3849
4504
|
const [shareGrant, setShareGrant] = react.useState("idle");
|
|
3850
4505
|
const [downloadAppOpen, setDownloadAppOpen] = react.useState(false);
|
|
4506
|
+
const [captionsOn, setCaptionsOn] = react.useState(false);
|
|
4507
|
+
const [captionPrefs, setCaptionPrefs] = react.useState(
|
|
4508
|
+
() => readStudioCaptionPrefs()
|
|
4509
|
+
);
|
|
4510
|
+
const captionHintShown = react.useRef(false);
|
|
3851
4511
|
const [connectTimedOut, setConnectTimedOut] = react.useState(false);
|
|
3852
4512
|
const [screenPermission, setScreenPermission] = react.useState(
|
|
3853
4513
|
() => meeting.screenSharePermission()
|
|
@@ -3873,6 +4533,19 @@ function StudioRoom({
|
|
|
3873
4533
|
const pip = useDocumentPictureInPicture();
|
|
3874
4534
|
const gateFromContext = useStudioWindowGate();
|
|
3875
4535
|
const { reportDuplicate } = windowGate ?? gateFromContext;
|
|
4536
|
+
const resolvedCaptionsBaseUrl = captionsBaseUrl ?? studioCaptionsBaseUrl(apiUrl, session.stream.id);
|
|
4537
|
+
const captionsAvailable = session.stream.captionsEnabled !== false;
|
|
4538
|
+
const captionsReady = isStudioActiveStatus(session.stream.status);
|
|
4539
|
+
const captionPlayout = useStudioCaptions({
|
|
4540
|
+
enabled: captionsOn && captionsAvailable,
|
|
4541
|
+
baseUrl: resolvedCaptionsBaseUrl,
|
|
4542
|
+
locale: captionPrefs.locale,
|
|
4543
|
+
streamStatus: session.stream.status
|
|
4544
|
+
});
|
|
4545
|
+
const effectBackgrounds = react.useMemo(
|
|
4546
|
+
() => defaultStudioEffectBackgrounds(cdnBase),
|
|
4547
|
+
[cdnBase]
|
|
4548
|
+
);
|
|
3876
4549
|
const onAir = isLiveStudioStatus(session.stream.status);
|
|
3877
4550
|
const active = isStudioActiveStatus(session.stream.status);
|
|
3878
4551
|
if (onAir && liveStartedAtRef.current === null) {
|
|
@@ -4557,6 +5230,10 @@ function StudioRoom({
|
|
|
4557
5230
|
labels,
|
|
4558
5231
|
settingsOpen,
|
|
4559
5232
|
statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
|
|
5233
|
+
captionPrefs,
|
|
5234
|
+
onCaptionPrefsChange: setCaptionPrefs,
|
|
5235
|
+
captionsAvailable,
|
|
5236
|
+
effectBackgrounds,
|
|
4560
5237
|
onToggleCamera,
|
|
4561
5238
|
onToggleMute: () => void onToggleMute(),
|
|
4562
5239
|
onOpenSettings: () => setSettingsOpen(true),
|
|
@@ -4664,27 +5341,37 @@ function StudioRoom({
|
|
|
4664
5341
|
}
|
|
4665
5342
|
) : null,
|
|
4666
5343
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col md:flex-row", children: [
|
|
4667
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
5344
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex min-h-0 min-w-0 flex-1 flex-col", children: [
|
|
5345
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5346
|
+
StudioStage,
|
|
5347
|
+
{
|
|
5348
|
+
participants: stageParticipants,
|
|
5349
|
+
meeting,
|
|
5350
|
+
reducedMotion,
|
|
5351
|
+
canModerate: session.role === "moderator",
|
|
5352
|
+
canManageStage,
|
|
5353
|
+
onAir,
|
|
5354
|
+
labels,
|
|
5355
|
+
onKick: (id) => void onKick(id),
|
|
5356
|
+
onMute: (id) => void onHostMedia(id, "audio"),
|
|
5357
|
+
onStopCamera: (id) => void onHostMedia(id, "video"),
|
|
5358
|
+
onStopScreen: (id) => void onHostMedia(id, "screen"),
|
|
5359
|
+
onToggleStage: (tile) => void onSetStage(tile, !isOnStage(tile.stageStatus)),
|
|
5360
|
+
onTogglePin: (tile) => void onTogglePin(tile),
|
|
5361
|
+
onToggleChat: (tile) => void onToggleChat(tile),
|
|
5362
|
+
canTakeOff: (id) => canTakeOffAir(participants, id, onAir),
|
|
5363
|
+
onOpenPeople: () => setSideTab("people")
|
|
5364
|
+
}
|
|
5365
|
+
),
|
|
5366
|
+
captionsOn ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
5367
|
+
video.CaptionOverlay,
|
|
5368
|
+
{
|
|
5369
|
+
screen: captionPlayout.screen,
|
|
5370
|
+
className: "bottom-3",
|
|
5371
|
+
textClassName: captionSizeClass(captionPrefs.size)
|
|
5372
|
+
}
|
|
5373
|
+
) : null
|
|
5374
|
+
] }),
|
|
4688
5375
|
sideTab ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4689
5376
|
StudioSidePanel,
|
|
4690
5377
|
{
|
|
@@ -4797,6 +5484,12 @@ function StudioRoom({
|
|
|
4797
5484
|
stageEnabled,
|
|
4798
5485
|
canLeaveStage: selfId != null && canTakeOffAir(participants, selfId, onAir),
|
|
4799
5486
|
reducedMotion,
|
|
5487
|
+
captionsOn,
|
|
5488
|
+
captionsAvailable,
|
|
5489
|
+
captionsReady,
|
|
5490
|
+
captionPrefs,
|
|
5491
|
+
onCaptionPrefsChange: setCaptionPrefs,
|
|
5492
|
+
effectBackgrounds,
|
|
4800
5493
|
onToggleMute: () => void onToggleMute(),
|
|
4801
5494
|
onToggleCamera,
|
|
4802
5495
|
onToggleScreen,
|
|
@@ -4824,6 +5517,16 @@ function StudioRoom({
|
|
|
4824
5517
|
onCloseSettings: setSettingsOpen,
|
|
4825
5518
|
onOpenPip,
|
|
4826
5519
|
onClosePip: pip.close,
|
|
5520
|
+
onToggleCaptions: () => {
|
|
5521
|
+
if (!captionsAvailable) return;
|
|
5522
|
+
if (!captionsOn && !captionHintShown.current) {
|
|
5523
|
+
captionHintShown.current = true;
|
|
5524
|
+
sonner.toast.message(
|
|
5525
|
+
captionsReady ? labels.captionsDelayed : labels.captionsNotReady
|
|
5526
|
+
);
|
|
5527
|
+
}
|
|
5528
|
+
setCaptionsOn((current) => !current);
|
|
5529
|
+
},
|
|
4827
5530
|
onLeave: () => void onLeave()
|
|
4828
5531
|
}
|
|
4829
5532
|
),
|
|
@@ -5218,16 +5921,16 @@ function usePrefersReducedMotion() {
|
|
|
5218
5921
|
// src/studio/lib/guest-identity.ts
|
|
5219
5922
|
var NAME_KEY = "livo-studio-display-name";
|
|
5220
5923
|
var ID_KEY = "livo-studio-guest-id";
|
|
5221
|
-
function readStudioGuestIdentity(storage =
|
|
5924
|
+
function readStudioGuestIdentity(storage = defaultStorage4()) {
|
|
5222
5925
|
const guestId = storage?.getItem(ID_KEY)?.trim() || crypto.randomUUID();
|
|
5223
5926
|
storage?.setItem(ID_KEY, guestId);
|
|
5224
5927
|
const displayName = storage?.getItem(NAME_KEY)?.trim() ?? "";
|
|
5225
5928
|
return { displayName, guestId };
|
|
5226
5929
|
}
|
|
5227
|
-
function persistStudioGuestName(displayName, storage =
|
|
5930
|
+
function persistStudioGuestName(displayName, storage = defaultStorage4()) {
|
|
5228
5931
|
storage?.setItem(NAME_KEY, displayName.trim());
|
|
5229
5932
|
}
|
|
5230
|
-
function
|
|
5933
|
+
function defaultStorage4() {
|
|
5231
5934
|
if (typeof window === "undefined") return null;
|
|
5232
5935
|
try {
|
|
5233
5936
|
return window.localStorage;
|
|
@@ -5596,10 +6299,19 @@ function studioMeetingFromRtk(raw) {
|
|
|
5596
6299
|
if (typeof window !== "undefined") {
|
|
5597
6300
|
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
5598
6301
|
}
|
|
6302
|
+
let videoEffects = readStudioVideoEffects();
|
|
6303
|
+
async function applyCurrentVideoEffects() {
|
|
6304
|
+
await applyStudioVideoEffects({
|
|
6305
|
+
self: meeting.self,
|
|
6306
|
+
meeting,
|
|
6307
|
+
config: videoEffects
|
|
6308
|
+
});
|
|
6309
|
+
}
|
|
5599
6310
|
async function republishCameraIfOn() {
|
|
5600
6311
|
if (meeting.self?.videoEnabled !== true) return;
|
|
5601
6312
|
await meeting.self.disableVideo?.();
|
|
5602
6313
|
await meeting.self.enableVideo?.();
|
|
6314
|
+
await applyCurrentVideoEffects();
|
|
5603
6315
|
}
|
|
5604
6316
|
async function acceptAllowedStageIfOffAir() {
|
|
5605
6317
|
if (currentStageStatus() !== "OFF_STAGE") return;
|
|
@@ -5787,6 +6499,7 @@ function studioMeetingFromRtk(raw) {
|
|
|
5787
6499
|
async join() {
|
|
5788
6500
|
await meeting.join?.();
|
|
5789
6501
|
await joinStageIfAllowed();
|
|
6502
|
+
await applyCurrentVideoEffects();
|
|
5790
6503
|
},
|
|
5791
6504
|
async leave() {
|
|
5792
6505
|
if (typeof window !== "undefined") {
|
|
@@ -5799,8 +6512,10 @@ function studioMeetingFromRtk(raw) {
|
|
|
5799
6512
|
else await meeting.self?.disableAudio?.();
|
|
5800
6513
|
},
|
|
5801
6514
|
async setVideoEnabled(enabled) {
|
|
5802
|
-
if (enabled)
|
|
5803
|
-
|
|
6515
|
+
if (enabled) {
|
|
6516
|
+
await meeting.self?.enableVideo?.();
|
|
6517
|
+
await applyCurrentVideoEffects();
|
|
6518
|
+
} else await meeting.self?.disableVideo?.();
|
|
5804
6519
|
},
|
|
5805
6520
|
async setScreenShareEnabled(enabled) {
|
|
5806
6521
|
if (enabled) await meeting.self?.enableScreenShare?.();
|
|
@@ -6184,6 +6899,14 @@ function studioMeetingFromRtk(raw) {
|
|
|
6184
6899
|
}
|
|
6185
6900
|
});
|
|
6186
6901
|
},
|
|
6902
|
+
async setVideoEffects(config) {
|
|
6903
|
+
videoEffects = config;
|
|
6904
|
+
persistStudioVideoEffects(config);
|
|
6905
|
+
await applyCurrentVideoEffects();
|
|
6906
|
+
},
|
|
6907
|
+
videoEffects() {
|
|
6908
|
+
return videoEffects;
|
|
6909
|
+
},
|
|
6187
6910
|
async setDisplayName(name) {
|
|
6188
6911
|
const trimmed = name.trim().slice(0, STUDIO_DISPLAY_NAME_MAX);
|
|
6189
6912
|
if (!trimmed) return;
|
|
@@ -6415,6 +7138,21 @@ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn
|
|
|
6415
7138
|
return null;
|
|
6416
7139
|
}
|
|
6417
7140
|
}
|
|
7141
|
+
async function getPublicStreamMeta(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
|
|
7142
|
+
try {
|
|
7143
|
+
const body = await publicJson(
|
|
7144
|
+
`${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
|
|
7145
|
+
{},
|
|
7146
|
+
fetchFn
|
|
7147
|
+
);
|
|
7148
|
+
return parsePublicStreamMeta(body);
|
|
7149
|
+
} catch (error) {
|
|
7150
|
+
if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
|
|
7151
|
+
return { status: "ended", captionsEnabled: false };
|
|
7152
|
+
}
|
|
7153
|
+
return null;
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
6418
7156
|
var POLL_INTERVAL_MS = 5e3;
|
|
6419
7157
|
function LivoHostStudio({
|
|
6420
7158
|
token,
|
|
@@ -6430,6 +7168,9 @@ function LivoHostStudio({
|
|
|
6430
7168
|
const reducedMotion = usePrefersReducedMotion();
|
|
6431
7169
|
const [session, setSession] = react.useState(null);
|
|
6432
7170
|
const [status, setStatus] = react.useState(null);
|
|
7171
|
+
const [captionsEnabled, setCaptionsEnabled] = react.useState(
|
|
7172
|
+
void 0
|
|
7173
|
+
);
|
|
6433
7174
|
const [error, setError] = react.useState(null);
|
|
6434
7175
|
const { meeting, error: meetingError } = useRealtimeKitMeeting(
|
|
6435
7176
|
session?.authToken ?? null
|
|
@@ -6478,6 +7219,20 @@ function LivoHostStudio({
|
|
|
6478
7219
|
window.clearInterval(id);
|
|
6479
7220
|
};
|
|
6480
7221
|
}, [client, status]);
|
|
7222
|
+
react.useEffect(() => {
|
|
7223
|
+
if (!session) return;
|
|
7224
|
+
let cancelled = false;
|
|
7225
|
+
const poll = async () => {
|
|
7226
|
+
const next = await getPublicStreamMeta(session.stream.id, apiUrl);
|
|
7227
|
+
if (!cancelled && next) setCaptionsEnabled(next.captionsEnabled);
|
|
7228
|
+
};
|
|
7229
|
+
void poll();
|
|
7230
|
+
const id = window.setInterval(() => void poll(), POLL_INTERVAL_MS);
|
|
7231
|
+
return () => {
|
|
7232
|
+
cancelled = true;
|
|
7233
|
+
window.clearInterval(id);
|
|
7234
|
+
};
|
|
7235
|
+
}, [apiUrl, session]);
|
|
6481
7236
|
const liveSession = react.useMemo(() => {
|
|
6482
7237
|
if (!session) return null;
|
|
6483
7238
|
if (!status) return session;
|
|
@@ -6485,10 +7240,11 @@ function LivoHostStudio({
|
|
|
6485
7240
|
...session,
|
|
6486
7241
|
stream: {
|
|
6487
7242
|
...session.stream,
|
|
6488
|
-
status
|
|
7243
|
+
status,
|
|
7244
|
+
captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
|
|
6489
7245
|
}
|
|
6490
7246
|
};
|
|
6491
|
-
}, [session, status]);
|
|
7247
|
+
}, [captionsEnabled, session, status]);
|
|
6492
7248
|
if (error || meetingError) {
|
|
6493
7249
|
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive p-6", role: "alert", children: error ?? meetingError });
|
|
6494
7250
|
}
|
|
@@ -6506,7 +7262,8 @@ function LivoHostStudio({
|
|
|
6506
7262
|
embed,
|
|
6507
7263
|
chrome,
|
|
6508
7264
|
playerOrigin,
|
|
6509
|
-
appDownloadUrl
|
|
7265
|
+
appDownloadUrl,
|
|
7266
|
+
apiUrl
|
|
6510
7267
|
}
|
|
6511
7268
|
);
|
|
6512
7269
|
}
|
|
@@ -6525,6 +7282,9 @@ function LivoGuestStudio({
|
|
|
6525
7282
|
const reducedMotion = usePrefersReducedMotion();
|
|
6526
7283
|
const [session, setSession] = react.useState(null);
|
|
6527
7284
|
const [status, setStatus] = react.useState(null);
|
|
7285
|
+
const [captionsEnabled, setCaptionsEnabled] = react.useState(
|
|
7286
|
+
void 0
|
|
7287
|
+
);
|
|
6528
7288
|
const [error, setError] = react.useState(null);
|
|
6529
7289
|
const [displayName, setDisplayName] = react.useState("");
|
|
6530
7290
|
const [guestId, setGuestId] = react.useState("");
|
|
@@ -6628,8 +7388,11 @@ function LivoGuestStudio({
|
|
|
6628
7388
|
if (current === "ended" || current === "error") return;
|
|
6629
7389
|
let cancelled = false;
|
|
6630
7390
|
const poll = async () => {
|
|
6631
|
-
const next = await
|
|
6632
|
-
if (!cancelled && next)
|
|
7391
|
+
const next = await getPublicStreamMeta(session.stream.id, apiUrl);
|
|
7392
|
+
if (!cancelled && next) {
|
|
7393
|
+
setStatus(next.status);
|
|
7394
|
+
setCaptionsEnabled(next.captionsEnabled);
|
|
7395
|
+
}
|
|
6633
7396
|
};
|
|
6634
7397
|
void poll();
|
|
6635
7398
|
const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
|
|
@@ -6644,9 +7407,13 @@ function LivoGuestStudio({
|
|
|
6644
7407
|
if (!status) return session;
|
|
6645
7408
|
return {
|
|
6646
7409
|
...session,
|
|
6647
|
-
stream: {
|
|
7410
|
+
stream: {
|
|
7411
|
+
...session.stream,
|
|
7412
|
+
status,
|
|
7413
|
+
captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
|
|
7414
|
+
}
|
|
6648
7415
|
};
|
|
6649
|
-
}, [session, status]);
|
|
7416
|
+
}, [captionsEnabled, session, status]);
|
|
6650
7417
|
react.useEffect(() => {
|
|
6651
7418
|
if (retryAfterMs == null) return;
|
|
6652
7419
|
const id = window.setTimeout(() => {
|
|
@@ -6707,7 +7474,8 @@ function LivoGuestStudio({
|
|
|
6707
7474
|
initialCameraOn: cameraOn,
|
|
6708
7475
|
initialMuted: muted,
|
|
6709
7476
|
initialDevices: deviceSelection,
|
|
6710
|
-
appDownloadUrl
|
|
7477
|
+
appDownloadUrl,
|
|
7478
|
+
apiUrl
|
|
6711
7479
|
}
|
|
6712
7480
|
);
|
|
6713
7481
|
}
|
|
@@ -6883,7 +7651,37 @@ function studioRoomLabels(t) {
|
|
|
6883
7651
|
chatPin: t("chatPin"),
|
|
6884
7652
|
chatUnpin: t("chatUnpin"),
|
|
6885
7653
|
chatSave: t("chatSave"),
|
|
6886
|
-
waitingSkeleton: t("waitingSkeleton")
|
|
7654
|
+
waitingSkeleton: t("waitingSkeleton"),
|
|
7655
|
+
more: t("more"),
|
|
7656
|
+
captions: t("captions"),
|
|
7657
|
+
captionsOn: t("captionsOn"),
|
|
7658
|
+
captionsOff: t("captionsOff"),
|
|
7659
|
+
captionsNotReady: t("captionsNotReady"),
|
|
7660
|
+
captionsDelayed: t("captionsDelayed"),
|
|
7661
|
+
captionsLanguage: t("captionsLanguage"),
|
|
7662
|
+
captionsSize: t("captionsSize"),
|
|
7663
|
+
captionsSizeS: t("captionsSizeS"),
|
|
7664
|
+
captionsSizeM: t("captionsSizeM"),
|
|
7665
|
+
captionsSizeL: t("captionsSizeL"),
|
|
7666
|
+
settingsGeneral: t("settingsGeneral"),
|
|
7667
|
+
settingsAudio: t("settingsAudio"),
|
|
7668
|
+
settingsVideo: t("settingsVideo"),
|
|
7669
|
+
settingsEffects: t("settingsEffects"),
|
|
7670
|
+
settingsSubtitles: t("settingsSubtitles"),
|
|
7671
|
+
effectNone: t("effectNone"),
|
|
7672
|
+
effectBlur: t("effectBlur"),
|
|
7673
|
+
effectBlurStrong: t("effectBlurStrong"),
|
|
7674
|
+
effectBackgrounds: t("effectBackgrounds"),
|
|
7675
|
+
effectFilters: t("effectFilters"),
|
|
7676
|
+
effectFilterNone: t("effectFilterNone"),
|
|
7677
|
+
effectFilterGrayscale: t("effectFilterGrayscale"),
|
|
7678
|
+
effectFilterSepia: t("effectFilterSepia"),
|
|
7679
|
+
effectFilterWarm: t("effectFilterWarm"),
|
|
7680
|
+
effectFilterCool: t("effectFilterCool"),
|
|
7681
|
+
effectFilterBright: t("effectFilterBright"),
|
|
7682
|
+
localeEn: t("localeEn"),
|
|
7683
|
+
localeEs: t("localeEs"),
|
|
7684
|
+
localePt: t("localePt")
|
|
6887
7685
|
};
|
|
6888
7686
|
}
|
|
6889
7687
|
|
|
@@ -6901,6 +7699,7 @@ exports.applyEmbedTheme = applyEmbedTheme;
|
|
|
6901
7699
|
exports.controlActionsFromClient = controlActionsFromClient;
|
|
6902
7700
|
exports.createStudioControlClient = createStudioControlClient;
|
|
6903
7701
|
exports.decodeEmbedTheme = decodeEmbedTheme;
|
|
7702
|
+
exports.getPublicStreamMeta = getPublicStreamMeta;
|
|
6904
7703
|
exports.getPublicStreamStatus = getPublicStreamStatus;
|
|
6905
7704
|
exports.getStudioJoinStatus = getStudioJoinStatus;
|
|
6906
7705
|
exports.joinStudioGuest = joinStudioGuest;
|