@livo-tv/sdk 1.3.3 → 1.4.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 +209 -30
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.cts +1 -0
- package/dist/studio/index.d.ts +1 -0
- package/dist/studio/index.js +209 -30
- package/dist/studio/index.js.map +1 -1
- package/dist/styles.css +18 -0
- package/package.json +1 -1
package/dist/studio/index.cjs
CHANGED
|
@@ -1274,15 +1274,15 @@ function AudioWaveform({
|
|
|
1274
1274
|
if (!canvas || !track || reducedMotion) return;
|
|
1275
1275
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
1276
1276
|
if (!AudioCtx) return;
|
|
1277
|
-
const
|
|
1278
|
-
const source =
|
|
1279
|
-
const analyser =
|
|
1277
|
+
const ctx3 = new AudioCtx();
|
|
1278
|
+
const source = ctx3.createMediaStreamSource(new MediaStream([track]));
|
|
1279
|
+
const analyser = ctx3.createAnalyser();
|
|
1280
1280
|
analyser.fftSize = 64;
|
|
1281
1281
|
source.connect(analyser);
|
|
1282
1282
|
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
1283
1283
|
const graphic = canvas.getContext("2d");
|
|
1284
1284
|
const surface = canvas;
|
|
1285
|
-
let
|
|
1285
|
+
let frame2 = 0;
|
|
1286
1286
|
function draw() {
|
|
1287
1287
|
if (!graphic) return;
|
|
1288
1288
|
analyser.getByteFrequencyData(data);
|
|
@@ -1300,12 +1300,12 @@ function AudioWaveform({
|
|
|
1300
1300
|
barHeight
|
|
1301
1301
|
);
|
|
1302
1302
|
}
|
|
1303
|
-
|
|
1303
|
+
frame2 = requestAnimationFrame(draw);
|
|
1304
1304
|
}
|
|
1305
|
-
|
|
1305
|
+
frame2 = requestAnimationFrame(draw);
|
|
1306
1306
|
return () => {
|
|
1307
|
-
cancelAnimationFrame(
|
|
1308
|
-
void
|
|
1307
|
+
cancelAnimationFrame(frame2);
|
|
1308
|
+
void ctx3.close();
|
|
1309
1309
|
};
|
|
1310
1310
|
}, [track, reducedMotion]);
|
|
1311
1311
|
if (reducedMotion) {
|
|
@@ -1591,10 +1591,10 @@ async function playSpeakerTest(deviceId) {
|
|
|
1591
1591
|
if (!AudioCtx) {
|
|
1592
1592
|
throw new Error("AudioContext unavailable");
|
|
1593
1593
|
}
|
|
1594
|
-
const
|
|
1595
|
-
void
|
|
1596
|
-
const oscillator =
|
|
1597
|
-
const gain =
|
|
1594
|
+
const ctx3 = new AudioCtx();
|
|
1595
|
+
void ctx3.resume();
|
|
1596
|
+
const oscillator = ctx3.createOscillator();
|
|
1597
|
+
const gain = ctx3.createGain();
|
|
1598
1598
|
oscillator.type = "sine";
|
|
1599
1599
|
oscillator.frequency.value = TEST_HZ;
|
|
1600
1600
|
gain.gain.value = 0.18;
|
|
@@ -1604,7 +1604,7 @@ async function playSpeakerTest(deviceId) {
|
|
|
1604
1604
|
const probe = document.createElement("audio");
|
|
1605
1605
|
const canRoute = Boolean(deviceId) && typeof probe.setSinkId === "function";
|
|
1606
1606
|
if (canRoute && deviceId) {
|
|
1607
|
-
const destination =
|
|
1607
|
+
const destination = ctx3.createMediaStreamDestination();
|
|
1608
1608
|
gain.connect(destination);
|
|
1609
1609
|
audio = probe;
|
|
1610
1610
|
audio.autoplay = true;
|
|
@@ -1615,13 +1615,13 @@ async function playSpeakerTest(deviceId) {
|
|
|
1615
1615
|
await audio.play();
|
|
1616
1616
|
} catch {
|
|
1617
1617
|
gain.disconnect();
|
|
1618
|
-
gain.connect(
|
|
1618
|
+
gain.connect(ctx3.destination);
|
|
1619
1619
|
audio.srcObject = null;
|
|
1620
1620
|
audio.remove();
|
|
1621
1621
|
audio = null;
|
|
1622
1622
|
}
|
|
1623
1623
|
} else {
|
|
1624
|
-
gain.connect(
|
|
1624
|
+
gain.connect(ctx3.destination);
|
|
1625
1625
|
}
|
|
1626
1626
|
oscillator.start();
|
|
1627
1627
|
await new Promise((resolve) => {
|
|
@@ -1633,7 +1633,7 @@ async function playSpeakerTest(deviceId) {
|
|
|
1633
1633
|
audio.srcObject = null;
|
|
1634
1634
|
audio.remove();
|
|
1635
1635
|
}
|
|
1636
|
-
await
|
|
1636
|
+
await ctx3.close();
|
|
1637
1637
|
}
|
|
1638
1638
|
}
|
|
1639
1639
|
function StudioDeviceSelectGroup({
|
|
@@ -2416,7 +2416,7 @@ function StudioChatPanel({
|
|
|
2416
2416
|
isModerator: isModerator2,
|
|
2417
2417
|
labels
|
|
2418
2418
|
}) {
|
|
2419
|
-
const [
|
|
2419
|
+
const [tick2, setTick] = react.useState(0);
|
|
2420
2420
|
const [draft, setDraft] = react.useState("");
|
|
2421
2421
|
const [editingId, setEditingId] = react.useState(null);
|
|
2422
2422
|
const listRef = react.useRef(null);
|
|
@@ -2426,9 +2426,9 @@ function StudioChatPanel({
|
|
|
2426
2426
|
);
|
|
2427
2427
|
const messages = react.useMemo(
|
|
2428
2428
|
() => meeting.chatMessages().filter((row) => !row.targetUserIds?.length),
|
|
2429
|
-
[meeting,
|
|
2429
|
+
[meeting, tick2]
|
|
2430
2430
|
);
|
|
2431
|
-
const pinned = react.useMemo(() => meeting.pinnedChatMessages(), [meeting,
|
|
2431
|
+
const pinned = react.useMemo(() => meeting.pinnedChatMessages(), [meeting, tick2]);
|
|
2432
2432
|
const limit = meeting.chatMaxTextLimit();
|
|
2433
2433
|
const pictures = react.useMemo(() => {
|
|
2434
2434
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -2928,6 +2928,171 @@ function StudioSidePanel({
|
|
|
2928
2928
|
] })
|
|
2929
2929
|
] });
|
|
2930
2930
|
}
|
|
2931
|
+
|
|
2932
|
+
// src/studio/lib/audio-levels.ts
|
|
2933
|
+
var LEVEL_SENSITIVITY = 4;
|
|
2934
|
+
var LEVEL_ATTACK = 0.55;
|
|
2935
|
+
var LEVEL_DECAY = 0.12;
|
|
2936
|
+
var SPEAKING_ON = 0.12;
|
|
2937
|
+
var SPEAKING_OFF = 0.06;
|
|
2938
|
+
var SPEAKING_HOLD_MS = 180;
|
|
2939
|
+
var tracks = /* @__PURE__ */ new Map();
|
|
2940
|
+
var ctx2 = null;
|
|
2941
|
+
var frame = 0;
|
|
2942
|
+
var tickRunning = false;
|
|
2943
|
+
function audioContextCtor2() {
|
|
2944
|
+
if (typeof window === "undefined") return null;
|
|
2945
|
+
return window.AudioContext || window.webkitAudioContext || null;
|
|
2946
|
+
}
|
|
2947
|
+
function ensureContext2() {
|
|
2948
|
+
const AudioCtx = audioContextCtor2();
|
|
2949
|
+
if (!AudioCtx) return null;
|
|
2950
|
+
if (!ctx2 || ctx2.state === "closed") {
|
|
2951
|
+
ctx2 = new AudioCtx();
|
|
2952
|
+
}
|
|
2953
|
+
return ctx2;
|
|
2954
|
+
}
|
|
2955
|
+
function rmsFromTimeDomain(data) {
|
|
2956
|
+
if (data.length === 0) return 0;
|
|
2957
|
+
let sum = 0;
|
|
2958
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
2959
|
+
const centered = ((data[i] ?? 128) - 128) / 128;
|
|
2960
|
+
sum += centered * centered;
|
|
2961
|
+
}
|
|
2962
|
+
return Math.sqrt(sum / data.length);
|
|
2963
|
+
}
|
|
2964
|
+
function normalizeAudioLevel(rms) {
|
|
2965
|
+
return Math.min(1, Math.max(0, rms * LEVEL_SENSITIVITY));
|
|
2966
|
+
}
|
|
2967
|
+
function smoothAudioLevel(previous, raw) {
|
|
2968
|
+
const factor = raw > previous ? LEVEL_ATTACK : LEVEL_DECAY;
|
|
2969
|
+
return previous + (raw - previous) * factor;
|
|
2970
|
+
}
|
|
2971
|
+
function nextSpeakingGate(gate, level, now) {
|
|
2972
|
+
if (!gate.speaking) {
|
|
2973
|
+
if (level >= SPEAKING_ON) {
|
|
2974
|
+
return { speaking: true, quietSince: null };
|
|
2975
|
+
}
|
|
2976
|
+
return { speaking: false, quietSince: null };
|
|
2977
|
+
}
|
|
2978
|
+
if (level >= SPEAKING_OFF) {
|
|
2979
|
+
return { speaking: true, quietSince: null };
|
|
2980
|
+
}
|
|
2981
|
+
const quietSince = gate.quietSince ?? now;
|
|
2982
|
+
if (now - quietSince >= SPEAKING_HOLD_MS) {
|
|
2983
|
+
return { speaking: false, quietSince: null };
|
|
2984
|
+
}
|
|
2985
|
+
return { speaking: true, quietSince };
|
|
2986
|
+
}
|
|
2987
|
+
function tick() {
|
|
2988
|
+
for (const entry of tracks.values()) {
|
|
2989
|
+
entry.analyser.getByteTimeDomainData(entry.data);
|
|
2990
|
+
const raw = normalizeAudioLevel(rmsFromTimeDomain(entry.data));
|
|
2991
|
+
entry.smoothed = smoothAudioLevel(entry.smoothed, raw);
|
|
2992
|
+
for (const cb of entry.listeners) {
|
|
2993
|
+
cb(entry.smoothed);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
if (tracks.size > 0) {
|
|
2997
|
+
frame = requestAnimationFrame(tick);
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
tickRunning = false;
|
|
3001
|
+
frame = 0;
|
|
3002
|
+
}
|
|
3003
|
+
function startTick() {
|
|
3004
|
+
if (tickRunning) return;
|
|
3005
|
+
tickRunning = true;
|
|
3006
|
+
frame = requestAnimationFrame(tick);
|
|
3007
|
+
}
|
|
3008
|
+
function stopTickIfIdle() {
|
|
3009
|
+
if (tracks.size > 0 || !frame) return;
|
|
3010
|
+
cancelAnimationFrame(frame);
|
|
3011
|
+
tickRunning = false;
|
|
3012
|
+
frame = 0;
|
|
3013
|
+
}
|
|
3014
|
+
function subscribeStudioAudioLevel(track, cb) {
|
|
3015
|
+
const context = ensureContext2();
|
|
3016
|
+
if (!context) return () => void 0;
|
|
3017
|
+
void context.resume();
|
|
3018
|
+
const id = track.id;
|
|
3019
|
+
let entry = tracks.get(id);
|
|
3020
|
+
if (!entry) {
|
|
3021
|
+
const source = context.createMediaStreamSource(new MediaStream([track]));
|
|
3022
|
+
const analyser = context.createAnalyser();
|
|
3023
|
+
analyser.fftSize = 512;
|
|
3024
|
+
analyser.smoothingTimeConstant = 0;
|
|
3025
|
+
source.connect(analyser);
|
|
3026
|
+
entry = {
|
|
3027
|
+
source,
|
|
3028
|
+
analyser,
|
|
3029
|
+
data: new Uint8Array(analyser.fftSize),
|
|
3030
|
+
refs: 0,
|
|
3031
|
+
smoothed: 0,
|
|
3032
|
+
listeners: /* @__PURE__ */ new Set()
|
|
3033
|
+
};
|
|
3034
|
+
tracks.set(id, entry);
|
|
3035
|
+
}
|
|
3036
|
+
entry.refs += 1;
|
|
3037
|
+
entry.listeners.add(cb);
|
|
3038
|
+
startTick();
|
|
3039
|
+
return () => {
|
|
3040
|
+
const current = tracks.get(id);
|
|
3041
|
+
if (!current) return;
|
|
3042
|
+
current.listeners.delete(cb);
|
|
3043
|
+
current.refs -= 1;
|
|
3044
|
+
if (current.refs <= 0) {
|
|
3045
|
+
current.source.disconnect();
|
|
3046
|
+
current.analyser.disconnect();
|
|
3047
|
+
tracks.delete(id);
|
|
3048
|
+
}
|
|
3049
|
+
stopTickIfIdle();
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// src/studio/hooks/use-speaking-glow.ts
|
|
3054
|
+
function clearSpeakingGlow(el) {
|
|
3055
|
+
el.style.removeProperty("--livo-speaking-level");
|
|
3056
|
+
el.removeAttribute("data-speaking");
|
|
3057
|
+
}
|
|
3058
|
+
function useSpeakingGlow({
|
|
3059
|
+
track,
|
|
3060
|
+
enabled,
|
|
3061
|
+
reducedMotion
|
|
3062
|
+
}) {
|
|
3063
|
+
const [el, setEl] = react.useState(null);
|
|
3064
|
+
const gateRef = react.useRef({ speaking: false, quietSince: null });
|
|
3065
|
+
const setRef = react.useCallback((node) => {
|
|
3066
|
+
setEl(node);
|
|
3067
|
+
}, []);
|
|
3068
|
+
react.useEffect(() => {
|
|
3069
|
+
if (!el || !enabled || !track) {
|
|
3070
|
+
if (el) clearSpeakingGlow(el);
|
|
3071
|
+
gateRef.current = { speaking: false, quietSince: null };
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const unsubscribe = subscribeStudioAudioLevel(track, (level) => {
|
|
3075
|
+
const node = el;
|
|
3076
|
+
gateRef.current = nextSpeakingGate(gateRef.current, level, Date.now());
|
|
3077
|
+
if (gateRef.current.speaking) {
|
|
3078
|
+
node.dataset.speaking = "true";
|
|
3079
|
+
if (!reducedMotion) {
|
|
3080
|
+
node.style.setProperty("--livo-speaking-level", String(level));
|
|
3081
|
+
} else {
|
|
3082
|
+
node.style.removeProperty("--livo-speaking-level");
|
|
3083
|
+
}
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
clearSpeakingGlow(node);
|
|
3087
|
+
});
|
|
3088
|
+
return () => {
|
|
3089
|
+
unsubscribe();
|
|
3090
|
+
clearSpeakingGlow(el);
|
|
3091
|
+
gateRef.current = { speaking: false, quietSince: null };
|
|
3092
|
+
};
|
|
3093
|
+
}, [el, enabled, reducedMotion, track]);
|
|
3094
|
+
return setRef;
|
|
3095
|
+
}
|
|
2931
3096
|
function DropdownMenu({ ...props }) {
|
|
2932
3097
|
return /* @__PURE__ */ jsxRuntime.jsx(menu.Menu.Root, { "data-slot": "dropdown-menu", ...props });
|
|
2933
3098
|
}
|
|
@@ -3076,12 +3241,18 @@ function StudioTile({
|
|
|
3076
3241
|
const onStage = isOnStage(tile.stageStatus);
|
|
3077
3242
|
const [confirm, setConfirm] = react.useState(null);
|
|
3078
3243
|
const showMenu = (canModerate || canManageStage) && !tile.isSelf;
|
|
3244
|
+
const glowRef = useSpeakingGlow({
|
|
3245
|
+
track: tile.kind === "screen" || !tile.audioEnabled ? null : meeting.participantAudioTrack(tile.id),
|
|
3246
|
+
enabled: onStage,
|
|
3247
|
+
reducedMotion
|
|
3248
|
+
});
|
|
3079
3249
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3080
3250
|
"article",
|
|
3081
3251
|
{
|
|
3252
|
+
ref: glowRef,
|
|
3082
3253
|
"aria-label": tile.name,
|
|
3083
3254
|
className: cn(
|
|
3084
|
-
"bg-muted relative overflow-hidden rounded-xl",
|
|
3255
|
+
"livo-studio-tile bg-muted relative overflow-hidden rounded-xl",
|
|
3085
3256
|
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
3257
|
),
|
|
3087
3258
|
children: [
|
|
@@ -3111,6 +3282,13 @@ function StudioTile({
|
|
|
3111
3282
|
contain: tile.kind === "screen"
|
|
3112
3283
|
}
|
|
3113
3284
|
),
|
|
3285
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3286
|
+
"div",
|
|
3287
|
+
{
|
|
3288
|
+
"aria-hidden": true,
|
|
3289
|
+
className: "livo-studio-tile-glow pointer-events-none absolute inset-0 z-20 rounded-xl"
|
|
3290
|
+
}
|
|
3291
|
+
),
|
|
3114
3292
|
/* @__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
3293
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "min-w-0 truncate text-xs font-medium text-white", children: [
|
|
3116
3294
|
tile.name,
|
|
@@ -3253,9 +3431,7 @@ function StudioStage({
|
|
|
3253
3431
|
onOpenPeople
|
|
3254
3432
|
}) {
|
|
3255
3433
|
const displayTiles = expandStudioTiles(participants);
|
|
3256
|
-
const stage = studioStageLayout(displayTiles
|
|
3257
|
-
activeSpeakerId: meeting.activeSpeakerId()
|
|
3258
|
-
});
|
|
3434
|
+
const stage = studioStageLayout(displayTiles);
|
|
3259
3435
|
const sharing = stage.spotlight.length > 0;
|
|
3260
3436
|
const overflowLabel = labels.stageOverflow.replace(
|
|
3261
3437
|
"{count}",
|
|
@@ -3588,7 +3764,7 @@ function StudioRoom({
|
|
|
3588
3764
|
const [screenRequestPending, setScreenRequestPending] = react.useState(false);
|
|
3589
3765
|
const [pending, setPending] = react.useState(false);
|
|
3590
3766
|
const [publishing, setPublishing] = react.useState(false);
|
|
3591
|
-
const [
|
|
3767
|
+
const [tick2, setTick] = react.useState(0);
|
|
3592
3768
|
const [leaveState, setLeaveState] = react.useState(null);
|
|
3593
3769
|
const [sideTab, setSideTab] = react.useState(null);
|
|
3594
3770
|
const [settingsOpen, setSettingsOpen] = react.useState(false);
|
|
@@ -3832,7 +4008,7 @@ function StudioRoom({
|
|
|
3832
4008
|
});
|
|
3833
4009
|
}, [meeting]);
|
|
3834
4010
|
const sawParticipants = react.useRef(false);
|
|
3835
|
-
const participants = react.useMemo(() => meeting.participants(), [meeting,
|
|
4011
|
+
const participants = react.useMemo(() => meeting.participants(), [meeting, tick2]);
|
|
3836
4012
|
react.useEffect(() => {
|
|
3837
4013
|
const other = duplicateSelfParticipant(participants);
|
|
3838
4014
|
if (!other) return;
|
|
@@ -3847,22 +4023,22 @@ function StudioRoom({
|
|
|
3847
4023
|
() => participants.filter((row) => isOnStage(row.stageStatus)),
|
|
3848
4024
|
[participants]
|
|
3849
4025
|
);
|
|
3850
|
-
const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting,
|
|
4026
|
+
const waitlisted = react.useMemo(() => meeting.waitlisted(), [meeting, tick2]);
|
|
3851
4027
|
const connectionState = react.useMemo(
|
|
3852
4028
|
() => meeting.connectionState(),
|
|
3853
|
-
[meeting,
|
|
4029
|
+
[meeting, tick2]
|
|
3854
4030
|
);
|
|
3855
4031
|
const lastMeetingError = react.useMemo(
|
|
3856
4032
|
() => meeting.lastMeetingError(),
|
|
3857
|
-
[meeting,
|
|
4033
|
+
[meeting, tick2]
|
|
3858
4034
|
);
|
|
3859
4035
|
const canManageStage = meeting.canManageStage();
|
|
3860
4036
|
const stageEnabled = meeting.stageEnabled();
|
|
3861
4037
|
const stageRequests = react.useMemo(
|
|
3862
4038
|
() => canManageStage ? meeting.stageRequests() : [],
|
|
3863
|
-
[canManageStage, meeting,
|
|
4039
|
+
[canManageStage, meeting, tick2]
|
|
3864
4040
|
);
|
|
3865
|
-
const chatMessages = react.useMemo(() => meeting.chatMessages(), [meeting,
|
|
4041
|
+
const chatMessages = react.useMemo(() => meeting.chatMessages(), [meeting, tick2]);
|
|
3866
4042
|
const inWaitingRoom = meeting.inWaitingRoom();
|
|
3867
4043
|
const screenAvailable = canStartScreenShare(participants);
|
|
3868
4044
|
const durableScreenBlock = screenShareBlock(
|
|
@@ -5781,6 +5957,9 @@ function studioMeetingFromRtk(raw) {
|
|
|
5781
5957
|
localVideoTrack() {
|
|
5782
5958
|
return meeting.self?.videoTrack ?? null;
|
|
5783
5959
|
},
|
|
5960
|
+
participantAudioTrack(participantId) {
|
|
5961
|
+
return participantFor(participantId)?.audioTrack ?? null;
|
|
5962
|
+
},
|
|
5784
5963
|
participantVideoTrack(participantId) {
|
|
5785
5964
|
return participantFor(participantId)?.videoTrack ?? null;
|
|
5786
5965
|
},
|