Skip to content

Repository files navigation

PAT Monitor, with the dog of the program's own icon, and the line: Go, WebRTC, Tailscale, CED-tiny.

PAT Monitor

A pet and baby monitor. It captures the webcam and the microphone, encodes them with the machine's hardware encoder, and streams them to a browser over WebRTC, at home or from anywhere, without opening a port on the router.

The whole program is a single executable with no external dependencies. It runs on Windows: video comes from Media Foundation, audio from WASAPI with an embedded Opus encoder, and remote access from an embedded Tailscale node. The name stands for Pet And Toddler.

The monitor being watched: the room fills the page, with a live badge and a signal meter at the top and one bar at the bottom carrying talk-back, a clip button, mute, the three detections, the details and sign-out.

Saved to a phone's Home Screen, the page opens with no browser bars, and held sideways in full screen it gives the whole screen to the room, with the same controls floating on the picture:

The same room at a phone's size, sideways and full screen: talk-back, clip and mute float on the left of the picture, the three detections on the right, with the live badge, the signal meter and the button that leaves full screen.

Features

  • Live video and audio in a browser. H.264 and Opus over WebRTC. Any number of viewers share a single encode.
  • Adaptive quality. The bitrate tracks a target quantizer inside the ceiling the network allows; below that, the resolution steps down; below the last resolution step, the frame rate steps down to 5 and then 2 fps. Audio always stays at full quality. Measured on Intel Quick Sync, on AMD, on NVIDIA and on the Microsoft software encoder.
  • Remote access without router configuration. An embedded Tailscale node publishes the signalling over HTTPS with a valid certificate through Funnel. The audio and video then travel directly between the two devices.
  • Password protection, with sessions, a rate limiter on failed logins, and the Funnel refusing to start until a password exists.
  • Guided setup in the browser, opened automatically on first run. Four screens for home-only use, six if remote access is wanted, showing live device and tunnel status at each step.
  • Tray icon whose colour reports the state, and a panel on right-click drawn to match the pages: the address as a QR code to point a phone at, the pending action if there is one, the guided setup, the video and log folders, disconnect-all-devices, a forgotten-password reset, and quit.
  • Detection: motion, a baby crying, a dog barking, each with its own switch. An event raises a band and a chime on the open page, and is written to the log. Sound is recognised by CED-tiny, a 527-class audio tagger that runs on the CPU inside the binary — no download, no GPU, nothing to install — woken by a shape detector, so at rest it does not run at all. The classes and the thresholds were measured on three public datasets and are in baselines/sounds.txt.
  • Event clips. Every detected event is written to disk: four to six seconds before it and ten after. The Clip button records one on demand out of the same pre-roll, and what it writes is exempt from retention. /clips lists them, plays them back, downloads and deletes them, and a per-clip lock exempts or releases the others. Retention is a disk quota plus an age, and no clip, kept or not, is written while the disk has less than 1 GB free: the page and the tray say so until there is room again.
  • Camera and microphone are chosen from the page. The panel behind More lists the webcams and the microphones the machine has; choosing one reopens that capture without restarting anything, and a line beside the box says which device is really being used when the chosen one is not connected. Where Windows offers Automatic framing for the webcam, the monitor turns it off while it watches, because it crops the room around a face; the switch in Windows' camera settings stays as it is for other programs.
  • Talk-back. Press the button and your voice comes out of the PC speakers. Half duplex: while you speak the monitor stops sending the room, so the microphone cannot pick up the speakers. One person talks at a time.
  • Diagnostics: rotating log file, per-session viewer log with ICE path and packet loss, and standalone measurement tools.

The interface takes its language from the browser — English, Italian, German, French, Spanish, Simplified Chinese, Portuguese (Brazil and Portugal) or Japanese, English being what everything else falls back to — with a selector in the top right corner for when the browser gets it wrong. Traditional Chinese is not there: a browser asking for zh-TW gets the Simplified catalogue.

