GASではカメラ起動できない?
リアルタイムカメラを起動する裏技

GAS WebAppiframe (HtmlService)getUserMedia()allow="camera" なし → ブロックボタンクリック時window.open('', ...)about:blank を別窓で開くpostMessage()window.open()about:blank popupgetUserMedia()iframe外 → カメラ起動できる撮影後に親窓へ画像を返す

GAS HtmlService の iframe 制限により getUserMedia() は直接使えません。about:blankポップアップで回避する実装を解説します。

|対象: GAS HtmlService / getUserMedia / リアルタイムカメラ

Table of Contents

なぜ普通の方法ではカメラが起動しないのか

GAS Webアプリは HtmlService で配信されます。この画面は Google 側の iframe 内で動作します。 そのため、通常のWebアプリのように以下を書いても、環境によっては失敗します。

navigator.mediaDevices.getUserMedia({
  video: true,
  audio: false
});

失敗する理由

GAS Webアプリが iframe 内で動作しており、その iframe にカメラ利用を許可するallow="camera"が付与されていないためです。 つまり、GASの画面内で直接リアルタイムカメラを起動するのは基本的に厳しいです。

回避策:about:blank のポップアップを使う

ポイントは、GASのiframe内でカメラを起動しないことです。about:blankのポップアップは GAS の iframe 外で動くため、PCブラウザではgetUserMedia()が通るケースがあります。

Flow

01
GAS Webアプリ画面
02
ボタンクリック
03
window.open('', ...) を実行
04
about:blank の別ウィンドウを開く
05
document.write() でカメラ画面を生成
06
その別ウィンドウ内で getUserMedia() を実行
07
撮影後 postMessage でGAS側へ画像データを返す

PC向けの実装コード

GAS の HtmlService 上で動かす JavaScript に、以下の関数を追加します。 ボタンクリック時にポップアップを開き、その中でカメラを起動します。

function openCameraPopup(target) {
  const popup = window.open(
    '',
    'camera_popup',
    'width=480,height=700,toolbar=no,menubar=no'
  );

  if (!popup) {
    alert('ポップアップがブロックされました');
    return;
  }

  const html = `
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body {
      margin: 0;
      background: #000;
      display: flex;
      flex-direction: column;
      height: 100vh;
      font-family: sans-serif;
    }
    video {
      width: 100%;
      flex: 1;
      object-fit: cover;
    }
    button {
      margin: 16px auto;
      width: 80px;
      height: 80px;
      border-radius: 50%;
      background: #fff;
      border: 4px solid #888;
      cursor: pointer;
    }
    #status {
      color: #ccc;
      text-align: center;
      padding-bottom: 12px;
    }
  </style>
</head>
<body>
  <video id="video" autoplay muted playsinline></video>
  <button id="capture">撮影</button>
  <div id="status">カメラ起動中...</div>
  <canvas id="canvas" hidden></canvas>

  <script>
    let stream = null;

    navigator.mediaDevices.getUserMedia({
      audio: false,
      video: { facingMode: { ideal: 'environment' } }
    }).then(function(s) {
      stream = s;
      const video = document.getElementById('video');
      video.srcObject = stream;
      document.getElementById('status').textContent = '撮影できます';
    }).catch(function(error) {
      document.getElementById('status').textContent =
        'カメラ起動失敗: ' + error.message;
    });

    document.getElementById('capture').onclick = function() {
      const video = document.getElementById('video');
      const canvas = document.getElementById('canvas');

      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      canvas.getContext('2d').drawImage(video, 0, 0);

      const dataUrl = canvas.toDataURL('image/jpeg', 0.9);

      if (window.opener) {
        window.opener.postMessage({
          type: 'cameraCapture',
          target: '${target}',
          dataUrl: dataUrl
        }, '*');
      }

      if (stream) {
        stream.getTracks().forEach(function(track) { track.stop(); });
      }
      window.close();
    };

    window.addEventListener('beforeunload', function() {
      if (stream) {
        stream.getTracks().forEach(function(track) { track.stop(); });
      }
    });
  </script>
</body>
</html>
`;

  popup.document.write(html);
  popup.document.close();
}

