React アプリでカメラ撮影し画像データを取得する [react-webcam 使用]

こんにちは、広野です。

React アプリ内で、PC やスマホ等のデバイスに備え付けのカメラから写真撮影し、そのデータをファイルとして Amazon S3 に送る処理をつくる機会がありました。思ったより苦労したので、動いたコードを自分の備忘も兼ねて紹介したいと思います。

つくったもの

アプリの画面イメージ

  1. アプリ画面に、デバイスのカメラと連動した撮影画面と撮影ボタンが表示されます。
  2. 撮影ボタンを押すと、その下に撮影した画像のプレビュー画面が表示されます。以降は送信ボタンにより Amazon S3 にデータを送ります。
  3. 裏の処理になりますが、取得した画像データは Base 64 エンコードされたデータなので、Amazon S3 に送るために Blob 変換した後、File オブジェクトに変換します。

本記事ではここまでの React コードを紹介します。

(補足) 前提となるアプリ基盤

以下のような AWS を活用した React アプリ基盤があるのですが、ここは割愛します。
Amazon S3 にファイルを送信する仕組みも別記事にありますので、リンクを張っておきます。

  • React アプリ基盤

  • React アプリから Amazon S3 にファイルを送信する仕組み

開発に使用したもの

  • React 18.2.0
  • react-webcam 7.0.1
  • base-64 1.0.0
  • cross-blob 3.0.2
  • @mui/joy 5.0.0-alpha.71
  • react-device-detect 2.2.3
  • その他 MUI パーツ各種

react-webcam は React アプリにデバイスのカメラを連動させるのによく使われるモジュールのようです。他のもいくつか試しましたが、React 18 に通常のインストールができずあきらめました。

base-64 と cross-blob は Base 64 エンコードされたデータを Blob に変換するために使用しました。一般的にこの処理は atob (ascii to blob) と言われます。

@mui/joy は react-webcam により表示されるカメラ画像の大きさを Responsive 対応させるために使用しました。やっていることは以下の記事と同じことです。

react-device-detect は、ユーザのデバイスによりカメラ画像のアスペクト比を変える判定をするために使用しました。PC なら 4:3 、スマホなら 9:16 固定にしています。そのため、実際に撮影される写真の範囲がカメラ画像と少々異なる可能性があります。(すみません)

その他、MUI の WEB パーツを使っているのでコード内に登場します。この詳細は割愛します。

React コード解説

モジュールインストール

npm の例です。以下のように追加モジュールをインストールします。

npm install --save react-webcam base-64 cross-blob react-device-detect @mui/material @mui/icons-material @emotion/react @emotion/styled @mui/joy

CSS 設定

react-webcam をアプリ内の期待する位置に表示させるために、以下の設定をしました。アプリ共通 CSS に追加して React コードの必要な箇所に ClassName= で適用していますが、React コード内のインラインスタイル等で指定してもかまいません。

previewimg はプレビュー画像用の CSS です。適宜変更下さい。

/* react-webcam */
.webcam {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0px;
  left: 0px;
}
.previewimg {
  width: 100%;
  max-width: 500px;
  height: auto;
}

React コード抜粋

解説はインラインでコメントとして記載します。

実際には細かいエラー処理や他の画像取得パターンもありコードが複雑なのですが、説明のため極力簡易化しています。

コンポーネント内、関数部分

import React, { useState, useRef, useCallback } from 'react';
import { base64toBlob } from './Functions'; //Base 64 を Blob に変換する関数は外出ししています。
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Fab from '@mui/material/Fab';
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
import Webcam from 'react-webcam';
import AspectRatio from '@mui/joy/AspectRatio';
import { CssVarsProvider } from '@mui/joy/styles';
import { isMobile } from 'react-device-detect';

//中略//

const webcamRef = useRef(null);
const [cameraPreview, setCameraPreview] = useState();
const [file, setFile] = useState();

