標準のHTML5でも動画プレイヤーのカスタマイズがしたい!

# 概要
皆さんこんにちは。急に暖かくなったと思ったら少し肌寒くなり土日の雨で桜が散ってしまいそうですが、花粉症などいかがお過ごしでしょうか。(乳酸菌さん!もっとアレルギー抑えて!)
さて、今回は動画プレイヤーをいい感じの見た目にしたいのですが、ググっても大抵 [video.js](https://ja.player.support.brightcove.com/styling/customizing-player-appearance.html) か jQueryを使った記事が多いです。
MDNにも[Video player styling basics](https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/Video_player_styling_basics)というページはありますが、なんかカッコよくありません。
というわけでChatGPTに書いてもらいました。

#### 要件

- デザインがそれなりにダサくないこと
- マウスを動かさずにしばらくするとコントローラーは非表示になる
- プログレスバー、時間表示、ピクチャinピクチャボタンはいらない
- 再生停止ボタン、ミュートボタン、音量調整が欲しい

#### 出来上がり

すんなりといい感じのものが出来上がりました。左はマウスを動かした時で、右は1秒何もしなかったときに非表示になる様子です。

<img alt="コントローラーがある時.png" width=480 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/54dfb780-f701-a921-4dd1-d070c3a3c9b4.png" />
<img width=480 alt="コントローラーはすぐに非表示になる.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/d7e7da93-db10-7ea4-2bae-747d20f5aea7.png" />

#### ChatGPT3.5にお願いしたこと

途中で回答が途切れるのを除けばお願いしたのは立ったの三つでした。(課金してるんだから途切れないようにして欲しいなぁ)

```text:一つ目
youtubeのような動画プレイヤーのコントローラー部分を作っています。
video.jsやjqueryは使わず以下の機能を持つコントローラーを HTML/JS/CSS で書いてください

・再生ボタンと一時停止ボタンがトグルする
・再生時間の表示は不要
・プログレスバーは不要
・フルスクリーンボタンがある
・音量調整ボタンがある
・マウスオーバーするとコントローラーが表示される
・1秒以上、マウスが動いていないとコントローラーが非表示になる
```
基本的なHTML/JS/CSSが生成される。詳細は後ほど。
ただしボタンの見た目を `background-image: url('path/to/unmuted.png');` として来たので FontAwesome 使ってねとお伝え。

```text:二つ目
ボタンの表示を、画像ではなく fontawesome のアイコンを使うようにしてください。
html と css だけお願いします
````

ちゃんとFontAwesome5以降に対応した iタグとCSSを生成してくれる。
ボタンが全部左に寄っていたので、音量コントローラとフルスクリーンボタンは右に置いてもらうようにお願い。

```text:三つ目
音量ボタンとフルスクリーンボタンは右端でお願いします
```

これであとは既存のページに組み込めばOK
ただし、フルスクリーンにするとデフォルトの動画コントローラーが表示されてしまい、聞き方が悪かったのか意図を理解してもらえなかったので自分で直した。(後述)

# ソースコードと解説

実際に生成されたコードを見てみます。
videoタグの下にinputタグでカスタムコントローラを作る形です。 `<i class="fas fa-volume-up">` のようにフォントアイコンもちゃんと選んでくれています。

```html:index.html
<!DOCTYPE html>
<html>
<head>
  ...
  <script src="player.js"></script>
  <link rel="stylesheet" href="player.css">
</head>
<body>
  ...
  <!-- 実際にChatGPTが生成したところ -->
  <div class="video-container">
    <video id="myVideo" playsinline></video>
    <div class="video-controls">
      <button id="playPauseBtn"><i class="fas fa-play"></i></button>
      <div class="volume-control">
        <button id="muteBtn"><i class="fas fa-volume-up"></i></button>
        <div class="volume-slider">
          <input type="range" min="0" max="100" value="100" class="slider" id="volumeRange">
        </div>
      </div>
      <button id="fullscreenBtn"><i class="fas fa-expand"></i></button>
    </div>
  <!-- ここまで -->
...
</body>
</html>
```

次にJavaScriptです。
ボタンのクリック処理やスライダの音量処理などリファレンスのように綺麗に描かれています。
マウスを動かした時だけ表示する処理もちゃんとかけています。

```javascript:player.js
window.addEventListener('load', () => {
  // Video element
  const video = document.getElementById('myVideo');

  // Buttons
  const playPauseBtn = document.getElementById('playPauseBtn');
  const muteBtn = document.getElementById('muteBtn');
  const volumeUpBtn = document.getElementById('volumeUpBtn');
  const fullscreenBtn = document.getElementById('fullscreenBtn');

  // Slider
  const volumeRange = document.getElementById('volumeRange');

  // Play/Pause button event listener
  playPauseBtn.addEventListener('click', () => {
    if (video.paused) {
      console.log("play");

      // 今回は Insertable MediaStream で動画の再生・停止を行うのでここに処理を追加した
      ...
      video.srcObject = stream; // MediaStream
      // あとは別workerでデータを流し込む

      video.play();
      playPauseBtn.classList.add('playing');
    } else {
      console.log("pause");
      video.pause();
      playPauseBtn.classList.remove('playing');
    }
  });

  // Mute button event listener
  muteBtn.addEventListener('click', () => {
    if (video.muted) {
      video.muted = false;
      muteBtn.classList.remove('muted');
      volumeRange.value = video.volume * 100;
    } else {
      video.muted = true;
      muteBtn.classList.add('muted');
      volumeRange.value = 0;
    }
  });

  // Volume range event listener
  volumeRange.addEventListener('input', () => {
    video.volume = volumeRange.value / 100;
    if (video.volume === 0) {
      video.muted = true;
      muteBtn.classList.add('muted');
    } else {
      video.muted = false;
      muteBtn.classList.remove('muted');
    }
  });

  // Fullscreen button event listener
  fullscreenBtn.addEventListener('click', () => {
    // videoタグを直接指定するのではなく、親要素を指定する
    const target = document.querySelector('.video-container');
    if (target.requestFullscreen) {
      target.requestFullscreen();
    } else if (target.webkitRequestFullscreen) { /* Safari */
      target.webkitRequestFullscreen();
    }
  });

  // Hide/show video controls on mouse move
  let timeout;
  document.addEventListener('mousemove', () => {
    clearTimeout(timeout);
    document.querySelector('.video-controls').style.display = 'flex';
    timeout = setTimeout(() => {
      document.querySelector('.video-controls').style.display = 'none';
    }, 1000);
  });
});
```

特にmousemoveイベントのところですが、
処理の最初に `cleatTimeout()` を呼んでいるのでタイマーが大量に生成されることもなく、マウスを動かして始めてから1秒後に(まだマウスを動かしているのに)非表示になってしまうというような、よくあるミスが最初からないのも良いですね。

最後にCSSです。
多少細かいツッコミはあるかもしれませんが。CSSのスタイルの引き出しが全くない自分からすれば十分に助かります。
特に音量調整スライダのあたりとかほぼイメージ通りでした。(これが縦方向スライダならなお良いですが)

```css
/*
 * video player
*/
.video-container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
  width: fit-content;
}


