// ==UserScript==
// @name 外挂弹幕插件
// @version 0.2.5
// @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.alipan.com/drive/file/backup*
// @match https://g.alicdn.com/*
// @match https://www.tucao.cam/play/*
// @match *://*/m3u8.php*
// @match https://aniopen.an-i.workers.dev/*
// @run-at document-start
// @grant unsafeWindow
// @grant GM_setValue
// @grant GM_getValue
// @icon https://avatars.githubusercontent.com/u/1879224?v=4
// @require https://cdn.jsdelivr.net/npm/@xpadev-net/niconicomments@0.2.55/dist/bundle.min.js
// @require https://cdn.jsdelivr.net/npm/danmaku@2.0.6/dist/danmaku.min.js
// @namespace https://greasyfork.org/users/927887
// @downloadURL none
// ==/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 waitMessage(handler, timeoutMs) {
return new Promise((resolve, reject) => {
let timeoutId;
function handleMessage(event) {
if (handler(event.data)) {
// Remove the message event listener
window.removeEventListener('message', handleMessage);
// Clear the timeout
clearTimeout(timeoutId);
// Resolve the promise
resolve(event.data);
}
}
// Add a message event listener
window.addEventListener('message', handleMessage);
// Set a timeout to reject the promise if the timeout is reached
timeoutId = setTimeout(() => {
window.removeEventListener('message', handleMessage);
reject(new Error('Timeout reached'));
}, timeoutMs);
});
}
async function sleep(time) {
await new Promise((resolve) => setTimeout(resolve, time));
}
await waitForDOMContentLoaded()
let danmakuPlayer
let toastText = (function () {
let html = `
`
document.body.insertAdjacentHTML("beforeend", html)
let bubbleContainer = document.querySelector('.df-bubble-container')
function createToast(text) {
if (!document.getElementById(bubbleContainer.id)) {
document.body.insertAdjacentHTML("beforeend", html)
bubbleContainer = document.querySelector('.df-bubble-container')
}
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 = `
`
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(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/'/g, "'")
.replace(/"/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>/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
}
}
danmu.fontsize = 25
biliDanmakuList.push(
{
text: danmu.content,
time: danmu.progress / 1000,
mode: modeDict[danmu.mode],
style: {
originFontSize: danmu.fontsize,
fontSize: danmu.fontsize * currentSetting.scale + 'px',
color: intToHexColor(danmu.color),
textShadow: '-1px -1px #000, -1px 1px #000, 1px -1px #000, 1px 1px #000'
}
}
)
}
while (!danmakuPlayer) {
await sleep(500)
}
biliDanmakuList.sort((a, b) =>
b.id > a.id
)
for (let danmaku of biliDanmakuList) {
danmakuPlayer.emit(danmaku)
}
if (nicoCommentList.length !== 0) {
loadNicoCommentArt(nicoCommentList)
}
}
return loadDanmaku
})();
let settingPanelOptions = [
{type: 'slider', id: 'speed', label: "弹幕速度", range: [0.5, 1.5], default: 1},
{type: 'slider', id: 'scale', label: "字体大小", range: [0.5, 1.5], default: 1},
{type: 'slider', id: 'opacity', label: '不透明度', range: [0, 1], default: 1},
{
type: 'row', children: [
{
'type': 'numberInput', 'id': 'danmakuOffset', label: "弹幕延迟", default: 0,
},
{
'type': 'textSelector',
'id': 'maxHeight',
label: "显示区域",
optionText: ['25%', '50%', '75%', '100%'],
optionValue: ['25%', '50%', '75%', '100%'],
default: '100%'
},
]
}
]
let currentSetting = getLocalSetting("danmakuSetting")
setDefaultValue(currentSetting, settingPanelOptions)
function getLocalSetting(key) {
let value = GM_getValue(key)
console.log('get', key, value)
if (value) {
return value
} else {
return {}
}
}
function setDefaultValue(currentSetting, settingPanelOptions) {
for (let option of settingPanelOptions) {
if (option.id) {
if (!currentSetting[option.id]) {
currentSetting[option.id] = option.default
}
} else if (option.children) {
for (let child of option.children) {
if (child.id) {
if (!currentSetting[child.id]) {
currentSetting[child.id] = child.default
}
}
}
}
}
}
function saveLocalSetting(key, value) {
console.log('save', key, value)
GM_setValue(key, value)
}
let currentOffset = 0
function updateDanmakuOffset(value) {
for (let comment of danmakuPlayer.comments) {
comment.time = comment.time - currentOffset + value
}
danmakuPlayer.clear()
currentOffset = value
let message
if (currentOffset < 0) {
message = `弹幕提前${-currentOffset}秒出现`
} else if (currentOffset > 0) {
message = `弹幕延迟${currentOffset}秒出现`
} else {
message = '重置弹幕时间'
}
toastText(message)
}
let showSettingPanel = (function (settingPanelOptions, changeHandle) {
let panelStyles = `
`
// Create the setting panel HTML string based on the provided options
function createPanelHTML(options) {
let html = '
'
options.forEach(option => {
if (option.type === 'slider') {
html += `
${currentSetting[option.id] || option.default}
`;
} else if (option.type === 'equal-row' || option.type === 'row') {
html += `