Files
temetro/frontend/components/meetings/use-audio-level.ts
T
Khalid Abdi 913a217e1d feat(meetings): live invite, scheduling calendar, Discord-style redesign
- Live invite: ring a clinic member into a room (socket call:invite + a bell
  notification); the invitee gets a toast with a Join action. Notifications of
  type "meeting" deep-link to /messages/meetings?room=, which auto-joins.
- Scheduling: new scheduled_meetings table + /api/meetings/events (list mine,
  create, delete); a Calendar tab on the Meetings page with a month picker
  (meeting-day dots), the day's agenda, and a Schedule-meeting dialog
  (title/date/time/participants).
- Redesign: rounded control bar with tooltips (mic/cam/screen/invite + separated
  red Leave), speaking ring on tiles (Web Audio), and live room-occupancy counts
  via call:presence broadcasts. Migration 0025.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:07:41 +03:00

47 lines
1.4 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
// Returns true while the given stream's audio is above a speaking threshold —
// used to draw a Discord-style "speaking" ring around a participant's tile.
// Degrades silently if the Web Audio API isn't available.
export function useSpeaking(stream: MediaStream | null): boolean {
const [speaking, setSpeaking] = useState(false);
useEffect(() => {
if (!stream || stream.getAudioTracks().length === 0) {
setSpeaking(false);
return;
}
let ctx: AudioContext | null = null;
let raf = 0;
try {
ctx = new AudioContext();
const source = ctx.createMediaStreamSource(stream);
const analyser = ctx.createAnalyser();
analyser.fftSize = 512;
source.connect(analyser);
const data = new Uint8Array(analyser.frequencyBinCount);
const tick = () => {
analyser.getByteTimeDomainData(data);
let sum = 0;
for (let i = 0; i < data.length; i++) {
const v = (data[i]! - 128) / 128;
sum += v * v;
}
setSpeaking(Math.sqrt(sum / data.length) > 0.045);
raf = requestAnimationFrame(tick);
};
tick();
} catch {
/* no Web Audio — no ring */
}
return () => {
cancelAnimationFrame(raf);
ctx?.close().catch(() => {});
};
}, [stream]);
return speaking;
}