feat: browser capture page — MediaRecorder WebSocket streaming + mute

This commit is contained in:
2026-05-28 10:51:21 -05:00
parent 8a3e7e2b53
commit 2cc256ed6e
+134
View File
@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LinkedStorm Capture</title>
<style>
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0;
display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
.card { background: #1e293b; border-radius: 16px; padding: 32px; text-align: center;
min-width: 280px; box-shadow: 0 4px 32px rgba(0,0,0,0.4); }
h1 { font-size: 1.4em; margin: 0 0 8px; color: #f1f5f9; }
.room-label { font-size: 1em; color: #94a3b8; margin-bottom: 24px; }
.status { display: flex; align-items: center; justify-content: center; gap: 10px;
font-size: 0.9em; margin-bottom: 24px; }
.dot { width: 12px; height: 12px; border-radius: 50%; }
.dot.green { background: #4ade80; box-shadow: 0 0 8px #4ade80; animation: pulse 2s infinite; }
.dot.red { background: #ef4444; }
.dot.gray { background: #475569; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
button { padding: 12px 28px; border-radius: 8px; border: none; font-size: 1em;
cursor: pointer; transition: all 0.15s; }
#muteBtn { background: #334155; color: #e2e8f0; }
#muteBtn:hover { background: #475569; }
#muteBtn.muted { background: #7f1d1d; color: #fca5a5; }
.room-input { background: #334155; border: 1px solid #475569; border-radius: 8px;
padding: 8px 12px; color: #e2e8f0; font-size: 0.9em; width: 100%;
box-sizing: border-box; margin-bottom: 16px; }
</style>
</head>
<body>
<div class="card">
<h1>LinkedStorm</h1>
<div id="setup" style="display:none">
<p style="color:#94a3b8;font-size:0.85em">Enter room name to start capturing</p>
<input id="roomInput" class="room-input" placeholder="e.g. Kitchen" />
<button onclick="startCapture()" style="background:#4ade80;color:#052e16">Start Capturing</button>
</div>
<div id="capture" style="display:none">
<div class="room-label">Room: <span id="roomDisplay"></span></div>
<div class="status">
<div class="dot" id="statusDot"></div>
<span id="statusText">Connecting...</span>
</div>
<button id="muteBtn" onclick="toggleMute()">Mute</button>
</div>
</div>
<script>
const params = new URLSearchParams(location.search);
let roomName = params.get('room') || localStorage.getItem('linkedstorm_room');
let ws = null, mediaRecorder = null, muted = false;
function init() {
if (roomName) {
document.getElementById('setup').style.display = 'none';
document.getElementById('capture').style.display = 'block';
document.getElementById('roomDisplay').textContent = roomName;
connect();
} else {
document.getElementById('setup').style.display = 'block';
}
}
function startCapture() {
roomName = document.getElementById('roomInput').value.trim();
if (!roomName) return;
localStorage.setItem('linkedstorm_room', roomName);
document.getElementById('setup').style.display = 'none';
document.getElementById('capture').style.display = 'block';
document.getElementById('roomDisplay').textContent = roomName;
connect();
}
function setStatus(state, text) {
document.getElementById('statusDot').className = 'dot ' + state;
document.getElementById('statusText').textContent = text;
}
async function connect() {
setStatus('gray', 'Connecting...');
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: { channelCount: 1, sampleRate: 16000 }
});
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsHost = location.hostname;
ws = new WebSocket(`${wsProto}//${wsHost}:8300/ws/${encodeURIComponent(roomName)}`);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
setStatus('green', muted ? 'Muted' : 'Streaming');
startRecorder(stream);
};
ws.onclose = () => {
setStatus('gray', 'Reconnecting...');
setTimeout(connect, 3000);
};
ws.onerror = () => ws.close();
} catch (e) {
setStatus('red', 'Microphone access denied');
}
}
function startRecorder(stream) {
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = e => {
if (ws && ws.readyState === WebSocket.OPEN && !muted && e.data.size > 0) {
e.data.arrayBuffer().then(buf => ws.send(buf));
}
};
mediaRecorder.start(250);
}
function toggleMute() {
muted = !muted;
const btn = document.getElementById('muteBtn');
if (muted) {
btn.textContent = 'Unmute';
btn.classList.add('muted');
setStatus('red', 'Muted');
if (ws && ws.readyState === WebSocket.OPEN)
ws.send(JSON.stringify({ type: 'mute' }));
} else {
btn.textContent = 'Mute';
btn.classList.remove('muted');
setStatus('green', 'Streaming');
if (ws && ws.readyState === WebSocket.OPEN)
ws.send(JSON.stringify({ type: 'unmute' }));
}
}
init();
</script>
</body>
</html>