YouTube 多重播放器

以新分頁或新視窗,同時播放複數Youtube影片。

// ==UserScript==
// @name         YouTube 多重播放器
// @namespace    http://tampermonkey.net/
// @version      4.3
// @match        https://www.youtube.com/
// @match        https://www.youtube.com/feed/*
// @match        https://www.youtube.com/playlist?list=*
// @match        https://www.youtube.com/@*
// @match        https://www.youtube.com/gaming
// @exclude      https://www.youtube.com/watch*
// @grant        none
// @license      MIT
// @description  以新分頁或新視窗,同時播放複數Youtube影片。
// ==/UserScript==

(function(){
    const validateURL = () => {
        const patterns = [
            /^https:\/\/www\.youtube\.com\/$/,
            /^https:\/\/www\.youtube\.com\/feed\/.*/,
            /^https:\/\/www\.youtube\.com\/playlist\?list=.*/,
            /^https:\/\/www\.youtube\.com\/@.*/,
            /^https:\/\/www\.youtube\.com\/gaming$/
        ];
        return patterns.some(p => p.test(window.location.href));
    };

    setTimeout(() => {
        if(!validateURL()){
            const panel = document.getElementById('ytMulti_panel');
            if(panel) panel.remove();
            return;
        }
    }, 60000);

    const STORAGE_POS = 'ytMulti_btnPos';
    const STORAGE_LIST1 = 'ytMulti_videoList1';
    const STORAGE_LIST2 = 'ytMulti_videoList2';
    const STORAGE_MODE = 'ytMulti_openMode';
    const STORAGE_CURRENT = 'ytMulti_currentList';
    let currentList = localStorage.getItem(STORAGE_CURRENT) || 'list1';

    const panel = document.createElement('div');
    panel.id = 'ytMulti_panel';
    panel.style.cssText = `
        position: fixed;
        background: rgba(0,0,0,0.8);
        color: #fff;
        padding: 8px 12px;
        border-radius: 8px;
        z-index: 9999;
        display: flex;
        align-items: center;
        cursor: move;
        gap: 10px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.2);
        font-family: Arial, sans-serif;
        backdrop-filter: blur(4px);
    `;
    document.body.appendChild(panel);

    const savedPos = JSON.parse(localStorage.getItem(STORAGE_POS) || 'null');
    if(savedPos){
        panel.style.top = savedPos.top;
        panel.style.left = savedPos.left;
        panel.style.right = 'auto';
    }

    panel.addEventListener('mousedown', e => {
        e.preventDefault();
        let startX = e.clientX, startY = e.clientY;
        const rect = panel.getBoundingClientRect();
        function onMove(ev){
            panel.style.top = rect.top + ev.clientY - startY + 'px';
            panel.style.left = rect.left + ev.clientX - startX + 'px';
        }
        function onUp(){
            localStorage.setItem(STORAGE_POS, JSON.stringify({top: panel.style.top, left: panel.style.left}));
            window.removeEventListener('mousemove', onMove);
            window.removeEventListener('mouseup', onUp);
        }
        window.addEventListener('mousemove', onMove);
        window.addEventListener('mouseup', onUp);
    });

    function createStyledButton(text){
        const btn = document.createElement('button');
        btn.textContent = text;
        btn.style.cssText = `
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            background: #ff0000;
            color: white;
            cursor: pointer;
            transition: all 0.2s;
            font-size: 14px;
            font-weight: 500;
            text-shadow: 0 1px 2px rgba(0,0,0,0.2);
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        `;
        btn.addEventListener('mouseover', () => btn.style.background = '#cc0000');
        btn.addEventListener('mouseout', () => btn.style.background = '#ff0000');
        return btn;
    }

    const playBtn = createStyledButton('▶ 播放');
    const modeBtn = createStyledButton(localStorage.getItem(STORAGE_MODE) === 'tab' ? '分頁' : '視窗');
    const listBtn = createStyledButton(currentList === 'list1' ? 'List1' : 'List2');

    panel.append(playBtn, modeBtn, listBtn);

    panel.addEventListener('dragover', e => e.preventDefault());
    panel.addEventListener('drop', e => {
        e.preventDefault();
        const data = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain');
        const vid = parseYouTubeID(data);
        if(!vid) return;

        const storageKey = currentList === 'list1' ? STORAGE_LIST1 : STORAGE_LIST2;
        const ids = JSON.parse(localStorage.getItem(storageKey) || '[]');
        if(!ids.includes(vid)){
            ids.push(vid);
            localStorage.setItem(storageKey, JSON.stringify(ids));
            listBtn.textContent = currentList === 'list1' ? `List1 (${ids.length})` : `List2 (${ids.length})`;
        }
    });

    modeBtn.addEventListener('click', () => {
        const mode = localStorage.getItem(STORAGE_MODE) === 'tab' ? 'window' : 'tab';
        localStorage.setItem(STORAGE_MODE, mode);
        modeBtn.textContent = mode === 'tab' ? '分頁' : '視窗';
    });

    listBtn.addEventListener('click', () => {
        currentList = currentList === 'list1' ? 'list2' : 'list1';
        localStorage.setItem(STORAGE_CURRENT, currentList);
        const storageKey = currentList === 'list1' ? STORAGE_LIST1 : STORAGE_LIST2;
        const count = JSON.parse(localStorage.getItem(storageKey) || '[]').length;
        listBtn.textContent = currentList === 'list1' ? `List1 (${count})` : `List2 (${count})`;
    });

    playBtn.addEventListener('click', () => {
        const storageKey = currentList === 'list1' ? STORAGE_LIST1 : STORAGE_LIST2;
        const ids = JSON.parse(localStorage.getItem(storageKey) || '[]');
        if(!ids.length) return alert('當前清單無影片');

        const html = makeBlobPage(ids, currentList);
        const blobUrl = URL.createObjectURL(new Blob([html], {type: 'text/html'}));
        const mode = localStorage.getItem(STORAGE_MODE);
        mode === 'tab' ? window.open(blobUrl, '_blank') : window.open(blobUrl, '_blank', 'width=800,height=600');
    });

    function parseYouTubeID(url){
        const m = url.match(/(?:v=|youtu\.be\/)([A-Za-z0-9_-]{11})/);
        return m ? m[1] : null;
    }

    function makeBlobPage(ids, listKey){
        const listJson = JSON.stringify(ids);
        return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>多重播放</title><style>
            body{margin:0;padding:0;background:#000;overflow:hidden;}
            .container{position:absolute;top:0;left:0;width:100vw;height:100vh;display:flex;flex-wrap:wrap;align-content:flex-start;}
            .video-wrapper{position:relative;overflow:hidden;will-change:transform;}
            .video-wrapper iframe{width:100%;height:100%;border:none;transform:scale(0.999);}
            .remove-btn{
                position:absolute;top:6px;right:6px;
                width:20px;height:20px;
                background:#ff4444;
                border-radius:3px;
                display:none;
                cursor:pointer;
                z-index:9999;
                box-shadow:0 0 3px rgba(0,0,0,0.3);
            }
            .remove-btn::after{
                content:'×';
                color:white;
                font-size:16px;
                position:absolute;
                top:50%;left:50%;
                transform:translate(-50%,-50%);
            }
            .video-wrapper:hover .remove-btn{display:block;}
        </style></head><body><div class="container"></div><script>
            const ASPECT_RATIO = 16/9;
            const ids = ${listJson};
            const listKey = ${JSON.stringify(listKey)};
            const container = document.querySelector('.container');

            function calculateLayout(){
                const W = container.offsetWidth;
                const H = container.offsetHeight;
                const n = ids.length;
                if(n === 0) return {cols:0, rows:0, itemWidth:0, itemHeight:0};

                let bestCols = 1;
                let bestRows = 1;
                let bestItemWidth = 0;
                let bestItemHeight = 0;
                let bestScore = 0;

                for(let cols=1; cols<=Math.min(n,12); cols++){
                    const rows = Math.ceil(n/cols);
                    let itemWidth = W/cols;
                    let itemHeight = itemWidth/ASPECT_RATIO;

                    if(rows*itemHeight > H){
                        itemHeight = H/rows;
                        itemWidth = itemHeight*ASPECT_RATIO;
                    }

                    const usedWidth = cols*itemWidth;
                    const usedHeight = rows*itemHeight;
                    const areaScore = usedWidth*usedHeight;
                    const penalty = (W-usedWidth)*0.1 + (H-usedHeight)*0.2;
                    const totalScore = areaScore - penalty;

                    if(totalScore > bestScore){
                        bestScore = totalScore;
                        bestCols = cols;
                        bestRows = rows;
                        bestItemWidth = itemWidth;
                        bestItemHeight = itemHeight;
                    }
                }

                return {cols:bestCols, rows:bestRows, itemWidth:bestItemWidth, itemHeight:bestItemHeight};
            }

            function updateLayout(){
                const {cols, rows, itemWidth, itemHeight} = calculateLayout();
                container.style.fontSize = '0';
                Array.from(container.children).forEach((wrap, index) => {
                    const col = index%cols;
                    const row = Math.floor(index/cols);
                    wrap.style.width = itemWidth + 'px';
                    wrap.style.height = itemHeight + 'px';
                    wrap.style.transform = \`translate(\${col*itemWidth}px, \${row*itemHeight}px)\`;
                });
            }

            function createVideo(id, idx){
                const wrap = document.createElement('div');
                wrap.className = 'video-wrapper';
                wrap.style.display = 'inline-block';
                wrap.style.position = 'absolute';

                const ifr = document.createElement('iframe');
                ifr.src = 'https://www.youtube.com/embed/'+id+'?autoplay=1&playsinline=1&rel=0&modestbranding=1&origin='+encodeURIComponent(window.location.origin);
                ifr.allow = 'autoplay; encrypted-media; fullscreen';
                ifr.allowfullscreen = true;

                const delBtn = document.createElement('div');
                delBtn.className = 'remove-btn';
                delBtn.onclick = () => {
                    const storageKey = listKey === 'list1' ? 'ytMulti_videoList1' : 'ytMulti_videoList2';
                    const stored = JSON.parse(localStorage.getItem(storageKey) || '[]');
                    stored.splice(stored.indexOf(id), 1);
                    localStorage.setItem(storageKey, JSON.stringify(stored));
                    wrap.remove();
                    updateLayout();
                };

                wrap.append(ifr, delBtn);
                return wrap;
            }

            ids.forEach((id, idx) => container.appendChild(createVideo(id, idx)));
            updateLayout();
            window.addEventListener('resize', updateLayout);
            setInterval(updateLayout, 500);
        <\/script></body></html>`;
    }

    const initListCount = () => {
        const count1 = JSON.parse(localStorage.getItem(STORAGE_LIST1) || '[]').length;
        const count2 = JSON.parse(localStorage.getItem(STORAGE_LIST2) || '[]').length;
        listBtn.textContent = currentList === 'list1' ? `List1 (${count1})` : `List2 (${count2})`;
    };
    initListCount();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址