Universal Video Recorder

Tracks videos > 5 mins with a dedicated red overlay. Pulses on play, downloads on pause.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();

})();