GAS側の画面で画像を受け取る

ポップアップで撮影した画像はpostMessage()で親画面に返します。GAS側の画面では以下で受け取ります。

window.addEventListener('message', function(event) {
  if (!event.data || event.data.type !== 'cameraCapture') {
    return;
  }

  const target = event.data.target;
  const dataUrl = event.data.dataUrl;

  console.log('撮影対象:', target);
  console.log('画像データ:', dataUrl);

  // ここでプレビュー表示、Drive保存、OCR処理などにつなげる
});

実務での注意点

1. window.open はクリックイベント内で同期実行する

ボタンクリック直後にwindow.open()を呼ぶ必要があります。非同期処理の後に呼ぶと、ブラウザにポップアップブロックされやすくなります。

button.addEventListener('click', function() {
  openCameraPopup('receipt');
});

2. postMessage の "*" は本番では注意

サンプルでは簡略化のためにpostMessage(data, '*')としています。本番では送信先 origin を絞れるなら絞った方が安全です。 ただし、GAS Webアプリはscript.google.comscript.googleusercontent.comが絡むため、実環境でlocation.originを確認して設計する必要があります。

3. モバイルでは別方式が現実的

スマホではwindow.open()方式が不安定になることがあります。file input 方式の方が安定します。

<input
  type="file"
  accept="image/*"
  capture="environment"
  id="cameraInput"
>
document.getElementById('cameraInput').addEventListener('change', function(e) {
  const file = e.target.files[0];
  if (!file) return;

  console.log(file);
});

PCとモバイルの分岐例

タッチデバイス判定でPC・モバイルを分岐し、それぞれ最適な方式を使います。

function isTouchDevice() {
  return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}

function startCamera(target) {
  if (isTouchDevice()) {
    document.getElementById('cameraInput').click();
  } else {
    openCameraPopup(target);
  }
}

まとめ

GAS Webアプリでは、通常のWebアプリのように画面内で直接リアルタイムカメラを起動するのは難しいです。 しかし、PCであればabout:blankのポップアップを使うことで、GASだけでもリアルタイムカメラ起動は可能です。

PC

about:blank ポップアップ方式でリアルタイムカメラ可能

モバイル

capture属性付き file input が現実的

GAS iframe内で直接 getUserMedia

基本的に厳しい

応用できる業務用途

領収書撮影・OCR連携
本人確認書類の取得
現場写真アップロード
Drive自動保存

よくある質問

基本的に難しいです。GAS Webアプリはiframe内で動作しており、そのiframeにはallow="camera"が付与されていないため、getUserMedia()がブロックされることがほとんどです。PCであれば、about:blankのポップアップウィンドウを開いてその中でgetUserMedia()を呼ぶ回避策が有効です。

スマホでは、リアルタイムプレビューにこだわらずに、input要素のtype="file"とcapture="environment"属性を使う方法が現実的です。カメラアプリが起動し、撮影後に画像ファイルとして受け取れます。GASのWebアプリ内で使えます。

postMessage()を使います。ポップアップ内でcanvas.toDataURL()で画像をBase64に変換し、window.opener.postMessage()でGAS側の親画面へ送ります。GAS側ではwindow.addEventListener('message', ...)で受け取って処理できます。

ブラウザはユーザー操作に紐づかないwindow.open()をブロックします。ボタンのclickイベントハンドラー内で同期的にwindow.open()を呼ぶ必要があります。非同期処理(setTimeout、Promiseのthen、google.script.run)の後に呼ぶとブロックされやすくなります。

はい。postMessageで受け取ったBase64画像を、GASサーバー側でDrive保存、Vision API(OCR)送信、スプレッドシート記録などに連携できます。領収書撮影・本人確認書類確認・現場写真記録などの業務用途に応用できます。

GAS開発・業務Webシステムを
相談する。

カメラ機能・OCR連携・Drive保存などを含む業務システムの開発もご相談いただけます。 内容が固まっていなくても大丈夫です。現在の業務・課題をお聞きした上でご提案します。

GAS Webシステム開発を見る