Niconico My Theater

自分のPC内の動画ファイルと差し替えてニコニコできます。

当前为 2018-10-25 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Niconico My Theater
// @namespace   knoa.jp
// @description 自分のPC内の動画ファイルと差し替えてニコニコできます。
// @include     https://www.nicovideo.jp/watch/*
// @version     0.2
// @grant       none
// ==/UserScript==

(function(){
  const SCRIPTNAME = 'NiconicoMyTheater';
  const DEBUG = false;/* 再生時刻のズレの調整に対応しました。
[使い方]
ブラウザで再生できる動画ファイル(.mp4, .m4v, .mkv など)をご用意ください。
動画プレイヤーの設定ボタン(歯車)の左隣に、ファイルと差し替えるボタンが現れます。
再生時刻が元の動画とズレている場合は、秒単位でプラスマイナス調整できます。

[to do]
ZenzaWatch対応。
ローディング表示
置き換え完了時に何かリアクションを。
元の動画に戻すボタンで確認してから戻せるように。
ニコニコの時刻表示に時間単位追加

[bug]
Firefoxのblob処理が重いのかどうか?
*/
  if(window === top && console.time) console.time(SCRIPTNAME);
  const SHORTSHIFT = 1;// Shift + 左右キーで移動する(秒)
  let site = {
    get: {
      playerOptionButton: () => $('button[data-title="設定"]'),
      playerPlayTime: () => $('.PlayerPlayTime'),
      playtime: () => $('.PlayerPlayTime-playtime'),
      originalVideo: () => $('#VideoPlayer video[src]'),
      videoStartButton: () => $('.VideoStartButton'),
      footerContainerLinks: () => $('.FooterContainer-links'),
    },
  };
  let originalVideo, replacedVideo, ajustmentTime = 0, startTimer;
  let core = {
    initialize: function(){
      core.addFileButton();
      core.linkVideos();
      core.listenEvents();
      core.addFooter();
      core.addStyle();
    },
    addFileButton: function(){
      let playerOptionButton = site.get.playerOptionButton();
      if(!playerOptionButton) return setTimeout(core.addFileButton, 1000);
      let fileButton = createElement(core.html.fileButton());
      let input = fileButton.querySelector('input[type="file"]');
      fileButton.addEventListener('click', function(e){
        input.click();
      });
      // ファイルを差し替えたときの処理
      input.addEventListener('change', function(e){
        log('changed!');
        let object = URL.createObjectURL(input.files[0]);
        replacedVideo.src = object;
        replacedVideo.classList.add('loaded');
        // オリジナルビデオの状態をコピー
        replacedVideo.currentTime = originalVideo.currentTime - ajustmentTime;
        replacedVideo.playbackRate = originalVideo.playbackRate;
        replacedVideo.volume = originalVideo.volume;
        // オリジナルビデオは影の存在となる
        originalVideo.style.visibility = 'hidden';//displayは上書きされる
        originalVideo.muted = true;
        // ローディング表示
        //
        // 時間調整入力欄の追加
        core.addAjustmentInput();
      });
      playerOptionButton.parentNode.insertBefore(fileButton, playerOptionButton);
    },
    addAjustmentInput: function(){
      let playerPlayTime = site.get.playerPlayTime();
      if(!playerPlayTime) return setTimeout(core.addAjustmentInput, 1000);
      let ajustment = createElement(core.html.ajustment());
      let input = ajustment.querySelector('input');
      // 調整値を変えたときの処理
      input.addEventListener('change', function(e){
        ajustmentTime = parseInt(input.value);
        core.syncVideos();
      });
      playerPlayTime.appendChild(ajustment);
    },
    linkVideos: function(){
      // リプレイスビデオ要素の準備
      originalVideo = site.get.originalVideo();
      if(!originalVideo) return setTimeout(core.linkVideos, 1000);
      replacedVideo = createElement(core.html.replacedVideo());
      originalVideo.parentNode.insertBefore(replacedVideo, originalVideo);
      // 連動
      originalVideo.addEventListener('play', function(e){
        log('originalVideo: play!');
        core.syncVideos();
      });
      originalVideo.addEventListener('pause', function(e){
        log('originalVideo: pause!');
        core.syncVideos();
      });
      originalVideo.addEventListener('seeking', function(e){
        log('originalVideo: seeking!');
        core.syncVideos();
      });
      originalVideo.addEventListener('canplay', function(e){
        log('originalVideo: canplay!');
        core.syncVideos();
      });
      originalVideo.addEventListener('volumechange', function(e){
        replacedVideo.volume = originalVideo.volume;
      });
      // 連動(?)
      replacedVideo.addEventListener('seeking', function(e){
        log('replacedVideo: seeking!');
      });
      replacedVideo.addEventListener('canplay', function(e){
        log('replacedVideo: canplay!');
      });
    },
    syncVideos: function(){
      let currentTime = originalVideo.currentTime - ajustmentTime;
      clearTimeout(startTimer);
      if(currentTime < 0){
        replacedVideo.pause();
        startTimer = setTimeout(function(){replacedVideo.play()}, - currentTime * 1000);
      }else{
        originalVideo.paused ? replacedVideo.pause() : replacedVideo.play();
      }
      replacedVideo.playbackRate = originalVideo.playbackRate;
      replacedVideo.currentTime = currentTime;
    },
    listenEvents: function(){
      window.addEventListener('keydown', function(e){
        switch(true){
          case(e.key === 'ArrowLeft' && e.shiftKey):
            // 単純に減らすだけだとプレイヤの時刻が連動しない仕様のようなので、標準の10秒戻るを併用する。
            // その際、表示時刻もジャンプしてしまうので、一度だけ抑制する。
            let playtime = site.get.playtime();
            let observer = observe(playtime, function(records){
              playtime.textContent = records[0].removedNodes[0].textContent;
              observer.disconnect();
            }, {childList: true});
            originalVideo.currentTime-= SHORTSHIFT;
            originalVideo.currentTime+= 10;
            document.querySelector('button[data-title="10秒戻る"]').click();
            break;
          case(e.key === 'ArrowRight' && e.shiftKey):
            // 増加はふつうに連動してくれる。
            originalVideo.currentTime+= SHORTSHIFT;
            break;
          default:
            return;
        }
        e.preventDefault();
        core.syncVideos();
      }, true);
    },
    addFooter: function(){
      let footerContainerLinks = site.get.footerContainerLinks();
      if(!footerContainerLinks) return setTimeout(core.addFooter, 1000);
      footerContainerLinks.appendChild(createElement(core.html.footer()));
    },
    addStyle: function(){
      let style = createElement(core.html.style());
      document.head.appendChild(style);
    },
    html: {
      fileButton: () => `
        <button class="ActionButton ControllerButton FileButton" type="button" data-title="ファイルに差し替える">
          <div class="ControllerButton-inner">
            <!-- https://www.onlinewebfonts.com/icon/112309 -->
            <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" enable-background="new 0 0 1000 1000" viewBox="0 0 1000 1000" xml:space="preserve">
              <metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
              <g><path d="M581.7,10H132.5v980h735V295.8L581.7,10z M785.8,908.3H214.2V91.7h374.5l197.1,197.1V908.3z"></path><path d="M540.8,10v326.7h326.7L540.8,10z"></path><path d="M377.5,418.3V745l285.8-163.3L377.5,418.3z"></path></g>
            </svg>
          </div>
          <input type="file" id="${SCRIPTNAME}-file">
        </button>
      `,
      ajustment: () => `
        <span data-title="差し替えファイルの再生時刻を調整する">+<input type="number" id="${SCRIPTNAME}-ajustment" value="0"></span>
      `,
      replacedVideo: () => `
        <video preload="auto" id="${SCRIPTNAME}-replaced" ${DEBUG ? 'controls' : ''}>
      `,
      footer: () => `
        <li><a href="http://www.onlinewebfonts.com">oNline Web Fonts</a></li>
      `,
      style: () => `
        <style type="text/css">
          input#${SCRIPTNAME}-file{
            display: none;
          }
          input#${SCRIPTNAME}-ajustment{
            width: 3em;
            height: 1.5em;
          }
          video#${SCRIPTNAME}-replaced{
            transition: opacity .5s;
            opacity: 0;
            z-index: 100;
            position: absolute;
            width: 100%;
            height: 100%;
            top: 0px;
            left: 0px;
            bottom: 0px;
            right: 0px;
            display: block;
          }
          video#${SCRIPTNAME}-replaced.loaded{
            opacity: 1;
          }
        </style>
      `,
    },
  };
  let $ = function(s){return document.querySelector(s)};
  let $$ = function(s){return document.querySelectorAll(s)};
  let observe = function(element, callback, options = {childList: true, attributes: false, characterData: false}){
    let observer = new MutationObserver(callback.bind(element));
    observer.observe(element, options);
    return observer;
  };
  let createElement = function(html){
    let outer = document.createElement('div');
    outer.innerHTML = html;
    return outer.firstElementChild;
  };
  let log = function(){
    if(!DEBUG) return;
    let l = log.last = log.now || new Date(), n = log.now = new Date();
    let stack = new Error().stack, callers = stack.match(/^([^/<]+(?=<?@))/gm) || stack.match(/[^. ]+(?= \(<anonymous)/gm) || [];
    console.log(
      SCRIPTNAME + ':',
      /* 00:00:00.000  */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
      /* +0.000s       */ '+' + ((n-l)/1000).toFixed(3) + 's',
      /* :00           */ ':' + stack.match(/:[0-9]+:[0-9]+/g)[1].split(':')[1],/*LINE*/
      /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
      /* caller        */ (callers[1] || '')  + '()',
      ...arguments
    );
  };
  core.initialize();
  if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME);
})();