TAILSCALE 対応カメラの製作

更新日: 2026.08.08

概要

ここで製作したESP32-camの続きです。 外部からのアクセスに対しTailscaleは非常に便利なのですがESP32-CAMにTailscaleは使えません。 そこでTailscaleに登録出来るサーバを通して外部よりカメラにアクセスしようと考えています。 さらにそのサーバがカメラを複数台管理出来ればカメラの数に関係無くサーバ1台のみのTailscaleへ 登録で全てのカメラにアクセス出来ます。今回はこの要求に対応したESP32-CAMサーバの製作です。

ESP32-CAMとカメラ管理サーバとTailscale

現状のESP32-CAMサーバ単体、管理サーバを通した場合、最後にTailscaleを使用した場合の通信状態の理解。

現状のESP32-CAM

ESP32-CAM & its controll HP
  • クライアントがカメラサーバ(例192.168.x.x)にアクセスし、操作HPを取得
  • 操作HPがカメラサーバとデータのやり取りを行いカメラを操作

カメラ管理サーバを通した場合

ESP32-CAM & Multi_Server
  • ESP32-CAMを複数台製作(例:192.168.a,b,c...)
  • 同じLAN内に管理用サーバ(例192.168.x.x)を上げる。管理サーバは
    • ESP32-CAMを登録する機能(画面左の小窓に登録)
    • 選んだカメラを表示する機能(右の大きい窓に表示)
    • "<iframe>"を使って管理サーバの中にカメラサーバをそのまま取り込む。
      • 管理サーバ側のHPで例えば< iframe.src = "http://192.168.x.a"><iframe>と指定しカメラサーバのHTMLを読み込む。
      • これにより管理サーバの中でカメラHPが独自に動く。

この状態で管理サーバにTailscaleを使用すれば

    • ESP32-CAMの数に関係無くTailscaleに登録するのは管理サーバ1つのみ
    • Tailscaleに登録出来ないESP32-CAMを外部からアクセス出来る

理想的と思ったのですがそうでは無かったです。

この状態でカメラ管理サーバにTailscaleを使用した場合

サーバにTailscaleを適応したら "<iframe>"部分が真っ黒になってしまいました。

ESP32-CAM & Multi_Server under Tailscale

これは

  • Tailscaleにより管理サーバは仮想環境のIP(例:100.x.x.x)になる。
  • ESP32−CAMサーバにTailscaleを適応していないのでカメラHPは自分のサーバ(例192.168.x.a,b,c)にアクセスしようとする
  • 仮想環境にESP32−CAMのIP(例192.168.x.a,b,c)は無く、データが取れず画像を表示出来ない。(真っ暗になる)。

ちょっとがっかり。でも対策が有りました。

中継サーバを使う

Tailscale下でもサーバ同士ならデータのやり取りが出来る事が分かりました。 Tailscale下となった管理サーバは仮想環境IP(例:100.x.x.x)とローカルLAN内IP(例192.168.x.x)で機能するようです。

ESP32-CAM & Multi_Server under Tailscale with Proxy server
  • "<iframe>"内のカメラHPがカメラサーバにアクセスする時に一度管理サーバを通す。
    • これはカメラHPのカメラサーバアクセスURLの先頭に目印(例:/xyz/)をつければ可能
  • リクエストを受けた管理サーバは目印を取って実際のカメラサーバにアクセスしデータを取得。
  • 取得したデータをそのまま"<iframe>"内のカメラHPに返す。
  • "<iframe>"内のカメラHPがそのデータを表示する。

これでTailscale下でカメラの表示が出来る様になったのですが新たな問題が。

カメラ側HPは2つ必要?

元々このカメラサーバは中継サーバを通さずに直接アクセス出来ていました。 当然ですが先頭に目印をつければそれが邪魔をして直接アクセスは出来ません。 直接、サーバ両方のアクセスに対応するにはカメラHPに呼び出された時に 直接かサーバーからかを判断する機能が必要です。実はこれ、"window.location" を使えば 判断出来る事が分かりました。

両方のアクセス(直接、サーバ)に対応するカメラ側HPの製作

カメラ側HPの冒頭で下記を実行しアクセス先を判断します。直接アクセスする場合は、IPアドレスで(例:"192.168.x.a") サーバからアクセスする時には"/proxy/?targetUrl=192.168.x.a"とアクセスすれば下記の箇所で両者を判別出来ます。

index_2ax.html html
<script> let timerId = null; let para, sv_para; window.proxyBaseUrl = ""; if(window.location.search.includes("targetUrl") || window.location.pathname.includes("/proxy/")){ const urlParams = new URLSearchParams(window.location.search); targetUrl = urlParams.get("targetUrl"); if (targetUrl) { try { const parsedUrl = new URL(targetUrl); window.proxyBaseUrl = "http://" + parsedUrl.hostname; } catch (e) { console.log("URLの解析に失敗しました: ", e); } } }
  • 145行:グローバル変数 window.proxyBaseUrl = "";を宣言
  • 147行:window.locationに"targetUrl"または"/proxy/"の有無を判別
    • 有る場合:サーバからのアクセス。
      • window.proxyBaseUrl にカメラIPを保存("http://" + parsedUrl.hostname;)
    • ない場合:直接アクセス。
      • 何もしない。window.proxyBaseUrl は空。
  • グローバル変数 window.proxyBaseUrlには直接アクセスなら空。サーバからのアクセスなら接続先のカメラIPが保存されます。

