Turn an old Android phone into a feature-rich IP camera with IP Webcam, and fix iPhone Safari two-way audio using an Apache proxy and a small JavaScript patch.
相模原市で IoT 設計を受託しているファームロジックスです。
皆様のところには、使わなくなった Android スマートフォンはありませんか? 今回は、そんなスマートフォンをネットカメラに変身させる素晴らしいアプリ “IP Webcam” と、さらにそれを iPhone からでも双方向音声に対応させる修正パッチのご紹介です。
ファームロジックスでは、2007年(創業前ですが)にパナソニック社製の BL-C131 というネットカメラを導入しています。当時としては画期的な製品であり、今でも故障せずに元気に働いてくれているのですが、次のように、さすがに性能が見劣りするようになってしまいました。
- カメラの解像度が最大でも 640×480 ピクセル
- 現代のウェブブラウザでは音声を聞くことができない
などです。
最近、現役引退させたスマートフォン Huawei P20 lite(型番 ANE-LX2J)を眺めながら、これをネットカメラにできないかな、と思いつきました。つまり、このスマートフォンを宅内、オフィス内に設置し、外から部屋の様子を見ることができないか、ということです。
調べてみると、そんな Android アプリがいくつかあるようですが、今回は、Thyoni Tech という開発元の IP Webcam というアプリを試してみることにしました。このアプリは、名称があまりに一般的でネット検索で見つけづらいと思うので、こちらにリンクを貼らせていただきます。
特徴ですが、
- Android 5.0 以上で動作(ただし、機種に依存するらしいです)
- アプリサイズがコンパクト(12MB 程度)
- 比較的頻繁に更新(メンテナンス)されている
- 双方向音声通信(two-way audio)に対応するなど、非常に高機能
- 一般のウェブブラウザから閲覧できる
- ネット上の公開サービスを使わなくても利用できる
といったところが、私は気に入りました。特に、「ネット上の公開サービスを使わなくても利用できる」というところは重要で、宅内(あるいはオフィス内。以下略)に頑健な VPN サーバーがあれば、パスワードの漏洩を気にすることなく、end-to-end で暗号化してネットカメラを運用することができる、ということになります。(インターネット上のサービスを利用するタイプのアプリでは、その辺りが心配ですよね。)
一方で、少し残念なのは、
- 正直言って、GUI があまり洗練されているとは言えない
- iPhone の Safari ブラウザでは「双方向音声」が利用できない
という点です。
後者については、macOS 上の Safari(バージョン15.0)、iPhone X(iOS 16.7.12)の Safari で、双方向音声が利用できないことを確認済です。私は詳しくないのですが、iOS ではどのようなブラウザを使っても描画・通信の中核は同一エンジン(WebKit)なので、Safari、Chrome、Firefox、Edge などを問わず、この振舞いは変わらないものと考えています。
どのようになるのか。実際に Safari で画面を見てみましょう。
IP Webcam に外部からウェブブラウザで接続すると、上部に次のような操作パネルが表示されます。
ここで、Two-way audio というボタンをクリックし、画面に従って HTTPS 通信を選ぶと双方向音声ができるはずなのですが、Safari ではカメラ画像の下に次のような表示が出て、Two-way audio のトグルがオフに戻ってしまいます。
ここからは技術的なお話
さて、どうしましょうか。実は個人的な事情ですが、家族が iPhone を使っていて、Safari で双方向音声ができないことを残念に思った、というのが発端です。ここからは、かなり技術的な話となりますので、もし、御自分では対応が難しい方は、ウェブ技術やサーバー運用技術を御存知の方に以下の説明を見て頂き、対応をお願いしてください。(ファームロジックスにお問い合わせ頂ければ、有償で対応させて頂くことも可能です。)
まずは前提として VPN
IP Webcam アプリ自体は、前述したように、インターネット上の公開サーバー等を使わずに利用できるソフトウェアですが、その結果として、IP Webcam の動作しているスマートフォンに、インターネットからセキュアに(安全に)アクセスできるようにするには、VPN 等の技術を使う必要があります。
おうちのルーターにポートフォワーディングという機能があれば VPN なしでも接続することはできるのですが、インターネットから攻撃対象となったり、HTTPS を使わずにアクセスしてしまった場合にパスワード漏洩のリスクがあったりするので、お勧めできません。信頼できる最新バージョンの VPN ソフトウェアを利用することを強くお勧めいたします。
最初の想定: TLS 証明書が自己署名なのがいけないんじゃないか?
正攻法では、Safari のブラウザ Developer Tools を使って、エラーの原因を追跡すべきなのでしょうが、私はウェブ技術の専門家ではないので、それは諦めました。
最初に想像したのは、IP Webcam アプリに HTTPS(TLS)でアクセスすると、自己署名の証明書(Self-Signed Certificate。いわゆるオレオレ証明書)を使用することになるので、Safari ブラウザが WebSocket 絡みの通信を拒絶しているのではないか、というものでした。(それ以前に、Safari ではマイク入力をサポートしていないのではないか、という疑いもありましたが、まずは証明書から疑いました。実際には、私の試したバージョンの Safari ではマイクはサポートされていました。)
なお、Safari で双方向音声が通らない真の原因については、ブログの末尾のほうで言及させていただきます。
どうしたら良いのでしょう? 今回は、ウェブサーバーソフトウェアの Apache 2 HTTP Server を利用し、Reverse proxy によって HTTPS を中継してやれば良いのではないか、というアイデアを思いつきました。ファームロジックスでは、自己署名ではない証明書を持ったウェブサーバーを Apache で運用しているので、それを利用することにします。
注意点 2つ
ここで、2つの注意点があります。
1つ目は、公開している Apache サーバーを流用する場合、IP Webcam が外部(インターネット)からアクセスできないように配慮が必要ということです。つまり、Apache のバックエンドに IP Webcam を置くとはいうものの、このApache フロントエンドはあくまでも VPN 経由でアクセスすることが前提、ということです。(本当は専用の Apache サーバーを動かせばスマートですが、今回は既存のインターネット公開用 Apache サーバーを流用したので、話がややこしくなっています。すみません。)
Apache サーバーはグローバル IP アドレスを使って公開しているので、Apache を動作させているホスト自体への IP アドレス制限ではうまくいきません。Apache 自身の機能を使って制限する必要があります。具体的には、サイトコンフィグレーションの .conf ファイルで
<Location />
Require ip 192.168.0.0/24
</Location>
のように記述することになります。(当然ですが、192.168.0.0/24 は例です。みなさんのサイトに合わせて設定してください。) また、Location を使わない、もっと良い方法があるかもしれません。セキュリティの重要な肝ですので、正しくアクセス制御できているか十分に確認してください。(以下、同様の注意は省略しますが、自己責任でお願いします。)
2つ目の注意点です。これは蛇足ではありますが、TLS サーバー証明書の SAN(Subject Alternative Name)に、公開するホスト名が含まれていることを確認してください。たとえば IP Webcam を foo.bar.com で公開する場合、bar.com だけを対象にした証明書では foo.bar.com に対して有効になりません。必要に応じて foo.bar.com を含む証明書、または *.bar.com のワイルドカード証明書を用意してください。
Reverse proxy の設定
次は、Apache サーバーの Reverse proxy の設定です。IP Webcam では、双方向音声時に WebSocket を利用するので、wss の記述も重要です。ブログの後のほうで、全て網羅した configuration ファイルを示しますので、しばらくは断片でご容赦ください。
また、ここからの注意ですが、使用する IP Webcam アプリのバージョンは 1.18.3r.911 を想定しています。バージョンが異なる場合、この設定ではうまくいかない可能性があります。
<VirtualHost *:443>
ServerName foo.bar.com
SSLEngine on
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
ProxyPreserveHost On
ProxyRequests Off
SSLProxyEngine On
# --- Accept self-signed on backend (weaker security) ---
SSLProxyVerify none
SSLProxyCheckPeerName off
SSLProxyCheckPeerCN off
# --- WebSocket endpoint ---
ProxyPass "/audioin.wav" \
"wss://ipwebcam.intra.bar.com:8080/audioin.wav" retry=0
ProxyPassReverse "/audioin.wav" \
"wss://ipwebcam.intra.bar.com:8080/audioin.wav"
# --- Usual HTTPS requests ---
ProxyPass "/" \
"https://ipwebcam.intra.bar.com:8080/" retry=0
ProxyPassReverse "/" \
"https://ipwebcam.intra.bar.com:8080/"
</VirtualHost>
なお、この設定では Apache とバックエンド間の TLS 証明書検証を無効化していますので、注意してください。(IP Webcam が閉域ネットワーク内にあることが前提です。念の為。)
さらに、Apache では次のようにして必要なモジュールを有効化する必要があります。
sudo a2enmod proxy sudo a2enmod proxy_http sudo a2enmod proxy_wstunnel sudo a2enmod ssl sudo systemctl reload apache2
ブラウザの「オレオレ警告」は無くなったけど…
さて、これでどうでしょう? 試してみたところ、スマートフォンや PC の Google Chrome では、自己署名証明書の警告は無くなりましたが、Safari ブラウザから双方向音声ができない問題は解決しませんでした。(がっかり)
どうしましょう。私は WebSocket や MediaDevices(WebRTC API)、それらを JavaScript で設計するノウハウがないので、詰んでしまった気がしました。しかし、時代は AI 時代です。ChatGPT に相談してみることにしましょう。
JavaScript コンソールのエラーを ChatGPT に分析してもらう
IP Webcam を Safari ブラウザから開いた状態で Two-way audio スライダーをクリックすると、Safari の JavaScript コンソールに、次のようなエラーが出ることが分かりました。
[Error] Unhandled Promise Rejection: TypeError: Can only call MediaDevices.getUserMedia on instances of MediaDevices
(anonymous関数)
rejectPromise
getUserMedia
twaInit (ipwebcam.js:662)
(anonymous関数) (ipwebcam.js:584)
このエラーについて、ChatGPT に分析して貰いましょう。分析のためには、JavaScript のコードがあったほうが良さそうなので、ipwebcam.js も渡してみます。以下が ChatGPT の分析結果です。
Safari で発生しているこのエラーは、
getUserMediaを別の変数に代入してから呼び出すと、内部で使用されているthisの参照が失われ、例外が発生してしまうというブラウザ実装上の差異に起因していると考えられます。元の
ipwebcam.jsでは、ブラウザごとの差異を吸収する目的でnavigator.getUserMediaに複数の実装を合成代入していましたが、この書き方が Safari では正しく動作しませんでした。そのため、Promise ベースの
navigator.mediaDevices.getUserMedia()を優先して使用し、Safari でもthisが壊れない呼び方に切り替える、という方針を採用しました。従来のコールバック型 API は、レガシー環境向けのフォールバックとして明確に分離しています。
だそうです。(汗) JavaScript と WebSocket エクスパートの皆さん、お分かりになりますでしょうか。
さて、以下がパッチです。繰り返しになりますが、これは IP Webcam 1.18.3r.911 に対するものです。もしバージョンが変わって ipwebcam.js が変更となった場合(このパッチと同様の修正が適用されていればベストなのですが)、新しいバージョンの ipwebcam.js とこのパッチを ChatGPT に渡せば、新しいバージョン用のパッチを提供してくれるのではないかと思います。(ChatGPT 任せですみません。)
--- orig/ipwebcam.js 2026-01-23 18:32:12.000000000 +0900
+++ ipwebcam.patched.js 2026-01-23 19:09:21.000000000 +0900
@@ -653,14 +653,32 @@
return
}
- var navigator = navigator || window.navigator;
- navigator.getUserMedia = navigator.getUserMedia ||
- navigator.webkitGetUserMedia ||
- navigator.mozGetUserMedia ||
- navigator.mediaDevices.getUserMedia ||
- null;
- navigator.getUserMedia({audio:true}, twaSoundAllowed, soundNotAllowed);
- }
+ // Use Promise-based getUserMedia to avoid Safari 15 `this` issues.
+ if (window.navigator.mediaDevices &&
+ typeof window.navigator.mediaDevices.getUserMedia ===
+ 'function') {
+ window.navigator.mediaDevices.getUserMedia({ audio: true })
+ .then(twaSoundAllowed)
+ .catch(soundNotAllowed);
+ return;
+ }
+
+ // Fallback for legacy callback-based implementations.
+ var legacyGetUserMedia = window.navigator.getUserMedia ||
+ window.navigator.webkitGetUserMedia ||
+ window.navigator.mozGetUserMedia ||
+ null;
+ if (!legacyGetUserMedia) {
+ soundNotAllowed(new Error('getUserMedia not supported'));
+ return;
+ }
+ legacyGetUserMedia.call(
+ window.navigator,
+ { audio: true },
+ twaSoundAllowed,
+ soundNotAllowed
+ );
+ }
var twaStart = function () {
if (!twaWebsocket) {
return twaConnectWebsocket()
さて、ipwebcam.js を修正して貰ったのは良いのですが、これをどうしたら良いのでしょう? 素晴らしいことに、Apache ウェブサーバーでは、Reverse proxy をする際に、特定のファイルだけ差し替える、ということができるのだそうです。Apache のサイト .conf ファイルで次のように書けます。
ProxyPass "/js/ipwebcam.js" "!" Alias /js/ipwebcam.js /var/www/patched/ipwebcam.js
まとめてみよう
さて、今までの修正を全てまとめてみましょう。Apache 2 のサイト .conf ファイルは次のようになります。
# ------------------------------------------------------------
# foo.bar.com
# Reverse proxy to IP Webcam (HTTPS + WSS) with Safari support
# ------------------------------------------------------------
# --- HTTP: force HTTPS and restrict by IP -------------------
<VirtualHost *:80>
ServerName foo.bar.com
# Allow only internal network
<Location />
Require ip 192.168.0.0/24
</Location>
# Force HTTPS
Redirect permanent / https://foo.bar.com/
</VirtualHost>
# --- HTTPS: main reverse proxy ------------------------------
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName foo.bar.com
ErrorLog ${APACHE_LOG_DIR}/ipwebcam-error.log
CustomLog ${APACHE_LOG_DIR}/ipwebcam-access.log combined
# Common TLS settings (cert, protocols, ciphers)
SSLEngine on
SSLCertificateFile /path/to/fullchain.pem
SSLCertificateKeyFile /path/to/privkey.pem
# Allow only internal network
<Location />
Require ip 192.168.0.0/24
</Location>
ProxyRequests Off
SSLProxyEngine On
# Backend uses self-signed certificate
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
SSLProxyCheckPeerExpire off
# --------------------------------------------------------
# Safari compatibility for WebSocket
#
# Safari is strict about frames and fails when
# permessage-deflate is negotiated through a proxy.
# We drop the extension header only for websocket
# upgrade requests.
# --------------------------------------------------------
SetEnvIfNoCase Upgrade websocket is_websocket=1
RequestHeader unset Sec-WebSocket-Extensions env=is_websocket
# Forward original scheme/host to backend
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Host "foo.bar.com"
# --------------------------------------------------------
# Serve patched ipwebcam.js locally
#
# Safari 15 breaks when getUserMedia() is called
# with a lost 'this' binding in the original script.
# We override only this file and keep everything
# else proxied to the backend.
#
# NOTE: must be BEFORE ProxyPass "/"
# --------------------------------------------------------
ProxyPass "/js/ipwebcam.js" "!"
Alias /js/ipwebcam.js /var/www/patched/ipwebcam.js
<Location "/js/ipwebcam.js">
Require ip 192.168.0.0/24
</Location>
# --------------------------------------------------------
# WebSocket endpoint (two-way audio)
#
# Must use wss:// directly; routing through normal
# HTTPS ProxyPass breaks frames and causes
# "opcode 12" errors in Safari.
# --------------------------------------------------------
ProxyPass "/audioin.wav" \
"wss://ipwebcam.intra.bar.com:8080/audioin.wav" retry=0
ProxyPassReverse "/audioin.wav" \
"wss://ipwebcam.intra.bar.com:8080/audioin.wav"
# --------------------------------------------------------
# Default reverse proxy to backend
# --------------------------------------------------------
ProxyPass "/" \
"https://ipwebcam.intra.bar.com:8080/" retry=0
ProxyPassReverse "/" \
"https://ipwebcam.intra.bar.com:8080/"
</VirtualHost>
</IfModule>
なお、私は Apache サーバー設定のエクスパートでないので、全てを説明しきれません。ChatGPT に解説して貰いましたので、その説明で代えさせてください。(AI 時代のブログだなあ…)
ChatGPT による Apache .conf ファイルの解説
以下の通りです。
- 2 つの VirtualHost に分けている
*:80は HTTP 用、*:443は HTTPS 用(TLS 終端 + リバースプロキシ本体)。
- HTTP (80) は HTTPS に強制リダイレクトしている
Redirect permanent / https://.../により、平文アクセスを常に HTTPS に寄せる。
- アクセス元 IP 制限を入れている
<Location /> Require ip ...で、指定したネットワークからのみ閲覧できるようにしている。- (同じ制限を 80/443 両方に入れているので、HTTP 側からの覗き見も抑止できる。)
- HTTPS 側で TLS 証明書を指定している
SSLEngine onとSSLCertificateFile / SSLCertificateKeyFile(またはInclude)で証明書を設定。
- Apache を「前段リバースプロキシ」として動かしている
ProxyRequests Off(フォワードプロキシ化を防ぐ)ProxyPass / ProxyPassReverseでバックエンド(IP Webcam)に転送している。
- バックエンドが自己署名証明書でも通す設定になっている
SSLProxyEngine Onと、SSLProxyVerify noneなどで検証を緩めている。- これは「手軽さ優先」の設定なので、読者には “理解した上で使う” 注意点になる。
- Safari の WebSocket 互換性対策をしている
SetEnvIfNoCase Upgrade websocket ...で WebSocket リクエストだけを判別し、RequestHeader unset Sec-WebSocket-Extensions ...でpermessage-deflate交渉を落としている。- 目的は「Safari がプロキシ越しの圧縮 WebSocket で壊れるケースを避ける」こと。
- バックエンドへ元情報を渡すヘッダを付けている
X-Forwarded-Proto/X-Forwarded-Hostを付与して、バックエンドが “元は HTTPS” を認識できるようにしている。
ipwebcam.jsだけローカル差し替えしているProxyPass "/js/ipwebcam.js" "!"で、このパスだけ “プロキシ対象から除外”。Alias /js/ipwebcam.js ...で、ローカルの patched 版を配信。<Location "/js/ipwebcam.js"> Require ip ...で、このファイル単体にも IP 制限をかけている。- 重要: この 2 行(除外 + Alias)は
ProxyPass "/"より前に置く必要がある。
- Two-way audio の WebSocket エンドポイントを専用に通している
ProxyPass "/audioin.wav" "wss://.../audioin.wav"のように wss:// を明示している。- 通常の
ProxyPass "/"に混ぜると、Safari 側でフレームが壊れる(opcode エラー等)ケースを避ける意図。
- 最後に “それ以外全部” をバックエンドへ転送している
ProxyPass "/" "https://...:8080/"がデフォルトの転送ルール。- これにより UI や静的ファイル等は基本すべてバックエンド任せ。
- 前提となる Apache モジュールがある
- 典型的には
mod_ssl,mod_proxy,mod_proxy_http,mod_proxy_wstunnel,mod_headers(環境によりrewrite等)を有効化している必要がある。
- 典型的には
まとめ
今まで説明してきたように、JavaScript のファイルにパッチを当て、Apache のサイトコンフィグレーションファイルを設定することで、Safari ブラウザでも双方向音声ができるようになりました。
さて、話が長くなりましたが、Safari で双方向音声が通らなかった真の原因は、ChatGPT によれば、WebSocket 圧縮と JavaScript 側の実装差異であるということでした。
なお、上記とは話題が違いますが、iPhone や Safari から IP Webcam を利用する上で一つ注意点があります。これを最初に書くべきだったかもしれません。IP Webcam のウェブインターフェイス画面に音声形式の選択ボタン(Audio player)がありますが、「HTML5 Wav」だと、IP Webcam が動いているスマートフォンからの音声が 聞こえないと思います。私の経験では、「HTML5 AAC」を選択すると音声が聞こえるようです。
お問い合わせはお気軽に!
今回の内容ですが、Apple ユーザーの方には非常に役立つものと自負していますが、内容はかなり技術寄りだったかと思います。正直言いまして、私も全てを理解しているとは申し上げられません。ただし、お問い合わせがあればできるだけ回答させて頂きたいと思いますので、お気軽にご相談ください。

