AlexxIT

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars192
mentions4
0481216Oct 4Oct 18Nov 2
UTC days · repositories AlexxIT owns2 repositories

Package downloads

No package download history available yet.

Top repositories

AlexxIT’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside AlexxIT’s account where AlexxIT opened or reviewed pull requests in the past 365 days.

Who noticed AlexxIT

Everyone who starred or forked AlexxIT’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

4posts
Nov 30 posts
  1. A free pet cam from an old iPhone: no app, just WebRTC --- title: A free pet cam from an old iPhone: no app, just WebRTC published: true tags: homelab, homeassistant, webrtc, tutorial --- I had an old iPhone in a drawer. It is stuck on iOS 12.5.8 and cannot install current apps. The camera still works. I wanted to check on my pet while I am away. I tried the App Store first. I found no app that runs on iOS 12 and is free to use. Every one I tried had in-app purchases or severe limits, such as a single camera. So I wrote a web page instead. Safari opens it, and it publishes the camera over WebRTC to Home Assistant. There is nothing to install on the phone, no subscription and no limits. It also sends a push with a picture when something moves. Three things get in the way. iOS only gives camera access to HTTPS pages. Home Assistant's own HTTPS breaks plain-HTTP clients. And iOS 12 Safari would not open a websocket to my server at all. Contents - [Overview](#overview) - [Setup](#setup) - [Operate](#operate) - [Troubleshooting](#troubleshooting) - [What I would change](#what-i-would-change) --- Overview old iPhone (Safari, iOS 12) Home Assistant OS https://haos.home.arpa/local/cam.html | +-----------------------------+ | 1. HTTPS POST (SDP offer) | NGINX SSL proxy app :443 | +-----------------------------------> | /go2rtc/ -> go2rtc :1984 | | | everything else -> HA :80 | | 2. WebRTC video, UDP :8555 | | +-----------------------------------> | go2rtc (WebRTC Camera) | | | motion on the phone | package pet_cam.yaml | | 3. POST /api/webhook/<id> | webhook -> snapshot -> | +-----------------------------------> | notify mobile app | +-----------------------------+ - No app on the phone. Safari opens one web page. - The phone publishes its camera to go2rtc over WebRTC. - Signalling is one HTTPS POST, not a websocket. - Movement is detected on the phone. The server does no video processing. - Home Assistant sends a push with a snapshot to the Companion app on another phone. - It survives an HA restart and a full VM reboot. I tested both. - Watching while away needs a way into your home network. I use Tailscale with a subnet router and an app connector for *.home.arpa, described in an earlier article. The viewing device must trust the CA from Step 5. Remote viewing is not part of the steps below and I did not test it for this article. Before you start What I ran this on: - Home Assistant OS 2026.10 in a Proxmox VM, 2 cores, 4 GB RAM. - HACS installed. - A LAN DNS server I can add records to (Pi-hole). The router's DNS would also do. - A second phone with the Home Assistant Companion app, for the push. - A Linux or macOS machine with openssl. - The Terminal & SSH app installed in HA. Every file write below uses it. - Phone and HA on the same LAN. The phone must reach HA on TCP 443 and UDP 8555 (WebRTC media). The checks that use http://haos.home.arpa:1984 also need TCP 1984 from the laptop. If your network blocks it, use https://haos.home.arpa/go2rtc/api/streams with --cacert ca.crt after Step 6. Each step says where to run things: - laptop: your own machine. - HA UI: the Home Assistant web interface. - HA shell: the Terminal & SSH app. It has the ha command and mounts /config, /ssl and /share. Write files with cat > /path and paste, or with nano. - Pi-hole host: wherever your DNS server runs. Names used below: haos.home.arpa for the hostname, 192.168.1.50 for the HA address. Replace both with yours. home.arpa is the name reserved for home networks (RFC 8375). Why a proxy and not HA's own TLS HA can serve TLS itself with ssl_certificate in the http: config. I did not use it. Two reasons: - It switches the one listener to HTTPS-only. Anything on your network that talks plain HTTP to HA breaks. In my case that was another service reading HA entities. - On HA 2026.10 the http: block in configuration.yaml is ignored. The http integration imports it once into .storage/http, sets yaml_migration_done, and from then on raises a repair issue if the YAML is still there. So HTTP stays on port 80 and a proxy terminates TLS on 443. If you only need live video and no motion alerts, you can skip the proxy. See [Skip the proxy if you only need video](#skip-the-proxy-if-you-only-need-video). Gotcha: on HA under Supervisor the web port is 80, not 8123. See [Check the version facts first](#check-the-version-facts-first). Check .storage/http (server_port) or just curl http://<ha-ip>/ before you trust old tutorials. --- Setup Eight steps. Do them in order. Step 1: Make a local CA and a server certificate Alternative: use a certificate from a public CA and skip the CA, the phone trust step (Step 5) and manual renewal. I did not test it. See [Use a real certificate](#use-a-real-certificate). One CA you install on the phones. One server certificate signed by it. The certificate must list every name and address you will use. Run this on the laptop. The HAOS host shell has no openssl: mkdir -p ~/homelab-certs/haos && cd ~/homelab-certs/haos # CA, valid 10 years openssl genrsa -out ca.key 4096 openssl req -x509 -new -key ca.key -sha256 -days 3650 \ -subj "/CN=Homelab Local CA" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -out ca.crt # server key + request openssl genrsa -out haos.key 2048 openssl req -new -key haos.key -subj "/CN=haos.home.arpa" -out haos.csr # extensions: the SAN is what browsers check cat > ext.cnf <<'EOF' basicConstraints=CA:FALSE keyUsage=digitalSignature,keyEncipherment extendedKeyUsage=serverAuth subjectAltName=DNS:haos.home.arpa,IP:192.168.1.50 EOF # sign it, 825 days openssl x509 -req -in haos.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -days 825 -sha256 -extfile ext.cnf -out haos.crt openssl verify -CAfile ca.crt haos.crt chmod 600 ca.key haos.key Verify it worked: the last openssl verify prints haos.crt: OK. I used 825 days, the limit Apple documents for TLS server certificates, as a safe value. Private CAs may be exempt. I did not test a longer period. Keep ca.key off the server. You need it again only to renew. If you lose it, every device has to trust a new CA. Copy haos.crt and haos.key into HA's /ssl folder. The NGINX app in Step 2 reads certificates from there. Use the HA shell from the Terminal & SSH app: cat > /ssl/haos.crt, paste the contents of haos.crt from the laptop, end with Ctrl-D, and repeat for haos.key. Check each file: head -1 must print -----BEGIN ... and tail -1 must print -----END .... See [Check files after you write them](#check-files-after-you-write-them). On my install /ssl is /mnt/data/supervisor/ssl on the host. The later curl --cacert ca.crt examples assume you run them on the laptop in ~/homelab-certs/haos. Step 2: Install the NGINX proxy app and point it at the certificate In the HA UI: Settings, Apps (called Add-ons before 2025), install NGINX Home Assistant SSL proxy from the official store. Open its Configuration tab and set: domain: haos.home.arpa certfile: haos.crt keyfile: haos.key hsts: "" cloudflare: false use_ssl_backend: false client_max_body_size_megabytes: 1 customize: active: false default: nginx_proxy_default*.conf servers: nginx_proxy/*.conf real_ip_from: [] The last three options are the defaults. Enable "Start on boot", then start it. Gotcha: leave hsts empty. HSTS is per host name, not per port. If you set it, browsers force http://haos.home.arpa to HTTPS and anything still on plain HTTP is locked out. Gotcha: HTTPS works only with the host name, not the bare IP. A browser does not send a name (SNI) when you type an IP. The app's catch-all server rejects those connections (ssl_reject_handshake on in its nginx template). I tried adding a second server block for the IP. It is never chosen without SNI, so it does not help. Use the name. Gotcha: if you edit app options through the Supervisor API instead of the UI, the call needs every option, not only the ones you change. A partial body fails with Missing option 'cloudflare'. Step 3: Add the DNS record Add a record haos.home.arpa -> 192.168.1.50 to your LAN DNS. On the Pi-hole host, for Pi-hole v6: pihole-FTL --config dns.hosts '["192.168.1.50 haos.home.arpa"]' That command replaces the whole list. Read it first with pihole-FTL --config dns.hosts and include the existing entries in the new array. Pi-hole applies the change without a restart. The laptop and the old phone must use that DNS server. Mine get it from the router. If yours do not, set it by hand in the Wi-Fi settings. Verify it worked: getent hosts haos.home.arpa (or nslookup haos.home.arpa) on the laptop returns 192.168.1.50. Step 4: Tell HA to trust the proxy Open https://haos.home.arpa/ now. It returns 400: Bad Request. The HA log says: A request from a reverse proxy was received from 172.30.33.1, but your HTTP integration is not set-up for reverse proxies HA must be told which proxy addresses to believe. On 2026.10 this lives in .storage/http, not YAML. Use the HA shell. The Terminal & SSH app has nano and vi. 1. Stop core: ha core stop. 2. Copy the file first: cp /config/.storage/http /config/http.bak. Delete the copy when everything works. 3. Open /config/.storage/http. Inside the existing data -> stable object, add the two keys use_x_forwarded_for and trusted_proxies. Leave every other key alone. The file looks like this (yours may have more keys and other values): { "version": 2, "minor_version": 2, "key": "http", "data": { "stable": { "server_port": 80, "use_x_forwarded_for": true, "trusted_proxies": ["172.30.33.0/24"], "ip_ban_enabled": true }, "pending": null, "yaml_migration_done": true } } Keep the JSON valid. 4. While core is stopped, also create the folders later steps need: mkdir -p /config/www /config/packages. HA only serves /local/ for a www folder that existed when core started. 5. Start core: ha core start. On a running HA 2026.10 the file exists. If yours is missing or looks very different, you are on another version and this step may not apply. Use the address from your own log line (HA UI, Settings, System, Logs; the UI still works over plain HTTP at this point). 172.30.33.0/24 is the network of the proxy app on my install. The HAOS host itself is 172.30.32.1. They are different networks. Gotcha: edit the file with core stopped. HA manages .storage files itself, and an edit made while it runs can be overwritten. Verify it worked: curl --cacert ca.crt https://haos.home.arpa/ -o /dev/null -w "%{http_code}\n" # 200 curl http://haos.home.arpa/ -o /dev/null -w "%{http_code}\n" # 200 Both must answer. Step 5: Trust the CA on the phone Skip this step if you use a [real certificate](#use-a-real-certificate). Do this on the iOS 12 phone: 1. Get ca.crt onto the phone. In the HA shell: cp or paste it to /config/www/ca.crt. On the phone, open http://haos.home.arpa/local/ca.crt in Safari (plain HTTP on purpose, since HTTPS is not trusted yet). Install the profile (Settings, General, Profile). Delete /config/www/ca.crt afterwards. It is a public certificate, but there is no reason to leave it served. 2. Settings, General, About, Certificate Trust Settings. Switch the CA on. Installing alone is not enough. 3. Open https://haos.home.arpa/ in Safari. No warning and a lock means it worked. Gotcha: tapping through a certificate warning lets the page load. It does not make the phone trust the host. Install and enable the CA. Step 6: go2rtc, the stream and the proxy path This step uses the WebRTC Camera integration. A visible go2rtc app may be simpler. See [Try the go2rtc app instead of the integration](#try-the-go2rtc-app-instead-of-the-integration). go2rtc is the part that receives the WebRTC video. HA has its own copy for its camera features. That one is internal. You want a separate one on port 1984 with a stream you define. Install the WebRTC Camera integration: 1. HA UI, HACS, menu, Custom repositories. Add https://github.com/AlexxIT/WebRTC as an Integration. 2. Download it from HACS and restart HA. 3. Settings, Devices & services, Add integration, WebRTC Camera. It has no options. On my install no further restart was needed for the next check. 4. In the HA shell: ls /config/go2rtc*. A file like go2rtc-1.9.9 should be there, and curl http://haos.home.arpa:1984/api should answer with JSON. If the binary does not appear, nothing below will work. I have no tested fallback. On my install a binary /config/go2rtc-1.9.9 appeared at that moment (its file time matches the time the config entry was created). It runs after every HA start and listens on 1984 (API), 8554 (RTSP) and 8555 (WebRTC). I did not read the integration's code to confirm that it launches the binary. The restart tests below are my evidence. HA's built-in go2rtc is a different process on other ports. Ignore it. Declare the stream in /config/go2rtc.yaml (the HA config folder): streams: old_phone_pet_cam: The value is empty on purpose. It makes the stream exist so something can publish into it. Restart core with ha core restart so go2rtc rereads the file. Verify it worked: curl http://haos.home.arpa:1984/api/streams # {"old_phone_pet_cam":{"producers":null,"consumers":null}} Gotcha: without the stream in go2rtc.yaml, publishing fails with stream not found. The viewer error streams: unknown error only means the stream exists and nobody is publishing. Gotcha: do not start a second go2rtc, for example as a Docker container. It loses the race for ports 1984 and 8554 to the one HA starts and logs address already in use, while the first one keeps answering. The setup then looks fine and the second instance does nothing. go2rtc speaks plain HTTP. The phone's page is HTTPS, so route go2rtc through the NGINX app. With customize.active on, the app includes /share/<customize.default> inside its HTTPS server block, before its own location /. I confirmed this in the app's nginx template. Create /share/nginx_proxy_default.conf (the Samba share share; host path /mnt/data/supervisor/share/ on my VM): # go2rtc (host process, plain HTTP/WS on :1984) behind the app's TLS. # homeassistant.local.hass.io = the HAOS host (172.30.32.1), same name the app uses to reach HA. location /go2rtc/ { proxy_pass http://homeassistant.local.hass.io:1984/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $http_host; proxy_read_timeout 3600s; } Set customize.active: true in the app options (same options as Step 2) and restart the app. If /go2rtc/ then returns 502, the name does not resolve in your app version. Replace it in the file with the HAOS host address (172.30.32.1 on mine). homeassistant.local.hass.io is the name the app itself uses to reach HA. Inside the app it resolves to the HAOS host (172.30.32.1 on mine), where go2rtc listens on all interfaces. $connection_upgrade is defined by the app's own nginx config, so the include can use it. Verify it worked: curl --cacert ca.crt https://haos.home.arpa/go2rtc/api/streams Gotcha: go2rtc on 1984 and 8554 has no login. Anyone on your LAN can see the stream and the API. Do not forward those ports. Gotcha: wss://haos.home.arpa:1984 will not work. go2rtc has no TLS on that port. Everything goes through /go2rtc/ on 443. Step 7: The camera page Generate one webhook id on the laptop. You use the same value three times: in cam.html, in pet_cam.yaml (Step 8) and in the test curl (Step 8): python3 -c 'import secrets; print(secrets.token_hex(16))' Put the file below at /config/www/cam.html (use the HA shell). The www folder was created in Step 4. HA then serves the file at https://haos.home.arpa/local/cam.html. Replace <WEBHOOK_ID> with your id: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>iOS 12 Pet Cam Streamer</title> <style> body { font-family: -apple-system, sans-serif; background: #000; color: #fff; text-align: center; margin: 0; padding: 20px; } video { width: 100%; max-width: 400px; background: #222; border-radius: 8px; transform: scaleX(-1); } .status { margin: 15px; font-size: 14px; color: #aaa; font-weight: bold; } </style> </head> <body> <h3>iOS 12 Camera Broadcaster</h3> <!-- These elements MUST exist before the script can find them --> <video id="localVideo" autoplay playsinline muted></video> <div id="status" class="status">Loading configuration...</div> <div id="motion" class="status">Motion: waiting</div> <script> var STREAM_NAME = "old_phone_pet_cam"; // WHIP publish: SDP offer POSTed over plain HTTPS (iOS 12 Safari cannot open // websockets to this server). Path is proxied to go2rtc by the HA nginx app. var WHIP_URL = "/go2rtc/api/webrtc?dst=" + STREAM_NAME; var pc = null; function setStatus(text, color) { var el = document.getElementById("status"); el.innerText = text; if (color) { el.style.color = color; } } function waitForIce(peer, ms) { return new Promise(function(resolve) { if (peer.iceGatheringState === "complete") { resolve(); return; } var done = function() { peer.onicegatheringstatechange = null; resolve(); }; peer.onicegatheringstatechange = function() { if (peer.iceGatheringState === "complete") { done(); } }; setTimeout(done, ms); }); } function post(url, body) { return new Promise(function(resolve, reject) { var x = new XMLHttpRequest(); x.open("POST", url); x.setRequestHeader("Content-Type", "application/sdp"); x.onload = function() { if (x.status >= 200 && x.status < 300) { resolve(x.responseText); } else { reject(new Error("WHIP HTTP " + x.status)); } }; x.onerror = function() { reject(new Error("WHIP network error")); }; x.send(body); }); } var localStream = null; var retryTimer = null; // Any failure (go2rtc/HA restarting, network drop) tears the peer down and retries. function scheduleRetry(reason) { setStatus(reason + " - retrying in 5s...", "#f44336"); if (pc) { try { pc.close(); } catch (e) {} pc = null; } if (retryTimer) { return; } retryTimer = setTimeout(function() { retryTimer = null; connect(); }, 5000); } function connect() { setStatus("Connecting to go2rtc engine..."); var peer = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }); pc = peer; localStream.getTracks().forEach(function(t) { peer.addTrack(t, localStream); }); peer.oniceconnectionstatechange = function() { if (pc !== peer) { return; } var st = peer.iceConnectionState; if (st === "connected" || st === "completed") { setStatus("LIVE: BROADCASTING TO HOME ASSISTANT", "#4caf50"); } else if (st === "failed" || st === "disconnected" || st === "closed") { scheduleRetry("ICE " + st); } }; peer.createOffer() .then(function(offer) { return peer.setLocalDescription(offer); }) .then(function() { setStatus("Gathering ICE candidates..."); return waitForIce(peer, 3000); }) .then(function() { setStatus("Sending offer to go2rtc..."); return post(WHIP_URL, peer.localDescription.sdp); }) .then(function(answerSdp) { setStatus("Got answer, connecting media..."); return peer.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: answerSdp })); }) .catch(function(err) { if (pc === peer) { scheduleRetry("Error: " + (err && err.message ? err.message : err)); } }); } function startStream() { var video = document.getElementById("localVideo"); setStatus("Requesting device camera access..."); var constraints = { audio: false, video: { facingMode: "environment", width: 640, height: 480, frameRate: 15 } }; navigator.mediaDevices.getUserMedia(constraints) .then(function(stream) { localStream = stream; video.srcObject = stream; connect(); startMotion(); }) .catch(function(err) { setStatus("Camera error: " + (err && err.message ? err.message : err), "#f44336"); }); } // ---- Motion detection (runs on the phone, no server load) ---- // Frame-diff on a tiny grayscale canvas. On motion, POST the local-only HA // webhook; the HA automation sends the push + snapshot. var MOTION_WEBHOOK = "/api/webhook/<WEBHOOK_ID>"; var MOTION = { W: 64, H: 48, PIXEL: 25, FRACTION: 0.02, INTERVAL: 500, COOLDOWN: 60000, WARMUP: 8000 }; var motionPrev = null, motionLast = 0, motionStart = 0, motionRetryAt = 0; function setMotion(text, color) { var el = document.getElementById("motion"); el.innerText = text; el.style.color = color || "#aaa"; } function motionTick() { var video = document.getElementById("localVideo"); if (video.readyState < 2) { return; } var cv = document.getElementById("motionCanvas"); var ctx = cv.getContext("2d"); ctx.drawImage(video, 0, 0, MOTION.W, MOTION.H); var d = ctx.getImageData(0, 0, MOTION.W, MOTION.H).data; var n = MOTION.W * MOTION.H, gray = new Array(n), changed = 0, i; for (i = 0; i < n; i++) { gray[i] = (d[i * 4] + d[i * 4 + 1] + d[i * 4 + 2]) / 3; if (motionPrev && Math.abs(gray[i] - motionPrev[i]) > MOTION.PIXEL) { changed++; } } var frac = changed / n; motionPrev = gray; var now = new Date().getTime(); if (now - motionStart < MOTION.WARMUP) { setMotion("Motion: warming up"); return; } if (frac > MOTION.FRACTION) { if (now - motionLast > MOTION.COOLDOWN && now > motionRetryAt) { sendMotion(frac, now); } else { setMotion("Motion: detected (waiting)", "#ff9800"); } } else if (now - motionLast > MOTION.COOLDOWN) { setMotion("Motion: none"); } } // Cooldown starts only when HA accepted the webhook; on failure retry in 10 s. function sendMotion(frac, now) { var x = new XMLHttpRequest(); x.open("POST", MOTION_WEBHOOK); x.setRequestHeader("Content-Type", "application/json"); x.onload = function() { if (x.status >= 200 && x.status < 300) { motionLast = new Date().getTime(); setMotion("Motion: ALERT SENT " + new Date().toLocaleTimeString(), "#ff9800"); } else { motionRetryAt = new Date().getTime() + 10000; setMotion("Motion: ALERT FAILED (HTTP " + x.status + ")", "#f44336"); } }; x.onerror = function() { motionRetryAt = new Date().getTime() + 10000; setMotion("Motion: ALERT FAILED (network)", "#f44336"); }; motionRetryAt = now + 10000; // no duplicate send while this one is in flight x.send(JSON.stringify({ changed_pct: Math.round(frac * 100) })); } function startMotion() { var cv = document.createElement("canvas"); cv.id = "motionCanvas"; cv.width = MOTION.W; cv.height = MOTION.H; cv.style.display = "none"; document.body.appendChild(cv); motionStart = new Date().getTime(); setInterval(motionTick, MOTION.INTERVAL); } setTimeout(startStream, 500); </script> </body> </html> What it does: - No framework, var and XHR only. iOS 12 Safari does not run modern syntax or bundles. - setTimeout(startStream, 500). An early version crashed with Cannot set properties of null on the phone. Adding the delay fixed it. I kept it. - WHIP instead of a websocket. WHIP is a way to publish WebRTC with one HTTP POST. The page POSTs its SDP offer (the text that describes the media session) to /go2rtc/api/webrtc?dst=old_phone_pet_cam and reads the SDP answer from the response. - It retries every 5 seconds on any failure. After an HA restart it publishes again without anyone touching the phone. Gotcha: my first version used go2rtc's websocket API (/api/ws). Start with WHIP instead. See [Start with WHIP, not a websocket](#start-with-whip-not-a-websocket). On iOS 12.5.8 Safari it failed every time with close code 1006. It failed through nginx and against go2rtc's own TLS port. The page itself loaded with no certificate warning, so trust was not the cause. A websocket opened to Home Assistant's own /api/websocket failed the same way. I did not find the cause, so I used WHIP. A status line per stage found the failure quickly. See [Build a diagnostic page first](#build-a-diagnostic-page-first). Desktop Chrome worked with the websocket through nginx. I do not know why iOS 12 fails. Gotcha: dst= publishes, src= views. With src= go2rtc treats your offer as a viewer and answers stream not found or unknown error. Note: ICE candidates are the addresses the two sides can use to reach each other. The page sends them inside the offer (non-trickle) and waits up to 3 seconds for gathering to finish before it posts. Note: the page uses Google's public STUN server (stun.l.google.com), a service that tells a client its public address. It is the only outside address involved. On one LAN the local candidates should be enough. I did not test removing it. Gotcha: HA serves /local/ with long cache headers. After you edit cam.html, open it with a new query string (cam.html?v=2), or Safari keeps running the old copy. Gotcha: cam.html contains the webhook id, and /local/ has no login. Anyone on your LAN who opens the page can read it. The webhook is local_only, so the exposure is limited to your LAN. Open the page on the old phone and allow the camera. The status line goes through Connecting, Gathering ICE candidates, Sending offer, Got answer and ends on LIVE: BROADCASTING TO HOME ASSISTANT. Verify it worked: the producers list now has an entry whose user_agent contains iPhone and iPhone OS 12: curl http://haos.home.arpa:1984/api/streams To watch the stream in a desktop browser: https://haos.home.arpa/go2rtc/stream.html?src=old_phone_pet_cam The same stream is available as RTSP at rtsp://192.168.1.50:8554/old_phone_pet_cam. You can add it to HA with the Generic Camera integration if you want an entity. Step 8: Motion alerts (optional) Detection runs in the page. Every 500 ms it draws the video into a 64 by 48 canvas and compares it with the previous frame. If more than 2 percent of the pixels changed by more than 25 gray levels, it POSTs a webhook to HA. The first 8 seconds are ignored. After an alert it waits 60 seconds. The cooldown starts only when HA answers with a 2xx. On a failed POST the page shows ALERT FAILED and tries again after 10 seconds. The constants are at the top of the motion block in cam.html: PIXEL, FRACTION, COOLDOWN, WARMUP. On the HA side you need three things. First, packages must be enabled in configuration.yaml. Merge this into your existing homeassistant: key if you have one. A second homeassistant: key breaks the file: homeassistant: packages: !include_dir_named packages Second, find your notify service name in Developer tools, Actions, by searching notify.mobile_app. It is notify.mobile_app_<device>, built from the device name in the Companion app. Put yours into the package below. Third, create /config/packages/pet_cam.yaml (the folder was created in Step 4). Use your webhook id from Step 7. The package's automation: list merges with an existing automation: !include automations.yaml: # Pet cam: motion webhook -> snapshot -> push shell_command: # Remove the old frame first so a failed fetch never sends a stale picture pet_cam_snapshot_clear: "rm -f /config/www/pet_cam_last.jpg" # Still frame from go2rtc (runs inside the HA container, host network) pet_cam_snapshot: "curl -sf -o /config/www/pet_cam_last.jpg 'http://localhost:1984/api/frame.jpeg?src=old_phone_pet_cam'" automation: - alias: "Pet cam: motion notification" id: pet_cam_motion_notify mode: single max_exceeded: silent triggers: - trigger: webhook webhook_id: "<WEBHOOK_ID>" allowed_methods: [POST] local_only: true actions: - action: shell_command.pet_cam_snapshot_clear - action: shell_command.pet_cam_snapshot continue_on_error: true # no publisher -> push goes out without an image - action: notify.mobile_app_<device> data: title: "Pet cam" message: "Movement detected" data: tag: pet_cam_motion image: "/local/pet_cam_last.jpg?t={{ now().timestamp() | int }}" Replace <device> with your device name first. The configuration check does not validate a notify service. A wrong name fails when the automation runs (see Settings, System, Logs). Then run Developer tools, YAML, Check configuration, and restart HA core with ha core restart. I restarted. I did not test whether a reload is enough. Verify it worked: send the webhook by hand from the laptop: curl -X POST https://haos.home.arpa/api/webhook/<WEBHOOK_ID> --cacert ca.crt The Companion app shows a push with a picture. Gotcha: HA answers 200 to a webhook POST even when the id does not exist. A 200 proves nothing. Check the push, or check that /config/www/pet_cam_last.jpg has a new timestamp. Gotcha: local_only: true is checked against the real client address. That only works because of Step 4 (use_x_forwarded_for). Without it every request looks like it comes from the proxy. Gotcha: the snapshot is saved under /config/www/. /local/ serves that folder with no login to anyone on your LAN who knows the URL. The webhook id is a secret for the same reason. Keep it out of git. Gotcha: a failed snapshot used to send the previous picture with a fresh timestamp. The package now deletes the old file first and uses continue_on_error. When there is no publisher, the push goes out without an image. Gotcha: the snapshot step runs curl inside the HA container and asks go2rtc for a JPEG at /api/frame.jpeg. go2rtc needs ffmpeg for that. Both are in the HA container on HAOS. On a different install, check docker exec homeassistant which curl ffmpeg. If either is missing, the snapshot fails and the push goes out without a picture. To tune it: too many false alerts means raise FRACTION or PIXEL. Missed movement means lower them. Expect lighting changes to trigger it. --- Operate Restarts I tested two things. - ha core restart: go2rtc came back, and the phone page reconnected on its own. - Full VM reboot (qm reboot): HA answered again after about 70 seconds. go2rtc was up. The iPhone was publishing again with no manual step. Each was one run. Keep the phone plugged in and the screen awake. The page does not prevent the phone from sleeping. Renewing the certificate Before the server certificate expires: 1. Run the signing command from Step 1 again with the same ext.cnf and a new haos.csr. The CA stays the same, so no device needs to be touched. 2. Copy the new haos.crt and haos.key into /ssl. 3. Restart the NGINX app. --- Troubleshooting | Symptom | Cause | Fix | |---|---|---| | https://haos.home.arpa/ returns 400: Bad Request | HA does not trust the proxy | Step 4: use_x_forwarded_for and trusted_proxies in .storage/http, core stopped | | http: block in configuration.yaml has no effect | HA 2026.10 imports it once and ignores it after | Edit .storage/http with core stopped | | HTTPS by IP fails with unrecognized name | Browsers send no SNI for an IP; the catch-all server rejects it | Use the host name | | http://haos.home.arpa suddenly redirects to HTTPS | HSTS set on the proxy app | Set hsts to an empty string, clear HSTS in the browser | | Apps API call fails with Missing option 'cloudflare' | The options call needs all fields | Send every option, or use the UI | | Camera page does nothing on iOS | Page is not HTTPS, or CA not trusted | Steps 1 to 5 | | Status stays on Connecting to go2rtc engine | Websocket code on iOS 12 | Use the WHIP version of the page (Step 7) | | stream not found | Stream is not in go2rtc.yaml, or src= used instead of dst= | Step 6, and use dst= to publish | | Viewer shows streams: unknown error | Nobody is publishing | Load the page on the phone | | go2rtc log says address already in use | A second go2rtc instance | Remove it; keep the one HA starts | | Page edits have no effect on the phone | /local/ is cached | Open cam.html?v=N with a new N | | Webhook returns 200 but no push | Wrong webhook id, or wrong notify service | Compare ids, check the service name in Developer tools | | Push arrives without a picture | Snapshot fetch failed, usually no publisher | Check https://haos.home.arpa/go2rtc/api/streams for a producer | | Too many motion alerts | Thresholds too low | Raise FRACTION or PIXEL | --- What I would change These are the things I would do differently now. Items marked "not tested" are ideas, not results. Use a real certificate What I did: made a local CA, signed a server certificate, installed the CA on the phone, and put the expiry in a calendar. What I would try: get a certificate from a public CA with a DNS challenge, for example Let's Encrypt through the DuckDNS app. The NGINX app reads fullchain.pem and privkey.pem from /ssl. The phone then needs no CA, and renewal is automatic. That removes the CA part of Step 1 and all of Step 5. Cost: you need a public name (DuckDNS or your own domain) whose record points at your LAN address. That puts your LAN address in public DNS. iOS 12 should accept Let's Encrypt's current root. I did not check that on the phone. Status: not tested. Skip the proxy if you only need video go2rtc can serve HTTPS itself (tls_listen, tls_cert and tls_key in its api section) and can serve static files (static_dir; check the docs for your version). If it serves cam.html too, the page and the WHIP POST share one origin. That removes the NGINX app, the .storage/http edit and the /go2rtc/ proxy. The limit: the motion webhook cannot work. An HTTPS page cannot call a plain-HTTP Home Assistant, because the browser blocks it as mixed content. So this only fits live video without alerts. Status: not tested. I enabled tls_listen once for a websocket test, which failed on iOS 12. I did not try WHIP against it. Try the go2rtc app instead of the integration I used the WebRTC Camera integration. It put a go2rtc binary in /config that I cannot see in the UI, and I could only confirm that it starts with HA by restarting. A go2rtc app from the community store (AlexxIT's hassio-addons repository) would run go2rtc as a visible app, with its config in the UI and a start-on-boot switch. Status: not tested. I do not know its ports or config format. Start with WHIP, not a websocket go2rtc's examples use /api/ws. On iOS 12.5.8 it never connected, and most of my time went into that. A POST of the SDP offer to /api/webrtc?dst=<stream> worked on every device I tried. Use it from the first version. dst= publishes, src= views. Build a diagnostic page first A phone gives you no console. When the page did nothing, I guessed: certificate trust, then HTTP/2. Both guesses were wrong. What found the problem was a status line per stage, plus two probe websockets (one to Home Assistant, one to go2rtc) that printed OPEN or ERROR with the close code. Three page loads showed that websockets failed in general. Put a status line per stage into the first version. Check the version facts first I assumed port 8123 and that the http: YAML works. On HA 2026.10 the port was 80 and the YAML is ignored after the first import. Reading .storage/http and running curl against the port takes a minute. It would have saved two wrong turns. Check files after you write them A deploy command with an unset shell variable once wrote empty files over live ones. Print the size (wc -c) or the first line after every write, and keep a copy of the old file until the new one works.

    1 reaction · 1 comment
  2. Reddit · · r/webos · u/Solid_Milk3104 · AlexxIT/go2rtc

    Oscar Cam - A free, LAN-only IP camera grid for rooted LG webOS TVs A free, LAN-only IP camera grid for rooted LG webOS TVs. No account, no cloud, no telemetry. Camera credentials stay on the TV. I built this for my own use but thought others may be interested in this solution. Now I can pull up my cameras from either one of my LG C3's. What you need - A rooted LG TV running webOS 5 or later, with the Homebrew Channel installed. - A machine on your home network to run go2rtc (a NAS, a Raspberry Pi, an old PC, or a mini-PC — as a plain binary or a Docker container). go2rtc does NOT run on the TV itself, and the TV app cannot host it: Oscar Cam is only the viewer, and go2rtc is the server that restreams your cameras into a form the TV browser can play. That machine needs to be on while you are watching. If you do not have a server of any kind, a Raspberry Pi or any old computer will do — it does not have to be a Synology. - ONVIF cameras, or any camera go2rtc can already stream. Cameras that natively serve MJPEG or HLS work without go2rtc, but most IP cameras are RTSP-only, and raw RTSP does not play in the TV browser — that is why go2rtc is in the stack. Already running Agent DVR (or Frigate)? Its MJPEG endpoint (Agent DVR: http://:8090/video.mjpg?oid=N) can be added as an MJPEG camera in Oscar Cam — no go2rtc needed for those cameras. Agent DVR is also available as a Home Assistant add-on. Compatible by design; not yet tested on the C3. Testers wanted Oscar Cam is proven on one TV (an LG C3) and one set of cameras. If you try it on a different TV or camera, please report back — good or bad — using the test report template (.github/ISSUE_TEMPLATE/test-report.md): your TV model and webOS version, your go2rtc setup, your camera makes and models, what you did, and what happened. The Diagnostics block in Settings (run the connection tests and copy what each line says) is the most useful thing you can include when something does not play. Burn-in protection (OLED) Burn-in protection (OLED) Grids left up for hours can burn into an OLED panel. Oscar Cam has three protections, all on by default and adjustable in Settings: Any remote key or pointer movement wakes everything back up. These help, but they are not a substitute for turning the grid off when you are done watching. Hide the interface after 10 seconds without remote input. Shift the whole picture a few pixels every 5 minutes (this also moves the cameras' on-screen timestamps). Dim after 15 minutes idle, blank after 30. ONVIF cameras on your network and add them with a tap. Arrange them in Edit layout, including a 4x4 grid (16 tiles is untested — 9 simultaneous substreams is the proven cap). Install Install Oscar Cam, open it, and set your go2rtc server URL in Settings (for example 192.168.0.58). Then add cameras by hand in Cameras, or use Discover cameras to find Oscar Cam In the Homebrew Channel, add this repository URL: raw.githubusercontent.com

    3 points · 1 comment
  3. Tapo C220をRTSPとONVIFでどこまで操作できるか実機で調べた TP-Link の首振りカメラ Tapo C220 は、Tapo アプリのほかに RTSP と ONVIF でも操作できる。 ただ、アプリでできることのうち、どこまでが標準の経路で届くのかは、仕様表を見ても分からない。 2026 年 10 月 9 日から 24 日にかけて、手元の 1 台(ハードウェア 2.0、ファームウェア 1.3.1 Build 260515)を、アプリを介さず Python と ffmpeg から操作した。 確かめた操作は、映像と音声の受信、録画、首振り、検知イベントの受信、スピーカーからの再生、microSD の録画の取り出し、設定の読み書きである。 調べるために自作したライブラリと WebUI で動かしたが、ここでは実機で分かったことだけを書く。 ライブラリは skrtk98/tapo-c220 で、実測の値や生の応答を含む詳しい結果は同じリポジトリの Wiki で公開している。 最初に分かったこと 確認したこと 観察した結果 使う側への影響 映像と音声 RTSP で 2560×1440 と 640×360 の H.264、8kHz の G.711 A-law が取れる。同時接続は 3 本まで 録画、ライブ配信、静止画の切り出しを同時に動かすと、すぐ上限に届く RTSP の時刻 RTCP の時刻が約 0.5 秒ごとに交互に約 ±0.38 秒ずれる そのまま使うと、WebRTC でも MP4 でも映像が毎秒止まる。時刻を付け直す必要がある 首振り ONVIF の PTZ で、絶対、相対、連続の移動とプリセットが使える。ズームは無い pan の正の向きが、速度と位置で逆になる 検知イベント ONVIF の PullPoint は使えない。カメラから通知を送らせる方式なら届く 受け取る側が、カメラからの着信を待ち受ける必要がある スピーカー RTSP にも ONVIF にも音声を送る経路が無い Tapo 独自の非公式な経路を使うしかない microSD の録画 ONVIF の録画検索も RTSP での再生も無い 同じく独自の経路が要り、取り出しは等速でしか進まない 設定と警報 設定は独自の API で読み書きできる。警報を今すぐ鳴らす確かな方法は見つからなかった 警報の代わりに、サイレンの音をスピーカーへ送る 標準の経路で足りるのは、映像と音声の受信、首振り、検知イベントの受信までである。 スピーカー、録画の取り出し、設定は、非公式のプロトコルに頼ることになる。 経路は標準の2つと独自の2つに分かれる カメラで開いていたポートは 443、554、2020、8800 の 4 つだった。 それぞれの役割と認証は次のとおりである。 分類 経路 ポート 通信の形 認証 使いみち 標準 RTSP 554 RTP を流し続ける カメラアカウント(Digest) 映像と音声、録画、静止画 標準 ONVIF 2020 SOAP を1往復ずつ カメラアカウント(WS-Security) 首振り、検知イベント Tapo 独自 制御 API 443 暗号化した JSON を1往復ずつ Tapo アカウント 設定、録画の一覧、検知の記録 Tapo 独自 ストリーム 8800 1本の接続に流し続ける Tapo アカウント スピーカー、録画の取り出し 認証に使うアカウントが経路によって違う。 カメラアカウントは、Tapo アプリの「高度な設定」から「カメラのアカウント」で作る、RTSP と ONVIF 用のユーザ名とパスワードである。 独自の 2 経路は、カメラアカウントでは認証が通らず、Tapo アプリへのログインに使うクラウドのアカウントのパスワードを求める。 独自の経路を使うプログラムは、クラウドのパスワードを手元に置くことになる。 RTSP で受け取れる映像と音声 URL は rtsp://ユーザ:パスワード@ホスト:554/stream1 の形で、認証は Digest である。 stream1 は H.264 High の 2560×1440、stream2 は H.264 High の 640×360 で、音声はどちらも G.711 A-law の 8kHz モノラルだった。 ONVIF は音声を 16kHz と答えるが、SDP も実際の中身も 8kHz である。 ONVIF のプロファイルには、もう 1 つ stream8(MJPEG 640×360)がある。 しかし 5 秒間に届いた約 300 枚のうち、違う画像は 5 枚だけで、残りは同じ画像の送り直しだった(約 25Mbps)。 Chromium ではこの JPEG が灰色にしか描かれなかったので、使っていない。 同時に受けられるのは 3 接続までである。 4 本目は接続の時点で拒まれる。 同じストリームを 3 本開いても、別々のストリームを開いても、上限は同じだった。 音声だけが欲しいときも、映像のトラックごと受け取る必要がある。 ffmpeg の -allowed_media_types audio で音声のトラックだけを SETUP すると、何も届かないまま時間切れになった。 RTCP の時刻のずれで映像が毎秒止まる 受け取った映像と音声をそのまま使うと、RTCP の時刻のずれで再生が乱れる。 RTP のタイムスタンプ自体は正しく進み、パケットも滑らかに届いていた。 30fps で、到着の間隔は中央値 33ms、最大でも 80ms 弱、ping は 2ms 前後で損失は無かった。 ところが、約 0.5 秒ごとに届く RTCP の Sender Report の NTP 時刻が、交互に約 ±0.38 秒ずれていた。 libav(ffmpeg のライブラリ)は Sender Report に合わせて pts を補正するので、pts が約 0.35 秒戻っては約 0.42 秒飛ぶ動きを毎秒繰り返す。 これをそのまま WebRTC でブラウザへ送ると、Chromium は 20 秒の間に映像を 20 回(合計 7 秒)止め、音声の約 43% を補間で埋めた。 ffmpeg で保存した MP4 も、映像が毎秒途切れた。 ffmpeg で保存するときは、届いた時点の時計で時刻を付け直すと、乱れが大きく減る。 付け直す前の MP4 には 16 秒で 24 回、最大 420ms の乱れが出たが、付け直したあとは 12 秒で 1〜5 回、最大 73〜110ms になった。 ffmpeg -rtsp_transport tcp -use_wallclock_as_timestamps 1 \ -i "rtsp://ユーザ:パスワード@ホスト:554/stream1" \ -c:v copy -c:a aac -b:a 64k -f mp4 out.mp4 WebRTC へ載せ替えるときは、パケットのタイムスタンプを自分で付け直した。 音声は受け取った標本の数から、映像は時刻の飛びを通常のフレーム間隔に置き換えて求めた。 対策の後は、止まりが 0 回、補間が 0%、30fps で再生できた。 なお、カメラの H.264 と G.711 A-law は WebRTC の標準のコーデックなので、変換せずに RTP へ載せ替えられる。 カメラは High プロファイルで送ってくるが、ブラウザとは Baseline(42001f)で取り決めたまま、Chromium では復号できた。 ほかのブラウザでは確かめていない。 カメラへ音声を送る口は RTSP に無い ONVIF には、RTSP でカメラへ音声を送り返す拡張(backchannel)が定められている。 Require: www.onvif.org/ver20/backchannel を付けて DESCRIBE しても、C220 の SDP は映像と音声(受信)の 2 本だけだった。 ONVIF 側でも、GetAudioOutputs と GetAudioDecoderConfigurations は空を返す。 deviceIO の能力は AudioOutputs="1" と答えるが、使える出力は無かった。 ONVIF の首振りは向きの符号に注意する ONVIF の入口は xn--zck4a3c で、認証は WS-Security の UsernameToken(PasswordDigest)だけだった。 サービスは device、media(ver10)、events、analytics、imaging、ptz、deviceIO で、版はどれも 2.20 である。 プロファイルは stream1、stream2、stream8 に対応する 3 つで、どれにも PTZ の設定が付く。 GetSnapshotUri と GotoHomePosition は ter:ActionNotSupported を返す。 静止画は RTSP から 1 枚切り出して代えた。 位置、移動量、速度は、どれも -1 から 1 に正規化した値で扱う。 ズームは無く、プリセットは 8 件まで保存できた。 pan の正の向きが、速度と位置で逆になる ContinuousMove で正の pan の速度を送ると、画面は右へ動いた。 一方、位置を扱う GetStatus、AbsoluteMove、RelativeMove では、正の pan は左を向く。 tilt は、速度でも位置でも正が上だった。 据え置きで使ったこの個体での実測なので、ほかの個体やファームウェアでも同じかは分からない。 天井に逆さに取り付け、Tapo アプリで画像の上下反転をオンにすると、首振りは上下左右とも逆になる。 実機では、「上へ」の指示で部屋の下を、「左へ」の指示で部屋の右を向いた。 逆さに取り付けて部屋から見た向きで操作したい場合は、送る側で向きを入れ替える必要がある。 連続移動の Timeout は整数の秒で送る ContinuousMove の Timeout に PT0.5S のような小数の秒を送ると、カメラの HTTP サーバは壊れた応答(500)を返した。 整数の秒(PT1S)なら受け付ける。 Timeout を省くと、カメラの既定の 180 秒まで動き続ける。 押している間だけ動かしたい場合は、5 秒の Timeout を付けて送り、押している間は 3 秒ごとに送り直す形にした。 移動の指示には、すぐ応答が返る。 GetStatus の位置は動いている途中でも変わるが、MoveStatus は常に UNKNOWN だった。 動き終えたかどうかは、位置が変わらなくなったことで判断するしかない。 動いている途中に次の移動を送ると、前の移動は打ち切られる。 1 秒動かしたときの位置の変化は、pan が速度 0.1、0.5、1.0 で約 0.06、0.15、0.20、tilt が速度 0.5 で約 0.71 だった。 指示の往復の時間を含む粗い値だが、正規化した値の上では tilt のほうがかなり速く動く。 検知イベントはカメラから送らせる ONVIF のイベントサービスは PullPoint に対応すると答え、動体、いたずら、ライン横断、人物、TPSmartEvent のトピックを持つ。 ところが、CreatePullPointSubscription で購読は作れても、PullMessages を送ると、カメラは何も返さずに接続を切った。 WS-BaseNotification の Subscribe なら届いた。 受け取る側が HTTP で待ち受け、その URL を Subscribe で渡すと、カメラがそこへ Notify を POST してくる。 このため、受け取る側のマシンは、カメラからの着信をファイアウォールで通す必要がある。 コンテナの中で受ける場合は、待ち受けるアドレスと、カメラに伝える宛先(ホストの LAN 側のアドレス)を分けて設定する。 通知の中身は次のように振る舞った。 検知している間は true の状態を約 1 秒ごとに送り、収まると false を 1 回送る。 Tapo アプリ(または独自の API)で検知をオンにした種類だけが届く。 通知に付く時刻(UtcTime)は信頼できない。人物の検知では、最初の時刻のまま変わらなかった。届いた時刻を使うほうがよい。 実機で届くのを確かめたのは、動体と人物だけである。 いたずら、ライン横断、TPSmartEvent は発生させられていない。 首振りの直後の動体は、届くときと届かないときがあった。 RTSP と ONVIF では届かない機能 スピーカーからの再生、microSD の録画、設定の読み書きは、標準の経路には無かった。 これらには、Tapo アプリが使う独自の 2 経路(443 番と 8800 番)を使った。 手順は自分で解析したものではなく、go2rtc の Tapo 対応(pkg/tapo)と pytapo に従った。 443 番の API のログインは、pytapo(3.4.26)にそのまま任せている。 プロトコルの詳細はこれらの実装と Wiki に譲り、ここでは使ってみて分かった結果と制約だけを書く。 スピーカーは鳴るが、鳴ったことは確かめられない 8800 番の経路で、8kHz の G.711 A-law を 20ms ずつ実時間の速さで送ると、カメラのスピーカーから鳴った。 テスト音を鳴らしながらカメラのマイクで録音し、再生した時刻に音量が上がることで確かめた。 この経路では、カメラから受信の確認が返らない。 カメラが接続を閉じたことと、送ったのにカメラが受け取っていない量(TCP の確認応答が返っていない量)が増えたことまでは、送る側で分かる。 しかし、受け取った音を実際に鳴らしたかどうかは分からない。 書き込みの成否だけを見ていると、カメラが止まっても OS の送信バッファが埋まるまで書き込みが成功し続けるので、数分間異常に気づけなかった。 スピーカーへの送信と、次に書く録画の取り出しは、同じ 8800 番でも同時に使えた。 microSD の録画は等速でしか取り出せない ONVIF には録画を検索して再生するサービス(Profile G)が無く、RTSP でも再生できない。 録画のある日、1 日の中の録画の区間、カードの容量と空きは 443 番の API で取れ、録画の中身は 8800 番で取り出せた。 取り出しはファイルの転送ではなく「再生」なので、等速でしか届かない。 20 秒ぶんの取り出しに 20.8 秒かかった。 2 倍以上の速さを指定するとデータは速く届くが、音声が届かなくなり、映像の時刻の進み方も変わったため、使っていない。 ほかにも、使ってみて分かった振る舞いと制約があった。 終了の時刻を指定しても守られず、流れ続ける。映像の時刻で必要な長さに達したら、受け取る側から切る。 区間の切れ目では再生は終わらず、録画の無い時間を飛ばして次の区間へ続く。録画された最も新しい時刻まで流すと、終了の通知が届く。通知のあともカメラは接続を閉じないので、受け取る側から閉じる。カードの範囲内で録画の無い時刻を指定した場合は、映像は届かず終了の通知だけが返る。 終了の通知より先に通信が途絶えたり接続が切れたりした場合は、録画の終わりではなく途中で切れたものとして扱える。 届く音声は G.711 µ-law の 16kHz で、MPEG-TS の中では ffmpeg が知らないストリーム種別(0x91)になっている。MP4 にするには、音声を自分で取り出して MP4 に格納し直す必要があった。 取り出している間は、録画の区間の一覧をカメラが返さない。一覧に要る ID の発行が USER_ID_FULL(-71101)で拒まれる。両立させる方法は見つからなかった。録画のある日、検知の記録、カードの状態は、取り出している間も取れる。 区間の一覧には種別 1 と 2 があり、1 は長く連続した区間、2 は短い区間として返る。 1 が常時録画、2 が検知による録画だと推定しているが、公開された資料では確かめられていない。 設定は読み書きできるが、警報を今すぐ鳴らせない 443 番の API で、画質、昼夜の切り替え、検知の種類、警報の音と光、音量、プライバシーモードなどを読み書きできた。 書き込みは、値を変えて読み直し、元に戻す形で確かめた。 確かめた項目は、LED、通知、昼夜、画質の 4 項目、歪みの補正、電源周波数、OSD の曜日、スピーカーとマイクの音量、マイクのミュート、検知の 10 種類、自動追尾、上書き録画、警報の音、光、音量、プライバシーモードである。 映像の解像度とビットレートは読むだけにした。 警報だけは、今すぐ鳴らす確かな方法が見つからなかった。 pytapo の手動警報や、新しいファームウェア向けとされる別名のメソッドを試したが、どれも「そのメソッドは無い」(-40210)が返った。 doManualAlarm という名前のメソッドは存在し、{"action":"start"} を渡すとエラー無しで応答した。 ただし、鳴らしながらカメラのマイクで録った音からは、警報が鳴ったとは判断できなかった。 実機の前で耳と目で確かめるまでは、サイレンの音を作ってスピーカーの経路で鳴らす方法で代えている。 音の大きさはスピーカーの音量で決まり、カメラの光は点かない。 ファームウェアの自動更新の設定は、書き込むと認証のエラー(-40209)で拒まれた。 SD カードの初期化とファームウェアの更新の開始は、取り返しがつかないので試していない。 独自の経路を使う前に確かめること 独自の 2 経路は公開された仕様ではない。 ファームウェアの更新で使えなくなる可能性があり、TP-Link が動作を保証するものでもない。 Tapo アプリの利用規約(TP-Link Application Terms of Use、2024 年 12 月 13 日発効)は、アプリに関わるソフトウェアを逆コンパイルやリバースエンジニアリングで調べることを禁じている。 今回はアプリを解析せず、公開済みの go2rtc と pytapo の手順を使ったが、独自の経路を使う場合は、各自で規約を読んで判断してほしい。 規約は地域や時期によって変わりうる。 また、前に書いたとおり、独自の経路はクラウドのアカウントのパスワードを求める。 このパスワードはカメラだけでなく Tapo アカウント全体に通じるので、置き場所はカメラアカウントより慎重に選ぶ必要がある。 長く動かしたときに起きたこと ライブ映像と録画の再生を、Chromium で同時に 40 分流し続けた。 映像は止まらず、録画の区間の切れ目をまたいでも再生が続いた。 このとき、サーバのメモリが毎分約 0.7MB ずつ増え続けた。 原因は、RTSP の読み取りに使っていた aiortc の MediaPlayer だった。 自前のスレッドで RTSP を読む形に替えると、8 分で 0.6MB の増加に収まった。 数時間の連続運転は試していない。 ffmpeg を子プロセスとして起動する構成では、親が強制終了されたときに ffmpeg が残り、RTSP の同時接続(3 本)を埋めたままになることがあった。 Linux では、setpriv --pdeathsig を通して起動し、親と一緒に止まるようにした。 標準の経路で済むかどうか 映像を見る、録画する、首を振る、検知を受け取る、という用途なら、C220 は RTSP と ONVIF だけで足りる。 その場合に気をつけるのは、RTCP の時刻のずれ、同時接続の上限、pan の符号、PullPoint が使えないことの 4 点である。 スピーカー、microSD の録画、設定まで扱うなら、非公式のプロトコルとクラウドのパスワードを受け入れることになる。 どこまでを標準の経路にとどめるかは、この 2 つを引き受けられるかで決まる。

  4. Most cheap IP cameras only speak RTSP, which is why the live view takes ages to load. go2rtc restreams them as WebRTC so they open almost instantly in a browser or Home Assistant. One small binary, nothing goes through a cloud. AlexxIT/go2rtc

    63 views
Trending awards, every completed day and week
Gold
01st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
0Owned by AlexxIT now

No trending awards yet

None of the repositories AlexxIT owns has finished in the top three of a completed trending day or week, by stars, forks or PR authors. See who is trending now.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.