動画の両端にダブルクリックするとn秒送り/戻りする機能を追加するユーザースクリプト

スマホの動画アプリなどにあるダブルタップで10秒送りなどが快適なので、PC画面やスマホのブラウザでの動画閲覧時にも出来ないかなと作成。

シークや送りボタンは繊細な操作が必要ですし、左右キーでの送りは一旦マウスで動画にフォーカスしないといけないことも多かったので、もっと大雑把にしたい。みたいな。

スマホのシークが苦手なんです。

# YouTubeでの例

![yotubepress.gif](https://qiita-image-store.s3.amazonaws.com/0/62354/67eae170-da9a-949d-d3a6-03558eb97f43.gif)

(動画にadが出ると要素位置がずれるので別途切っています。要素追加位置をadより上位(兄)にすれば共存可能です)

# 動作検証環境

* Firefox 64bit 66.0.2
* Tampermonkey v4.8.5847

# 基本形コード

再生速度変更ボタン追加コードも混じってますが。

```js

(function() {
  'use strict';
  // まずはメイン画面のロード完了を待つ
  const mo = new MutationObserver((data1, data2) => {
    const video = document.querySelector('video')
    if (video) {
      //addSpeedOptions()
      addSeekCurtain()
      mo.disconnect();
      return
    }
  })
  mo.observe(document.body, {
    childList: true, subtree: true
  });


  const addSpeedOptions = () => {

    const video = document.querySelector('video')
    const menu = document.querySelector('menu')
    const div = document.createElement('div')
    div.className = 'menuclass'
    div.style.display = 'block'
    div.style.marginRight = '5px'
    div.style.width = 'auto'

    const changeSpeed = (speed, state='=') => {
      return () => {
        if (state == '=') {
          video.playbackRate = speed
        } else if (state == '-') {
          video.playbackRate -= speed
        } else if (state == '+') {
          video.playbackRate += speed
        }
      }
    }

    let clone = div.cloneNode()
    clone.textContent = '=1'
    clone.addEventListener('click', changeSpeed(1))
    menu.appendChild(clone)
    clone = div.cloneNode()
    clone.textContent = '=1.25'
    clone.addEventListener('click', changeSpeed(1.25))
    menu.appendChild(clone)
    clone = div.cloneNode()
    clone.textContent = '+.25'
    clone.addEventListener('click', changeSpeed(0.25, '+'))
    menu.appendChild(clone)
    clone = div.cloneNode()
    clone.textContent = '-.25'
    clone.addEventListener('click', changeSpeed(0.25, '-'))
    menu.appendChild(clone)
    clone = div.cloneNode()
    clone.textContent = '=2'
    clone.addEventListener('click', changeSpeed(2))
    menu.appendChild(clone)
  }

  const addSeekCurtain = () => {
    // <div style="position: absolute;height: 80%;width: 20%;display: inline-block;">test left</div>
    // <div style="position: absolute;right: 0;display: inline-block;height: 80%;width: 20%;">test right</div>

    const video = document.querySelector('video')
    const videoWrapper = document.querySelector('wrapper')
    const common = document.createElement('div')
    common.style.position = 'absolute'
    common.style.height = '80%'
    common.style.width = '20%'
    common.style.display = 'inline-block'

    common.style.zIndex = '100'

    const left = common.cloneNode()
    const right = common.cloneNode()
    right.style.right = '0'


    const seek = (seconds, state='+') => {
      return (e) => {
        console.log(e)
        e.stopPropagation();
        if (state == '-') {
          video.currentTime -= seconds
        } else if (state == '+') {
          video.currentTime += seconds
        }
      }
    }

    left.addEventListener('dblclick', seek(5, '-'))
    right.addEventListener('dblclick', seek(5, '+'))

    videoWrapper.appendChild(left)
    videoWrapper.appendChild(right)

    //video.parentNode.insertBefore(left, video)
    //video.parentNode.insertBefore(right, video)

    //video.appendChild(left)
    //video.appendChild(right)

    // 通常はmouseupのクリック間隔でダブルクリック判定→フルスクリーン化しているようなので
    // シークのダブルクリックにmouseupが反応しないようにして、クリックによる再生・停止を改めてキャプチャーフェーズで定めてやる
    // ...にはやはりclick間隔でdblclickをエミュレートしないといけないので、再生・停止は機能しない簡易版にするよ
    left.addEventListener('click', e => {
      e.stopPropagation();
    })
    right.addEventListener('click', e => {
      e.stopPropagation();
    })
  }
})();

```

```js
  // まずはメイン画面のロード完了を待つ
  const mo = new MutationObserver((data1, data2) => {
    const video = document.querySelector('video')
    if (video) {
      //addSpeedOptions()
      addSeekCurtain()
      mo.disconnect();
      return
    }
  })
  mo.observe(document.body, {
    childList: true, subtree: true
  });
```

ここは要素が現れるのを待つ個人的汎用コードですね。
イケている動画サイトではメインの動画要素が遅延読み込みされることがあるので待機する必要があります。
videoが出たら`if (video)`追加するコードを実行します。

```js
    const video = document.querySelector('video')
    const videoWrapper = document.querySelector('wrapper')
```

操作対象のvideoと、要素の追加先や動画のコントローラーとなっているWrapperを取得しています。
動画サイトによっては追加先とコントローラーが別々の要素だったり、コントローラーがvideo要素だったりでいろいろです。
Wrapperは「動画と同じサイズ・同じ位置のdiv要素」と捕らえています。

```js
    const common = document.createElement('div')
    common.style.position = 'absolute'
    common.style.height = '80%'
    common.style.width = '20%'
    common.style.display = 'inline-block'

    common.style.zIndex = '100'

    const left = common.cloneNode()
    const right = common.cloneNode()
    right.style.right = '0'
```

ダブルクリックを実装する追加要素です。動画部分にオーバーレイさせます。
`z-index`はサイトによっては不要です。多分基本的には不要だと思いますが、お守りに。

Wrapperに入れれば上手く動画サイズにフィットするはずですが、サイトによっては別途調整が要るでしょう。

高さが動画の80%なのは、動画下部にメニューがオーバーレイされるので干渉避けに。幅は中央の標準クリック動作が邪魔にならない程度にお好みで。
右側の送り側は`right.style.right = '0'`で右端に寄せておきます。

![left.jpg](https://qiita-image-store.s3.amazonaws.com/0/62354/ac455e8a-8a1d-5fbd-aa3f-a3c60e724b19.jpeg)

![right.jpg](https://qiita-image-store.s3.amazonaws.com/0/62354/96384f19-0764-443e-6893-22be09f908d5.jpeg)

YouTubeならもう少しheightをとってもいいですね。

ただし、右側は右下の設定のポップアップとよく干渉するので、heightを少なめにしとくといいと思います。

```js
    const seek = (seconds, state='+') => {
      return (e) => {
        console.log(e)
        e.stopPropagation();
        if (state == '-') {
          video.currentTime -= seconds
        } else if (state == '+') {
          video.currentTime += seconds
        }
      }
    }

    left.addEventListener('dblclick', seek(5, '-'))
    right.addEventListener('dblclick', seek(5, '+'))
```

再生位置変更機能です。それ以上でも以下でもなく。seekは良くない命名だとおもいますけど、なんていえばよいのか。

```js
    videoWrapper.appendChild(left)
    videoWrapper.appendChild(right)

    //video.parentNode.insertBefore(left, video)
    //video.parentNode.insertBefore(right, video)

    //video.appendChild(left)
    //video.appendChild(right)
```

要素の追加。上手く重なる位置を調べましょう。
コメントアウト含めてこれらとセレクターの調整で上手くいくかな？と。

```js
    // 通常はmouseupのクリック間隔でダブルクリック判定→フルスクリーン化しているようなので
    // シークのダブルクリックにmouseupが反応しないようにして、クリックによる再生・停止を改めてキャプチャーフェーズで定めてやる
    // ...にはやはりclick間隔でdblclickをエミュレートしないといけないので、再生・停止は機能しない簡易版にするよ
    left.addEventListener('click', e => {
      e.stopPropagation();
    })
    right.addEventListener('click', e => {
      e.stopPropagation();
    })
```

コメントのまま。これもサイトによっては不要です。
`dblclick`のほうも`e.stopPropagation();`でバブリングを止めていますが、`click`イベントで`dblclick`判定をしているものと同時発生してしまうので、`click`イベントもバブリングしないようにします。
シングルクリックも機能しなくなりますが、このイベント内で同様にシングル・ダブルクリックを判定して、シングルなら親をクリックするような記述をすれば、もともとのシングルクリック機能を残せると思います。

Wrapperは動画サイズを持つと共に、イベントを管理していることが多いので追加要素に`stopPropagation`を入れていますが、場合によってはイベントは別要素の場合もあるかもしれません。

![wrapper.jpg](https://qiita-image-store.s3.amazonaws.com/0/62354/c3b8b845-7440-25ac-84c1-23e5ab1e94d1.jpeg)

YouTubeの場合は`#movie_player`がWrapperでありイベント管理であるので、この子供として要素を追加しています。

# YouTube版コード

```js
// ==UserScript==
// @name         Youtube Video Speed
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  動画n秒送り
// @author       khsk
// @match        https://www.youtube.com/watch?*
// @grant        none
// ==/UserScript==

(function() {
  'use strict';
  // まずはメイン画面のロード完了を待つ
  const mo = new MutationObserver((data1, data2) => {
    const video = document.querySelector('.video-stream.html5-main-video')
    if (video) {
      addSeekCurtain()
      mo.disconnect();
      return
    }
  })
  mo.observe(document.body, {
    childList: true, subtree: true
  });

  const addSeekCurtain = () => {
    // <div style="position: absolute;height: 80%;width: 20%;display: inline-block;">test left</div>
    // <div style="position: absolute;right: 0;display: inline-block;height: 80%;width: 20%;">test right</div>

    const video = document.querySelector('.video-stream.html5-main-video')
    const videoWrapper = document.querySelector('#movie_player')
    const common = document.createElement('div')
    common.style.position = 'absolute'
    common.style.height = '80%'
    common.style.width = '20%'
    common.style.display = 'inline-block'

    common.style.zIndex = '100'

    const left = common.cloneNode()
    const right = common.cloneNode()
    right.style.right = '0'

    const seek = (seconds, state='+') => {
      return (e) => {
        if (state == '-') {
          video.currentTime -= seconds
        } else if (state == '+') {
          video.currentTime += seconds
        }
      }
    }

    left.addEventListener('dblclick', seek(5, '-'))
    right.addEventListener('dblclick', seek(5, '+'))

    videoWrapper.appendChild(left)
    videoWrapper.appendChild(right)
  }
})();
```

***

サイトによって不要記述も多いので調べながら削るか全部のせで気にせずか。

# 参考

* [html5のvideoに関する覚書的なメモ - Qiita](https://qiita.com/honami/items/f5e2851ba281e5c155ab)
* [JavaScript シングル/ダブルクリックイベントを両方取得する - Qiita](https://qiita.com/sashim1343/items/e3728bea913cadab677d)