A complete picture of what Old Bird does on the phone, the wire, and the page.
The phone runs a TLS-wrapped HTTP server on the configured HTTPS port (default 8443). On first run, TlsKeys generates a 20-year self-signed RSA-2048 certificate inside AndroidKeyStore under the alias oldbird-tls. Browsers warn once and remember.
Visiting https://<phone-ip>:8443/ serves the embedded viewer (viewer.html) bundled with the app:
<canvas> drives both the live preview and any browser-side recording.multipart/x-mixed-replace response with createImageBitmap + drawImage — no <img> tag tricks.captureStream(0) + MediaRecorder for WebM/VP9+Opus with audio; Firefox falls back to wrapping raw JPEG bytes in an AVI/MJPG container (Firefox's captureStream + MediaRecorder combination produces empty output for JPEG-decoded canvases)./status every 5 s for battery, charging state, temperature and voltage./settings and posts back changes — the same source of truth the in-app UI writes to.The HTTP port (default 8080) serves a separate landing page only. It explains the self-signed-certificate warning with per-browser steps (Chrome / Edge / Firefox / Safari) and a button that builds the HTTPS URL from location.hostname. No camera endpoints are reachable over plain HTTP.
Camera2 is the only path. Camera2Source opens an ImageReader in YUV_420_888, converts to NV21 on the camera handler thread, and publishes frames to two consumers:
YuvImage.compressToJpeg straight from NV21, broadcast through FrameBroadcaster — this is the source of /stream.EncoderHub feeds NV21 to a long-lived MediaCodec H.264 encoder. The hot path swaps NV21→NV12 in place when the encoder reports COLOR_FormatYUV420SemiPlanar, and de-interleaves to I420 when it doesn't. One encoder, N consumers (recording + RTSP + motion clip), 1× CPU.Only camera-facing changes need a real CameraDevice close+reopen. Resolution change reconfigures the capture session on the same open device. Quality, rotation, exposure, fps cap, zoom and flash apply via volatile field writes plus a fresh repeating CaptureRequest — the HAL is never touched. StreamService.applySettings dispatches to the lightest path that fits the delta, so changing JPEG quality from the viewer does not drop frames.
The MediaTek HAL on the Xperia XZ1 will, over time, wedge its exe_cq kernel thread until reboot if camera close+reopen is hammered. Three mitigations:
Camera2Source.start bounds the open wait at 5 s. Without it, start() blocks forever holding lifecycleLock, deadlocking every prefs listener and consumer-count callback.postDelayed so the HAL can finish tearing down.AudioSource opens AudioRecord on the device MIC in 16 kHz mono 16-bit signed PCM. Two consumers:
/audio serves the raw PCM stream verbatim — the viewer decodes it with the Web Audio API and pipes it into a MediaStreamDestination so WebM browser recordings include audio.AudioBroadcaster and feeds an AAC MediaCodec encoder for RTSP and on-device recording.RtspServer speaks RTSP/1.0 over TCP, with RTP and RTCP over UDP unicast. Per-session UDP port pair (even/odd, RFC 3550), one packetiser per codec:
config= field and in-band.csd-0.Same RequestGate as the HTTP server: IP allowlist, then rate limit, then Basic Auth. URL is rtsp://oldbird:<password>@<phone-ip>:<port>/. The DESCRIBE handler waits up to 1.5 s for SPS/PPS/ASC; if the encoder hasn't emitted format yet it returns 503 and the client should retry.
Note: RTP-over-TCP (interleaved) is not implemented. VLC defaults to TCP transport, so pass --rtsp-tcp=0 or set the preference off. ffplay defaults to UDP and works.
DeviceRecorder subscribes to EncoderHub and writes its encoded H.264 + AAC into MediaMuxer MP4 segments under getExternalFilesDir(DIRECTORY_MOVIES):
A second DeviceRecorder instance runs concurrently for WebDAV upload, writing into cacheDir/webdav/ and handing each finalised file to WebDavUploader.
WebDavUploader drains a bounded queue on a background thread:
PUT with Basic Auth, three attempts with backoff, deletes the local file on success.failed-upload-*.mp4 and kept up to a configurable count; oldest evicted past the cap.mod_dav, nginx dav_ext, rclone serve, Synology WebDAV, etc.MotionDetector reads NV21 directly off the camera (the first w*h bytes are the Y plane), sub-samples a 16×12 cell grid, and computes per-cell luminance differences against the previous frame. One sensitivity slider (1..100) scales both the per-cell pixel-diff threshold and the minimum-cells-changed threshold together. When motion is enabled:
DeviceRecorder starts with the filename prefix motion-.MotionAlerter fires a one-shot HTTP request to your configured webhook. Concurrent fires are dropped while one is in flight.The webhook shape is selected per provider — see Integrations → Motion alerts for the wire formats Old Bird produces.
Optional. TunnelManager opens a one-way SSH reverse tunnel to localhost.run:
localhost.run a stable identity.localhost.run back to the loopback HTTP mirror on a chosen local port.ConnectivityManager.NetworkCallback wakes the loop early when a usable network appears.users/{uid}/profile/main; the passphrase itself never leaves the device.Switching this off restores LAN-only operation. The local server keeps running; only the tunnel and registration go away.
Every request — HTTP, HTTPS, RTSP — flows through RequestGate:
100.64.0.0/10) for CGNAT/Tailscale. Checked before auth so off-LAN clients can't probe credentials.Retry-After: 60. Lockout applies even to the correct password during the window (defends against observation attacks).oldbird, password is a randomly-generated 12-character string created on first service start. Constant-time comparison.The camera and microphone only run while a consumer is attached — a /stream client, an /audio client, an active recorder, an RTSP session, or motion detection. Five seconds after the last consumer disappears, the foreground service tears the camera down. This is what keeps the device cool and the battery healthy for 24/7 operation.
The persistent notification reflects the live state: video+audio+rec+rtsp:8554+motion on 192.168.1.4 https:8443 http:8080, or just idle when nothing's running.