react-webcamでカメラの起動直後のキャプチャ取得が失敗する

2021.08.30

こんにちは、CX事業本部 IoT事業部の若槻です。

今回は、react-webcamでカメラを起動直後にキャプチャを取得しようとすると取得失敗するので対処方法を考えてみました。

react-webcamでカメラを起動直後にキャプチャを取得しようとすると取得失敗する

以前のエントリで、Reactアプリケーションにカメラ撮影機能を簡単に実装できるreact-webcamを使用して、カメラアプリを実装しました。

その際の実装のデモが下記となります。

さてこの時のアプリケーションは[開始]-[キャプチャ]とボタンをクリックすると端末のカメラで撮影したイメージを取得できるのですが、その際に[開始]をクリックした直後(1秒未満以内)に[キャプチャ]をクリックするとキャプチャ取得に失敗してしまいます。

一方で[開始]をクリックしてから1,2秒置いてから[キャプチャ]をクリックした場合は、正常にキャプチャが取得できます。

原因、対処方法

原因としては、[開始]をクリックすると、端末のカメラが起動してから撮影映像が取得できるまでラグがあることにより、直ぐに[キャプチャ]をクリックして撮影映像からキャプチャをクリックしようとすると撮影映像がnullであるため取得に失敗するためであると考えられます。

そこで、この撮影映像が取得できるまでの間はキャプチャボタンを押せないように出来ないか?という観点で方法を考えてみます。

(有効でなかった方法)webcamRefでnull判定をする

react-webcamの実体のコンポーネントとなる<Webcam />では、refプロパティにReactのrefを指定することにより、そのrefからキャプチャを取得できるようになります。

  <Webcam
    audio={false}
    width={540}
    height={360}
    ref={webcamRef}
    screenshotFormat="image/jpeg"
    videoConstraints={videoConstraints}
    className={classes.webcam}
  />

そこで、下記のようにwebcamRef.currentにデータがある場合(webcamRefのnot null判定時)のみ[キャプチャ]ボタンを表示するようにしてみました。

src/App.ts

import { useRef, useState, useCallback } from "react";
import Webcam from "react-webcam";
import { makeStyles } from "@material-ui/core/styles";
import "./styles.css";

const useStyles = makeStyles(() => ({
  webcam: {
    position: "absolute",
    top: "0px",
    left: "0px",
    visibility: "hidden"
  }
}));

const videoConstraints = {
  width: 720,
  height: 360,
  facingMode: "user"
};

export const App = () => {
  const classes = useStyles();

  const [isCaptureEnable, setIsCaptureEnable] = useState<boolean>(false);
  const webcamRef = useRef<Webcam>(null);
  const capture = useCallback(() => {
    webcamRef.current?.getScreenshot();
  }, [webcamRef]);

  return (
    <>
      <header>
        <h1>カメラアプリ(ビデオ非表示)</h1>
      </header>
      {isCaptureEnable || (
        <button onClick={() => setIsCaptureEnable(true)}>開始</button>
      )}
      {isCaptureEnable && (
        <>
          <div>
            <button onClick={() => setIsCaptureEnable(false)}>終了</button>
          </div>
          <div>
            <Webcam
              audio={false}
              width={540}
              height={360}
              ref={webcamRef}
              screenshotFormat="image/jpeg"
              videoConstraints={videoConstraints}
              className={classes.webcam}
            />
          </div>
          {webcamRef.current && <button onClick={capture}>キャプチャ</button>}
        </>
      )}
    </>
  );
};

しかし、[開始]ボタンをクリックして待っても、[キャプチャ]ボタンが表示されませんでした。

それもそのはずで、useRef()のref属性は値が変更されても画面の再レンダリングが発生しないからです。

useRef は中身が変更になってもそのことを通知しないということを覚えておいてください。.current プロパティを書き換えても再レンダーは発生しません。DOM ノードを ref に割り当てたり割り当てを解除したりする際に何らかのコードを走らせたいという場合は、コールバック ref を代わりに使用してください。

よってwebcamRefを使った方法では取得失敗を回避することは出来ませんでした。

(有効だった方法)待ち時間を設ける

[開始]ボタンをクリック(カメラを起動開始)してから1秒後に[キャプチャ]ボタンを表示するようにすれば、力技ではありますが対処できました。

src/App.ts

import { useRef, useState, useCallback, useEffect } from "react";
import Webcam from "react-webcam";
import { makeStyles } from "@material-ui/core/styles";
import "./styles.css";

const useStyles = makeStyles(() => ({
  webcam: {
    position: "absolute",
    top: "0px",
    left: "0px",
    visibility: "hidden"
  }
}));

const videoConstraints = {
  width: 720,
  height: 360,
  facingMode: "user"
};

export const App = () => {
  const classes = useStyles();

  const [isCaptureEnable, setCaptureEnable] = useState<boolean>(false);
  const webcamRef = useRef<Webcam>(null);
  const [url, setUrl] = useState<string | null>(null);
  const capture = useCallback(() => {
    const imageSrc = webcamRef.current?.getScreenshot();
    if (imageSrc) {
      setUrl(imageSrc);
    }
  }, [webcamRef]);

  const [count, setCount] = useState(0);
  useEffect(() => {
    // カメラ開始状態時のみsetInterval()でカウントアップを行う
    if (isCaptureEnable) {
      const interval = setInterval(() => {
        setCount((c) => c + 1);
      }, 1000);
      return () => clearInterval(interval);
    }
    // 終了状態時はカウントを0にリセットする
    setCount(0);
  }, [isCaptureEnable]);

  return (
    <>
      <header>
        <h1>カメラアプリ(ビデオ非表示)</h1>
      </header>
      {isCaptureEnable || (
        <button onClick={() => setCaptureEnable(true)}>開始</button>
      )}
      {isCaptureEnable && (
        <>
          <div>
            <button onClick={() => setCaptureEnable(false)}>終了</button>
            <>カメラ起動から {count} 秒経過</>
          </div>
          <div>
            <Webcam
              audio={false}
              width={540}
              height={360}
              ref={webcamRef}
              screenshotFormat="image/jpeg"
              videoConstraints={videoConstraints}
              className={classes.webcam}
            />
          </div>
          {count > 0 && <button onClick={capture}>キャプチャ</button>}
        </>
      )}
      {url && (
        <>
          <div>
            <button
              onClick={() => {
                setUrl(null);
              }}
            >
              削除
            </button>
          </div>
          <div>
            <img src={url} alt="Screenshot" />
          </div>
        </>
      )}
    </>
  );
};

カメラの起動開始から1秒未満の場合は[キャプチャ]ボタンが非表示となっています。

1秒経過後以降は[キャプチャ]ボタンが表示されるようになります。

1秒経過後以降であればキャプチャ取得は失敗しないようにすることができました。

おわりに

react-webcamでカメラを起動後にすぐにキャプチャを取得しようとすると取得失敗するので対処方法を考えてみました。

webcamRefを使用した方法は出来そうで出来なかったので、出来たという方がいたら教えてください。

参考

以上