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-11-16
// @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==
// Settings
let S = {
bitsPerSecond: 20000000, // default 20 Mbps
};
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;
}
const mimeCandidates = [
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/webm'
];
let mime = null;
for (const m of mimeCandidates) {
try {
if (MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(m)) { mime = m; break; }
} catch (err) {}
}
const options = mime
? { mimeType: mime, bitsPerSecond: Number(S.bitsPerSecond) || undefined }
: (Number(S.bitsPerSecond) ? { bitsPerSecond: Number(S.bitsPerSecond) } : undefined);
recorder = options ? new MediaRecorder(recorderStream, options) : new MediaRecorder(recorderStream);
chunks = [];
recorder.ondataavailable = e => { if (e.data && e.data.size) 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();