Greasy Fork is available in English.

Comment Render Smoother

公式HTML5プレイヤーのコメントの動きをなめらかにする(主にFirefox)

2019/02/07時点のページです。最新版はこちら。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name           Comment Render Smoother
// @description    公式HTML5プレイヤーのコメントの動きをなめらかにする(主にFirefox)
// @match          *://www.nicovideo.jp/watch/*
// @grant          none
// @author         guest
// @license        public domain
// @version        0.0.4
// @namespace https://greasyfork.org/users/218
// @noframes
// ==/UserScript==

const monkey = (() => {
  let fps = 60;
  let playbackRate = 1.0;

  const toFrameTime = (time) => { //return time; // こっちは体感しづらいかも
    const timeMs = time * 1000;
    const MSEC_PER_FRAME = 1000 / fps * playbackRate;
    const nextFrame = Math.ceil(timeMs / MSEC_PER_FRAME);
    return nextFrame * MSEC_PER_FRAME / 1000;
  };

  const init = () => {
    if (!window.__videoplayer) { return; }
    const player = window.__videoplayer;
    const _currentTime = player.currentTime.bind(player);
    const _playbackRate = player.playbackRate.bind(player);
    _playbackRate();

    console.log('%cinitialize Comment Render Smoother playbackRate:%s', 'background: cyan;', playbackRate);

    let worldTime = performance.now();
    let lastVideoTime = _currentTime();

    let stallCount = 0;

    player.currentTime = (time) => {
      const now = performance.now();
      if (typeof time === 'number') {
        lastVideoTime = time;
        worldTime = now;
        return _currentTime(time);
      }


      const isPlaying = !player.paused();
      const videoTime = _currentTime();
      const timeDiff = (now - worldTime) / 1000 * playbackRate;
      const predictionTime = lastVideoTime + timeDiff;

      if (isPlaying && lastVideoTime === videoTime) {
        stallCount ++;
      } else {
        stallCount = 0;
      }

      // stallCount = 0; // debug...
      if (
        !isPlaying ||                     // 再生してない or
        lastVideoTime > videoTime ||      // 時間が戻った ≒ シークした or
        //videoTime - lastVideoTime > playbackRate ||  // いきなり1秒以上も進んだ ≒ シークした or
        Math.abs(predictionTime - videoTime) > 1 || // 予測とn秒以上の誤差ができた
        stallCount > 5                    // 詰まってんじゃねーの?  の時
      ) {
        lastVideoTime = videoTime;
        worldTime = now;
        _playbackRate();

        return toFrameTime(videoTime);
      } else {
        return toFrameTime(predictionTime);
      }
    };

    player.playbackRate = rate => {
      if (typeof rate !== 'number') {
        const currentRate = _playbackRate();
        if (playbackRate !== currentRate) {
          console.log('%cupdate playbackRate %s -> %s', 'background: yellow;', playbackRate, currentRate);
          playbackRate = currentRate;
          worldTime = performance.now();
          lastVideoTime = _currentTime();
        }
        return currentRate;
      }
      if (rate === playbackRate || rate <= 0) {
        return;
      }
      console.log('%cset playbackRate %s -> %s', 'background: orange;', playbackRate, rate);

      playbackRate = rate;
      worldTime = performance.now();
      lastVideoTime = _currentTime();
      return _playbackRate(rate);
    };
    // player.play();
  };

  // TODO: なんかプレイヤー初期化のタイミングでやる
  setTimeout(init, 5000);

});

if (document.querySelector('#js-app')) {
  const script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.setAttribute('charset', 'UTF-8');
  script.appendChild(document.createTextNode(`(${monkey})();`));
  document.body.appendChild(script);
}