YouTube - Live Recoder

Records live Youtube streams directly from the browser

// ==UserScript==
// @name         YouTube - Live Recoder
// @namespace    https://greasyfork.org/ja/users/941284-ぐらんぴ
// @version      2025-08-26
// @description  Records live Youtube streams directly from the browser
// @author       ぐらんぴ
// @match        https://*.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @run-at       document-start
// @require      https://greasyfork.org/scripts/433051-trusted-types-helper/code/Trusted-Types%20Helper.js
// @license      MIT
// ==/UserScript==

let $s = (el) => document.querySelector(el), $sa = (el) => document.querySelectorAll(el), $c = (el) => document.createElement(el)
let recorder, chunks = [], isRecording = false, seconds = 0, timerInterval;

window.addEventListener("yt-navigate-finish", e => {
    if(e.detail.pageType == "watch"){
        if(e.detail.response?.playerResponse?.videoDetails?.isLive){// live

            let btn = $c('button');
            btn.textContent = ` [RECORD]`;
            btn.className = "GRMP";
            btn.style.cursor = "pointer";
            btn.style.color = "white";
            btn.style.background = "none";
            btn.style.border = "none";
            btn.style.cursor = "pointer";
            btn.addEventListener("click", () => {
                const video = $s("video");
                if(!video){
                    alert("Video element not found.");
                    return;
                }

                if(video.paused || video.readyState < 3){
                    video.play().catch(err => console.warn("Video play failed:", err));
                }

                if(!isRecording){
                    try{
                        let stream;
                        let recorderStream;

                        if(navigator.userAgent.indexOf('Firefox') > -1){ // Firefox
                            const audioCtx = new AudioContext();
                            const sourceNode = audioCtx.createMediaElementSource(video);
                            const destinationNode = audioCtx.createMediaStreamDestination();

                            sourceNode.connect(audioCtx.destination); // keep audio playback
                            sourceNode.connect(destinationNode);      // send to recorder

                            stream = video.mozCaptureStream();

                            recorderStream = new MediaStream([
                                ...stream.getVideoTracks(),
                                ...destinationNode.stream.getAudioTracks()
                            ]);
                        }else{ // Chrome/Edge
                            recorderStream = video.captureStream();
                        }

                        if(!recorderStream){
                            alert("Failed to capture stream");
                            return;
                        }

                        recorder = new MediaRecorder(recorderStream);
                        chunks = [];

                        recorder.ondataavailable = e => chunks.push(e.data);
                        recorder.onstop = () => {
                            clearInterval(timerInterval);
                            btn.textContent = ` [RECORD]`;

                            const blob = new Blob(chunks, { type: 'video/webm' });
                            const url = URL.createObjectURL(blob);
                            const a = document.createElement('a');
                            a.href = url;

                            //filename
                            const now = new Date();
                            const month = String(now.getMonth() + 1).padStart(2, '0');
                            const day = String(now.getDate()).padStart(2, '0');
                            //let name = e.detail.response.playerResponse.videoDetails?.author
                            //let title = e.detail.response.playerResponse.videoDetails?.title

                            //let filename = name + "_" + title + "_" + month + "/" + day + ".webm";
                            try{
                                a.download = location.search.slice(3) + "_" + month + "/" + day + ".webm";
                            }catch(e){ //alert('Could not get filename', e)
                                a.download = location + "_" + month + "/" + day + ".webm";
                            };
                            a.click();
                        };

                        recorder.start();
                        isRecording = true;
                        seconds = 0;
                        btn.textContent = formatTime(seconds);

                        timerInterval = setInterval(() => {
                            seconds++;
                            btn.textContent = formatTime(seconds);
                        }, 1000);

                    }catch(e){ alert("Recording failed: " + e);
                             }
                }else{
                    recorder.stop();
                    isRecording = false;
                    clearInterval(timerInterval);
                    btn.textContent = ` [RECORD]`;
                }
            });
            if(!$s('.GRMP')){
                if($s('.ytp-right-controls') == null){
                    const intervalId = setInterval(() => {
                        if($s('.ytp-right-controls')) return;
                        clearInterval(intervalId);
                        $s('.ytp-right-controls').appendChild(btn);
                    },500);
                }else $s('.ytp-right-controls').appendChild(btn);
            }
            function formatTime(sec){
                const m = String(Math.floor(sec / 60)).padStart(2, '0');
                const s = String(sec % 60).padStart(2, '0');
                return ` [${m}:${s}]`;
            }
        }
    }
});