HTMLに埋め込んだYouTube動画の状態を監視して、指定した再生位置になったら何かする方法

本記事では、HTMLに埋め込んだYouTube動画プレーヤーを監視 ＆ その再生情報を取得してJavaScriptであれこれする方法についてまとめています。

機能実装には __「YouTube IFrame Player API」__ を使用します。
（仕様の詳細は[公式のリファレンス](https://developers.google.com/youtube/iframe_api_reference?hl=ja)をご覧ください。）

## YouTube IFrame Player APIとは？
YouTubeさんが提供してくれている無料のAPIで、JavaScriptで制御可能な動画プレーヤーをウェブサイト上の任意の場所に `<iframe>` として配置するものです。

API経由で配置したYouTube動画については、動画プレーヤーの状態変化をイベントとして検知できるほか、JavaScriptでプレイヤーを直接操作することも可能。
- 再生 / 一時停止 / 再生終了の状態
- 再生位置（シークバー）の変化
- 再生速度、音量、画質の変化

などなど、埋め込んだ動画の状態とウェブサイトの動作とをいい感じに連携させることができるわけです。便利ですね。

## 簡単な使い方
今回はこのAPIの活用例として、__"動画が指定した地点まで再生されたら何か処理を実行する"__ という機能を実装してみます。
こちらサンプルコードです。

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>YouTube API Sample</title>
</head>
<body>
  <!-- プレーヤー配置用の空タグ -->
  <div id="player"></div>
  
  <script>
    // YouTube IFrame API スクリプトタグを生成
    const scriptTag = document.createElement('script');
    scriptTag.src = 'https://www.youtube.com/iframe_api';
    document.body.appendChild(scriptTag); // body終了前にタグを出力

    // グローバルでプレイヤーを定義
    let player;

    // iframe のセットアップ
    function onYouTubeIframeAPIReady() {
      player = new YT.Player('player', {
        videoId: 'xxxxxxxxxxx', // 動画ID
        events: {
          onStateChange: onPlayerStateChange // 再生状態の変化を検知
        }
      });
    }

    // 再生位置を監視する関数
    function onPlayerStateChange(event) {
      let checkTime;
      if (event.data === YT.PlayerState.PLAYING) {
        // 動画が再生中のとき
        checkTime = setInterval(() => {
          const currentTime = player.getCurrentTime(); // 再生位置を取得
          if (currentTime >= 60) {
            // 再生位置が60秒以上となったとき

            //任意の処理
            console.log('指定した再生時間に達しました！');

            clearInterval(checkTime); // 監視を停止
          }
        }, 1000);
      } else if (event.data === YT.PlayerState.PAUSED) {
        // 動画が一時停止されたとき
        clearInterval(checkTime); // 監視を停止
      }
    }
  </script>
</body>
</html>
```

ちなみに上記コードはわりとミニマルな内容となっていますが、`// iframe のセットアップ` において`YT.Player()` の第二引数をさらに追加していけば、動的に埋め込むYouTube動画プレーヤーの設定をある程度カスタマイズしたうえで生成することもできます。

通常 `<iframe>` タグ内で指定する `autoplay` や `loop` といったプロパティは、こんな感じで `YT.Player`クラスの`playerVars`オブジェクトの中に入れてあげましょう。
```javascript
function onYouTubeIframeAPIReady() {
  player = new YT.Player('player', {
    videoId: 'xxxxxxxxxxx', // 動画ID
    width: 1280, // プレーヤーの幅
    height: 720, // プレーヤーの高さ
    playerVars: {
      'autoplay': 1, // 読み込み時自動再生のオン/オフ
      'controls': 0, // コントロールの表示/非表示
      'loop': 1 // 繰り返し再生のオン/オフ
    },
    events: {
      'onStateChange': onPlayerStateChange, // 再生状態の変化を検知
      'onPlaybackRateChange': onPlayerPlaybackRateChange, // 再生速度の変化を検知
      'onError': onPlayerError // エラーの発生を検知
    }
  });
}
```
## サンプルコードの解説
先ほどのJavaScriptのざっくりとした流れは、だいたい次のようになっています。
1. APIコードを読み込み
2. HTML内に動画プレイヤーを生成
3. 再生位置を毎秒チェックし一定以上なら処理実行

### 1. APIコードを読み込み
```javascript
// YouTube IFrame API スクリプトタグを生成
const scriptTag = document.createElement('script');
scriptTag.src = 'https://www.youtube.com/iframe_api';
document.body.appendChild(scriptTag); // body終了前にタグを出力
```
まずは YouTube IFrame Player API の読み込みから。
`<script src="https://www.youtube.com/iframe_api"></script>` 
に相当するタグを生成して、`</body>` 前に書き込んでいます。

### 2. HTML内に動画プレイヤーを生成
```javascript
// グローバルでプレイヤーを定義
let player;

// iframe のセットアップ
function onYouTubeIframeAPIReady() {
  player = new YT.Player('player', {
    videoId: 'xxxxxxxxxxx', // 動画ID
    events: {
      onStateChange: onPlayerStateChange // 再生状態の変化を検知
    }
  });
}
```
`onYouTubeIframeAPIReady()` は、APIのロードが完了したタイミングで自動的に呼び出される関数です。
ここでプレーヤーを定義することで、動的に `<iframe>` が埋め込まれます。

`YT.Player()` の第一引数には、配置先となる`<div>`の`id`属性を指定。
第二引数には動画ID（YouTube動画URL末尾の「.../watch?v=xxxxxxxxxxx」の部分）と、イベントリスナーを登録しています。

動画の再生状態（再生 / 一時停止 / 再生終了 など）に変化があると、後に記述する `onPlayerStateChange` 関数が実行されます。

### 3. 再生位置を毎秒チェックし一定以上なら処理実行
```javascript
// 再生位置を監視する関数
function onPlayerStateChange(event) {
  let checkTime;
  if (event.data === YT.PlayerState.PLAYING) {
    // 動画が再生中のとき
    checkTime = setInterval(() => {
      const currentTime = player.getCurrentTime(); // 再生位置を取得
      if (currentTime >= 60) {
        // 再生位置が60秒以上となったとき

        //任意の処理
        console.log('指定した再生時間に達しました！');

        clearInterval(checkTime); // 監視を停止
      }
    }, 1000);
  } else if (event.data === YT.PlayerState.PAUSED) {
    // 動画が一時停止されたとき
    clearInterval(checkTime); // 監視を停止
  }
}
```
そしてメインの処理です。
APIがプレーヤーの状態を `data` プロパティに入れてくれているので、それで動画再生中を検知。
`setInterval()` で 1,000ms ＝ 1秒ごとの時間チェックを繰り返し行っています。

`player.getCurrentTime()` の値（秒）が基準値（サンプルだと60秒）以上となったタイミングで、任意の処理が走る仕組みです。
何かDOMを操作するとか、ページ遷移させるとか、目的に応じてお好きなようにどうぞ。

その後の `clearInterval(checkTime)` は、毎秒繰り返されるチェックを中断させる処理です。
まあこれがなくても問題なく動くっちゃ動くのですが、必要がないときにわざわざメモリを使ってチェックを走らせておくのは嫌なので、目的達成後や動画停止時にはリセットしておきましょう。

## 追記： プレーヤーを `const` で定義する
よりモダンで安全にプレーヤーを用意する方法を[コメント](https://qiita.com/yuu_design00/items/070ad30385837061e944#comment-8538aa67fb300183ef0b)で教えていただきました。
```javascript
// #region script の読み込み待ち
{
  const { promise, resolve } = Promise.withResolvers();
  window.onYouTubeIframeAPIReady = resolve;
  await promise;
}
// #endregion

// グローバルでプレイヤーを定義
const player = new YT.Player("player", {
  videoId: "xxxxxxxxxxx", // 動画ID
  events: {
    onStateChange: onPlayerStateChange, // 再生状態の変化を検知
  },
});
```
`Promise.withResolvers()` を使って非同期処理（ `onYouTubeIframeAPIReady` の準備）の完了を `await` で待っておくことにより、`let player` とわざわざ再代入可能な変数を先に置かなくても `const player` で確実に `YT.Player()` を代入することができ、参照透過性が高まります。

なおこの場合トップレベルで `await` を使っているので、通常のスクリプトではエラーとなってしまいます。
`type="module"` 属性を付けてモジュール方式で読み込んでおくようにしましょう。
```html
<script type="module">
...
  const { promise, resolve } = Promise.withResolvers();
  window.onYouTubeIframeAPIReady = resolve;
  await promise;
...
</script>
```