@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.
- package/dist/studio/index.cjs +1038 -259
- 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 +1039 -261
- 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,204 +4086,360 @@ 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
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
4102
|
+
async function onDeviceChange(kind, deviceId) {
|
|
4103
|
+
persistStudioDeviceSelection(kind, deviceId);
|
|
4104
|
+
try {
|
|
4105
|
+
await meeting.setDevice(kind, deviceId);
|
|
4106
|
+
} catch {
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
const showStageModerator = session.role === "moderator" && canManageStage;
|
|
4110
|
+
const showStageGuest = session.role === "guest" && stageEnabled;
|
|
4111
|
+
const captionsLabel = !captionsAvailable ? labels.captions : !captionsReady ? labels.captionsNotReady : captionsOn ? labels.captionsOn : labels.captionsOff;
|
|
4112
|
+
return /* @__PURE__ */ jsxRuntime.jsx("footer", { className: "flex flex-wrap items-center justify-center gap-3 border-t px-3 py-3 sm:justify-between", children: /* @__PURE__ */ jsxRuntime.jsxs(TooltipProvider, { children: [
|
|
4113
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
|
|
4114
|
+
showMediaControls ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4115
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4116
|
+
MediaSplit,
|
|
4117
|
+
{
|
|
4118
|
+
label: muted ? labels.unmute : labels.mute,
|
|
4119
|
+
pressed: muted,
|
|
4120
|
+
tone: muted ? "danger" : "default",
|
|
4121
|
+
onClick: onToggleMute,
|
|
4122
|
+
pickerLabel: labels.micDevice,
|
|
4123
|
+
options: devices.audio,
|
|
4124
|
+
selected: effective.audio,
|
|
4125
|
+
onSelect: (id) => void onDeviceChange("audio", id),
|
|
4126
|
+
children: muted ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, {})
|
|
4127
|
+
}
|
|
4128
|
+
),
|
|
4129
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4130
|
+
MediaSplit,
|
|
4131
|
+
{
|
|
4132
|
+
label: labels.camera,
|
|
4133
|
+
pressed: cameraOn,
|
|
4134
|
+
onClick: onToggleCamera,
|
|
4135
|
+
pickerLabel: labels.cameraDevice,
|
|
4136
|
+
options: devices.video,
|
|
4137
|
+
selected: effective.video,
|
|
4138
|
+
onSelect: (id) => void onDeviceChange("video", id),
|
|
4139
|
+
children: cameraOn ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.VideoOff, {})
|
|
4140
|
+
}
|
|
4141
|
+
)
|
|
4142
|
+
] }) : null,
|
|
4143
|
+
showStageModerator ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3625
4144
|
ControlIcon,
|
|
3626
4145
|
{
|
|
3627
|
-
label:
|
|
3628
|
-
pressed:
|
|
3629
|
-
|
|
3630
|
-
onClick:
|
|
3631
|
-
|
|
4146
|
+
label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : labels.goOnAir,
|
|
4147
|
+
pressed: selfStageStatus !== "ON_STAGE",
|
|
4148
|
+
disabled: selfStageStatus === "ON_STAGE" && !canLeaveStage,
|
|
4149
|
+
onClick: () => {
|
|
4150
|
+
if (selfStageStatus === "ON_STAGE") {
|
|
4151
|
+
if (!canLeaveStage) {
|
|
4152
|
+
sonner.toast.message(labels.offAirLastVisible);
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
onLeaveStage();
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
onAskStage();
|
|
4159
|
+
},
|
|
4160
|
+
children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
|
|
3632
4161
|
}
|
|
3633
|
-
),
|
|
3634
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4162
|
+
) : null,
|
|
4163
|
+
showStageGuest ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3635
4164
|
ControlIcon,
|
|
3636
4165
|
{
|
|
3637
|
-
label: labels.
|
|
3638
|
-
pressed:
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : labels.goOnAir,
|
|
3648
|
-
pressed: selfStageStatus !== "ON_STAGE",
|
|
3649
|
-
disabled: selfStageStatus === "ON_STAGE" && !canLeaveStage,
|
|
3650
|
-
onClick: () => {
|
|
3651
|
-
if (selfStageStatus === "ON_STAGE") {
|
|
3652
|
-
if (!canLeaveStage) {
|
|
3653
|
-
sonner.toast.message(labels.offAirLastVisible);
|
|
4166
|
+
label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
|
|
4167
|
+
pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
|
|
4168
|
+
disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
|
|
4169
|
+
onClick: () => {
|
|
4170
|
+
if (selfStageStatus === "ON_STAGE") {
|
|
4171
|
+
if (!canLeaveStage) {
|
|
4172
|
+
sonner.toast.message(labels.offAirLastVisible);
|
|
4173
|
+
return;
|
|
4174
|
+
}
|
|
4175
|
+
onLeaveStage();
|
|
3654
4176
|
return;
|
|
3655
4177
|
}
|
|
3656
|
-
|
|
3657
|
-
|
|
4178
|
+
onAskStage();
|
|
4179
|
+
},
|
|
4180
|
+
children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
|
|
4181
|
+
}
|
|
4182
|
+
) : null,
|
|
4183
|
+
showScreenControl ? canShareFreely ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4184
|
+
ControlIcon,
|
|
4185
|
+
{
|
|
4186
|
+
label: labels.screen,
|
|
4187
|
+
pressed: screenOn,
|
|
4188
|
+
disabled: screenPending || !screenAvailable && !screenOn,
|
|
4189
|
+
pending: screenPending,
|
|
4190
|
+
onClick: onToggleScreen,
|
|
4191
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {})
|
|
4192
|
+
}
|
|
4193
|
+
) : shareGrant === "requested" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4194
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4195
|
+
ControlIcon,
|
|
4196
|
+
{
|
|
4197
|
+
label: labels.shareRequested,
|
|
4198
|
+
pressed: true,
|
|
4199
|
+
disabled: true,
|
|
4200
|
+
onClick: () => void 0,
|
|
4201
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {})
|
|
3658
4202
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
{
|
|
3667
|
-
label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
|
|
3668
|
-
pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
|
|
3669
|
-
disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
|
|
3670
|
-
onClick: () => {
|
|
3671
|
-
if (selfStageStatus === "ON_STAGE") {
|
|
3672
|
-
if (!canLeaveStage) {
|
|
3673
|
-
sonner.toast.message(labels.offAirLastVisible);
|
|
3674
|
-
return;
|
|
3675
|
-
}
|
|
3676
|
-
onLeaveStage();
|
|
3677
|
-
return;
|
|
4203
|
+
),
|
|
4204
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4205
|
+
ControlIcon,
|
|
4206
|
+
{
|
|
4207
|
+
label: labels.cancelShareRequest,
|
|
4208
|
+
onClick: onCancelShareRequest,
|
|
4209
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, {})
|
|
3678
4210
|
}
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
|
|
3682
|
-
}
|
|
3683
|
-
) : null,
|
|
3684
|
-
showScreenControl ? canShareFreely ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3685
|
-
ControlIcon,
|
|
3686
|
-
{
|
|
3687
|
-
label: labels.screen,
|
|
3688
|
-
pressed: screenOn,
|
|
3689
|
-
disabled: screenPending || !screenAvailable && !screenOn,
|
|
3690
|
-
pending: screenPending,
|
|
3691
|
-
onClick: onToggleScreen,
|
|
3692
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {})
|
|
3693
|
-
}
|
|
3694
|
-
) : shareGrant === "requested" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4211
|
+
)
|
|
4212
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ControlIcon, { label: labels.askToShare, onClick: onAskToShare, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MonitorUp, {}) }) : null,
|
|
3695
4213
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3696
4214
|
ControlIcon,
|
|
3697
4215
|
{
|
|
3698
|
-
label:
|
|
3699
|
-
pressed:
|
|
3700
|
-
disabled:
|
|
3701
|
-
onClick:
|
|
3702
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.
|
|
4216
|
+
label: captionsLabel,
|
|
4217
|
+
pressed: captionsOn,
|
|
4218
|
+
disabled: !captionsAvailable,
|
|
4219
|
+
onClick: onToggleCaptions,
|
|
4220
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Captions, {})
|
|
3703
4221
|
}
|
|
3704
4222
|
),
|
|
4223
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
4224
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4225
|
+
DropdownMenuTrigger,
|
|
4226
|
+
{
|
|
4227
|
+
render: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4228
|
+
Button,
|
|
4229
|
+
{
|
|
4230
|
+
type: "button",
|
|
4231
|
+
size: "icon-lg",
|
|
4232
|
+
variant: "secondary",
|
|
4233
|
+
"aria-label": labels.more
|
|
4234
|
+
}
|
|
4235
|
+
),
|
|
4236
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MoreVertical, {})
|
|
4237
|
+
}
|
|
4238
|
+
),
|
|
4239
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuContent, { align: "center", className: "min-w-44", children: [
|
|
4240
|
+
fullscreenSupported ? /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuItem, { onClick: () => void enterFullscreen(), children: [
|
|
4241
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize, {}),
|
|
4242
|
+
labels.fullscreen
|
|
4243
|
+
] }) : null,
|
|
4244
|
+
pipSupported ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4245
|
+
DropdownMenuItem,
|
|
4246
|
+
{
|
|
4247
|
+
onClick: () => {
|
|
4248
|
+
if (pipOpen) onClosePip();
|
|
4249
|
+
else onOpenPip();
|
|
4250
|
+
},
|
|
4251
|
+
children: [
|
|
4252
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.PictureInPicture2, {}),
|
|
4253
|
+
pipOpen ? labels.closePip : labels.openPip
|
|
4254
|
+
]
|
|
4255
|
+
}
|
|
4256
|
+
) : null,
|
|
4257
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DropdownMenuItem, { onClick: onOpenSettings, children: [
|
|
4258
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, {}),
|
|
4259
|
+
labels.settings
|
|
4260
|
+
] })
|
|
4261
|
+
] })
|
|
4262
|
+
] })
|
|
4263
|
+
] }),
|
|
4264
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
|
|
3705
4265
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3706
4266
|
ControlIcon,
|
|
3707
4267
|
{
|
|
3708
|
-
label: labels.
|
|
3709
|
-
|
|
3710
|
-
|
|
4268
|
+
label: unreadChat > 0 ? `${labels.chat} (${unreadChat})` : labels.chat,
|
|
4269
|
+
pressed: chatOpen,
|
|
4270
|
+
badge: unreadChat,
|
|
4271
|
+
onClick: onToggleChat,
|
|
4272
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquare, {})
|
|
3711
4273
|
}
|
|
3712
|
-
)
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
4274
|
+
),
|
|
4275
|
+
session.role === "moderator" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4276
|
+
ControlIcon,
|
|
4277
|
+
{
|
|
4278
|
+
label: peopleBadge > 0 ? `${labels.people} (${peopleBadge})` : labels.people,
|
|
4279
|
+
pressed: peopleOpen,
|
|
4280
|
+
badge: peopleBadge,
|
|
4281
|
+
onClick: onTogglePeople,
|
|
4282
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, {})
|
|
4283
|
+
}
|
|
4284
|
+
) : null,
|
|
4285
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4286
|
+
ControlIcon,
|
|
4287
|
+
{
|
|
4288
|
+
label: labels.settings,
|
|
4289
|
+
pressed: settingsOpen,
|
|
4290
|
+
onClick: onOpenSettings,
|
|
4291
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, {})
|
|
4292
|
+
}
|
|
4293
|
+
),
|
|
4294
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4295
|
+
StudioSettingsDialog,
|
|
4296
|
+
{
|
|
4297
|
+
open: settingsOpen,
|
|
4298
|
+
onOpenChange: onCloseSettings,
|
|
4299
|
+
meeting,
|
|
4300
|
+
reducedMotion,
|
|
4301
|
+
cameraOn,
|
|
4302
|
+
labels,
|
|
4303
|
+
captionPrefs,
|
|
4304
|
+
onCaptionPrefsChange,
|
|
4305
|
+
captionsAvailable,
|
|
4306
|
+
effectBackgrounds
|
|
4307
|
+
}
|
|
4308
|
+
),
|
|
4309
|
+
session.role === "guest" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4310
|
+
ConfirmAction,
|
|
4311
|
+
{
|
|
4312
|
+
triggerLabel: labels.leave,
|
|
4313
|
+
title: labels.leaveTitle,
|
|
4314
|
+
description: labels.leaveDescription,
|
|
4315
|
+
confirmLabel: labels.leaveConfirm,
|
|
4316
|
+
pending,
|
|
4317
|
+
onConfirm: onLeave,
|
|
4318
|
+
trigger: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4319
|
+
Button,
|
|
4320
|
+
{
|
|
4321
|
+
type: "button",
|
|
4322
|
+
variant: "destructive",
|
|
4323
|
+
size: "icon-lg",
|
|
4324
|
+
disabled: pending,
|
|
4325
|
+
"aria-label": labels.leave,
|
|
4326
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PhoneOff, {})
|
|
4327
|
+
}
|
|
4328
|
+
)
|
|
4329
|
+
}
|
|
4330
|
+
) : null
|
|
4331
|
+
] })
|
|
4332
|
+
] }) });
|
|
4333
|
+
}
|
|
4334
|
+
function MediaSplit({
|
|
4335
|
+
label,
|
|
4336
|
+
pressed,
|
|
4337
|
+
tone = "default",
|
|
4338
|
+
onClick,
|
|
4339
|
+
pickerLabel,
|
|
4340
|
+
options,
|
|
4341
|
+
selected,
|
|
4342
|
+
onSelect,
|
|
4343
|
+
children
|
|
4344
|
+
}) {
|
|
4345
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center", children: [
|
|
3754
4346
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3755
4347
|
ControlIcon,
|
|
3756
4348
|
{
|
|
3757
|
-
label
|
|
3758
|
-
pressed
|
|
3759
|
-
|
|
3760
|
-
|
|
4349
|
+
label,
|
|
4350
|
+
pressed,
|
|
4351
|
+
tone,
|
|
4352
|
+
onClick,
|
|
4353
|
+
children
|
|
3761
4354
|
}
|
|
3762
4355
|
),
|
|
3763
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
cameraDevice: labels.cameraDevice,
|
|
3779
|
-
micDevice: labels.micDevice,
|
|
3780
|
-
speakerDevice: labels.speakerDevice,
|
|
3781
|
-
testSpeaker: labels.testSpeaker,
|
|
3782
|
-
testSpeakerError: labels.testSpeakerError,
|
|
3783
|
-
cameraOff: labels.cameraOff,
|
|
3784
|
-
micLevel: labels.micLevel
|
|
4356
|
+
options.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
4357
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4358
|
+
DropdownMenuTrigger,
|
|
4359
|
+
{
|
|
4360
|
+
render: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4361
|
+
Button,
|
|
4362
|
+
{
|
|
4363
|
+
type: "button",
|
|
4364
|
+
size: "icon-xs",
|
|
4365
|
+
variant: "secondary",
|
|
4366
|
+
className: "-ml-1",
|
|
4367
|
+
"aria-label": pickerLabel
|
|
4368
|
+
}
|
|
4369
|
+
),
|
|
4370
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronUp, { className: "size-3.5" })
|
|
3785
4371
|
}
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
4372
|
+
),
|
|
4373
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuContent, { align: "start", side: "top", className: "min-w-52", children: options.map((option) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4374
|
+
DropdownMenuItem,
|
|
4375
|
+
{
|
|
4376
|
+
onClick: () => onSelect(option.deviceId),
|
|
4377
|
+
children: [
|
|
4378
|
+
option.deviceId === selected ? "\u2713 " : "",
|
|
4379
|
+
option.label
|
|
4380
|
+
]
|
|
4381
|
+
},
|
|
4382
|
+
option.deviceId
|
|
4383
|
+
)) })
|
|
4384
|
+
] }) : null
|
|
4385
|
+
] });
|
|
4386
|
+
}
|
|
4387
|
+
var POLL_MS = 2e3;
|
|
4388
|
+
var PLAYOUT_MS = 250;
|
|
4389
|
+
var STUDIO_EXTRA_LAG_MS = 200;
|
|
4390
|
+
function useStudioCaptions({
|
|
4391
|
+
enabled,
|
|
4392
|
+
baseUrl,
|
|
4393
|
+
locale,
|
|
4394
|
+
streamStatus
|
|
4395
|
+
}) {
|
|
4396
|
+
const [playout, setPlayout] = react.useState(
|
|
4397
|
+
video.EMPTY_LIVE_CAPTION_PLAYOUT
|
|
4398
|
+
);
|
|
4399
|
+
const cuesRef = react.useRef([]);
|
|
4400
|
+
react.useEffect(() => {
|
|
4401
|
+
if (!enabled) {
|
|
4402
|
+
cuesRef.current = [];
|
|
4403
|
+
setPlayout(video.EMPTY_LIVE_CAPTION_PLAYOUT);
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
if (!baseUrl || !isStudioActiveStatus(streamStatus)) return;
|
|
4407
|
+
let cancelled = false;
|
|
4408
|
+
const url = video.liveCaptionsUrl(baseUrl, locale);
|
|
4409
|
+
const poll = async () => {
|
|
4410
|
+
const loaded = await video.loadLiveCaptions(url);
|
|
4411
|
+
if (cancelled || !loaded.ok) return;
|
|
4412
|
+
cuesRef.current = video.mergeLiveCaptionCues(cuesRef.current, loaded.cues);
|
|
4413
|
+
};
|
|
4414
|
+
void poll();
|
|
4415
|
+
const timer = window.setInterval(() => void poll(), POLL_MS);
|
|
4416
|
+
return () => {
|
|
4417
|
+
cancelled = true;
|
|
4418
|
+
window.clearInterval(timer);
|
|
4419
|
+
};
|
|
4420
|
+
}, [baseUrl, enabled, locale, streamStatus]);
|
|
4421
|
+
react.useEffect(() => {
|
|
4422
|
+
if (!enabled || !baseUrl || !isStudioActiveStatus(streamStatus)) {
|
|
4423
|
+
setPlayout(video.EMPTY_LIVE_CAPTION_PLAYOUT);
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
const tick2 = () => {
|
|
4427
|
+
setPlayout(
|
|
4428
|
+
(prev) => video.advanceLiveCaptionPlayout(
|
|
4429
|
+
prev,
|
|
4430
|
+
cuesRef.current,
|
|
4431
|
+
Date.now(),
|
|
4432
|
+
0,
|
|
4433
|
+
void 0,
|
|
4434
|
+
STUDIO_EXTRA_LAG_MS
|
|
3807
4435
|
)
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
4436
|
+
);
|
|
4437
|
+
};
|
|
4438
|
+
tick2();
|
|
4439
|
+
const timer = window.setInterval(tick2, PLAYOUT_MS);
|
|
4440
|
+
return () => window.clearInterval(timer);
|
|
4441
|
+
}, [baseUrl, enabled, streamStatus]);
|
|
4442
|
+
return playout;
|
|
3811
4443
|
}
|
|
3812
4444
|
var STAGE_GRANT_VERIFY_MS = 4e3;
|
|
3813
4445
|
var STUDIO_CONNECT_WATCHDOG_MS = 12e3;
|
|
@@ -3826,7 +4458,10 @@ function StudioRoom({
|
|
|
3826
4458
|
initialCameraOn = true,
|
|
3827
4459
|
initialMuted = false,
|
|
3828
4460
|
initialDevices,
|
|
3829
|
-
appDownloadUrl
|
|
4461
|
+
appDownloadUrl,
|
|
4462
|
+
captionsBaseUrl,
|
|
4463
|
+
apiUrl,
|
|
4464
|
+
cdnBase
|
|
3830
4465
|
}) {
|
|
3831
4466
|
const [cameraOn, setCameraOn] = react.useState(initialCameraOn);
|
|
3832
4467
|
const [screenOn, setScreenOn] = react.useState(false);
|
|
@@ -3848,6 +4483,11 @@ function StudioRoom({
|
|
|
3848
4483
|
const [egressError, setEgressError] = react.useState(null);
|
|
3849
4484
|
const [shareGrant, setShareGrant] = react.useState("idle");
|
|
3850
4485
|
const [downloadAppOpen, setDownloadAppOpen] = react.useState(false);
|
|
4486
|
+
const [captionsOn, setCaptionsOn] = react.useState(false);
|
|
4487
|
+
const [captionPrefs, setCaptionPrefs] = react.useState(
|
|
4488
|
+
() => readStudioCaptionPrefs()
|
|
4489
|
+
);
|
|
4490
|
+
const captionHintShown = react.useRef(false);
|
|
3851
4491
|
const [connectTimedOut, setConnectTimedOut] = react.useState(false);
|
|
3852
4492
|
const [screenPermission, setScreenPermission] = react.useState(
|
|
3853
4493
|
() => meeting.screenSharePermission()
|
|
@@ -3873,6 +4513,19 @@ function StudioRoom({
|
|
|
3873
4513
|
const pip = useDocumentPictureInPicture();
|
|
3874
4514
|
const gateFromContext = useStudioWindowGate();
|
|
3875
4515
|
const { reportDuplicate } = windowGate ?? gateFromContext;
|
|
4516
|
+
const resolvedCaptionsBaseUrl = captionsBaseUrl ?? studioCaptionsBaseUrl(apiUrl, session.stream.id);
|
|
4517
|
+
const captionsAvailable = session.stream.captionsEnabled !== false;
|
|
4518
|
+
const captionsReady = isStudioActiveStatus(session.stream.status);
|
|
4519
|
+
const captionPlayout = useStudioCaptions({
|
|
4520
|
+
enabled: captionsOn && captionsAvailable,
|
|
4521
|
+
baseUrl: resolvedCaptionsBaseUrl,
|
|
4522
|
+
locale: captionPrefs.locale,
|
|
4523
|
+
streamStatus: session.stream.status
|
|
4524
|
+
});
|
|
4525
|
+
const effectBackgrounds = react.useMemo(
|
|
4526
|
+
() => defaultStudioEffectBackgrounds(cdnBase),
|
|
4527
|
+
[cdnBase]
|
|
4528
|
+
);
|
|
3876
4529
|
const onAir = isLiveStudioStatus(session.stream.status);
|
|
3877
4530
|
const active = isStudioActiveStatus(session.stream.status);
|
|
3878
4531
|
if (onAir && liveStartedAtRef.current === null) {
|
|
@@ -4557,6 +5210,10 @@ function StudioRoom({
|
|
|
4557
5210
|
labels,
|
|
4558
5211
|
settingsOpen,
|
|
4559
5212
|
statusLabel: connectionState === "connecting" ? labels.connecting : labels.waitingHost,
|
|
5213
|
+
captionPrefs,
|
|
5214
|
+
onCaptionPrefsChange: setCaptionPrefs,
|
|
5215
|
+
captionsAvailable,
|
|
5216
|
+
effectBackgrounds,
|
|
4560
5217
|
onToggleCamera,
|
|
4561
5218
|
onToggleMute: () => void onToggleMute(),
|
|
4562
5219
|
onOpenSettings: () => setSettingsOpen(true),
|
|
@@ -4664,27 +5321,37 @@ function StudioRoom({
|
|
|
4664
5321
|
}
|
|
4665
5322
|
) : null,
|
|
4666
5323
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 flex-col md:flex-row", children: [
|
|
4667
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
5324
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex min-h-0 min-w-0 flex-1 flex-col", children: [
|
|
5325
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5326
|
+
StudioStage,
|
|
5327
|
+
{
|
|
5328
|
+
participants: stageParticipants,
|
|
5329
|
+
meeting,
|
|
5330
|
+
reducedMotion,
|
|
5331
|
+
canModerate: session.role === "moderator",
|
|
5332
|
+
canManageStage,
|
|
5333
|
+
onAir,
|
|
5334
|
+
labels,
|
|
5335
|
+
onKick: (id) => void onKick(id),
|
|
5336
|
+
onMute: (id) => void onHostMedia(id, "audio"),
|
|
5337
|
+
onStopCamera: (id) => void onHostMedia(id, "video"),
|
|
5338
|
+
onStopScreen: (id) => void onHostMedia(id, "screen"),
|
|
5339
|
+
onToggleStage: (tile) => void onSetStage(tile, !isOnStage(tile.stageStatus)),
|
|
5340
|
+
onTogglePin: (tile) => void onTogglePin(tile),
|
|
5341
|
+
onToggleChat: (tile) => void onToggleChat(tile),
|
|
5342
|
+
canTakeOff: (id) => canTakeOffAir(participants, id, onAir),
|
|
5343
|
+
onOpenPeople: () => setSideTab("people")
|
|
5344
|
+
}
|
|
5345
|
+
),
|
|
5346
|
+
captionsOn ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
5347
|
+
video.CaptionOverlay,
|
|
5348
|
+
{
|
|
5349
|
+
screen: captionPlayout.screen,
|
|
5350
|
+
className: "bottom-3",
|
|
5351
|
+
textClassName: captionSizeClass(captionPrefs.size)
|
|
5352
|
+
}
|
|
5353
|
+
) : null
|
|
5354
|
+
] }),
|
|
4688
5355
|
sideTab ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4689
5356
|
StudioSidePanel,
|
|
4690
5357
|
{
|
|
@@ -4797,6 +5464,12 @@ function StudioRoom({
|
|
|
4797
5464
|
stageEnabled,
|
|
4798
5465
|
canLeaveStage: selfId != null && canTakeOffAir(participants, selfId, onAir),
|
|
4799
5466
|
reducedMotion,
|
|
5467
|
+
captionsOn,
|
|
5468
|
+
captionsAvailable,
|
|
5469
|
+
captionsReady,
|
|
5470
|
+
captionPrefs,
|
|
5471
|
+
onCaptionPrefsChange: setCaptionPrefs,
|
|
5472
|
+
effectBackgrounds,
|
|
4800
5473
|
onToggleMute: () => void onToggleMute(),
|
|
4801
5474
|
onToggleCamera,
|
|
4802
5475
|
onToggleScreen,
|
|
@@ -4824,6 +5497,16 @@ function StudioRoom({
|
|
|
4824
5497
|
onCloseSettings: setSettingsOpen,
|
|
4825
5498
|
onOpenPip,
|
|
4826
5499
|
onClosePip: pip.close,
|
|
5500
|
+
onToggleCaptions: () => {
|
|
5501
|
+
if (!captionsAvailable) return;
|
|
5502
|
+
if (!captionsOn && !captionHintShown.current) {
|
|
5503
|
+
captionHintShown.current = true;
|
|
5504
|
+
sonner.toast.message(
|
|
5505
|
+
captionsReady ? labels.captionsDelayed : labels.captionsNotReady
|
|
5506
|
+
);
|
|
5507
|
+
}
|
|
5508
|
+
setCaptionsOn((current) => !current);
|
|
5509
|
+
},
|
|
4827
5510
|
onLeave: () => void onLeave()
|
|
4828
5511
|
}
|
|
4829
5512
|
),
|
|
@@ -5218,16 +5901,16 @@ function usePrefersReducedMotion() {
|
|
|
5218
5901
|
// src/studio/lib/guest-identity.ts
|
|
5219
5902
|
var NAME_KEY = "livo-studio-display-name";
|
|
5220
5903
|
var ID_KEY = "livo-studio-guest-id";
|
|
5221
|
-
function readStudioGuestIdentity(storage =
|
|
5904
|
+
function readStudioGuestIdentity(storage = defaultStorage4()) {
|
|
5222
5905
|
const guestId = storage?.getItem(ID_KEY)?.trim() || crypto.randomUUID();
|
|
5223
5906
|
storage?.setItem(ID_KEY, guestId);
|
|
5224
5907
|
const displayName = storage?.getItem(NAME_KEY)?.trim() ?? "";
|
|
5225
5908
|
return { displayName, guestId };
|
|
5226
5909
|
}
|
|
5227
|
-
function persistStudioGuestName(displayName, storage =
|
|
5910
|
+
function persistStudioGuestName(displayName, storage = defaultStorage4()) {
|
|
5228
5911
|
storage?.setItem(NAME_KEY, displayName.trim());
|
|
5229
5912
|
}
|
|
5230
|
-
function
|
|
5913
|
+
function defaultStorage4() {
|
|
5231
5914
|
if (typeof window === "undefined") return null;
|
|
5232
5915
|
try {
|
|
5233
5916
|
return window.localStorage;
|
|
@@ -5596,10 +6279,19 @@ function studioMeetingFromRtk(raw) {
|
|
|
5596
6279
|
if (typeof window !== "undefined") {
|
|
5597
6280
|
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
5598
6281
|
}
|
|
6282
|
+
let videoEffects = readStudioVideoEffects();
|
|
6283
|
+
async function applyCurrentVideoEffects() {
|
|
6284
|
+
await applyStudioVideoEffects({
|
|
6285
|
+
self: meeting.self,
|
|
6286
|
+
meeting,
|
|
6287
|
+
config: videoEffects
|
|
6288
|
+
});
|
|
6289
|
+
}
|
|
5599
6290
|
async function republishCameraIfOn() {
|
|
5600
6291
|
if (meeting.self?.videoEnabled !== true) return;
|
|
5601
6292
|
await meeting.self.disableVideo?.();
|
|
5602
6293
|
await meeting.self.enableVideo?.();
|
|
6294
|
+
await applyCurrentVideoEffects();
|
|
5603
6295
|
}
|
|
5604
6296
|
async function acceptAllowedStageIfOffAir() {
|
|
5605
6297
|
if (currentStageStatus() !== "OFF_STAGE") return;
|
|
@@ -5787,6 +6479,7 @@ function studioMeetingFromRtk(raw) {
|
|
|
5787
6479
|
async join() {
|
|
5788
6480
|
await meeting.join?.();
|
|
5789
6481
|
await joinStageIfAllowed();
|
|
6482
|
+
await applyCurrentVideoEffects();
|
|
5790
6483
|
},
|
|
5791
6484
|
async leave() {
|
|
5792
6485
|
if (typeof window !== "undefined") {
|
|
@@ -5799,8 +6492,10 @@ function studioMeetingFromRtk(raw) {
|
|
|
5799
6492
|
else await meeting.self?.disableAudio?.();
|
|
5800
6493
|
},
|
|
5801
6494
|
async setVideoEnabled(enabled) {
|
|
5802
|
-
if (enabled)
|
|
5803
|
-
|
|
6495
|
+
if (enabled) {
|
|
6496
|
+
await meeting.self?.enableVideo?.();
|
|
6497
|
+
await applyCurrentVideoEffects();
|
|
6498
|
+
} else await meeting.self?.disableVideo?.();
|
|
5804
6499
|
},
|
|
5805
6500
|
async setScreenShareEnabled(enabled) {
|
|
5806
6501
|
if (enabled) await meeting.self?.enableScreenShare?.();
|
|
@@ -6184,6 +6879,14 @@ function studioMeetingFromRtk(raw) {
|
|
|
6184
6879
|
}
|
|
6185
6880
|
});
|
|
6186
6881
|
},
|
|
6882
|
+
async setVideoEffects(config) {
|
|
6883
|
+
videoEffects = config;
|
|
6884
|
+
persistStudioVideoEffects(config);
|
|
6885
|
+
await applyCurrentVideoEffects();
|
|
6886
|
+
},
|
|
6887
|
+
videoEffects() {
|
|
6888
|
+
return videoEffects;
|
|
6889
|
+
},
|
|
6187
6890
|
async setDisplayName(name) {
|
|
6188
6891
|
const trimmed = name.trim().slice(0, STUDIO_DISPLAY_NAME_MAX);
|
|
6189
6892
|
if (!trimmed) return;
|
|
@@ -6415,6 +7118,21 @@ async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn
|
|
|
6415
7118
|
return null;
|
|
6416
7119
|
}
|
|
6417
7120
|
}
|
|
7121
|
+
async function getPublicStreamMeta(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
|
|
7122
|
+
try {
|
|
7123
|
+
const body = await publicJson(
|
|
7124
|
+
`${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
|
|
7125
|
+
{},
|
|
7126
|
+
fetchFn
|
|
7127
|
+
);
|
|
7128
|
+
return parsePublicStreamMeta(body);
|
|
7129
|
+
} catch (error) {
|
|
7130
|
+
if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
|
|
7131
|
+
return { status: "ended", captionsEnabled: false };
|
|
7132
|
+
}
|
|
7133
|
+
return null;
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
6418
7136
|
var POLL_INTERVAL_MS = 5e3;
|
|
6419
7137
|
function LivoHostStudio({
|
|
6420
7138
|
token,
|
|
@@ -6430,6 +7148,9 @@ function LivoHostStudio({
|
|
|
6430
7148
|
const reducedMotion = usePrefersReducedMotion();
|
|
6431
7149
|
const [session, setSession] = react.useState(null);
|
|
6432
7150
|
const [status, setStatus] = react.useState(null);
|
|
7151
|
+
const [captionsEnabled, setCaptionsEnabled] = react.useState(
|
|
7152
|
+
void 0
|
|
7153
|
+
);
|
|
6433
7154
|
const [error, setError] = react.useState(null);
|
|
6434
7155
|
const { meeting, error: meetingError } = useRealtimeKitMeeting(
|
|
6435
7156
|
session?.authToken ?? null
|
|
@@ -6478,6 +7199,20 @@ function LivoHostStudio({
|
|
|
6478
7199
|
window.clearInterval(id);
|
|
6479
7200
|
};
|
|
6480
7201
|
}, [client, status]);
|
|
7202
|
+
react.useEffect(() => {
|
|
7203
|
+
if (!session) return;
|
|
7204
|
+
let cancelled = false;
|
|
7205
|
+
const poll = async () => {
|
|
7206
|
+
const next = await getPublicStreamMeta(session.stream.id, apiUrl);
|
|
7207
|
+
if (!cancelled && next) setCaptionsEnabled(next.captionsEnabled);
|
|
7208
|
+
};
|
|
7209
|
+
void poll();
|
|
7210
|
+
const id = window.setInterval(() => void poll(), POLL_INTERVAL_MS);
|
|
7211
|
+
return () => {
|
|
7212
|
+
cancelled = true;
|
|
7213
|
+
window.clearInterval(id);
|
|
7214
|
+
};
|
|
7215
|
+
}, [apiUrl, session]);
|
|
6481
7216
|
const liveSession = react.useMemo(() => {
|
|
6482
7217
|
if (!session) return null;
|
|
6483
7218
|
if (!status) return session;
|
|
@@ -6485,10 +7220,11 @@ function LivoHostStudio({
|
|
|
6485
7220
|
...session,
|
|
6486
7221
|
stream: {
|
|
6487
7222
|
...session.stream,
|
|
6488
|
-
status
|
|
7223
|
+
status,
|
|
7224
|
+
captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
|
|
6489
7225
|
}
|
|
6490
7226
|
};
|
|
6491
|
-
}, [session, status]);
|
|
7227
|
+
}, [captionsEnabled, session, status]);
|
|
6492
7228
|
if (error || meetingError) {
|
|
6493
7229
|
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive p-6", role: "alert", children: error ?? meetingError });
|
|
6494
7230
|
}
|
|
@@ -6506,7 +7242,8 @@ function LivoHostStudio({
|
|
|
6506
7242
|
embed,
|
|
6507
7243
|
chrome,
|
|
6508
7244
|
playerOrigin,
|
|
6509
|
-
appDownloadUrl
|
|
7245
|
+
appDownloadUrl,
|
|
7246
|
+
apiUrl
|
|
6510
7247
|
}
|
|
6511
7248
|
);
|
|
6512
7249
|
}
|
|
@@ -6525,6 +7262,9 @@ function LivoGuestStudio({
|
|
|
6525
7262
|
const reducedMotion = usePrefersReducedMotion();
|
|
6526
7263
|
const [session, setSession] = react.useState(null);
|
|
6527
7264
|
const [status, setStatus] = react.useState(null);
|
|
7265
|
+
const [captionsEnabled, setCaptionsEnabled] = react.useState(
|
|
7266
|
+
void 0
|
|
7267
|
+
);
|
|
6528
7268
|
const [error, setError] = react.useState(null);
|
|
6529
7269
|
const [displayName, setDisplayName] = react.useState("");
|
|
6530
7270
|
const [guestId, setGuestId] = react.useState("");
|
|
@@ -6628,8 +7368,11 @@ function LivoGuestStudio({
|
|
|
6628
7368
|
if (current === "ended" || current === "error") return;
|
|
6629
7369
|
let cancelled = false;
|
|
6630
7370
|
const poll = async () => {
|
|
6631
|
-
const next = await
|
|
6632
|
-
if (!cancelled && next)
|
|
7371
|
+
const next = await getPublicStreamMeta(session.stream.id, apiUrl);
|
|
7372
|
+
if (!cancelled && next) {
|
|
7373
|
+
setStatus(next.status);
|
|
7374
|
+
setCaptionsEnabled(next.captionsEnabled);
|
|
7375
|
+
}
|
|
6633
7376
|
};
|
|
6634
7377
|
void poll();
|
|
6635
7378
|
const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
|
|
@@ -6644,9 +7387,13 @@ function LivoGuestStudio({
|
|
|
6644
7387
|
if (!status) return session;
|
|
6645
7388
|
return {
|
|
6646
7389
|
...session,
|
|
6647
|
-
stream: {
|
|
7390
|
+
stream: {
|
|
7391
|
+
...session.stream,
|
|
7392
|
+
status,
|
|
7393
|
+
captionsEnabled: captionsEnabled ?? session.stream.captionsEnabled
|
|
7394
|
+
}
|
|
6648
7395
|
};
|
|
6649
|
-
}, [session, status]);
|
|
7396
|
+
}, [captionsEnabled, session, status]);
|
|
6650
7397
|
react.useEffect(() => {
|
|
6651
7398
|
if (retryAfterMs == null) return;
|
|
6652
7399
|
const id = window.setTimeout(() => {
|
|
@@ -6707,7 +7454,8 @@ function LivoGuestStudio({
|
|
|
6707
7454
|
initialCameraOn: cameraOn,
|
|
6708
7455
|
initialMuted: muted,
|
|
6709
7456
|
initialDevices: deviceSelection,
|
|
6710
|
-
appDownloadUrl
|
|
7457
|
+
appDownloadUrl,
|
|
7458
|
+
apiUrl
|
|
6711
7459
|
}
|
|
6712
7460
|
);
|
|
6713
7461
|
}
|
|
@@ -6883,7 +7631,37 @@ function studioRoomLabels(t) {
|
|
|
6883
7631
|
chatPin: t("chatPin"),
|
|
6884
7632
|
chatUnpin: t("chatUnpin"),
|
|
6885
7633
|
chatSave: t("chatSave"),
|
|
6886
|
-
waitingSkeleton: t("waitingSkeleton")
|
|
7634
|
+
waitingSkeleton: t("waitingSkeleton"),
|
|
7635
|
+
more: t("more"),
|
|
7636
|
+
captions: t("captions"),
|
|
7637
|
+
captionsOn: t("captionsOn"),
|
|
7638
|
+
captionsOff: t("captionsOff"),
|
|
7639
|
+
captionsNotReady: t("captionsNotReady"),
|
|
7640
|
+
captionsDelayed: t("captionsDelayed"),
|
|
7641
|
+
captionsLanguage: t("captionsLanguage"),
|
|
7642
|
+
captionsSize: t("captionsSize"),
|
|
7643
|
+
captionsSizeS: t("captionsSizeS"),
|
|
7644
|
+
captionsSizeM: t("captionsSizeM"),
|
|
7645
|
+
captionsSizeL: t("captionsSizeL"),
|
|
7646
|
+
settingsGeneral: t("settingsGeneral"),
|
|
7647
|
+
settingsAudio: t("settingsAudio"),
|
|
7648
|
+
settingsVideo: t("settingsVideo"),
|
|
7649
|
+
settingsEffects: t("settingsEffects"),
|
|
7650
|
+
settingsSubtitles: t("settingsSubtitles"),
|
|
7651
|
+
effectNone: t("effectNone"),
|
|
7652
|
+
effectBlur: t("effectBlur"),
|
|
7653
|
+
effectBlurStrong: t("effectBlurStrong"),
|
|
7654
|
+
effectBackgrounds: t("effectBackgrounds"),
|
|
7655
|
+
effectFilters: t("effectFilters"),
|
|
7656
|
+
effectFilterNone: t("effectFilterNone"),
|
|
7657
|
+
effectFilterGrayscale: t("effectFilterGrayscale"),
|
|
7658
|
+
effectFilterSepia: t("effectFilterSepia"),
|
|
7659
|
+
effectFilterWarm: t("effectFilterWarm"),
|
|
7660
|
+
effectFilterCool: t("effectFilterCool"),
|
|
7661
|
+
effectFilterBright: t("effectFilterBright"),
|
|
7662
|
+
localeEn: t("localeEn"),
|
|
7663
|
+
localeEs: t("localeEs"),
|
|
7664
|
+
localePt: t("localePt")
|
|
6887
7665
|
};
|
|
6888
7666
|
}
|
|
6889
7667
|
|
|
@@ -6901,6 +7679,7 @@ exports.applyEmbedTheme = applyEmbedTheme;
|
|
|
6901
7679
|
exports.controlActionsFromClient = controlActionsFromClient;
|
|
6902
7680
|
exports.createStudioControlClient = createStudioControlClient;
|
|
6903
7681
|
exports.decodeEmbedTheme = decodeEmbedTheme;
|
|
7682
|
+
exports.getPublicStreamMeta = getPublicStreamMeta;
|
|
6904
7683
|
exports.getPublicStreamStatus = getPublicStreamStatus;
|
|
6905
7684
|
exports.getStudioJoinStatus = getStudioJoinStatus;
|
|
6906
7685
|
exports.joinStudioGuest = joinStudioGuest;
|