@livo-tv/sdk 1.2.0 → 1.2.2
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 -68
- 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 +307 -70
- 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
|
}
|
|
@@ -2212,7 +2305,7 @@ function BubbleGroup({ className, ...props }) {
|
|
|
2212
2305
|
);
|
|
2213
2306
|
}
|
|
2214
2307
|
var bubbleVariants = classVarianceAuthority.cva(
|
|
2215
|
-
"group/bubble relative flex w-fit max-w-[80%]
|
|
2308
|
+
"group/bubble relative flex w-fit min-w-0 max-w-[80%] 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 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 w-fit max-w-full flex-col gap-1" : "flex w-fit max-w-full flex-col gap-1",
|
|
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
|
}
|
|
@@ -2609,7 +2709,7 @@ function StudioSidePanel({
|
|
|
2609
2709
|
const panelists = participants.filter((row) => isOnStage(row.stageStatus));
|
|
2610
2710
|
const audience = participants.filter((row) => !isOnStage(row.stageStatus));
|
|
2611
2711
|
return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: "bg-background flex min-h-0 w-full flex-1 flex-col border-t md:w-80 md:flex-none md:border-l md:border-t-0", children: [
|
|
2612
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex
|
|
2712
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex", children: [
|
|
2613
2713
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2614
2714
|
Button,
|
|
2615
2715
|
{
|
|
@@ -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"
|
|
@@ -3263,14 +3365,24 @@ function StudioToolbar({
|
|
|
3263
3365
|
children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
|
|
3264
3366
|
}
|
|
3265
3367
|
) : null,
|
|
3266
|
-
session.role === "guest" && stageEnabled
|
|
3368
|
+
session.role === "guest" && stageEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3267
3369
|
ControlIcon,
|
|
3268
3370
|
{
|
|
3269
|
-
label: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
|
|
3371
|
+
label: selfStageStatus === "ON_STAGE" ? labels.goOffAir : selfStageStatus === "REQUESTED_TO_JOIN_STAGE" ? labels.stageRequested : labels.askToGoOnAir,
|
|
3270
3372
|
pressed: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
|
|
3271
|
-
disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE",
|
|
3272
|
-
onClick:
|
|
3273
|
-
|
|
3373
|
+
disabled: selfStageStatus === "REQUESTED_TO_JOIN_STAGE" || selfStageStatus === "ON_STAGE" && !canLeaveStage,
|
|
3374
|
+
onClick: () => {
|
|
3375
|
+
if (selfStageStatus === "ON_STAGE") {
|
|
3376
|
+
if (!canLeaveStage) {
|
|
3377
|
+
sonner.toast.message(labels.offAirLastVisible);
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
onLeaveStage();
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
onAskStage();
|
|
3384
|
+
},
|
|
3385
|
+
children: selfStageStatus === "ON_STAGE" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, {}) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {})
|
|
3274
3386
|
}
|
|
3275
3387
|
) : null,
|
|
3276
3388
|
showScreenControl ? canShareFreely ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3445,6 +3557,12 @@ function StudioRoom({
|
|
|
3445
3557
|
const seenWaitlisted = react.useRef(/* @__PURE__ */ new Set());
|
|
3446
3558
|
const knockPrimed = react.useRef(false);
|
|
3447
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);
|
|
3448
3566
|
const grantCheckTimers = react.useRef([]);
|
|
3449
3567
|
const screenNoticePrimed = react.useRef(false);
|
|
3450
3568
|
const lastSelfMedia = react.useRef({
|
|
@@ -3473,6 +3591,9 @@ function StudioRoom({
|
|
|
3473
3591
|
const rejected = isStudioRejected(roomState);
|
|
3474
3592
|
useBeforeUnload(active && !sessionOver);
|
|
3475
3593
|
useWakeLock(active && !sessionOver);
|
|
3594
|
+
react.useEffect(() => {
|
|
3595
|
+
unlockStudioSounds();
|
|
3596
|
+
}, []);
|
|
3476
3597
|
react.useEffect(() => {
|
|
3477
3598
|
if (onAir) {
|
|
3478
3599
|
setPublishing(false);
|
|
@@ -3698,9 +3819,57 @@ function StudioRoom({
|
|
|
3698
3819
|
const incoming = chatMessages.filter((row) => row.userId !== selfUserId);
|
|
3699
3820
|
const unread = sideTab === "chat" ? 0 : Math.max(0, incoming.length - seenIncoming);
|
|
3700
3821
|
const peopleBadge = waitlisted.length + stageRequests.length;
|
|
3822
|
+
const selfStageStatus = self?.stageStatus ?? meeting.stageStatus();
|
|
3701
3823
|
react.useEffect(() => {
|
|
3702
3824
|
if (sideTab === "chat") setSeenIncoming(incoming.length);
|
|
3703
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]);
|
|
3704
3873
|
react.useEffect(() => {
|
|
3705
3874
|
if (session.role === "moderator" && waitlisted.length > 0 && !peopleAutoOpened.current) {
|
|
3706
3875
|
peopleAutoOpened.current = true;
|
|
@@ -3717,7 +3886,10 @@ function StudioRoom({
|
|
|
3717
3886
|
if (seenWaitlisted.current.has(guest.id)) continue;
|
|
3718
3887
|
seenWaitlisted.current.add(guest.id);
|
|
3719
3888
|
sonner.toast.message(labels.knock.replace("{name}", guest.name));
|
|
3720
|
-
if (session.role === "moderator")
|
|
3889
|
+
if (session.role === "moderator") {
|
|
3890
|
+
playStudioSound("request");
|
|
3891
|
+
setSideTab("people");
|
|
3892
|
+
}
|
|
3721
3893
|
}
|
|
3722
3894
|
}, [labels.knock, session.role, waitlisted]);
|
|
3723
3895
|
react.useEffect(() => {
|
|
@@ -3754,6 +3926,7 @@ function StudioRoom({
|
|
|
3754
3926
|
}
|
|
3755
3927
|
if (message.type === "share-request") {
|
|
3756
3928
|
if (session.role !== "moderator") return;
|
|
3929
|
+
playStudioSound("request");
|
|
3757
3930
|
sonner.toast.message(labels.shareRequestFrom.replace("{name}", message.name), {
|
|
3758
3931
|
id: `share-request:${message.participantId}`,
|
|
3759
3932
|
action: {
|
|
@@ -4129,10 +4302,13 @@ function StudioRoom({
|
|
|
4129
4302
|
size: "icon-sm",
|
|
4130
4303
|
"aria-label": labels.share,
|
|
4131
4304
|
onClick: () => {
|
|
4132
|
-
void
|
|
4133
|
-
playerWatchUrl(session.stream.id, playerOrigin)
|
|
4134
|
-
|
|
4135
|
-
|
|
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
|
+
});
|
|
4136
4312
|
},
|
|
4137
4313
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Share2, {})
|
|
4138
4314
|
}
|
|
@@ -4465,6 +4641,7 @@ function StudioDevicePreflight({
|
|
|
4465
4641
|
setMicOk(Boolean(audio));
|
|
4466
4642
|
setAudioTrack(audio);
|
|
4467
4643
|
if (videoRef.current) {
|
|
4644
|
+
videoRef.current.setAttribute("webkit-playsinline", "");
|
|
4468
4645
|
videoRef.current.srcObject = video ? next : null;
|
|
4469
4646
|
}
|
|
4470
4647
|
bindEnded(next, gen);
|
|
@@ -4494,7 +4671,7 @@ function StudioDevicePreflight({
|
|
|
4494
4671
|
autoPlay: true,
|
|
4495
4672
|
muted: true,
|
|
4496
4673
|
playsInline: true,
|
|
4497
|
-
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"
|
|
4498
4675
|
}
|
|
4499
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 }) }),
|
|
4500
4677
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4755,44 +4932,42 @@ function screenShareTrack(meeting, participantId) {
|
|
|
4755
4932
|
}
|
|
4756
4933
|
return findParticipant(meeting, participantId)?.screenShareTracks?.video ?? null;
|
|
4757
4934
|
}
|
|
4758
|
-
var
|
|
4759
|
-
function
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4935
|
+
var pendingMediaPlay = /* @__PURE__ */ new Set();
|
|
4936
|
+
function tryPlay(el) {
|
|
4937
|
+
if (typeof el.play !== "function") return;
|
|
4938
|
+
void el.play().then(
|
|
4939
|
+
() => {
|
|
4940
|
+
pendingMediaPlay.delete(el);
|
|
4941
|
+
},
|
|
4942
|
+
() => {
|
|
4943
|
+
pendingMediaPlay.add(el);
|
|
4944
|
+
}
|
|
4945
|
+
);
|
|
4946
|
+
}
|
|
4947
|
+
function replayPendingMedia() {
|
|
4948
|
+
for (const el of pendingMediaPlay) {
|
|
4949
|
+
tryPlay(el);
|
|
4767
4950
|
}
|
|
4768
4951
|
}
|
|
4769
4952
|
if (typeof document !== "undefined") {
|
|
4770
|
-
document.addEventListener("pointerdown",
|
|
4953
|
+
document.addEventListener("pointerdown", replayPendingMedia, {
|
|
4771
4954
|
passive: true
|
|
4772
4955
|
});
|
|
4773
4956
|
}
|
|
4774
4957
|
function bindTrackElement(el, track, kind) {
|
|
4775
4958
|
if (!track) {
|
|
4776
4959
|
el.srcObject = null;
|
|
4777
|
-
|
|
4960
|
+
pendingMediaPlay.delete(el);
|
|
4778
4961
|
return;
|
|
4779
4962
|
}
|
|
4780
4963
|
const current = el.srcObject;
|
|
4781
4964
|
const currentTrack = current instanceof MediaStream ? kind === "video" ? current.getVideoTracks()[0] : current.getAudioTracks()[0] : void 0;
|
|
4782
4965
|
if (currentTrack?.id === track.id) {
|
|
4966
|
+
if (el.paused) tryPlay(el);
|
|
4783
4967
|
return;
|
|
4784
4968
|
}
|
|
4785
4969
|
el.srcObject = new MediaStream([track]);
|
|
4786
|
-
|
|
4787
|
-
void el.play().then(
|
|
4788
|
-
() => {
|
|
4789
|
-
if (el instanceof HTMLAudioElement) pendingAudioPlay.delete(el);
|
|
4790
|
-
},
|
|
4791
|
-
() => {
|
|
4792
|
-
if (el instanceof HTMLAudioElement) pendingAudioPlay.add(el);
|
|
4793
|
-
}
|
|
4794
|
-
);
|
|
4795
|
-
}
|
|
4970
|
+
tryPlay(el);
|
|
4796
4971
|
}
|
|
4797
4972
|
function roomLeftState(payload, fallback) {
|
|
4798
4973
|
if (typeof payload === "string" && payload) return payload;
|
|
@@ -5279,7 +5454,15 @@ function studioMeetingFromRtk(raw) {
|
|
|
5279
5454
|
notifyStage();
|
|
5280
5455
|
return;
|
|
5281
5456
|
}
|
|
5282
|
-
|
|
5457
|
+
try {
|
|
5458
|
+
await meeting.stage?.join?.();
|
|
5459
|
+
} catch {
|
|
5460
|
+
const userId = meeting.self?.userId;
|
|
5461
|
+
if (userId) {
|
|
5462
|
+
await meeting.stage?.grantAccess?.([userId]);
|
|
5463
|
+
}
|
|
5464
|
+
await meeting.stage?.join?.();
|
|
5465
|
+
}
|
|
5283
5466
|
joinedStageLocally = true;
|
|
5284
5467
|
await republishCameraIfOn();
|
|
5285
5468
|
notifyStage();
|
|
@@ -5569,7 +5752,11 @@ function useRealtimeKitMeeting(authToken, generation = 0) {
|
|
|
5569
5752
|
const [meeting, setMeeting] = react.useState(null);
|
|
5570
5753
|
const [error, setError] = react.useState(null);
|
|
5571
5754
|
react.useEffect(() => {
|
|
5572
|
-
if (!authToken)
|
|
5755
|
+
if (!authToken) {
|
|
5756
|
+
setMeeting(null);
|
|
5757
|
+
setError(null);
|
|
5758
|
+
return;
|
|
5759
|
+
}
|
|
5573
5760
|
let cancelled = false;
|
|
5574
5761
|
let handle = null;
|
|
5575
5762
|
setError(null);
|
|
@@ -5744,6 +5931,21 @@ async function getStudioJoinStatus(token, apiUrl = defaultApiUrl(), fetchFn = fe
|
|
|
5744
5931
|
throw error;
|
|
5745
5932
|
}
|
|
5746
5933
|
}
|
|
5934
|
+
async function getPublicStreamStatus(streamId, apiUrl = defaultApiUrl(), fetchFn = fetch) {
|
|
5935
|
+
try {
|
|
5936
|
+
const body = await publicJson(
|
|
5937
|
+
`${apiUrl.replace(/\/$/, "")}/public/streams/${encodeURIComponent(streamId)}`,
|
|
5938
|
+
{},
|
|
5939
|
+
fetchFn
|
|
5940
|
+
);
|
|
5941
|
+
return parsePublicStreamStatus(body);
|
|
5942
|
+
} catch (error) {
|
|
5943
|
+
if (error instanceof StudioApiError && (error.status === 404 || error.status === 410)) {
|
|
5944
|
+
return "ended";
|
|
5945
|
+
}
|
|
5946
|
+
return null;
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5747
5949
|
var POLL_INTERVAL_MS = 5e3;
|
|
5748
5950
|
function LivoHostStudio({
|
|
5749
5951
|
token,
|
|
@@ -5838,6 +6040,7 @@ function LivoHostStudio({
|
|
|
5838
6040
|
);
|
|
5839
6041
|
}
|
|
5840
6042
|
var POLL_INTERVAL_MS2 = 5e3;
|
|
6043
|
+
var PREVIEW_POLL_INTERVAL_MS = 2e3;
|
|
5841
6044
|
function LivoGuestStudio({
|
|
5842
6045
|
token,
|
|
5843
6046
|
apiUrl,
|
|
@@ -5849,6 +6052,7 @@ function LivoGuestStudio({
|
|
|
5849
6052
|
}) {
|
|
5850
6053
|
const reducedMotion = usePrefersReducedMotion();
|
|
5851
6054
|
const [session, setSession] = react.useState(null);
|
|
6055
|
+
const [status, setStatus] = react.useState(null);
|
|
5852
6056
|
const [error, setError] = react.useState(null);
|
|
5853
6057
|
const [displayName, setDisplayName] = react.useState("");
|
|
5854
6058
|
const [guestId, setGuestId] = react.useState("");
|
|
@@ -5898,6 +6102,7 @@ function LivoGuestStudio({
|
|
|
5898
6102
|
);
|
|
5899
6103
|
setWaitingForHost(false);
|
|
5900
6104
|
setRetryAfterMs(null);
|
|
6105
|
+
setStatus(null);
|
|
5901
6106
|
setSession(next);
|
|
5902
6107
|
} catch (err) {
|
|
5903
6108
|
const code = studioErrorCode(err);
|
|
@@ -5914,9 +6119,9 @@ function LivoGuestStudio({
|
|
|
5914
6119
|
setError(err instanceof Error ? err.message : lobbyLabels.joinError);
|
|
5915
6120
|
setWaitingForHost(false);
|
|
5916
6121
|
}
|
|
5917
|
-
setJoining(false);
|
|
5918
6122
|
} finally {
|
|
5919
6123
|
joinInFlight.current = false;
|
|
6124
|
+
setJoining(false);
|
|
5920
6125
|
}
|
|
5921
6126
|
}, [apiUrl, displayName, guestId, lobbyLabels, token]);
|
|
5922
6127
|
react.useEffect(() => {
|
|
@@ -5945,6 +6150,31 @@ function LivoGuestStudio({
|
|
|
5945
6150
|
window.clearInterval(id);
|
|
5946
6151
|
};
|
|
5947
6152
|
}, [apiUrl, join, lobbyLabels.endedTitle, token, waitingForHost]);
|
|
6153
|
+
react.useEffect(() => {
|
|
6154
|
+
if (!session) return;
|
|
6155
|
+
const current = status ?? session.stream.status;
|
|
6156
|
+
if (current === "ended" || current === "error") return;
|
|
6157
|
+
let cancelled = false;
|
|
6158
|
+
const poll = async () => {
|
|
6159
|
+
const next = await getPublicStreamStatus(session.stream.id, apiUrl);
|
|
6160
|
+
if (!cancelled && next) setStatus(next);
|
|
6161
|
+
};
|
|
6162
|
+
void poll();
|
|
6163
|
+
const intervalMs = current === "public" ? POLL_INTERVAL_MS2 : PREVIEW_POLL_INTERVAL_MS;
|
|
6164
|
+
const id = window.setInterval(() => void poll(), intervalMs);
|
|
6165
|
+
return () => {
|
|
6166
|
+
cancelled = true;
|
|
6167
|
+
window.clearInterval(id);
|
|
6168
|
+
};
|
|
6169
|
+
}, [apiUrl, session, status]);
|
|
6170
|
+
const liveSession = react.useMemo(() => {
|
|
6171
|
+
if (!session) return null;
|
|
6172
|
+
if (!status) return session;
|
|
6173
|
+
return {
|
|
6174
|
+
...session,
|
|
6175
|
+
stream: { ...session.stream, status }
|
|
6176
|
+
};
|
|
6177
|
+
}, [session, status]);
|
|
5948
6178
|
react.useEffect(() => {
|
|
5949
6179
|
if (retryAfterMs == null) return;
|
|
5950
6180
|
const id = window.setTimeout(() => {
|
|
@@ -5957,7 +6187,7 @@ function LivoGuestStudio({
|
|
|
5957
6187
|
if (meetingError) {
|
|
5958
6188
|
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive p-6", role: "alert", children: meetingError });
|
|
5959
6189
|
}
|
|
5960
|
-
if (!
|
|
6190
|
+
if (!liveSession || !meeting) {
|
|
5961
6191
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5962
6192
|
StudioGuestLobby,
|
|
5963
6193
|
{
|
|
@@ -5982,7 +6212,7 @@ function LivoGuestStudio({
|
|
|
5982
6212
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5983
6213
|
StudioRoom,
|
|
5984
6214
|
{
|
|
5985
|
-
session,
|
|
6215
|
+
session: liveSession,
|
|
5986
6216
|
meeting,
|
|
5987
6217
|
control: {
|
|
5988
6218
|
livestream: async () => "starting",
|
|
@@ -5991,7 +6221,12 @@ function LivoGuestStudio({
|
|
|
5991
6221
|
},
|
|
5992
6222
|
onRejoin: () => {
|
|
5993
6223
|
setSession(null);
|
|
6224
|
+
setStatus(null);
|
|
5994
6225
|
setSessionGen((n) => n + 1);
|
|
6226
|
+
setJoining(false);
|
|
6227
|
+
setWaitingForHost(false);
|
|
6228
|
+
setError(null);
|
|
6229
|
+
joinInFlight.current = false;
|
|
5995
6230
|
},
|
|
5996
6231
|
reducedMotion,
|
|
5997
6232
|
labels,
|
|
@@ -6133,6 +6368,8 @@ function studioRoomLabels(t) {
|
|
|
6133
6368
|
admitAllAudience: t("admitAllAudience"),
|
|
6134
6369
|
askToGoOnAir: t("askToGoOnAir"),
|
|
6135
6370
|
stageRequested: t("stageRequested"),
|
|
6371
|
+
stageApproved: t("stageApproved"),
|
|
6372
|
+
stageDenied: t("stageDenied"),
|
|
6136
6373
|
stageRequests: t("stageRequests"),
|
|
6137
6374
|
people: t("people"),
|
|
6138
6375
|
panelists: t("panelists"),
|
|
@@ -6184,6 +6421,7 @@ exports.applyEmbedTheme = applyEmbedTheme;
|
|
|
6184
6421
|
exports.controlActionsFromClient = controlActionsFromClient;
|
|
6185
6422
|
exports.createStudioControlClient = createStudioControlClient;
|
|
6186
6423
|
exports.decodeEmbedTheme = decodeEmbedTheme;
|
|
6424
|
+
exports.getPublicStreamStatus = getPublicStreamStatus;
|
|
6187
6425
|
exports.getStudioJoinStatus = getStudioJoinStatus;
|
|
6188
6426
|
exports.joinStudioGuest = joinStudioGuest;
|
|
6189
6427
|
exports.redeemStudioHost = redeemStudioHost;
|