.video-controls {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  display: none;
  background-color: rgba(0, 0, 0, 0.5);
}

.video-controls button {
  background-color: transparent;
  border: none;
  outline: none;
  cursor: pointer;
  margin: 0 10px;
  height: 40px;
  width: 40px;
  color: #fff;
  font-size: 20px;
}

#playPauseBtn.playing i::before {
  content: '\f04c'; /* FontAwesomeアイコンコード */
}

#muteBtn.muted i::before {
  content: '\f026'; /* FontAwesomeアイコンコード */
}

#fullscreenBtn i::before {
  content: '\f065'; /* FontAwesomeアイコンコード */
}

.volume-control {
  display: flex;
  align-items: center;
  margin-left: auto;
}

.volume-slider {
  position: relative;
  width: 80px;
  margin: 0 10px;
}

.slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 10px;
  background: #fff;
  outline: none;
  opacity: 0.7;
  -webkit-transition: .2s;
  transition: opacity .2s;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  background: #fff;
  cursor: pointer;
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}

.slider::-moz-range-thumb {
  width: 18px;
  height: 18px;
  background: #fff;
  cursor: pointer;
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}

.volume-control button {
  height: 30px;
  width: 30px;
  font-size: 18px;
}

@media (max-width: 767px) {
  .video-controls button {
    height: 30px;
    width: 30px;
    font-size: 16px;
    margin: 0 5px;
  }

  .volume-control {
    margin-left: 0;
  }

  .volume-control button {
    height: 25px;
    width: 25px;
    font-size: 14px;
  }

  .volume-slider {
    width: 60px;
  }
}
```



### フルスクリーンにしたときにデフォルトのコントローラーが表示されてしまう

ChatGPTがデフォルトで生成したコードは以下のようにvideoタグそのものをフルスクリーンの対象に指定すると
せっかく作ったカスタムコントローラは表示されず、デフォルトのコントローラが表示されてしまいます。
そのため、カスタムコントローラも含めた親要素を指定しましょう。

```javascript:player.js
  // Fullscreen button event listener
  fullscreenBtn.addEventListener('click', () => {
    if (video.requestFullscreen) {
      video.requestFullscreen();
    } else if (video.webkitRequestFullscreen) { /* Safari */
      video.webkitRequestFullscreen();
    } else if (video.msRequestFullscreen) { /* IE11 */
      video.msRequestFullscreen();
    }
  });

