React TypeScriptでvideoタグだけで動画を読み込む

# やりたいこと

React TypeScript で開発していて、埋め込み動画を用意して、自動再生させたい！

やった事の顛末をストーリー風に書いてみました。

# あらすじ

1. 動画読込
1. 自動再生
1. リプレイ機能

# Video系のライブラリっている？

https://github.com/video-react/video-react

https://github.com/cookpete/react-player

Reactで開発していて動画を再生したい！というとき、探すとそれっぽいライブラリが出てきます。これってライブラリ使わなきゃいけない理由でもあるの？と思ったので、無しでやったらどうなるのか試してみました。

結論から言うと、ブラウザの user agent stylesheet のおかげで割とリッチに仕上がるので、用途によってはライブラリ不要ですね。また、今回の手法は背景動画を仕込むときにも使えそうな方法です。ここから`lazyload`とかの技術を足していくと良さそうです。

# コード

いきなりサブコンポーネントですが、こんな感じになりました。

```tsx:sample.tsx
import React, { useRef, useEffect } from 'react'
import video_mp4 from "../movie/sample.mp4"
import video_webm from "../movie/sample.webm"

export function Translate() {
    const videoRef = useRef<HTMLVideoElement>(null);
    useEffect(() => {
        videoRef.current?.play();
    }, []);
    return (
        <React.StrictMode>
                    <video controls muted ref={videoRef} >
                        <source src={video_mp4} type="video/mp4" />
                        <source src={video_webm} type="video/webm" />
                        <p>Your browser doesn't support HTML5 video.</p>
                    </video>
        </React.StrictMode>
    );
}
```

# 動画をimportしてみる

```tsx:sample.tsx
import video_mp4 from "../movie/sample.mp4"
import video_webm from "../movie/sample.webm"
```
で動画をソースに含めます。

# TS(2307)

このとき、`mp4`、 `webm`の`import`部分で`ts(2307)`のエラーが出ます。

:::note alert
モジュール <code>../movie/sample.mp4</code> またはそれに対応する型宣言が見つかりません。ts(2307)
:::

そこで、`react-app-env.d.ts`に`declare`文を追記します。

```tsx:./src/react-app-env.d.ts
/// <reference types="react-scripts" />
declare module '*.mp4' {
    const src: string;
    export default src;
}
declare module '*.webm' {
    const src: string;
    export default src;
}
```

# videoタグを設置

`<video>`を定義して、先ほどの動画を表示させます。

```tsx:sample.tsx
 <video controls muted ref={videoRef} >
```

`controls` を書くと、user agent stylesheet の再生ボタンとかを設定してくれます。
背景動画とかを置くなら `controls`は消せばOKです。
`ref`には、`useRef()`で参照を設定しています。これは後で解説します。

```tsx:sample.tsx
<source src={video_mp4} type="video/mp4" />
```

ここでは、先ほどソースに含めた動画をvideoの中で代替動画一覧に指定しています。ユーザー画面では、sourceのいずれかが実際に使用されます。

# 自動再生

しかし、sourceに動画を指定しただけでは再生が行われません。`<video>`に`autoPlay`属性を指定すると、自動再生されます。しかし今回は使用せず、代わりに、先ほど`useRef()`で参照を設定して置いたものを活用しましょう。

```tsx:sample.tsx
useEffect(() => {
  videoRef.current?.play();
}, []);
```

`useEffect()`でレンダリング終了後に、`HtmlVideoElement`の`play()`をトリガーします。

# 追加要望でリプレイボタンを付けたい！

「まさか動画があるなんて！最初から見せてよ！」
「再生コントロールでスライダーをマウスでドラッグしろ？面倒なこと言わないで！」
という要望にお応えして、リプレイボタンを設置することになりました。

ボタンをクリックしたら、動画の最初に戻るような機能を加えてみましょう。

```diff_tsx:sample.tsx
import React, { useRef, useEffect } from 'react'
import video_mp4 from "../movie/sample.mp4"
import video_webm from "../movie/sample.webm"
+ import { Button } from '@material-ui/core';

export function Translate() {
    const videoRef = useRef<HTMLVideoElement>(null);
    useEffect(() => {
        videoRef.current?.play();
    }, []);
+    const StartReplay = () => {
+       videoRef.current?.currentTime = 0;
+    }
    return (
        <React.StrictMode>
                    <video controls muted ref={videoRef} >
                        <source src={video_mp4} type="video/mp4" />
                        <source src={video_webm} type="video/webm" />
                        <p>Your browser doesn't support HTML5 video.</p>
                    </video>
+                    <Button onClick={StartReplay} children="replay" variant="contained" color="primary" />
        </React.StrictMode>
    );
}
```

# currentTimeで振り出しに

コードの説明を見ていきましょう。

`HtmlVideoElement`のプロパティ`currentTime`をゼロにすると、動画の最初に戻ることは分かっています。そこで、`StartReplay`関数を用意します。その中で、`currentTime`を操作する処理を入れます。

```tsx:sample.tsx
const StartReplay = () => {
   videoRef.current?.currentTime = 0;
}
```

# TS(2779)

さて、ここで問題発生です。`currentTime`のところがエラーになります。

:::note alert
代入式の左辺には、省略可能なプロパティ アクセスを指定できません。ts(2779)
:::

解決方法は、「存在する」と確実に分かる状況を作り出すことです。

```tsx:sample.tsx
const StartReplay = () => {
    if (videoRef.current?.currentTime) {
        videoRef.current.currentTime = 0;
    }
}
```

これでエラーは解決です。

# 始めに戻っただけ

これで良し！と思いますが、残念。これだと、動画が終了後にリプレイボタンは機能しません。動画内のどの時刻にカーソルがあるかを示す`currentTime`と、再生と停止を司る`play()`は別の問題なのです。

```tsx:sample.tsx
const StartReplay = () => {
    if (videoRef.current?.currentTime) {
        videoRef.current.currentTime = 0;
    }
    videoRef.current?.play();
}
```

`currentTime`をゼロにした後に、`play()`で再生を開始させましょう。

# まとめ

最後にまとめてみましょう。

```diff_tsx:sample.tsx
import React, { useRef, useEffect } from 'react'
import video_mp4 from "../movie/sample.mp4"
import video_webm from "../movie/sample.webm"
+ import { Button } from '@material-ui/core';

export function Translate() {
    const videoRef = useRef<HTMLVideoElement>(null);
    useEffect(() => {
        videoRef.current?.play();
    }, []);
+    const StartReplay = () => {
+       if (videoRef.current?.currentTime) {
+        videoRef.current.currentTime = 0;
+        }
+        videoRef.current?.play();
+    }
    return (
        <React.StrictMode>
                    <video controls muted ref={videoRef} >
                        <source src={video_mp4} type="video/mp4" />
                        <source src={video_webm} type="video/webm" />
                        <p>Your browser doesn't support HTML5 video.</p>
                    </video>
+                    <Button onClick={StartReplay} children="replay" variant="contained" color="primary" />
        </React.StrictMode>
    );
}
```


# 環境

- create-react-app : 4.0.3
- react : 17
- typescript : 4

```json-doc:./tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}
```

# 関連公式

https://developer.mozilla.org/ja/docs/Web/HTML/Element/video

https://www.w3schools.com/tags/ref_av_dom.asp

**Excelsior!**