135 lines
5.0 KiB
HTML
135 lines
5.0 KiB
HTML
<!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>
|