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

SOOP 라이브 및 클립 창에서 화면 캡쳐시 플레이어 컨트롤러 부분을 보이지 않게 할 수 있도록 컨트롤러 비활성화/캡쳐 버튼을 추가합니다. (html2canvas 대신 drawImage 사용)

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

// ==UserScript==
// @name         SOOP 클립 및 라이브 깔끔하게 캡쳐
// @namespace    http://tampermonkey.net/
// @version      1.21
// @license      MIT
// @description  SOOP 라이브 및 클립 창에서 화면 캡쳐시 플레이어 컨트롤러 부분을 보이지 않게 할 수 있도록 컨트롤러 비활성화/캡쳐 버튼을 추가합니다. (html2canvas 대신 drawImage 사용)
// @author       Linseed, Gemini
// @match        https://stbbs.sooplive.co.kr/vodclip/create.php
// @match        https://play.sooplive.co.kr/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

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

    /**
     * 필요한 DOM 요소들이 로드될 때까지 기다립니다.
     * videoLayer와 player_ctrlBox가 모두 존재하면 main 함수를 실행합니다.
     */
    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');

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

    /**
     * 스크립트의 메인 로직을 실행합니다.
     * @param {HTMLElement} videoLayer - 캡쳐할 대상이 되는 비디오 레이어 요소
     * @param {HTMLElement} ctrlBox - 숨기거나 표시할 컨트롤 박스 요소
     * @param {HTMLElement | null} titleElement - 버튼을 추가할 제목 요소
     * @param {HTMLElement | null} nicknameElement - 버튼을 추가할 닉네임 요소
     */
    function main(videoLayer, ctrlBox, titleElement, nicknameElement) {
        // --- 1. 상태 변수 정의 ---
        let isLocked = false;

        // --- 2. UI 요소 생성 ---
        const container = document.createElement('div');
        container.id = 'soop-script-container';
        
        if (titleElement) {
            // logo class 다음에 버튼 컨테이너를 추가합니다.
            const logoElement = document.querySelector('.logo');
            if (logoElement) {
                logoElement.after(container);
            }
        } else if (nicknameElement) {
            // nickname class의 바로 뒤에 버튼 컨테이너를 추가합니다.
            nicknameElement.after(container);
        }

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

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

        // --- 3. UI 스타일 적용 ---
        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);
            }
        `);

        // --- 4. 핵심 기능 함수 정의 ---

        /**
         * 잠금 상태를 설정하고 UI를 업데이트합니다.
         * @param {boolean} locked - 설정할 잠금 상태 (true: 잠김, false: 열림)
         */
        const setLockState = (locked) => {
            isLocked = locked;
            lockButton.textContent = isLocked ? '🔒' : '🔓';
            // display 속성을 변경하여 컨트롤 박스를 숨기거나 표시합니다.
            ctrlBox.style.display = isLocked ? 'none' : '';
        };

        // --- 5. 이벤트 리스너 등록 ---

        // 잠금 버튼 클릭 이벤트
        lockButton.addEventListener('click', () => {
            setLockState(!isLocked);
        });

        // 캡쳐 버튼 클릭 이벤트
        captureButton.addEventListener('click', async () => {
            const originalLockState = isLocked;

            // 1. 캡쳐를 위해 컨트롤러를 일시적으로 숨깁니다.
            if (!originalLockState) {
                setLockState(true);
                // UI가 업데이트될 시간을 잠시 기다립니다 (100ms).
                await new Promise(resolve => setTimeout(resolve, 100));
            }

            // 2. 비디오 프레임을 캔버스에 그려서 캡쳐합니다.
            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 link = document.createElement('a');

                // 파일명에 날짜와 시간을 포함하여 중복을 방지합니다.
                const timestamp = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '');
                link.download = `soop-capture-${timestamp}.png`;
                link.href = canvas.toDataURL('image/png');
                link.click();

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