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.

The examples use Old Bird's default username, oldbird. If you set custom viewer credentials in the app, replace it with your selected username.

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, and the viewer username and password shown in 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 HTTP Basic authentication with the viewer username and password shown in the app. The default username is oldbird. 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, and active: true for an in-progress file)
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 -_.. An entry with active: true is still in progress: it is downloadable but won't have a moov atom yet, so 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.

Off-LAN viewing (relay broker)

Sign in with Google and subscribe to Pro, then enable off-LAN viewing. Open relay.oldbird.app in any browser and sign in with the same Google account. The phone opens an outbound WebSocket to the relay, which registers the camera in Firestore ({name, url, lastSeen}) and bridges browsers or the in-app viewer to the camera over an HTTP-over-WebSocket proxy and WebRTC. There is no SSH tunnel, no third-party tunnel provider, and no end-to-end passphrase.

Trust note: the relay terminates the connection and passes traffic through in memory (it stores nothing), so off-LAN viewing is not end-to-end encrypted. If your threat model excludes that hop, keep off-LAN viewing off and use Tailscale or a self-hosted reverse proxy on your LAN instead.