---
title: DVR / Archive
url: https://doc.pstreamer.tv/en/manual/dvr.html
lang: en
product: Perfect Streamer
version: 1.13.3.445
---

# DVR / Archive

Since version 1.13, **Perfect Streamer** provides a built-in DVR — a persistent on-disk stream archive that operates in parallel with regular OTT delivery (HLS / DASH). The archive is written automatically, without a separate process, and is played back through the same OTT URLs as live — the only difference is the query parameter.

**Features:**

- Recording of each OTT stream to the archive on the selected storage.
- HLS, DASH and Low-Latency HLS playback of the archive (VOD) on the same URLs as live (DASH and LL-HLS — on CMAF, in *OTT/HLS/LL-HLS/LL-Dash*).
- Subtitle support (WebVTT) — written alongside TS chunks.
- Multiple storages — a stream is bound to one; different streams may write to different disks.
- Automatic cleanup by retention time and by disk usage.
- EPG-aligned VOD — archive delivery by EPG-event reference.
- Adaptive VOD — supported for adaptive groups.

**DVR requires no separate license.** It is enabled per stream by adding a storage binding.

DVR does not replace live broadcasting. If a stream has an archive, the client receives a live playlist with the same behaviour as without DVR. The archive starts playing only when the client explicitly requests VOD mode via a URL query parameter (see below).

## Storage configuration

A storage is a record in the **Configuration / DVR Storage** section. Each record describes one on-disk directory into which PSS writes archive files. A stream uses one storage.

When adding a storage, the following are configured:

**Name** — display name.

**Dir Path** — path to the on-disk directory. After record creation, the path **cannot be changed** — to move the archive to another disk, delete the record and add a new one with the new path. Existing files **are not touched on disk** when the record is deleted.

**Max Usage**, % — disk usage threshold (default 90 %). When exceeded, size-based cleanup activates (see below). Minimum 1 %, maximum 100 %.

**Cleanup Interval**, sec — cleanup task period (default 10 sec). On each tick, everything older than the stream retention depth is trimmed first; then, if **Max Usage** is exceeded, old chunks are removed.

**Disk Pressure Grace**, sec — how many seconds **Used %** must continuously exceed **Max Usage** before **Size-based cleanup** starts (default 60 sec). Filters out short spikes.

**Disk Pressure Cut**, sec — upper bound for one cleanup tick: how many seconds of video per stream may be deleted at once (default 300 sec). The remainder is carried over to the next tick.

**Disk Emergency Bytes** — free-space threshold below which the storage transitions to *Error* state and recording stops (default 2 GiB). Automatic recovery occurs when free space ≥ 2 × this value.

**Alarm Disk-Full Hysteresis**, % — the margin below **Max Usage** down to which size-based cleanup lowers the fill, so that **Used %** does not fluctuate right at the threshold (2 % by default).

Most default values suit typical installations; adjustments are typically required only for **Max Usage** and **Dir Path**.

**It makes sense to create one storage per disk.** If several records with different subdirectories are set on the same disk, they will compete for free space — the disk is shared, but cleanup is per storage.

## Binding a stream to a storage

In the **Stream / OTT** settings, a **DVR** section appears:

**Storage** — drop-down list of available storages; *0* means “archive disabled for this stream”.

**Storage Hours** — archive depth for this stream, in hours, from 1 to 2160 (up to 90 days). Chunks older than this value are deleted on each tick of the cleanup task (**Rolling cleanup**).

**Storage Min Hour** — lower protection threshold (hours). The cleanup task never deletes chunks younger than this, even under **Max Usage** pressure. Use it when business logic requires a guaranteed fresh recording, e.g. “the last 2 hours are always present”.

Changing **Storage** on the fly:

- setting *0* — disables archiving; on-disk chunks are **preserved**, new ones are not written. VOD sessions on this stream begin returning 404;
- selecting a different storage — the stream is detached from the old one and starts writing to the new one. Files from the old disk are not migrated.

Changes to **Storage Hours** and **Storage Min Hour** apply immediately — the cleanup task picks up the new value on the next tick.

After configuration, the stream begins writing the archive automatically as soon as it enters *Running* state.

## VOD: archive playback

The archive is played through **the same URLs** as live HLS / DASH broadcasting (see the *OTT service* section) — only the query parameter differs.

### URLs and parameters

URLs for HLS, DASH and Low-Latency HLS (DASH and LL-HLS — on CMAF, require *OTT/HLS/LL-HLS/LL-Dash* mode):

