@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.d.cts
CHANGED
|
@@ -196,6 +196,7 @@ type StudioMeetingHandle = {
|
|
|
196
196
|
moderatorIds: () => string[];
|
|
197
197
|
localAudioTrack: () => MediaStreamTrack | null;
|
|
198
198
|
localVideoTrack: () => MediaStreamTrack | null;
|
|
199
|
+
participantAudioTrack: (participantId: string) => MediaStreamTrack | null;
|
|
199
200
|
participantVideoTrack: (participantId: string) => MediaStreamTrack | null;
|
|
200
201
|
participantScreenTrack: (participantId: string) => MediaStreamTrack | null;
|
|
201
202
|
activeSpeakerId: () => string | null;
|
package/dist/studio/index.d.ts
CHANGED
|
@@ -196,6 +196,7 @@ type StudioMeetingHandle = {
|
|
|
196
196
|
moderatorIds: () => string[];
|
|
197
197
|
localAudioTrack: () => MediaStreamTrack | null;
|
|
198
198
|
localVideoTrack: () => MediaStreamTrack | null;
|
|
199
|
+
participantAudioTrack: (participantId: string) => MediaStreamTrack | null;
|
|
199
200
|
participantVideoTrack: (participantId: string) => MediaStreamTrack | null;
|
|
200
201
|
participantScreenTrack: (participantId: string) => MediaStreamTrack | null;
|
|
201
202
|
activeSpeakerId: () => string | null;
|
package/dist/studio/index.js
CHANGED
|
@@ -1250,15 +1250,15 @@ function AudioWaveform({
|
|
|
1250
1250
|
if (!canvas || !track || reducedMotion) return;
|
|
1251
1251
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
1252
1252
|
if (!AudioCtx) return;
|
|
1253
|
-
const
|
|
1254
|
-
const source =
|
|
1255
|
-
const analyser =
|
|
1253
|
+
const ctx3 = new AudioCtx();
|
|
1254
|
+
const source = ctx3.createMediaStreamSource(new MediaStream([track]));
|
|
1255
|
+
const analyser = ctx3.createAnalyser();
|
|
1256
1256
|
analyser.fftSize = 64;
|
|
1257
1257
|
source.connect(analyser);
|
|
1258
1258
|
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
1259
1259
|
const graphic = canvas.getContext("2d");
|
|
1260
1260
|
const surface = canvas;
|
|
1261
|
-
let
|
|
1261
|
+
let frame2 = 0;
|
|
1262
1262
|
function draw() {
|
|
1263
1263
|
if (!graphic) return;
|
|
1264
1264
|
analyser.getByteFrequencyData(data);
|
|
@@ -1276,12 +1276,12 @@ function AudioWaveform({
|
|
|
1276
1276
|
barHeight
|
|
1277
1277
|
);
|
|
1278
1278
|
}
|
|
1279
|
-
|
|
1279
|
+
frame2 = requestAnimationFrame(draw);
|
|
1280
1280
|
}
|
|
1281
|
-
|
|
1281
|
+
frame2 = requestAnimationFrame(draw);
|
|
1282
1282
|
return () => {
|
|
1283
|
-
cancelAnimationFrame(
|
|
1284
|
-
void
|
|
1283
|
+
cancelAnimationFrame(frame2);
|
|
1284
|
+
void ctx3.close();
|
|
1285
1285
|
};
|
|
1286
1286
|
}, [track, reducedMotion]);
|
|
1287
1287
|
if (reducedMotion) {
|
|
@@ -1567,10 +1567,10 @@ async function playSpeakerTest(deviceId) {
|
|
|
1567
1567
|
if (!AudioCtx) {
|
|
1568
1568
|
throw new Error("AudioContext unavailable");
|
|
1569
1569
|
}
|
|
1570
|
-
const
|
|
1571
|
-
void
|
|
1572
|
-
const oscillator =
|
|
1573
|
-
const gain =
|
|
1570
|
+
const ctx3 = new AudioCtx();
|
|
1571
|
+
void ctx3.resume();
|
|
1572
|
+
const oscillator = ctx3.createOscillator();
|
|
1573
|
+
const gain = ctx3.createGain();
|
|
1574
1574
|
oscillator.type = "sine";
|
|
1575
1575
|
oscillator.frequency.value = TEST_HZ;
|
|
1576
1576
|
gain.gain.value = 0.18;
|
|
@@ -1580,7 +1580,7 @@ async function playSpeakerTest(deviceId) {
|
|
|
1580
1580
|
const probe = document.createElement("audio");
|
|
1581
1581
|
const canRoute = Boolean(deviceId) && typeof probe.setSinkId === "function";
|
|
1582
1582
|
if (canRoute && deviceId) {
|
|
1583
|
-
const destination =
|
|
1583
|
+
const destination = ctx3.createMediaStreamDestination();
|
|
1584
1584
|
gain.connect(destination);
|
|
1585
1585
|
audio = probe;
|
|
1586
1586
|
audio.autoplay = true;
|
|
@@ -1591,13 +1591,13 @@ async function playSpeakerTest(deviceId) {
|
|
|
1591
1591
|
await audio.play();
|
|
1592
1592
|
} catch {
|
|
1593
1593
|
gain.disconnect();
|
|
1594
|
-
gain.connect(
|
|
1594
|
+
gain.connect(ctx3.destination);
|
|
1595
1595
|
audio.srcObject = null;
|
|
1596
1596
|
audio.remove();
|
|
1597
1597
|
audio = null;
|
|
1598
1598
|
}
|
|
1599
1599
|
} else {
|
|
1600
|
-
gain.connect(
|
|
1600
|
+
gain.connect(ctx3.destination);
|
|
1601
1601
|
}
|
|
1602
1602
|
oscillator.start();
|
|
1603
1603
|
await new Promise((resolve) => {
|
|
@@ -1609,7 +1609,7 @@ async function playSpeakerTest(deviceId) {
|
|
|
1609
1609
|
audio.srcObject = null;
|
|
1610
1610
|
audio.remove();
|
|
1611
1611
|
}
|
|
1612
|
-
await
|
|
1612
|
+
await ctx3.close();
|
|
1613
1613
|
}
|
|
1614
1614
|
}
|
|
1615
1615
|
function StudioDeviceSelectGroup({
|
|
@@ -2392,7 +2392,7 @@ function StudioChatPanel({
|
|
|
2392
2392
|
isModerator: isModerator2,
|
|
2393
2393
|
labels
|
|
2394
2394
|
}) {
|
|
2395
|
-
const [
|
|
2395
|
+
const [tick2, setTick] = useState(0);
|
|
2396
2396
|
const [draft, setDraft] = useState("");
|
|
2397
2397
|
const [editingId, setEditingId] = useState(null);
|
|
2398
2398
|
const listRef = useRef(null);
|
|
@@ -2402,9 +2402,9 @@ function StudioChatPanel({
|
|
|
2402
2402
|
);
|
|
2403
2403
|
const messages = useMemo(
|
|
2404
2404
|
() => meeting.chatMessages().filter((row) => !row.targetUserIds?.length),
|
|
2405
|
-
[meeting,
|
|
2405
|
+
[meeting, tick2]
|
|
2406
2406
|
);
|
|
2407
|
-
const pinned = useMemo(() => meeting.pinnedChatMessages(), [meeting,
|
|
2407
|
+
const pinned = useMemo(() => meeting.pinnedChatMessages(), [meeting, tick2]);
|
|
2408
2408
|
const limit = meeting.chatMaxTextLimit();
|
|
2409
2409
|
const pictures = useMemo(() => {
|
|
2410
2410
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -2904,6 +2904,171 @@ function StudioSidePanel({
|
|
|
2904
2904
|
] })
|
|
2905
2905
|
] });
|
|
2906
2906
|
}
|
|
2907
|
+
|
|
2908
|
+
// src/studio/lib/audio-levels.ts
|
|
2909
|
+
var LEVEL_SENSITIVITY = 4;
|
|
2910
|
+
var LEVEL_ATTACK = 0.55;
|
|
2911
|
+
var LEVEL_DECAY = 0.12;
|
|
2912
|
+
var SPEAKING_ON = 0.12;
|
|
2913
|
+
var SPEAKING_OFF = 0.06;
|
|
2914
|
+
var SPEAKING_HOLD_MS = 180;
|
|
2915
|
+
var tracks = /* @__PURE__ */ new Map();
|
|
2916
|
+
var ctx2 = null;
|
|
2917
|
+
var frame = 0;
|
|
2918
|
+
var tickRunning = false;
|
|
2919
|
+
function audioContextCtor2() {
|
|
2920
|
+
if (typeof window === "undefined") return null;
|
|
2921
|
+
return window.AudioContext || window.webkitAudioContext || null;
|
|
2922
|
+
}
|
|
2923
|
+
function ensureContext2() {
|
|
2924
|
+
const AudioCtx = audioContextCtor2();
|
|
2925
|
+
if (!AudioCtx) return null;
|
|
2926
|
+
if (!ctx2 || ctx2.state === "closed") {
|
|
2927
|
+
ctx2 = new AudioCtx();
|
|
2928
|
+
}
|
|
2929
|
+
return ctx2;
|
|
2930
|
+
}
|
|
2931
|
+
function rmsFromTimeDomain(data) {
|
|
2932
|
+
if (data.length === 0) return 0;
|
|
2933
|
+
let sum = 0;
|
|
2934
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
2935
|
+
const centered = ((data[i] ?? 128) - 128) / 128;
|
|
2936
|
+
sum += centered * centered;
|
|
2937
|
+
}
|
|
2938
|
+
return Math.sqrt(sum / data.length);
|
|
2939
|
+
}
|
|
2940
|
+
function normalizeAudioLevel(rms) {
|
|
2941
|
+
return Math.min(1, Math.max(0, rms * LEVEL_SENSITIVITY));
|
|
2942
|
+
}
|
|
2943
|
+
function smoothAudioLevel(previous, raw) {
|
|
2944
|
+
const factor = raw > previous ? LEVEL_ATTACK : LEVEL_DECAY;
|
|
2945
|
+
return previous + (raw - previous) * factor;
|
|
2946
|
+
}
|
|
2947
|
+
function nextSpeakingGate(gate, level, now) {
|
|
2948
|
+
if (!gate.speaking) {
|
|
2949
|
+
if (level >= SPEAKING_ON) {
|
|
2950
|
+
return { speaking: true, quietSince: null };
|
|
2951
|
+
}
|
|
2952
|
+
return { speaking: false, quietSince: null };
|
|
2953
|
+
}
|
|
2954
|
+
if (level >= SPEAKING_OFF) {
|
|
2955
|
+
return { speaking: true, quietSince: null };
|
|
2956
|
+
}
|
|
2957
|
+
const quietSince = gate.quietSince ?? now;
|
|
2958
|
+
if (now - quietSince >= SPEAKING_HOLD_MS) {
|
|
2959
|
+
return { speaking: false, quietSince: null };
|
|
2960
|
+
}
|
|
2961
|
+
return { speaking: true, quietSince };
|
|
2962
|
+
}
|
|
2963
|
+
function tick() {
|
|
2964
|
+
for (const entry of tracks.values()) {
|
|
2965
|
+
entry.analyser.getByteTimeDomainData(entry.data);
|
|
2966
|
+
const raw = normalizeAudioLevel(rmsFromTimeDomain(entry.data));
|
|
2967
|
+
entry.smoothed = smoothAudioLevel(entry.smoothed, raw);
|
|
2968
|
+
for (const cb of entry.listeners) {
|
|
2969
|
+
cb(entry.smoothed);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
if (tracks.size > 0) {
|
|
2973
|
+
frame = requestAnimationFrame(tick);
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
tickRunning = false;
|
|
2977
|
+
frame = 0;
|
|
2978
|
+
}
|
|
2979
|
+
function startTick() {
|
|
2980
|
+
if (tickRunning) return;
|
|
2981
|
+
tickRunning = true;
|
|
2982
|
+
frame = requestAnimationFrame(tick);
|
|
2983
|
+
}
|
|
2984
|
+
function stopTickIfIdle() {
|
|
2985
|
+
if (tracks.size > 0 || !frame) return;
|
|
2986
|
+
cancelAnimationFrame(frame);
|
|
2987
|
+
tickRunning = false;
|
|
2988
|
+
frame = 0;
|
|
2989
|
+
}
|
|
2990
|
+
function subscribeStudioAudioLevel(track, cb) {
|
|
2991
|
+
const context = ensureContext2();
|
|
2992
|
+
if (!context) return () => void 0;
|
|
2993
|
+
void context.resume();
|
|
2994
|
+
const id = track.id;
|
|
2995
|
+
let entry = tracks.get(id);
|
|
2996
|
+
if (!entry) {
|
|
2997
|
+
const source = context.createMediaStreamSource(new MediaStream([track]));
|
|
2998
|
+
const analyser = context.createAnalyser();
|
|
2999
|
+
analyser.fftSize = 512;
|
|
3000
|
+
analyser.smoothingTimeConstant = 0;
|
|
3001
|
+
source.connect(analyser);
|
|
3002
|
+
entry = {
|
|
3003
|
+
source,
|
|
3004
|
+
analyser,
|
|
3005
|
+
data: new Uint8Array(analyser.fftSize),
|
|
3006
|
+
refs: 0,
|
|
3007
|
+
smoothed: 0,
|
|
3008
|
+
listeners: /* @__PURE__ */ new Set()
|
|
3009
|
+
};
|
|
3010
|
+
tracks.set(id, entry);
|
|
3011
|
+
}
|
|
3012
|
+
entry.refs += 1;
|
|
3013
|
+
entry.listeners.add(cb);
|
|
3014
|
+
startTick();
|
|
3015
|
+
return () => {
|
|
3016
|
+
const current = tracks.get(id);
|
|
3017
|
+
if (!current) return;
|
|
3018
|
+
current.listeners.delete(cb);
|
|
3019
|
+
current.refs -= 1;
|
|
3020
|
+
if (current.refs <= 0) {
|
|
3021
|
+
current.source.disconnect();
|
|
3022
|
+
current.analyser.disconnect();
|
|
3023
|
+
tracks.delete(id);
|
|
3024
|
+
}
|
|
3025
|
+
stopTickIfIdle();
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// src/studio/hooks/use-speaking-glow.ts
|
|
3030
|
+
function clearSpeakingGlow(el) {
|
|
3031
|
+
el.style.removeProperty("--livo-speaking-level");
|
|
3032
|
+
el.removeAttribute("data-speaking");
|
|
3033
|
+
}
|
|
3034
|
+
function useSpeakingGlow({
|
|
3035
|
+
track,
|
|
3036
|
+
enabled,
|
|
3037
|
+
reducedMotion
|
|
3038
|
+
}) {
|
|
3039
|
+
const [el, setEl] = useState(null);
|
|
3040
|
+
const gateRef = useRef({ speaking: false, quietSince: null });
|
|
3041
|
+
const setRef = useCallback((node) => {
|
|
3042
|
+
setEl(node);
|
|
3043
|
+
}, []);
|
|
3044
|
+
useEffect(() => {
|
|
3045
|
+
if (!el || !enabled || !track) {
|
|
3046
|
+
if (el) clearSpeakingGlow(el);
|
|
3047
|
+
gateRef.current = { speaking: false, quietSince: null };
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
const unsubscribe = subscribeStudioAudioLevel(track, (level) => {
|
|
3051
|
+
const node = el;
|
|
3052
|
+
gateRef.current = nextSpeakingGate(gateRef.current, level, Date.now());
|
|
3053
|
+
if (gateRef.current.speaking) {
|
|
3054
|
+
node.dataset.speaking = "true";
|
|
3055
|
+
if (!reducedMotion) {
|
|
3056
|
+
node.style.setProperty("--livo-speaking-level", String(level));
|
|
3057
|
+
} else {
|
|
3058
|
+
node.style.removeProperty("--livo-speaking-level");
|
|
3059
|
+
}
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
clearSpeakingGlow(node);
|
|
3063
|
+
});
|
|
3064
|
+
return () => {
|
|
3065
|
+
unsubscribe();
|
|
3066
|
+
clearSpeakingGlow(el);
|
|
3067
|
+
gateRef.current = { speaking: false, quietSince: null };
|
|
3068
|
+
};
|
|
3069
|
+
}, [el, enabled, reducedMotion, track]);
|
|
3070
|
+
return setRef;
|
|
3071
|
+
}
|
|
2907
3072
|
function DropdownMenu({ ...props }) {
|
|
2908
3073
|
return /* @__PURE__ */ jsx(Menu.Root, { "data-slot": "dropdown-menu", ...props });
|
|
2909
3074
|
}
|
|
@@ -3052,12 +3217,18 @@ function StudioTile({
|
|
|
3052
3217
|
const onStage = isOnStage(tile.stageStatus);
|
|
3053
3218
|
const [confirm, setConfirm] = useState(null);
|
|
3054
3219
|
const showMenu = (canModerate || canManageStage) && !tile.isSelf;
|
|
3220
|
+
const glowRef = useSpeakingGlow({
|
|
3221
|
+
track: tile.kind === "screen" || !tile.audioEnabled ? null : meeting.participantAudioTrack(tile.id),
|
|
3222
|
+
enabled: onStage,
|
|
3223
|
+
reducedMotion
|
|
3224
|
+
});
|
|
3055
3225
|
return /* @__PURE__ */ jsxs(
|
|
3056
3226
|
"article",
|
|
3057
3227
|
{
|
|
3228
|
+
ref: glowRef,
|
|
3058
3229
|
"aria-label": tile.name,
|
|
3059
3230
|
className: cn(
|
|
3060
|
-
"bg-muted relative overflow-hidden rounded-xl",
|
|
3231
|
+
"livo-studio-tile bg-muted relative overflow-hidden rounded-xl",
|
|
3061
3232
|
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"
|
|
3062
3233
|
),
|
|
3063
3234
|
children: [
|
|
@@ -3087,6 +3258,13 @@ function StudioTile({
|
|
|
3087
3258
|
contain: tile.kind === "screen"
|
|
3088
3259
|
}
|
|
3089
3260
|
),
|
|
3261
|
+
/* @__PURE__ */ jsx(
|
|
3262
|
+
"div",
|
|
3263
|
+
{
|
|
3264
|
+
"aria-hidden": true,
|
|
3265
|
+
className: "livo-studio-tile-glow pointer-events-none absolute inset-0 z-20 rounded-xl"
|
|
3266
|
+
}
|
|
3267
|
+
),
|
|
3090
3268
|
/* @__PURE__ */ 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: [
|
|
3091
3269
|
/* @__PURE__ */ jsxs("p", { className: "min-w-0 truncate text-xs font-medium text-white", children: [
|
|
3092
3270
|
tile.name,
|
|
@@ -3229,9 +3407,7 @@ function StudioStage({
|
|
|
3229
3407
|
onOpenPeople
|
|
3230
3408
|
}) {
|
|
3231
3409
|
const displayTiles = expandStudioTiles(participants);
|
|
3232
|
-
const stage = studioStageLayout(displayTiles
|
|
3233
|
-
activeSpeakerId: meeting.activeSpeakerId()
|
|
3234
|
-
});
|
|
3410
|
+
const stage = studioStageLayout(displayTiles);
|
|
3235
3411
|
const sharing = stage.spotlight.length > 0;
|
|
3236
3412
|
const overflowLabel = labels.stageOverflow.replace(
|
|
3237
3413
|
"{count}",
|
|
@@ -3564,7 +3740,7 @@ function StudioRoom({
|
|
|
3564
3740
|
const [screenRequestPending, setScreenRequestPending] = useState(false);
|
|
3565
3741
|
const [pending, setPending] = useState(false);
|
|
3566
3742
|
const [publishing, setPublishing] = useState(false);
|
|
3567
|
-
const [
|
|
3743
|
+
const [tick2, setTick] = useState(0);
|
|
3568
3744
|
const [leaveState, setLeaveState] = useState(null);
|
|
3569
3745
|
const [sideTab, setSideTab] = useState(null);
|
|
3570
3746
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
@@ -3808,7 +3984,7 @@ function StudioRoom({
|
|
|
3808
3984
|
});
|
|
3809
3985
|
}, [meeting]);
|
|
3810
3986
|
const sawParticipants = useRef(false);
|
|
3811
|
-
const participants = useMemo(() => meeting.participants(), [meeting,
|
|
3987
|
+
const participants = useMemo(() => meeting.participants(), [meeting, tick2]);
|
|
3812
3988
|
useEffect(() => {
|
|
3813
3989
|
const other = duplicateSelfParticipant(participants);
|
|
3814
3990
|
if (!other) return;
|
|
@@ -3823,22 +3999,22 @@ function StudioRoom({
|
|
|
3823
3999
|
() => participants.filter((row) => isOnStage(row.stageStatus)),
|
|
3824
4000
|
[participants]
|
|
3825
4001
|
);
|
|
3826
|
-
const waitlisted = useMemo(() => meeting.waitlisted(), [meeting,
|
|
4002
|
+
const waitlisted = useMemo(() => meeting.waitlisted(), [meeting, tick2]);
|
|
3827
4003
|
const connectionState = useMemo(
|
|
3828
4004
|
() => meeting.connectionState(),
|
|
3829
|
-
[meeting,
|
|
4005
|
+
[meeting, tick2]
|
|
3830
4006
|
);
|
|
3831
4007
|
const lastMeetingError = useMemo(
|
|
3832
4008
|
() => meeting.lastMeetingError(),
|
|
3833
|
-
[meeting,
|
|
4009
|
+
[meeting, tick2]
|
|
3834
4010
|
);
|
|
3835
4011
|
const canManageStage = meeting.canManageStage();
|
|
3836
4012
|
const stageEnabled = meeting.stageEnabled();
|
|
3837
4013
|
const stageRequests = useMemo(
|
|
3838
4014
|
() => canManageStage ? meeting.stageRequests() : [],
|
|
3839
|
-
[canManageStage, meeting,
|
|
4015
|
+
[canManageStage, meeting, tick2]
|
|
3840
4016
|
);
|
|
3841
|
-
const chatMessages = useMemo(() => meeting.chatMessages(), [meeting,
|
|
4017
|
+
const chatMessages = useMemo(() => meeting.chatMessages(), [meeting, tick2]);
|
|
3842
4018
|
const inWaitingRoom = meeting.inWaitingRoom();
|
|
3843
4019
|
const screenAvailable = canStartScreenShare(participants);
|
|
3844
4020
|
const durableScreenBlock = screenShareBlock(
|
|
@@ -5757,6 +5933,9 @@ function studioMeetingFromRtk(raw) {
|
|
|
5757
5933
|
localVideoTrack() {
|
|
5758
5934
|
return meeting.self?.videoTrack ?? null;
|
|
5759
5935
|
},
|
|
5936
|
+
participantAudioTrack(participantId) {
|
|
5937
|
+
return participantFor(participantId)?.audioTrack ?? null;
|
|
5938
|
+
},
|
|
5760
5939
|
participantVideoTrack(participantId) {
|
|
5761
5940
|
return participantFor(participantId)?.videoTrack ?? null;
|
|
5762
5941
|
},
|