【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:

  1. 配對(訊號交換):使用 PeerJS 的免費公用伺服器,讓兩支手機互相「找到對方」。攝影機用固定代碼註冊,觀看端拿代碼去連。
  2. 穿透網路(STUN):透過 Google 與 Cloudflare 的 STUN 伺服器,讓手機知道自己在外網的位址,嘗試直接打洞連線。
  3. 影像直送(P2P):連上之後,影像和聲音是兩支手機之間點對點加密直送,不經過任何雲端儲存。
  4. 中繼備援(TURN):如果兩邊網路太嚴格(例如一支 Wi-Fi、一支 4G)打不通,就需要 TURN 伺服器幫忙轉送。

配對時觀看端會帶上 PIN,攝影機端比對錯誤會直接拒絕並斷線,所以就算有人知道代碼,沒有 PIN 也看不到畫面。

三、部署到 GitHub Pages(免費 https)

手機瀏覽器規定一定要 https 才能開相機,GitHub Pages 剛好免費又自帶 https:

  1. 在 GitHub 建一個 repo(例如 old-phone-cam),上傳 index.html
  2. 進入 Settings → Pages,Branch 選 main// (root),按 Save
  3. 約 1 分鐘後,網址就是 https://<你的帳號>.github.io/old-phone-cam/

四、使用方式

  1. 舊手機打開網址 → 點「這支當攝影機」→ 允許相機與麥克風 → 接上充電器、架好位置
  2. 你的手機用相機掃舊手機畫面上的 QR Code,就會自動連線
  3. 連過一次之後,首頁的「我的攝影機」會記住,之後一鍵就能看

五、跨網路連不上?設定 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 流量超量
FreeUS$0500 MB不超收,用完就停
GrowthUS$99150 GBUS$0.40/GB
BusinessUS$199500 GBUS$0.20/GB

價格可能調整,請以 Metered 官方價格頁 為準。

500 MB 夠用多久?720p 影像大約 1.5~2 Mbps,換算每小時約 0.7~0.9 GB,所以免費額度大概只夠經中繼看 30~40 分鐘。好消息是:只有「走中繼」的流量才計費,在家用同一個 Wi-Fi、或網路條件好能直連時完全不會用到 TURN。觀看畫面右上角會顯示「直連」或「經 TURN 中繼」,可以留意一下。

如果偶爾出門看一下,免費方案就夠;若要長時間在外面看,就得考慮付費方案,或自己用 VPS 架 coturn(主機費另計)。

設定步驟

  1. 到 metered.ca 免費註冊,建立一個 TURN App,在 Dashboard 的 TURN Credentials 取得 iceServers 設定
  2. 在攝影機那支手機首頁打開「進階連線設定」,把 Metered 給的整段程式碼直接貼進去,網頁會自動拆出網址、帳號、密碼
  3. 按「測試登入」,看到「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 下載原始碼,部署一份自己的來試試看,有問題或建議也可以在下面留言 🙌

留言

這個網誌中的熱門文章

Arduino IDE 1.6.5 + BH1750 + CD74HC4067 多工器

Arduino 模擬 Modbus Slave

鹵素燈泡 軌道燈 改用 LED 燈 完整轉換