chariscait's picture
Increase video window height (480-680px) and frame height (620px)
8dea023 verified
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; font-family: -apple-system, 'Inter', sans-serif; }
#container { width: 100%; position: relative; border-radius: 12px; overflow: hidden; background: #0a0a1a; }
video {
width: 100%; display: block;
border-radius: 12px;
background: #0a0a1a;
min-height: 480px;
max-height: 680px;
object-fit: cover;
}
#overlay {
position: absolute; top: 10px; left: 10px;
display: flex; gap: 8px; align-items: center; z-index: 10;
}
.badge {
padding: 5px 12px; border-radius: 8px; font-size: 12px;
font-weight: 700; letter-spacing: 0.5px;
backdrop-filter: blur(4px);
}
.badge-live { background: rgba(255,50,50,0.9); color: white; animation: pulse 1.5s infinite; }
.badge-ready { background: rgba(100,100,100,0.85); color: #ddd; }
.badge-ended { background: rgba(0,180,100,0.85); color: white; }
.badge-timer { background: rgba(0,0,0,0.7); color: #00D4FF; font-variant-numeric: tabular-nums; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
#controls { text-align: center; margin-top: 12px; }
button {
padding: 12px 32px; border: none; border-radius: 12px;
font-size: 15px; font-weight: 700; cursor: pointer;
transition: all 0.2s; letter-spacing: 0.5px;
}
#startBtn {
background: linear-gradient(90deg, #9B6FCE, #6C63FF);
color: white; box-shadow: 0 4px 15px rgba(108,99,255,0.3);
}
#startBtn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(108,99,255,0.4); }
#stopBtn {
background: linear-gradient(90deg, #FF4444, #CC2200);
color: white; display: none;
box-shadow: 0 4px 15px rgba(255,68,68,0.3);
}
#stopBtn:hover { transform: translateY(-1px); }
#errorMsg {
color: #FF6B6B; font-size: 13px; margin-top: 8px;
display: none; text-align: center;
}
</style>
</head>
<body>
<div id="container">
<video id="video" autoplay playsinline muted></video>
<div id="overlay">
<span id="statusBadge" class="badge badge-ready">READY</span>
<span id="timerBadge" class="badge badge-timer" style="display:none">60s</span>
</div>
</div>
<div id="controls">
<button id="startBtn">&#9654; START SESSION</button>
<button id="stopBtn">&#9209; STOP &amp; VIEW REPORT</button>
</div>
<div id="errorMsg"></div>
<canvas id="canvas" style="display:none"></canvas>
<script>
// ── Streamlit Component Protocol ────────────────────────────────
function sendToStreamlit(type, data) {
window.parent.postMessage({ isStreamlitMessage: true, type: type, ...data }, "*");
}
function setComponentValue(value) {
sendToStreamlit("streamlit:setComponentValue", { value: value });
}
function setFrameHeight(height) {
sendToStreamlit("streamlit:setFrameHeight", { height: height });
}
function componentReady() {
sendToStreamlit("streamlit:componentReady", { apiVersion: 1 });
}
// ── Persistent State (survives Streamlit re-renders) ────────────
if (!window._ws) {
window._ws = {
stream: null,
capturing: false,
frameInterval: null,
audioRecorder: null,
audioInterval: null,
startTime: 0,
timerInterval: null,
frameCount: 0,
};
}
const S = window._ws;
// ── DOM Elements ────────────────────────────────────────────────
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const statusBadge = document.getElementById('statusBadge');
const timerBadge = document.getElementById('timerBadge');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const errorMsg = document.getElementById('errorMsg');
// ── Restore UI if already capturing (after Streamlit re-render) ─
if (S.capturing && S.stream) {
video.srcObject = S.stream;
statusBadge.textContent = '\u25CF LIVE';
statusBadge.className = 'badge badge-live';
timerBadge.style.display = '';
startBtn.style.display = 'none';
stopBtn.style.display = '';
}
// ── Start Capture ───────────────────────────────────────────────
startBtn.onclick = async function() {
errorMsg.style.display = 'none';
try {
S.stream = await navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: 'user' },
audio: true
});
video.srcObject = S.stream;
S.capturing = true;
S.startTime = Date.now();
S.frameCount = 0;
canvas.width = 640;
canvas.height = 480;
// Capture + send a video frame every 2 seconds
captureAndSend(); // immediate first frame
S.frameInterval = setInterval(captureAndSend, 2000);
// Audio: record 5-second chunks via MediaRecorder (longer = better STT)
try {
const audioTrack = S.stream.getAudioTracks()[0];
if (audioTrack) {
const audioStream = new MediaStream([audioTrack]);
startAudioChunk(audioStream);
S.audioInterval = setInterval(function() {
if (S.audioRecorder && S.audioRecorder.state === 'recording') {
S.audioRecorder.stop();
startAudioChunk(audioStream);
}
}, 5000);
}
} catch(ae) { console.log('[Webcam] Audio setup error:', ae); }
// Timer countdown
updateTimer();
S.timerInterval = setInterval(updateTimer, 1000);
// UI update
statusBadge.textContent = '\u25CF LIVE';
statusBadge.className = 'badge badge-live';
timerBadge.style.display = '';
startBtn.style.display = 'none';
stopBtn.style.display = '';
setComponentValue({ type: 'started', ts: Date.now() });
// Auto-stop after 62 seconds (small buffer)
setTimeout(function() { if (S.capturing) stopCapture(); }, 62000);
} catch(err) {
errorMsg.textContent = 'Camera/mic access denied: ' + err.message;
errorMsg.style.display = 'block';
setComponentValue({ type: 'error', message: err.message });
}
};
// ── Stop Capture ────────────────────────────────────────────────
stopBtn.onclick = function() { stopCapture(); };
function stopCapture() {
S.capturing = false;
if (S.frameInterval) { clearInterval(S.frameInterval); S.frameInterval = null; }
if (S.audioInterval) { clearInterval(S.audioInterval); S.audioInterval = null; }
if (S.timerInterval) { clearInterval(S.timerInterval); S.timerInterval = null; }
if (S.audioRecorder && S.audioRecorder.state === 'recording') {
try { S.audioRecorder.stop(); } catch(e) {}
}
if (S.stream) {
S.stream.getTracks().forEach(function(t) { t.stop(); });
S.stream = null;
}
video.srcObject = null;
statusBadge.textContent = 'SESSION ENDED';
statusBadge.className = 'badge badge-ended';
timerBadge.style.display = 'none';
startBtn.style.display = 'none';
stopBtn.style.display = 'none';
setComponentValue({ type: 'stopped', ts: Date.now() });
}
// ── Frame Capture ───────────────────────────────────────────────
function captureAndSend() {
if (!S.capturing || video.readyState < 2) return;
canvas.width = video.videoWidth || 640;
canvas.height = video.videoHeight || 480;
ctx.drawImage(video, 0, 0);
var dataUrl = canvas.toDataURL('image/jpeg', 0.65);
S.frameCount++;
setComponentValue({
type: 'frame',
data: dataUrl,
frame: S.frameCount,
ts: Date.now()
});
}
// ── Audio Recording ─────────────────────────────────────────────
function startAudioChunk(audioStream) {
try {
var mimeType = 'audio/webm;codecs=opus';
if (!MediaRecorder.isTypeSupported(mimeType)) {
mimeType = 'audio/webm';
}
if (!MediaRecorder.isTypeSupported(mimeType)) {
mimeType = ''; // browser default
}
S.audioRecorder = new MediaRecorder(audioStream, mimeType ? { mimeType: mimeType } : {});
S.audioRecorder.ondataavailable = function(e) {
if (e.data.size > 0 && S.capturing) {
var reader = new FileReader();
reader.onload = function() {
setComponentValue({
type: 'audio',
data: reader.result,
ts: Date.now()
});
};
reader.readAsDataURL(e.data);
}
};
S.audioRecorder.start();
} catch(e) { console.log('[Webcam] MediaRecorder error:', e); }
}
// ── Timer ───────────────────────────────────────────────────────
function updateTimer() {
if (!S.capturing) return;
var elapsed = Math.floor((Date.now() - S.startTime) / 1000);
var remaining = Math.max(0, 60 - elapsed);
timerBadge.textContent = remaining + 's';
if (remaining <= 10) {
timerBadge.style.color = '#FF4444';
}
if (remaining <= 0) { stopCapture(); }
}
// ── Handle Streamlit re-render messages ─────────────────────────
window.addEventListener("message", function(event) {
if (event.data && event.data.type === "streamlit:render") {
setFrameHeight(document.body.scrollHeight || 520);
}
});
// ── Init ────────────────────────────────────────────────────────
setFrameHeight(620);
componentReady();
</script>
</body>
</html>