Recordings & export
How Vistralio stores video, how long it keeps it, how to review it in the dedicated playback page, and how to get it out.
How recording works
For each camera with a record path set, the worker spawns a long-lived
ffmpeg process that pulls the stream and writes segmented MP4 files to
<recordings path>/<camera_id>/. Filenames are timestamps:
2026-04-09_14-30-00.mp4.
Video is always copied (no transcoding) for efficiency. Audio is transcoded to whatever codec you configured (default AAC for browser compatibility).
Per-camera recording settings
Camera cog → Recording tab:
| Setting | Effect |
|---|---|
| Retention (days) | Files older than this are deleted by the hourly retention sweeper |
| Max segment length (seconds) | ffmpeg -segment_time. Default 600s = 10-minute files |
| Max segment file size (MB) | ffmpeg -fs cap. 0 means no cap (only time-based splitting) |
Why segment?
Lots of small files are easier to manage than a single rolling file:
- The retention sweeper can delete one file at a time
- Exports can concat just the segments inside the requested window
- A corrupt write only loses one segment, not the whole day
Audio in recordings
Camera cog → Audio tab:
| Codec preference | What it does |
|---|---|
auto |
Transcode to AAC 128k — most broadly compatible |
aac |
Same as auto |
opus |
libopus 96k — better quality per bit, but spotty Safari support |
mp3 |
libmp3lame 128k |
If the camera's audio codec is already AAC and you don't need to remux, set
the preference field via the API to copy for zero-CPU passthrough.
If "Record and stream audio" is off, the worker passes -an to ffmpeg
and the recording is video-only.
Per-camera worker architecture
Each camera runs inside its own systemd unit: vistralio-worker@{tenant_id}-{cam_id}.service. This means:
- A crash or restart in one camera's worker does not affect other cameras
- Each worker handles recording, detection, and retention for exactly one camera
- The Tenant Manager (
vistralio-tenant-manager.service) polls the database every 30 seconds to start or stop workers as cameras are enabled or disabled
Workers are named with composite IDs (e.g. vistralio-worker@1-12.service = tenant 1, camera 12) so you can see them in systemctl list-units "vistralio-worker@*".
Graceful shutdown
When a worker is stopped (e.g. during an update or configuration change), it closes the active ffmpeg recording process cleanly before exiting, allowing ffmpeg to write its moov atom and leave the segment file in a fully playable state.
Retention sweeper
Runs once per hour inside each camera's worker. For each camera it walks
<recordings path>/<id>/ and deletes any segment whose file mtime
is older than retention_days * 86400 seconds. Look for log lines:
vistralio.recordings retention: deleting /var/lib/vistralio/recordings/3/2026-03-25_02-10-00.mp4
The same hourly sweep also retries building event clips for any events that are missing one (e.g. events where the recording was still in-progress when the event finalised).
Storage locations
Vistralio now keeps storage-related settings in one place:
Admin -> Advanced -> System:- recordings folder
- snapshots folder
- exports folder
- event clips folder
- log folder
- approved storage roots
This keeps storage concerns out of the camera settings dialog. Camera settings control what gets recorded; system settings control where the files live.
Exports and event clips are intentionally kept separate from the main recordings folder so operators can clean up long-running recordings without accidentally removing saved exports or event evidence clips.
Exporting footage via the WebUI
- Click Export in the sidebar.
- Pick a date and a start time / end time.
- Tick the cameras you want.
- Decide whether to include audio.
- Click Export.
The page shows the job status. Once it flips to ready, download links
appear — one per camera. Previous jobs remain listed underneath the request
form so operators can revisit old exports without rebuilding them. The files
are real MP4s built by ffmpeg -f concat against the matching segments, with
audio stream copied or stripped according to your choice.
Dedicated playback page
Open Playback in the sidebar to review footage without opening a camera's live viewer. The page provides:
- A camera selector in the top-left
- A date picker for the day you want to inspect
- A large playback window
- A daily segment timeline at the bottom
- Playback controls for previous / play / next
- Speed presets: x0.2, x0.5, x1, x2, x5
The playback page is designed for operators who need to jump camera-to-camera quickly while staying in recorded footage review mode.
Timeline behavior
Each segment appears as a block on the timeline. Click a block, or click near one, to start playback from that point in the day. The timeline can be zoomed for tighter inspection.
Export jobs and the API
Behind the scenes the WebUI calls:
POST /api/recordings/export
{
"camera_ids": [1, 3],
"start": "2026-04-09T08:00:00Z",
"end": "2026-04-09T09:00:00Z",
"format": "mp4",
"with_audio": true
}
You can drive this from your own scripts:
curl -X POST https://cctv.example.com/api/recordings/export \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"camera_ids":[1],"start":"2026-04-09T08:00:00Z",
"end":"2026-04-09T09:00:00Z","format":"mp4","with_audio":true}'
# → {"export_id":"ab12cd34ef56","status_url":"/api/recordings/export/ab12cd34ef56"}
curl https://cctv.example.com/api/recordings/export/ab12cd34ef56 \
-H "Authorization: Bearer $TOKEN"
# When status == "ready":
curl -OJ -L "https://cctv.example.com/api/recordings/export/ab12cd34ef56/download?file=ab12cd34ef56_cam1.mp4" \
-H "Authorization: Bearer $TOKEN"
Permissions
| Action | Permission |
|---|---|
| Watch live + recorded video | recordings.view |
| Delete a recording | recordings.delete |
| Run an export | recordings.export |
Related endpoints
GET /api/recordings/segments?camera_id=&start=&end=— list segments in a windowGET /api/recordings/segment/{camera_id}/{name}— fetch one segmentGET /api/recordings/snapshot/{event_id}— snapshot JPEGGET /api/recordings/clip/{event_id}— short event clipPOST /api/recordings/export— start an export jobGET /api/recordings/export/{id}— poll statusGET /api/recordings/export/{id}/download?file=— download