SOOP 클립 및 라이브 깔끔하게 캡쳐

SOOP 라이브 및 클립 창에서 화면 캡쳐시 플레이어 컨트롤러 부분을 보이지 않게 할 수 있도록 컨트롤러 비활성화/캡쳐 버튼을 추가합니다. (단축키: Alt+L 컨트롤러 활성화/비활성화, Alt+P 컨트롤러 자동으로 숨기고 캡쳐)

当前为 2025-10-02 提交的版本,查看 最新版本

// ==UserScript==
// @name         SOOP 클립 및 라이브 깔끔하게 캡쳐
// @namespace    http://tampermonkey.net/
// @version      1.52
// @license      MIT
// @description  SOOP 라이브 및 클립 창에서 화면 캡쳐시 플레이어 컨트롤러 부분을 보이지 않게 할 수 있도록 컨트롤러 비활성화/캡쳐 버튼을 추가합니다. (단축키: Alt+L 컨트롤러 활성화/비활성화, Alt+P 컨트롤러 자동으로 숨기고 캡쳐)
// @author       Linseed, Gemini
// @match        https://stbbs.sooplive.co.kr/*
// @match        https://play.sooplive.co.kr/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addValueChangeListener
// @require      https://github.com/PRO-2684/GM_config/releases/download/v1.2.1/config.min.js#md5=525526b8f0b6b8606cedf08c651163c2

// ==/UserScript==

(function() {
    'use strict';
 
    const configDesc = {
        "$default": {
            autoClose: false
        },
        saveMethod: {
            name: "Save as / 저장방식",
            type: "enum",
            options: ["Clipboard", "File", "Both"],
            value: 0 // 기본값을 clipboard로 설정
        }
    }
    const config = new GM_config(configDesc, { immediate: false });

    // --- 스크립트 설정 ---
    const CHECK_INTERVAL = 500; // 플레이어 요소를 찾기 위한 반복 확인 간격 (ms)

    /**
     * 필요한 DOM 요소들이 로드될 때까지 기다립니다.
     */
    const waitForElements = setInterval(() => {
        const videoLayer = document.getElementById('videoLayer');
        const ctrlBox = document.querySelector('.player_ctrlBox');
        const titleElement = document.querySelector('.u_clip_title');
        const nicknameElement = document.querySelector('.nickname');
        const playerInfo = document.getElementById('player_info');
        const viewCtrl = document.querySelector('.view_ctrl');

        if (videoLayer && ctrlBox && (titleElement || nicknameElement)) {
            clearInterval(waitForElements);
            main(videoLayer, ctrlBox, titleElement, nicknameElement, playerInfo, viewCtrl);
        }
    }, CHECK_INTERVAL);

    /**
     * 스크립트의 메인 로직을 실행합니다.
     */
    function main(videoLayer, ctrlBox, titleElement, nicknameElement, playerInfo, viewCtrl) {
        let isLocked = false;

        const container = document.createElement('div');
        container.id = 'soop-script-container';
        
        if (titleElement) {
            titleElement.after(container);
        } else if (nicknameElement) {
            nicknameElement.after(container);
        }

        const lockButton = document.createElement('button');
        lockButton.id = 'lockBtn';
        lockButton.textContent = '🔓';
        lockButton.title = '컨트롤러 잠금/해제 (Alt + L)';
        container.appendChild(lockButton);

        const captureButton = document.createElement('button');
        captureButton.id = 'captureBtn';
        captureButton.textContent = '📷';
        captureButton.title = '현재 화면 캡쳐 (Alt + P)';
        container.appendChild(captureButton);

        GM_addStyle(`
            #soop-script-container { display: inline-flex; gap: 8px; margin-left: 10px; vertical-align: middle; }
            #soop-script-container button { padding: 4px 8px; font-size: 16px; border: 1px solid #ccc; border-radius: 6px; background-color: rgba(240, 240, 240, 0.85); cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: all 0.2s ease-in-out; line-height: 1; }
            #soop-script-container button:hover { background-color: rgba(255, 255, 255, 1); transform: translateY(-1px); }
        `);

        const setLockState = (locked) => {
            isLocked = locked;
            lockButton.textContent = isLocked ? '🔒' : '🔓';
            ctrlBox.style.display = isLocked ? 'none' : '';
            if (playerInfo) playerInfo.style.display = isLocked ? 'none' : '';
            if (viewCtrl) viewCtrl.style.display = isLocked ? 'none' : '';
        };

        lockButton.addEventListener('click', () => setLockState(!isLocked));

        captureButton.addEventListener('click', async () => {
            const originalLockState = isLocked;
            if (!originalLockState) {
                setLockState(true);
                await new Promise(resolve => setTimeout(resolve, 100));
            }

            try {
                const videoElement = videoLayer.querySelector('video');
                if (!videoElement) throw new Error('플레이어에서 <video> 요소를 찾을 수 없습니다.');

                const canvas = document.createElement('canvas');
                canvas.width = videoElement.videoWidth;
                canvas.height = videoElement.videoHeight;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);

                // --- 설정값에 따라 저장 방식 분기 ---
                const saveMethod = config.get('saveMethod');

                const saveToClipboard = (canvasEl) => {
                    return new Promise((resolve, reject) => {
                        canvasEl.toBlob(async (blob) => {
                            if (blob) {
                                try {
                                    await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
                                    resolve();
                                } catch (err) {
                                    reject(err);
                                }
                            } else {
                                reject(new Error('Canvas toBlob returned null.'));
                            }
                        }, 'image/png');
                    });
                };

                const saveToFile = (canvasEl) => {
                    const link = document.createElement('a');
                    const timestamp = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '');
                    link.download = `soop-capture-${timestamp}.png`;
                    link.href = canvasEl.toDataURL('image/png');
                    link.click();
                };

                if (saveMethod == 0 || saveMethod == 2) {
                    try {
                        await saveToClipboard(canvas);
                    } catch(e) {
                        console.error('클립보드 저장 실패:', e);
                        alert('클립보드 저장에 실패했습니다. 브라우저 콘솔을 확인해주세요.');
                    }
                }
                if (saveMethod == 1 || saveMethod == 2) {
                    saveToFile(canvas);
                }

            } catch (error) {
                console.error('스크립트 캡쳐 오류:', error);
                alert('화면 캡쳐에 실패했습니다. 비디오가 다른 도메인에서 재생되는 경우(CORS) 캡쳐가 불가능할 수 있습니다. 브라우저 콘솔을 확인해주세요.');
            } finally {
                if (!originalLockState) {
                    setLockState(false);
                }
            }
        });

        document.addEventListener('keydown', (event) => {
            if (!event.altKey) return;
            switch (event.key.toLowerCase()) {
                case 'l':
                    event.preventDefault();
                    lockButton.click();
                    break;
                case 'p':
                    event.preventDefault();
                    captureButton.click();
                    break;
            }
        });
    }
})();