// ==UserScript==
// @name bilibili vtb直播同传man字幕显示
// @version 202210431
// @description !!!
// @author siro
// @match http://live.bilibili.com/*
// @match https://live.bilibili.com/*
// @require https://cdn.staticfile.org/jquery/1.12.4/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/pako/1.0.10/pako.min.js
// @namespace http://www.xiaosiro.cn
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
//脚本多次加载这可能是因为目标页面正在加载帧或iframe。
//
//将这下行添加到脚本代码部分的顶部:
if (window.top != window.self) //-- Don't run on frames or iframes
return;
var room_id=22129083;//默认房间号
var uid=0;
var url;
var mytoken;
var port;
var rawHeaderLen = 16;
var packetOffset = 0;
var headerOffset = 4;
var verOffset = 6;
var opOffset = 8;
var seqOffset = 12;
var socket;
var utf8decoder = new TextDecoder();
var f=0; //不知道为什么会建立两次连接,用这个标记一下。
var zimuBottom=40;//修改此数值改变字幕距底部的高度
var zimuColor="#FFFFFF";//修改此处改变字幕颜色
var zimuFontSize=25;//修改此处改变字体大小
var zimuShadow=1;//启动弹幕阴影
var zimuShadowColor="#66CCFF"// 弹幕阴影颜色
var deltime=3000;//字幕存在时间
var IsSikiName=0;// 1为启动同传man过滤 0为不启动,默认不启动
//如果要启动同传man过滤,启动后需要修改SikiName里括号里的内容
//如SikiName=["斋藤飞鳥Offcial","小明1","小明2"],则只会显示名字为,斋藤飞鳥Offcial,小明1,小明2的同传
//此变量为字符串数字,元素为字符串变量,元素内容由 , 分隔(不是中文下的 ,)
var SikiName=["白峰さやか"];
var isSpecialRoom=false;
var isTop=false;// 默认生成在底部;
if(!document.getElementById("live-player-ctnr")){
console.log('特殊主题直播间,20s后执行脚本');
isSpecialRoom=true;
zimuBottom=zimuBottom+150;
setTimeout(()=>myCode(), 20000);
}else{
myCode();
}
function myCode(){
console.log("开始执行脚本");
// 创建页面字幕元素
var danmudiv=$('<div></div>');
danmudiv.attr('id','danmu');
var danmudivwidth;
if($("#live-player-ctnr")){
danmudivwidth=$("#live-player-ctnr").width();
}else{
danmudivwidth="900px";
}
console.log(danmudivwidth);
danmudiv.css({
"min-width":"100px",
"width":"100%",
"magin":"0 auto",
"position":"absolute",
"left":"0px",
"bottom":zimuBottom+"px",
"z-index":"14",
"color":zimuColor,
"font-size": zimuFontSize+"px",
"text-align":"center",
"font-weight": "bold",
"pointer-events":"none",
"text-shadow":"0 0 0.2em #F87, 0 0 0.2em #F87",
});
if(isTop){
danmudiv.css("bottom","");
danmudiv.css("top",zimuBottom+"px");
}
if(!document.getElementById("live-player-ctnr")){
console.log('主页面无此元素,尝试注入父div...');//player-ctnr
//$("iframe:eq(1)").attr('id','danmulive')
console.log();
danmudiv.appendTo($("#player-ctnr"));
}else{
danmudiv.appendTo($("#live-player-ctnr"));
}
// 创建控制面板
var danmuControldiv=$('<div>字幕设置</div>');
danmuControldiv.attr('id','danmuControldiv');
danmuControldiv.css({
"height": "60px",
"top": "100px",
"left": "0",
"width": "16px",
"z-index": "999998",
"display": "flex",
"flex-direction": "column",
"justify-content": "center",
"align-items": "center",
"position": "fixed",
"transform": "translateY(-50%)",
"background":"#FFF",
"border-radius": "2px",
});
danmuControldiv.appendTo($("body"));
var danmuControlBody=$(`<div id="danmuControlBody" style="flex-direction:column;position: fixed;top: 100px;left: 0;width: 16px;z-index: 999999;display: none;padding: 5px;border-radius: 5px;border: 1px solid #0AADFF;width: 300px;background-color: #FFF;">
<label>字体大小:</label><input type="number">px<br>
<label>字幕颜色:</label><input type="color"><br>
<label>字幕高度:</label><input type="number">px<br>
<label>字幕阴影:</label><input type="checkbox"><br>
<label>字幕阴影颜色:</label><input type="color"><br>
<label>字幕显示在顶部:</label><input type="checkbox"><br>
<div style="margin:0 auto;width: 120px;margin-top: 5px;">
<input id="danmuControlOK" type="button" value="确定"> <input id="danmuControlOld" type="button" value="默认">
</div>
<div id="closeDiv" style="background-color: red;color: seashell;position: absolute;top: 3px;right: 3px;width: 15px;height: 15px;line-height: 15px;text-align: center;cursor: pointer;">x</div>
</div>`);
function upDanmudiv(){
danmudiv.css({
"bottom":zimuBottom+"px",
"color":zimuColor,
"font-size": zimuFontSize+"px",
"z-index": "999999",
});
if(zimuShadow==1){
danmudiv.css({
"text-shadow":"0 0 0.2em "+zimuShadowColor+", 0 0 0.2em "+zimuShadowColor,
});
}else{
danmudiv.css({
"text-shadow":"0 0 0",
});
}
if(isTop){
danmudiv.css("bottom","");
danmudiv.css("top",zimuBottom+"px");
}else{
danmudiv.css("bottom",zimuBottom+"px");
}
}
function bindDanmuDate(){
var inputs=$("#danmuControlBody").children("input");
inputs[0].value=zimuFontSize;
inputs[1].value=zimuColor;
if(isSpecialRoom){
inputs[2].value=zimuBottom-150;
}else{
inputs[2].value=zimuBottom;
}
inputs[3].checked=(zimuShadow==0?false:true);
inputs[4].value=zimuShadowColor;
inputs[5].value= (isTop==0?false:true);
}
function saveDanmuDate(){
var inputs=$("#danmuControlBody").children("input");
zimuFontSize=inputs[0].value;
zimuColor=inputs[1].value;
if(isSpecialRoom){
zimuBottom=inputs[2].value;
zimuBottom+=150;
}else{
zimuBottom=inputs[2].value;
}
zimuShadow=(inputs[3].checked?1:0);
zimuShadowColor=inputs[4].value;
isTop=(inputs[5].checked?1:0);
upDanmudiv();
}
danmuControlBody.appendTo($("body"));
$("#danmuControldiv").on('click', function () {
$("#danmuControlBody").css("display","flex");
bindDanmuDate();
}
);
$("#closeDiv").on('click', function () {
$("#danmuControlBody").css("display","none");
}
);
$("#danmuControlOK").on('click', function () {
saveDanmuDate();
}
);
$("#danmuControlOld").on('click', function () {
zimuBottom=40;//修改此数值改变字幕距底部的高度
zimuColor="#FF0000";//修改此处改变字幕颜色
zimuFontSize=25;//修改此处改变字体大小
zimuShadow=1;//启动弹幕阴影
zimuShadowColor="#000F87"// 弹幕阴影颜色
upDanmudiv();
}
);
//获取当前房间编号
var UR = document.location.toString();
var arrUrl = UR.split("//");
var start = arrUrl[1].indexOf("/");
var relUrl = arrUrl[1].substring(start+1);//stop省略,截取从start开始到结尾的所有字符
if(relUrl.indexOf("?") != -1){
relUrl = relUrl.split("?")[0];
}
room_id=parseInt(relUrl);
//获取你的uid
$.ajax({
url: 'https://api.live.bilibili.com/xlive/web-ucenter/user/get_user_info',
type: 'GET',
dataType: 'json',
success: function (data) {
//console.log(data.data);
uid=data.data.uid;
//console.log(uid);
},
xhrFields: {
withCredentials: true // 这里设置了withCredentials
},
});
//获取真实房间号
$.ajax({
url: '//api.live.bilibili.com/room/v1/Room/room_init?id=' + room_id,
type: 'GET',
dataType: 'json',
success: function (data) {
room_id=data.data.room_id;
}
});
//获取弹幕连接和token
$.ajax({
url: '//api.live.bilibili.com/room/v1/Danmu/getConf?room_id='+room_id+'&platform=pc&player=web',
type: 'GET',
dataType: 'json',
success: function (data) {
url = data.data.host_server_list[1].host;
port = data.data.host_server_list[1].wss_port;
mytoken = data.data.token;
DanmuSocket();
},
xhrFields: {withCredentials: true}
})
// 蜜汁字符转换
function txtEncoder(str){
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i = 0, strlen = str.length; i < strlen; i++) {
bufView[i] = str.charCodeAt(i);
}
return bufView;
}
// 合并
function mergeArrayBuffer(ab1, ab2) {
var u81 = new Uint8Array(ab1),
u82 = new Uint8Array(ab2),
res = new Uint8Array(ab1.byteLength + ab2.byteLength);
res.set(u81, 0);
res.set(u82, ab1.byteLength);
return res.buffer;
}
//发送心跳包
function heartBeat() {
var headerBuf = new ArrayBuffer(rawHeaderLen);
var headerView = new DataView(headerBuf, 0);
var ob="[object Object]";
var bodyBuf = txtEncoder(ob);
headerView.setInt32(packetOffset, rawHeaderLen + bodyBuf.byteLength);
headerView.setInt16(headerOffset, rawHeaderLen);
headerView.setInt16(verOffset, 1);
headerView.setInt32(opOffset, 2);
headerView.setInt32(seqOffset, 1);
//console.log('发送信条');
socket.send(mergeArrayBuffer(headerBuf, bodyBuf));
};
// 导入css
var style = document.createElement("style");
style.type = "text/css";
var text = document.createTextNode(`#danmu .message {
transition: height 0.2s ease-in-out, margin 0.2s ease-in-out;
}
#danmu .message .text {
text-align:center;
font-weight: bold;
pointer-events:none;
}
@keyframes message-move-in {
0% {
opacity: 0;
transform: translateY(100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
#danmu .message.move-in {
animation: message-move-in 0.3s ease-in-out;
}
@keyframes message-move-out {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-100%);
}
}
#danmu .message.move-out {
animation: message-move-out 0.3s ease-in-out;
animation-fill-mode: forwards;
}`
);
style.appendChild(text);
var head = document.getElementsByTagName("head")[0];
head.appendChild(style);
// 消息渲染器
class Message {
//构造函数
constructor() {
const containerId = 'danmu';
this.containerEl = document.getElementById(containerId);
}
show({text = '' ,duration = 2000}) {
// 创建一个Element对象
let messageEl = document.createElement('div');
// 设置消息class,这里加上move-in可以直接看到弹出效果
messageEl.className = 'message move-in';
// 消息内部html字符串
messageEl.innerHTML = `
<div class="text">${text}</div>
`;
// 追加到message-container末尾
// this.containerEl属性是我们在构造函数中创建的message-container容器
this.containerEl.appendChild(messageEl);
// 用setTimeout来做一个定时器
setTimeout(() => {
// 首先把move-in这个弹出动画类给移除掉,要不然会有问题,可以自己测试下
messageEl.className = messageEl.className.replace('move-in', '');
// 增加一个move-out类
messageEl.className += 'move-out';
// move-out动画结束后把元素的高度和边距都设置为0
// 由于我们在css中设置了transition属性,所以会有一个过渡动画
messageEl.addEventListener('animationend', () => {
messageEl.setAttribute('style', 'height: 0; margin: 0');
});
// 这个地方是监听动画结束事件,在动画结束后把消息从dom树中移除。
// 如果你是在增加move-out后直接调用messageEl.remove,那么你不会看到任何动画效果
//messageEl.addEventListener('transitionend', () => {
// // Element对象内部有一个remove方法,调用之后可以将该元素从dom树种移除!
// messageEl.remove();
//});
// 以上方法似乎无效,所以用一个定时器来完成
setTimeout(() => {
messageEl.remove();
}, duration+10000);
}, duration);
}
}
const message = new Message();
//数据包解析 感谢https://github.com/lovelyyoshino/Bilibili-Live-API/blob/master/API.WebSocket.md
const textEncoder = new TextEncoder('utf-8');
const textDecoder = new TextDecoder('utf-8');
const readInt = function(buffer,start,len){
let result = 0
for(let i=len - 1;i >= 0;i--){
result += Math.pow(256,len - i - 1) * buffer[start + i]
}
return result
}
const writeInt = function(buffer,start,len,value){
let i=0
while(i<len){
buffer[start + i] = value/Math.pow(256,len - i - 1)
i++
}
}
function encode(str,op){
let data = textEncoder.encode(str);
let packetLen = 16 + data.byteLength;
let header = [0,0,0,0,0,16,0,1,0,0,0,op,0,0,0,1]
writeInt(header,0,4,packetLen)
return (new Uint8Array(header.concat(...data))).buffer
}
function decode(blob) {
let buffer = new Uint8Array(blob)
let result = {}
result.packetLen = readInt(buffer, 0, 4)
result.headerLen = readInt(buffer, 4, 2)
result.ver = readInt(buffer, 6, 2)
result.op = readInt(buffer, 8, 4)
result.seq = readInt(buffer, 12, 4)
if (result.op === 5) {
result.body = []
let offset = 0;
while (offset < buffer.length) {
let packetLen = readInt(buffer, offset + 0, 4)
let headerLen = 16// readInt(buffer,offset + 4,4)
if (result.ver == 2) {
let data = buffer.slice(offset + headerLen, offset + packetLen);
let newBuffer =pako.inflate(new Uint8Array(data));
const obj = decode(newBuffer);
const body = obj.body;
result.body = result.body.concat(body);
} else {
let data = buffer.slice(offset + headerLen, offset + packetLen);
let body = textDecoder.decode(data);
if (body) {
result.body.push(JSON.parse(body));
}
}
offset += packetLen;
}
} else if (result.op === 3) {
result.body = {
count: readInt(buffer, 16, 4)
};
}
return result;
}
// socket连接
function DanmuSocket() {
var ws = 'wss';
if(f){
return;
}
socket = new WebSocket(ws + '://' + url + ':' + port + '/sub');
f=1;
socket.binaryType = 'arraybuffer';
// Connection opened
socket.addEventListener('open', function (event) {
console.log('Danmu WebSocket Server Connected.');
console.log('Handshaking...');
var token = JSON.stringify({
'uid': uid,
'roomid': room_id,
'key': mytoken,
'protover':1,
});
var headerBuf = new ArrayBuffer(rawHeaderLen);
var headerView = new DataView(headerBuf, 0);
var bodyBuf = txtEncoder(token);
headerView.setInt32(packetOffset, rawHeaderLen + bodyBuf.byteLength);
headerView.setInt16(headerOffset, rawHeaderLen);
headerView.setInt16(verOffset, 1);
headerView.setInt32(opOffset, 7);
headerView.setInt32(seqOffset, 1);
socket.send(mergeArrayBuffer(headerBuf, bodyBuf));
// heartBeat();
var Id = setInterval(function () {
heartBeat();
}, 30*1000);
});
socket.addEventListener('error', function (event) {
console.log('WebSocket 错误: ', event);
socket.close();
f=0;
console.log('WebSocket 重连 ');
DanmuSocket();
});
socket.addEventListener('close', function (event) {
console.log('WebSocket 关闭 ');
f=0;
sleep(5000);
console.log('WebSocket 重连 ');
DanmuSocket();
});
// Listen for messages
socket.addEventListener('message', function (msgEvent) {
const packet = decode(msgEvent.data);
switch (packet.op) {
case 8:
//console.log('加入房间');
break;
case 3:
//console.log(`人气`);
break;
case 5:
packet.body.forEach((body)=>{
switch (body.cmd) {
case 'DANMU_MSG':
var tongchuan= body.info[1];
var manName=body.info[2][1];
//message.show({
// text: tongchuan,
// duration: deltime,
// });
if(tongchuan.indexOf("【") != -1){
tongchuan=tongchuan.replace("【"," ");
tongchuan=tongchuan.replace("】","");
if(!IsSikiName){
//console.log("显示字幕");
message.show({
text: tongchuan,
duration: deltime,
});
}else if((SikiName.indexOf(manName)>-1)){
message.show({
text: tongchuan,
duration:deltime,
});
}
}
//console.log(`${body.info[2][1]}: ${body.info[1]}`);
break;
case 'SEND_GIFT':
//console.log(`${body.data.uname} ${body.data.action} ${body.data.num} 个 ${body.data.giftName}`);
break;
case 'WELCOME':
//console.log(`欢迎 ${body.data.uname}`);
break;
// 此处省略很多其他通知类型
default:
//console.log(body);
}
})
break;
}
});
}
};
// 延迟执行
/* 弹幕json示例
{
"info": [
[
0,
1,
25,
16777215,
1526267394,
-1189421307,
0,
"46bc1d5e",
0
],
"空投!",
[
10078392,
"白の驹",
0,
0,
0,
10000,
1,
""
],
[
11,
"狗雨",
"宫本狗雨",
102,
10512625,
""
],
[
23,
0,
5805790,
">50000"
],
[
"title-111-1",
"title-111-1"
],
0,
0,
{
"uname_color": ""
}
],
"cmd": "DANMU_MSG"
}
*/