Scenario

The Home Automation Bridge

Doorbell, Grafana, Pi-camera — all into MeshHold.

Bridge GPIO sensors, alerting platforms, and cheap IP cameras into your private mesh — without exposing any of them to the public internet.

Pi or Pi-class hardware? The .deb package is the path of least friction on Raspberry Pi OS (Bookworm and later). If you'd rather keep your Pi's host clean and run everything from containers, the meshhold:latest-arm64 Docker image is the same daemon and lets you share the SD card root with Home Assistant and friends without rpm/deb conflicts. The auto-answer + call_media config below works in either deployment — only the install command differs.

Inbound webhooks

POST/GET /api/v1/hooks/<token> — endpoints in the shape of Slack / Mattermost / Discord. Configured in config.yaml only — there is no CRUD UI; rotation = edit YAML + restart.

call action — fire a video call

webhooks:
  - name: "doorbell"
    token: "long-random-token-1"
    action: "call"
    target_node_id: "12D3KooW..."
    video: true

Wire a GPIO button to curl /api/v1/hooks/long-random-token-1 and the doorbell calls your phone.

message action — post to a chat room

  - name: "grafana-alerts"
    token: "long-random-token-2"
    action: "message"
    room_id: "vault-uuid-2"
    fallback_text: "Alert (no payload)"

Parses Slack-shape JSON, payload=<json>, and text/plain bodies.

Headless Pi-camera (auto-answer)

The same daemon runs in call_media mode on a Raspberry Pi:

node:
  call_media:
    enabled: true
    video_devices:
      - "/dev/video0"
      - "/dev/video2"
    audio_device: "default"
    # video_input_format defaults to "mjpeg" — leave it unless your
    # camera is raw-only (see the capture-format note below).

Capture format (why HD video may not appear). The daemon captures with ffmpeg's -input_format mjpeg by default. Almost every USB UVC webcam can only deliver 720p/1080p at 30 fps as Motion-JPEG; raw YUYV at those sizes exceeds USB 2.0 bandwidth, so the camera caps at ~10 fps and ffmpeg's default raw capture stalls or throws Input/output error — the call connects with audio only and no video. Check what your camera actually supports with v4l2-ctl --list-formats-ext. If it exposes only a raw format (no MJPG), set video_input_format: "yuyv" (or the listed fourcc); use "auto" to let ffmpeg choose. When video capture fails mid-call the daemon now logs headless: video capture ended unexpectedly at warn level instead of silently dropping video.

Issue an mgmt-key with the camera cap; the holder can place video calls and the Pi auto-answers. Remote camera switching is supported (NEXT / PREV / SPECIFIC requests). Digital zoom + pan over a call applies to browser-based endpoints — the headless ffmpeg pipeline here streams the full frame and does not crop, so zoom requests to this Pi are a no-op.

Device permissions. call_media shells out to ffmpeg, which opens /dev/video* and the ALSA devices directly. On a .deb/.rpm install the daemon runs as the unprivileged meshhold system user, which must belong to the video (camera) and audio (microphone) groups — those device nodes are root:video / root:audio, mode 0660. Packages from v0.7.179 onward add this membership automatically in the post-install hook. On an older install, or when you run the daemon under a custom user, grant it yourself and restart so the new groups take effect (membership is only read at process start):

sh sudo usermod -aG video,audio meshhold sudo systemctl restart meshhold

Verify the daemon user can actually open the camera before debugging further up the stack — this is the most common reason a call connects but carries no remote video:

sh sudo -u meshhold ffmpeg -hide_banner -loglevel error \ -f v4l2 -i /dev/video0 -t 1 -f null - && echo "camera OK"

ffmpeg and a libvpx-vp9 encoder must also be present (ffmpeg -encoders | grep vp9); install the distro's ffmpeg package if missing.

Pi as a "baby monitor"

Same setup as above with a single static video_devices entry and a mgmt-key shared with the parents' phones.