The recordings page: the player at the top with the open clip's time and kind, Download and Delete; under it a filter per kind of clip with its count, then the clips grouped by day, each row with what was recognised, its time and length, and a star to keep it.

Requirements

  • Windows 10 or 11, x64.
  • A webcam and a microphone. Either one can be missing or fail without stopping the other.
  • An H.264 encoder, which every supported Windows has: the machine's hardware one where there is one, the Microsoft software encoder otherwise. Which it is is not chosen by vendor, and pat-diag lists what the machine offers.
  • A free Tailscale account, only for access from outside the house. Sign-in uses an existing Google, Microsoft or Apple account, and nothing has to be installed on the viewing phone.

PAT Monitor is not a medical device or a safety system. It can miss crying, barking or motion, the picture stops if the PC or the network does, and it calls no one: its warnings appear only on the monitor's PC, on pages that are open and on devices with notifications turned on. Notifications can arrive late or not at all.

Getting started

  1. Install it from the Microsoft Store, which also keeps it updated, and start it from the Start menu. Or download the zip from Releases, unpack it anywhere, and run pat-monitor.exe. That binary is unsigned, so SmartScreen shows a warning: choose More info and then Run anyway.
  2. The guided setup opens at http://localhost:8080/onboarding. It asks for a password, shows the camera and the microphone level so they can be checked, and then offers remote access.
  3. The final screen gives the address to open on a phone and a QR code for it.

The fork in the guided setup: at home only, ready now, or from outside too, five minutes.

The last screen of the guided setup: the monitor is on, with the address at home and the address from outside, each with a QR code to point a phone at.

The program then runs in the notification area. Left-clicking the icon opens the monitor; right-clicking opens a panel with the QR code, the state and the commands — among them quitting, which is what releases the camera and the microphone.

Start it from the console of that machine, not from inside a Remote Desktop session. Windows audio endpoints are per-session: in an RDP session the microphone does not exist and talk-back cannot open, while the camera works, so it looks like a microphone fault. Disconnecting does not fix it — the process does not change session — so a monitor started that way stays deaf for as long as it runs. It says so in the log and keeps the video going.

Configuration

Outside the Microsoft Store, config file and logs live in %APPDATA%\PAT Monitor\, and Tailscale's data for this PC in %LOCALAPPDATA%\PAT Monitor\tsnet, which is moved there from the first folder the first time access from outside starts. The log writes the public address with your tailnet's name replaced by <tailnet>, except at -v, so look a -v log over before attaching it anywhere. Installed from the Store they are there too if that folder already exists; otherwise Windows keeps them in the app's private folder under %LOCALAPPDATA%\Packages, and the log-folder button in the notification-area panel opens the right one either way. The file is YAML and is written by the setup, so editing it by hand is optional.

key default meaning
listen_addr :8080 local listening address, plain HTTP; put no proxy in front of it, since the first setup trusts connections from this PC
quality high video preset: high 720p30 2500 kbit/s, medium 540p30 1200, low 360p15 500
target_qp 30 target quantizer for the saving loop, 0 disables it
camera_device_id first usable camera to use, by the symbolic link pat-diag prints
mic_device_id system default microphone to use
mic_gain_db 0 extra gain applied to the capture
speaker_device_id system default audio output used for talk-back
prefer_encoder measured force a specific H.264 encoder
session_ttl_hours 168 how long a login lasts
stun_servers Google, Cloudflare STUN servers for both ends
update_check true ask GitHub once a day whether a newer release exists
funnel_enabled false publish on the Internet through Tailscale
funnel_hostname patmon- plus a per-machine fingerprint node name in the tailnet
tailscale_auth_key empty pre-authorise the node instead of approving it from the browser; used at the first login only and kept in plain text, so remove it once the node is in
detect_cry, detect_bark, detect_motion true which events to look and listen for
clips_max_mb 1000 disk quota for the event clips, 0 means no limit
clips_max_days 14 how long a clip is kept, 0 means no expiry

