ニコ生 - 録画機能

ニコゲー非表示, 広告削除, 録画機能

// ==UserScript==
// @name         ニコ生 - 録画機能
// @namespace    https://greasyfork.org/ja/users/941284-ぐらんぴ
// @version      2025-08-02
// @description  ニコゲー非表示, 広告削除, 録画機能
// @author       ぐらんぴ
// @match        https://live.nicovideo.jp/watch/lv*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nicovideo.jp
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==

let m3u8, lastHref, recorder, chunks = [], isRecording = false, log = console.log;
let $s = (el) => document.querySelector(el), $sa = (el) => document.querySelectorAll(el), $c = (el) => document.createElement(el)

// 待機
window.onload = () => {
    nicoGame_remover()
    record()
    ad_remover()
}

function record(){
    let addon = $s(".addon-controller");
    let btn = $c('button');

    btn.textContent = `録画`;
    btn.className = "GRMP";
    btn.style.cursor = "pointer";
    btn.style.backgroundColor = "black";
    btn.style.color = "white";
    btn.style.border = "none";

    let timerInterval;
    let recorder, chunks = [], isRecording = false;
    let seconds = 0;

    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{
                const stream = video.captureStream();
                if(!stream){
                    alert("Failed to capture stream.");
                    return;
                }

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

                recorder.ondataavailable = e => chunks.push(e.data);
                recorder.onstop = () => {
                    clearInterval(timerInterval);
                    btn.textContent = `録画`;

                    const blob = new Blob(chunks, { type: 'video/webm' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    let name = $s(".user-name-area > span > span > a") || $s(".channel-name-anchor")
                    let title = $s(".___program-title___vjykj > span").textContent.trim()
                    let date = $s(".___onair-time___c0czD").textContent.trim().replace('開始', '')
                    a.download = name.textContent.trim() + " - " + title + " - " + date + ".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 = `録画`;
        }
    });
    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}`;
    }
}
function nicoGame_remover(){
    let parent = $s(".addon-controller"),
        btn = document.createElement("button");
    btn.className = "grmp";
    btn.style.cursor = "pointer";
    btn.style.backgroundColor = "black";
    btn.style.border = "none";
    btn.setAttribute("aria-label", "ニコゲー非表示");
    btn.setAttribute("role", "img");

    btn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     viewBox="0,0,256,256"
     width="20px"
     height="20px">
  <g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1"
     stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10"
     stroke-dasharray="" stroke-dashoffset="0" font-family="none"
     font-weight="none" font-size="none" text-anchor="none"
     style="mix-blend-mode: normal">
     <g transform="scale(10.66667,10.66667)">
      <path d="M12,2c-5.511,0 -10,4.489 -10,10c0,5.511 4.489,10 10,10c5.511,0 10,-4.489 10,-10c0,-5.511 -4.489,-10 -10,-10zM12,4c4.43012,0 8,3.56988 8,8c0,1.85307 -0.63074,3.55056 -1.68164,4.9043l-11.22266,-11.22266c1.35374,-1.0509 3.05123,-1.68164 4.9043,-1.68164zM5.68164,7.0957l11.22266,11.22266c-1.35374,1.0509 -3.05123,1.68164 -4.9043,1.68164c-4.43012,0 -8,-3.56988 -8,-8c0,-1.85307 0.63074,-3.55056 1.68164,-4.9043z"/>
    </g>
  </g>
</svg>`;
    btn.style.cursor = "pointer";
    btn.style.backgroundColor = "black";
    btn.style.border = "none";
    btn.className = "grmp";
    btn.addEventListener('click', ()=>{
        try{
            gameDisplay2 = $s("#akashic-gameview > div > div:nth-child(2)")
            gameDisplay3 = $s("#akashic-gameview > div > div:nth-child(3)")
            if(!!gameDisplay3){
                if(gameDisplay3.style.display !== 'none'){
                    gameDisplay3.style.display = 'none'
                }else(gameDisplay3.style.display = '')
            }else{
                if(gameDisplay2.style.display !== 'none'){
                    gameDisplay2.style.display = 'none'
                }else(gameDisplay2.style.display = '')
            }
        }catch(e){//log('nicoGame_remover: 'e)
        }
    })
    parent.appendChild(btn)

}
function ad_remover(){
    const ads = [
        ".ad-banner",
        ".ga-ns-player-ad-panel",
        ".___app-download-banner___B0Yqm",
        ".___program-information-ad-area___u0hZN",
        ".ga-ns-ad-footer",
        ".___premium-member-registration-appeal-panel___P3yv0",
        ".___premium-merit-appeal-banner___vvXtw",
    ];

    GM_addStyle(ads.map(sel => `${sel} { display: none !important; }`).join("\n"));
}