// ==UserScript==
// @name CC chat launcher
// @namespace http://tampermonkey.net/
// @homepage https://gist.github.com/whh4git/b2ab24441a2329e1569e806a3aca9704
// @version 0.4
// @description 爬取CC直播的聊天内容并将其转换成弹幕
// @author You
// @match http://cc.163.com/*
// @match https://cc.163.com/*
// @icon data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAAgACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD4c8MeHItUmL2ynHSWAj5098V7L4Q+FqafdadPq4Gj2c4WSG41KF4o542GQyFgAwwR0PQitL4f+EtOmhkvr+3ltzawtO1xGjI4VQWPUc8Doa+44/BfxF8UR/DWDTdJ0+6s9Ms4bS6muoQGjijhEb+W2ODuC859fpXHh6yqScqb2Pn8vxFLHSnOm2uXRrpqfEXxE+EU114N8R6z4Zgj1zTNOaOO71DTwPKty7KoBbPJJdeBnGea+WNW8Pp4fZhP81w33v8ACv1r1v4U+MvidP4otLPXofDun28d5pNzoM1xE0tzdRbJIZdqMcgFNwBwxU9BjNfm18UPhxcafeC6uLiGeWUb2jeQDYT1Uj1B4/CvpqV8dKXPNQ82/wAj23KMLRk7I674By6x8UvEy6NoluzLFC11cFrhkJRCBjcTjlivHfpX2ddfF/40fCnw7DZ6y95Z6HIgWJtY05HtWDHIH2iMKcnsPMDV4R/wTt8NrpOn+MtQl5uj9iiO0ZeKHdKXC8H5iBnp1Va/Q3S/GHjfUtEig0qxl1Dw/LG1vb3UtiC5jRAMspGCS4K/dxtzxnmvl6eHjGP7t2OjK5YbAU3CFKM4yd3ff79z42+HnjjxlpHjHUZfCFpp8HifxHKYhJpentJctuYuyIZ5ZQik5ZjgcDJOAMfG/wC0N8PvEfgTxrqVjqvzXfmG4KrKWyshLAhiBnksCfVT6V+w3hTwdrHhPxFe6ronhC10maRniE1vpcMUjx+acgEABMr5Z6AE5JHHHwb+2cBrGleFtf1C1C3dxNe2c20gk7ZAUAYAZAxJjgfePHar5ZU4uUnc9LGYmjipJUqSivvf3vp5H//Z
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
let LAUNCH_SPEED = GM_getValue('LAUNCH_SPEED', 10000) // 弹幕速度(通过屏幕时间)
let TEXT_COLOR = GM_getValue('TEXT_COLOR', '#FFFFFF') // 字体颜色
let TEXT_OPACITY = GM_getValue('TEXT_OPACITY', 0.9) // 字体不透明度
let TEXT_SIZE = GM_getValue('TEXT_SIZE', 22) // 字体大小
let TEXT_WEIGHT = GM_getValue('TEXT_WEIGHT', 700) // 字体加粗
let TEXT_AREA = parseInt(GM_getValue('TEXT_AREA', 4)) // 显示区域
let TEXT_LAYOUT = parseInt(GM_getValue('TEXT_LAYOUT', 1)) // 弹幕布局
let TEXT_FAMILY = GM_getValue('TEXT_FAMILY', 'inherit') // 字体族,目前只能手动设置
let MAX_TEXT_REPEAT_NUM = parseInt(GM_getValue('MAX_TEXT_REPEAT_NUM', Number.MAX_SAFE_INTEGER)) // 最大重复弹幕数
const areaText = {
1: '1/4屏',
2: '1/2屏',
3: '3/4屏',
4: '全屏',
}
const layoutText = {
1: '顶部优先',
2: '均匀排布',
3: '中部优先',
4: '竖向文字',
}
const orbit_queue = [0]
const text_repeat_map = {}
let orbit_mapper = get_orbit_mapper(TEXT_LAYOUT)
let text_screen_num = 0
let orbit_next = 0;
let containerHeight = 1 // 容器高度
// 轮询等待元素出现
function waitElement(selector, callback, timeout = 5000) {
const POLLING_CYCLE = 500
if (!selector || !callback) {
return
}
let once = 0
let timerId = setInterval(() => {
once += POLLING_CYCLE;
let target = document.querySelector(selector)
if (target) {
clearInterval(timerId)
callback(target)
}
if (once > timeout) {
clearInterval(timerId)
}
}, POLLING_CYCLE)
}
// 随机ID
function randomID() {
return 'id' + parseInt(Math.random() * Number.MAX_SAFE_INTEGER).toString();
}
function get_orbit_mapper(tag) {
switch (layoutText[tag]) {
case '均匀排布':// 均匀排布
return function (i, orbit_num, orbit_begin) {
return (i + orbit_begin) % orbit_num
}
case '中部优先':// 中部优先
return function (i, orbit_num) {
if (i % 2 == 0) {
return parseInt(orbit_num / 2) - parseInt(i / 2)
} else {
return parseInt(orbit_num / 2) + parseInt(i / 2) + 1
}
}
default:// 顶部优先
return function (i) {
return i
}
}
}
// 计算弹道,尽量实现不覆盖
// 长弹幕和短弹幕速度不一致,只算了结尾没算开头,还是有可能覆盖
function orbit_calc(wrapWidth, textWidth) {
let orbit_heigt = TEXT_SIZE * (1 + 1 / (2 + parseInt(text_screen_num / 22)))
let orbit_num = parseInt((containerHeight / orbit_heigt) * (TEXT_AREA / 4));
let now = new Date().getTime();
let head_over = now + ((wrapWidth - textWidth) / wrapWidth) * LAUNCH_SPEED;
let tail_over = now + LAUNCH_SPEED + (TEXT_SIZE * 2 * LAUNCH_SPEED / wrapWidth)
let orbit_selected = orbit_mapper(0, orbit_next, orbit_num);
for (let i = 0; i < orbit_num; i++) {
let orbit_i = orbit_mapper(i, orbit_num, orbit_next)
if (!orbit_queue[orbit_i] || orbit_queue[orbit_i] <= head_over) {
orbit_selected = orbit_i;
break;
}
if (orbit_queue[orbit_i] < orbit_queue[orbit_selected]) {
orbit_selected = orbit_i;
}
}
orbit_next = (orbit_selected + 1);
orbit_queue[orbit_selected] = tail_over;
return `${orbit_selected * orbit_heigt}px`
}
// 发射!
function launcher(container, chat_text) {
if ((text_repeat_map[chat_text] || 0) < MAX_TEXT_REPEAT_NUM) {
let text = document.createElement('span');
let wrap = document.createElement('div');
text.innerText = chat_text
Object.assign(text.style, {
'margin-top': '1px',
'white-space': 'nowrap',
'user-select': 'none',
'color': TEXT_COLOR,
'opacity': TEXT_OPACITY,
'font-size': `${TEXT_SIZE}px`,
'font-weight': TEXT_WEIGHT,
'font-family': TEXT_FAMILY,
'text-shadow': '-1px 0 #000, 0 1px #000, 1px 0 #000, 0 -1px #000',
})
Object.assign(wrap.style, {
'position': 'absolute',
'padding-left': '100%',
'transition': `all ${(LAUNCH_SPEED / 1000)}s linear`,
})
wrap.append(text)
container.append(wrap)
Object.assign(wrap.style, {
'top': orbit_calc(wrap.clientWidth, text.clientWidth),
'transform': 'translateX(-100%)',
})
// 计数
if (text_repeat_map[chat_text]) {
text_repeat_map[chat_text]++
} else {
text_repeat_map[chat_text] = 1
}
text_screen_num++
setTimeout(() => {
container.removeChild(wrap)
// 计数
text_repeat_map[chat_text]--
if (!text_repeat_map[chat_text]) {
delete text_repeat_map[chat_text]
}
text_screen_num--
}, LAUNCH_SPEED)
}
}
// 设置聊天消息加载器
function loader(container) {
new MutationObserver((mutationList) => {
mutationList.forEach((mutation) => {
if (mutation.type == 'childList') {
mutation.addedNodes.forEach((node) => {
let chat_text = node.querySelector('.screen-msg-wrap li.chat_item>div>span');
if (chat_text) {
launcher(container, chat_text.innerText)
}
})
}
});
}).observe(document.querySelector('.chat-list-wrap #js-chat-list-ul'), { childList: true })
}
// 创建弹幕容器
function create_container() {
let container = document.createElement('div')
Object.assign(container.style, {
'position': 'absolute',
'top': '0px',
'left': '0px',
'width': '100%',
'height': '100%',
})
waitElement("#js-chat-list-ul", () => {
loader(container)
}, 5000)
return container;
}
// 创建弹幕控制器
function create_controls() {
let ids = {
speed: { show: randomID(), value: randomID() },
color: { show: randomID(), value: randomID() },
opacity: { show: randomID(), value: randomID() },
size: { show: randomID(), value: randomID() },
weight: { show: randomID(), value: randomID() },
area: { show: randomID(), value: randomID() },
layout: { show: randomID(), value: randomID() },
repeat_num: { show: randomID(), value: randomID() },
}
let controls = document.createElement('div');
controls.className = 'video-player-control-item video-player-vbrs'
Object.assign(controls.style, { display: 'block' })
let controls_html = `
<style type="text/css">
div.video-player-control-item.video-player-vbrs:hover
div.video-player-vbr-list.player-comment-panel-wrapper{display: initial;}
</style>
<div>
<div class="video-player-vbr-current">弹幕设置</div>
</div>
<div class="video-player-vbr-list player-comment-panel-wrapper" style="width: max-content;">
<div style="display: table;">
<div style="display: table-cell;">
<div>
<p>弹幕颜色 <span id="${ids.color.show}">${TEXT_COLOR}</span></p>
<input id="${ids.color.value}" type="color" value="${TEXT_COLOR}">
</div>
<div>
<p>不透明度 <span id="${ids.opacity.show}">${TEXT_OPACITY * 100}%</span></p>
<input id="${ids.opacity.value}" type="range" max="1.0" min="0.1" step="0.1" value="${TEXT_OPACITY}">
</div>
<div>
<p>弹幕字号 <span id="${ids.size.show}">${TEXT_SIZE}</span>px</p>
<input id="${ids.size.value}" type="range" max="72" min="12" step="1" value="${TEXT_SIZE}">
</div>
<div>
<p>字体加粗 <span id="${ids.weight.show}">${TEXT_WEIGHT}</span></p>
<input id="${ids.weight.value}" type="range" max="1000" min="100" step="100" value="${TEXT_WEIGHT}">
</div>
</div>
<div style="display: table-cell;width: 32px;"></div>
<div style="display: table-cell;">
<div>
<p>弹幕速度 <span id="${ids.speed.show}">${LAUNCH_SPEED / 1000}</span>s</p>
<input id="${ids.speed.value}" type="range" max="20000" min="5000" step="1000" value="${LAUNCH_SPEED}">
</div>
<div>
<p>显示区域 <span id="${ids.area.show}">${areaText[TEXT_AREA]}</span></p>
<input id="${ids.area.value}" type="range" max="4" min="1" step="1" value="${TEXT_AREA}">
</div>
<div>
<p>弹幕布局 <span id="${ids.layout.show}">${layoutText[TEXT_LAYOUT]}</span></p>
<input id="${ids.layout.value}" type="range" max="3" min="1" step="1" value="${TEXT_LAYOUT}">
</div>
<div>
<p>重复弹幕数
<span id="${ids.repeat_num.show}">${(MAX_TEXT_REPEAT_NUM < 9) ? MAX_TEXT_REPEAT_NUM : '无限'}</span>
</p>
<input id="${ids.repeat_num.value}" type="range" max="10" min="1" step="1" value="${MAX_TEXT_REPEAT_NUM}">
</div>
</div>
</div>
</div>`
controls.innerHTML = controls_html
controls.querySelector(`#${ids.speed.value}`).addEventListener('input', (event) => {
LAUNCH_SPEED = event.target.valueAsNumber;
GM_setValue('LAUNCH_SPEED', LAUNCH_SPEED)
controls.querySelector(`#${ids.speed.show}`).innerText = LAUNCH_SPEED / 1000;
});
controls.querySelector(`#${ids.color.value}`).addEventListener('input', (event) => {
TEXT_COLOR = event.target.value;
GM_setValue('TEXT_COLOR', TEXT_COLOR)
controls.querySelector(`#${ids.color.show}`).innerText = TEXT_COLOR;
controls.querySelector(`#${ids.color.show}`).style.color = TEXT_COLOR
});
controls.querySelector(`#${ids.opacity.value}`).addEventListener('input', (event) => {
TEXT_OPACITY = event.target.valueAsNumber;
GM_setValue('TEXT_OPACITY', TEXT_OPACITY)
controls.querySelector(`#${ids.opacity.show}`).innerText = TEXT_OPACITY * 100 + '%';
controls.querySelector(`#${ids.opacity.show}`).style.opacity = TEXT_OPACITY
});
controls.querySelector(`#${ids.size.value}`).addEventListener('input', (event) => {
TEXT_SIZE = event.target.valueAsNumber;
GM_setValue('TEXT_SIZE', TEXT_SIZE)
controls.querySelector(`#${ids.size.show}`).innerText = TEXT_SIZE;
});
controls.querySelector(`#${ids.weight.value}`).addEventListener('input', (event) => {
TEXT_WEIGHT = event.target.valueAsNumber;
GM_setValue('TEXT_WEIGHT', TEXT_WEIGHT)
controls.querySelector(`#${ids.weight.show}`).innerText = TEXT_WEIGHT;
});
controls.querySelector(`#${ids.area.value}`).addEventListener('input', (event) => {
TEXT_AREA = parseInt(event.target.value);
GM_setValue('TEXT_AREA', TEXT_AREA)
controls.querySelector(`#${ids.area.show}`).innerText = areaText[TEXT_AREA];
});
controls.querySelector(`#${ids.layout.value}`).addEventListener('input', (event) => {
TEXT_LAYOUT = parseInt(event.target.value);
GM_setValue('TEXT_LAYOUT', TEXT_LAYOUT)
orbit_mapper = get_orbit_mapper(TEXT_LAYOUT)
controls.querySelector(`#${ids.layout.show}`).innerText = layoutText[TEXT_LAYOUT];
});
controls.querySelector(`#${ids.repeat_num.value}`).addEventListener('input', (event) => {
let value = parseInt(event.target.value);
MAX_TEXT_REPEAT_NUM = (value < 10) ? value : Number.MAX_SAFE_INTEGER;
GM_setValue('MAX_TEXT_REPEAT_NUM', MAX_TEXT_REPEAT_NUM)
controls.querySelector(`#${ids.repeat_num.show}`).innerText = (value < 10) ? value : '无限';
});
return controls;
}
// 等待播放器加载
waitElement(".cc-h5player-container", (playerContainer) => {
let container = create_container()
playerContainer.append(container)
containerHeight = container.clientHeight;
// 监听窗口大小变化
new MutationObserver((mutationList) => {
containerHeight = container.clientHeight;
console.log(containerHeight)
}).observe(document.querySelector('div.comment-canvas'), { attributes: true, attributeFilter: ['class', 'style'] })
}, 5000)
// 等待控制器加载
waitElement("div.video-player-controls-main", (controls) => {
controls.append(create_controls())
}, 5000)
// 去广告
let ad_list = [
'.video-watermark', '#pic-in-pic-btn > img', '#pic-in-pic-btn > span',
'#float-plugin-container-43751-4', '#live_left_bottom_box_wrap',
'#gift-bubbles', '#gift-banner', '.gift-banner', '.gift-simp-banner',
'#player-banner', '#new-player-banner',
'#mounts_mp4_player', '#mounts_player', '#mounts_banner', '#mounts_player_png'
]
let ad_style = document.createElement('style')
ad_style.setAttribute('type', 'text/css')
ad_style.innerText = `${ad_list.join(',')}{display:none!important;}`.replace("\n", '')
document.body.append(ad_style)
})();