That is every key. The file also holds password_hash, an argon2id hash, and onboarding_done: state the setup writes, not settings. camera_name is the superseded way of choosing the camera, read only when camera_device_id is empty: a name is shared by two cameras of the same model.

If the chosen camera is not connected the monitor opens the first usable one and says so, with a warning in the log and an alert on the page: the picture may be of another room, and that is the one thing you cannot tell by looking at it.

Command line:

flag effect
-config PATH use a different configuration file
-listen ADDR override the listening address
-set-password set the password and exit; refused while the monitor is running
-show-config print the active configuration and exit
-version print the version and exit
-v debug logging
-audio-test-tone replace the microphone with a generated tone
-bitrate N pin the video bitrate, for diagnostics
-simulate-fault CODES alternate the given faults every twenty seconds, to exercise the alerts
-simulate-panic WHERE raise a deliberate panic in video, audio, accessory or now, to exercise the recovery
-pprof ADDR enable pprof on a loopback address

Updates

Once a day the monitor asks GitHub whether a newer release exists. If there is one, the notification area says so — a balloon the first time, then a row in the panel that opens the release page — and the log records it. Nothing is downloaded and nothing is replaced: updating means fetching the new zip and unpacking it over the old one, with the monitor closed.

Releases marked pre-release on GitHub are never reported.

The Microsoft Store version does none of this: the Store updates it, and the binary inside the package is built with the check switched off, whatever update_check says.

What leaves the house is one request a day carrying this machine's address and the version it is running — nothing about the configuration, the tailnet or who is watching. update_check: false stops it, and then the monitor speaks to nobody.

Each release is a zip holding the binary, LICENSE, NOTICE and licenses/, plus a detached signature made with a key that is not on GitHub. Nothing checks that signature yet: it ships now so that a later version can.

How remote access works

The Tailscale node runs inside the process, so Tailscale does not have to be installed on the machine. Funnel gives the monitor a public HTTPS address with a real certificate, which serves the page and the WebRTC signalling. Audio and video do not go through it: ICE opens a direct path between the monitor and the phone. If that path cannot be established, for instance on a network that blocks UDP, the connection fails: there is no relay to fall back to. The remedy for such a network is Tailscale on the device you watch from, which relays on its own when a direct path is not available.

Notifications with the page closed

The same warnings the page shows can reach a phone or a computer with the page closed, as system notifications. It is off until it is turned on, one device at a time, from More → Notifications on this device. The switch appears only where the browser can receive them — on the https://…ts.net address, and on an iPhone or iPad only in the web app added to the Home Screen — and elsewhere the card says why. Each device has settings of its own that decide whether a notification arrives while it sleeps; the procedure for Windows, Android and iPhone lists them, in every language of the interface, and the link in the page opens the one in the page's language. The message goes through the push service the browser uses (Apple, Google, Mozilla or Microsoft), encrypted for that device.

Building

Go 1.26 or newer and PowerShell. No CGo, no C compiler.

$env:CGO_ENABLED = '0'
.\build.ps1              # what ships: no console, stripped
.\build.ps1 -Console     # the same monitor with a console, plus the tools
go vet ./... ; go test ./...

With no flag the build produces the program as it is delivered: no console, the tray icon its only presence.

build.ps1 rather than go build: the build stamps the version and the Tailscale node version through the linker, and generates the executable's icon resource.

.\build.ps1 -Release makes what gets published. It refuses a tree with uncommitted changes, regenerates licenses/, and leaves dist\PAT-Monitor-<version>-windows-amd64.zip — the binary, LICENSE, NOTICE and licenses/ inside one folder — with a detached .sig beside it.

The signature needs a signing key, and a fork needs its own. go run ./cmd/pat-sign -generate -key release.key writes the private half and prints the public one to paste into internal/update/pubkey.go; build.ps1 reads the private half from -Key or from PATMON_SIGNING_KEY and will not build a release without it. Keep it out of the repository — *.key is in .gitignore — and back it up: a lost key cannot be replaced for installations that already exist.

