How Helen Gets Made A making-of, not the live show Watch her live

Cameras · Which camera · the network

Reserve the address, block the cloud, name the job

Eleven cameras, one /24, one camera account, and four config files that each spell out a camera's name and have to agree on it. Skip the DHCP reservation and Frigate loses a camera the next time the router hands out addresses; misspell the id in one file and the Shorts pipeline never sees that camera at all.

This comes after which camera to buy and before the cameras go into Frigate.

Helen at the water bowl, seen by the camera whose job, address and name are all 'water'
The camera that took this frame is cat_water_bowl in Frigate, "Water Cam" to the public, station water to the pipeline, and UDP port 5103 to the mosaic. Same job, four names, one table.

Aiming and cat height are the half of a camera install people photograph. The other half is a table with one row per camera, and the rule that no camera exists in any config unless it is in the table. Ours is below, followed by the naming schema, the line it draws between the house and the show, and the two checklists a new camera goes through. The Frigate blocks that consume it are on the camera-setup page and in full on the annotated config.

The table

Every camera gets a DHCP reservation on the router the day it is unboxed, so the address in config.yml never moves. All eleven are on one 192.168.1.0/24; the host numbers are yours to choose, and the rest of this site writes them as 192.168.1.<N>.

Camera → job → name → where it may appear
Frigate id · jobAddress · public label · reach
cat_water_bowl · fountain, from above192.168.1.<N> · Water Cam · public stills, mosaic, Shorts (always eligible)
cat_food_bowl · kibble and wet dish, body and bowl192.168.1.<N> · Food Cam · public stills, Shorts
cat_treat_dispenser · the dispenser the treat button fires192.168.1.<N> · Treat Cam · public stills, mosaic, Shorts
cat_scratching_pad · overhead of the whole room (the Court)192.168.1.<N> · Scratch Cam on the proxy, "Court" everywhere else · public stills, Shorts
helen_food_face · head-on at the bowl192.168.1.<N> · Face Cam · not on the website; the single-camera 24/7 stream and the mosaic's top-left pane, Shorts
bed_cam · the bed192.168.1.<N> · Bed · mosaic only
playroom_cam192.168.1.<N> · Playroom · Frigate only, detecting
bed_cam_2192.168.1.<N> · audio trial; enabled: false
front_door, carport · house192.168.1.<N> · no label · record only, 21 days, detect off, never leaves the box
back_yard · house, the one non-Tapo camera192.168.1.<N> · paths /live/ch0 and /live/ch1 instead of /stream1//stream2 · record only, never leaves the box

Notice the one leftover. cat_scratching_pad was a scratching pad; the camera was tilted up in August and is now the room's overhead. The Frigate id stayed, the proxy still labels it "Scratch Cam", and everything editorial calls it the Court. The cameras page says to trust the picture and rename later, but renaming a Frigate camera means renaming its recordings directory, its events and every reference in four files, so the id stays and the alias list below absorbs it. Name the job right the first time.

The naming rule, and the four places a name lives

Frigate id: cat_<job> for a Helen station, <place>_cam for an angle that is not a station, plain <place> for the house. Public label: <Job> Cam. Lower-case, underscores, no numbers unless there is a second angle on the same job. The id is typed into four places and each one has to match exactly:

1. Frigate — go2rtc pulls the camera once; the camera block reads the restream (from /mnt/d/frigate/config/config.yml)
go2rtc:
  streams:
    cat_water_bowl:
    - rtsp://<CAMERA_USER>:<CAMERA_PASSWORD>@192.168.1.<N>:554/stream1
    cat_water_bowl_sub:
    - rtsp://<CAMERA_USER>:<CAMERA_PASSWORD>@192.168.1.<N>:554/stream2

cameras:
  cat_water_bowl:
    ffmpeg:
      inputs:
      - path: rtsp://127.0.0.1:8554/cat_water_bowl_sub
        roles:
        - detect
      - path: rtsp://127.0.0.1:8554/cat_water_bowl
        roles:
        - record
    detect:
      width: 640
      height: 360
      fps: 2
      enabled: true
    audio:
      enabled: false
    objects:
      track:
      - cat
      - person
2. The public proxy — the allowlist is a list of ids with labels (pm2 env for helen-cam-proxy)
HELEN_CAMS=cat_water_bowl:Water Cam,cat_food_bowl:Food Cam,cat_treat_dispenser:Treat Cam,cat_scratching_pad:Scratch Cam
3. The Shorts pipeline — a station is a place, a camera is an angle on it; ids resolve by alias (helen_multiangle.py)
STATIONS = {
    "water": {
        "title": "Water Cam",
        "touch": "her water bowl",
        "aliases": ("cat_water_bowl", "water_bowl", "waterbowl", "cat_water", "water_cam"),
        "weight": 3.0,          # the drinking shot is the money shot
        "always_keep": True,    # eligible at any length, as today
    },
    # ...
    # cat_scratching_pad was re-aimed into an OVERHEAD of the whole room.
    # The Frigate id is legacy; the station is "court".
    "court": {
        "title": "Court Cam",
        "touch": "her court",
        "aliases": ("cat_scratching_pad", "scratching_pad", "scratch_pad",
                    "cat_scratch", "scratch_cam", "scratching_post",
                    "court_cam", "overhead"),
        "weight": 1.5,
        "always_keep": False,
    },
}

