📸

Reactでカメラを使って画像を撮影するダイアログを実装してみた

2022/05/22に公開

スマホやタブレットのカメラを使って画像を撮るダイアログを実装してみた。

最近仕事で扱ったため、備忘録として記事に残しておく。

要件

  • 想定端末はAndroid, iOSのモバイル端末
  • Webアプリ上でカメラを起動する
  • 撮影ボタンを押して、画像をDataURL形式で保持できる

コンポーネント構成と役割

下記の3つのコンポーネントに分けて実装した。

  • App.tsx
    • ダイアログの開閉
    • 撮影した画像データのstateを保持
    • 撮影した画像を表示
  • WebcamDialog.tsx
    • フルスクリーンのダイアログの要素
    • 画面の向きによってWebcamコンポーネントを表示・非表示を切り替え
    • ボタンを押すと、ref経由でWebcam.tsxに定義してあるcaptureメソッドを呼び出して、App.tsxのstateに画像データを格納する
  • Webcam.tsx
    • propsで渡されたwidthheightのカメラ画像を写す要素を表示
    • 撮影データを返すことができるcaptureメソッドを持ち、親コンポーネントからref経由で呼び出しが可能

カメラの機能に関わるやり取りは全てWebcamコンポーネント内に処理を書くことができたので、他のコンポーネントは特に難しいことはしていない。

Webcamコンポーネントの解説

Webcamコンポーネントでは、mount時にカメラのstreamをvideo要素に流し込むようにしており、unmount時にstreamを停止させるようにしている。

モバイルデバイスで縦長画像を取得するときgetUserMediaに渡すaspectRatioに指定する比率のwidthとheightを入れ替えて指定する必要があります。

Webcam.tsx
const Webcam: ForwardRefRenderFunction<WebcamHandles, Props> = (
  { width, height },
  ref
) => {
  const videoRef = useRef<HTMLVideoElement>(null);

  // 親コンポーネントからref経由で実行できるメソッドを定義
  useImperativeHandle(ref, () => ({
    // video要素で表示している画像のdataURLを返すメソッド
    capture() {
      let canvas = document.createElement("canvas");
      if (videoRef === null || videoRef.current === null) {
        return null;
      }
      const { videoWidth, videoHeight } = videoRef.current;
      canvas.width = videoWidth;
      canvas.height = videoHeight;
      const context = canvas.getContext("2d");
      if (context === null || videoRef.current === null) {
        return null;
      }
      context.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
      return canvas.toDataURL("image/jpeg");
    }
  }));

  // カメラのstreamを取得して返すメソッド
  const getStream = useCallback(async () => {
    // モバイルデバイスが縦向きの場合はアスペクト比を縦横入れ替えて指定する
    const aspectRatio = isMobile ? height / width : width / height;
    return await navigator.mediaDevices.getUserMedia({
      video: {
        facingMode: "environment",
        width: {
          ideal: IDEAL_VIDEO_WIDTH
        },
        aspectRatio
      },
      audio: false
    });
  }, [width, height]);

  useEffect(() => {
    let stream: MediaStream | null = null;
    let video = videoRef.current;

    // 取得したstreamをvideo要素に流す
    const setVideo = async () => {
      stream = await getStream();
      if (video === null || !stream) {
        return;
      }
      video.srcObject = stream;
      video.play();
    };

    setVideo();

    // streamを停止させる
    const cleanupVideo = () => {
      if (!stream) {
        return;
      }
      stream.getTracks().forEach((track) => track.stop());
      if (video === null) {
        return;
      }
      video.srcObject = null;
    };
    return cleanupVideo;
  }, [getStream]);

  return <video ref={videoRef} playsInline width={width} height={height} />;
};

export default forwardRef(Webcam);

ちょっと設定を変えたら録画や録音などもできそうで、何かいいアプリが作れる可能性を感じますね。

Discussion