feat(frontend): live room view, beat connectivity test, session resume
- PodView now shows LIVE LiveKit participants (updates as people join/leave, active-speaker highlight) instead of the static roster — no refresh needed. - "Play beat" button publishes a generated 4/4 beat into the room so every participant hears the same audio (speaker lights up) — a real connectivity test. - "Share my screen" is now a deliberate button; joining only connects to the room (fast/reliable), so a denied/slow screen prompt no longer fails the join. - Session persisted in sessionStorage with auto-reconnect, so a refresh keeps you in the room instead of dropping back to the pod list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
export interface BeatHandle {
|
||||
track: MediaStreamTrack;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a simple 4-on-the-floor beat (kick + hi-hat) as an audio
|
||||
* MediaStreamTrack to publish into a LiveKit room. Also routes to the local
|
||||
* speakers so the publisher hears it too. Pure Web Audio — no asset/CORS.
|
||||
*/
|
||||
export function startBeat(): BeatHandle {
|
||||
const ctx = new AudioContext();
|
||||
void ctx.resume();
|
||||
const dest = ctx.createMediaStreamDestination();
|
||||
const master = ctx.createGain();
|
||||
master.gain.value = 0.5;
|
||||
master.connect(dest); // -> published track (remote listeners)
|
||||
master.connect(ctx.destination); // -> local speakers (publisher)
|
||||
|
||||
const bpm = 120;
|
||||
const spb = 60 / bpm;
|
||||
let next = ctx.currentTime + 0.1;
|
||||
let beat = 0;
|
||||
|
||||
function kick(time: number, accent: boolean) {
|
||||
const osc = ctx.createOscillator();
|
||||
const g = ctx.createGain();
|
||||
osc.frequency.setValueAtTime(accent ? 180 : 150, time);
|
||||
osc.frequency.exponentialRampToValueAtTime(50, time + 0.12);
|
||||
g.gain.setValueAtTime(0.0001, time);
|
||||
g.gain.exponentialRampToValueAtTime(accent ? 1 : 0.7, time + 0.005);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, time + 0.18);
|
||||
osc.connect(g);
|
||||
g.connect(master);
|
||||
osc.start(time);
|
||||
osc.stop(time + 0.2);
|
||||
}
|
||||
|
||||
function hat(time: number) {
|
||||
const size = Math.floor(ctx.sampleRate * 0.05);
|
||||
const buffer = ctx.createBuffer(1, size, ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < size; i++) data[i] = Math.random() * 2 - 1;
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
const hp = ctx.createBiquadFilter();
|
||||
hp.type = 'highpass';
|
||||
hp.frequency.value = 7000;
|
||||
const g = ctx.createGain();
|
||||
g.gain.setValueAtTime(0.12, time);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, time + 0.05);
|
||||
noise.connect(hp);
|
||||
hp.connect(g);
|
||||
g.connect(master);
|
||||
noise.start(time);
|
||||
noise.stop(time + 0.05);
|
||||
}
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
while (next < ctx.currentTime + 0.2) {
|
||||
kick(next, beat % 4 === 0);
|
||||
hat(next + spb / 2);
|
||||
next += spb;
|
||||
beat++;
|
||||
}
|
||||
}, 50);
|
||||
|
||||
const track = dest.stream.getAudioTracks()[0]!;
|
||||
return {
|
||||
track,
|
||||
stop: () => {
|
||||
window.clearInterval(timer);
|
||||
track.stop();
|
||||
void ctx.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user