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.
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:
- 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.
/clipslists 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.
- 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-diaglists 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.
- 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. - 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. - The final screen gives the address to open on a phone and a QR code for it.
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.
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 |
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.
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.
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.
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.
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.
| 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.
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.





