Greasy Fork

Greasy Fork is available in English.

外挂弹幕插件

为任意网页播放器提供了加载本地弹幕的功能

目前为 2023-08-23 提交的版本。查看 最新版本

// ==UserScript==
// @name         外挂弹幕插件
// @version      0.1.1
// @description  为任意网页播放器提供了加载本地弹幕的功能
// @author       DeltaFlyer
// @copyright    2023, DeltaFlyer(https://github.com/DeltaFlyerW)
// @license      MIT
// @match        https://pan.baidu.com/pfile/video*
// @match        https://www.aliyundrive.com/drive/legacy*
// @match        https://www.tucao.cam/play/*
// @run-at       document-start
// @grant        unsafeWindow
// @icon         https://avatars.githubusercontent.com/u/1879224?v=4
// @require      https://cdn.jsdelivr.net/npm/@xpadev-net/[email protected]/dist/bundle.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/danmaku.min.js
// @namespace    http://greasyfork.icu/users/927887
// ==/UserScript==


(async function main() {
    async function waitForDOMContentLoaded() {
        return new Promise((resolve) => {
            console.log(document.readyState)
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', resolve);
            } else {
                resolve();
            }
        });
    }

    async function sleep(time) {
        await new Promise((resolve) => setTimeout(resolve, time));
    }

    await waitForDOMContentLoaded()
    let danmakuPlayer
    let toastText = (function () {
        let html = `
<style>
.df-bubble-container {
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 1000;
    display: block !important;
}

.df-bubble {
    background-color: #333;
    color: white;
    padding: 10px 20px;
    border-radius: 5px;
    margin-bottom: 10px;
    opacity: 0;
    transition: opacity 0.5s ease-in-out;
    max-width: 300px;
    box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.2);
    display: block !important;
}

.df-show-bubble {
    opacity: 1;
}
</style>
<div class="df-bubble-container" id="bubbleContainer"></div>`
        document.body.insertAdjacentHTML("beforeend", html)
        let bubbleContainer = document.querySelector('.df-bubble-container')

        function createToast(text) {
            console.log('toast', text)
            const bubble = document.createElement('div');
            bubble.classList.add('df-bubble');
            bubble.textContent = text;

            bubbleContainer.appendChild(bubble);
            setTimeout(() => {
                bubble.classList.add('df-show-bubble');
                setTimeout(() => {
                    bubble.classList.remove('df-show-bubble');
                    setTimeout(() => {
                        bubbleContainer.removeChild(bubble);
                    }, 500); // Remove the bubble after fade out
                }, 3000); // Show bubble for 3 seconds
            }, 100); // Delay before showing the bubble
        }

        return createToast
    })();
    let loadDanmaku = (function () {
        let [loadNicoCommentArt, clearNicoComment] = (function loadNicoCommentArt() {
            function buildCanvas() {
                // Get a reference to the existing element in the document
                let html = `
<style>
  #nico-canvas,
  #nico-container
   {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    height: 100%;
    width: 100%;
    object-fit: contain;
    pointer-events: none;
    z-index: 999;
  }
</style>
<div id="nico-container">
<canvas id="nico-canvas" width="1920" height="1080""></canvas>
</div>
`
                videoElem.parentElement.insertAdjacentHTML('beforeend', html);
                return videoElem.parentElement.querySelector("#nico-canvas")
            }

            let niconiComments
            let canvasElem
            let interval

            return [async function (comments) {
                if (!niconiComments) {
                    canvasElem = buildCanvas()
                    console.log('buildNicoCanvas', canvasElem)
                    niconiComments = new NiconiComments(canvasElem, [], {
                        mode: 'default',
                        keepCA: true,
                    });
                    interval = setInterval(() => {
                        niconiComments.drawCanvas(Math.floor(videoElem.currentTime * 100))
                    }, 10);
                }
                niconiComments.addComments(...comments)
                console.log('addCommentArt', niconiComments, comments)
            }, function () {
                if (canvasElem) {
                    canvasElem.parentElement.removeChild(canvasElem)
                    clearInterval(interval)
                    niconiComments = undefined
                    interval = undefined
                    canvasElem = undefined
                }
            }];
        })();


        function xmlunEscape(content) {
            return content.replace(';', ';')
                .replace(/&amp;/g, '&')
                .replace(/&lt;/g, '<')
                .replace(/&gt;/g, '>')
                .replace(/&apos;/g, "'")
                .replace(/&quot;/g, '"')
        }

        function findAll(inputString, regex) {
            const matches = [];
            let match;

            while ((match = regex.exec(inputString)) !== null) {
                matches.push(match);
            }
            return matches;
        }

        function xml2danmu(sdanmu) {
            const extraArgRegex = /(\S+?)\s*=\s*"(.*?)"/g
            let ldanmu = findAll(sdanmu, /<d p="(.*?)"(.*?)>(.*?)<\/d>/g);
            for (let i = 0; i < ldanmu.length; i++) {
                let danmu = ldanmu[i]
                let argv = danmu[1].split(',')
                let result = {
                    color: Number(argv[3]),
                    content: xmlunEscape(danmu[3]),
                    ctime: Number(argv[4]),
                    fontsize: Number(argv[2]),
                    id: Number(argv[7]),
                    idStr: argv[7],
                    midHash: argv[6],
                    mode: Number(argv[1]),
                    progress: Math.round(Number(argv[0]) * 1000),
                    weight: 8
                }
                if (danmu[2].length !== 0) {
                    for (let extraArg of findAll(danmu[2], extraArgRegex)) {
                        result[extraArg[1]] = xmlunEscape(extraArg[2])
                    }
                }
                ldanmu[i] = result
            }
            return ldanmu
        }

        let isCommentArt = (function () {
            let caCommands = ['full', 'patissier', 'ender', 'mincho', 'gothic', 'migi', 'hidari', 'shita']
            let caCharRegex = new RegExp(' ◥█◤■◯△×\u05C1\u0E3A\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u200B\u200C\u200D\u200E\u200F\u3000\u3164\u2580\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588\u2589\u258A\u258B\u258C\u258D\u258E\u258F\u2590\u2591\u2592\u2593\u2594\u2595\u2596\u2597\u2598\u2599\u259A\u259B\u259C\u259D\u259E\u259F\u25E2\u25E3\u25E4\u25E5'.split('').join('|'))

            return function (danmu) {
                let command = danmu.mail
                let content = danmu.content
                let isCommentArt = content.split("\n").length > 2;
                if (caCharRegex.exec(content)) {
                    isCommentArt = true
                }
                let lcommand = command.split(' ')
                for (let command of lcommand) {
                    switch (command) {
                        case  'owner': {
                            isCommentArt = true
                            danmu.owner = true
                            break
                        }
                        case caCommands.includes(command): {
                            isCommentArt = true
                            break
                        }
                        case command[0] === "@": {
                            isCommentArt = true
                            break
                        }
                    }
                }
                if (isCommentArt) {
                    return {
                        vpos: Math.round(danmu.progress / 10),
                        date: danmu.time,
                        content: danmu.content,
                        mail: danmu.mail.split(' ')
                    }
                }
            }
        })();

        function intToHexColor(colorInt) {
            const red = (colorInt >> 16) & 0xFF;
            const green = (colorInt >> 8) & 0xFF;
            const blue = colorInt & 0xFF;

            const hex = ((1 << 24) | (red << 16) | (green << 8) | blue).toString(16).slice(1);

            return `#${hex}`;
        }

        async function loadDanmaku(text) {
            let ldanmu = xml2danmu(text)
            console.log(ldanmu)
            toastText(`从文件中读取到${ldanmu.length}条弹幕`)
            let modeDict = {
                1: 'rtl',
                4: 'bottom',
                5: 'top'
            }
            let nicoCommentList = []
            let biliDanmakuList = []
            for (let danmu of ldanmu) {
                if (danmu.mail) {
                    let art = isCommentArt(danmu)
                    if (art) {
                        nicoCommentList.push(art)
                        continue
                    }
                }
                biliDanmakuList.push(
                    {
                        text: danmu.content,
                        time: danmu.progress / 1000,
                        mode: modeDict[danmu.mode],
                        style: {
                            fontSize: danmu.fontsize + 'px',
                            color: intToHexColor(danmu.color),
                            textShadow: '-1px -1px #000, -1px 1px #000, 1px -1px #000, 1px 1px #000'
                        }
                    }
                )
            }
            while (!danmakuPlayer) {
                await sleep(500)
            }
            for (let danmaku of biliDanmakuList) {
                danmakuPlayer.emit(danmaku)
            }
            if (nicoCommentList.length !== 0) {
                loadNicoCommentArt(nicoCommentList)
            }
        }

        return loadDanmaku
    })();

    (function createFileDropMask() {
        const mask = document.createElement('div');
        mask.id = "danmakuLoaderMask"
        mask.style.position = 'fixed';
        mask.style.top = '0';
        mask.style.left = '0';
        mask.style.width = '100%';
        mask.style.height = '100%';
        mask.style.backgroundColor = 'rgba(0, 0, 0, 0)';
        mask.style.zIndex = '9999';
        mask.style.pointerEvents = 'none';
        mask.style.opacity = '0';

        document.documentElement.insertBefore(mask, document.documentElement.firstChild);

        const handleFileDrop = function (event) {
            event.preventDefault();
            for (let file of event.dataTransfer.files) {
                const reader = new FileReader();
                reader.onload = function (event) {
                    console.log(['File content:', event.target.result]);
                    loadDanmaku(event.target.result)
                };
                reader.readAsText(file);
            }
        };

        document.addEventListener('dragover', function (event) {
            event.preventDefault();
            mask.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        });

        document.addEventListener('dragleave', function (event) {
            event.preventDefault();
            mask.style.backgroundColor = 'rgba(0, 0, 0, 0)';
        });

        document.addEventListener('drop', handleFileDrop);
    })();

    (function createToolbar(config) {
        let html = `
<style>
  body {
    margin: 0;
    padding: 0;
    font-family: Arial, sans-serif;
    overflow: hidden;
  }

  #triggerArea {
    position: fixed;
    top: 10%;
    left: 0;
    width: 10%;
    height: 80%;
    cursor: pointer;
  }

  #toolbar {
    position: fixed;
    top: 50%;
    left: -250px;
    transform: translateY(-50%);
    background-color: #333;
    color: #fff;
    padding: 10px;
    border-top-right-radius: 5px;
    border-bottom-right-radius: 5px;
    cursor: grab;
    transition: left 0.3s;
    z-index: 999999;
  }

  #toolbar:active {
    cursor: grabbing;
  }

  #toolbar button {
    display: block;
    margin: 5px 0;
    padding: 8px;
    background-color: #555;
    border: none;
    color: #fff;
    cursor: pointer;
    border-radius: 3px;
  }
</style>
<div id="triggerArea"></div>
<div id="toolbar"></div>
`

        function getToolbarSetting() {
            if (localStorage['dfToolbar']) {
                return JSON.parse(localStorage['dfToolbar'])
            } else {
                return {}
            }
        }

        function saveToolbarSetting(setting) {
            localStorage['dfToolbar'] = JSON.stringify(setting)
        }

        document.body.insertAdjacentHTML('beforeend', html)
        const triggerArea = document.getElementById('triggerArea');
        const toolbar = document.getElementById('toolbar');
        let isDragging = false;
        let isExpanded = false;
        let startY = 0;
        let initialTop = 0;
        let currentSetting = getToolbarSetting()
        if (currentSetting['offsetTopPercent']) {
            toolbar.offsetTop = currentSetting['offsetTopPercent'] * window.innerHeight
        }
        console.log('createToolbar', config)
        for (let option of Object.keys(config.options)) {
            let button = document.createElement("button")
            button.innerText = option
            button.addEventListener('click', config.options[option])
            toolbar.appendChild(button)
        }

        expandToolbar()
        setTimeout(collapseToolbar, 3000)

        function expandToolbar() {
            if (!isExpanded) {
                toolbar.style.left = '0';
                isExpanded = true;
            }
        }

        function collapseToolbar() {
            if (isExpanded) {
                toolbar.style.left = '-250px';
                isExpanded = false;
            }
        }

        triggerArea.addEventListener('mouseenter', () => {
            expandToolbar();
        });

        triggerArea.addEventListener('mouseleave', () => {
            collapseToolbar();
            if (isDragging) {
                isDragging = false
                dragEndHandle()
            }
        });

        toolbar.addEventListener('mouseenter', () => {
            expandToolbar();
        });

        toolbar.addEventListener('mouseleave', () => {
            if (!isDragging) {
                collapseToolbar();
            }
        });

        toolbar.addEventListener('mousedown', (e) => {
            if (e.target === toolbar) {
                console.log(e.type, e)
                isDragging = true;
                startY = e.clientY;
                initialTop = toolbar.offsetTop;
            }
        });


        let draggingHandle = (e) => {
            if (!isDragging) return;
            const deltaY = e.clientY - startY;
            toolbar.style.top = `${initialTop + deltaY}px`;
        }

        let dragEndHandle = (e) => {
            if (isDragging) {
                isDragging = false;
                let currentSetting = getToolbarSetting()
                currentSetting.offsetTopPercent = toolbar.offsetTop / window.innerHeight
                saveToolbarSetting(currentSetting)
            }
        }

        window.addEventListener('mousemove', draggingHandle);
        window.addEventListener('mouseup', dragEndHandle);
    })
    ({
        options: {
            "加载本地弹幕": function createFileSelector() {
                const input = document.createElement('input');
                input.type = 'file';

                return new Promise((resolve, reject) => {
                    input.addEventListener('change', (event) => {
                        for (let file of event.target.files) {
                            const reader = new FileReader();
                            reader.onload = function (event) {
                                console.log(['File content:', event.target.result]);
                                loadDanmaku(event.target.result)
                            };
                            reader.readAsText(file);
                        }
                        resolve()
                    });
                    input.click();
                });
            }
        }
    });

    function buildContainer(videoElem) {
        // Get a reference to the existing element in the document
        let html = `
<style>
  #danmaku-container
   {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    height: 100%;
    width: 100%;
    object-fit: contain;
    pointer-events: none;
    z-index: 999;
    line-height: 1.2;
  }
</style>
<div id="danmaku-container">
</div>
`
        videoElem.parentElement.insertAdjacentHTML('beforeend', html);
        return videoElem.parentElement.querySelector("#danmaku-container")
    }

    let videoElem

    async function startHook() {
        videoElem = null
        danmakuPlayer = null
        while (!videoElem) {
            let videos = document.querySelectorAll('video')
            for (let videoElement of videos) {
                if (!videoElement.paused) {
                    videoElem = videoElement
                    console.log(videoElement, videos, videoElement.paused)
                }
            }
            await sleep(500)
        }
        danmakuPlayer = new Danmaku({
            container: buildContainer(videoElem),
            media: videoElem,
            comments: []
        });
        toastText("danmakuPlayer initialed")
        console.log("danmakuPlayer inited", danmakuPlayer)

        let lastWidth = videoElem.offsetWidth
        unsafeWindow.danmaku = danmakuPlayer

        while (true) {
            if (videoElem.offsetWidth !== lastWidth) {
                console.log(lastWidth, videoElem.offsetWidth)
                if (videoElem.offsetWidth !== 0) {
                    danmakuPlayer.resize()
                    lastWidth = videoElem.offsetWidth
                } else {
                    danmakuPlayer.destroy()
                    toastText("danmakuPlayer destroyed")
                    break
                }
            }
            await sleep(500)
        }
    }

    while (true) {
        await startHook()
    }
})()