哔哩哔哩画中画弹幕

哔哩哔哩画中画支持显示弹幕

// ==UserScript==
// @name         哔哩哔哩画中画弹幕
// @namespace    qwq0
// @version      0.27
// @description  哔哩哔哩画中画支持显示弹幕
// @author       QwQ~
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/medialist/play/*
// @match        https://www.bilibili.com/list/*
// @match        https://www.bilibili.com/bangumi/play/*
// @match        https://live.bilibili.com/*
// @match        https://www.acfun.cn/v/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// ==/UserScript==

setTimeout(function ()
{
    "use strict";
    function setValue(name, value)
    {
        if (window["GM_setValue"])
            GM_setValue(name, value);
    }
    function getValue(name, defaultValue)
    {
        return (window["GM_getValue"] ? GM_getValue(name, defaultValue) : defaultValue);
    }

    let videoHolder = null;
    let video = null;

    let width = 0;
    let height = 0;

    let canvas = document.createElement("canvas");
    let context = canvas.getContext("2d");
    let canvasWidth = canvas.width = 0;
    let canvasHeight = canvas.height = 0;
    let danmuFontsize = 0;
    let textCanvasArray = Array(3).fill(0).map(() => document.createElement("canvas"));
    let textCanvasContextArray = textCanvasArray.map(o => o.getContext("2d"));
    let nVideo = document.createElement("video");

    let timeoutId = 0;
    let forceRequestAnimationFrame = true;

    let isFirefox = navigator.userAgent.indexOf("Firefox") > -1;

    if (isFirefox)
    {
        document.body.appendChild(nVideo);
        nVideo.style.position = "fixed";
        nVideo.style.zIndex = 10000;
        nVideo.style.left = "0";
        nVideo.style.top = "85px";
        nVideo.style.width = "30px";
        nVideo.style.height = "30px";
        nVideo.style.backgroundColor = "rgba(0, 0, 0, 0.3)";
        timeoutId = setTimeout(draw, Math.floor(1000 / 60));
    }
    let lastPlayOrPauseTime = 0;
    nVideo.addEventListener("play", () =>
    {
        if (video && performance.now() - lastPlayOrPauseTime > 15)
        {
            lastPlayOrPauseTime = performance.now();
            video.play();
        }
    });
    nVideo.addEventListener("pause", () =>
    {
        if (video && performance.now() - lastPlayOrPauseTime > 15)
        {
            lastPlayOrPauseTime = performance.now();
            video.pause();
        }
    });

    let customDanmuMaxLine = getValue("danmuMaxLine", 12);

    let danmuList = [];
    let danmuLineMaxX = [];
    let danmuLineLock = [];
    let danmuMaxLine = Number(customDanmuMaxLine ? customDanmuMaxLine : 12);
    let danmuMaxCount = 50;
    let danmuLogOutput = false;
    let danmuRendering = false;

    if (!Number.isInteger(danmuMaxLine))
        danmuMaxLine = 12;

    async function addDanmu(text, color)
    {
        if (
            text != "" &&
            timeoutId &&
            !danmuRendering &&
            textCanvasContextArray.length > 0 &&
            danmuList.length <= danmuMaxCount &&
            (danmuList.length <= 20 || Math.random() < 20 / danmuList.length)
        )
        {
            let lineNum = 0;
            for (let i = 0; i < danmuMaxLine; i++)
            {
                lineNum = i;
                if (!(danmuLineMaxX[lineNum] > canvasWidth || danmuLineLock[lineNum]))
                    break;
                else if (i + 1 == danmuMaxLine)
                    return;
            }
            if (!color)
                color = "rgb(255, 255, 255)";

            let textCanvasContext = textCanvasContextArray.pop();
            let textCanvas = textCanvasContext.canvas;

            let textWidth = textCanvasContext.measureText(text).width;
            textCanvasContext.clearRect(0, 0, textWidth, danmuFontsize);
            textCanvasContext.fillStyle = color;
            textCanvasContext.fillText(text, 0, 0);
            if (textWidth > 0)
            {
                danmuLineLock[lineNum] = true;
                danmuList.push({ text: text, color: color, x: canvasWidth, y: lineNum * danmuFontsize, l: lineNum, w: textWidth, i: await createImageBitmap(textCanvas, 0, 0, textWidth, danmuFontsize) });
                danmuLineLock[lineNum] = false;
                danmuLineMaxX[lineNum] = canvasWidth + textWidth;
            }
            textCanvasContextArray.push(textCanvasContext);
        }
    }
    let danmuObserver = new MutationObserver(e =>
    {
        e.forEach(o =>
        {
            // console.log("danmu(all)", o);
            if (o.type == "childList")
            {
                o.addedNodes.forEach(ele =>
                {
                    // console.log("danmu(ele)", ele);
                    if (ele.innerText)
                    {
                        let text = String(ele.innerText);
                        let color = ele.style.color;
                        if (!color)
                            color = ele.style.getPropertyValue("--color");
                        if (ele.style.opacity != "0")
                            addDanmu(text.split("\n")[0], color);
                        if (danmuLogOutput)
                            console.log("danmu(it)", color, text, ele);
                    }
                    else if (ele.textContent)
                    {
                        let text = String(ele.textContent);
                        let color = o.target.style.color;
                        if (!color)
                            color = o.target.style.getPropertyValue("--color");
                        addDanmu(text.split("\n")[0], color);
                        if (danmuLogOutput)
                            console.log("danmu(ct)", color, text, ele);
                    }
                });
            }
        });
    });

    let danmuHolder = null;
    let subtitlePanel = null;
    setInterval(() =>
    {
        let nowVideoHolder = document.getElementsByClassName("bilibili-player-video")[0] ||
            document.getElementsByClassName("bpx-player-video-wrap")[0] ||
            document.getElementById("live-player") ||
            document.getElementsByClassName("container-video")[0];

        if (!nowVideoHolder)
            return;

        let nowVideo = nowVideoHolder.getElementsByTagName("video")[0];
        if (nowVideo && video != nowVideo)
        {
            videoHolder = nowVideoHolder;
            video = nowVideo;
            video.addEventListener("play", () =>
            {
                console.log("[哔哩哔哩画中画弹幕]", "视频播放");
                nVideo.play();
            });
            video.addEventListener("pause", () =>
            {
                console.log("[哔哩哔哩画中画弹幕]", "视频暂停");
                nVideo.pause();
            });
            video.addEventListener("enterpictureinpicture", () =>
            {
                if (!timeoutId)
                {
                    timeoutId = setTimeout(draw, Math.floor(1000 / 60));
                    setTimeout(() =>
                    {
                        nVideo.requestPictureInPicture();
                        nVideo.play();
                    }, 250);
                }
                else
                {
                    nVideo.requestPictureInPicture();
                    nVideo.play();
                }
            });
            let style = document.createElement("style");
            style.innerText = `
            .bpx-player-ctrl-btn.bpx-player-ctrl-pip, .bilibili-player-video-btn.bilibili-player-video-btn-pip.closed
            {
                filter: drop-shadow(1px 1px 3px #49e3dc);
            }
            `;
            document.body.appendChild(style);
            if (navigator.mediaSession)
            {
                try
                {
                    navigator.mediaSession.setActionHandler("play", () =>
                    {
                        video.play();
                        nVideo.play();
                    });
                    navigator.mediaSession.setActionHandler("pause", () =>
                    {
                        video.pause();
                        nVideo.pause();
                    });
                }
                catch (err)
                {
                    console.warn("[哔哩哔哩画中画弹幕]", "绑定媒体功能键时发生错误");
                }
            }
        }

        let nowDanmuHolder = document.getElementsByClassName("bilibili-player-video-danmaku")[0] ||
            document.getElementsByClassName("bpx-player-row-dm-wrap")[0] ||
            document.getElementsByClassName("web-player-danmaku")[0] ||
            document.getElementsByClassName("danmaku-screen")[0];

        if (nowDanmuHolder != danmuHolder || width != video.videoWidth || height != video.videoHeight)
        {
            danmuHolder = nowDanmuHolder;
            danmuObserver.disconnect();
            width = video.videoWidth;
            height = video.videoHeight;
            canvasWidth = canvas.width = (Math.min(height, width) < 700 ? width : Math.floor(width / 2));
            canvasHeight = canvas.height = (Math.min(height, width) < 700 ? height : Math.floor(height / 2));
            danmuFontsize = Math.floor(Math.min(canvasWidth, canvasHeight) / 14.5);
            textCanvasArray.forEach(o =>
            {
                o.height = danmuFontsize;
                o.width = danmuFontsize * 35;
            });
            textCanvasContextArray.forEach(o =>
            {
                o.textBaseline = "top";
                o.shadowBlur = 3;
                o.shadowColor = "rgb(0, 0, 0)";
                o.font = `${danmuFontsize}px SimHei,"Microsoft JhengHei",Arial,Helvetica,sans-serif`;
            });


            nVideo.srcObject = canvas.captureStream(60);
            setTimeout(() =>
            {
                if (video && !video.paused)
                    nVideo.play();
            }, 1500);
            danmuObserver.observe(danmuHolder, { childList: true, subtree: true });
            console.log("[哔哩哔哩画中画弹幕]", "视频切换");
            console.log("[哔哩哔哩画中画弹幕]", "视频分辨率", width, height);
            console.log("[哔哩哔哩画中画弹幕]", "渲染分辨率", canvasWidth, canvasHeight);
        }


        let nowSubtitlePanel = document.getElementsByClassName("bpx-player-subtitle-panel-wrap")[0];
        subtitlePanel = nowSubtitlePanel;
    }, 900);

    let lastTime = performance.now();
    function draw()
    {
        let nowTime = performance.now();
        let timeInterval = nowTime - lastTime;
        lastTime = nowTime;
        if (video)
        {
            context.globalAlpha = 1;
            context.drawImage(video, 0, 0, width, height, 0, 0, canvasWidth, canvasHeight);
            if (video.readyState >= 1)
            {
                context.globalAlpha = 0.7;
                danmuLineMaxX.length = 0;
                danmuList = danmuList.filter(o =>
                {
                    if (!video.paused)
                        o.x -= timeInterval * danmuFontsize * 0.0035;
                    let rightX = o.x + o.w;
                    if (rightX <= 0)
                        return false;
                    context.drawImage(o.i, Math.round(o.x), Math.round(o.y));
                    if (!(rightX <= danmuLineMaxX[o.l]))
                        danmuLineMaxX[o.l] = rightX;
                    return true;
                });

                let subtitleText = (subtitlePanel ? subtitlePanel.innerText : "");
                if (subtitleText)
                {
                    context.globalAlpha = 0.5;
                    context.fillStyle = "rgb(0, 0, 0)";
                    let subtitleWidth = context.measureText(subtitleText).width;
                    context.fillRect((canvasWidth - subtitleWidth) / 2, canvasHeight - danmuFontsize * 1.5, subtitleWidth, danmuFontsize);

                    context.globalAlpha = 1;
                    context.textBaseline = "bottom";
                    context.textAlign = "center";
                    context.font = `${danmuFontsize}px SimHei,"Microsoft JhengHei",Arial,Helvetica,sans-serif`;
                    context.fillStyle = "rgb(255, 255, 255)";
                    context.fillText(subtitleText, canvasWidth / 2, canvasHeight - danmuFontsize * 0.5);
                }
            }
            else
            {
                danmuList.length = 0;
            }
        }
        if (forceRequestAnimationFrame || isFirefox)
            timeoutId = requestAnimationFrame(draw);
        else
            timeoutId = setTimeout(draw, Math.floor(1000 / 60));
    }
    let pipdmCommandObj = {
        help: () =>
        {
            console.log("[哔哩哔哩画中画弹幕]", ([
                "画中画弹幕插件指令帮助",
                "pipdm.maxLine 修改画中画弹幕最大行数",
                "pipdm.danmuLog 开启弹幕日志输出",
                "pipdm.help 显示此帮助文本"
            ]).join("\n"));
        },
        maxLine: () =>
        {
            let newValue = 0 | (prompt("设置画中画弹幕最大行数", danmuMaxLine));
            if (newValue != undefined && newValue > 0 && Number.isInteger(newValue))
            {
                if (newValue > 16)
                    danmuMaxLine = 16;
                else
                    danmuMaxLine = newValue;
                console.log("[哔哩哔哩画中画弹幕]", `已将画中画弹幕最大行数设置为 ${danmuMaxLine} 行`);
                setValue("danmuMaxLine", danmuMaxLine);
            }
            else
            {
                console.log("[哔哩哔哩画中画弹幕]", `设置的数值无效`);
            }
        },
        danmuLog: () =>
        {
            danmuLogOutput = !danmuLogOutput;
            console.log("[哔哩哔哩画中画弹幕]", `已${danmuLogOutput ? "开启" : "关闭"}弹幕日志输出`);
        }
    };
    (window["unsafeWindow"] ? unsafeWindow : window).pipdm = new Proxy({
        maxLine: "修改画中画弹幕最大行数",
        danmuLog: "开启弹幕日志输出",
        help: "显示指令帮助"
    }, {
        get: (target, key) =>
        {
            if (pipdmCommandObj[key])
                pipdmCommandObj[key]();
            else
                console.log("[哔哩哔哩画中画弹幕]", "不存在此指令\n输入 pipdm.help 以显示指令帮助");
            return () => { };
        }
    });

    console.log("[哔哩哔哩画中画弹幕]", "已加载");
    console.log("[哔哩哔哩画中画弹幕]", "输入 pipdm.help 以显示指令帮助");
}, 500);