您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
!!!
// ==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" } */