YouTube Live Screen Comment Scroller

YouTube Live のコメントをニコニコ風にスクロールさせます。

  1. // ==UserScript==
  2. // @name YouTube Live Screen Comment Scroller
  3. // @namespace knoa.jp
  4. // @description YouTube Live のコメントをニコニコ風にスクロールさせます。
  5. // @include https://www.youtube.com/watch*
  6. // @version 0.2.0
  7. // @grant none
  8. // ==/UserScript==
  9.  
  10. /*
  11. 他のページからの遷移では起動しない
  12. スパチャに背景色付けて流したいかも(最上段で最大2倍ゆっくり?)
  13. */
  14. (function(){
  15. /* カスタマイズ */
  16. var SCRIPTNAME = 'ScreenCommentScroller';
  17. var COLOR = '#ffffff';/*コメント色*/
  18. var OCOLOR = '#000000';/*コメント縁取り色*/
  19. var OWIDTH = 1/10;/*コメント縁取りの太さ(比率)*/
  20. var OPACITY = '0.25';/*コメントの不透明度*/
  21. var MAXLINES = 10;/*コメント最大行数*/
  22. var LINEHEIGHT = 1.2;/*コメント行高さ*/
  23. var DURATION = 5;/*スクロール秒数*/
  24. var FPS = 60;/*秒間コマ数*/
  25. /* サイト定義 */
  26. var site = {
  27. getScreen: function(){return document.querySelector('#player-container')},
  28. getBoard: function(){if(document.querySelector('#chatframe')){return document.querySelector('#chatframe').contentWindow.document.querySelector('#item-offset > #items')}},
  29. getMessage: function(node){return node.querySelector('#message')},
  30. getPlay: function(){return document.querySelector('button.ytp-play-button')},
  31. getVideo: function(){return document.querySelector('video.video-stream')},
  32. isPlaying: function(play){return (play.attributes['aria-label'].value.match(/停止/)!==null)},
  33. };
  34. /* 処理本体 */
  35. let retry = 10;
  36. var screen, board, play, video, canvas, context, lines = [], fontsize;
  37. var core = {
  38. /* 初期化 */
  39. initialize: function(){
  40. console.log(SCRIPTNAME, 'initialize...');
  41. /* 主要要素が取得できるまで読み込み待ち */
  42. screen = site.getScreen();
  43. board = site.getBoard();
  44. play = site.getPlay();
  45. video = site.getVideo();
  46. //console.log(SCRIPTNAME, screen, board, play, video);
  47. if(!screen || !board || !play || !video){
  48. window.setTimeout(function(){
  49. if(retry--) core.initialize();
  50. }, 1000);
  51. return;
  52. }
  53. /* コメントをスクロールさせるCanvasの設置 */
  54. /* (描画処理の軽さは HTML5 Canvas, CSS Position Left, CSS Transition の順) */
  55. canvas = document.createElement('canvas');
  56. canvas.id = SCRIPTNAME;
  57. screen.appendChild(canvas);
  58. context = canvas.getContext('2d');
  59. /* メイン処理 */
  60. core.addStyle();
  61. core.listenComments();
  62. core.scrollComments();
  63. /**/
  64. window.addEventListener('popstate', function(){
  65. core.initialize();
  66. });
  67. document.body.addEventListener('DOMAttrModified', function(){
  68. if(video.src == site.getVideo().src) return;
  69. core.initialize();
  70. });
  71. },
  72. /* *スクリーンサイズに変化があればcanvasも変化させる* */
  73. modify: function(){
  74. if(canvas.width == screen.offsetWidth) return;
  75. //console.log(SCRIPTNAME, 'modify...');
  76. canvas.width = screen.offsetWidth;
  77. canvas.height = screen.offsetHeight;
  78. fontsize = (canvas.height / MAXLINES) / LINEHEIGHT;
  79. context.font = 'bold ' + (fontsize) + 'px sans-serif';
  80. context.fillStyle = COLOR;
  81. context.strokeStyle = OCOLOR;
  82. context.lineWidth = fontsize * OWIDTH;
  83. },
  84. /* スタイル付与 */
  85. addStyle: function(){
  86. //console.log(SCRIPTNAME, 'addStyle...');
  87. let head = document.getElementsByTagName('head')[0];
  88. if (!head) return;
  89. let style = document.createElement('style');
  90. style.type = 'text/css';
  91. style.innerHTML = ''+
  92. 'canvas#'+SCRIPTNAME+'{' +
  93. ' pointer-events: none;' +
  94. ' position: absolute;' +
  95. ' top: 0;' +
  96. ' left: 0;' +
  97. ' width: 100%;' +
  98. ' height: 100%;' +
  99. ' opacity: '+OPACITY+';' +
  100. ' z-index: 99999;' +
  101. '}'+
  102. '';
  103. head.appendChild(style);
  104. },
  105. /* コメントの新規追加を見守る */
  106. listenComments: function(){
  107. //console.log(SCRIPTNAME, 'listenComments...', board);
  108. observe(board, function(records){
  109. records.forEach(record => {
  110. record.addedNodes.forEach(node => {
  111. let message = site.getMessage(node);
  112. if(message === null) return;
  113. core.modify();
  114. core.attachComment(message);
  115. });
  116. });
  117. });
  118. },
  119. /* コメントが追加されるたびにスクロールキューに追加 */
  120. attachComment: function(comment){
  121. //console.log(SCRIPTNAME, 'attachComment...', comment);
  122. let record = {};
  123. record.text = comment.textContent.replace(/[\r\n]/g, '');/*流れる文字列*/
  124. record.width = context.measureText(record.text).width;/*文字列の幅*/
  125. record.life = DURATION * FPS;/*文字列が消えるまでのコマ数*/
  126. record.left = canvas.width;/*左端からの距離*/
  127. record.delta = (canvas.width + record.width) / (record.life);/*コマあたり移動距離*/
  128. record.reveal = record.width / record.delta;/*文字列が右端から抜けてあらわになるまでのコマ数*/
  129. record.touch = canvas.width / record.delta;/*文字列が左端に触れるまでのコマ数*/
  130. /* 追加されたコメントをどの行に流すかを決定する */
  131. for(let i=0; i<MAXLINES; i++){
  132. let length = lines[i] ? lines[i].length : 0;/*同じ行に詰め込まれているコメント数*/
  133. switch(true){
  134. /* 行が空いていれば追加 */
  135. case(lines[i] == undefined || !length):
  136. lines[i] = [];
  137. /* 以前のコメントより長い(速い)文字列なら、左端に到達する時間で判断する */
  138. case(lines[i][length - 1].reveal < 0 && lines[i][length - 1].delta > record.delta):
  139. /* 以前のコメントより短い(遅い)文字列なら、右端から姿を見せる時間で判断する */
  140. case(lines[i][length - 1].life < record.touch && lines[i][length - 1].delta < record.delta):
  141. /*条件に当てはまればすべてswitch文のあとの処理で行に追加*/
  142. break;
  143. default:
  144. /*条件に当てはまらなければ次の行に入れられるかの判定へ*/
  145. continue;
  146. }
  147. record.top = ((canvas.height / MAXLINES) * i) + fontsize;
  148. lines[i].push(record);
  149. break;
  150. }
  151. },
  152. /* FPSタイマー駆動 */
  153. scrollComments: function(){
  154. //console.log(SCRIPTNAME, 'scrollComment...');
  155. var interval = window.setInterval(function(){
  156. /* 再生中じゃなければ処理しない */
  157. if(!site.isPlaying(play)) return;
  158. /* Canvas描画 */
  159. context.clearRect(0, 0, canvas.width, canvas.height);
  160. for(let i=0; lines[i]; i++){
  161. for(let j=0; lines[i][j]; j++){
  162. /*視認性を向上させるスクロール文字の縁取りは、幸いにもパフォーマンスにほぼ影響しない*/
  163. context.strokeText(lines[i][j].text, lines[i][j].left, lines[i][j].top);
  164. context.fillText(lines[i][j].text, lines[i][j].left, lines[i][j].top);
  165. lines[i][j].life--;
  166. lines[i][j].reveal--;
  167. lines[i][j].touch--;
  168. lines[i][j].left -= lines[i][j].delta;
  169. }
  170. if(lines[i][0] && lines[i][0].life == 0){
  171. lines[i].shift();
  172. }
  173. }
  174. }, 1000/FPS);
  175. },
  176. };
  177. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  178. let observer = new MutationObserver(callback.bind(element));
  179. observer.observe(element, options);
  180. return observer;
  181. };
  182. core.initialize();
  183. })();

QingJ © 2025

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