Old Bird sticks to standards: HTTPS, MJPEG, RTSP, RTP, RFC 4918 WebDAV, plain JSON webhooks. Anything that speaks those plugs in directly.
Stream URL: rtsp://oldbird:<password>@<phone-ip>:<port>/. H.264 video + AAC audio, RTP over UDP unicast, RTCP sender reports every 5 s.
Default UDP transport, works out of the box.
# Live preview
ffplay -rtsp_transport udp rtsp://oldbird:[email protected]:8554/
# Transcode 10 seconds to MKV (use MKV/Opus for archival; -c copy to MP4
# silently drops audio because of strict PTS handling in the MP4 muxer).
ffmpeg -rtsp_transport udp -i rtsp://oldbird:[email protected]:8554/ -t 10 out.mkv
VLC defaults to RTP-over-TCP (interleaved). Old Bird only speaks RTP-over-UDP, so SETUP returns 461 Unsupported Transport. Force UDP transport:
vlc --rtsp-tcp=0 rtsp://...gst-launch-1.0 rtspsrc location=rtsp://oldbird:[email protected]:8554/ \
protocols=udp latency=200 ! rtph264depay ! avdec_h264 ! autovideosink
Add a Media Source, uncheck Local file, set Input to the RTSP URL and Input Format to rtsp. Add -rtsp_transport udp in Input format options.
In config.yml:
cameras:
oldbird_garage:
ffmpeg:
input_args: preset-rtsp-restream
inputs:
- path: rtsp://oldbird:[email protected]:8554/
input_args: -rtsp_transport udp
roles:
- record
- detect
detect:
width: 640
height: 480
fps: 5
Frigate's preset-rtsp-restream will re-publish to its bundled go2rtc, so other consumers don't all hammer the phone.
Install the RTSP plugin, add a camera with the Old Bird URL. Scrypted's go2rtc front-end handles transport quirks automatically.
Old Bird ships an ONVIF Profile S server + WS-Discovery responder, so HA finds the camera automatically.
oldbird and the password from the app, finish. HA gets the H.264 RTSP stream + JPEG snapshot in one shot.https://oldbird:[email protected]:8443/stream — fallback if ONVIF doesn't fit. No audio, set Verify SSL = off.UDP — manual fallback for H.264 + audio.{event, camera, ip, timestamp} shape maps directly onto a Webhook trigger; reference fields with {{ trigger.json.camera }}.The same ONVIF endpoint works with Synology Surveillance Station, BlueIris, iSpy, Agent DVR, Frigate (via go2rtc's ONVIF source), Milestone XProtect, etc. Most adopt it as a generic ONVIF camera with no manufacturer-specific config. Profile S only — no PTZ, no events service, no audio backchannel.
Add a Network camera, type MJPEG, URL https://192.168.1.4:8443/stream, username oldbird, password from the app. Or use the RTSP URL for H.264 + audio.
The simplest way to consume the stream. No special clients required.
GET /stream — multipart/x-mixed-replace; boundary=oldbirdframe. Each part is a JPEG with Content-Type: image/jpeg. Many browsers, libraries and tools handle this natively.GET /audio — raw PCM s16le, 16 kHz, mono, no container. ffplay -f s16le -ar 16000 -ac 1 -i <url> or pipe into aplay on Linux.Browsers happily render <img src="https://oldbird:[email protected]:8443/stream"> in place. Note that Chromium rejects credentialled URLs in fetch(); the embedded viewer constructs absolute URLs without credentials and lets the browser's cached Basic Auth handle auth.
Anything implementing RFC 4918 PUT + Basic Auth works. Tested combinations:
URL: https://cloud.example.com/remote.php/dav/files/<username>/oldbird/. Generate an app password (Settings → Security → Devices & sessions) and use that as the WebDAV password — not your account password.
Quickest local target:
rclone serve webdav --user nurettin --pass hunter2 \
--htpasswd '' --addr 0.0.0.0:8999 ./oldbird-archive/
Old Bird URL: http://192.168.1.10:8999/ (use https:// if you put a reverse proxy in front).
mod_dav<Location /oldbird/>
Dav On
AuthType Basic
AuthName "WebDAV"
AuthUserFile /etc/apache2/oldbird.htpasswd
Require valid-user
</Location>
dav_extlocation /oldbird/ {
dav_methods PUT DELETE MKCOL COPY MOVE;
dav_ext_methods PROPFIND OPTIONS;
create_full_put_path on;
client_max_body_size 0;
auth_basic "WebDAV";
auth_basic_user_file /etc/nginx/oldbird.htpasswd;
}
All three ship a WebDAV server. Enable WebDAV, create a dedicated user, allow only the target share. The directory URL in Old Bird is https://<nas>:<dav-port>/<share>/ with the trailing slash.
Picked from Settings → Motion detection → Alert provider. Each provider declares the fields it consumes; the settings UI and the /motion JSON shape stay in lock-step.
| Provider | URL | Auth header | Token / chat id | Body / Content-Type |
|---|---|---|---|---|
| Generic JSON | required | optional | — | fixed JSON |
| Custom | required | optional | — | template + Content-Type |
| ntfy.sh | required | optional | — | plain text + Title/Tags |
| Slack | required | — | — | Slack JSON |
| Discord | required | — | — | Discord JSON |
| Telegram bot | — | — | both | Telegram JSON |
Canonical shape. Targets ntfy / Home Assistant Webhook / IFTTT Webhooks / your own proxy.
POST <url>
Content-Type: application/json; charset=utf-8
User-Agent: oldbird/motion
Authorization: <your header value, if set>
{"event":"motion","camera":"Garage","ip":"192.168.1.4","timestamp":1714233600000}
Power-user escape hatch: arbitrary template + Content-Type override. Placeholders are substituted at fire time:
{camera} — the camera name from Settings{ip} — the camera's LAN IP at fire time{time} — HH:mm:ss local time{timestamp} — epoch milliseconds (numeric)Example: Pushover (form-encoded).
URL : https://api.pushover.net/1/messages.json
Content-Type : application/x-www-form-urlencoded
Body : token=APP_TOKEN&user=USER_KEY&title={camera}&message=Motion+at+{time}
Public ntfy or self-hosted. The body is plain text; Title + Tags headers carry metadata. Self-hosted ntfy with token auth: paste Bearer tk_... into the Authorization field.
POST https://ntfy.sh/your-topic
Content-Type: text/plain; charset=utf-8
User-Agent: oldbird/motion
Title: Garage
Tags: rotating_light,camera_with_flash
Motion detected at 14:32:18
Get a webhook URL from api.slack.com/apps → Your App → Incoming Webhooks. The URL itself is the secret — no Authorization header.
POST https://hooks.slack.com/services/T0.../B0.../...
Content-Type: application/json; charset=utf-8
{"text":"Motion on *Garage* at 14:32:18"}
Channel settings → Integrations → Webhooks → Copy URL. Same shape as Slack but the field is content.
POST https://discord.com/api/webhooks/.../...
Content-Type: application/json; charset=utf-8
{"content":"**Motion** on `Garage` at 14:32:18"}
Talk to @BotFather to create a bot and get a token. Then message your bot and visit https://api.telegram.org/bot<TOKEN>/getUpdates to find your chat id (numeric for personal/group chats, @channelname for public channels). The URL is reconstructed from the token; leave the URL field blank.
POST https://api.telegram.org/bot<TOKEN>/sendMessage
Content-Type: application/json; charset=utf-8
{"chat_id":1234567890,"text":"Motion on Garage at 14:32:18"}
Adding a provider: the provider list is a Kotlin sealed class. Drop a new object into MotionAlertProvider.kt, append it to all, declare which fields you consume. The settings UI, /motion JSON contract and the viewer all pick it up automatically.
All endpoints are HTTPS on https_port (default 8443) and require Authorization: Basic oldbird:<password>. The plain-HTTP port serves only the landing page.
| Method | Path | Purpose |
|---|---|---|
| GET | / · /index.html · /viewer.html | Embedded HTML viewer |
| GET | /stream · /stream.mjpg | multipart/x-mixed-replace MJPEG |
| GET | /audio · /audio.pcm | Raw PCM s16le 16 kHz mono |
| GET | /status · /status.json | Battery, charging, temperature, voltage, streaming/audio flags |
| GET / POST | /settings | Read or update video settings (width, height, quality, facing) |
| GET / POST | /recording | Read or update on-device recording (active, segmentSeconds, maxSegments) |
| GET | /recordings | List MP4 segments on the device (name, size, mtime) |
| GET | /recordings/<name> | Download a segment (Content-Disposition: attachment) |
| DELETE | /recordings/<name> | Delete a segment |
| GET / POST | /rtsp | Read or update the RTSP server (enabled, port) |
| GET / POST | /webdav | Read or update WebDAV upload (active, url, username, password, segmentSeconds, maxFailedKept, wifiOnly) |
| GET / POST | /wifi | Inspect or toggle the Wi-Fi lock |
| GET / POST | /motion | Read or update motion detection (enabled, sensitivity, postRollSeconds, idleFps, record, alertUrl, alertAuth, etc.) |
Filenames in /recordings/<name> are restricted to alphanumerics plus -_.. The active (in-progress) segment is downloadable but won't have a moov atom yet — ffprobe will reject it. Wait for rollover.
Three ways to reach Old Bird from outside the LAN, in increasing order of how much trust they require:
Recommended. Install Tailscale on the phone and on the device you're connecting from. The phone will pick up a 100.x CGNAT address that the IP allowlist already permits (RFC 6598 carve-out). No port-forwarding, no public exposure, full TLS verification with the on-device cert pinned by the viewer.
Put Caddy or nginx in front of the phone, terminate Let's Encrypt for a real public hostname, and proxy to https://<phone-ip>:8443/. Turn off Private networks only on the phone (the proxy will be the source IP) and add a stricter allowlist or basic-auth at the proxy. Trust profile: same as exposing any home service.
Toggle Internet access in Settings. Old Bird opens an SSH reverse tunnel to localhost.run and registers the resulting public URL with your viewer via Firestore (Google sign-in required). Camera credentials in the registry are encrypted with a per-user PBKDF2 key derived from your end-to-end passphrase. The traffic itself is TLS to localhost.run; localhost.run is a third party and sees your encrypted streams.
Trust note: the tunnel terminates TLS at localhost.run and re-encrypts to the phone over loopback. If your threat model excludes that hop, prefer Tailscale or a self-hosted reverse proxy.