@livo-tv/sdk 1.3.3 → 1.5.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 +421 -142
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.cts +10 -3
- package/dist/studio/index.d.ts +10 -3
- package/dist/studio/index.js +421 -142
- package/dist/studio/index.js.map +1 -1
- package/dist/styles.css +23 -0
- package/package.json +2 -2
package/dist/studio/index.cjs
CHANGED
|
@@ -542,52 +542,38 @@ function canTakeOffAir(tiles, participantId, onAir) {
|
|
|
542
542
|
(tile) => tile.id !== participantId && isOnStage(tile.stageStatus)
|
|
543
543
|
);
|
|
544
544
|
}
|
|
545
|
+
function pipSpotlightFrom(tile, kind) {
|
|
546
|
+
return {
|
|
547
|
+
participantId: tile.id,
|
|
548
|
+
name: tile.name,
|
|
549
|
+
kind,
|
|
550
|
+
picture: tile.picture
|
|
551
|
+
};
|
|
552
|
+
}
|
|
545
553
|
function resolveStudioPipSpotlight(tiles, activeSpeakerId) {
|
|
546
554
|
const visible = tiles.filter((tile) => isOnStage(tile.stageStatus));
|
|
547
555
|
const pinned = visible.find((tile) => tile.pinned === true);
|
|
548
556
|
if (pinned) {
|
|
549
|
-
return
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
557
|
+
return pipSpotlightFrom(
|
|
558
|
+
pinned,
|
|
559
|
+
pinned.screenShareEnabled ? "screen" : pinned.videoEnabled ? "camera" : "idle"
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId) : void 0;
|
|
563
|
+
if (speaker) {
|
|
564
|
+
return pipSpotlightFrom(speaker, speaker.videoEnabled ? "camera" : "idle");
|
|
555
565
|
}
|
|
556
566
|
const sharing = visible.find((tile) => tile.screenShareEnabled);
|
|
557
567
|
if (sharing) {
|
|
558
|
-
return
|
|
559
|
-
participantId: sharing.id,
|
|
560
|
-
name: sharing.name,
|
|
561
|
-
kind: "screen",
|
|
562
|
-
picture: sharing.picture
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
const speaker = activeSpeakerId ? visible.find((tile) => tile.id === activeSpeakerId && tile.videoEnabled) : void 0;
|
|
566
|
-
if (speaker) {
|
|
567
|
-
return {
|
|
568
|
-
participantId: speaker.id,
|
|
569
|
-
name: speaker.name,
|
|
570
|
-
kind: "camera",
|
|
571
|
-
picture: speaker.picture
|
|
572
|
-
};
|
|
568
|
+
return pipSpotlightFrom(sharing, "screen");
|
|
573
569
|
}
|
|
574
570
|
const remoteCam = visible.find((tile) => !tile.isSelf && tile.videoEnabled);
|
|
575
571
|
if (remoteCam) {
|
|
576
|
-
return
|
|
577
|
-
participantId: remoteCam.id,
|
|
578
|
-
name: remoteCam.name,
|
|
579
|
-
kind: "camera",
|
|
580
|
-
picture: remoteCam.picture
|
|
581
|
-
};
|
|
572
|
+
return pipSpotlightFrom(remoteCam, "camera");
|
|
582
573
|
}
|
|
583
574
|
const self = visible.find((tile) => tile.isSelf);
|
|
584
575
|
if (!self) return null;
|
|
585
|
-
return
|
|
586
|
-
participantId: self.id,
|
|
587
|
-
name: self.name,
|
|
588
|
-
kind: self.videoEnabled ? "camera" : "idle",
|
|
589
|
-
picture: self.picture
|
|
590
|
-
};
|
|
576
|
+
return pipSpotlightFrom(self, self.videoEnabled ? "camera" : "idle");
|
|
591
577
|
}
|
|
592
578
|
function resolveStudioPipSelfView(tiles, spotlight) {
|
|
593
579
|
const self = tiles.find((tile) => tile.isSelf);
|
|
@@ -706,6 +692,9 @@ function screenShareBlock(captureSupported, permission) {
|
|
|
706
692
|
if (permission === "NOT_ALLOWED") return "permission";
|
|
707
693
|
return null;
|
|
708
694
|
}
|
|
695
|
+
function showScreenShareControl(block) {
|
|
696
|
+
return block !== "permission";
|
|
697
|
+
}
|
|
709
698
|
|
|
710
699
|
// src/studio/lib/parse.ts
|
|
711
700
|
function isRecord(value) {
|
|
@@ -1274,15 +1263,15 @@ function AudioWaveform({
|
|
|
1274
1263
|
if (!canvas || !track || reducedMotion) return;
|
|
1275
1264
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
1276
1265
|
if (!AudioCtx) return;
|
|
1277
|
-
const
|
|
1278
|
-
const source =
|
|
1279
|
-
const analyser =
|
|
1266
|
+
const ctx3 = new AudioCtx();
|
|
1267
|
+
const source = ctx3.createMediaStreamSource(new MediaStream([track]));
|
|
1268
|
+
const analyser = ctx3.createAnalyser();
|
|
1280
1269
|
analyser.fftSize = 64;
|
|
1281
1270
|
source.connect(analyser);
|
|
1282
1271
|
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
1283
1272
|
const graphic = canvas.getContext("2d");
|
|
1284
1273
|
const surface = canvas;
|
|
1285
|
-
let
|
|
1274
|
+
let frame2 = 0;
|
|
1286
1275
|
function draw() {
|
|
1287
1276
|
if (!graphic) return;
|
|
1288
1277
|
analyser.getByteFrequencyData(data);
|
|
@@ -1300,12 +1289,12 @@ function AudioWaveform({
|
|
|
1300
1289
|
barHeight
|
|
1301
1290
|
);
|
|
1302
1291
|
}
|
|
1303
|
-
|
|
1292
|
+
frame2 = requestAnimationFrame(draw);
|
|
1304
1293
|
}
|
|
1305
|
-
|
|
1294
|
+
frame2 = requestAnimationFrame(draw);
|
|
1306
1295
|
return () => {
|
|
1307
|
-
cancelAnimationFrame(
|
|
1308
|
-
void
|
|
1296
|
+
cancelAnimationFrame(frame2);
|
|
1297
|
+
void ctx3.close();
|
|
1309
1298
|
};
|
|
1310
1299
|
}, [track, reducedMotion]);
|
|
1311
1300
|
if (reducedMotion) {
|
|
@@ -1488,6 +1477,19 @@ function DialogHeader({ className, ...props }) {
|
|
|
1488
1477
|
}
|
|
1489
1478
|
);
|
|
1490
1479
|
}
|
|
1480
|
+
function DialogFooter({ className, ...props }) {
|
|
1481
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1482
|
+
"div",
|
|
1483
|
+
{
|
|
1484
|
+
"data-slot": "dialog-footer",
|
|
1485
|
+
className: cn(
|
|
1486
|
+
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
1487
|
+
className
|
|
1488
|
+
),
|
|
1489
|
+
...props
|
|
1490
|
+
}
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1491
1493
|
function DialogTitle({
|
|
1492
1494
|
className,
|
|
1493
1495
|
...props
|
|
@@ -1591,10 +1593,10 @@ async function playSpeakerTest(deviceId) {
|
|
|
1591
1593
|
if (!AudioCtx) {
|
|
1592
1594
|
throw new Error("AudioContext unavailable");
|
|
1593
1595
|
}
|
|
1594
|
-
const
|
|
1595
|
-
void
|
|
1596
|
-
const oscillator =
|
|
1597
|
-
const gain =
|
|
1596
|
+
const ctx3 = new AudioCtx();
|
|
1597
|
+
void ctx3.resume();
|
|
1598
|
+
const oscillator = ctx3.createOscillator();
|
|
1599
|
+
const gain = ctx3.createGain();
|
|
1598
1600
|
oscillator.type = "sine";
|
|
1599
1601
|
oscillator.frequency.value = TEST_HZ;
|
|
1600
1602
|
gain.gain.value = 0.18;
|
|
@@ -1604,7 +1606,7 @@ async function playSpeakerTest(deviceId) {
|
|
|
1604
1606
|
const probe = document.createElement("audio");
|
|
1605
1607
|
const canRoute = Boolean(deviceId) && typeof probe.setSinkId === "function";
|
|
1606
1608
|
if (canRoute && deviceId) {
|
|
1607
|
-
const destination =
|
|
1609
|
+
const destination = ctx3.createMediaStreamDestination();
|
|
1608
1610
|
gain.connect(destination);
|
|
1609
1611
|
audio = probe;
|
|
1610
1612
|
audio.autoplay = true;
|
|
@@ -1615,13 +1617,13 @@ async function playSpeakerTest(deviceId) {
|
|
|
1615
1617
|
await audio.play();
|
|
1616
1618
|
} catch {
|
|
1617
1619
|
gain.disconnect();
|
|
1618
|
-
gain.connect(
|
|
1620
|
+
gain.connect(ctx3.destination);
|
|
1619
1621
|
audio.srcObject = null;
|
|
1620
1622
|
audio.remove();
|
|
1621
1623
|
audio = null;
|
|
1622
1624
|
}
|
|
1623
1625
|
} else {
|
|
1624
|
-
gain.connect(
|
|
1626
|
+
gain.connect(ctx3.destination);
|
|
1625
1627
|
}
|
|
1626
1628
|
oscillator.start();
|
|
1627
1629
|
await new Promise((resolve) => {
|
|
@@ -1633,7 +1635,7 @@ async function playSpeakerTest(deviceId) {
|
|
|
1633
1635
|
audio.srcObject = null;
|
|
1634
1636
|
audio.remove();
|
|
1635
1637
|
}
|
|
1636
|
-
await
|
|
1638
|
+
await ctx3.close();
|
|
1637
1639
|
}
|
|
1638
1640
|
}
|
|
1639
1641
|
function StudioDeviceSelectGroup({
|
|
@@ -2047,6 +2049,19 @@ var badgeVariants = classVarianceAuthority.cva(
|
|
|
2047
2049
|
function Badge({ className, variant, ...props }) {
|
|
2048
2050
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
2049
2051
|
}
|
|
2052
|
+
|
|
2053
|
+
// src/studio/lib/studio-video-style.ts
|
|
2054
|
+
var STUDIO_VIDEO_MEDIA_STYLE = `
|
|
2055
|
+
.livo-studio-tile-video::-webkit-media-controls-start-playback-button,
|
|
2056
|
+
.livo-studio-inline-video::-webkit-media-controls-start-playback-button {
|
|
2057
|
+
display: none !important;
|
|
2058
|
+
-webkit-appearance: none;
|
|
2059
|
+
}
|
|
2060
|
+
.livo-studio-tile-video::-webkit-media-controls,
|
|
2061
|
+
.livo-studio-inline-video::-webkit-media-controls {
|
|
2062
|
+
display: none !important;
|
|
2063
|
+
}
|
|
2064
|
+
`;
|
|
2050
2065
|
function StudioPipPanel({
|
|
2051
2066
|
meeting,
|
|
2052
2067
|
labels,
|
|
@@ -2101,7 +2116,7 @@ function StudioPipPanel({
|
|
|
2101
2116
|
/* @__PURE__ */ jsxRuntime.jsx(StudioLiveBadge, { children: labels.live }),
|
|
2102
2117
|
elapsed ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground tabular-nums text-xs", children: elapsed }) : null
|
|
2103
2118
|
] }) : null,
|
|
2104
|
-
selfView ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2119
|
+
selfView && meeting.localVideoTrack() ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2105
2120
|
"div",
|
|
2106
2121
|
{
|
|
2107
2122
|
"data-testid": "pip-self-view",
|
|
@@ -2158,7 +2173,8 @@ function PipSpotlight({
|
|
|
2158
2173
|
spotlight,
|
|
2159
2174
|
cameraOffLabel
|
|
2160
2175
|
}) {
|
|
2161
|
-
|
|
2176
|
+
const track = spotlight && spotlight.kind !== "idle" ? spotlight.kind === "screen" ? meeting.participantScreenTrack(spotlight.participantId) : meeting.participantVideoTrack(spotlight.participantId) : null;
|
|
2177
|
+
if (!spotlight || spotlight.kind === "idle" || !track) {
|
|
2162
2178
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-2", children: [
|
|
2163
2179
|
spotlight?.picture ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2164
2180
|
"img",
|
|
@@ -2171,7 +2187,6 @@ function PipSpotlight({
|
|
|
2171
2187
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-xs", children: spotlight?.name ?? cameraOffLabel })
|
|
2172
2188
|
] });
|
|
2173
2189
|
}
|
|
2174
|
-
const track = spotlight.kind === "screen" ? meeting.participantScreenTrack(spotlight.participantId) : meeting.participantVideoTrack(spotlight.participantId);
|
|
2175
2190
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2176
2191
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2177
2192
|
PipTrackVideo,
|
|
@@ -2190,11 +2205,7 @@ function PipTrackVideo({
|
|
|
2190
2205
|
const ref = react.useRef(null);
|
|
2191
2206
|
react.useEffect(() => {
|
|
2192
2207
|
const el = ref.current;
|
|
2193
|
-
if (!el) return;
|
|
2194
|
-
if (!track) {
|
|
2195
|
-
el.srcObject = null;
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2208
|
+
if (!el || !track) return;
|
|
2198
2209
|
el.setAttribute("webkit-playsinline", "");
|
|
2199
2210
|
el.srcObject = new MediaStream([track]);
|
|
2200
2211
|
void el.play().catch(() => void 0);
|
|
@@ -2202,19 +2213,24 @@ function PipTrackVideo({
|
|
|
2202
2213
|
el.srcObject = null;
|
|
2203
2214
|
};
|
|
2204
2215
|
}, [track]);
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
{
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2216
|
+
if (!track) return null;
|
|
2217
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2218
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: STUDIO_VIDEO_MEDIA_STYLE }),
|
|
2219
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2220
|
+
"video",
|
|
2221
|
+
{
|
|
2222
|
+
ref,
|
|
2223
|
+
autoPlay: true,
|
|
2224
|
+
playsInline: true,
|
|
2225
|
+
muted: true,
|
|
2226
|
+
disableRemotePlayback: true,
|
|
2227
|
+
className: cn(
|
|
2228
|
+
"livo-studio-inline-video absolute inset-0 size-full",
|
|
2229
|
+
fit === "contain" ? "object-contain" : "object-cover"
|
|
2230
|
+
)
|
|
2231
|
+
}
|
|
2232
|
+
)
|
|
2233
|
+
] });
|
|
2218
2234
|
}
|
|
2219
2235
|
function pipScreenControls({
|
|
2220
2236
|
labels,
|
|
@@ -2416,7 +2432,7 @@ function StudioChatPanel({
|
|
|
2416
2432
|
isModerator: isModerator2,
|
|
2417
2433
|
labels
|
|
2418
2434
|
}) {
|
|
2419
|
-
const [
|
|
2435
|
+
const [tick2, setTick] = react.useState(0);
|
|
2420
2436
|
const [draft, setDraft] = react.useState("");
|
|
2421
2437
|
const [editingId, setEditingId] = react.useState(null);
|
|
2422
2438
|
const listRef = react.useRef(null);
|
|
@@ -2426,9 +2442,9 @@ function StudioChatPanel({
|
|
|
2426
2442
|
);
|
|
2427
2443
|
const messages = react.useMemo(
|
|
2428
2444
|
() => meeting.chatMessages().filter((row) => !row.targetUserIds?.length),
|
|
2429
|
-
[meeting,
|
|
2445
|
+
[meeting, tick2]
|
|
2430
2446
|
);
|
|
2431
|
-
const pinned = react.useMemo(() => meeting.pinnedChatMessages(), [meeting,
|
|
2447
|
+
const pinned = react.useMemo(() => meeting.pinnedChatMessages(), [meeting, tick2]);
|
|
2432
2448
|
const limit = meeting.chatMaxTextLimit();
|
|
2433
2449
|
const pictures = react.useMemo(() => {
|
|
2434
2450
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -2928,6 +2944,171 @@ function StudioSidePanel({
|
|
|
2928
2944
|
] })
|
|
2929
2945
|
] });
|
|
2930
2946
|
}
|
|
2947
|
+
|
|
2948
|
+
// src/studio/lib/audio-levels.ts
|
|
2949
|
+
var LEVEL_SENSITIVITY = 4;
|
|
2950
|
+
var LEVEL_ATTACK = 0.55;
|
|
2951
|
+
var LEVEL_DECAY = 0.12;
|
|
2952
|
+
var SPEAKING_ON = 0.12;
|
|
2953
|
+
var SPEAKING_OFF = 0.06;
|
|
2954
|
+
var SPEAKING_HOLD_MS = 180;
|
|
2955
|
+
var tracks = /* @__PURE__ */ new Map();
|
|
2956
|
+
var ctx2 = null;
|
|
2957
|
+
var frame = 0;
|
|
2958
|
+
var tickRunning = false;
|
|
2959
|
+
function audioContextCtor2() {
|
|
2960
|
+
if (typeof window === "undefined") return null;
|
|
2961
|
+
return window.AudioContext || window.webkitAudioContext || null;
|
|
2962
|
+
}
|
|
2963
|
+
function ensureContext2() {
|
|
2964
|
+
const AudioCtx = audioContextCtor2();
|
|
2965
|
+
if (!AudioCtx) return null;
|
|
2966
|
+
if (!ctx2 || ctx2.state === "closed") {
|
|
2967
|
+
ctx2 = new AudioCtx();
|
|
2968
|
+
}
|
|
2969
|
+
return ctx2;
|
|
2970
|
+
}
|
|
2971
|
+
function rmsFromTimeDomain(data) {
|
|
2972
|
+
if (data.length === 0) return 0;
|
|
2973
|
+
let sum = 0;
|
|
2974
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
2975
|
+
const centered = ((data[i] ?? 128) - 128) / 128;
|
|
2976
|
+
sum += centered * centered;
|
|
2977
|
+
}
|
|
2978
|
+
return Math.sqrt(sum / data.length);
|
|
2979
|
+
}
|
|
2980
|
+
function normalizeAudioLevel(rms) {
|
|
2981
|
+
return Math.min(1, Math.max(0, rms * LEVEL_SENSITIVITY));
|
|
2982
|
+
}
|
|
2983
|
+
function smoothAudioLevel(previous, raw) {
|
|
2984
|
+
const factor = raw > previous ? LEVEL_ATTACK : LEVEL_DECAY;
|
|
2985
|
+
return previous + (raw - previous) * factor;
|
|
2986
|
+
}
|
|
2987
|
+
function nextSpeakingGate(gate, level, now) {
|
|
2988
|
+
if (!gate.speaking) {
|
|
2989
|
+
if (level >= SPEAKING_ON) {
|
|
2990
|
+
return { speaking: true, quietSince: null };
|
|
2991
|
+
}
|
|
2992
|
+
return { speaking: false, quietSince: null };
|
|
2993
|
+
}
|
|
2994
|
+
if (level >= SPEAKING_OFF) {
|
|
2995
|
+
return { speaking: true, quietSince: null };
|
|
2996
|
+
}
|
|
2997
|
+
const quietSince = gate.quietSince ?? now;
|
|
2998
|
+
if (now - quietSince >= SPEAKING_HOLD_MS) {
|
|
2999
|
+
return { speaking: false, quietSince: null };
|
|
3000
|
+
}
|
|
3001
|
+
return { speaking: true, quietSince };
|
|
3002
|
+
}
|
|
3003
|
+
function tick() {
|
|
3004
|
+
for (const entry of tracks.values()) {
|
|
3005
|
+
entry.analyser.getByteTimeDomainData(entry.data);
|
|
3006
|
+
const raw = normalizeAudioLevel(rmsFromTimeDomain(entry.data));
|
|
3007
|
+
entry.smoothed = smoothAudioLevel(entry.smoothed, raw);
|
|
3008
|
+
for (const cb of entry.listeners) {
|
|
3009
|
+
cb(entry.smoothed);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (tracks.size > 0) {
|
|
3013
|
+
frame = requestAnimationFrame(tick);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
tickRunning = false;
|
|
3017
|
+
frame = 0;
|
|
3018
|
+
}
|
|
3019
|
+
function startTick() {
|
|
3020
|
+
if (tickRunning) return;
|
|
3021
|
+
tickRunning = true;
|
|
3022
|
+
frame = requestAnimationFrame(tick);
|
|
3023
|
+
}
|
|
3024
|
+
function stopTickIfIdle() {
|
|
3025
|
+
if (tracks.size > 0 || !frame) return;
|
|
3026
|
+
cancelAnimationFrame(frame);
|
|
3027
|
+
tickRunning = false;
|
|
3028
|
+
frame = 0;
|
|
3029
|
+
}
|
|
3030
|
+
function subscribeStudioAudioLevel(track, cb) {
|
|
3031
|
+
const context = ensureContext2();
|
|
3032
|
+
if (!context) return () => void 0;
|
|
3033
|
+
void context.resume();
|
|
3034
|
+
const id = track.id;
|
|
3035
|
+
let entry = tracks.get(id);
|
|
3036
|
+
if (!entry) {
|
|
3037
|
+
const source = context.createMediaStreamSource(new MediaStream([track]));
|
|
3038
|
+
const analyser = context.createAnalyser();
|
|
3039
|
+
analyser.fftSize = 512;
|
|
3040
|
+
analyser.smoothingTimeConstant = 0;
|
|
3041
|
+
source.connect(analyser);
|
|
3042
|
+
entry = {
|
|
3043
|
+
source,
|
|
3044
|
+
analyser,
|
|
3045
|
+
data: new Uint8Array(analyser.fftSize),
|
|
3046
|
+
refs: 0,
|
|
3047
|
+
smoothed: 0,
|
|
3048
|
+
listeners: /* @__PURE__ */ new Set()
|
|
3049
|
+
};
|
|
3050
|
+
tracks.set(id, entry);
|
|
3051
|
+
}
|
|
3052
|
+
entry.refs += 1;
|
|
3053
|
+
entry.listeners.add(cb);
|
|
3054
|
+
startTick();
|
|
3055
|
+
return () => {
|
|
3056
|
+
const current = tracks.get(id);
|
|
3057
|
+
if (!current) return;
|
|
3058
|
+
current.listeners.delete(cb);
|
|
3059
|
+
current.refs -= 1;
|
|
3060
|
+
if (current.refs <= 0) {
|
|
3061
|
+
current.source.disconnect();
|
|
3062
|
+
current.analyser.disconnect();
|
|
3063
|
+
tracks.delete(id);
|
|
3064
|
+
}
|
|
3065
|
+
stopTickIfIdle();
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
// src/studio/hooks/use-speaking-glow.ts
|
|
3070
|
+
function clearSpeakingGlow(el) {
|
|
3071
|
+
el.style.removeProperty("--livo-speaking-level");
|
|
3072
|
+
el.removeAttribute("data-speaking");
|
|
3073
|
+
}
|
|
3074
|
+
function useSpeakingGlow({
|
|
3075
|
+
track,
|
|
3076
|
+
enabled,
|
|
3077
|
+
reducedMotion
|
|
3078
|
+
}) {
|
|
3079
|
+
const [el, setEl] = react.useState(null);
|
|
3080
|
+
const gateRef = react.useRef({ speaking: false, quietSince: null });
|
|
3081
|
+
const setRef = react.useCallback((node) => {
|
|
3082
|
+
setEl(node);
|
|
3083
|
+
}, []);
|
|
3084
|
+
react.useEffect(() => {
|
|
3085
|
+
if (!el || !enabled || !track) {
|
|
3086
|
+
if (el) clearSpeakingGlow(el);
|
|
3087
|
+
gateRef.current = { speaking: false, quietSince: null };
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
const unsubscribe = subscribeStudioAudioLevel(track, (level) => {
|
|
3091
|
+
const node = el;
|
|
3092
|
+
gateRef.current = nextSpeakingGate(gateRef.current, level, Date.now());
|
|
3093
|
+
if (gateRef.current.speaking) {
|
|
3094
|
+
node.dataset.speaking = "true";
|
|
3095
|
+
if (!reducedMotion) {
|
|
3096
|
+
node.style.setProperty("--livo-speaking-level", String(level));
|
|
3097
|
+
} else {
|
|
3098
|
+
node.style.removeProperty("--livo-speaking-level");
|
|
3099
|
+
}
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
clearSpeakingGlow(node);
|
|
3103
|
+
});
|
|
3104
|
+
return () => {
|
|
3105
|
+
unsubscribe();
|
|
3106
|
+
clearSpeakingGlow(el);
|
|
3107
|
+
gateRef.current = { speaking: false, quietSince: null };
|
|
3108
|
+
};
|
|
3109
|
+
}, [el, enabled, reducedMotion, track]);
|
|
3110
|
+
return setRef;
|
|
3111
|
+
}
|
|
2931
3112
|
function DropdownMenu({ ...props }) {
|
|
2932
3113
|
return /* @__PURE__ */ jsxRuntime.jsx(menu.Menu.Root, { "data-slot": "dropdown-menu", ...props });
|
|
2933
3114
|
}
|
|
@@ -3076,32 +3257,30 @@ function StudioTile({
|
|
|
3076
3257
|
const onStage = isOnStage(tile.stageStatus);
|
|
3077
3258
|
const [confirm, setConfirm] = react.useState(null);
|
|
3078
3259
|
const showMenu = (canModerate || canManageStage) && !tile.isSelf;
|
|
3260
|
+
const hasVideoTrack = tileHasVideoTrack(meeting, tile);
|
|
3261
|
+
const glowRef = useSpeakingGlow({
|
|
3262
|
+
track: tile.kind === "screen" || !tile.audioEnabled ? null : meeting.participantAudioTrack(tile.id),
|
|
3263
|
+
enabled: onStage,
|
|
3264
|
+
reducedMotion
|
|
3265
|
+
});
|
|
3079
3266
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3080
3267
|
"article",
|
|
3081
3268
|
{
|
|
3269
|
+
ref: glowRef,
|
|
3082
3270
|
"aria-label": tile.name,
|
|
3083
3271
|
className: cn(
|
|
3084
|
-
"bg-muted relative overflow-hidden rounded-xl",
|
|
3272
|
+
"livo-studio-tile bg-muted relative overflow-hidden rounded-xl",
|
|
3085
3273
|
compact ? "aspect-video w-40 shrink-0 md:w-full" : tile.kind === "screen" ? "min-h-0 flex-1 md:min-h-48" : fill ? "size-full min-h-0 md:aspect-video md:h-auto" : "aspect-video"
|
|
3086
3274
|
),
|
|
3087
3275
|
children: [
|
|
3088
|
-
tile.kind === "idle" ? /* @__PURE__ */ jsxRuntime.
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-background flex size-16 items-center justify-center rounded-full text-xl", children: tile.name.slice(0, 1).toUpperCase() }),
|
|
3097
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3098
|
-
AudioWaveform,
|
|
3099
|
-
{
|
|
3100
|
-
track: tile.isSelf ? meeting.localAudioTrack() : null,
|
|
3101
|
-
reducedMotion
|
|
3102
|
-
}
|
|
3103
|
-
)
|
|
3104
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3276
|
+
tile.kind === "idle" || !hasVideoTrack ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3277
|
+
IdlePersonTile,
|
|
3278
|
+
{
|
|
3279
|
+
tile,
|
|
3280
|
+
meeting,
|
|
3281
|
+
reducedMotion
|
|
3282
|
+
}
|
|
3283
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3105
3284
|
StudioTileVideo,
|
|
3106
3285
|
{
|
|
3107
3286
|
meeting,
|
|
@@ -3111,6 +3290,13 @@ function StudioTile({
|
|
|
3111
3290
|
contain: tile.kind === "screen"
|
|
3112
3291
|
}
|
|
3113
3292
|
),
|
|
3293
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3294
|
+
"div",
|
|
3295
|
+
{
|
|
3296
|
+
"aria-hidden": true,
|
|
3297
|
+
className: "livo-studio-tile-glow pointer-events-none absolute inset-0 z-20 rounded-xl"
|
|
3298
|
+
}
|
|
3299
|
+
),
|
|
3114
3300
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-x-0 bottom-0 z-10 flex items-end justify-between gap-2 bg-linear-to-t from-black/70 to-transparent p-2", children: [
|
|
3115
3301
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "min-w-0 truncate text-xs font-medium text-white", children: [
|
|
3116
3302
|
tile.name,
|
|
@@ -3193,6 +3379,37 @@ function TileStatusIcons({
|
|
|
3193
3379
|
tile.chatCanSend === false ? /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { label: labels.muteChat, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MessageSquareOff, {}) }) : null
|
|
3194
3380
|
] });
|
|
3195
3381
|
}
|
|
3382
|
+
function tileHasVideoTrack(meeting, tile) {
|
|
3383
|
+
if (tile.kind === "screen") {
|
|
3384
|
+
return meeting.participantScreenTrack(tile.id) != null;
|
|
3385
|
+
}
|
|
3386
|
+
if (tile.kind === "idle") return false;
|
|
3387
|
+
if (tile.isSelf) return meeting.localVideoTrack() != null;
|
|
3388
|
+
return meeting.participantVideoTrack(tile.id) != null;
|
|
3389
|
+
}
|
|
3390
|
+
function IdlePersonTile({
|
|
3391
|
+
tile,
|
|
3392
|
+
meeting,
|
|
3393
|
+
reducedMotion
|
|
3394
|
+
}) {
|
|
3395
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-3", children: [
|
|
3396
|
+
tile.picture ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3397
|
+
"img",
|
|
3398
|
+
{
|
|
3399
|
+
src: tile.picture,
|
|
3400
|
+
alt: "",
|
|
3401
|
+
className: "size-16 rounded-full object-cover"
|
|
3402
|
+
}
|
|
3403
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-background flex size-16 items-center justify-center rounded-full text-xl", children: tile.name.slice(0, 1).toUpperCase() }),
|
|
3404
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3405
|
+
AudioWaveform,
|
|
3406
|
+
{
|
|
3407
|
+
track: tile.isSelf ? meeting.localAudioTrack() : null,
|
|
3408
|
+
reducedMotion
|
|
3409
|
+
}
|
|
3410
|
+
)
|
|
3411
|
+
] });
|
|
3412
|
+
}
|
|
3196
3413
|
function StatusIcon({
|
|
3197
3414
|
label,
|
|
3198
3415
|
children
|
|
@@ -3216,23 +3433,27 @@ function StudioTileVideo({
|
|
|
3216
3433
|
meeting.attachVideo(participantId, el, kind);
|
|
3217
3434
|
return () => meeting.attachVideo(participantId, null, kind);
|
|
3218
3435
|
}, [kind, meeting, participantId]);
|
|
3219
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
3220
|
-
"
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
"
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3436
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3437
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: STUDIO_VIDEO_MEDIA_STYLE }),
|
|
3438
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3439
|
+
"video",
|
|
3440
|
+
{
|
|
3441
|
+
ref,
|
|
3442
|
+
autoPlay: true,
|
|
3443
|
+
playsInline: true,
|
|
3444
|
+
muted: true,
|
|
3445
|
+
disablePictureInPicture: true,
|
|
3446
|
+
disableRemotePlayback: true,
|
|
3447
|
+
controlsList: "nodownload noplaybackrate noremoteplayback",
|
|
3448
|
+
className: cn(
|
|
3449
|
+
"livo-studio-tile-video absolute inset-0 size-full",
|
|
3450
|
+
"pointer-events-none",
|
|
3451
|
+
contain ? "object-contain" : "object-cover",
|
|
3452
|
+
mirror && "-scale-x-100"
|
|
3453
|
+
)
|
|
3454
|
+
}
|
|
3455
|
+
)
|
|
3456
|
+
] });
|
|
3236
3457
|
}
|
|
3237
3458
|
function StudioStage({
|
|
3238
3459
|
participants,
|
|
@@ -3253,9 +3474,7 @@ function StudioStage({
|
|
|
3253
3474
|
onOpenPeople
|
|
3254
3475
|
}) {
|
|
3255
3476
|
const displayTiles = expandStudioTiles(participants);
|
|
3256
|
-
const stage = studioStageLayout(displayTiles
|
|
3257
|
-
activeSpeakerId: meeting.activeSpeakerId()
|
|
3258
|
-
});
|
|
3477
|
+
const stage = studioStageLayout(displayTiles);
|
|
3259
3478
|
const sharing = stage.spotlight.length > 0;
|
|
3260
3479
|
const overflowLabel = labels.stageOverflow.replace(
|
|
3261
3480
|
"{count}",
|
|
@@ -3326,6 +3545,32 @@ function StudioStage({
|
|
|
3326
3545
|
}
|
|
3327
3546
|
);
|
|
3328
3547
|
}
|
|
3548
|
+
function StudioDownloadAppDialog({
|
|
3549
|
+
open,
|
|
3550
|
+
onOpenChange,
|
|
3551
|
+
downloadUrl,
|
|
3552
|
+
labels
|
|
3553
|
+
}) {
|
|
3554
|
+
const href = downloadUrl?.trim() ?? "";
|
|
3555
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(DialogContent, { children: [
|
|
3556
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DialogHeader, { children: [
|
|
3557
|
+
/* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: labels.screenDownloadAppTitle }),
|
|
3558
|
+
/* @__PURE__ */ jsxRuntime.jsx(DialogDescription, { children: labels.screenUnsupportedDevice })
|
|
3559
|
+
] }),
|
|
3560
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DialogFooter, { children: [
|
|
3561
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3562
|
+
Button,
|
|
3563
|
+
{
|
|
3564
|
+
type: "button",
|
|
3565
|
+
variant: "outline",
|
|
3566
|
+
onClick: () => onOpenChange(false),
|
|
3567
|
+
children: labels.screenDownloadAppDismiss
|
|
3568
|
+
}
|
|
3569
|
+
),
|
|
3570
|
+
href ? /* @__PURE__ */ jsxRuntime.jsx(Button, { render: /* @__PURE__ */ jsxRuntime.jsx("a", { href, target: "_blank", rel: "noreferrer" }), children: labels.screenDownloadAppAction }) : /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", disabled: true, children: labels.screenDownloadAppAction })
|
|
3571
|
+
] })
|
|
3572
|
+
] }) });
|
|
3573
|
+
}
|
|
3329
3574
|
function StudioToolbar({
|
|
3330
3575
|
session,
|
|
3331
3576
|
meeting,
|
|
@@ -3580,7 +3825,8 @@ function StudioRoom({
|
|
|
3580
3825
|
windowGate,
|
|
3581
3826
|
initialCameraOn = true,
|
|
3582
3827
|
initialMuted = false,
|
|
3583
|
-
initialDevices
|
|
3828
|
+
initialDevices,
|
|
3829
|
+
appDownloadUrl
|
|
3584
3830
|
}) {
|
|
3585
3831
|
const [cameraOn, setCameraOn] = react.useState(initialCameraOn);
|
|
3586
3832
|
const [screenOn, setScreenOn] = react.useState(false);
|
|
@@ -3588,7 +3834,7 @@ function StudioRoom({
|
|
|
3588
3834
|
const [screenRequestPending, setScreenRequestPending] = react.useState(false);
|
|
3589
3835
|
const [pending, setPending] = react.useState(false);
|
|
3590
3836
|
const [publishing, setPublishing] = react.useState(false);
|
|
3591
|
-
const [
|
|
3837
|
+
const [tick2, setTick] = react.useState(0);
|
|
3592
3838
|
const [leaveState, setLeaveState] = react.useState(null);
|
|
3593
3839
|
const [sideTab, setSideTab] = react.useState(null);
|
|
3594
3840
|
const [settingsOpen, setSettingsOpen] = react.useState(false);
|
|
@@ -3601,6 +3847,7 @@ function StudioRoom({
|
|
|
3601
3847
|
);
|
|
3602
3848
|
const [egressError, setEgressError] = react.useState(null);
|
|
3603
3849
|
const [shareGrant, setShareGrant] = react.useState("idle");
|
|
3850
|
+
const [downloadAppOpen, setDownloadAppOpen] = react.useState(false);
|
|
3604
3851
|
const [connectTimedOut, setConnectTimedOut] = react.useState(false);
|
|
3605
3852
|
const [screenPermission, setScreenPermission] = react.useState(
|
|
3606
3853
|
() => meeting.screenSharePermission()
|
|
@@ -3642,6 +3889,11 @@ function StudioRoom({
|
|
|
3642
3889
|
const roomState = leaveState ?? meeting.roomState();
|
|
3643
3890
|
const sessionOver = isStudioSessionOver(session.stream.status, roomState);
|
|
3644
3891
|
const rejected = isStudioRejected(roomState);
|
|
3892
|
+
const connectionState = react.useMemo(
|
|
3893
|
+
() => meeting.connectionState(),
|
|
3894
|
+
[meeting, tick2]
|
|
3895
|
+
);
|
|
3896
|
+
const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
|
|
3645
3897
|
useBeforeUnload(active && !sessionOver);
|
|
3646
3898
|
useWakeLock(active && !sessionOver);
|
|
3647
3899
|
react.useEffect(() => {
|
|
@@ -3805,12 +4057,13 @@ function StudioRoom({
|
|
|
3805
4057
|
void meeting.setScreenShareEnabled(false);
|
|
3806
4058
|
}, [guestOffStage, meeting]);
|
|
3807
4059
|
react.useEffect(() => {
|
|
3808
|
-
if (!sessionOver && !rejected) return;
|
|
4060
|
+
if (!sessionOver && !rejected && !guestConnectFailed) return;
|
|
4061
|
+
pip.close();
|
|
3809
4062
|
setShareGrant("idle");
|
|
3810
4063
|
void meeting.setAudioEnabled(false);
|
|
3811
4064
|
void meeting.setVideoEnabled(false);
|
|
3812
4065
|
void meeting.setScreenShareEnabled(false);
|
|
3813
|
-
}, [meeting, rejected, sessionOver]);
|
|
4066
|
+
}, [guestConnectFailed, meeting, pip.close, rejected, sessionOver]);
|
|
3814
4067
|
react.useEffect(() => {
|
|
3815
4068
|
return meeting.subscribeMedia(() => {
|
|
3816
4069
|
const self2 = meeting.participants().find((row) => row.isSelf);
|
|
@@ -3832,7 +4085,7 @@ function StudioRoom({
|
|
|
3832
4085
|
});
|
|
3833
4086
|
}, [meeting]);
|
|
3834
4087
|
const sawParticipants = react.useRef(false);
|
|
3835
|
-
const participants = react.useMemo(() => meeting.participants(), [meeting,
|
|
4088
|
+
const participants = react.useMemo(() => meeting.participants(), [meeting, tick2]);
|
|
3836
4089
|
react.useEffect(() => {
|
|
3837
4090
|
const other = duplicateSelfParticipant(participants);
|
|
3838
4091
|
if (!other) return;
|
|
@@ -3847,29 +4100,25 @@ function StudioRoom({
|
|
|
3847
4100
|
() => participants.filter((row) => isOnStage(row.stageStatus)),
|
|
3848
4101
|
[participants]
|
|
3849
4102
|
);
|
|
3850
|
-
const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting,
|
|
3851
|
-
const connectionState = react.useMemo(
|
|
3852
|
-
() => meeting.connectionState(),
|
|
3853
|
-
[meeting, tick]
|
|
3854
|
-
);
|
|
4103
|
+
const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting, tick2]);
|
|
3855
4104
|
const lastMeetingError = react.useMemo(
|
|
3856
4105
|
() => meeting.lastMeetingError(),
|
|
3857
|
-
[meeting,
|
|
4106
|
+
[meeting, tick2]
|
|
3858
4107
|
);
|
|
3859
4108
|
const canManageStage = meeting.canManageStage();
|
|
3860
4109
|
const stageEnabled = meeting.stageEnabled();
|
|
3861
4110
|
const stageRequests = react.useMemo(
|
|
3862
4111
|
() => canManageStage ? meeting.stageRequests() : [],
|
|
3863
|
-
[canManageStage, meeting,
|
|
4112
|
+
[canManageStage, meeting, tick2]
|
|
3864
4113
|
);
|
|
3865
|
-
const chatMessages = react.useMemo(() => meeting.chatMessages(), [meeting,
|
|
4114
|
+
const chatMessages = react.useMemo(() => meeting.chatMessages(), [meeting, tick2]);
|
|
3866
4115
|
const inWaitingRoom = meeting.inWaitingRoom();
|
|
3867
4116
|
const screenAvailable = canStartScreenShare(participants);
|
|
3868
4117
|
const durableScreenBlock = screenShareBlock(
|
|
3869
4118
|
captureSupported,
|
|
3870
4119
|
screenPermission
|
|
3871
4120
|
);
|
|
3872
|
-
const showScreenControl = durableScreenBlock
|
|
4121
|
+
const showScreenControl = showScreenShareControl(durableScreenBlock);
|
|
3873
4122
|
const canGoLive = session.role === "moderator" && canPublish(session.stream.status);
|
|
3874
4123
|
const showPublishButton = session.role === "moderator" && !onAir && session.stream.status !== "ended" && session.stream.status !== "error";
|
|
3875
4124
|
const self = participants.find((row) => row.isSelf);
|
|
@@ -3965,17 +4214,10 @@ function StudioRoom({
|
|
|
3965
4214
|
}, [connectionState, session.role]);
|
|
3966
4215
|
react.useEffect(() => {
|
|
3967
4216
|
if (inWaitingRoom || screenNoticePrimed.current) return;
|
|
3968
|
-
if (
|
|
4217
|
+
if (durableScreenBlock !== "permission") return;
|
|
3969
4218
|
screenNoticePrimed.current = true;
|
|
3970
|
-
sonner.toast.message(
|
|
3971
|
-
|
|
3972
|
-
);
|
|
3973
|
-
}, [
|
|
3974
|
-
durableScreenBlock,
|
|
3975
|
-
inWaitingRoom,
|
|
3976
|
-
labels.screenNotAllowed,
|
|
3977
|
-
labels.screenUnsupportedDevice
|
|
3978
|
-
]);
|
|
4219
|
+
sonner.toast.message(labels.screenNotAllowed);
|
|
4220
|
+
}, [durableScreenBlock, inWaitingRoom, labels.screenNotAllowed]);
|
|
3979
4221
|
react.useEffect(() => {
|
|
3980
4222
|
return meeting.subscribeBroadcast((message) => {
|
|
3981
4223
|
if (message.type === "host-media") {
|
|
@@ -4084,6 +4326,10 @@ function StudioRoom({
|
|
|
4084
4326
|
setCameraOn((current) => !current);
|
|
4085
4327
|
}, []);
|
|
4086
4328
|
const onToggleScreen = react.useCallback(() => {
|
|
4329
|
+
if (durableScreenBlock === "device") {
|
|
4330
|
+
setDownloadAppOpen(true);
|
|
4331
|
+
return;
|
|
4332
|
+
}
|
|
4087
4333
|
if (!canShareFreely || screenRequestPending) return;
|
|
4088
4334
|
if (!screenOn && !canStartScreenShare(meeting.participants())) {
|
|
4089
4335
|
sonner.toast.message(labels.screenBusy);
|
|
@@ -4093,12 +4339,17 @@ function StudioRoom({
|
|
|
4093
4339
|
setScreenOn((current) => !current);
|
|
4094
4340
|
}, [
|
|
4095
4341
|
canShareFreely,
|
|
4342
|
+
durableScreenBlock,
|
|
4096
4343
|
labels.screenBusy,
|
|
4097
4344
|
meeting,
|
|
4098
4345
|
screenOn,
|
|
4099
4346
|
screenRequestPending
|
|
4100
4347
|
]);
|
|
4101
4348
|
const onAskToShare = react.useCallback(async () => {
|
|
4349
|
+
if (durableScreenBlock === "device") {
|
|
4350
|
+
setDownloadAppOpen(true);
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4102
4353
|
const local = meeting.participants().find((row) => row.isSelf);
|
|
4103
4354
|
if (!local) return;
|
|
4104
4355
|
try {
|
|
@@ -4115,7 +4366,12 @@ function StudioRoom({
|
|
|
4115
4366
|
} catch {
|
|
4116
4367
|
sonner.toast.error(labels.shareRequestError);
|
|
4117
4368
|
}
|
|
4118
|
-
}, [
|
|
4369
|
+
}, [
|
|
4370
|
+
durableScreenBlock,
|
|
4371
|
+
labels.shareRequestError,
|
|
4372
|
+
labels.shareRequestSent,
|
|
4373
|
+
meeting
|
|
4374
|
+
]);
|
|
4119
4375
|
const verifyGrantTookEffect = react.useCallback(
|
|
4120
4376
|
(userId, retried = false) => {
|
|
4121
4377
|
const id = window.setTimeout(() => {
|
|
@@ -4278,7 +4534,6 @@ function StudioRoom({
|
|
|
4278
4534
|
}
|
|
4279
4535
|
) });
|
|
4280
4536
|
}
|
|
4281
|
-
const guestConnectFailed = session.role === "guest" && (connectionState === "failed" || connectionState === "connecting" && connectTimedOut);
|
|
4282
4537
|
if (guestConnectFailed) {
|
|
4283
4538
|
const detail = [meeting.roomState(), lastMeetingError].filter((value) => Boolean(value)).join(" \u2014 ");
|
|
4284
4539
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-dvh flex-col items-center justify-center p-6", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4612,7 +4867,21 @@ function StudioRoom({
|
|
|
4612
4867
|
}
|
|
4613
4868
|
),
|
|
4614
4869
|
pip.pipWindow.document.body
|
|
4615
|
-
) : null
|
|
4870
|
+
) : null,
|
|
4871
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4872
|
+
StudioDownloadAppDialog,
|
|
4873
|
+
{
|
|
4874
|
+
open: downloadAppOpen,
|
|
4875
|
+
onOpenChange: setDownloadAppOpen,
|
|
4876
|
+
downloadUrl: appDownloadUrl,
|
|
4877
|
+
labels: {
|
|
4878
|
+
screenDownloadAppTitle: labels.screenDownloadAppTitle,
|
|
4879
|
+
screenUnsupportedDevice: labels.screenUnsupportedDevice,
|
|
4880
|
+
screenDownloadAppAction: labels.screenDownloadAppAction,
|
|
4881
|
+
screenDownloadAppDismiss: labels.screenDownloadAppDismiss
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
)
|
|
4616
4885
|
] });
|
|
4617
4886
|
}
|
|
4618
4887
|
function isOverconstrained(error) {
|
|
@@ -5781,6 +6050,9 @@ function studioMeetingFromRtk(raw) {
|
|
|
5781
6050
|
localVideoTrack() {
|
|
5782
6051
|
return meeting.self?.videoTrack ?? null;
|
|
5783
6052
|
},
|
|
6053
|
+
participantAudioTrack(participantId) {
|
|
6054
|
+
return participantFor(participantId)?.audioTrack ?? null;
|
|
6055
|
+
},
|
|
5784
6056
|
participantVideoTrack(participantId) {
|
|
5785
6057
|
return participantFor(participantId)?.videoTrack ?? null;
|
|
5786
6058
|
},
|
|
@@ -6152,7 +6424,8 @@ function LivoHostStudio({
|
|
|
6152
6424
|
embed = false,
|
|
6153
6425
|
chrome,
|
|
6154
6426
|
playerOrigin,
|
|
6155
|
-
theme
|
|
6427
|
+
theme,
|
|
6428
|
+
appDownloadUrl
|
|
6156
6429
|
}) {
|
|
6157
6430
|
const reducedMotion = usePrefersReducedMotion();
|
|
6158
6431
|
const [session, setSession] = react.useState(null);
|
|
@@ -6232,7 +6505,8 @@ function LivoHostStudio({
|
|
|
6232
6505
|
labels,
|
|
6233
6506
|
embed,
|
|
6234
6507
|
chrome,
|
|
6235
|
-
playerOrigin
|
|
6508
|
+
playerOrigin,
|
|
6509
|
+
appDownloadUrl
|
|
6236
6510
|
}
|
|
6237
6511
|
);
|
|
6238
6512
|
}
|
|
@@ -6245,7 +6519,8 @@ function LivoGuestStudio({
|
|
|
6245
6519
|
lobbyLabels,
|
|
6246
6520
|
embed = false,
|
|
6247
6521
|
playerOrigin,
|
|
6248
|
-
theme
|
|
6522
|
+
theme,
|
|
6523
|
+
appDownloadUrl
|
|
6249
6524
|
}) {
|
|
6250
6525
|
const reducedMotion = usePrefersReducedMotion();
|
|
6251
6526
|
const [session, setSession] = react.useState(null);
|
|
@@ -6431,7 +6706,8 @@ function LivoGuestStudio({
|
|
|
6431
6706
|
playerOrigin,
|
|
6432
6707
|
initialCameraOn: cameraOn,
|
|
6433
6708
|
initialMuted: muted,
|
|
6434
|
-
initialDevices: deviceSelection
|
|
6709
|
+
initialDevices: deviceSelection,
|
|
6710
|
+
appDownloadUrl
|
|
6435
6711
|
}
|
|
6436
6712
|
);
|
|
6437
6713
|
}
|
|
@@ -6536,6 +6812,9 @@ function studioRoomLabels(t) {
|
|
|
6536
6812
|
pipTitle: t("pipTitle"),
|
|
6537
6813
|
pipError: t("pipError"),
|
|
6538
6814
|
screenUnsupportedDevice: t("screenUnsupportedDevice"),
|
|
6815
|
+
screenDownloadAppTitle: t("screenDownloadAppTitle"),
|
|
6816
|
+
screenDownloadAppAction: t("screenDownloadAppAction"),
|
|
6817
|
+
screenDownloadAppDismiss: t("screenDownloadAppDismiss"),
|
|
6539
6818
|
screenNotAllowed: t("screenNotAllowed"),
|
|
6540
6819
|
askToShare: t("askToShare"),
|
|
6541
6820
|
shareRequested: t("shareRequested"),
|