ReactPlayerでループ時にイベントを発火させる方法

# はじめに
どうも最近Reactを触って1ヶ月のぴよぴよ🐣です。
今回の記事は2023/07の情報をもとに記載しているため、今後ReactPlayerの対応によっては以下の方法よりもっと良い方法ができるようになるかもしれないことを念頭において読んでいただけると幸いです。

# 結論
[公式のドキュメントにも書いてありますが](https://github.com/cookpete/react-player#:~:text=onEnded,set%20to%20true)、ループ終了時には`onEnded`は発火しませんので、不可能です。
![スクリーンショット 2023-07-17 0.16.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207958/8f2ffe08-3a16-ca4a-1578-a632f4c5f27f.png)

なので、今回は**疑似的に**発火させる方法をご紹介します。
1. `onDuration`で動画全体の秒数を取得する
1. `onProgress`で動画の数秒前に行いたい処理を実行する

大雑把に動きやコードが見たい方はこちらからどうぞ

https://github.com/cuculu321/ReactPlayerLooping/

# ことの始まり

動画教材を扱うサービスであれば「動画が見終わったか」をDBなどに取得したときは多いでしょう。
これをループした時に行いたいということがあったのがことの始まりです。

## Issueを見てみる

既にいくつかループ時にEndedが発火しないというIssueは立っており、そこでも`onDuration`と`onProgress`を使ってねーと言われています。
https://github.com/CookPete/react-player/issues/496
https://github.com/cookpete/react-player/issues/471


### 悪い例：onProgress=0で動かしてみる

0秒目を支点にすれば、onDurationを使わなくて良いじゃん！と思いますが、動画を最初からにするとonProgressが動作します。そのため、「最後まで見た」ということはできません。
```react
  const [loopCount, setLoopCount] = React.useState(0)

  function handlerOnProgress(state) {
    if (state.playedSeconds <= 0) {
      setLoopCount(loopCount + 1)
    }
  }

  return (
    <div>
      <ReactPlayer
        url='https://samplelib.com/lib/preview/mp4/sample-5s.mp4' 
        controls={true}
        playing={true}
        loop={true}
        onEnded={() => console.log('onEnded')} //悲しいかな発火しない子
        onProgress={handlerOnProgress}
        />
        <h3>
          Loop count: {loopCount}
        </h3>
      </div>
  );
```

### 良い例：onDurationとonProgressを使う

この場合は「動画の最後」にしっかり発火するので、正常にループの直前に動作してくれます。
ただ、onProgressの実行間隔の[デフォルトは10000ms](https://github.com/cookpete/react-player#:~:text=progressInterval,1000)なので、`progressInterval`を短くするか、1.5秒など少しゆとりのある時間にしましょう。
ただ、この実装だとループする度に発火することになります。
```react
  const [loopCount, setLoopCount] = React.useState(0)
  const [duration, setDuration] = React.useState(0)

  function handlerOnProgress(state) {
    if (state.playedSeconds > duration - 1.5) {
      setLoopCount(loopCount + 1)
    }
  }

  return (
    <div>
      <ReactPlayer
        url='https://samplelib.com/lib/preview/mp4/sample-5s.mp4' 
        controls={true}
        playing={true}
        loop={true}
        onEnded={() => console.log('onEnded')} //悲しいかな発火しない子
        onDuration={setDuration}
        onProgress={handlerOnProgress}
        />
        <h3>
          Loop count: {loopCount}
        </h3>
      </div>
  );
```

### 間違いなく最初のループだけで動作する例

今回私がやりたかったのは、ループの1回目飲みを取得することだったの、以下のような実装にしました。

```react
  const [loopCount, setLoopCount] = React.useState(0)
  const [duration, setDuration] = React.useState(0)
  const [playerVideo, setPlayerVideo] = React.useState(true)

  function handlerOnProgress(state) {
    if (playerVideo && state.playedSeconds > duration - 1.5) {
      setLoopCount(loopCount + 1)
      setPlayerVideo(false)
    }
  }

  return (
    <div>
      <ReactPlayer
        url='https://samplelib.com/lib/preview/mp4/sample-5s.mp4' 
        controls={true}
        playing={true}
        loop={true}
        onEnded={() => console.log('onEnded')} //悲しいかな発火しない子
        onDuration={setDuration}
        onProgress={handlerOnProgress}
        />
        <h3>
          Loop count: {loopCount}
        </h3>
      </div>
  );
```



# 総括

実際に動画をループするだけなら、Safari以外なら右クリックでループさせることができるので要件がループだけならそれでカバーするのもありだと個人的には思います。

また、今回ChatGPTをペアプロしながら実装しましたが間違ったこともいうので、皆さんも実際に実装するときは自分の目でドキュメントやコードを読むようにしましょう！
**実際に言われたこと一覧**
> わい：onProgressを有効・無効で切り替えることはできますか？
> ChatGPT : progressIntervalをnullにすることで可能です

progressIntervalは実際にはnumberしか持たないのでこの方法は使えません。
https://github.com/cookpete/react-player/blob/2811bc59b9368170acc20d4f1e39555413d0d9e1/src/props.js#L16