bilibili vtb直播同传man字幕显示

!!!

// ==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="确定">&nbsp;&nbsp;&nbsp;&nbsp;<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"
}
*/