【DIY】舊手機秒變 IP Cam:移動偵測、對講、錄影,免費部署到 GitHub Pages
家裡抽屜總有幾支退役的舊手機,電池還行、鏡頭也還能用,就這樣放著實在可惜。市面上有「阿福管家(AlfredCamera)」這類 App 可以把舊手機變成監視器,但要裝 App、要註冊帳號,進階功能還要付費。
所以我自己寫了一個「舊機監視器」:只有一個 index.html 網頁檔,放在 GitHub Pages 上,兩支手機用瀏覽器打開就能用,免裝 App、免註冊、不經雲端儲存。這篇把功能、原理、部署方式和開發中踩到的坑整理起來。
👉 原始碼:github.com/cphsia/old-phone-cam
一、能做什麼?
同一個網頁有兩種角色,開啟後選一個就好:
📷 攝影機模式(舊手機)
- 即時影像+聲音傳送
- 移動偵測:畫面有變化就自動截圖,推送給觀看端
- 可被遠端切換前/後鏡頭、開手電筒、響警報器
- 螢幕常亮(Wake Lock),另有「黑屏省電」模式,點兩下恢復
- 可勾選「開網頁自動進入攝影機模式」,停電重開機後不用手動操作
- 畫面上直接顯示配對 QR Code、攝影機代碼與 4 位數 PIN,PIN 可隨時更換
📱 觀看模式(你的手機)
- 掃 QR Code 一鍵連線,或手動輸入「代碼+PIN」
- 按住對講:像對講機一樣,按住說話、放開停止
- 錄影(存到觀看那支手機)、截圖
- 夜視增強(提高亮度與對比、轉黑白)、全螢幕
- 遠端調整移動偵測開關與靈敏度
- 移動通知:震動+提示音+系統通知,附截圖縮圖
- 顯示攝影機端電量、目前是「直連」還是「經 TURN 中繼」
- 斷線自動重連、「我的攝影機」常用清單一鍵連線
二、運作原理
整個系統沒有自己的後端伺服器,靠的是瀏覽器內建的 WebRTC:
- 配對(訊號交換):使用 PeerJS 的免費公用伺服器,讓兩支手機互相「找到對方」。攝影機用固定代碼註冊,觀看端拿代碼去連。
- 穿透網路(STUN):透過 Google 與 Cloudflare 的 STUN 伺服器,讓手機知道自己在外網的位址,嘗試直接打洞連線。
- 影像直送(P2P):連上之後,影像和聲音是兩支手機之間點對點加密直送,不經過任何雲端儲存。
- 中繼備援(TURN):如果兩邊網路太嚴格(例如一支 Wi-Fi、一支 4G)打不通,就需要 TURN 伺服器幫忙轉送。
配對時觀看端會帶上 PIN,攝影機端比對錯誤會直接拒絕並斷線,所以就算有人知道代碼,沒有 PIN 也看不到畫面。
三、部署到 GitHub Pages(免費 https)
手機瀏覽器規定一定要 https 才能開相機,GitHub Pages 剛好免費又自帶 https:
- 在 GitHub 建一個 repo(例如
old-phone-cam),上傳index.html - 進入 Settings → Pages,Branch 選
main// (root),按 Save - 約 1 分鐘後,網址就是
https://<你的帳號>.github.io/old-phone-cam/
四、使用方式
- 舊手機打開網址 → 點「這支當攝影機」→ 允許相機與麥克風 → 接上充電器、架好位置
- 你的手機用相機掃舊手機畫面上的 QR Code,就會自動連線
- 連過一次之後,首頁的「我的攝影機」會記住,之後一鍵就能看
五、跨網路連不上?設定 TURN 中繼
在同一個 Wi-Fi 底下通常直接就能連;但人在外面用 4G 看家裡的攝影機時,常會卡在「已配對但收不到影像」。這時就需要 TURN 伺服器,在兩支手機中間轉送影像。
為什麼要使用 TURN Server?
要理解 TURN,得先知道兩支手機在網路上「找不到彼此」的原因:NAT(網路位址轉換)。
家裡的 Wi-Fi 分享器、電信業者的 4G/5G 網路,都會把很多台裝置藏在同一個對外 IP 後面。手機拿到的通常是 192.168.x.x 這種內部位址,外面的人無法直接連進來。就像住在有管理員的大樓裡,訪客只知道大樓地址,不知道你住幾樓幾號,管理員也不會隨便放人進來。
WebRTC 依序嘗試三種方法,前一種不行才換下一種:
| 方式 | 怎麼連 | 什麼時候用得上 |
|---|---|---|
| ① 區網直連 | 兩支手機在同一個區網,直接互傳 | 同一個 Wi-Fi |
| ② STUN 打洞 | 先問 STUN 伺服器「我在外面看起來是哪個 IP 和埠」,雙方交換後同時往對方送封包,在 NAT 上「打出一個洞」直接連線 | 大多數家用網路 |
| ③ TURN 中繼 | 雙方都連到 TURN 伺服器,由它幫忙轉送所有影像 | 打洞失敗時的最後手段 |
直連/打洞成功:
攝影機手機 ───────────────► 觀看手機 (不經伺服器,不計費)
打洞失敗,改走 TURN:
攝影機手機 ──► TURN 伺服器 ──► 觀看手機 (伺服器轉送,依流量計費)
那什麼情況下「打洞」會失敗?
- 行動網路的電信級 NAT(CGNAT):手機用 4G/5G 時,通常和成千上萬個用戶共用同一個對外 IP,而且常是最難打洞的「對稱型 NAT」,每次連不同對象都換一個對外埠,STUN 查到的位址對方根本用不上。
- 兩端都在嚴格的 NAT 後面:一邊嚴格時,另一邊寬鬆還有機會;兩邊都嚴格就幾乎一定失敗。
- 公司、學校、飯店、公共 Wi-Fi 的防火牆:常擋掉 UDP 或只開放 80/443 埠,P2P 封包直接被丟掉。
這就是為什麼「在家用同一個 Wi-Fi 測試都正常,一出門用 4G 就連不上」,畫面上會停在「已配對但收不到影像」。配對訊息走的是 PeerJS 伺服器所以能通,但影像本身打不出洞。這時候 TURN 就是唯一的解法。它通常跑在 80/443 埠,還支援 TLS 加密的 turns:,連嚴格的防火牆也能穿過去。
對監視器這種「人在外面、攝影機在家」的使用情境來說,沒有 TURN,就等於只能在家裡看,所以它雖然不是必要,卻是讓這個專案真正實用的關鍵。
另外不用擔心隱私:WebRTC 的影音一律使用 DTLS-SRTP 端對端加密,TURN 伺服器只負責轉送加密過的封包,看不到畫面內容。
TURN 是要付費的嗎?
STUN 只幫忙「問路」,流量很小,Google、Cloudflare 都免費提供;但 TURN 要實際幫你轉送整條影像串流,吃的是伺服器頻寬,所以基本上都是依流量計費,只是多數服務商有每月免費額度。以本文使用的 Metered 為例(撰文時 2026 年 11 月的官方價格):
| 方案 | 月費 | 每月含 TURN 流量 | 超量 |
|---|---|---|---|
| Free | US$0 | 500 MB | 不超收,用完就停 |
| Growth | US$99 | 150 GB | US$0.40/GB |
| Business | US$199 | 500 GB | US$0.20/GB |
價格可能調整,請以 Metered 官方價格頁 為準。
500 MB 夠用多久?720p 影像大約 1.5~2 Mbps,換算每小時約 0.7~0.9 GB,所以免費額度大概只夠經中繼看 30~40 分鐘。好消息是:只有「走中繼」的流量才計費,在家用同一個 Wi-Fi、或網路條件好能直連時完全不會用到 TURN。觀看畫面右上角會顯示「直連」或「經 TURN 中繼」,可以留意一下。
如果偶爾出門看一下,免費方案就夠;若要長時間在外面看,就得考慮付費方案,或自己用 VPS 架 coturn(主機費另計)。
設定步驟
- 到 metered.ca 免費註冊,建立一個 TURN App,在 Dashboard 的 TURN Credentials 取得 iceServers 設定
- 在攝影機那支手機首頁打開「進階連線設定」,把 Metered 給的整段程式碼直接貼進去,網頁會自動拆出網址、帳號、密碼
- 按「測試登入」,看到「TURN 登入成功,可跨網路中繼」就代表沒問題,再按「儲存並重新整理」
Sample 程式碼
① Metered 後台給的 iceServers(整段貼進「TURN 網址」欄即可)。帳號密碼請換成你自己的;免費方案的主機是 standard.relay.metered.ca:
const myPeerConnection = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.relay.metered.ca:80" },
{
urls: "turn:standard.relay.metered.ca:80",
username: "你的帳號",
credential: "你的密碼",
},
{
urls: "turn:standard.relay.metered.ca:80?transport=tcp",
username: "你的帳號",
credential: "你的密碼",
},
{
urls: "turn:standard.relay.metered.ca:443",
username: "你的帳號",
credential: "你的密碼",
},
{
urls: "turns:standard.relay.metered.ca:443?transport=tcp",
username: "你的帳號",
credential: "你的密碼",
},
],
});
網頁會用正規表示式從這段文字裡抓出所有 turn:/turns: 開頭的網址,以及 username、credential 的值,自動填進三個欄位:
function parseTurnText(txt){
const urls = [...new Set(txt.match(/turns?:[^\s"'`,\]\}]+/g) || [])];
const user = (txt.match(/username["']?\s*[:=]\s*["'`]([^"'`]+)/) || [])[1] || '';
const pass = (txt.match(/credential["']?\s*[:=]\s*["'`]([^"'`]+)/) || [])[1] || '';
return { urls, user, pass };
}
② 程式裡怎麼把 TURN 加進 PeerJS:免費的 STUN 永遠放前面,有設定 TURN 時再附加上去,瀏覽器會自動優先嘗試直連,打不通才走中繼:
function iceConfig(){
const servers = [
{ urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] },
{ urls: 'stun:stun.cloudflare.com:3478' }
];
const t = JSON.parse(localStorage.getItem('turn') || 'null');
if (t && t.urls) servers.push({
urls: t.urls.split(/[\s,]+/).filter(Boolean),
username: t.user,
credential: t.pass
});
return { iceServers: servers };
}
// 攝影機端與觀看端都用同一份設定建立 Peer
const peer = new Peer('oldphonecam-v1-' + camId, { config: iceConfig() });
③ 測試 TURN 帳密是否正確:強制只用中繼(relay),拿得到中繼位址就代表登入成功:
const pc = new RTCPeerConnection({
iceServers: [{ urls, username, credential }],
iceTransportPolicy: 'relay' // 只允許 TURN
});
pc.onicecandidate = e => {
if (e.candidate && / typ relay /.test(e.candidate.candidate))
console.log('TURN 登入成功');
};
pc.onicecandidateerror = e => {
if ([401, 403, 441].includes(e.errorCode))
console.log('帳號或密碼錯誤');
};
pc.createDataChannel('turn-test');
pc.createOffer().then(o => pc.setLocalDescription(o));
貼心的地方是:攝影機設好 TURN 之後,QR Code 會一起帶上 TURN 設定,觀看端掃描時自動填入;已配對的觀看端也會在連線時自動同步,不用兩支手機各填一次。
⚠️ 小提醒:TURN 帳號密碼請不要放進公開的 GitHub repo。我一開始放了 turn.json 在 repo 裡,後來才意識到等於公開給所有人用,已經刪掉,改成只在手機上設定。同理,含 TURN 設定的 QR Code 也別隨便拍照分享。
六、開發筆記:幾個值得一提的技術細節
1. 移動偵測怎麼做?
不用任何 AI 模型,作法很陽春但很省電:每 0.5 秒把畫面縮成 64×48 的灰階小圖,跟上一張逐點比較,亮度差超過門檻的像素算「有變化」。變化比例超過靈敏度換算的門檻,就截一張 480 寬、蓋上時間戳的 JPEG 送出去。另外加了 15 秒冷卻,避免一個人走過去就連發十幾張通知。
// 灰階:R*0.3 + G*0.6 + B*0.1
g[j] = (px[i]*3 + px[i+1]*6 + px[i+2]) / 10;
// 靈敏度 10~95 換算成需要變化的像素比例
const need = 0.004 + (100 - sens) / 100 * 0.09;
if (ratio > need && now - last > 15000) onMotion();
2. 切換鏡頭不斷線
切換前後鏡頭時重新取得一條新的影像串流,再用 WebRTC 的 RTCRtpSender.replaceTrack() 把每個觀看者連線裡的軌道換掉,觀看端畫面會直接切過去,不需要重新連線。
3. 對講的小技巧
觀看端接聽時先回傳一條「靜音」的音訊串流,等使用者第一次按住對講、拿到麥克風權限後,再用 replaceTrack() 換成真正的麥克風。放開按鈕時只是把麥克風軌道 enabled = false,下次按住幾乎零延遲。
4. 真的去「登入」TURN 來測試
TURN 設定填錯是最常見的問題。測試按鈕的作法是建立一個 iceTransportPolicy: 'relay'(只允許中繼)的 RTCPeerConnection,如果能拿到 typ relay 的 candidate 就代表登入成功;若收到 401/403 錯誤碼則判定為帳號密碼錯誤,連不到則提示網址或網路問題。
5. 連線狀態看得到
觀看端會依序顯示「① 連接配對伺服器 → ② 尋找攝影機 → ③ 已配對 → ④ 接收影像」,並在連上後讀取 getStats() 判斷目前是直連還是走 TURN。連不上時也會告訴你卡在哪一步,而不是一直轉圈圈。
七、限制與注意事項
- iPhone 當攝影機時,網頁不能切到背景或鎖屏,要保持畫面開著(可以用「黑屏省電」)
- 兩邊網路都很嚴格(部分 4G、公司網路)時需要設定 TURN
- 錄影是存在觀看端手機,攝影機端不做 24 小時連續錄影
- 遠端手電筒要看手機與瀏覽器是否支援(Android Chrome 大多可以)
- 配對靠 PeerJS 公用伺服器,偶爾不穩時會自動重試
- 舊手機長時間插電請注意散熱與電池膨脹問題
八、結語
從構想到能用,整個專案就是一個 HTML 檔,沒有後端、沒有資料庫,靠瀏覽器內建的 WebRTC 就做出了「阿福管家」的大部分功能。拿來看家裡的寵物、門口、店面都很方便,舊手機也終於有了第二春。
如果你也有閒置的舊手機,歡迎到 GitHub 下載原始碼,部署一份自己的來試試看,有問題或建議也可以在下面留言 🙌

留言
張貼留言
請多指教