HOUSE_DENY = ("front_door", "back_yard", "driveway", "porch", "remote_", ...)   # ignored at discovery
NEVER_PUBLISH = frozenset({...})   # bedroom cameras: may record, may never publish. Checked first.
4. The 24/7 mosaic — one scheduled task per tile, id and port as arguments
powershell -ExecutionPolicy Bypass -File C:\ProgramData\HelenLive\tile-live.ps1 -Cam cat_water_bowl -Port 5103

The pipeline is the forgiving one: it reads the camera list from Frigate's /api/config at every run and matches ids against aliases, so a second angle named cat_water_bowl_side is picked up as the Water station with no code change. The proxy is the strict one: an id not in HELEN_CAMS is a 403, full stop. Discovery is meant to be easy and publication hard.

House versus public: the allowlist is a privacy boundary

Three of the eleven cameras watch the house: a front door, a carport, a back yard. They are in Frigate because Frigate is a good NVR and the same disk is already spinning. They differ from the Helen cameras in four ways, and every one of the four is a fence:

  • No go2rtc entry. Frigate pulls them straight from the camera. Nothing else on the box can ask for them by name on :8554, so no tile script, no proxy and no pipeline can restream one by accident.
  • detect: enabled: false. No events, so nothing for the Shorts pipeline to find even if it looked.
  • Not in HELEN_CAMS, not in LIVE_CAM_PATHS. The two allowlists on the public-proxy page refuse them by name.
  • In HOUSE_DENY. The pipeline ignores them at discovery, and any camera that is a bedroom is in a separate deny list that is checked before anything else and cannot be overridden by an alias.

The Helen cameras have their own gradations. Face is on YouTube and in the mosaic but not on the website. Bed is in the mosaic and nowhere else. Playroom is in Frigate and nowhere else. The rule that generates all of this is the one on the cameras page: no bedrooms, no people in frame, one sitting room. A camera's reach is a decision made once, in the table, and the configs are just the table written out.

What to block at the router

One caveat: we have not audited this end to end on our own router, so it is written as advice rather than as a description of our box. Consumer Wi-Fi cameras phone home. Tapo cameras register with TP-Link's cloud so the app works from anywhere, and they will keep trying whether or not you use the app. RTSP on the LAN does not need any of that. So:

  • Block the cameras' outbound internet at the router, by reservation. Most routers can do this per device ("parental control", "block internet access", or a firewall rule from the camera's address to WAN). RTSP to the NVR host keeps working and the app's remote view stops.
  • If your router does VLANs, put the cameras on one with no route to WAN and a single allow rule to the NVR host on TCP 554. If it does not, the per-device block above is most of the benefit.
  • Keep the NVR host's Frigate port off the WAN. No port forward to :5000 or :8554, ever. The public site reaches Frigate over Tailscale; see the proxy page.
  • Firmware updates then need the block lifted for an afternoon. Write that down next to the table so it is a chore and not a surprise.

The one credential

Every camera gets the same purpose-made RTSP account, created in the Tapo app as a "camera account" (TP-Link's FAQ 2680) and not the login you use for the app. It lives in a password manager and in exactly one file: the go2rtc block of config.yml, plus the direct inputs of the three house cameras. Everything downstream, from Frigate's own camera blocks to the tile scripts to the proxy, reads rtsp://127.0.0.1:8554/<id> or http://<TAILSCALE_IP>:5000 with no camera password at all. That is the practical reason to put go2rtc in front of the cameras even if you never restream anything: the secret is written once, and a config backup of any other file on the system contains nothing worth stealing. On this site it is always <CAMERA_USER> / <CAMERA_PASSWORD>.

Two checklists

Before a camera goes into Frigate
CheckHow
Job written on tape on the cameraWater, Food, Treat, Court, Face. Before it goes on the wall.
Row in the tableJob, id per the schema, reserved address, public label or "none".
DHCP reservation madeRouter → reservations → the camera's MAC → the address in the row. Reboot the camera and confirm it comes back on that address.
Camera account createdThe shared RTSP account, same on every camera; from the password manager, not from memory.
Both streams open in VLC/stream1 and /stream2 at the reserved address. Which-camera page, step five.
Outbound blocked at the routerPer-device block, then confirm VLC still works.
go2rtc entry, then camera blockTwo entries under go2rtc: streams: (<id>, <id>_sub), then the camera block reading 127.0.0.1:8554. House cameras: camera block only, direct URLs, detect: enabled: false.
Frigate agrees/api/stats shows the id with camera_fps ≈ 2 for a Helen cam and skipped_fps 0.0.
Before a camera goes public (most never do)
CheckHow
The table says publicIf the row says "none", stop. Change the table first, on purpose, and say why.
Nothing private in frameSit with the live view for a day. Entrances, windows onto neighbours, a person's chair. The mosaic dropped the Court pane for the Face pane for exactly this reason.
Proxy allowlistAdd <id>:<Label> to HELEN_CAMS, restart the proxy.
Site allowlistAdd the path to LIVE_CAM_PATHS, deploy the backend. A second, separate change, so it cannot happen by accident.
Pipeline stationConfirm station_of(id) is a real station and may_publish says yes; both fail closed for anything unknown.
Refusals still refuseEvery other id still returns 404 from the site and 403 from the proxy.