```
IE11対応もいらないのでこんな感じになります。

```javascript:player.js
  // Fullscreen button event listener
  fullscreenBtn.addEventListener('click', () => {
    // videoタグを直接指定するのではなく、親要素を指定する
    const target = document.querySelector('.video-container');
    if (target.requestFullscreen) {
      target.requestFullscreen();
    } else if (target.webkitRequestFullscreen) { /* Safari */
      target.webkitRequestFullscreen();
    }
  });
```

# 別解 : シャドウDOMを使う方法

色々とググっていると `video::-webkit-media-controls-overlay-play-button` のようなセレクタを使い、標準のビデオコントローラのスタイルを直接カスタマイズする方法を StackOverflow などで見かけました。

詳しい方に聞いたところ、これは[シャドウDOM](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_shadow_DOM)というらしく実装依存なのでこれは使わずに、今回のようにカスタムコントローラを追加で作ることが良いとのことでした。

Chromeのソースコードに列挙されていますがちゃんとしたリファレンスはなさそうです。

https://chromium.googlesource.com/chromium/blink/+/72fef91ac1ef679207f51def8133b336a6f6588f/Source/core/css/mediaControls.css?autodive=0%2F%2F%2F


# まとめ

いかがでしたでしょうか?
なおこの記事もChatGPTが書きました。というのは嘘です。
まあ、記事を書くのは学習内容を定着させる狙いが主なのでChatGPTがどんなにいい感じになっても記事は自分で書くでしょう。

ChatGPTは3.5でも十分に早くリファレンスのように綺麗なコードを生成してくれます。
それを理解して既存のコードに落とし込むのは人間が役目だと思いますし、まだまだChatGPTに食わせられないオフラインな情報やデータになっていない暗黙知的なところはありますが、どんどん生産性を上げて楽したいところです。


## どうでもいいおまけ

ChatGPTにレビューしてもらった

```text:私
以下のような技術記事を書きました。おかしいところや伝わりにくいところがあれば指摘してください。
```

```text:ChatGPT3.5
ご投稿いただきありがとうございます。以下に、改善点や気になった点をまとめてみました。

記事の冒頭の挨拶部分がやや冗長な感じがしました。テンポの良い文章になるよう、要点を絞ることができるとよいかもしれません。
「ChatGPTに書いてもらいました」という表現が、ChatGPTが完全に自動生成したかのような印象を与える可能性があります。もう少し自然な表現にすると良いかもしれません。
プログラミングのコード部分については、適切なコメントがついており、読みやすいコードとなっていると感じました。ただし、実際の処理の一部分が省略されているため、完全に理解するにはコードの全体像を把握する必要があるかもしれません。
シャドウDOMに関する解説が、少し分かりにくく感じました。シャドウDOMを知らない人にとっては、もう少し詳しく解説することが望ましいかもしれません。
以上です。参考になれば幸いです。
```

> 記事の冒頭の挨拶部分がやや冗長な感じがしました。

うるさい! 余計なお世話だ😅
じゃあどうしたらええのん?

```text:私
記事の冒頭部分はどうしたら良いですか
```

```text:ChatGPT3.5
記事の冒頭部分には、読者の興味を引くような導入が必要です。現在の文章では、多少無関係な情報が混ざっており、読者の注意を逸らしてしまう可能性があります。

代わりに、以下のような導入を試してみてはいかがでしょうか。


皆さんこんにちは。動画プレイヤーのカスタマイズ方法を探している方にとって、自分好みのデザインを実現することは重要な課題です。
しかし、既存の記事には自分に合った情報が見つからず、困っている方も多いのではないでしょうか。

そこで今回は、HTML/CSS/JSで動画プレイヤーのカスタムコントローラーを作成する方法をご紹介します。
プログレスバーや時間表示など、必要のないコントローラーを非表示にし、再生停止ボタンや音量調整ボタンを実装することで、スマートなデザインの動画プレイヤーを実現できます。
是非、最後までお付き合いください。
```

いやだ! こんなまとめ記事みたいな出だしなんて書きたくない😭

あと、誤字脱字チェックは他のサービスを使ったほうが良さそう。 


```text:私
以下のような記事を書きました。
誤字脱字があれば該当箇所を指摘してください。
```

```text:ChatGPT3.5
誤字脱字は見当たりませんでした。
```

> `clearTimeout()` を読んでいるので。

これを指摘するのは流石に難しいかな。他の誤字脱字チェックサービスても引っかからないなぁ