// カメラ撮影関数
// 撮影ボタンを押した後に実行する処理。
// imageSrc は Base 64 の画像 (JPEG) データ。
// 一旦これを cameraPreview ステートに格納し、画面上にプレビューとして表示します。
// 同時に、base64toBlob 関数で Base 64 から Blob にデータを変換します。
// その後、new File で Blob を File オブジェクトに変換します。ファイル名は固定にしています。
// File オブジェクトを File ステートに格納します。(これを Amazon S3 に送信)
const capture = useCallback(() => {
  const imageSrc = webcamRef.current.getScreenshot();
  if (imageSrc) {
    setCameraPreview(imageSrc);
    const blobw = base64toBlob(imageSrc);
    const filew = new File([blobw], "captured.jpg",{type:"image/jpeg"});
    setFile(filew);
  }
},[webcamRef]);

Base 64 を Blob に変換する関数

  • Functions.js として他の共通関数とともにメインのコンポーネントからは外出ししています。
import { decode as atob } from 'base-64';
import Blob from 'cross-blob';

const base64toBlob = (base64image) => {
  const parts = base64image.split(';base64,');
  const imageType = parts[0].split(':')[1];
  const decodedData = atob(parts[1]);
  const uInt8Array = new Uint8Array(decodedData.length);
  for (let i = 0; i < decodedData.length; ++i) {
    uInt8Array[i] = decodedData.charCodeAt(i);
  }
  return new Blob([uInt8Array], { type: imageType });
};

export { base64toBlob };

コンポーネント内、画面表示部分 (return 内の抜粋)

{/*
カメラから送られてくる画像を表示する部分が Webcam です。
CssVarsProvider と AspectRatio で Webcam を囲み、Webcam の表示内容がユーザの画面サイズによって
アスペクト比固定で動的に変わるようにしています。(レスポンシブ対応)
isMobile でスマホ使用時、PC使用時でアスペクト比や使用するレンズの位置を変えています。
facingMode が environment だとスマホの背面カメラ、user だとスマホのセルフィーカメラまたは PC のカメラです。
*/}
<CssVarsProvider>
  <AspectRatio variant="plain" ratio={(isMobile) ? "9/16" : "4/3"} sx={{width:"100%"}} my={2}>
    <Webcam
      audio={false}
      ref={webcamRef}
      screenshotFormat="image/jpeg"
      screenshotQuality={1}
      videoConstraints={(isMobile) ? {facingMode:{exact:"environment"}} : {facingMode:"user"}}
      className="webcam"
    />
  </AspectRatio>
</CssVarsProvider>

{/*
撮影ボタン部分
撮影ボタンは Fab です。onClick で capture 関数が呼び出され、そのときのカメラ画像が
Base 64 JPEG データとしてアプリ内で取得され、その後 capture 関数内で File オブジェクトに変換されます。
Stack で囲んでいるのは、撮影ボタンの位置をセンタリングしたいだけです。
*/}
<Stack direction="row" justifyContent="center" alignItems="center" spacing={0} my={2}>
  <Fab onClick={capture} color="error">
    <PhotoCameraIcon />
  </Fab>
</Stack>

{/*
プレビュー部分
cameraPreview ステートは撮影により取得した Base 64 の JPEG 画像データです。
これをそのまま img タグに食わせると、プレビューを表示できます。
撮影前 (cameraPreview に値が格納されていないとき) は
プレビューが表示されないコードにしています。
*/}
{(cameraPreview) && (
  <React.Fragment>
    <Typography variant="caption" component="div" mt={1} gutterBottom>送信画像プレビュー</Typography>
    <img id="cameraPreview" src={cameraPreview} alt="cameraPreview" className="previewimg"/>
  </React.Fragment>
)}

まとめ

いかがでしたでしょうか?

出来上がってみると何てことないコードなのですが、特に以下 2 点に苦労しました。

  • react-webcam で生成されるカメラ画像の表示位置とサイズ調整。公式ドキュメントのサンプルコードのままだと意図しない変な位置にずれるので、CSS や 親コンポーネントの設定の組み合わせで何とかしました。
  • react-webcam で取得した画像データ (Base 64) からファイルオブジェクトに変換する処理。そもそもそのようなデータフォーマットに詳しくないので、何が正解かわからない状況で動くようにするのは苦労しました。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもAWSすげー!って気持ちの方が強いです。
取得資格:AWS 認定は12資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2023 Japan AWS Top Engineer 受賞
2022 - 2023 Japan AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
アプリケーション開発ソリューション
シェアする
タイトルとURLをコピーしました