index_2ax.html html
function getProxyUrl(originalPath) { if (window.proxyBaseUrl) { let decodedBase = decodeURIComponent(window.proxyBaseUrl); let cameraIp = decodedBase.replace("/proxy/?targetUrl=", "").replace(/\/+$/, ""); let finalTarget = cameraIp + originalPath; return "/proxy/?targetUrl=" + encodeURIComponent(finalTarget); } else { return originalPath; } } document.getElementById("10").addEventListener('change', function (event) { let targetElement = event.target || event.srcElement; let val = "1," + targetElement.value; let originalPath = '/?2=' + encodeURIComponent(val); let finalUrl = getProxyUrl(originalPath); // サーバに解像度変更コマンドを送信してリロード let xhr = new XMLHttpRequest(); xhr.open('GET', finalUrl); xhr.onload = function() { if (xhr.status === 200) { console.log("解像度が変更されました"); item_setup();

カメラHPがカメラサーバにアクセスする時に

    • 189行:これがカメラHPがカメラサーバにアクセスする為のURL
    • 190行:アクセスする前に関数getProxyUrl()でURLを変換する
    • 174行:getProxyUrl(originalPath)関数
      • window.proxyBaseUrl
        • 空:直接アクセスー>変更無し
        • 有:サーバアクセスー>頭にwindow.proxyBaseUrlを追加
    • 変換後のURLで直接、サーバからのアクセスに関わらず正しくカメラにアクセス出来る。

追加のESP32-CAM

手持ちに”WROVER_KIT”、”ESP32S3_EYE”が有ったのでそれらもStreamingカメラとして追加しました。

Location of sample sketch

今回追加した2機種はボードの大きさがAI_THINKERより大きく、2軸のステージに乗せるのを諦めて固定型にしました。 スマホフォルダーを使って下記に様にしています。首振り機能は無いですが魚眼レンズを使用すれば影範囲は広がります。

Location of sample sketch

各機種の回路は下記の通り。左が2軸ステージに載せた場合。右が固定の場合の回路です。

Location of sample sketch

実行する前に

今回使用したコードをここに保存しました。 実行する前に下記の準備を行って下さい。

  • "wifi_config.txt" の修正
    • このファイルをエディターで開くと下記の様になっています。
    • SSID,PASSWORD,aaa,bbb,xxx,mail_addr

    • 6個のパラメタをカンマ "," で区切った文字列。各パラメタに下記の値をセットします。
        パラメタ説明
        SSIDルータのSSID
        PASSWORDルータのパスワード
        aaaGatewayアドレス3番めの値 例:192.168.aaa.bbb
        bbbGatewayアドレス4番めの値 例:192.168.aaa.bbb
        xxxカメラアドレス4番めの値  例:192.168.aaa.xxx
        mail_addrメールの送り先アドレス
    • Gatewayアドレスはルータの機種で違いますが、192.168.aaa.bbbでカバーできるはずです。
    • カメラのハンドリングを考えてIPアドレスを固定します。IPアドレスは "192.168.aaa.xxx" になります。
    • プログラムで改行を目印にこの文字列を読み込んでいます。必ず改行を入れてファイルを保存して下さい。

  • "esp32_net_camera.ino"の修正
      esp32_net_camera.ino arduino
      // --- Gmail設定 --- #define SMTP_HOST "smtp.gmail.com" #define SMTP_PORT 465 /* The sign in credentials */ #define AUTHOR_EMAIL "xxxxxxxx@gmail.com" #define AUTHOR_PASSWORD "yyyyyyyyyyyyyyyy" /* Recipient's email*/ String recipient_email = "zzzzzzzzzzzzz";
    • 72行:#define AUTHOR_EMAIL "xxxxxxxx@gmail.com" //Gmail 貴方のメールアドレス
      • ここに自分のGメールアドレスを指定
    • 73行:#define AUTHOR_PASSWORD "yyyyyyyyyyyyyyyy" //16桁のアプリパスワード
      • ここにGmailで取得した16桁のアプリパスワードを指定
    • 76行:String recipient_email = "zzzzzzzzzzzzz";
      • ここにメールの送信先アドレスを指定して下さい。
    • esp32_net_camera.ino arduino
      if(digitalRead(mode_pin)){ Serial.println("Starting in AP Mode..."); WiFi.softAP("ESP32CAM-SETUP", "abcdefg"); // SSID & Passowrd Serial.print("AP IP Address: "); Serial.println(WiFi.softAPIP()); httpd_handle_t wifi_server = NULL;
      • 221行:WiFi.softAP("ESP32CAM-SETUP", "abcdefg"); // SSID & Passowrd
        • ESP32-CAMがAPとして立ち上がる時のSSIDとPassowrdを指定します
        • SSID:"ESP32CAM-SETUP" と設定していますが、必要に応じて変更下さい。
        • Passowrd:"abcdefg" と設定していますが、必要に応じて変更下さい。

  • カメラ、HPをコード "esp32-net-camera.ino" の冒頭で指定します。
      esp32-net-camera.ino arduino
      //#define CAMERA_MODEL_AI_THINKER // Has PSRAM //#define CAMERA_MODEL_WROVER_KIT // Has PSRAM #define CAMERA_MODEL_ESP32S3_EYE // Has PSRAM #include "pin_define.h" //#define index_2ax #define index_fix #if defined(index_2ax) #define main_html "/index_2ax.html" #elif defined(index_fix) #define main_html "/index_fix.html" #endif
    • 13(AI_THINKER), 14(WROVER_KIT), 15(ESP32S3_EYE)行でボードを選択
    • 19(2軸用), 20(固定用)行でHPの種類を選択します
    • 固定用のHPは2軸用からカメラ移動用の十字キーを除いています。
        Location of sample sketch

最後に

直接、サーバからアクセス対応のESP32-CAMサーバが出来ました。 これに対応したサーバはここで説明しています。 このサーバとの組み合わせならTailscaleに対応します。ESP32-CAMに外部からアクセス出来ます。

SINCE 2026