CHZZK - Recorder (HLS)

Records live Chzzk streams directly from the browser

// ==UserScript==
// @name         CHZZK - Recorder (HLS)
// @name:en      CHZZK - Recorder (HLS)
// @name:ko      치지직 - 레코더 (HLS)
// @namespace    https://greasyfork.org/ja/users/941284-ぐらんぴ
// @version      2025-08-21
// @description  Records live Chzzk streams directly from the browser
// @description:en Records live Chzzk streams directly from the browser
// @description:ko 브라우저에서 직접 Chzzk 라이브 스트림을 녹화합니다.
// @author       ぐらんぴ
// @match        https://*.naver.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=naver.com
// @grant        none
// @run-at       document-start
// @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;

function observeUrlChanges(){
    let lastUrl = location.href;

    const observer = new MutationObserver(() => {
        if(location.href !== lastUrl){
            lastUrl = location.href;
            checkPageChange();
        }
    });

    observer.observe(document.body, { subtree: true, childList: true, });

    window.addEventListener('hashchange', () => {
        if(location.href !== lastUrl){
            lastUrl = location.href;
            checkPageChange();
        }
    });
}
// ページ変更チェック
function checkPageChange(){
    record()
}
// 初期実行 & 監視開始
function record(){
    let awaitAddon = setInterval(() => {
        if(!$S(".video_information_control__UTm8Z")) return;
        clearInterval(awaitAddon);
        let addon = $S(".video_information_control__UTm8Z")

        let btn = $C('button');
        btn.textContent = ` [RECORD]`;
        btn.className = "GRMP";
        btn.style.color = "white";
        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');
                        try{
                            let name = $S('.name_text__yQG50').textContent
                            let title = $S('.video_information_title__jrLfG').textContent

                            if(location.pathname.startsWith('/video/')){// archive
                                a.download = name + "_" + title + "_" + location.pathname.slice(7) + ".webm";
                            }else{// live
                                a.download = name + "_" + title + "_" + month + "/" + day + ".webm";
                            }
                        }catch(e){ //alert('Could not get filename', e)
                            a.download = location.pathname + "_" + 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')) addon.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}]`;
        }
    }, 500);
}record()
observeUrlChanges();