siro / bilibili vtb直播同传man字幕显示

// ==UserScript==
// @name      bilibili vtb直播同传man字幕显示
// @version   202005202
// @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
// @copyright 2020, siro (https://openuserjs.org/users/siro)
// @license MIT
// ==/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="40px";//修改此数值改变字幕距底部的高度
var zimuColor="red";//修改此处改变字幕颜色
var zimuFontSize="25px";//修改此处改变字体大小
var deltime=3000;//字幕存在时间
var IsSikiName=0;// 1为启动同传man过滤 0为不启动,默认不启动
//如果要启动同传man过滤,启动后需要修改SikiName里括号里的内容
//如SikiName=["斋藤飞鳥Offcial","小明1","小明2"],则只会显示名字为,斋藤飞鳥Offcial,小明1,小明2的同传
//此变量为字符串数字,元素为字符串变量,元素内容由 , 分隔(不是中文下的 ,)
var SikiName=["白峰さやか"];

// 创建页面字幕元素
var danmudiv=$('<div></div>');
danmudiv.attr('id','danmu');
danmudiv.css({
    "min-width":"100px",
    "width":$("#live-player-ctnr").width(),
    "magin":"0 auto",
    "position":"absolute",
    "left":"0px",
    "bottom":zimuBottom,
    "z-index":"14",
    "color":zimuColor,
    "font-size": zimuFontSize,
    "text-align":"center",
    "font-weight": "bold",
    "pointer-events":"none",
});
danmudiv.appendTo($("#live-player-ctnr"));

//获取当前房间编号
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();
            });
        }, duration);
    }

}

const message = new Message();

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;
         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];
                            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"
}
*/