@livo-tv/sdk 1.2.1 → 1.2.3
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 +306 -71
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.cts +4 -1
- package/dist/studio/index.d.ts +4 -1
- package/dist/studio/index.js +306 -72
- package/dist/studio/index.js.map +1 -1
- package/dist/styles.css +6 -0
- package/package.json +2 -2
package/dist/studio/index.cjs
CHANGED
|
@@ -764,6 +764,12 @@ function parseStudioJoinStatus(value) {
|
|
|
764
764
|
if (value.streamStatus !== null && streamStatus === null) return null;
|
|
765
765
|
return { ready: value.ready, streamStatus };
|
|
766
766
|
}
|
|
767
|
+
function parsePublicStreamStatus(value) {
|
|
768
|
+
if (!isRecord(value)) return null;
|
|
769
|
+
const nested = isRecord(value.stream) ? value.stream : value;
|
|
770
|
+
const status = readString(nested, "status");
|
|
771
|
+
return status;
|
|
772
|
+
}
|
|
767
773
|
function studioErrorCode(error) {
|
|
768
774
|
if (!error || typeof error !== "object") return null;
|
|
769
775
|
const body = "body" in error ? error.body : null;
|
|
@@ -771,6 +777,90 @@ function studioErrorCode(error) {
|
|
|
771
777
|
return body.error;
|
|
772
778
|
}
|
|
773
779
|
|
|
780
|
+
// src/studio/lib/studio-sounds.ts
|
|
781
|
+
var CHAT_DEBOUNCE_MS = 400;
|
|
782
|
+
var CHAT_HZ = 880;
|
|
783
|
+
var CHAT_MS = 80;
|
|
784
|
+
var REQUEST_LOW_HZ = 660;
|
|
785
|
+
var REQUEST_HIGH_HZ = 880;
|
|
786
|
+
var REQUEST_NOTE_MS = 80;
|
|
787
|
+
var GAIN = 0.1;
|
|
788
|
+
var ctx = null;
|
|
789
|
+
var lastChatAt = 0;
|
|
790
|
+
function audioContextCtor() {
|
|
791
|
+
if (typeof window === "undefined") return null;
|
|
792
|
+
return window.AudioContext || window.webkitAudioContext || null;
|
|
793
|
+
}
|
|
794
|
+
function prefersReducedMotion() {
|
|
795
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
799
|
+
}
|
|
800
|
+
function ensureContext() {
|
|
801
|
+
const AudioCtx = audioContextCtor();
|
|
802
|
+
if (!AudioCtx) return null;
|
|
803
|
+
if (!ctx || ctx.state === "closed") {
|
|
804
|
+
ctx = new AudioCtx();
|
|
805
|
+
}
|
|
806
|
+
return ctx;
|
|
807
|
+
}
|
|
808
|
+
function unlockStudioSounds() {
|
|
809
|
+
const context = ensureContext();
|
|
810
|
+
if (!context) return;
|
|
811
|
+
void context.resume();
|
|
812
|
+
}
|
|
813
|
+
function playTone(context, frequency, startAt, durationSec) {
|
|
814
|
+
const oscillator = context.createOscillator();
|
|
815
|
+
const gain = context.createGain();
|
|
816
|
+
oscillator.type = "sine";
|
|
817
|
+
oscillator.frequency.value = frequency;
|
|
818
|
+
gain.gain.setValueAtTime(GAIN, startAt);
|
|
819
|
+
gain.gain.exponentialRampToValueAtTime(1e-3, startAt + durationSec);
|
|
820
|
+
oscillator.connect(gain);
|
|
821
|
+
gain.connect(context.destination);
|
|
822
|
+
oscillator.start(startAt);
|
|
823
|
+
oscillator.stop(startAt + durationSec);
|
|
824
|
+
}
|
|
825
|
+
function playNow(context, kind) {
|
|
826
|
+
const now = context.currentTime;
|
|
827
|
+
if (kind === "chat") {
|
|
828
|
+
playTone(context, CHAT_HZ, now, CHAT_MS / 1e3);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
playTone(context, REQUEST_LOW_HZ, now, REQUEST_NOTE_MS / 1e3);
|
|
832
|
+
playTone(
|
|
833
|
+
context,
|
|
834
|
+
REQUEST_HIGH_HZ,
|
|
835
|
+
now + REQUEST_NOTE_MS / 1e3,
|
|
836
|
+
REQUEST_NOTE_MS / 1e3
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
function playStudioSound(kind) {
|
|
840
|
+
if (prefersReducedMotion()) return;
|
|
841
|
+
if (kind === "chat") {
|
|
842
|
+
const at = Date.now();
|
|
843
|
+
if (at - lastChatAt < CHAT_DEBOUNCE_MS) return;
|
|
844
|
+
lastChatAt = at;
|
|
845
|
+
}
|
|
846
|
+
const context = ensureContext();
|
|
847
|
+
if (!context) return;
|
|
848
|
+
const run = () => {
|
|
849
|
+
if (context.state !== "running") return;
|
|
850
|
+
playNow(context, kind);
|
|
851
|
+
};
|
|
852
|
+
if (context.state === "running") {
|
|
853
|
+
run();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
void context.resume().then(run, () => void 0);
|
|
857
|
+
}
|
|
858
|
+
if (typeof document !== "undefined") {
|
|
859
|
+
document.addEventListener("pointerdown", unlockStudioSounds, {
|
|
860
|
+
passive: true
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
774
864
|
// src/studio/lib/bridge.ts
|
|
775
865
|
function postLivoEvent(type, payload) {
|
|
776
866
|
if (typeof window === "undefined") return;
|
|
@@ -1184,9 +1274,9 @@ function AudioWaveform({
|
|
|
1184
1274
|
if (!canvas || !track || reducedMotion) return;
|
|
1185
1275
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
1186
1276
|
if (!AudioCtx) return;
|
|
1187
|
-
const
|
|
1188
|
-
const source =
|
|
1189
|
-
const analyser =
|
|
1277
|
+
const ctx2 = new AudioCtx();
|
|
1278
|
+
const source = ctx2.createMediaStreamSource(new MediaStream([track]));
|
|
1279
|
+
const analyser = ctx2.createAnalyser();
|
|
1190
1280
|
analyser.fftSize = 64;
|
|
1191
1281
|
source.connect(analyser);
|
|
1192
1282
|
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
@@ -1215,7 +1305,7 @@ function AudioWaveform({
|
|
|
1215
1305
|
frame = requestAnimationFrame(draw);
|
|
1216
1306
|
return () => {
|
|
1217
1307
|
cancelAnimationFrame(frame);
|
|
1218
|
-
void
|
|
1308
|
+
void ctx2.close();
|
|
1219
1309
|
};
|
|
1220
1310
|
}, [track, reducedMotion]);
|
|
1221
1311
|
if (reducedMotion) {
|
|
@@ -1501,10 +1591,10 @@ async function playSpeakerTest(deviceId) {
|
|
|
1501
1591
|
if (!AudioCtx) {
|
|
1502
1592
|
throw new Error("AudioContext unavailable");
|
|
1503
1593
|
}
|
|
1504
|
-
const
|
|
1505
|
-
void
|
|
1506
|
-
const oscillator =
|
|
1507
|
-
const gain =
|
|
1594
|
+
const ctx2 = new AudioCtx();
|
|
1595
|
+
void ctx2.resume();
|
|
1596
|
+
const oscillator = ctx2.createOscillator();
|
|
1597
|
+
const gain = ctx2.createGain();
|
|
1508
1598
|
oscillator.type = "sine";
|
|
1509
1599
|
oscillator.frequency.value = TEST_HZ;
|
|
1510
1600
|
gain.gain.value = 0.18;
|
|
@@ -1514,7 +1604,7 @@ async function playSpeakerTest(deviceId) {
|
|
|
1514
1604
|
const probe = document.createElement("audio");
|
|
1515
1605
|
const canRoute = Boolean(deviceId) && typeof probe.setSinkId === "function";
|
|
1516
1606
|
if (canRoute && deviceId) {
|
|
1517
|
-
const destination =
|
|
1607
|
+
const destination = ctx2.createMediaStreamDestination();
|
|
1518
1608
|
gain.connect(destination);
|
|
1519
1609
|
audio = probe;
|
|
1520
1610
|
audio.autoplay = true;
|
|
@@ -1525,13 +1615,13 @@ async function playSpeakerTest(deviceId) {
|
|
|
1525
1615
|
await audio.play();
|
|
1526
1616
|
} catch {
|
|
1527
1617
|
gain.disconnect();
|
|
1528
|
-
gain.connect(
|
|
1618
|
+
gain.connect(ctx2.destination);
|
|
1529
1619
|
audio.srcObject = null;
|
|
1530
1620
|
audio.remove();
|
|
1531
1621
|
audio = null;
|
|
1532
1622
|
}
|
|
1533
1623
|
} else {
|
|
1534
|
-
gain.connect(
|
|
1624
|
+
gain.connect(ctx2.destination);
|
|
1535
1625
|
}
|
|
1536
1626
|
oscillator.start();
|
|
1537
1627
|
await new Promise((resolve) => {
|
|
@@ -1543,7 +1633,7 @@ async function playSpeakerTest(deviceId) {
|
|
|
1543
1633
|
audio.srcObject = null;
|
|
1544
1634
|
audio.remove();
|
|
1545
1635
|
}
|
|
1546
|
-
await
|
|
1636
|
+
await ctx2.close();
|
|
1547
1637
|
}
|
|
1548
1638
|
}
|
|
1549
1639
|
function StudioDeviceSelectGroup({
|
|
@@ -1778,6 +1868,7 @@ function SettingsCameraPreview({
|
|
|
1778
1868
|
el.srcObject = null;
|
|
1779
1869
|
return;
|
|
1780
1870
|
}
|
|
1871
|
+
el.setAttribute("webkit-playsinline", "");
|
|
1781
1872
|
el.srcObject = new MediaStream([track]);
|
|
1782
1873
|
void el.play().catch(() => void 0);
|
|
1783
1874
|
return () => {
|
|
@@ -1797,7 +1888,7 @@ function SettingsCameraPreview({
|
|
|
1797
1888
|
autoPlay: true,
|
|
1798
1889
|
muted: true,
|
|
1799
1890
|
playsInline: true,
|
|
1800
|
-
className: "absolute inset-0 size-full -scale-x-100 object-cover"
|
|
1891
|
+
className: "livo-studio-inline-video absolute inset-0 size-full -scale-x-100 object-cover"
|
|
1801
1892
|
}
|
|
1802
1893
|
) });
|
|
1803
1894
|
}
|
|
@@ -1896,6 +1987,7 @@ function WaitingCameraPreview({
|
|
|
1896
1987
|
el.srcObject = null;
|
|
1897
1988
|
return;
|
|
1898
1989
|
}
|
|
1990
|
+
el.setAttribute("webkit-playsinline", "");
|
|
1899
1991
|
el.srcObject = new MediaStream([track]);
|
|
1900
1992
|
void el.play().catch(() => void 0);
|
|
1901
1993
|
return () => {
|
|
@@ -1915,7 +2007,7 @@ function WaitingCameraPreview({
|
|
|
1915
2007
|
autoPlay: true,
|
|
1916
2008
|
muted: true,
|
|
1917
2009
|
playsInline: true,
|
|
1918
|
-
className: "absolute inset-0 size-full -scale-x-100 object-cover"
|
|
2010
|
+
className: "livo-studio-inline-video absolute inset-0 size-full -scale-x-100 object-cover"
|
|
1919
2011
|
}
|
|
1920
2012
|
) });
|
|
1921
2013
|
}
|
|
@@ -2102,6 +2194,7 @@ function PipTrackVideo({
|
|
|
2102
2194
|
el.srcObject = null;
|
|
2103
2195
|
return;
|
|
2104
2196
|
}
|
|
2197
|
+
el.setAttribute("webkit-playsinline", "");
|
|
2105
2198
|
el.srcObject = new MediaStream([track]);
|
|
2106
2199
|
void el.play().catch(() => void 0);
|
|
2107
2200
|
return () => {
|
|
@@ -2116,7 +2209,7 @@ function PipTrackVideo({
|
|
|
2116
2209
|
playsInline: true,
|
|
2117
2210
|
muted: true,
|
|
2118
2211
|
className: cn(
|
|
2119
|
-
"absolute inset-0 size-full",
|
|
2212
|
+
"livo-studio-inline-video absolute inset-0 size-full",
|
|
2120
2213
|
fit === "contain" ? "object-contain" : "object-cover"
|
|
2121
2214
|
)
|
|
2122
2215
|
}
|
|
@@ -2206,13 +2299,13 @@ function BubbleGroup({ className, ...props }) {
|
|
|
2206
2299
|
"div",
|
|
2207
2300
|
{
|
|
2208
2301
|
"data-slot": "bubble-group",
|
|
2209
|
-
className: cn("flex min-w-
|
|
2302
|
+
className: cn("flex min-w-min flex-col gap-2", className),
|
|
2210
2303
|
...props
|
|
2211
2304
|
}
|
|
2212
2305
|
);
|
|
2213
2306
|
}
|
|
2214
2307
|
var bubbleVariants = classVarianceAuthority.cva(
|
|
2215
|
-
"group/bubble relative flex w-
|
|
2308
|
+
"group/bubble relative flex w-fit min-w-min max-w-full flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",
|
|
2216
2309
|
{
|
|
2217
2310
|
variants: {
|
|
2218
2311
|
variant: {
|
|
@@ -2257,7 +2350,7 @@ function BubbleContent({
|
|
|
2257
2350
|
props: mergeProps.mergeProps(
|
|
2258
2351
|
{
|
|
2259
2352
|
className: cn(
|
|
2260
|
-
"w-
|
|
2353
|
+
"w-auto max-w-full whitespace-pre-wrap rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed [overflow-wrap:break-word] group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50",
|
|
2261
2354
|
className
|
|
2262
2355
|
)
|
|
2263
2356
|
},
|
|
@@ -2438,17 +2531,24 @@ function StudioChatPanel({
|
|
|
2438
2531
|
}
|
|
2439
2532
|
),
|
|
2440
2533
|
groups.map((group) => {
|
|
2441
|
-
const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2534
|
+
const bubbles = group.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2535
|
+
"div",
|
|
2536
|
+
{
|
|
2537
|
+
className: group.mine ? "ml-auto flex max-w-[80%] flex-col gap-1 self-end" : "flex max-w-[80%] flex-col gap-1 self-start",
|
|
2538
|
+
children: [
|
|
2539
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2540
|
+
Bubble,
|
|
2541
|
+
{
|
|
2542
|
+
align: group.mine ? "end" : "start",
|
|
2543
|
+
variant: group.mine ? "default" : "secondary",
|
|
2544
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(BubbleContent, { children: row.text })
|
|
2545
|
+
}
|
|
2546
|
+
),
|
|
2547
|
+
messageActions(row, group.mine)
|
|
2548
|
+
]
|
|
2549
|
+
},
|
|
2550
|
+
row.id
|
|
2551
|
+
));
|
|
2452
2552
|
if (group.mine) {
|
|
2453
2553
|
return /* @__PURE__ */ jsxRuntime.jsx(BubbleGroup, { className: "items-end", children: bubbles }, group.rows[0].id);
|
|
2454
2554
|
}
|
|
@@ -3060,7 +3160,9 @@ function StudioTileVideo({
|
|
|
3060
3160
|
}) {
|
|
3061
3161
|
const ref = react.useRef(null);
|
|
3062
3162
|
react.useEffect(() => {
|
|
3063
|
-
|
|
3163
|
+
const el = ref.current;
|
|
3164
|
+
el?.setAttribute("webkit-playsinline", "");
|
|
3165
|
+
meeting.attachVideo(participantId, el, kind);
|
|
3064
3166
|
return () => meeting.attachVideo(participantId, null, kind);
|
|
3065
3167
|
}, [kind, meeting, participantId]);
|
|
3066
3168
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3073,7 +3175,7 @@ function StudioTileVideo({
|
|
|
3073
3175
|
disablePictureInPicture: true,
|
|
3074
3176
|
controlsList: "nodownload noplaybackrate noremoteplayback",
|
|
3075
3177
|
className: cn(
|
|
3076
|
-
"absolute inset-0 size-full",
|
|
3178
|
+
"livo-studio-tile-video absolute inset-0 size-full",
|
|
3077
3179
|
"pointer-events-none",
|
|
3078
3180
|
contain ? "object-contain" : "object-cover",
|
|
3079
3181
|
mirror && "-scale-x-100"
|
|
@@ -3455,6 +3557,12 @@ function StudioRoom({
|
|
|
3455
3557
|
const seenWaitlisted = react.useRef(/* @__PURE__ */ new Set());
|
|
3456
3558
|
const knockPrimed = react.useRef(false);
|
|
3457
3559
|
const peopleAutoOpened = react.useRef(false);
|
|
3560
|
+
const seenChatIds = react.useRef(/* @__PURE__ */ new Set());
|
|
3561
|
+
const chatSoundPrimed = react.useRef(false);
|
|
3562
|
+
const seenStageRequests = react.useRef(/* @__PURE__ */ new Set());
|
|
3563
|
+
const stageRequestPrimed = react.useRef(false);
|
|
3564
|
+
const awaitingStageDecision = react.useRef(false);
|
|
3565
|
+
const prevStageStatus = react.useRef(null);
|
|
3458
3566
|
const grantCheckTimers = react.useRef([]);
|
|
3459
3567
|
const screenNoticePrimed = react.useRef(false);
|
|
3460
3568
|
const lastSelfMedia = react.useRef({
|
|
@@ -3483,6 +3591,9 @@ function StudioRoom({
|
|
|
3483
3591
|
const rejected = isStudioRejected(roomState);
|
|
3484
3592
|
useBeforeUnload(active && !sessionOver);
|
|
3485
3593
|
useWakeLock(active && !sessionOver);
|
|
3594
|
+
react.useEffect(() => {
|
|
3595
|
+
unlockStudioSounds();
|
|
3596
|
+
}, []);
|
|
3486
3597
|
react.useEffect(() => {
|
|
3487
3598
|
if (onAir) {
|
|
3488
3599
|
setPublishing(false);
|
|
@@ -3708,9 +3819,57 @@ function StudioRoom({
|
|
|
3708
3819
|
const incoming = chatMessages.filter((row) => row.userId !== selfUserId);
|
|
3709
3820
|
const unread = sideTab === "chat" ? 0 : Math.max(0, incoming.length - seenIncoming);
|
|
3710
3821
|
const peopleBadge = waitlisted.length + stageRequests.length;
|
|
3822
|
+
const selfStageStatus = self?.stageStatus ?? meeting.stageStatus();
|
|
3711
3823
|
react.useEffect(() => {
|
|
3712
3824
|
if (sideTab === "chat") setSeenIncoming(incoming.length);
|
|
3713
3825
|
}, [incoming.length, sideTab]);
|
|
3826
|
+
react.useEffect(() => {
|
|
3827
|
+
if (!chatSoundPrimed.current) {
|
|
3828
|
+
for (const row of incoming) seenChatIds.current.add(row.id);
|
|
3829
|
+
chatSoundPrimed.current = true;
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
for (const row of incoming) {
|
|
3833
|
+
if (seenChatIds.current.has(row.id)) continue;
|
|
3834
|
+
seenChatIds.current.add(row.id);
|
|
3835
|
+
playStudioSound("chat");
|
|
3836
|
+
}
|
|
3837
|
+
}, [incoming]);
|
|
3838
|
+
react.useEffect(() => {
|
|
3839
|
+
if (session.role !== "moderator") return;
|
|
3840
|
+
if (!stageRequestPrimed.current) {
|
|
3841
|
+
for (const row of stageRequests) {
|
|
3842
|
+
seenStageRequests.current.add(row.userId);
|
|
3843
|
+
}
|
|
3844
|
+
stageRequestPrimed.current = true;
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
for (const row of stageRequests) {
|
|
3848
|
+
if (seenStageRequests.current.has(row.userId)) continue;
|
|
3849
|
+
seenStageRequests.current.add(row.userId);
|
|
3850
|
+
playStudioSound("request");
|
|
3851
|
+
}
|
|
3852
|
+
}, [session.role, stageRequests]);
|
|
3853
|
+
react.useEffect(() => {
|
|
3854
|
+
const next = selfStageStatus;
|
|
3855
|
+
const prev = prevStageStatus.current;
|
|
3856
|
+
prevStageStatus.current = next;
|
|
3857
|
+
if (session.role !== "guest") return;
|
|
3858
|
+
if (next === "REQUESTED_TO_JOIN_STAGE") {
|
|
3859
|
+
awaitingStageDecision.current = true;
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
if (!awaitingStageDecision.current) return;
|
|
3863
|
+
if (next === "ACCEPTED_TO_JOIN_STAGE" || next === "ON_STAGE") {
|
|
3864
|
+
awaitingStageDecision.current = false;
|
|
3865
|
+
sonner.toast.success(labels.stageApproved);
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (next === "OFF_STAGE" && prev === "REQUESTED_TO_JOIN_STAGE") {
|
|
3869
|
+
awaitingStageDecision.current = false;
|
|
3870
|
+
sonner.toast.message(labels.stageDenied);
|
|
3871
|
+
}
|
|
3872
|
+
}, [labels.stageApproved, labels.stageDenied, selfStageStatus, session.role]);
|
|
3714
3873
|
react.useEffect(() => {
|
|
3715
3874
|
if (session.role === "moderator" && waitlisted.length > 0 && !peopleAutoOpened.current) {
|
|
3716
3875
|
peopleAutoOpened.current = true;
|
|
@@ -3727,7 +3886,10 @@ function StudioRoom({
|
|
|
3727
3886
|
if (seenWaitlisted.current.has(guest.id)) continue;
|
|
3728
3887
|
seenWaitlisted.current.add(guest.id);
|
|
3729
3888
|
sonner.toast.message(labels.knock.replace("{name}", guest.name));
|
|
3730
|
-
if (session.role === "moderator")
|
|
3889
|
+
if (session.role === "moderator") {
|
|
3890
|
+
playStudioSound("request");
|
|
3891
|
+
setSideTab("people");
|
|
3892
|
+
}
|
|
3731
3893
|
}
|
|
3732
3894
|
}, [labels.knock, session.role, waitlisted]);
|
|
3733
3895
|
react.useEffect(() => {
|
|
@@ -3764,6 +3926,7 @@ function StudioRoom({
|
|
|
3764
3926
|
}
|
|
3765
3927
|
if (message.type === "share-request") {
|
|
3766
3928
|
if (session.role !== "moderator") return;
|
|
3929
|
+
playStudioSound("request");
|
|
3767
3930
|
sonner.toast.message(labels.shareRequestFrom.replace("{name}", message.name), {
|
|
3768
3931
|
id: `share-request:${message.participantId}`,
|
|
3769
3932
|
action: {
|
|
@@ -4139,10 +4302,13 @@ function StudioRoom({
|
|
|
4139
4302
|
size: "icon-sm",
|
|
4140
4303
|
"aria-label": labels.share,
|
|
4141
4304
|
onClick: () => {
|
|
4142
|
-
void
|
|
4143
|
-
playerWatchUrl(session.stream.id, playerOrigin)
|
|
4144
|
-
|
|
4145
|
-
|
|
4305
|
+
void shareOrCopyLink({
|
|
4306
|
+
url: playerWatchUrl(session.stream.id, playerOrigin),
|
|
4307
|
+
title: session.stream.title
|
|
4308
|
+
}).then((result) => {
|
|
4309
|
+
if (result === "copied") sonner.toast.success(labels.shareCopied);
|
|
4310
|
+
if (result === "failed") sonner.toast.error(labels.shareFailed);
|
|
4311
|
+
});
|
|
4146
4312
|
},
|
|
4147
4313
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, {})
|
|
4148
4314
|
}
|
|
@@ -4475,6 +4641,7 @@ function StudioDevicePreflight({
|
|
|
4475
4641
|
setMicOk(Boolean(audio));
|
|
4476
4642
|
setAudioTrack(audio);
|
|
4477
4643
|
if (videoRef.current) {
|
|
4644
|
+
videoRef.current.setAttribute("webkit-playsinline", "");
|
|
4478
4645
|
videoRef.current.srcObject = video ? next : null;
|
|
4479
4646
|
}
|
|
4480
4647
|
bindEnded(next, gen);
|
|
@@ -4504,7 +4671,7 @@ function StudioDevicePreflight({
|
|
|
4504
4671
|
autoPlay: true,
|
|
4505
4672
|
muted: true,
|
|
4506
4673
|
playsInline: true,
|
|
4507
|
-
className: "bg-muted aspect-video w-full rounded-xl object-cover"
|
|
4674
|
+
className: "livo-studio-inline-video bg-muted aspect-video w-full rounded-xl object-cover"
|
|
4508
4675
|
}
|
|
4509
4676
|
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-muted flex aspect-video w-full items-center justify-center rounded-xl", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground text-sm", children: cameraOffLabel }) }),
|
|
4510
4677
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4726,7 +4893,11 @@ function isRecorder(participant) {
|
|
|
4726
4893
|
function resolvedName(participant, nameOverrides) {
|
|
4727
4894
|
return nameOverrides.get(participant.id) ?? (participant.userId ? nameOverrides.get(participant.userId) : void 0) ?? participant.name?.trim() ?? "Guest";
|
|
4728
4895
|
}
|
|
4729
|
-
function
|
|
4896
|
+
function resolveTileStageStatus(participant, stageEnabled, fallback) {
|
|
4897
|
+
if (!stageEnabled) return "ON_STAGE";
|
|
4898
|
+
return participant.stageStatus ?? fallback ?? "OFF_STAGE";
|
|
4899
|
+
}
|
|
4900
|
+
function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides, stageStatus) {
|
|
4730
4901
|
return {
|
|
4731
4902
|
id: participant.id,
|
|
4732
4903
|
userId: participant.userId ?? "",
|
|
@@ -4736,7 +4907,7 @@ function tileFrom(participant, selfId, pinnedIds, chatSend, nameOverrides) {
|
|
|
4736
4907
|
videoEnabled: participant.videoEnabled === true,
|
|
4737
4908
|
screenShareEnabled: participant.screenShareEnabled === true,
|
|
4738
4909
|
picture: participant.picture ?? null,
|
|
4739
|
-
stageStatus
|
|
4910
|
+
stageStatus,
|
|
4740
4911
|
pinned: participant.isPinned === true || pinnedIds.has(participant.id),
|
|
4741
4912
|
chatCanSend: chatSend.get(participant.id) ?? true
|
|
4742
4913
|
};
|
|
@@ -4765,44 +4936,42 @@ function screenShareTrack(meeting, participantId) {
|
|
|
4765
4936
|
}
|
|
4766
4937
|
return findParticipant(meeting, participantId)?.screenShareTracks?.video ?? null;
|
|
4767
4938
|
}
|
|
4768
|
-
var
|
|
4769
|
-
function
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4939
|
+
var pendingMediaPlay = /* @__PURE__ */ new Set();
|
|
4940
|
+
function tryPlay(el) {
|
|
4941
|
+
if (typeof el.play !== "function") return;
|
|
4942
|
+
void el.play().then(
|
|
4943
|
+
() => {
|
|
4944
|
+
pendingMediaPlay.delete(el);
|
|
4945
|
+
},
|
|
4946
|
+
() => {
|
|
4947
|
+
pendingMediaPlay.add(el);
|
|
4948
|
+
}
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
function replayPendingMedia() {
|
|
4952
|
+
for (const el of pendingMediaPlay) {
|
|
4953
|
+
tryPlay(el);
|
|
4777
4954
|
}
|
|
4778
4955
|
}
|
|
4779
4956
|
if (typeof document !== "undefined") {
|
|
4780
|
-
document.addEventListener("pointerdown",
|
|
4957
|
+
document.addEventListener("pointerdown", replayPendingMedia, {
|
|
4781
4958
|
passive: true
|
|
4782
4959
|
});
|
|
4783
4960
|
}
|
|
4784
4961
|
function bindTrackElement(el, track, kind) {
|
|
4785
4962
|
if (!track) {
|
|
4786
4963
|
el.srcObject = null;
|
|
4787
|
-
|
|
4964
|
+
pendingMediaPlay.delete(el);
|
|
4788
4965
|
return;
|
|
4789
4966
|
}
|
|
4790
4967
|
const current = el.srcObject;
|
|
4791
4968
|
const currentTrack = current instanceof MediaStream ? kind === "video" ? current.getVideoTracks()[0] : current.getAudioTracks()[0] : void 0;
|
|
4792
4969
|
if (currentTrack?.id === track.id) {
|
|
4970
|
+
if (el.paused) tryPlay(el);
|
|
4793
4971
|
return;
|
|
4794
4972
|
}
|
|
4795
4973
|
el.srcObject = new MediaStream([track]);
|
|
4796
|
-
|
|
4797
|
-
void el.play().then(
|
|
4798
|
-
() => {
|
|
4799
|
-
if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
|
|
4800
|
-
},
|
|
4801
|
-
() => {
|
|
4802
|
-
if (el instanceof HTMLAudioElement) pendingAudioPlay.add(el);
|
|
4803
|
-
}
|
|
4804
|
-
);
|
|
4805
|
-
}
|
|
4974
|
+
tryPlay(el);
|
|
4806
4975
|
}
|
|
4807
4976
|
function roomLeftState(payload, fallback) {
|
|
4808
4977
|
if (typeof payload === "string" && payload) return payload;
|
|
@@ -5280,7 +5449,7 @@ function studioMeetingFromRtk(raw) {
|
|
|
5280
5449
|
notifyStage();
|
|
5281
5450
|
},
|
|
5282
5451
|
async joinStage() {
|
|
5283
|
-
if (
|
|
5452
|
+
if (meeting.self?.stageStatus === "ON_STAGE") {
|
|
5284
5453
|
notifyStage();
|
|
5285
5454
|
return;
|
|
5286
5455
|
}
|
|
@@ -5289,14 +5458,15 @@ function studioMeetingFromRtk(raw) {
|
|
|
5289
5458
|
notifyStage();
|
|
5290
5459
|
return;
|
|
5291
5460
|
}
|
|
5461
|
+
const userId = meeting.self?.userId;
|
|
5462
|
+
if (userId) {
|
|
5463
|
+
await meeting.stage?.grantAccess?.([userId]);
|
|
5464
|
+
}
|
|
5292
5465
|
try {
|
|
5293
5466
|
await meeting.stage?.join?.();
|
|
5294
|
-
} catch {
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
await meeting.stage?.grantAccess?.([userId]);
|
|
5298
|
-
}
|
|
5299
|
-
await meeting.stage?.join?.();
|
|
5467
|
+
} catch (error) {
|
|
5468
|
+
notifyStage();
|
|
5469
|
+
throw error;
|
|
5300
5470
|
}
|
|
5301
5471
|
joinedStageLocally = true;
|
|
5302
5472
|
await republishCameraIfOn();
|
|
@@ -5304,6 +5474,7 @@ function studioMeetingFromRtk(raw) {
|
|
|
5304
5474
|
},
|
|
5305
5475
|
async leaveStage() {
|
|
5306
5476
|
await meeting.stage?.leave?.();
|
|
5477
|
+
joinedStageLocally = false;
|
|
5307
5478
|
notifyStage();
|
|
5308
5479
|
},
|
|
5309
5480
|
stageStatus() {
|
|
@@ -5385,10 +5556,27 @@ function studioMeetingFromRtk(raw) {
|
|
|
5385
5556
|
participants() {
|
|
5386
5557
|
const self = meeting.self;
|
|
5387
5558
|
const pinned = pinnedIdSet();
|
|
5388
|
-
const
|
|
5559
|
+
const stageEnabled = self?.permissions?.stageEnabled !== false;
|
|
5560
|
+
const joinedRows = asList(meeting.participants?.joined).filter((row) => row.id !== self?.id && !isRecorder(row)).map(
|
|
5561
|
+
(row) => tileFrom(
|
|
5562
|
+
row,
|
|
5563
|
+
self?.id,
|
|
5564
|
+
pinned,
|
|
5565
|
+
chatSend,
|
|
5566
|
+
nameOverrides,
|
|
5567
|
+
resolveTileStageStatus(row, stageEnabled)
|
|
5568
|
+
)
|
|
5569
|
+
);
|
|
5389
5570
|
if (self?.id && !isRecorder(self)) {
|
|
5390
5571
|
joinedRows.unshift(
|
|
5391
|
-
tileFrom(
|
|
5572
|
+
tileFrom(
|
|
5573
|
+
self,
|
|
5574
|
+
self.id,
|
|
5575
|
+
pinned,
|
|
5576
|
+
chatSend,
|
|
5577
|
+
nameOverrides,
|
|
5578
|
+
resolveTileStageStatus(self, stageEnabled, meeting.stage?.status)
|
|
5579
|
+
)
|
|
5392
5580
|
);
|
|
5393
5581
|
}
|
|
5394
5582
|
return joinedRows;
|
|
@@ -5766,6 +5954,21 @@ async function getStudioJoinStatus(token, apiUrl = defaultApiUrl(), fetchFn = fe
|
|
|
5766
5954
|
throw error;
|
|
5767
5955
|
}
|
|
5768
5956
|
}
|
|
5957
|
+
async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
|
|
5958
|
+
try {
|
|
5959
|
+
const body = await publicJson(
|
|
5960
|
+
`${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
|
|
5961
|
+
{},
|
|
5962
|
+
fetchFn
|
|
5963
|
+
);
|
|
5964
|
+
return parsePublicStreamStatus(body);
|
|
5965
|
+
} catch (error) {
|
|
5966
|
+
if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
|
|
5967
|
+
return "ended";
|
|
5968
|
+
}
|
|
5969
|
+
return null;
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5769
5972
|
var POLL_INTERVAL_MS = 5e3;
|
|
5770
5973
|
function LivoHostStudio({
|
|
5771
5974
|
token,
|
|
@@ -5860,6 +6063,7 @@ function LivoHostStudio({
|
|
|
5860
6063
|
);
|
|
5861
6064
|
}
|
|
5862
6065
|
var POLL_INTERVAL_MS2 = 5e3;
|
|
6066
|
+
var PREVIEW_POLL_INTERVAL_MS = 2e3;
|
|
5863
6067
|
function LivoGuestStudio({
|
|
5864
6068
|
token,
|
|
5865
6069
|
apiUrl,
|
|
@@ -5871,6 +6075,7 @@ function LivoGuestStudio({
|
|
|
5871
6075
|
}) {
|
|
5872
6076
|
const reducedMotion = usePrefersReducedMotion();
|
|
5873
6077
|
const [session, setSession] = react.useState(null);
|
|
6078
|
+
const [status, setStatus] = react.useState(null);
|
|
5874
6079
|
const [error, setError] = react.useState(null);
|
|
5875
6080
|
const [displayName, setDisplayName] = react.useState("");
|
|
5876
6081
|
const [guestId, setGuestId] = react.useState("");
|
|
@@ -5920,6 +6125,7 @@ function LivoGuestStudio({
|
|
|
5920
6125
|
);
|
|
5921
6126
|
setWaitingForHost(false);
|
|
5922
6127
|
setRetryAfterMs(null);
|
|
6128
|
+
setStatus(null);
|
|
5923
6129
|
setSession(next);
|
|
5924
6130
|
} catch (err) {
|
|
5925
6131
|
const code = studioErrorCode(err);
|
|
@@ -5967,6 +6173,31 @@ function LivoGuestStudio({
|
|
|
5967
6173
|
window.clearInterval(id);
|
|
5968
6174
|
};
|
|
5969
6175
|
}, [apiUrl, join, lobbyLabels.endedTitle, token, waitingForHost]);
|
|
6176
|
+
react.useEffect(() => {
|
|
6177
|
+
if (!session) return;
|
|
6178
|
+
const current = status ?? session.stream.status;
|
|
6179
|
+
if (current === "ended" || current === "error") return;
|
|
6180
|
+
let cancelled = false;
|
|
6181
|
+
const poll = async () => {
|
|
6182
|
+
const next = await getPublicStreamStatus(session.stream.id, apiUrl);
|
|
6183
|
+
if (!cancelled && next) setStatus(next);
|
|
6184
|
+
};
|
|
6185
|
+
void poll();
|
|
6186
|
+
const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
|
|
6187
|
+
const id = window.setInterval(() => void poll(), intervalMs);
|
|
6188
|
+
return () => {
|
|
6189
|
+
cancelled = true;
|
|
6190
|
+
window.clearInterval(id);
|
|
6191
|
+
};
|
|
6192
|
+
}, [apiUrl, session, status]);
|
|
6193
|
+
const liveSession = react.useMemo(() => {
|
|
6194
|
+
if (!session) return null;
|
|
6195
|
+
if (!status) return session;
|
|
6196
|
+
return {
|
|
6197
|
+
...session,
|
|
6198
|
+
stream: { ...session.stream, status }
|
|
6199
|
+
};
|
|
6200
|
+
}, [session, status]);
|
|
5970
6201
|
react.useEffect(() => {
|
|
5971
6202
|
if (retryAfterMs == null) return;
|
|
5972
6203
|
const id = window.setTimeout(() => {
|
|
@@ -5979,7 +6210,7 @@ function LivoGuestStudio({
|
|
|
5979
6210
|
if (meetingError) {
|
|
5980
6211
|
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive p-6", role: "alert", children: meetingError });
|
|
5981
6212
|
}
|
|
5982
|
-
if (!
|
|
6213
|
+
if (!liveSession || !meeting) {
|
|
5983
6214
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5984
6215
|
StudioGuestLobby,
|
|
5985
6216
|
{
|
|
@@ -6004,7 +6235,7 @@ function LivoGuestStudio({
|
|
|
6004
6235
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6005
6236
|
StudioRoom,
|
|
6006
6237
|
{
|
|
6007
|
-
session,
|
|
6238
|
+
session: liveSession,
|
|
6008
6239
|
meeting,
|
|
6009
6240
|
control: {
|
|
6010
6241
|
livestream: async () => "starting",
|
|
@@ -6013,6 +6244,7 @@ function LivoGuestStudio({
|
|
|
6013
6244
|
},
|
|
6014
6245
|
onRejoin: () => {
|
|
6015
6246
|
setSession(null);
|
|
6247
|
+
setStatus(null);
|
|
6016
6248
|
setSessionGen((n) => n + 1);
|
|
6017
6249
|
setJoining(false);
|
|
6018
6250
|
setWaitingForHost(false);
|
|
@@ -6159,6 +6391,8 @@ function studioRoomLabels(t) {
|
|
|
6159
6391
|
admitAllAudience: t("admitAllAudience"),
|
|
6160
6392
|
askToGoOnAir: t("askToGoOnAir"),
|
|
6161
6393
|
stageRequested: t("stageRequested"),
|
|
6394
|
+
stageApproved: t("stageApproved"),
|
|
6395
|
+
stageDenied: t("stageDenied"),
|
|
6162
6396
|
stageRequests: t("stageRequests"),
|
|
6163
6397
|
people: t("people"),
|
|
6164
6398
|
panelists: t("panelists"),
|
|
@@ -6210,6 +6444,7 @@ exports.applyEmbedTheme = applyEmbedTheme;
|
|
|
6210
6444
|
exports.controlActionsFromClient = controlActionsFromClient;
|
|
6211
6445
|
exports.createStudioControlClient = createStudioControlClient;
|
|
6212
6446
|
exports.decodeEmbedTheme = decodeEmbedTheme;
|
|
6447
|
+
exports.getPublicStreamStatus = getPublicStreamStatus;
|
|
6213
6448
|
exports.getStudioJoinStatus = getStudioJoinStatus;
|
|
6214
6449
|
exports.joinStudioGuest = joinStudioGuest;
|
|
6215
6450
|
exports.redeemStudioHost = redeemStudioHost;
|