- [http://host:port/hls/stream/login/password/index.m3u8](http://host:port/hls/stream/login/password/index.m3u8)
- [http://host:port/dash/stream/login/password/index.mpd](http://host:port/dash/stream/login/password/index.mpd)
- [http://host:port/llhls/stream/login/password/index.m3u8](http://host:port/llhls/stream/login/password/index.m3u8)

Without query parameters — regular live with a **sliding window**. When the *t* parameter is present — VOD mode.

| Parameter | Purpose |
| --- | --- |
| `t=<epoch>` | VOD start time (Unix epoch, sec). *t=0* — from archive start. The presence of *t* (even *t=0*) enables VOD mode. |
| `d=<sec>` | VOD window duration, sec. *d=0* or parameter absent — “up to the current moment”. Meaningful only together with *t*. |
| `epg=<epoch>` | EPG-aligned VOD: the server itself locates the EPG event active at the given moment and takes its *start* and *duration* as window bounds. Incompatible with *t* and *d* (server-side override). See below. |
| `a`, `s`, `m`, `v` | Standard live parameters (see *OTT service*); `s` and `m` are ignored in VOD mode. |

Behavior by *t* and *d*:

| t | d | Window | 404 condition |
| --- | --- | --- | --- |
| none | — | live (sliding window) | — |
| 0 | none / 0 | [archive start, now] | DVR not bound |
| 0 | > 0 | [archive start, +d] | DVR not bound |
| > 0 | none / 0 | [t, now] | DVR not bound |
| > 0 | > 0 | [t, t+d] | DVR not bound |

Boundary normalization:

- *t* earlier than archive start — start is automatically aligned to the first available chunk (cleanup may have already trimmed). This is **not 404** — what remains is served.
- *t* in the future or window entirely earlier than the archive — empty but valid playlist (HLS: header only + `EXT-X-ENDLIST`; DASH: `@type="static"`, `mediaPresentationDuration="PT0S"`).
- Chunks are selected strictly by the chunk start time falling within the half-open interval *[t, t+d)* — there are no partial segments.

VOD on a stream without an archive (or whose storage is in *Error*) — **404** immediately on the master playlist / MPD. No session is created.

Error text in 404 body (visible in server logs and HTTP body):

- `VOD: stream not running` — the stream is in the config but not in *Running*.
- `VOD: no DVR archive` — no storage is set on the stream, or the storage is in *Error*.
- `VOD: DVR detached` — the stream was detached from the storage between requests.
- `VOD: EPG event not found` — no event found for `?epg=`.

VOD HLS playlist — **closed** (the player sees the duration and can seek):

```
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:5.000,
...
#EXT-X-ENDLIST
```

These markers are absent in the live playlist — that is the only difference.

VOD DASH MPD — **static**: `@type="static"`, fixed `mediaPresentationDuration`, explicit `<SegmentURL>`. Live DASH remains `@type="dynamic"`.

If the selected interval has **gaps** in the archive (e.g. a recording restart or cleanup trimmed a middle portion), the DASH MPD is automatically split into multiple `<Period>` — one per contiguous track. Players (VLC, dashjs, Shaka) seek across period boundaries without special configuration.

### EPG-aligned VOD

If the stream is bound to an EPG source (fields **EPG Source** and **EPG Channel** in the stream settings), the client may request the archive **by a moment that falls within an EPG event**:

- [http://host:port/hls/stream/login/password/index.m3u8?epg=1778500000](http://host:port/hls/stream/login/password/index.m3u8?epg=1778500000)
- [http://host:port/dash/stream/login/password/index.mpd?epg=1778500000](http://host:port/dash/stream/login/password/index.mpd?epg=1778500000)

The server locates the EPG event active at the given *epoch* and substitutes its *start* and *duration* as the VOD window bounds. Useful for program catalogs: the UI knows the event time but is not required to compute exact sec/ms boundaries.

If the stream is not bound to EPG or no event is active at that moment — **404 ``VOD: EPG event not found``**. Parameters *t* and *d* are ignored when *epg* is present.

### Adaptive streams

Adaptive groups (see [HLS Adaptive Multistream](ott.md#adaptive)) support the same VOD parameters:

- [http://host:port/hls/adaptive/group/login/password/index.m3u8?t=0](http://host:port/hls/adaptive/group/login/password/index.m3u8?t=0)
- [http://host:port/dash/adaptive/group/login/password/index.mpd?t=0](http://host:port/dash/adaptive/group/login/password/index.mpd?t=0)

Only variants that have a DVR storage configured are included in the master playlist (HLS). Variants without DVR are skipped (`VOD: variant N has no DVR` appears in the log); assembly from the remaining variants proceeds.

In the DASH variant, each quality becomes a separate `<Representation>` inside common `<Period>`; the player may switch between qualities without reopening the manifest.

### Player behavior

VOD HLS / DASH from the archive plays in standard players: VLC, hls.js, dashjs, Shaka, ffmpeg. Timeline seeking works.

If the player requests a segment that cleanup has already removed by that moment, the server returns **404 for that segment** (not 500). VLC, hls.js, dashjs skip such a segment and continue playback from the next one.

Additional capabilities:

- **Active session protection**: while a VOD session is open, cleanup does not delete chunks within its window (see below).
- **Live-edge bridge**: if a client in a VOD session requests a segment beyond the right boundary of the VOD window — e.g. moves forward along the timeline after reaching the archive end — the server automatically serves the segment from live memory. No redirects and no re-authorisation.
- **Playlist cache**: on repeated requests for the same VOD `index.m3u8` / `index.mpd`, the server returns a byte-for-byte identical response — without rebuilding. Suitable for a CDN in front of PSS.

## Subtitles in the archive

If the stream carries subtitles (DVB Subtitling, Teletext, or WebVTT) and the **OTT WebVTT** option is enabled in the stream settings, subtitles are archived in parallel with the TS chunks — as `.vtt` files next to `.ts`.

In live mode, the master playlist contains `EXT-X-MEDIA:TYPE=SUBTITLES`; in VOD mode the server returns a **VOD subtitle playlist** (with `ENDLIST`) and `.vtt` segments at the same URLs.

Notes:

- **HLS**: the `X-TIMESTAMP-MAP` header is preserved at the start of every `.vtt` (required by the HLS specification).
- **DASH**: the `X-TIMESTAMP-MAP` header is stripped on the fly (it is bound to the absolute PCR and conflicts with the DASH anchor; otherwise VLC shows empty subtitles).
- In an adaptive group: if the active sub-stream does not write subtitles, VTT segments return 404. On the next variant, the player may receive subtitles again.

Subtitles can be disabled either via the **OTT WebVTT** option on the stream, or by not enabling HLS in *OTT/HLS* mode at all.

## Cleanup and retention

PSS uses four cleanup strategies that run on every cleanup task tick (by default every 10 sec):

1. **Rolling cleanup** (per-stream): for each stream, chunks older than **Storage Hours** are deleted. Runs always, even with a half-empty disk.
2. **Size-based cleanup** (storage-wide): when **Used %** on disk continuously exceeds **Max Usage** for **Disk Pressure Grace** seconds, the oldest chunks are trimmed **proportionally** across all streams bound to the storage. Cleanup proceeds in batches of **Disk Pressure Cut** sec of video per stream per tick. Never deletes chunks younger than **Storage Min Hour**.
3. **Emergency disk-full cut**: when free space drops below **Disk Emergency Bytes**, cleanup runs aggressively and may delete even session-protected chunks. Recording stops until free space recovers with a hysteresis of × 2.
4. **Orphan scan**: files that are not accounted for in the index may remain on disk (after an abrupt shutdown of PSS). The collector walks through the stream subdirectories and deletes such “forgotten” files — the first run shortly after the service starts, then once an hour and whenever disk pressure is triggered. As protection against a race with writing, files younger than 60 sec are skipped.

**Note.** Cleanup tasks run in the background; the user normally does not have to act. If the archive grows faster than it is trimmed, lower **Storage Hours** on the streams or raise **Disk Pressure Cut**.

## Active VOD session protection

When a client opens a VOD session, a “protection slot” with the window start time is registered on every storage involved in its window. **Rolling** and **size-based** cleanup do not touch chunks within the open session’s window. The slot is released automatically when the session closes (FIN, timeout).

This means:

- If a client holds a VOD session for a long time, it is guaranteed to be able to seek to any moment within its window — chunks will not “vanish from under it”.
- Cleanup by **Max Usage** may temporarily fail to bring **Used %** to the desired threshold while a session is active; once the client leaves, cleanup catches up.
- **Emergency disk-full cut** and **Storage Min Hour** bypass the protection: if the disk is near zero free space, chunks are deleted and the client receives 404 for the affected segments (the player skips them).
- After a PSS restart, protection slots disappear — cleanup resumes immediately.

## Multiple storages

Any number of **DVR Storage** records may be created — one per directory / disk. Streams are bound to different storages independently. Cleanup and thresholds (**Max Usage**, **Disk Pressure**) operate per-storage.

Use cases:

- **Tiering by value**: a fast-SSD storage for premium channels with deep retention, a high-capacity HDD storage for the rest.
- **Dedicated disk for the archive**: so that DVR recording does not compete with the system disk or temporary files.
- **Project separation**: one disk for channel set #1, another for #2 — simplifies migration and audit.

Changing the stream binding (the **Storage** field in **Stream / OTT**) switches the recording on the fly. Old files remain untouched on the prior disk — they may either be deleted manually, or recording may be reattached by switching the stream back.

## Storage state and monitoring

The **Data / DVR Storage List** section (and the API `GET /data/dvr-storage-list`) reports per storage:

- **State** — *Ready* / *Error* (plus the intermediate *Not Ready* for a just-added storage).
- **Total / Free / Used Bytes** — free and used space on the disk.
- **Used %** — current usage percentage.
- **Archived Bytes** — total size of chunks accounted for in the index of all bound streams (excluding orphan files).
- **Pressure Since Sec** — moment (epoch) when **Used %** first exceeded **Max Usage** in the current pressure episode; *0* means “no pressure now”.
- **Active Task** — the maintenance background operation running right now: *gc-orphans* (removal of orphaned files), *disk-pressure-trim* (trimming under disk pressure) or *none*, and how many seconds it has already been running. Short-lived (< 2 s) operations do not flicker in the UI.
- **Last cleanup** — a summary of the most recent cleanup runs: how long ago the previous orphaned-file collection ran (and how many were deleted) and the last trim under disk pressure (how much space was freed).
- **Attached streams** — the list of attached streams; for each one the name, the recording-in-progress indicator (*active*), the configured archive depth (*retention-hours*) and the actual fill (*archived-sec* and *archived-bytes*) are shown. The sum of *archived-bytes* across the streams equals the **Archived Bytes** of the whole storage.

States:

- *Ready* — the directory is available, recording and cleanup proceed normally.
- *Error* — the storage directory is unavailable (unmounted, no permissions, file system error) or there is critically little free space on the disk; recording is stopped. Recovery is automatic — as soon as the directory is available again and enough free space appears.

If a storage has gone into *Error*, check whether the archive directory is mounted and whether there is free space on the disk — PSS does not exit this state on its own until the problem is physically resolved.

Archive fill over time:

- At the stream level (**Data / Stream**) the **storage-gap-percent** metric is available — the proportion of time gaps in the accumulated archive: *0 %* means continuous recording, higher values mean there are gaps in the archive (source restarts, segments trimmed by cleanup).
- The `GET /data/dvrstat` endpoint returns a histogram of archive fill by time intervals, with markers for recording events (source start/stop, **PMT** change, scrambling switches, index rebuild, cleanup run) and subtitle activity — for rendering the DVR-archive timeline in the admin interface.

## Protection against accidental data loss

Several admin-interface operations lead to **loss of recorded archive** or **cessation of VOD delivery**. Before execution, the admin UI displays a modal confirmation:

- **Deleting DVR Storage** — all bound streams lose VOD access; on-disk files remain but are unreachable through PSS without the storage record.
- **Switching the stream to another storage** — VOD over the old archive stops working.
- **Detaching the stream from storage** (Storage = 0) — same effect.
- **Reducing Max Usage** — may trigger size-based cleanup and delete old chunks.
- **Reducing Storage Hours / Storage Min Hour** — may push part of the archive out of the rolling window.

This is a deliberate product decision: the operation is permitted but requires confirmation, and deleted files remain on disk (can be restored from backup). Placing the archive on a separate file server / RAID substantially lowers the risk of irreversible loss.

## Current-version limitations

- A VOD session opened by a client **does not track** the live edge — if new chunks have been added to the archive during the session, the client must re-request the playlist (standard behavior of all HLS / DASH players).
- Assigning one storage per disk is the recommended layout — multiple records with different subdirectories on the same disk compete for free space.
- Segments are addressed by hash (HLS over MPEG-TS) or by the `$Number$.m4s` template with a shared `init.mp4` (live DASH over CMAF) / explicit `<SegmentURL>` to `.m4s` (VOD DASH). Changing the chunk size between live and VOD does not require reopening the URL.
- The orphan scanner runs hourly; to accelerate, restart PSS.
