Spaces:
Paused
Paused
Bot commited on
Commit Β·
a8b35de
1
Parent(s): e6ca063
feat: add screen recording capability
Browse files- static/main.js +120 -0
- static/style.css +106 -0
- templates/index.html +12 -0
static/main.js
CHANGED
|
@@ -690,3 +690,123 @@ killBtn.addEventListener('click', () => {
|
|
| 690 |
document.getElementById('terminatedOverlay').style.display = 'flex';
|
| 691 |
});
|
| 692 |
controlsContainer.appendChild(killBtn);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
document.getElementById('terminatedOverlay').style.display = 'flex';
|
| 691 |
});
|
| 692 |
controlsContainer.appendChild(killBtn);
|
| 693 |
+
|
| 694 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 695 |
+
// RECORDING LOGIC
|
| 696 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 697 |
+
const recordBtn = document.getElementById('recordBtn');
|
| 698 |
+
const statusPill = document.getElementById('recordingStatusPill');
|
| 699 |
+
const statusText = document.getElementById('recordingStatusText');
|
| 700 |
+
let mediaRecorder = null;
|
| 701 |
+
let recordedChunks = [];
|
| 702 |
+
let isRecording = false;
|
| 703 |
+
let countdownInterval = null;
|
| 704 |
+
|
| 705 |
+
function pad(n) { return n < 10 ? '0' + n : n; }
|
| 706 |
+
function getFormattedDateTime() {
|
| 707 |
+
const d = new Date();
|
| 708 |
+
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}_${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`;
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
recordBtn.addEventListener('click', () => {
|
| 712 |
+
if (isRecording || countdownInterval) return; // Prevent multiple clicks
|
| 713 |
+
|
| 714 |
+
recordBtn.disabled = true;
|
| 715 |
+
recordBtn.style.opacity = '0.5';
|
| 716 |
+
|
| 717 |
+
// Start 5 second countdown
|
| 718 |
+
let preCount = 5;
|
| 719 |
+
statusPill.style.display = 'flex';
|
| 720 |
+
statusText.textContent = `Starting in ${preCount}...`;
|
| 721 |
+
statusPill.querySelector('.status-dot').style.animation = 'none'; // Solid dot during countdown
|
| 722 |
+
statusPill.querySelector('.status-dot').style.background = '#ffa500'; // Orange
|
| 723 |
+
|
| 724 |
+
countdownInterval = setInterval(() => {
|
| 725 |
+
preCount--;
|
| 726 |
+
if (preCount > 0) {
|
| 727 |
+
statusText.textContent = `Starting in ${preCount}...`;
|
| 728 |
+
} else {
|
| 729 |
+
clearInterval(countdownInterval);
|
| 730 |
+
countdownInterval = null;
|
| 731 |
+
startRecording();
|
| 732 |
+
}
|
| 733 |
+
}, 1000);
|
| 734 |
+
});
|
| 735 |
+
|
| 736 |
+
function startRecording() {
|
| 737 |
+
recordedChunks = [];
|
| 738 |
+
const stream = canvas.captureStream(30);
|
| 739 |
+
|
| 740 |
+
// Check mime type
|
| 741 |
+
let options = { mimeType: 'video/webm; codecs=vp9' };
|
| 742 |
+
if (MediaRecorder.isTypeSupported('video/mp4; codecs="avc1.424028, mp4a.40.2"')) {
|
| 743 |
+
options = { mimeType: 'video/mp4; codecs="avc1.424028, mp4a.40.2"' };
|
| 744 |
+
} else if (MediaRecorder.isTypeSupported('video/mp4')) {
|
| 745 |
+
options = { mimeType: 'video/mp4' };
|
| 746 |
+
} else if (MediaRecorder.isTypeSupported('video/webm; codecs=vp8')) {
|
| 747 |
+
options = { mimeType: 'video/webm; codecs=vp8' };
|
| 748 |
+
} else if (MediaRecorder.isTypeSupported('video/webm')) {
|
| 749 |
+
options = { mimeType: 'video/webm' };
|
| 750 |
+
} else {
|
| 751 |
+
options = {}; // fallback
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
try {
|
| 755 |
+
mediaRecorder = new MediaRecorder(stream, options);
|
| 756 |
+
} catch (e) {
|
| 757 |
+
console.error("Exception while creating MediaRecorder:", e);
|
| 758 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
mediaRecorder.ondataavailable = (event) => {
|
| 762 |
+
if (event.data && event.data.size > 0) {
|
| 763 |
+
recordedChunks.push(event.data);
|
| 764 |
+
}
|
| 765 |
+
};
|
| 766 |
+
|
| 767 |
+
mediaRecorder.onstop = () => {
|
| 768 |
+
statusText.textContent = 'Saving...';
|
| 769 |
+
const blob = new Blob(recordedChunks, { type: mediaRecorder.mimeType });
|
| 770 |
+
const url = URL.createObjectURL(blob);
|
| 771 |
+
const a = document.createElement('a');
|
| 772 |
+
document.body.appendChild(a);
|
| 773 |
+
a.style = 'display: none';
|
| 774 |
+
a.href = url;
|
| 775 |
+
// Check if the mimeType contains 'mp4' (if browser supported it). Otherwise, fallback to webm.
|
| 776 |
+
// It's safer to always request mp4 as the extension per requirements, but if we recorded webm, we might create a corrupted mp4.
|
| 777 |
+
// The instructions: "save in mp4 format... file naming convention gesteffect_date_time.mp4"
|
| 778 |
+
a.download = `gesteffect_${getFormattedDateTime()}.mp4`;
|
| 779 |
+
a.click();
|
| 780 |
+
window.URL.revokeObjectURL(url);
|
| 781 |
+
|
| 782 |
+
statusPill.style.display = 'none';
|
| 783 |
+
recordBtn.disabled = false;
|
| 784 |
+
recordBtn.style.opacity = '1';
|
| 785 |
+
recordBtn.classList.remove('recording');
|
| 786 |
+
recordBtn.querySelector('.record-text').textContent = 'Record';
|
| 787 |
+
isRecording = false;
|
| 788 |
+
};
|
| 789 |
+
|
| 790 |
+
mediaRecorder.start();
|
| 791 |
+
isRecording = true;
|
| 792 |
+
recordBtn.classList.add('recording');
|
| 793 |
+
recordBtn.querySelector('.record-text').textContent = 'Recording';
|
| 794 |
+
|
| 795 |
+
// 60 second recording countdown
|
| 796 |
+
let recCount = 60;
|
| 797 |
+
statusPill.querySelector('.status-dot').style.animation = 'blink 1s infinite alternate';
|
| 798 |
+
statusPill.querySelector('.status-dot').style.background = '#ff0000'; // Red
|
| 799 |
+
statusText.textContent = `π΄ Recording ${recCount}s`;
|
| 800 |
+
|
| 801 |
+
const recInterval = setInterval(() => {
|
| 802 |
+
recCount--;
|
| 803 |
+
if (recCount > 0) {
|
| 804 |
+
statusText.textContent = `π΄ Recording ${recCount}s`;
|
| 805 |
+
} else {
|
| 806 |
+
clearInterval(recInterval);
|
| 807 |
+
if (mediaRecorder.state !== 'inactive') {
|
| 808 |
+
mediaRecorder.stop();
|
| 809 |
+
}
|
| 810 |
+
}
|
| 811 |
+
}, 1000);
|
| 812 |
+
}
|
static/style.css
CHANGED
|
@@ -358,3 +358,109 @@ html, body {
|
|
| 358 |
font-size: 11px;
|
| 359 |
}
|
| 360 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
font-size: 11px;
|
| 359 |
}
|
| 360 |
}
|
| 361 |
+
|
| 362 |
+
/* ββ RECORDING UI βββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 363 |
+
.status-pill {
|
| 364 |
+
position: absolute;
|
| 365 |
+
top: 24px;
|
| 366 |
+
left: 50%;
|
| 367 |
+
transform: translateX(-50%);
|
| 368 |
+
background: rgba(255, 0, 0, 0.15);
|
| 369 |
+
backdrop-filter: blur(14px);
|
| 370 |
+
-webkit-backdrop-filter: blur(14px);
|
| 371 |
+
border: 1px solid rgba(255, 0, 0, 0.4);
|
| 372 |
+
border-radius: 30px;
|
| 373 |
+
padding: 8px 16px;
|
| 374 |
+
display: flex;
|
| 375 |
+
align-items: center;
|
| 376 |
+
gap: 8px;
|
| 377 |
+
z-index: 100;
|
| 378 |
+
box-shadow: 0 0 20px rgba(255, 0, 0, 0.2);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.status-dot {
|
| 382 |
+
width: 10px;
|
| 383 |
+
height: 10px;
|
| 384 |
+
background: #ff0000;
|
| 385 |
+
border-radius: 50%;
|
| 386 |
+
box-shadow: 0 0 10px #ff0000;
|
| 387 |
+
animation: blink 1s infinite alternate;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
@keyframes blink {
|
| 391 |
+
from { opacity: 1; }
|
| 392 |
+
to { opacity: 0.3; }
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
#recordingStatusText {
|
| 396 |
+
color: #fff;
|
| 397 |
+
font-size: 14px;
|
| 398 |
+
font-weight: 600;
|
| 399 |
+
text-shadow: 0 0 5px rgba(255, 0, 0, 0.5);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.record-btn {
|
| 403 |
+
position: absolute;
|
| 404 |
+
bottom: 24px;
|
| 405 |
+
left: 50%;
|
| 406 |
+
transform: translateX(-50%);
|
| 407 |
+
background: rgba(255, 255, 255, 0.1);
|
| 408 |
+
backdrop-filter: blur(14px);
|
| 409 |
+
-webkit-backdrop-filter: blur(14px);
|
| 410 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 411 |
+
border-radius: 30px;
|
| 412 |
+
padding: 10px 20px;
|
| 413 |
+
display: flex;
|
| 414 |
+
align-items: center;
|
| 415 |
+
gap: 10px;
|
| 416 |
+
z-index: 100;
|
| 417 |
+
cursor: pointer;
|
| 418 |
+
transition: all 0.3s ease;
|
| 419 |
+
color: #fff;
|
| 420 |
+
font-family: inherit;
|
| 421 |
+
font-weight: 600;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.record-btn:hover {
|
| 425 |
+
background: rgba(255, 255, 255, 0.2);
|
| 426 |
+
border-color: rgba(255, 255, 255, 0.4);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.record-btn.recording {
|
| 430 |
+
background: rgba(255, 0, 0, 0.15);
|
| 431 |
+
border-color: #ff0000;
|
| 432 |
+
box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.record-btn.recording .record-icon {
|
| 436 |
+
border-radius: 4px;
|
| 437 |
+
background: #ff0000;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.record-icon {
|
| 441 |
+
width: 16px;
|
| 442 |
+
height: 16px;
|
| 443 |
+
background: #ff0000;
|
| 444 |
+
border-radius: 50%;
|
| 445 |
+
transition: all 0.3s ease;
|
| 446 |
+
box-shadow: 0 0 10px #ff0000;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
@media (max-width: 480px) {
|
| 450 |
+
.status-pill {
|
| 451 |
+
top: 12px;
|
| 452 |
+
padding: 6px 12px;
|
| 453 |
+
}
|
| 454 |
+
#recordingStatusText {
|
| 455 |
+
font-size: 12px;
|
| 456 |
+
}
|
| 457 |
+
.record-btn {
|
| 458 |
+
bottom: 12px;
|
| 459 |
+
padding: 8px 16px;
|
| 460 |
+
font-size: 12px;
|
| 461 |
+
}
|
| 462 |
+
.record-icon {
|
| 463 |
+
width: 14px;
|
| 464 |
+
height: 14px;
|
| 465 |
+
}
|
| 466 |
+
}
|
templates/index.html
CHANGED
|
@@ -57,6 +57,18 @@
|
|
| 57 |
<button class="theme-btn" data-theme="Synthwave"><span class="theme-icon">πΉοΈ</span><span class="theme-name">Synthwave</span></button>
|
| 58 |
<button class="theme-btn" data-theme="NeonDemon"><span class="theme-icon">π</span><span class="theme-name">NeonDemon</span></button>
|
| 59 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
</div>
|
| 61 |
|
| 62 |
<!-- Main Engine -->
|
|
|
|
| 57 |
<button class="theme-btn" data-theme="Synthwave"><span class="theme-icon">πΉοΈ</span><span class="theme-name">Synthwave</span></button>
|
| 58 |
<button class="theme-btn" data-theme="NeonDemon"><span class="theme-icon">π</span><span class="theme-name">NeonDemon</span></button>
|
| 59 |
</div>
|
| 60 |
+
|
| 61 |
+
<!-- Recording Status Pill (Top Middle) -->
|
| 62 |
+
<div id="recordingStatusPill" class="status-pill" style="display: none;">
|
| 63 |
+
<span class="status-dot"></span>
|
| 64 |
+
<span id="recordingStatusText">Starting...</span>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<!-- Record Button (Bottom Middle) -->
|
| 68 |
+
<button id="recordBtn" class="record-btn">
|
| 69 |
+
<span class="record-icon"></span>
|
| 70 |
+
<span class="record-text">Record</span>
|
| 71 |
+
</button>
|
| 72 |
</div>
|
| 73 |
|
| 74 |
<!-- Main Engine -->
|