bilibili vtb直播同传man字幕显示

!!!

  1. // ==UserScript==
  2. // @name bilibili vtb直播同传man字幕显示
  3. // @version 202210431
  4. // @description !!!
  5. // @author siro
  6. // @match http://live.bilibili.com/*
  7. // @match https://live.bilibili.com/*
  8. // @require https://cdn.staticfile.org/jquery/1.12.4/jquery.min.js
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/pako/1.0.10/pako.min.js
  10. // @namespace http://www.xiaosiro.cn
  11. // @grant unsafeWindow
  12. // @run-at document-idle
  13. // ==/UserScript==
  14.  
  15. //脚本多次加载这可能是因为目标页面正在加载帧或iframe。
  16. //
  17. //将这下行添加到脚本代码部分的顶部:
  18. if (window.top != window.self) //-- Don't run on frames or iframes
  19. return;
  20.  
  21. var room_id=22129083;//默认房间号
  22. var uid=0;
  23. var url;
  24. var mytoken;
  25. var port;
  26. var rawHeaderLen = 16;
  27. var packetOffset = 0;
  28. var headerOffset = 4;
  29. var verOffset = 6;
  30. var opOffset = 8;
  31. var seqOffset = 12;
  32. var socket;
  33. var utf8decoder = new TextDecoder();
  34. var f=0; //不知道为什么会建立两次连接,用这个标记一下。
  35. var zimuBottom=40;//修改此数值改变字幕距底部的高度
  36. var zimuColor="#FFFFFF";//修改此处改变字幕颜色
  37. var zimuFontSize=25;//修改此处改变字体大小
  38. var zimuShadow=1;//启动弹幕阴影
  39. var zimuShadowColor="#66CCFF"// 弹幕阴影颜色
  40. var deltime=3000;//字幕存在时间
  41. var IsSikiName=0;// 1为启动同传man过滤 0为不启动,默认不启动
  42. //如果要启动同传man过滤,启动后需要修改SikiName里括号里的内容
  43. //如SikiName=["斋藤飞鳥Offcial","小明1","小明2"],则只会显示名字为,斋藤飞鳥Offcial,小明1,小明2的同传
  44. //此变量为字符串数字,元素为字符串变量,元素内容由 , 分隔(不是中文下的 ,)
  45. var SikiName=["白峰さやか"];
  46. var isSpecialRoom=false;
  47. var isTop=false;// 默认生成在底部;
  48. if(!document.getElementById("live-player-ctnr")){
  49. console.log('特殊主题直播间,20s后执行脚本');
  50. isSpecialRoom=true;
  51. zimuBottom=zimuBottom+150;
  52. setTimeout(()=>myCode(), 20000);
  53. }else{
  54. myCode();
  55. }
  56.  
  57. function myCode(){
  58. console.log("开始执行脚本");
  59. // 创建页面字幕元素
  60. var danmudiv=$('<div></div>');
  61. danmudiv.attr('id','danmu');
  62. var danmudivwidth;
  63. if($("#live-player-ctnr")){
  64. danmudivwidth=$("#live-player-ctnr").width();
  65. }else{
  66. danmudivwidth="900px";
  67. }
  68. console.log(danmudivwidth);
  69. danmudiv.css({
  70. "min-width":"100px",
  71. "width":"100%",
  72. "magin":"0 auto",
  73. "position":"absolute",
  74. "left":"0px",
  75. "bottom":zimuBottom+"px",
  76. "z-index":"14",
  77. "color":zimuColor,
  78. "font-size": zimuFontSize+"px",
  79. "text-align":"center",
  80. "font-weight": "bold",
  81. "pointer-events":"none",
  82. "text-shadow":"0 0 0.2em #F87, 0 0 0.2em #F87",
  83. });
  84.  
  85. if(isTop){
  86. danmudiv.css("bottom","");
  87. danmudiv.css("top",zimuBottom+"px");
  88. }
  89.  
  90. if(!document.getElementById("live-player-ctnr")){
  91. console.log('主页面无此元素,尝试注入父div...');//player-ctnr
  92.  
  93. //$("iframe:eq(1)").attr('id','danmulive')
  94. console.log();
  95. danmudiv.appendTo($("#player-ctnr"));
  96. }else{
  97. danmudiv.appendTo($("#live-player-ctnr"));
  98. }
  99.  
  100.  
  101. // 创建控制面板
  102. var danmuControldiv=$('<div>字幕设置</div>');
  103. danmuControldiv.attr('id','danmuControldiv');
  104. danmuControldiv.css({
  105. "height": "60px",
  106. "top": "100px",
  107. "left": "0",
  108. "width": "16px",
  109. "z-index": "999998",
  110. "display": "flex",
  111. "flex-direction": "column",
  112. "justify-content": "center",
  113. "align-items": "center",
  114. "position": "fixed",
  115. "transform": "translateY(-50%)",
  116. "background":"#FFF",
  117. "border-radius": "2px",
  118. });
  119. danmuControldiv.appendTo($("body"));
  120. 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;">
  121. <label>字体大小:</label><input type="number">px<br>
  122. <label>字幕颜色:</label><input type="color"><br>
  123. <label>字幕高度:</label><input type="number">px<br>
  124. <label>字幕阴影:</label><input type="checkbox"><br>
  125. <label>字幕阴影颜色:</label><input type="color"><br>
  126. <label>字幕显示在顶部:</label><input type="checkbox"><br>
  127. <div style="margin:0 auto;width: 120px;margin-top: 5px;">
  128. <input id="danmuControlOK" type="button" value="确定">&nbsp;&nbsp;&nbsp;&nbsp;<input id="danmuControlOld" type="button" value="默认">
  129. </div>
  130. <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>
  131. </div>`);
  132. function upDanmudiv(){
  133. danmudiv.css({
  134. "bottom":zimuBottom+"px",
  135. "color":zimuColor,
  136. "font-size": zimuFontSize+"px",
  137. "z-index": "999999",
  138. });
  139. if(zimuShadow==1){
  140. danmudiv.css({
  141. "text-shadow":"0 0 0.2em "+zimuShadowColor+", 0 0 0.2em "+zimuShadowColor,
  142. });
  143. }else{
  144. danmudiv.css({
  145. "text-shadow":"0 0 0",
  146. });
  147. }
  148.  
  149. if(isTop){
  150. danmudiv.css("bottom","");
  151. danmudiv.css("top",zimuBottom+"px");
  152. }else{
  153. danmudiv.css("bottom",zimuBottom+"px");
  154. }
  155. }
  156. function bindDanmuDate(){
  157. var inputs=$("#danmuControlBody").children("input");
  158. inputs[0].value=zimuFontSize;
  159. inputs[1].value=zimuColor;
  160. if(isSpecialRoom){
  161. inputs[2].value=zimuBottom-150;
  162. }else{
  163. inputs[2].value=zimuBottom;
  164. }
  165. inputs[3].checked=(zimuShadow==0?false:true);
  166. inputs[4].value=zimuShadowColor;
  167. inputs[5].value= (isTop==0?false:true);
  168. }
  169. function saveDanmuDate(){
  170. var inputs=$("#danmuControlBody").children("input");
  171. zimuFontSize=inputs[0].value;
  172. zimuColor=inputs[1].value;
  173. if(isSpecialRoom){
  174. zimuBottom=inputs[2].value;
  175. zimuBottom+=150;
  176. }else{
  177. zimuBottom=inputs[2].value;
  178. }
  179. zimuShadow=(inputs[3].checked?1:0);
  180. zimuShadowColor=inputs[4].value;
  181. isTop=(inputs[5].checked?1:0);
  182. upDanmudiv();
  183. }
  184. danmuControlBody.appendTo($("body"));
  185. $("#danmuControldiv").on('click', function () {
  186. $("#danmuControlBody").css("display","flex");
  187. bindDanmuDate();
  188. }
  189. );
  190. $("#closeDiv").on('click', function () {
  191. $("#danmuControlBody").css("display","none");
  192. }
  193. );
  194. $("#danmuControlOK").on('click', function () {
  195. saveDanmuDate();
  196. }
  197. );
  198. $("#danmuControlOld").on('click', function () {
  199. zimuBottom=40;//修改此数值改变字幕距底部的高度
  200. zimuColor="#FF0000";//修改此处改变字幕颜色
  201. zimuFontSize=25;//修改此处改变字体大小
  202. zimuShadow=1;//启动弹幕阴影
  203. zimuShadowColor="#000F87"// 弹幕阴影颜色
  204. upDanmudiv();
  205. }
  206. );
  207.  
  208. //获取当前房间编号
  209. var UR = document.location.toString();
  210. var arrUrl = UR.split("//");
  211. var start = arrUrl[1].indexOf("/");
  212. var relUrl = arrUrl[1].substring(start+1);//stop省略,截取从start开始到结尾的所有字符
  213. if(relUrl.indexOf("?") != -1){
  214. relUrl = relUrl.split("?")[0];
  215. }
  216. room_id=parseInt(relUrl);
  217.  
  218. //获取你的uid
  219. $.ajax({
  220. url: 'https://api.live.bilibili.com/xlive/web-ucenter/user/get_user_info',
  221. type: 'GET',
  222. dataType: 'json',
  223. success: function (data) {
  224. //console.log(data.data);
  225. uid=data.data.uid;
  226. //console.log(uid);
  227. },
  228. xhrFields: {
  229. withCredentials: true // 这里设置了withCredentials
  230. },
  231. });
  232. //获取真实房间号
  233. $.ajax({
  234. url: '//api.live.bilibili.com/room/v1/Room/room_init?id=' + room_id,
  235. type: 'GET',
  236. dataType: 'json',
  237. success: function (data) {
  238. room_id=data.data.room_id;
  239.  
  240. }
  241. });
  242. //获取弹幕连接和token
  243. $.ajax({
  244. url: '//api.live.bilibili.com/room/v1/Danmu/getConf?room_id='+room_id+'&platform=pc&player=web',
  245. type: 'GET',
  246. dataType: 'json',
  247. success: function (data) {
  248. url = data.data.host_server_list[1].host;
  249. port = data.data.host_server_list[1].wss_port;
  250. mytoken = data.data.token;
  251. DanmuSocket();
  252. },
  253. xhrFields: {withCredentials: true}
  254. })
  255. // 蜜汁字符转换
  256. function txtEncoder(str){
  257. var buf = new ArrayBuffer(str.length);
  258. var bufView = new Uint8Array(buf);
  259. for (var i = 0, strlen = str.length; i < strlen; i++) {
  260. bufView[i] = str.charCodeAt(i);
  261. }
  262. return bufView;
  263. }
  264. // 合并
  265. function mergeArrayBuffer(ab1, ab2) {
  266. var u81 = new Uint8Array(ab1),
  267. u82 = new Uint8Array(ab2),
  268. res = new Uint8Array(ab1.byteLength + ab2.byteLength);
  269. res.set(u81, 0);
  270. res.set(u82, ab1.byteLength);
  271. return res.buffer;
  272. }
  273.  
  274. //发送心跳包
  275. function heartBeat() {
  276. var headerBuf = new ArrayBuffer(rawHeaderLen);
  277. var headerView = new DataView(headerBuf, 0);
  278. var ob="[object Object]";
  279. var bodyBuf = txtEncoder(ob);
  280. headerView.setInt32(packetOffset, rawHeaderLen + bodyBuf.byteLength);
  281. headerView.setInt16(headerOffset, rawHeaderLen);
  282. headerView.setInt16(verOffset, 1);
  283. headerView.setInt32(opOffset, 2);
  284. headerView.setInt32(seqOffset, 1);
  285. //console.log('发送信条');
  286. socket.send(mergeArrayBuffer(headerBuf, bodyBuf));
  287. };
  288. // 导入css
  289.  
  290. var style = document.createElement("style");
  291. style.type = "text/css";
  292. var text = document.createTextNode(`#danmu .message {
  293. transition: height 0.2s ease-in-out, margin 0.2s ease-in-out;
  294. }
  295.  
  296. #danmu .message .text {
  297. text-align:center;
  298. font-weight: bold;
  299. pointer-events:none;
  300. }
  301.  
  302. @keyframes message-move-in {
  303. 0% {
  304. opacity: 0;
  305. transform: translateY(100%);
  306. }
  307. 100% {
  308. opacity: 1;
  309. transform: translateY(0);
  310. }
  311. }
  312.  
  313. #danmu .message.move-in {
  314. animation: message-move-in 0.3s ease-in-out;
  315. }
  316.  
  317.  
  318. @keyframes message-move-out {
  319. 0% {
  320. opacity: 1;
  321. transform: translateY(0);
  322. }
  323. 100% {
  324. opacity: 0;
  325. transform: translateY(-100%);
  326. }
  327. }
  328. #danmu .message.move-out {
  329. animation: message-move-out 0.3s ease-in-out;
  330. animation-fill-mode: forwards;
  331. }`
  332. );
  333. style.appendChild(text);
  334. var head = document.getElementsByTagName("head")[0];
  335. head.appendChild(style);
  336.  
  337. // 消息渲染器
  338. class Message {
  339. //构造函数
  340. constructor() {
  341. const containerId = 'danmu';
  342. this.containerEl = document.getElementById(containerId);
  343. }
  344.  
  345. show({text = '' ,duration = 2000}) {
  346. // 创建一个Element对象
  347. let messageEl = document.createElement('div');
  348. // 设置消息class,这里加上move-in可以直接看到弹出效果
  349. messageEl.className = 'message move-in';
  350. // 消息内部html字符串
  351. messageEl.innerHTML = `
  352. <div class="text">${text}</div>
  353. `;
  354. // 追加到message-container末尾
  355. // this.containerEl属性是我们在构造函数中创建的message-container容器
  356. this.containerEl.appendChild(messageEl);
  357.  
  358. // 用setTimeout来做一个定时器
  359. setTimeout(() => {
  360. // 首先把move-in这个弹出动画类给移除掉,要不然会有问题,可以自己测试下
  361. messageEl.className = messageEl.className.replace('move-in', '');
  362. // 增加一个move-out类
  363. messageEl.className += 'move-out';
  364.  
  365. // move-out动画结束后把元素的高度和边距都设置为0
  366. // 由于我们在css中设置了transition属性,所以会有一个过渡动画
  367. messageEl.addEventListener('animationend', () => {
  368. messageEl.setAttribute('style', 'height: 0; margin: 0');
  369. });
  370.  
  371. // 这个地方是监听动画结束事件,在动画结束后把消息从dom树中移除。
  372. // 如果你是在增加move-out后直接调用messageEl.remove,那么你不会看到任何动画效果
  373. //messageEl.addEventListener('transitionend', () => {
  374. // // Element对象内部有一个remove方法,调用之后可以将该元素从dom树种移除!
  375. // messageEl.remove();
  376. //});
  377. // 以上方法似乎无效,所以用一个定时器来完成
  378. setTimeout(() => {
  379. messageEl.remove();
  380. }, duration+10000);
  381. }, duration);
  382. }
  383.  
  384. }
  385.  
  386. const message = new Message();
  387.  
  388.  
  389. //数据包解析 感谢https://github.com/lovelyyoshino/Bilibili-Live-API/blob/master/API.WebSocket.md
  390. const textEncoder = new TextEncoder('utf-8');
  391. const textDecoder = new TextDecoder('utf-8');
  392.  
  393. const readInt = function(buffer,start,len){
  394. let result = 0
  395. for(let i=len - 1;i >= 0;i--){
  396. result += Math.pow(256,len - i - 1) * buffer[start + i]
  397. }
  398. return result
  399. }
  400.  
  401. const writeInt = function(buffer,start,len,value){
  402. let i=0
  403. while(i<len){
  404. buffer[start + i] = value/Math.pow(256,len - i - 1)
  405. i++
  406. }
  407. }
  408.  
  409. function encode(str,op){
  410. let data = textEncoder.encode(str);
  411. let packetLen = 16 + data.byteLength;
  412. let header = [0,0,0,0,0,16,0,1,0,0,0,op,0,0,0,1]
  413. writeInt(header,0,4,packetLen)
  414. return (new Uint8Array(header.concat(...data))).buffer
  415. }
  416. function decode(blob) {
  417. let buffer = new Uint8Array(blob)
  418. let result = {}
  419. result.packetLen = readInt(buffer, 0, 4)
  420. result.headerLen = readInt(buffer, 4, 2)
  421. result.ver = readInt(buffer, 6, 2)
  422. result.op = readInt(buffer, 8, 4)
  423. result.seq = readInt(buffer, 12, 4)
  424. if (result.op === 5) {
  425. result.body = []
  426. let offset = 0;
  427. while (offset < buffer.length) {
  428. let packetLen = readInt(buffer, offset + 0, 4)
  429. let headerLen = 16// readInt(buffer,offset + 4,4)
  430. if (result.ver == 2) {
  431. let data = buffer.slice(offset + headerLen, offset + packetLen);
  432. let newBuffer =pako.inflate(new Uint8Array(data));
  433. const obj = decode(newBuffer);
  434. const body = obj.body;
  435. result.body = result.body.concat(body);
  436. } else {
  437. let data = buffer.slice(offset + headerLen, offset + packetLen);
  438. let body = textDecoder.decode(data);
  439. if (body) {
  440. result.body.push(JSON.parse(body));
  441. }
  442. }
  443. offset += packetLen;
  444. }
  445. } else if (result.op === 3) {
  446. result.body = {
  447. count: readInt(buffer, 16, 4)
  448. };
  449. }
  450. return result;
  451. }
  452.  
  453. // socket连接
  454. function DanmuSocket() {
  455. var ws = 'wss';
  456. if(f){
  457. return;
  458. }
  459. socket = new WebSocket(ws + '://' + url + ':' + port + '/sub');
  460. f=1;
  461. socket.binaryType = 'arraybuffer';
  462.  
  463. // Connection opened
  464. socket.addEventListener('open', function (event) {
  465. console.log('Danmu WebSocket Server Connected.');
  466. console.log('Handshaking...');
  467. var token = JSON.stringify({
  468. 'uid': uid,
  469. 'roomid': room_id,
  470. 'key': mytoken,
  471. 'protover':1,
  472. });
  473. var headerBuf = new ArrayBuffer(rawHeaderLen);
  474. var headerView = new DataView(headerBuf, 0);
  475. var bodyBuf = txtEncoder(token);
  476. headerView.setInt32(packetOffset, rawHeaderLen + bodyBuf.byteLength);
  477. headerView.setInt16(headerOffset, rawHeaderLen);
  478. headerView.setInt16(verOffset, 1);
  479. headerView.setInt32(opOffset, 7);
  480. headerView.setInt32(seqOffset, 1);
  481. socket.send(mergeArrayBuffer(headerBuf, bodyBuf));
  482. // heartBeat();
  483. var Id = setInterval(function () {
  484. heartBeat();
  485. }, 30*1000);
  486. });
  487.  
  488. socket.addEventListener('error', function (event) {
  489. console.log('WebSocket 错误: ', event);
  490. socket.close();
  491. f=0;
  492. console.log('WebSocket 重连 ');
  493. DanmuSocket();
  494. });
  495.  
  496. socket.addEventListener('close', function (event) {
  497. console.log('WebSocket 关闭 ');
  498. f=0;
  499. sleep(5000);
  500. console.log('WebSocket 重连 ');
  501. DanmuSocket();
  502. });
  503.  
  504. // Listen for messages
  505. socket.addEventListener('message', function (msgEvent) {
  506. const packet = decode(msgEvent.data);
  507. switch (packet.op) {
  508. case 8:
  509. //console.log('加入房间');
  510. break;
  511. case 3:
  512. //console.log(`人气`);
  513. break;
  514. case 5:
  515. packet.body.forEach((body)=>{
  516. switch (body.cmd) {
  517. case 'DANMU_MSG':
  518. var tongchuan= body.info[1];
  519. var manName=body.info[2][1];
  520. //message.show({
  521. // text: tongchuan,
  522. // duration: deltime,
  523. // });
  524. if(tongchuan.indexOf("【") != -1){
  525. tongchuan=tongchuan.replace("【"," ");
  526. tongchuan=tongchuan.replace("】","");
  527. if(!IsSikiName){
  528. //console.log("显示字幕");
  529. message.show({
  530. text: tongchuan,
  531. duration: deltime,
  532. });
  533. }else if((SikiName.indexOf(manName)>-1)){
  534. message.show({
  535. text: tongchuan,
  536. duration:deltime,
  537. });
  538. }
  539.  
  540. }
  541. //console.log(`${body.info[2][1]}: ${body.info[1]}`);
  542. break;
  543. case 'SEND_GIFT':
  544. //console.log(`${body.data.uname} ${body.data.action} ${body.data.num} 个 ${body.data.giftName}`);
  545. break;
  546. case 'WELCOME':
  547. //console.log(`欢迎 ${body.data.uname}`);
  548. break;
  549. // 此处省略很多其他通知类型
  550. default:
  551. //console.log(body);
  552. }
  553. })
  554. break;
  555. }
  556. });
  557. }
  558.  
  559. };
  560.  
  561. // 延迟执行
  562.  
  563.  
  564. /* 弹幕json示例
  565. {
  566. "info": [
  567. [
  568. 0,
  569. 1,
  570. 25,
  571. 16777215,
  572. 1526267394,
  573. -1189421307,
  574. 0,
  575. "46bc1d5e",
  576. 0
  577. ],
  578. "空投!",
  579. [
  580. 10078392,
  581. "白の驹",
  582. 0,
  583. 0,
  584. 0,
  585. 10000,
  586. 1,
  587. ""
  588. ],
  589. [
  590. 11,
  591. "狗雨",
  592. "宫本狗雨",
  593. 102,
  594. 10512625,
  595. ""
  596. ],
  597. [
  598. 23,
  599. 0,
  600. 5805790,
  601. ">50000"
  602. ],
  603. [
  604. "title-111-1",
  605. "title-111-1"
  606. ],
  607. 0,
  608. 0,
  609. {
  610. "uname_color": ""
  611. }
  612. ],
  613. "cmd": "DANMU_MSG"
  614. }
  615. */

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址