Screen Recorder

chromium-based only

// ==UserScript==
// @name         Screen Recorder
// @namespace    https://greasyfork.org/ja/users/941284-ぐらんぴ
// @version      1.1
// @description  chromium-based only
// @author       ぐらんぴ
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @resource     alertifyCSS  https://cdn.jsdelivr.net/npm/[email protected]/build/css/alertify.min.css
// @resource     alertifyTheme https://cdn.jsdelivr.net/npm/[email protected]/build/css/themes/default.min.css
// @resource     alertifyJS https://cdn.jsdelivr.net/npm/[email protected]/build/alertify.min.js
// @license      MIT
// ==/UserScript==

GM_addStyle(GM_getResourceText("alertifyCSS"))
GM_addStyle(GM_getResourceText("alertifyTheme"))

const alertifyScript = GM_getResourceText("alertifyJS")
const scriptTag = document.createElement("script")
scriptTag.textContent = alertifyScript
document.head.appendChild(scriptTag)

let recorder, chunks = [], isRecording = false
let combinedStream, logInterval, startTime
const registeredVideos = new WeakSet()

const $C = (tag, props = {}, styles = {}) => {
    const el = document.createElement(tag)
    Object.assign(el, props)
    Object.assign(el.style, styles)
    return el
}

const settings = {
    mimeType: "video/webm;codecs=vp8,opus", // Highest quality: "video/webm;codecs=vp9,opus",
    videoBitsPerSecond: 2_500_000,          // Highest quality: 10_000_000,
    frameRate: { ideal: 30, max: 60 },      // Highest quality: { ideal: 60, max: 120 }
    cursor: "always"                        // motion, never
}

const isFirefox = navigator.userAgent.toLowerCase().includes("firefox")

async function videoRecord(videoEl) {
    try {
        const videoStream = isFirefox
            ? videoEl.mozCaptureStream(settings.frameRate.ideal)
            : videoEl.captureStream(settings.frameRate.ideal)

        const audioCtx = new AudioContext()
        const srcNode = audioCtx.createMediaElementSource(videoEl)
        const destNode = audioCtx.createMediaStreamDestination()
        srcNode.connect(destNode)
        srcNode.connect(audioCtx.destination)

        let micStream = null
        try {
            micStream = await navigator.mediaDevices.getUserMedia({ audio: true })
        } catch (e) {
            console.warn("Mic access failed:", e)
        }

        const tracks = [
            ...videoStream.getVideoTracks(),
            ...destNode.stream.getAudioTracks(),
            ...(micStream ? micStream.getAudioTracks() : [])
        ]
        combinedStream = new MediaStream(tracks)

        startRecorder()
        alertify.notify("📹 Video recording started", "success", 5)
    } catch (err) {
        alertify.alert("Video record failed: " + err.message)
        console.error("Video record failed:", err)
    }
}

async function screenRecord() {
    try {
        const constraints = {
            video: { cursor: settings.cursor, frameRate: settings.frameRate },
            audio: isFirefox ? false : true
        }
        const displayStream = await navigator.mediaDevices.getDisplayMedia(constraints)

        let micStream = null
        try {
            micStream = await navigator.mediaDevices.getUserMedia({ audio: true })
        } catch (e) {
            console.warn("Mic access failed:", e)
        }

        const tracks = [
            ...displayStream.getVideoTracks(),
            ...displayStream.getAudioTracks(),
            ...(micStream ? micStream.getAudioTracks() : [])
        ]
        combinedStream = new MediaStream(tracks)

        startRecorder()
        alertify.notify("📺 Screen recording started", "success", 5)
    } catch (err) {
        alertify.alert("Screen record failed: " + err.message)
        console.error("Screen record failed:", err)
    }
}

function startRecorder() {
    recorder = new MediaRecorder(combinedStream, {
        mimeType: settings.mimeType,
        videoBitsPerSecond: settings.videoBitsPerSecond
    })

    recorder.ondataavailable = e => chunks.push(e.data)
    recorder.onstop = saveRecording
    recorder.start(1000)

    startTime = Date.now()
    logInterval = setInterval(() => {
        const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
        const sizeMB = (chunks.reduce((sum, c) => sum + c.size, 0) / (1024 * 1024)).toFixed(2)
        console.log(`⏱️ ${elapsed}s elapsed, 💾 ${sizeMB} MB recorded`)
    }, 10000)

    isRecording = true
}

function stopCapture() {
    if (!recorder || recorder.state === "inactive") return
    recorder.stop()
    combinedStream.getTracks().forEach(t => t.stop())
    clearInterval(logInterval)
    isRecording = false
    console.log("⏹️ Recording stopped")
}

function saveRecording() {
    const defaultName = `${new Date().toISOString().replace(/[:.]/g, "-")}`
    const filename = prompt("Enter filename:", defaultName) || defaultName

    const blob = new Blob(chunks, { type: settings.mimeType })
    const url = URL.createObjectURL(blob)
    const a = document.createElement("a")
    a.href = url
    a.download = `${filename}.webm`
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(url)

    chunks = []
    console.log(`💾 Saved: ${filename}.webm`)
}

// Firefox only: show floating button
function createScreenRecButton() {
    const btn = $C("button", {
        textContent: "🖥️ Screen REC/SAVE",
        onclick: () => isRecording ? stopCapture() : screenRecord()
    }, {
        position: "fixed",
        top: "10px",
        right: "10px",
        zIndex: "9999",
        padding: "8px 12px",
        fontSize: "14px",
        background: "#444",
        color: "#fff",
        border: "none",
        borderRadius: "4px",
        cursor: "pointer",
        boxShadow: "0 2px 6px rgba(0,0,0,0.3)"
    })

    document.body.appendChild(btn)
}

// Register screen record command or show button
if (!isFirefox) {
    GM_registerMenuCommand("🖥️ Screen REC/SAVE", () => {
        isRecording ? stopCapture() : screenRecord()
    })
} else {
    createScreenRecButton()
}

// Scan for <video> tags every 5 seconds
setInterval(() => {
    document.querySelectorAll("video").forEach((videoEl, i) => {
        if (registeredVideos.has(videoEl)) return

        const label = videoEl.getAttribute("title") || videoEl.getAttribute("aria-label") || `Video ${i + 1}`
        GM_registerMenuCommand(`🎥 Video REC/SAVE: ${label}`, () => {
            isRecording ? stopCapture() : videoRecord(videoEl)
        })

        registeredVideos.add(videoEl)
        console.log(`🆕 Registered video: ${label}`)
    })
}, 5000)