bilibili 多屏分控

bilibili控制多窗口

目前为 2023-09-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name bilibili 多屏分控
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description bilibili控制多窗口
  6. // @author svnzk
  7. // @match https://www.bilibili.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
  9. // @grant GM_addStyle
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13.  
  14. //***********************************
  15. // 配置项
  16.  
  17. // 是否自动网页全屏
  18. const player_web_max = true;
  19.  
  20.  
  21. //***********************************
  22.  
  23.  
  24.  
  25.  
  26.  
  27. const csstext = ".playlist{width:100px;position:fixed;top:20%;right:0;z-index:99999}@keyframes adding{from{margin-top:-50px}to{margin-top:10px}}@keyframes rmitem{0%{left:0}50%{left:100px;margin-top:10px}100%{left:100px;margin-top:-50px}}.playitem{width:190px;height:50px;background-color:aliceblue;border-top-left-radius:30px;border-bottom-left-radius:30px;margin:10px;position:relative;left:0;box-shadow:1px 1px 6px #6666;transition-duration:.5s;display:flex;align-items:center;animation:itemshow 1.5s}.rmitem{animation:rmitem 1s forwards!important}.prepend{animation:adding 1s,itemshow 1.5s}@keyframes itemshow{0%{left:100px}50%{left:-60px}100%{left:0}}.playitem:hover{left:-95px;box-shadow:1px 1px 7px #666a}.playitem>img{object-fit:cover;border-radius:5px;height:85%;margin:5px;margin-left:20px}.playitem>p{height:100%;overflow:hidden;font-size:12px;text-shadow:1px 1px 5px #666}.bi_ctrl_btn{position:fixed;top:300px;left:-120px;width:150px;height:30px;border-top-right-radius:50px;border-bottom-right-radius:50px;box-shadow:1px 1px 5px #666;transition:.5s;line-height:30px;text-shadow:1px 1px 5px #666;text-align: right;padding-right: 10px;}.ctrl_on{background-color:cadetblue;height:50px;left:0;text-align: left;}.ctrl_on>p{color:white}";
  28.  
  29. const id = getRandom();
  30. const bc = new BroadcastChannel("bili");
  31. const bcmsg = {};
  32.  
  33. function send() {
  34. bc.postMessage(bcmsg);
  35. bcmsg.stage = 0;
  36. bcmsg.url = 0;
  37. }
  38.  
  39. function getRandom() {return parseInt(Math.random()*100000);}
  40.  
  41. function findTag_a(elm) {
  42. if(elm.localName == "a") return elm;
  43. return findTag_a(elm.parentElement);
  44. }
  45.  
  46.  
  47.  
  48. // sleep函数延迟用
  49. const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
  50. // 异步等待播放器的网页全屏按键可用 然后点击
  51. async function playerWebMax() {
  52. let bt = document.querySelector(".bpx-player-ctrl-web");
  53. for(let t = 0;t<10; t++) {
  54. await sleep(1000);
  55. console.log("t=" + t);
  56. bt = document.querySelector(".bpx-player-ctrl-web");
  57. if(bt != null) {
  58. console.log(t + ")web max");
  59. bt.click();
  60. return;
  61. }
  62. }
  63. }
  64. // ========================= master =====================================
  65. // master类
  66. class BMaster {
  67. static fns = {};
  68. static start() {
  69. // 开始处理
  70. // 1. 向所有client发送hello
  71. bcmsg.stage = stage.helloClient;
  72. send();
  73. // 2. 处理鼠标事件
  74. // 接管左键
  75. document.body.onclick = BMaster.onClick;
  76.  
  77. // 接管中键
  78. document.body.onauxclick = BMaster.onClick;
  79.  
  80. // 接管右键
  81. // 这里只把菜单搞掉 因为事件已经在onclick触发了
  82. document.body.oncontextmenu = e => e.preventDefault();
  83.  
  84. // 创建函数列表
  85. BMaster.fns[stage.helloMaster] = BMaster.onHello;
  86. BMaster.fns[stage.hiMaster] = BMaster.onHi;
  87. BMaster.fns[stage.playEnded] = BMaster.onPlayEnded;
  88.  
  89. // 接管消息
  90. bc.onmessage = BMaster.onMsg;
  91.  
  92. // 给clients添加一个rm函数
  93. BMaster.clients.rm = (v) => {
  94. var pos = BMaster.clients.indexOf(v);
  95. if(pos != -1) BMaster.clients.splice(pos,1);
  96. }
  97. // 给ready添加一个rm函数
  98. BMaster.ready.rm = (v) => {
  99. var pos = BMaster.ready.indexOf(v);
  100. if(pos != -1) BMaster.ready.splice(pos,1);
  101. }
  102. }
  103.  
  104. // 接管点击事件
  105. static onClick(e) {
  106. // 这个函数需要处理3种点击事件
  107. // 1. 左键 click button=0
  108. // 2. 中键 auxclick button=1
  109. // 3. 右键 auxclick button=2 type=contextmenu
  110. e.preventDefault();
  111. // 如果点击了空白处 就不处理了
  112. if(e.srcElement.localName == "div") return;
  113.  
  114.  
  115. var tag_a = findTag_a(e.srcElement);
  116.  
  117.  
  118. // 处理左键 直接打开url
  119. if(e.button == 0) {
  120. // 把url发出
  121. bcmsg.stage = stage.newURL;
  122. bcmsg.url = tag_a.href;
  123. bcmsg.id = BMaster.clients.shift();
  124. // 此id在ready里也有也一并清除
  125. BMaster.ready.rm(bcmsg.id);
  126. send();
  127. return;
  128. }
  129.  
  130. // 获取title
  131. var title = e.srcElement.alt;
  132. if(e.srcElement.localName == "a") title = e.srcElement.text;
  133.  
  134. // 获取cover
  135. var cover = e.srcElement.localName == "img" ? e.srcElement.src : null;
  136.  
  137.  
  138.  
  139. // 处理中键 添加到playlist开头
  140. if(e.button == 1) {
  141. playlist.add({title:title,url:tag_a.href,cover:cover});
  142. }
  143.  
  144. // 处理右键 添加到playlist末尾
  145. // 右键的事件绑定了两次 一次是click的一次是contextmenu的
  146. if(e.button == 2) {
  147. playlist.append({title:title,url:tag_a.href,cover:cover});
  148. }
  149.  
  150. // 中键和右键都会触发一次向ready发送url的事件
  151. while(BMaster.ready.length) {
  152.  
  153. // 如果playlist没有了就结束
  154. if(playlist.isEmpty()) break;
  155.  
  156. // 取出ready 并把client也移除
  157. bcmsg.id = BMaster.ready.shift();
  158. bcmsg.url = playlist.pop();
  159. bcmsg.stage = stage.newURL;
  160. BMaster.clients.rm(bcmsg.id);
  161. send();
  162.  
  163. }
  164.  
  165. console.log("playlist");
  166. console.log(playlist.list);
  167.  
  168.  
  169.  
  170. }
  171.  
  172. static clients = [];
  173. static ready = [];
  174.  
  175. // 处理client主动发起的hello 回应hi
  176. static onHello(msg) {
  177. // 记录id
  178. if(!BMaster.clients.includes(msg.id)) BMaster.clients.push(msg.id);
  179. console.log("onHello 新id");
  180. console.log(BMaster.clients);
  181. // 回应Hi
  182. bcmsg.stage = stage.hiClient;
  183. send();
  184. }
  185.  
  186. // 处理client的Hi
  187. static onHi(msg) {
  188. // hello 之后 client 会发回hi
  189. // 1. 记录id
  190. if(!BMaster.clients.includes(msg.id)) BMaster.clients.push(msg.id);
  191. console.log("onHi新id");
  192. console.log(BMaster.clients);
  193.  
  194. // 2. 如果client是ready 那么加入ready列表中
  195. if(msg.ready) BMaster.ready.unshift(msg.id);
  196. console.log("ready:");
  197. console.log(BMaster.ready);
  198. }
  199.  
  200. // client 播放结束 插队
  201. static onPlayEnded(msg) {
  202.  
  203. // 先判断这个id存在 存在就移除
  204. if(BMaster.clients.includes(msg.id)) BMaster.clients.splice(BMaster.clients.indexOf(msg.id),1);
  205.  
  206. // 如果playlist有url就发过去
  207. if(!playlist.isEmpty()) {
  208. bcmsg.url = playlist.pop();
  209. bcmsg.stage = stage.newURL;
  210. bcmsg.id = msg.id;
  211. send();
  212. return;
  213. }
  214.  
  215. // 如果playlist是空的 就放入ready里
  216. // 如果client点击了一个视频 会再次触发这个事件 需要去重
  217. if(!BMaster.ready.includes(msg.id)) BMaster.ready.push(msg.id);
  218.  
  219. console.log("ready:");
  220. console.log(BMaster.ready);
  221.  
  222. // 将这个id插队到前面
  223. BMaster.clients.unshift(msg.id);
  224. console.log("插队");
  225. console.log(BMaster.clients);
  226.  
  227. }
  228. // Master 消息处理
  229. static onMsg(msg) {
  230. // 直接调用
  231. console.log("master onMsg stage="+msg.data.stage);
  232. BMaster.fns[msg.data.stage](msg.data);
  233.  
  234. }
  235. }
  236. // ========================= master =====================================
  237.  
  238.  
  239.  
  240.  
  241.  
  242.  
  243.  
  244.  
  245.  
  246.  
  247.  
  248.  
  249.  
  250.  
  251.  
  252.  
  253.  
  254. // ========================= client =====================================
  255. // client类
  256. class BClient {
  257. static fns = {};
  258. // 给Master发送hello
  259. static HelloMaster() {
  260.  
  261. //创建函数列表
  262. BClient.fns[stage.helloClient] = BClient.onHello;
  263. BClient.fns[stage.hiClient] = BClient.onHi;
  264. BClient.fns[stage.newURL] = BClient.onNewURL;
  265.  
  266. bcmsg.stage = stage.helloMaster;
  267. send();
  268. }
  269.  
  270. // master会回应hi
  271. static onHi(msg) {
  272. // 如果回应了hi 说明有master了 就把按钮消除
  273. CtrlBtn.display();
  274. console.log("client onHi");
  275. }
  276.  
  277. static onHello(msg) {
  278. // 此stage是master发起了hello 现在要变成client了
  279.  
  280. // 把video的状态也上报给master
  281. var video = document.querySelector("video");
  282. bcmsg.ready = video ? video.ended : true;
  283.  
  284. bcmsg.stage = stage.hiMaster;
  285. send();
  286. CtrlBtn.display();
  287. console.log("client onHello");
  288. }
  289.  
  290. static onNewURL(msg) {
  291. console.log("onNewURL");
  292.  
  293. // 如果id不是自己 就返回
  294. if(msg.id != id) return;
  295.  
  296. // 画面变白 作为响应
  297. document.body.style.display = "none";
  298.  
  299. // 处理来自master的链接
  300. window.location.assign(msg.url);
  301.  
  302. }
  303.  
  304. // video触发ended事件
  305. static onEnded(e) {
  306. bcmsg.stage = stage.playEnded;
  307. send();
  308. }
  309.  
  310.  
  311. // client 消息处理
  312. static onMsg(msg) {
  313. // client 不接收其他client的消息
  314. if(msg.data.stage >= stage.helloMaster) return;
  315. BClient.fns[msg.data.stage](msg.data);
  316. }
  317. }
  318. // ========================= client =====================================
  319.  
  320.  
  321.  
  322.  
  323.  
  324. class PlayList {
  325. constructor() {
  326. this.playlistdiv = document.createElement("div");
  327. this.playlistdiv.classList.add("playlist");
  328. document.body.appendChild(this.playlistdiv);
  329. this.list = [];
  330. }
  331.  
  332. newItem(d) {
  333. // 新建一个div 并绑定数据
  334. var div = document.createElement("div");
  335. var img = document.createElement("img");
  336. var p = document.createElement("p");
  337. div.classList.add("playitem");
  338. div.append(img,p);
  339. p.innerText = d.title;
  340. img.src = d.cover;
  341. img.alt = d.title;
  342.  
  343. return div;
  344. }
  345.  
  346. add(o) {
  347. this.list.unshift(o);
  348. var c = this.newItem(o);
  349. c.classList.add("prepend");
  350. this.playlistdiv.prepend(c);
  351. }
  352.  
  353. append(o) {
  354. this.list.push(o);
  355. var c = this.newItem(o);
  356. this.playlistdiv.append(c);
  357. }
  358.  
  359. isEmpty() {
  360. return this.list == 0 ? true : false;
  361. }
  362.  
  363. pop() {
  364. var rm = this.playlistdiv.querySelector(":first-child");
  365. rm.onanimationend = (e) => {e.srcElement.remove();}
  366. rm.classList.add("rmitem");
  367. return this.list.shift().url;
  368. }
  369. }
  370.  
  371.  
  372.  
  373.  
  374.  
  375. // 控制按钮
  376. class CtrlBtn {
  377. static div;
  378. constructor() {
  379. CtrlBtn.div = document.createElement("div");
  380. CtrlBtn.div.innerHTML = "on";
  381. CtrlBtn.div.className = "bi_ctrl_btn";
  382. CtrlBtn.div.onclick = CtrlBtn.onClick;
  383. document.body.appendChild(CtrlBtn.div);
  384. }
  385.  
  386. // 被点击了
  387. static onClick() {
  388. // 成为master
  389. BMaster.start();
  390. // 变绿
  391.  
  392. CtrlBtn.div.classList.add("ctrl_on");
  393. CtrlBtn.div.innerHTML = "<p>左键:触发播放</p><p>中键:添加到列表首</p><p>右键:添加到列表尾</p>"
  394. CtrlBtn.div.onclick = null;
  395.  
  396.  
  397. // 后续会添加再次点击取消master的功能 现在暂时这样用了
  398. }
  399.  
  400. static display() {
  401. CtrlBtn.div.style.display = "none";
  402. }
  403.  
  404. }
  405.  
  406. // 1. master start之后 会向client发hello client会回应hi
  407. // 2. 一个新的页面 默认会向master发hello 如果有master回应hi 那么就成为client
  408.  
  409. const stage = {
  410. // master
  411. helloClient : 11,
  412. hiClient : 12,
  413. newURL : 13,
  414.  
  415. // client
  416. helloMaster : 21,
  417. hiMaster : 22,
  418. playEnded : 23
  419. };
  420.  
  421. const playlist = new PlayList();
  422. // 开始
  423. // 一开始就作为client存在 如果点击了start 那么才会变成master
  424. (function() {
  425. GM_addStyle(csstext);
  426. // 监听消息
  427. bc.onmessage = BClient.onMsg;
  428. console.log("my id:"+id);
  429. bcmsg.id = id;
  430.  
  431. // 添加一个按钮
  432. var btn = new CtrlBtn();
  433.  
  434.  
  435. // 初次hallo
  436. BClient.HelloMaster();
  437.  
  438. var video = document.querySelector("video");
  439. if(video == null) {
  440. console.log("没有播放器");
  441. } else {
  442. video.onended = BClient.onEnded;
  443.  
  444. // 网页全屏
  445. if(player_web_max) playerWebMax();
  446.  
  447. }
  448.  
  449. })();

QingJ © 2025

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