.\build.ps1 -Publish -Key <path> cuts a release. It wants the commit tagged v<version> and the tag pushed, and GitHub CLI signed in. It runs gofmt, vet and the tests, builds the package and the signed archive, checks the signature, and leaves a draft release on GitHub with the zip and the .sig attached; the notes are written and the release published on the page. The key is deliberately not a repository secret and releases are not built by a workflow: the key exists to survive this account, so a workflow that could reach it would prove the opposite of what it signs.

Diagnostic tools

build.ps1 -Console puts the measurement tools in bin/ next to the monitor; which ones is build.ps1's business. The default build skips them.

tool what it reports
pat-diag Direct3D device, available H.264 encoders, camera formats and streams, microphone; -mic-modes measures the three microphone paths — shared, raw, exclusive — one after the other, and -out checks the talk-back output by listening to what the speakers play
pat-capture the full pipeline: delivery regularity, bitrate, quantizer distribution, keyframes
pat-viewer a synthetic WebRTC viewer, with on-demand keyframe requests
pat-opus the audio encoder, checked against an independent decoder
pat-sounds the sound recognition on labelled datasets: which classes to watch, at what threshold, and what the shape detector lets through

Three more are run from source: go run ./cmd/pat-licenses regenerates licenses/, go run ./cmd/pat-icon writes the icon resource, which the build already does, and go run ./cmd/pat-sign makes the release signing key and signs an archive with it, which build.ps1 -Release calls.

Source layout

package contents
cmd/pat-monitor the program, flags, wiring
internal/mf, internal/media Media Foundation capture and H.264 encoding, SPS and QP parsing, MP4 muxer
internal/audio, internal/audiocodec, internal/resample WASAPI capture, Opus encoding, and the rate conversion the analysis stream needs
internal/pipeline capture supervision, restart, resolution and cadence changes
internal/rtc WebRTC hub, congestion control, bitrate, resolution scale, quality loop
internal/server HTTP, authentication, signalling, web pages, session log
internal/tunnel Tailscale node and Funnel
internal/tray notification area icon and the panel behind it
internal/qr, internal/icon QR encoder and Windows icon writer
internal/wincom the reading of what CoInitializeEx returns
internal/opuswasm libopus in WebAssembly, one module per codec
internal/detect, internal/alerts motion, cry and bark detection, and the set of what is wrong right now
internal/ced, internal/gguf the sound recogniser that names what it heard, and the reader for the model file it runs
internal/guard a panic turned into a fault of the part it happened in, so the camera does not go off for it
internal/record the seconds before an event, kept in memory, and the clips written from them
internal/push the notifications sent with the page closed, and the devices that asked for them
internal/i18n language catalogues, with English as what the others fall back to
internal/encoder, internal/devices quality presets, camera and microphone enumeration
internal/update the daily check for a newer release, and the signing key
internal/diag, internal/applog, internal/config, internal/version delivery measurement, log file, configuration, build stamps
internal/doc no code: the guard on what this file and CLAUDE.md claim about the tree

The source, its comments and the log are in English. The interface is not: it lives in per-language catalogues under internal/i18n/catalogs/, one file per language and English as the base the others overlay.

CLAUDE.md carries the index of every rule the program was built on, and the chapters are in .claude/rules/, one slice per area, each declaring the packages it governs. They are plain markdown: a coding agent is handed the slice for the code it is touching, a reader opens whichever one the index points at.

License

Apache License 2.0. See LICENSE and NOTICE.

Third-party licences are in licenses/, one directory per module, generated from the packages linked into the executable. They are all permissive: MIT, BSD, ISC and Apache-2.0. What ships inside the binary without coming from a Go module — libopus as WebAssembly and the C bridge around it, the Go standard library, the Tabler icons — is in licenses/manually-added/, one directory per work.

About

Pet and baby monitor for Windows. Webcam and microphone to the browser over WebRTC, from anywhere, without opening a port. One executable, no dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages