Ctrl K
ComunidadGuías…

README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
***English** | [Français](README.fr.md)*

# blink2video

**Manage your Blink cameras from a computer, and keep what they record.**

Blink is built for the phone: one clip at a time, no archive, no desktop
counterpart. Recordings live on a USB stick in a Sync Module 2 or a microSD card
in a Sync Module XR, erased as it fills up, or in the subscription cloud, which
keeps them for a few weeks.

blink2video is the missing side. A local interface to watch cameras live and arm
detection. And what the phone cannot do at all: fetch clips before rotation
erases them, burn the time into the picture,
and assemble them into one video per day, per week and per month.

Everything runs on your machine. Nothing is sent anywhere.

The web interface listens on `127.0.0.1` only: nothing else on your local
network can reach it, just this machine. It has no login of its own beyond
the Blink account session, so anyone with access to this machine can open it.

## Video walkthrough

[![Watch the blink2video walkthrough: Blink on your PC, live view and archives](https://i.ytimg.com/vi/6rNqLI9K8Tc/hqdefault.jpg)](https://www.youtube.com/watch?v=6rNqLI9K8Tc)

**[Watch the English walkthrough on YouTube](https://www.youtube.com/watch?v=6rNqLI9K8Tc)**:
3 min 10 s, with captions and no voice-over. See why blink2video exists,
explore live view and archives, then follow Windows installation, first launch
and the main settings.
[Version française](https://www.youtube.com/watch?v=V2E4m_SLgb4).

## Features

- Live view of any camera in the browser, arming the system or a single camera.
- Camera audio in Live View and recordings (Settings → Video), starting muted
  with page-scoped listen controls.
- Live view recording on demand, one click while watching: saved to its own
  archive, browsable and filterable exactly like detection clips (discard,
  delete, camera and period filter).
- On-demand camera snapshot, one click from the Live tile or triggered
  remotely through a webhook URL (home automation, a smart button): saved
  under Pictures, browsable and deletable from the page.
- Optional outbound webhook (Settings): a plain HTTP POST with the camera
  name and file path, sent once a clip or picture has actually finished
  writing, for any home-automation tool that already accepts one.
- Incremental download of motion-detection clips from the module's local storage
  (Sync Module 2 USB stick or Sync Module XR microSD card)
  and from the subscription cloud, never fetching the same recording twice.
  Download history is also kept beside `Blink_Clips`, so deleting that folder
  does not download old recordings again or announce them as new.
- Camera state at a glance: battery, temperature, signal, model and firmware
  for each camera.
- Date and time burned into the picture, so any player keeps it. Size,
  color, background opacity and 24h or 12h (AM/PM) clock all configurable.
- One video per day, per ISO week and per month, for each camera.
- Uninteresting clips discarded in one click, moved aside rather than deleted,
  and never downloaded again.
- Continuous monitoring and alerts: camera offline, low battery, detection
  switched off, or nothing recorded for two days. Alerts to acknowledge,
  per-camera muting.
- French and English throughout: web interface, notifications, and every
  command's own console output. Detected from your browser, switchable from
  the page.
- Standalone bundle for Windows, Linux and macOS, ffmpeg included.

## Screenshots

![The Live tab](Screenshots/serve_direct.PNG)

Live view: one tile per camera, its latest thumbnail, arming for the system and
for each camera, battery, temperature, signal and the time of the reading. An
offline camera keeps reporting its last known values, and the interface says how
old they are.

![Recording the live view](Screenshots/direct_record_en.PNG)

The Record button, mid-recording ("Stop recording", in red): one click to start
saving the current stream to `Blink_Direct`, one to stop. Recordings appear
under Live Recordings, browsable and filterable exactly like detection clips.

To include camera audio in live listening and recordings, enable
**Settings → Video → Include camera audio in Live View and recordings**.
This is off by default and applies to new sessions. Audio-enabled sessions
use compatible MSE mode, which can take longer to start. Use **Listen/Mute**
in Live View or the saved video's volume controls. If the primary channel
stays silent, try **Alternate** in Settings. A missing audio track still
produces playable video.

Playback starts muted. Unmuting a video shares that choice and volume with
other videos on the same page, including players recreated while scrolling.
Changing views, gallery pages or filters, or reloading the page, resets playback
to muted. Playback mute does not change the audio saved in a recording.
Previously saved silent recordings remain silent.

![The Clips tab](Screenshots/serve0.PNG)

Clips newest first. Each card gives the camera, the duration, the date and
the model, and the "Écarter" button removes the clip from every assembled
video.

![The Filter panel](Screenshots/filtre.PNG)

One Filter button covers camera and time period together: quick presets
(today, this week, this month, last 2 months, or full history) or a precise
custom range down to the hour, to find a specific incident without endless
scrolling through an always-armed camera's history. The active filter stays
remembered between visits.

![The Settings panel](Screenshots/settings.PNG)

Settings, behind the gear icon: automatic startup with the session, automatic
page refresh, server port, videos folder with a native folder picker, and local
storage and cloud polling cadence. Also timestamp burned into the picture
with its size, color, background opacity and 24h/12h clock, time zone, the live view
protocol (WebRTC or MSE), daily/weekly/monthly archiving toggled
independently per period, per-camera alert muting,
automatic deletion after download per camera, and a button to stop the
whole thing without touching the command line.

## Getting started

**1. Install.** Download the archive for your system from the
[latest release](https://github.com/nico579/blink2video/releases/latest) and
unpack it. ffmpeg travels inside the bundle; nothing is installed system-wide.

| System | Archive | First run |
|---|---|---|
| Windows 10/11, x86-64 | `blink2video-windows-x86_64.zip` | `blink2video.exe` from a terminal |
| Linux x86-64, glibc 2.35+ (Ubuntu 22.04+, Debian 12+) | `blink2video-linux-x86_64.tar.gz` | `chmod +x blink2video`, then `./blink2video` |
| macOS 12+, Apple Silicon | `blink2video-macos-arm64.zip` | `xattr -dr com.apple.quarantine blink2video`, then `./blink2video` |

The archives embed WebRTC, the default live view protocol. On macOS the WebRTC
libraries need macOS 14 or later: on macOS 12 and 13 blink2video starts
normally and the live view falls back to MSE.

Windows 7 SP1 x64 has a [separate legacy build](WINDOWS7.en.md), published
alongside every [release](https://github.com/nico579/blink2video/releases/latest).
For native WebRTC, use Supermium 144 R5 or Thorium Legacy 122, both tested
on real cameras; keep MSE with Firefox ESR 115. See the
[tested browser matrix](WINDOWS7.en.md#which-browser-should-i-use-for-webrtc).

**2. Run it.** Double-click the executable (or `./blink2video` from a terminal).
No arguments needed: with no valid session yet, a browser tab opens by itself on
a sign-in page: your address, your password, then the code Blink sends. Only a
session token is kept, never the password. On the very first run, the Settings
panel then opens automatically. Check the videos folder and time zone in
particular, then click “Apply”: no clip is downloaded before that confirmation.
Monitoring, clip downloading and video assembly then start at their own pace,
and clips appear as they come in. This first-run flow is not repeated when the
videos folder is changed.

If the tab didn't open, or you closed it, `blink2video open` brings it back.

A tray icon also appears when `start` runs (Windows/macOS; Linux depends on
your desktop's tray support), with Open/Restart/Stop, no terminal needed.

**3. Settings.** The gear icon, top right, opens a panel: start automatically
when you log in, refresh the page on its own as clips arrive, how often local
storage and cloud are checked, and a button to stop everything. `blink2video autostart on`
and `blink2video stop` do the same from a terminal, if you'd rather.

<details>
<summary>From source, with Python 3.11 or newer</summary>

```bash
git clone https://github.com/nico579/blink2video
cd blink2video
python blink2video.py login
```

The first run creates an isolated environment in `~/.blink2video/venv` and
restarts there. `blink2video smoketest` then checks that everything works on your
machine.

The published binary is neither signed nor notarized: on macOS, Gatekeeper
refuses the first run of an archive downloaded through a browser, hence the
`xattr` in the table. Right-click then "Open" does the same. If you see
"Apple could not verify [...] is free of malware", that's this check, not an
actual malware detection: it just means no $99/year Apple developer account
signed the binary.

</details>

<details>
<summary>With Docker</summary>

```bash
git clone https://github.com/nico579/blink2video
cd blink2video
docker compose up -d
```

Or, without cloning, using the image published on
[Docker Hub](https://hub.docker.com/r/nico579dock/blink2video):

```bash
docker run -d --name blink2video --restart unless-stopped \
  -p 127.0.0.1:8765:8765 -e BLINK_TRUSTED_LOOPBACK_PROXY=1 \
  -v blink_data:/data nico579dock/blink2video
```

Open `http://127.0.0.1:8765` and sign in the same way. Settings, the session
and clips persist in a named volume (`blink_data`), separate from the image,
so rebuilding the image never loses anything.

The bundled `docker-compose.yml` publishes the port to `127.0.0.1` only,
matching the binary's own default: there is no login on the dashboard itself,
so nothing beyond this machine can reach it unless you deliberately widen the
port mapping yourself.

</details>

<details>
<summary>Reaching it remotely (reverse proxy)</summary>

Because the dashboard has no login, the server only accepts requests whose
`Host` header is `127.0.0.1`, `localhost` or `::1`, on top of requiring the
connection itself to come from this machine (a check `BLINK_TRUSTED_LOOPBACK_PROXY=1`
relaxes for the Docker bridge above, see its comment in `docker-compose.yml`).
A reverse proxy on the same machine satisfies the second part automatically,
but by default it forwards the `Host` your browser sent (its own public name
or LAN address), not `127.0.0.1`, and still gets refused with a 403 until
it's told to rewrite it. A plain TCP/L4 proxy (`socat`, a router's port
forwarding) can't do that rewriting at all, it never gets to see HTTP
headers, so it cannot reach this server no matter how it's configured; the
fix always needs an HTTP-aware reverse proxy.

The browser also sends an `Origin` header on anything beyond a plain page
load (saving a setting, stopping the monitoring, muting an alert...), and
the server checks that one too, the same way. A proxy that only rewrites
`Host` leaves `Origin` at the address the browser actually used, so the page
itself loads fine but every one of those actions still gets refused. Both
headers need rewriting.

Caddy:

```
your.domain.example {
  reverse_proxy 127.0.0.1:8765 {
    header_up Host 127.0.0.1
    header_up -Origin
  }
}
```

nginx:

```
location / {
    proxy_pass http://127.0.0.1:8765;
    proxy_set_header Host 127.0.0.1;
    proxy_set_header Origin "";
}
```

Put your own authentication (basic auth, a client certificate, an access
list) in front of either, the proxy is what's reachable from the network,
blink2video itself still has none.

With a mesh VPN (Tailscale, WireGuard) instead of a public domain, the same
rewrite still applies, the check only ever looks at the `Host` header, never
at how the connection got there:

```
http://100.x.y.z:9765 {
  reverse_proxy 127.0.0.1:8765 {
    header_up Host 127.0.0.1
    header_up -Origin
  }
}
```

(`100.x.y.z` is this machine's Tailscale address, from `tailscale ip`; the
explicit `http://` keeps Caddy from provisioning a certificate nobody asked
for.) No separate authentication needed in this case: only your own devices
on that network can reach the address at all.

That also means the rewrite above isn't needed at all on a mesh VPN: bind
blink2video directly to the tunnel address with `BLINK_BIND`, then set that
same address as the "Trusted host" in Settings, or pass it once with
`--trusted-host 100.x.y.z` (saved from then on, like any other setting).
blink2video then accepts requests addressed to it directly, no proxy in
between. Only ever trust that one specific address, and keep `BLINK_BIND`
on it too, never `0.0.0.0`, which would accept the same `Host` from the LAN
as well and defeat the point.

Trusted host also accepts a whole subnet in CIDR notation, e.g.
`192.168.1.0/24`, for a client without a fixed address (a Windows machine on
DHCP, say) rather than one specific VPN peer. This is a different, wider
guarantee: any device already on that subnet gets the same unauthenticated
access, not just the one machine a VPN tunnel would let through. Reasonable
on a trusted home LAN, never on anything you don't fully control.

An exact trusted host (an IPv4 address or hostname, not a CIDR subnet) also
allows embedding the page in an `<iframe>` from that host, whatever port
the embedding page is served on, e.g. a home automation dashboard like
ioBroker on 8081 or 8082. Without one, the page always refuses to be framed
at all (`Content-Security-Policy: frame-ancestors 'none'`), the standard
defense against clickjacking. A CIDR subnet never unlocks this, there's no
way to name "any host on this subnet" in that policy, and neither does an
IPv6 address, which that policy can't express either: use a hostname.

Trusted host takes several values, comma-separated, to combine these cases:
`192.168.1.10, 192.168.1.0/24, my-pc` lets the dashboard at `192.168.1.10`
embed the page, and accepts direct access from the whole subnet and through
the name `my-pc`. Each entry must match what the browser actually sends as
`Host`: a name typed in the address bar arrives as that name, never as the
IP it resolves to, so no subnet entry covers it: list the name too.

</details>

## The interface

The page, at `127.0.0.1:8765`, has six views. Each browser reopens on the last
one it used, so a wall screen can come back to Live while the browser on your
desk opens on Clips. To tell one installation from another, Settings, General,
"Page header" sets a title and an icon (an emoji), shown at the top of the
page and in the browser tab.

- **Live**: one tile per camera, its state, detection arming, a fullscreen
  button, a "Wake" button that forces the camera to check in right now for
  fresher battery/temperature/thumbnail readings instead of waiting for its
  next scheduled one, and a "Snapshot" button that does the same but keeps
  the resulting picture under Pictures instead of discarding it, also
  refreshing the tile's own thumbnail with it right away (both use a bit
  of battery, can take up to two minutes on a sleeping camera). A live
  view stops on its own: as soon as you close or reload the tab, after
  5 minutes at most in any case (a server-side cap, whatever the browser
  does), or earlier if you set "Auto-stop live view" in Settings.
  Offline cameras come last in each system, since there is nothing to watch
  on them.
  The "Rotation…" button shows the cameras one after another in
  fullscreen, for a wall screen for instance: each one stays for the
  number of seconds you choose (30 by default), counted from its first
  image, then the next one takes over, and the rotation starts again.
  Offline and hidden cameras are skipped, you can leave others out, and
  you can limit it to hours of the day; outside them it waits without
  waking anything. Esc or "Stop" ends it. Each turn wakes the camera, so
  battery cameras drain fast, and while it runs the local storage (USB)
  downloads wait for a pause or the end, since the Sync Module relays one
  thing at a time. These settings are kept in the browser, like the
  thumbnail size. A rotation still running when the page closes starts
  again when it reopens, after a wall screen restarts or an update reloads
  the page; one you stopped yourself does not. A browser only allows
  fullscreen after a click, so a rotation that resumes on its own fills the
  window instead: for a wall screen, start Chromium or Edge in kiosk mode
  (`--kiosk`), and the window is the whole screen.
- **Clips**: newest first, with a preview and an "Écarter" button that removes a
  clip from every assembled video.
- **Daily, Weekly, Monthly**: the assembled videos. Daily can also be grouped
  by day instead of by camera (Filter, "Group by"), to see every camera's
  video for the same date together.
- **Pictures**: on-demand snapshots, newest first, with a delete button.
  Filled by the Live tile's "Snapshot" button, or remotely through a webhook
  URL meant for home automation or a physical smart button (Settings,
  "Webhook picture", has the URL and lets you regenerate its secret).

The Refresh button fetches new clips and rebuilds the videos, showing progress.

<details>
<summary>Webhook picture, in detail</summary>

`GET /webhook/snapshot?camera=<name>&token=<secret>` triggers a snapshot the
same way the Live tile's Snapshot button does. It answers with a small JSON
body (`{"ok": true, "fichier": "..."}`, or `{"error": "..."}`), not an image:
fetch the picture itself afterward from the Pictures view or `/api/snapshots`.

The secret travels in the URL rather than a header because none of the usual
callers (Home Assistant, IFTTT, a GET-only smart button) can set a custom
header on a webhook call; this matches how those services' own webhooks work.
It is generated once, shown (and regenerable) under Settings, checked in
constant time against what's supplied: treat it like a password, since
whoever has it can trigger a photo on any camera by name. Unlike the rest of
the dashboard, this one route is reachable from anywhere on the network on
purpose, since a smart-home hub is rarely running on `127.0.0.1`; keep it off
the public internet all the same, a LAN-only caller is the intended use.

</details>

<details>
<summary>Status and arming webhooks, in detail</summary>

Two more routes for scripts (home automation, a scheduler of your own), with
the same secret as the picture webhook, so there is only one to keep safe.

- `GET /webhook/status?token=<secret>` answers with the state of every system
  and camera as JSON; add `&camera=<name>` for one camera. Each camera has
  `name`, `system`, `armed` (its detection), `online`, `status`, `battery` (a
  status such as `ok`, not a percentage), `battery_signal`, `voltage`,
  `temperature_c`, `wifi`, `firmware`, `model` and `age_seconds` (how old the
  reading is). The fields are those Blink gives: a camera that does not report
  one (a plugged-in Mini has no battery) shows `null`, and Blink has changed
  such fields before, so a script should tolerate a missing value.
- `GET` or `POST /webhook/arm?camera=<name>&armed=true&token=<secret>` arms or
  disarms one camera; use `system=<name>` instead of `camera` for a whole
  system. `armed` accepts `true`/`false`, `1`/`0`, `on`/`off`. The answer is
  `{"ok": true, "requested": ..., "changed": ..., "applied": ...}` with the
  camera or system as Blink reports it right after. `changed` is false when it
  was already in the requested state (as blink2video knew it before the call);
  `applied` is false if Blink had not caught up yet. An offline camera is not
  tried: you get `409` with its state and a clear message instead of waiting for
  a timeout. An unknown name is `404`, a name shared by two items is `409`, a
  missing or wrong secret is `403`.
- Several cameras in one call: repeat the parameter,
  `camera=Garage&camera=Loft&armed=false`. The answer is
  `{"ok": ..., "scope": "cameras", "cameras": [...]}` with one result per camera
  (its own `ok`, `status_code`, `changed`, `applied` or `error`), always with
  status `200`: an offline or unknown camera does not stop the others, and the
  overall `ok` is true only if all of them succeeded.

Treat the secret like a password: whoever has it can now also switch your
cameras off, not only take a picture. Every change made through the arming
webhook is appended to `armement-webhook.log` in the data folder, with the
caller's address. Same advice as for the picture: keep it on your local network.

</details>

<details>
<summary>Webhook notification, in detail</summary>

The opposite direction from the one above, and unrelated to it: `POST` a
small JSON body (`{"camera": "...", "chemin": "...", "type": "clip"}` or
`"snapshot"`) to a URL you set under Settings, once a clip or picture has
actually finished downloading or saving, never mid-transfer. Empty URL
(the default) disables it entirely.

Best-effort, no retry: a dropped notification, a typo'd URL, or the
receiving endpoint being briefly down never holds up or fails the download
it's reporting on. A plain HTTP POST is the target on purpose, since that's
what Home Assistant, Node-RED, n8n and similar tools already know how to
receive, no MQTT broker or custom UDP listener needed on your end.

</details>

## Being warned

A camera going offline, a fading battery, detection switched off or a camera
silent for two days opens a dialog you must acknowledge. Alerts fire on change
only, so a camera you knowingly leave offline warns you once.

## Updating

When a newer release exists, the interface shows an **Install 0.x.y** button.
It does everything: download, stop, replace, start again, with whatever verbs
were running. The downloaded version must first start and state its own
version number; the whole folder is then swapped, and if the new version does
not hold after the restart, the previous one is put back.

If a power cut strikes at the wrong moment and the `blink2video` folder is gone,
the previous version is waiting next to it as `.blink2video.backup-…`: rename
that folder to `blink2video` and start again. Your settings and clips do not
depend on it, they live elsewhere.

## Where files go

```
Blink_Clips/       raw clips, as the module recorded them
Blink_Normalized/  the same with the time burned in
Blink_Excluded/    discarded clips, kept rather than deleted
Blink_Daily/       one video per camera per day
Blink_Weekly/      one per ISO week
Blink_Monthly/     one per month
Blink_Direct/      live view recordings, saved on demand
Blink_Snapshots/   on-demand pictures, one per Snapshot click or webhook call
```

In `Documents/blink2video` by default, or in the videos folder chosen in
Settings. Settings, the Blink session and logs live apart, in the system's
application data folder (`%LOCALAPPDATA%\blink2video` on Windows,
`~/Library/Application Support/blink2video` on macOS,
`~/.local/share/blink2video` on Linux): an update or a reinstall never touches
them, and the source and executable versions share them. `BLINK_HOME` puts
everything in one folder of your choice. Coming from 0.13 or earlier, the first
launch copies the settings and the session over, and the videos stay where
they are: the videos folder points to them.

### Videos on a separate disk

A folder chosen in Settings is only ever created at that moment. If it is
missing later, say the disk is not mounted yet at boot, blink2video does not
recreate it on the system disk: downloads wait for it and start as soon as it
is back, assembling skips its turn, and a red banner names the folder in the
page. Settings stay usable meanwhile, and you can point them elsewhere.

Choose a subfolder of the disk, such as `/export/cam/blink2video`, rather than
the mount point itself: an unmounted mount point is still an empty folder, and
nothing can tell it from the real one. The default folder and `BLINK_HOME` are
still created on demand.

### Keeping files for a limited time

Nothing is ever deleted by default. In Settings, "Retention" gives each
category its own number of days: raw clips, daily, weekly and monthly videos,
live recordings and snapshots. 0 means keep forever. For example, raw clips
30, daily videos 90, monthly videos 0 keeps a month of raw footage, three
months of days, and every month for good.

The assembled videos are made from one another, which sets an order. A daily
video is rebuilt as long as its raw clips exist, and a weekly or monthly one
as long as its daily videos do, so each one can only be deleted after what it
is made of. Raw clips go a whole day at a time, and only once that day's
video is made; a daily video goes once its raw clips are gone and the weekly
and monthly videos that contain it exist; a weekly or monthly video goes once
none of its daily videos remain. Settings refuse an order that could not hold.
Live recordings and snapshots simply follow the date of each file.

The cleanup runs with the assembly, every five minutes, and also on its own
when daily assembly is turned off. Deleted clips stay in the download
history, so they are not downloaded again.

<details>
<summary>How it works</summary>

The normalized clip is the pivot. Each clip is re-encoded once, with the time
written into the picture, then kept for good. The daily, weekly and monthly
videos are only stream copies of those segments: no re-encoding, no generation
loss, a few seconds each.

That is what makes adding a clip cheap. A new arrival re-encodes only itself, at
roughly real time: a one minute clip costs about one minute, once.

The time is burned into the picture rather than added as a subtitle track, which
phones and messaging apps ignore. Time zone conversion happens on the Python
side, with ffmpeg running under `TZ=UTC0`: the chain depends on no system time
zone database.

A clip is identified by its camera and its recording instant, never by the
module's own identifier: restarting it renumbers everything, and clips already
downloaded would come back as new.

</details>

## Limits

- A camera out of range of the module accepts the live request but never sends a
  picture; the interface says so.
- On Linux, the imageio-ffmpeg binary is built without libfreetype and cannot
  burn the timestamp: `sudo apt install ffmpeg`. The tool tries every ffmpeg it
  finds and keeps the first that can write text. Windows and macOS need nothing.
- Notifications borrow each system's own tools: the native dialog on Windows,
  osascript on macOS, notify-send and zenity on Linux. Failing that, the watcher
  writes to `watch.log` and carries on. On Windows, the tool declares its
  notification identity on first use, in
  `HKCU\SOFTWARE\Classes\AppUserModelId\blink2video`: without it, the system
  drops notifications silently. Deleting that key undoes the declaration.
- Nothing runs while the computer is off: a camera failing at night is reported
  at the next logon.
- Blink refuses some API endpoints depending on the client version announced,
  with an "An app update is required" message. Live view and downloading work
  today; nothing guarantees Blink will not widen that refusal.
- Blink exposes no way to restart a stuck Sync Module: you have to unplug it.
- No iOS or Android build: continuous downloading and video assembly need a
  background process and ffmpeg, which neither mobile OS allows to run in the
  background. The web interface itself is just a page though: reaching it from
  a phone, tablet, or any other device on the LAN needs either a reverse proxy
  in front, forwarding to `127.0.0.1` with its own authentication and its
  `Host` header set accordingly, or, on a mesh VPN such as Tailscale/WireGuard,
  binding directly to that tunnel address and naming it as a trusted host
  from Settings instead (see "Reaching it remotely" above), the same pattern
  as the Docker container's own opt-in. The web UI has no login of its own,
  so the built-in server refuses any request that doesn't come from the
  local machine itself or from a trusted host; setting
  `BLINK_BIND=0.0.0.0` alone does not
  expose it to the LAN.

## Neighbours

Other projects touch the same hardware, and complement each other more than they
compete:

- [blinkpy](https://github.com/fronzbot/blinkpy) is the API library everything
  else builds on, including this.
- [BlinkCamWindowsDashboard](https://github.com/mikeoverbay/BlinkCamWindowsDashboard)
  offers a web dashboard and clip downloading, from the cloud only, so a
  subscription is required, and without live view.
- [blinkbridge](https://github.com/roger-/blinkbridge) exposes a camera over
  RTSP, to plug into an existing surveillance system.
- [blink-live-view](https://github.com/andreiele/blink-live-view) focuses on
  desktop live view.

blink2video differs on three points: it reads **both** sources, the module's local
storage and the subscription cloud, never fetching the same recording twice; it
burns the time into the picture and assembles one video per day, per week and
per month; and it runs on all three systems as a standalone bundle, ffmpeg
included.

## Command line

Everything above already works from the page. This chapter is for the terminal
alternative: scripting, a headless machine, a custom composition, or the full
list of options.

<details>
<summary>All options</summary>

<!-- verbes:début -->
One command, one verb per action. `blink2video <verb> --help` gives each one's options.

```bash
blink2video login       # sign in to the Blink account, two-step verification handled
blink2video list        # what the Sync Module currently holds
blink2video download    # fetch new clips before rotation erases them
blink2video merge       # normalize, stamp and assemble day, week and month
blink2video watch       # check the installation and alert when it degrades
blink2video serve       # serve the web interface, to watch, discard, see live
blink2video start       # start everything with the recommended settings
blink2video open        # open the web interface in the browser
blink2video stop        # stop the instance running in the background
blink2video restart     # stop then relaunch with the current settings
blink2video update      # install the latest published release
blink2video autostart   # register the command that follows with your session
blink2video smoketest   # check that the installation works on this machine
```
<!-- verbes:fin -->

Options follow the verb: `blink2video serve --port 8899`. Several verbs can be
named in a row, each with its own options. What finishes runs in sequence, what
does not finish runs alongside: `blink2video download merge` downloads **then**
assembles, while `serve`, or any verb given `--loop`, holds its own process until
`blink2video stop`.

`blink2video <verb> --help` is always authoritative; this table summarizes.

**Root**: `login`, `list`, `download`. With no verb, the help is shown.

| Option | Effect |
|---|---|
| `--hub NAME` | Sync Module to use; all modules when omitted |
| `--camera NAME` | keep only this camera |
| `--since DAYS` | keep only clips from the last N days |
| `--output FOLDER` | destination of raw clips (default `Blink_Clips`) |
| `--overwrite` | redownload previously processed clips, including after a purge |
| `--from usb\|cloud\|all` | where to look for clips: local storage (USB or microSD), the subscription cloud, or both (default); `usb` is retained as the historical CLI name |
| `--loop [MINUTES]` | repeat instead of acting once (default 10) |

**`blink2video merge`**: normalization and assembly.

| Option | Effect |
|---|---|
| `--exclude CLIP…` | discard clips: the raw file moves to `Blink_Excluded`, the segment is deleted, the clip is never downloaded again |
| `--include CLIP…` | undo a discard: the raw file comes back and the clip is re-normalized |
| `--date YYYY-MM-DD` | limit to one day |
| `--camera NAME` | limit to one camera |
| `--force` | rebuild everything even if nothing changed |
| `--no-weekly` | do not rebuild the weekly aggregates |
| `--no-monthly` | do not rebuild the monthly aggregates |
| `--keep-clips-days N`, `--keep-daily-days N`, `--keep-weekly-days N`, `--keep-monthly-days N`, `--keep-live-days N`, `--keep-snapshots-days N` | retention per category in days, 0 keeps forever (default); see "Keeping files for a limited time" |
| `--purge-only` | apply retention only, assemble nothing |
| `--no-timestamp` | do not burn the date and time into the image |
| `--preset NAME` | libx264 preset, from `ultrafast` to `veryslow` (default `veryfast`) |
| `--crf N` | quality, 0 to 51, lower is better (default 21) |
| `--font FILE` | .ttf font for the timestamp |
| `--font-size N` | timestamp size in pixels, 8 to 500 (default: scales with the video's height) |
| `--font-color COLOR` | timestamp color: a name (`white`, `yellow`...) or hex (`#RRGGBB`, `0xRRGGBB`), optionally with `@opacity` (e.g. `white@0.8`), default `white` |
| `--box-opacity N` | opacity of the band behind the timestamp, `0.0` (invisible) to `1.0` (opaque), default `0.55` |
| `--clock 24h\|12h` | time format of the timestamp: `24h` (`14:05:09`, default) or `12h` (`02:05:09 PM`) |
| `--timezone ZONE` | time zone of the timestamp (default `Europe/Paris`) |
| `--input`, `--output`, `--normalized-output`, `--excluded-output`, `--weekly-output`, `--monthly-output` | location of each folder |

**`blink2video watch`**: check the state, alert when it degrades. `--ignore
CAMERA…` mutes a camera, then carries on checking; `--unignore CAMERA…` undoes
it.

| Option | Effect |
|---|---|
| `--loop [MINUTES]` | repeat instead of acting once (default 10) |
| `--ignore CAMERA…` | mute a camera, then carry on checking |
| `--unignore CAMERA…` | unmute |
| `--test` | fire a verification notification |
| `--dry-run` | show without notifying or saving state |

**`blink2video start`**: the recommended setup in one command. It is exactly
equivalent to:

```bash
blink2video serve  watch --loop 10  download --from all --usb-loop 10 --cloud-loop 1  merge --loop 5
```

Options given after `start` go to the interface, `--port` for instance.

**`blink2video serve`**: serve the web interface.

| Option | Effect |
|---|---|
| `--port N` | listening port (default 8765) |
| `--open-browser` | open the page in the browser on startup |
| `--hub NAME` | Sync Module; all modules when omitted |
| `--thumbs FOLDER` | thumbnail cache, disposable |
| `--timezone ZONE` | display time zone |
| the same folder options as `merge` | |

**`blink2video open`**: open the interface in the browser, and say so when
nobody is listening. `--port` if you moved it.

**`blink2video stop`**: stop the running instance and all its verbs. No options.

**`blink2video autostart`**: what will start when you log in.

| Command | Effect |
|---|---|
| `autostart on` | register `blink2video start` |
| `autostart on <verbs…>` | register that command instead of the default |
| `autostart status` | what is registered, and what is running |
| `autostart off` | remove the entry |

`--dry-run` shows without changing anything.

**`blink2video smoketest`**: installation check. `--keep` keeps the working folder,
`--timezone` picks the time zone of the demonstration video.

**Environment variables**

| Variable | Effect |
|---|---|
| `BLINK_HOME` | one folder for everything: settings, session and videos, unless another videos folder is chosen in Settings. Without it, the system's application data folder and `Documents/blink2video` |
| `BLINK_BOOTSTRAP` | `auto`, `pip` or `none`: how the Python environment is handled |
| `BLINK_BIND` | internal address used by `serve`, defaulting to `127.0.0.1`. Needed to bind it inside the official Docker container, behind a `127.0.0.1` port publication and `BLINK_TRUSTED_LOOPBACK_PROXY=1` (see the Docker section), or to bind it directly on a mesh VPN address (Tailscale, WireGuard), the simplest way to reach it remotely without a reverse proxy: pair it with that same address as the "Trusted host" in Settings, or `--trusted-host` at the command line (see "Reaching it remotely" above). There is no authentication on the web UI, so the server refuses any request that doesn't come from the local machine itself or from a trusted host: setting this to `0.0.0.0` alone does not expose the UI to the LAN |

</details>

**First run**, one step at a time instead of the double-click:

```bash
blink2video login       # sign in, once
blink2video list         # check that it answers: clips held by the module
blink2video start        # same composition the double-click starts
```

**Grammar.** Three rules cover it. The verb first, its options after. Several
verbs can be named in a row, each with its own. What loops runs alongside, the
rest runs in sequence, in the order written. An option placed before the first
verb belongs to nobody, and is refused: `blink2video --loop 5 merge` will say so
rather than do something unexpected.

Everyday gestures:

```bash
blink2video start                  # everything, with the recommended settings
blink2video open                   # open the interface in the browser
blink2video stop                   # stop the background instance and its verbs
```

A single pass, leaving nothing running:

```bash
blink2video download               # both sources, once
blink2video download --from cloud  # one source only
blink2video download merge         # download, then assemble
blink2video download --since 7 merge   # catch up a week, then assemble
```

Redo one day, or discard a clip:

```bash
blink2video merge --camera jardin --date 2026-08-12
blink2video merge --exclude Blink_Clips/jardin/2026-08/2026-08-12_09-23-21Z_jardin.mp4
```

Compose your own, when the default does not fit:

```bash
blink2video serve --port 8899                       # the interface elsewhere
blink2video serve merge --loop 30                   # interface, and assembly every 30 min
blink2video watch --loop 5 download --from cloud --loop 1   # two loops, two paces
```

**Autostart.** The Settings checkbox does this for the recommended composition;
from a terminal:

```bash
blink2video autostart on                    # registers « blink2video start »
blink2video autostart status                # what is installed
blink2video autostart off                   # remove
blink2video autostart on watch --loop 30    # register the alerts only, instead
```

`autostart` runs nothing: it registers the command that follows it, exactly as
you would have typed it without the prefix. No administrator rights are needed,
and `--dry-run` shows what would happen.

On Linux, that entry is a systemd *user* service: it lives with that user's
session, starting when they log in and stopping when they leave. On a server
with a dedicated account that nobody logs into, enable lingering once, and the
service starts at boot instead:

```bash
sudo loginctl enable-linger blink2video     # the account that runs it
```

No system-wide unit is needed for that, and nothing has to run as root. An
account opened with `su` or `sudo` works too: `autostart on` finds its systemd
instance by itself, and says so when there is none yet.

<details>
<summary>Doing it yourself, without <code>autostart</code></summary>

**Windows**, a shortcut in the Startup folder:

```powershell
$s = (New-Object -ComObject WScript.Shell).CreateShortcut(
  "$([Environment]::GetFolderPath('Startup'))\blink2video.lnk")
$s.TargetPath = "C:\path\to\blink2video.exe"; $s.Arguments = "blink2video start"
$s.WorkingDirectory = "C:\path\to"; $s.Save()
```

Task Scheduler would also do, but its root folder requires elevation.

**macOS**, a launch agent in
`~/Library/LaunchAgents/com.nico579.blink2video.plist`:

```xml
<plist version="1.0"><dict>
  <key>Label</key><string>com.nico579.blink2video</string>
  <key>ProgramArguments</key>
  <array><string>/path/to/blink2video</string><string>serve</string><string>all</string><string>--loop</string><string>10</string><string>download</string><string>--from</string><string>cloud</string><string>--loop</string><string>1</string></array>
  <key>WorkingDirectory</key><string>/path/to</string>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
</dict></plist>
```

Loaded with `launchctl load`, stopped with `launchctl unload`.

**Linux**, a systemd user service in
`~/.config/systemd/user/blink2video.service`:

```ini
[Service]
ExecStart=/path/to/blink2video blink2video start
WorkingDirectory=/path/to
Restart=on-failure

[Install]
WantedBy=default.target
```

Enabled with `systemctl --user enable --now blink2video`, followed with
`journalctl --user -u blink2video -f`.

One launcher at a time: two give two watchers, and every notification twice.

</details>

**Updating.** The same thing the interface's button does:

```bash
blink2video update               # install the latest published release
blink2video update --check       # only say whether one exists
```

From a git clone, `update` runs `git pull` instead of downloading an archive.

To do it by hand, stop the instance first: it holds the interface port and
talks to the Sync Module.

```bash
blink2video stop                 # stops the instance and all its verbs
```

Then replace the folder with the new archive and start it again: the Startup
shortcut on Windows, `launchctl load` on macOS, `systemctl --user start
blink2video` on Linux. The next logon does it anyway.

`blink2video autostart status` says what is installed and whether an instance is
running. In a terminal, Ctrl+C is enough: `stop` exists for instances without a
console, which Ctrl+C cannot reach and whose verbs were left orphaned when only
the parent process was killed.

## Building

```bash
python build.py
```

Produces `dist/blink2video/`, about 110 MB, most of it ffmpeg. PyInstaller is not a
cross-compiler and the ffmpeg binary is platform-specific: each system needs its
own build. The release workflow handles that on GitHub runners.

## Code signing policy

The Windows executable is code-signed via [SignPath Foundation](https://signpath.org/),
free for open-source projects. Only the maintainer ([@nico579](https://github.com/nico579))
has commit access and controls the signing workflow, with two-factor
authentication required on that account. Signing is triggered from the public
release workflow ([release.yml](.github/workflows/release.yml)) against the
source tagged in this repository: no binary is signed outside that pipeline.

## Project status and responsible use

An independent project, used daily on Windows 10 against a real installation:
one module, four cameras, live view, arming and the archive. The Linux and macOS
executables are built and their video chain verified automatically, but they have
never run against real Blink hardware. Reproducible reports are welcome in the
issues.

Film responsibly. In France, the CNIL accepts watching your own home, but not
the public street nor a neighbour's doorway. This tool only keeps what your
cameras already record, but it makes the question more concrete by building a
lasting archive where the mobile app kept a few days.

That archive is personal data: images of your home, the presence patterns they
reveal, and a session file that grants access to your Amazon account. It all
stays on your machine, and the repository's `.gitignore` excludes those files.

## Licence, author and credits

Distributed under the GNU General Public License v3.0; see [LICENSE](LICENSE).
Consistency demands it too: the Linux bundle embeds a GPL build of ffmpeg, the
only one with libfreetype.

Designed and architected by Nicolas Martin ([@nico579](https://github.com/nico579)).
Code developed with the assistance of Claude (Anthropic) as a development tool.

blink2video also owes a great deal to the people who use it and take the time
to say what could be better. The ideas and reports of
[@MarkusKress](https://github.com/MarkusKress), who runs it headless on a Linux
server, shaped much of what it is today: on-demand snapshots and the webhook
that triggers them, a notice for each new clip or photo, use behind a reverse
proxy or inside a dashboard, the timestamp burned into the video, the automatic
end of live view and the grouping by day, an interface and command line fully in
English, and the updater and login autostart on Linux (see
[the issues opened by @MarkusKress](https://github.com/nico579/blink2video/issues?q=author%3AMarkusKress)).
[Joel Farthing](https://github.com/joelfarthing) wrote the first outside code
contribution: live view thumbnails and snapshots read from Blink's current state
([#24](https://github.com/nico579/blink2video/issues/24),
[#25](https://github.com/nico579/blink2video/pull/25)).

[blinkpy](https://github.com/fronzbot/blinkpy) provides access to the Blink API,
including the `immis` protocol without which live view would be out of reach, and
[ffmpeg](https://ffmpeg.org/) does all the video work. The notes in
[BlinkMonitorProtocol](https://github.com/MattTW/BlinkMonitorProtocol) helped to
understand what the API offers, and above all what it does not.

Not affiliated with Blink or Amazon. Blink is an Amazon trademark.