// ==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);