Old Bird

Integrations

Old Bird sticks to standards: HTTPS, MJPEG, RTSP, RTP, RFC 4918 WebDAV, plain JSON webhooks. Anything that speaks those plugs in directly.

On this page

RTSP players

Stream URL: rtsp://oldbird:<password>@<phone-ip>:<port>/. H.264 video + AAC audio, RTP over UDP unicast, RTCP sender reports every 5 s.

ffplay / ffmpeg

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

VLC defaults to RTP-over-TCP (interleaved). Old Bird only speaks RTP-over-UDP, so SETUP returns 461 Unsupported Transport. Force UDP transport:

GStreamer

gst-launch-1.0 rtspsrc location=rtsp://oldbird:[email protected]:8554/ \
  protocols=udp latency=200 ! rtph264depay ! avdec_h264 ! autovideosink

OBS Studio

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.

NVR / home automation

Frigate

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.

Scrypted

Install the RTSP plugin, add a camera with the Old Bird URL. Scrypted's go2rtc front-end handles transport quirks automatically.

Home Assistant

Old Bird ships an ONVIF Profile S server + WS-Discovery responder, so HA finds the camera automatically.

Other ONVIF-compatible recorders

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.

MotionEye

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.

MJPEG & PCM

The simplest way to consume the stream. No special clients required.

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.

WebDAV servers

Anything implementing RFC 4918 PUT + Basic Auth works. Tested combinations:

Nextcloud

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.

rclone serve

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).

Apache mod_dav

<Location /oldbird/>
  Dav On
  AuthType Basic
  AuthName "WebDAV"
  AuthUserFile /etc/apache2/oldbird.htpasswd
  Require valid-user
</Location>

nginx dav_ext

location /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;
}

Synology / TrueNAS / QNAP

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.

Motion alert providers

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.

ProviderURLAuth headerToken / chat idBody / Content-Type
Generic JSONrequiredoptionalfixed JSON
Customrequiredoptionaltemplate + Content-Type
ntfy.shrequiredoptionalplain text + Title/Tags
SlackrequiredSlack JSON
DiscordrequiredDiscord JSON
Telegram botbothTelegram JSON

Generic 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}

Custom

Power-user escape hatch: arbitrary template + Content-Type override. Placeholders are substituted at fire time:

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}

ntfy.sh

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

Slack incoming webhook

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"}

Discord webhook

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"}

Telegram bot

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.

HTTP API

All endpoints are HTTPS on https_port (default 8443) and require Authorization: Basic oldbird:<password>. The plain-HTTP port serves only the landing page.

MethodPathPurpose
GET/ · /index.html · /viewer.htmlEmbedded HTML viewer
GET/stream · /stream.mjpgmultipart/x-mixed-replace MJPEG
GET/audio · /audio.pcmRaw PCM s16le 16 kHz mono
GET/status · /status.jsonBattery, charging, temperature, voltage, streaming/audio flags
GET / POST/settingsRead or update video settings (width, height, quality, facing)
GET / POST/recordingRead or update on-device recording (active, segmentSeconds, maxSegments)
GET/recordingsList 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/rtspRead or update the RTSP server (enabled, port)
GET / POST/webdavRead or update WebDAV upload (active, url, username, password, segmentSeconds, maxFailedKept, wifiOnly)
GET / POST/wifiInspect or toggle the Wi-Fi lock
GET / POST/motionRead 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.

Remote access

Three ways to reach Old Bird from outside the LAN, in increasing order of how much trust they require:

Tailscale / WireGuard / Headscale

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.

Reverse proxy at home

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.

Built-in tunnel (localhost.run)

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.