Tracks videos > 5 mins with a dedicated red overlay. Pulses on play, downloads on pause.
// ==UserScript==
// @name Universal Video Recorder
// @namespace universal_video_recorder
// @version 1.0
// @description Tracks videos > 5 mins with a dedicated red overlay. Pulses on play, downloads on pause.
// @author dev.ansung
// @match *://*/*
// @match *://*.youtube.com/*
// @match *://*.vimeo.com/*
// @match *://*.dailymotion.com/*
// @match *://*.twitch.tv/*
// @match *://*.facebook.com/watch*
// @match *://*.facebook.com/*/videos/*
// @match *://*.twitter.com/*
// @match *://*.x.com/*
// @match *://*.streamable.com/*
// @match *://*.rumble.com/*
// @exclude *://*.bilibili.com/*
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const MIN_DURATION_SEC = 300; // 5 minutes
class VideoRecorder {
#video;
#overlay;
#recorder = null;
#chunks = [];
#animationFrameId = null;
constructor(videoElement) {
this.#video = videoElement;
this.#video.dataset.recorderAttached = "true";
this.#initOverlay();
this.#bindEvents();
this.#syncOverlay();
}
static injectStyles() {
if (document.getElementById('univ-video-styles')) return;
const style = document.createElement('style');
style.id = 'univ-video-styles';
style.textContent = `
.univ-video-overlay {
position: fixed;
pointer-events: none;
z-index: 2147483647;
outline: 4px solid red;
outline-offset: -4px;
}
.univ-video-overlay.univ-pulse-active {
animation: univ-pulse-anim 0.8s infinite alternate;
}
@keyframes univ-pulse-anim {
from {
outline-color: rgba(255, 0, 0, 1);
box-shadow: inset 0 0 40px rgba(255, 0, 0, 0.6);
}
to {
outline-color: rgba(255, 0, 0, 0.2);
box-shadow: inset 0 0 5px rgba(255, 0, 0, 0.1);
}
}
`;
document.head.append(style);
}
#initOverlay() {
this.#overlay = document.createElement('div');
this.#overlay.className = 'univ-video-overlay';
document.body.appendChild(this.#overlay);
}
// Arrow function preserves 'this' context for requestAnimationFrame
#syncOverlay = () => {
// Cleanup if the video is removed from the DOM
if (!document.body.contains(this.#video)) {
this.#overlay.remove();
cancelAnimationFrame(this.#animationFrameId);
return;
}
const rect = this.#video.getBoundingClientRect();
// Only render if video is visible
if (rect.width > 0 && rect.height > 0) {
this.#overlay.style.display = 'block';
this.#overlay.style.top = `${rect.top}px`;
this.#overlay.style.left = `${rect.left}px`;
this.#overlay.style.width = `${rect.width}px`;
this.#overlay.style.height = `${rect.height}px`;
} else {
this.#overlay.style.display = 'none';
}
this.#animationFrameId = requestAnimationFrame(this.#syncOverlay);
}
#bindEvents() {
this.#video.addEventListener('play', this.#startRecording);
this.#video.addEventListener('pause', this.#stopRecording);
}
#startRecording = () => {
try {
const stream = this.#video.captureStream();
const mimeTypes = ['video/webm;codecs=vp9', 'video/webm', 'video/mp4'];
const mime = mimeTypes.find(t => MediaRecorder.isTypeSupported(t)) || '';
this.#recorder = new MediaRecorder(stream, { mimeType: mime });
this.#chunks = [];
this.#recorder.ondataavailable = (e) => {
if (e.data && e.data.size > 0) this.#chunks.push(e.data);
};
this.#recorder.onstop = this.#downloadRecording;
this.#recorder.start();
this.#overlay.classList.add('univ-pulse-active');
console.log("VideoRecorder: Recording started.");
} catch (err) {
console.error("VideoRecorder: Failed to start recording. (Possible CORS issue)", err);
}
}
#stopRecording = () => {
this.#overlay.classList.remove('univ-pulse-active');
if (this.#recorder && this.#recorder.state === 'recording') {
this.#recorder.stop();
console.log("VideoRecorder: Recording stopped. Initiating download...");
}
}
#downloadRecording = () => {
if (this.#chunks.length === 0) return;
const blob = new Blob(this.#chunks, { type: this.#recorder.mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `video_capture_${Date.now()}.mp4`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
}
// --- Main Scanner ---
function scanForVideos() {
document.querySelectorAll('video').forEach(video => {
if (!video.dataset.recorderAttached && video.duration > MIN_DURATION_SEC) {
new VideoRecorder(video);
}
});
}
// Initialize
VideoRecorder.injectStyles();
setInterval(scanForVideos, 2000);
scanForVideos();
})();