YouTube Live Screen Comment Scroller

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

目前為 2018-02-11 提交的版本,檢視 最新版本

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