@livo-tv/sdk 1.2.1 → 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 +271 -59
- 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 +271 -60
- 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-
|
|
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
|
}
|
|
@@ -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(
|
|
@@ -4765,44 +4932,42 @@ function screenShareTrack(meeting, participantId) {
|
|
|
4765
4932
|
}
|
|
4766
4933
|
return findParticipant(meeting, participantId)?.screenShareTracks?.video ?? null;
|
|
4767
4934
|
}
|
|
4768
|
-
var
|
|
4769
|
-
function
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
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);
|
|
4777
4950
|
}
|
|
4778
4951
|
}
|
|
4779
4952
|
if (typeof document !== "undefined") {
|
|
4780
|
-
document.addEventListener("pointerdown",
|
|
4953
|
+
document.addEventListener("pointerdown", replayPendingMedia, {
|
|
4781
4954
|
passive: true
|
|
4782
4955
|
});
|
|
4783
4956
|
}
|
|
4784
4957
|
function bindTrackElement(el, track, kind) {
|
|
4785
4958
|
if (!track) {
|
|
4786
4959
|
el.srcObject = null;
|
|
4787
|
-
|
|
4960
|
+
pendingMediaPlay.delete(el);
|
|
4788
4961
|
return;
|
|
4789
4962
|
}
|
|
4790
4963
|
const current = el.srcObject;
|
|
4791
4964
|
const currentTrack = current instanceof MediaStream ? kind === "video" ? current.getVideoTracks()[0] : current.getAudioTracks()[0] : void 0;
|
|
4792
4965
|
if (currentTrack?.id === track.id) {
|
|
4966
|
+
if (el.paused) tryPlay(el);
|
|
4793
4967
|
return;
|
|
4794
4968
|
}
|
|
4795
4969
|
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
|
-
}
|
|
4970
|
+
tryPlay(el);
|
|
4806
4971
|
}
|
|
4807
4972
|
function roomLeftState(payload, fallback) {
|
|
4808
4973
|
if (typeof payload === "string" && payload) return payload;
|
|
@@ -5766,6 +5931,21 @@ async function getStudioJoinStatus(token, apiUrl = defaultApiUrl(), fetchFn = fe
|
|
|
5766
5931
|
throw error;
|
|
5767
5932
|
}
|
|
5768
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
|
+
}
|
|
5769
5949
|
var POLL_INTERVAL_MS = 5e3;
|
|
5770
5950
|
function LivoHostStudio({
|
|
5771
5951
|
token,
|
|
@@ -5860,6 +6040,7 @@ function LivoHostStudio({
|
|
|
5860
6040
|
);
|
|
5861
6041
|
}
|
|
5862
6042
|
var POLL_INTERVAL_MS2 = 5e3;
|
|
6043
|
+
var PREVIEW_POLL_INTERVAL_MS = 2e3;
|
|
5863
6044
|
function LivoGuestStudio({
|
|
5864
6045
|
token,
|
|
5865
6046
|
apiUrl,
|
|
@@ -5871,6 +6052,7 @@ function LivoGuestStudio({
|
|
|
5871
6052
|
}) {
|
|
5872
6053
|
const reducedMotion = usePrefersReducedMotion();
|
|
5873
6054
|
const [session, setSession] = react.useState(null);
|
|
6055
|
+
const [status, setStatus] = react.useState(null);
|
|
5874
6056
|
const [error, setError] = react.useState(null);
|
|
5875
6057
|
const [displayName, setDisplayName] = react.useState("");
|
|
5876
6058
|
const [guestId, setGuestId] = react.useState("");
|
|
@@ -5920,6 +6102,7 @@ function LivoGuestStudio({
|
|
|
5920
6102
|
);
|
|
5921
6103
|
setWaitingForHost(false);
|
|
5922
6104
|
setRetryAfterMs(null);
|
|
6105
|
+
setStatus(null);
|
|
5923
6106
|
setSession(next);
|
|
5924
6107
|
} catch (err) {
|
|
5925
6108
|
const code = studioErrorCode(err);
|
|
@@ -5967,6 +6150,31 @@ function LivoGuestStudio({
|
|
|
5967
6150
|
window.clearInterval(id);
|
|
5968
6151
|
};
|
|
5969
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]);
|
|
5970
6178
|
react.useEffect(() => {
|
|
5971
6179
|
if (retryAfterMs == null) return;
|
|
5972
6180
|
const id = window.setTimeout(() => {
|
|
@@ -5979,7 +6187,7 @@ function LivoGuestStudio({
|
|
|
5979
6187
|
if (meetingError) {
|
|
5980
6188
|
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive p-6", role: "alert", children: meetingError });
|
|
5981
6189
|
}
|
|
5982
|
-
if (!
|
|
6190
|
+
if (!liveSession || !meeting) {
|
|
5983
6191
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5984
6192
|
StudioGuestLobby,
|
|
5985
6193
|
{
|
|
@@ -6004,7 +6212,7 @@ function LivoGuestStudio({
|
|
|
6004
6212
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6005
6213
|
StudioRoom,
|
|
6006
6214
|
{
|
|
6007
|
-
session,
|
|
6215
|
+
session: liveSession,
|
|
6008
6216
|
meeting,
|
|
6009
6217
|
control: {
|
|
6010
6218
|
livestream: async () => "starting",
|
|
@@ -6013,6 +6221,7 @@ function LivoGuestStudio({
|
|
|
6013
6221
|
},
|
|
6014
6222
|
onRejoin: () => {
|
|
6015
6223
|
setSession(null);
|
|
6224
|
+
setStatus(null);
|
|
6016
6225
|
setSessionGen((n) => n + 1);
|
|
6017
6226
|
setJoining(false);
|
|
6018
6227
|
setWaitingForHost(false);
|
|
@@ -6159,6 +6368,8 @@ function studioRoomLabels(t) {
|
|
|
6159
6368
|
admitAllAudience: t("admitAllAudience"),
|
|
6160
6369
|
askToGoOnAir: t("askToGoOnAir"),
|
|
6161
6370
|
stageRequested: t("stageRequested"),
|
|
6371
|
+
stageApproved: t("stageApproved"),
|
|
6372
|
+
stageDenied: t("stageDenied"),
|
|
6162
6373
|
stageRequests: t("stageRequests"),
|
|
6163
6374
|
people: t("people"),
|
|
6164
6375
|
panelists: t("panelists"),
|
|
@@ -6210,6 +6421,7 @@ exports.applyEmbedTheme = applyEmbedTheme;
|
|
|
6210
6421
|
exports.controlActionsFromClient = controlActionsFromClient;
|
|
6211
6422
|
exports.createStudioControlClient = createStudioControlClient;
|
|
6212
6423
|
exports.decodeEmbedTheme = decodeEmbedTheme;
|
|
6424
|
+
exports.getPublicStreamStatus = getPublicStreamStatus;
|
|
6213
6425
|
exports.getStudioJoinStatus = getStudioJoinStatus;
|
|
6214
6426
|
exports.joinStudioGuest = joinStudioGuest;
|
|
6215
6427
|
exports.redeemStudioHost = redeemStudioHost;
|