React + Typescriptでスマホ動画撮影してみた

# 想定読者
* スマホ向けWebページでカメラの動画撮影を利用したい人
* ネイティブアプリのデリバリコストを鑑み、Web経由で顧客に提供したいPoC屋さん

# 実装イメージ
①動画撮影(stopped && Start Recordingボタン)
②録画中の様子をプレビューで確認できる(recording)
③録画終了後、自動で録画した内容を確認できる(stopped && reshootボタン)
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1424230/ecbbbc53-aa88-2f72-89a1-8d3ca3361292.gif)

# 課題だった点
Webでスマホの動画撮影する場合、注意しないとハマってしまう内容を以下の通り整理しました。

###①スマホ実機確認にはデプロイ必須####
MediaStream Recording APIは、【localhost】or【https経由でのアクセス】しか許容されておらず、yarn startでローカルホストを立てて、プライベートアドレス経由(例: http://{aaa.bbb.ccc.ddd}:3000)で、スマホ版ChromeやSafariからアクセスしても動作しないようです。

したがって、スマホで実機動作確認するには、手間ですがamplify add hostingや、firebase hosting, Herokuなどにデプロイして確認する必要があります。
※筆者は今回採用したライブラリが利用していると思われるこのAPIの仕様把握に結構時間を溶かしてしまいました。。。

> Starting with Chrome 47, getUserMedia() requests are only allowed from secure origins: HTTPS or localhost.

全文は[こちら](https://developers.google.com/web/updates/2015/10/chrome-47-webrtc)で確認できます。

###②スマホ版Safariで動画撮影中のプレビュー画面が表示されない####
videoタグにplaysInlineを指定しないと、スマホ版safariだと何を撮影しているか確認不可だった(裏では動作している)。

```typescript
<video ref={videoRef} width={w} height={h} controls playsInline autoPlay />
```

playsInlineを指定しないと真っ黒な画面が表示される。
<img width="250" alt="mojikyo45_640-2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1424230/91b4a0c7-c6c4-d709-30fe-77b2a208a36c.jpeg">

###③撮影前のプレビュー表示####
今回採用した動画撮影ライブラリの[react-media-recorder](https://www.npmjs.com/package/react-media-recorder)は、プレビューは録画中のみ表示される仕様でした。

撮影前もプレビュー表示しないと何を撮るかをユーザが理解しにくいために、撮影前は別のライブラリの[react-webcam](https://www.npmjs.com/package/react-webcam)を表示することとしました。
※もっと良い方法あると思っており、ご存知の方ご教示いただけると幸いです。

# 環境準備
※注意点: スマホで実機確認する際にはhttps経由でのアクセスが必要のため、デプロイ必須。

```sh
$ node -v
v16.13.0

$ yarn -v
1.22.17
```

インストール
※2021/12/18時点で、create-react-appがバージョン指定なしだとエラー吐くため、バージョン指定しています。
その内、改善されると思われるので以下の[@5.0.0]は適宜削除いただければと。

```sh
$ npx create-react-app@5.0.0 {プロジェクト名} --template typescript
$ yarn add react-media-recorder
$ yarn add react-webcam
```

# 実装
###●App.tsx###
cssはcreate-react-appのデフォルトから変更なしのため、割愛します。

```typescript
import './App.css';
import { useReactMediaRecorder } from "react-media-recorder";
import { useEffect, useRef, useState } from 'react';
import Webcam from 'react-webcam';

const videoConstraints = { facingMode: 'user' }
const w = 300, h = 300

export const App: React.FC = () => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const webcamRef = useRef<Webcam>(null)
  const [recordingState, setRecordingState] = useState<'idle' | 'recording' | 'recorded'>('idle')

  const {
    status,
    startRecording,
    stopRecording,
    mediaBlobUrl,
    previewStream,
  } = useReactMediaRecorder({ video: true });

  useEffect(() => {
    if (videoRef.current && previewStream) {
      videoRef.current.srcObject = previewStream;
    }
  }, [previewStream])

  return (
    <div className="App">
      <header className="App-header">
        <div>
          <p>{status}</p>
          {
            recordingState !== 'recording'
              ? recordingState === 'idle'
                ? <button onClick={() => { startRecording(); setRecordingState('recording'); }}>Start Recording</button>
                : <button onClick={() => { setRecordingState('idle') }}>reshoot</button>
              : <button onClick={() => { stopRecording(); setRecordingState('recorded'); }}>Stop Recording</button>
          }
          <br />
          {
            // idle状態はreact-media-recorderのプレビュー画面が表示されないため、WebCamという別ライブラリを使用した。
            recordingState === 'idle' &&
            < Webcam audio={false} ref={webcamRef} videoConstraints={videoConstraints} width={w} height={h} />
          }
          {
            // 録画中はWebCamが動作しなくなるため、react-media-recorderのプレビューを動作させる
            (recordingState === 'recording' && previewStream) &&
            // playsInlineがないと、録画中の画面プレビューが自動で再生されない
            <video ref={videoRef} width={w} height={h} controls playsInline autoPlay />
          }
          {
            // 録画完了後は、録画したものを表示する
            recordingState === 'recorded' &&
            <video src={mediaBlobUrl!} controls autoPlay playsInline width={w} height={h} />
          }
        </div>
      </header >
    </div >
  );
}
```
###●index.tsx###
※デフォルトから変更なしだが、念の為記載

```typescript
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
```


# 感想など

カメラの取り回しは、正直ネイティブアプリのAPIを叩いてる方が簡単です。
ただ、テスト用のアプリを非エンジニアに配るのはそれ以上に手間で大変です。
iPhoneもAndroidも必ずプリインされているブラウザでアクセスしてもらう方法として誰かの役に立てば嬉しく思います。