Просмотр и экспорт
Как запросить архив за интервал времени, отмотать поток назад (таймшифт), открыть архив во встраиваемом плеере и выгрузить фрагмент файлом.
Архив отдаётся тем же протоколом, что и живое видео: плеер получает обычный плейлист HLS, только описывающий не последние секунды, а запрошенный интервал времени. Отдельного «протокола архива» нет, дополнительных портов и служб не требуется.
Все маршруты архива начинаются с /<поток>/dvr/. Если у потока нет строки dvr, любой из них
отвечает 404:
stream is not recorded
Плейлист архива#
GET /<поток>/dvr/playlist.m3u8?<интервал>
Форма интервала определяет тип плейлиста — «живой» (EVENT, догоняет текущий момент) или
«закрытый» (VOD, конечная запись с EXT-X-ENDLIST):
| Запрос | Что отдаётся | Тип |
|---|---|---|
?rewind=<секунд> |
последние N секунд, дальше догоняет живой край | EVENT |
?timeshift=<unix-секунды> |
с указанного момента и дальше | EVENT |
?from=<мс> |
с указанного момента и дальше | EVENT |
?from=<мс>&to=<мс> |
закрытый интервал | VOD |
?from=<мс>&duration=<секунд> |
закрытый интервал | VOD |
?from=…&…&event=true |
EVENT, который превратится в VOD и получит ENDLIST, когда запись действительно остановится |
EVENT → VOD |
| (без параметров) | последний час | EVENT |
Время во from/to — миллисекунды Unix, timeshift — секунды. Момент в будущем отклоняется.
# последние 10 минут, живой плейлист curl -sS "http://media.example.com:8080/cam1/dvr/playlist.m3u8?rewind=600" # закрытый интервал: 20 секунд начиная с 12:48:53 UTC FROM=$(date -u -d '2026-08-19 12:48:53' +%s000) # → 1787143733000 TO=$((FROM + 20000)) curl -sS "http://media.example.com:8080/cam1/dvr/playlist.m3u8?from=$FROM&to=$TO"
#EXTM3U #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:6 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-MAP:URI="/cam1/dvr/init/1787143661468.mp4" #EXT-X-PROGRAM-DATE-TIME:2026-08-19T12:48:51.468Z #EXTINF:6.000, /cam1/dvr/seg/1787143661468/315-342.m4s … #EXT-X-ENDLIST
Обратите внимание: запрошено 12:48:53, а первый сегмент начинается в 12:48:51.468. Начало
интервала всегда «притягивается» к ближайшему ключевому кадру не позже запрошенного момента —
иначе воспроизведение стартовало бы с недекодируемого места.
В теле плейлиста:
EXT-X-PROGRAM-DATE-TIMEу каждого сегмента — абсолютное время кадра, по нему плеер строит шкалу;EXT-X-DISCONTINUITYна каждой границе записей — там, где непрерывность прерывалась;EXT-X-MAPперевыдаётся только при реальной смене параметров кодирования.
Лишний параметр в URL (например, добавленный CDN или браузером обход кеша) не приводит к ошибке — он просто пропускается.
Два ограничения окна#
- Окно плейлиста молча урезается до 6 часов, при этом сохраняется самый свежий край интервала. Запросив сутки, вы получите последние 6 часов из них, без предупреждения.
- Запрос к календарю или таймлайну шире 31 суток — ошибка
422:
timeline requires from < to and a window no larger than 31 days
Для более длинных интервалов запрашивайте архив частями.
Таймшифт#
Таймшифт — это тот же архив, но «привязанный к живому краю»: зритель отматывает назад и продолжает смотреть, а плеер по мере накопления записи догоняет реальное время.
GET /<поток>/dvr/playlist.m3u8?rewind=300 # отмотать на 5 минут назад GET /<поток>/dvr/playlist.m3u8?timeshift=<unix> # начать с абсолютного момента
Оба варианта дают плейлист типа EVENT: он не заканчивается, а дописывается по мере записи. Это же поведение доступно во встраиваемом плеере — режим Timeshift.
Короткие адреса для приставок и панелей#
Часть устройств (ТВ-приставки, видеостены, простые панели) не умеет собирать URL с параметрами запроса. Для них есть эквиваленты в виде пути:
| Адрес | Эквивалент |
|---|---|
/<поток>/rewind-<секунд>.m3u8 |
?rewind=<секунд> — EVENT |
/<поток>/timeshift-<unix-секунды>.m3u8 |
?timeshift=… — EVENT |
/<поток>/clip-<от-сек>-<длительность-сек>.m3u8 |
закрытый плейлист VOD |
/<поток>/clip-<от-сек>-<длительность-сек>.mp4 |
выгрузка фрагмента файлом |
curl -sS "http://media.example.com:8080/cam1/rewind-60.m3u8" | head -4
#EXTM3U #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:6 #EXT-X-PLAYLIST-TYPE:EVENT
Служебные маршруты#
Ими пользуются плеер и панель управления; они же удобны для скриптов и мониторинга.
| Маршрут | Назначение |
|---|---|
GET /<поток>/dvr/api/days |
сутки (UTC), за которые есть записи: {"days":["ГГГГ-ММ-ДД", …]} |
GET /<поток>/dvr/api/timeline?from=<мс>&to=<мс> |
покрытие: непрерывные диапазоны с числом записей, объёмом и ярусом хранения |
GET /<поток>/dvr/init/<мс>.mp4 |
заголовок инициализации записи |
GET /<поток>/dvr/seg/<мс>/<a>-<b>.m4s |
кусок медиаданных |
GET /<поток>/dvr/thumb/<мс>.mp4 |
кадр-превью для наведения на шкалу: ближайший ключевой кадр |
GET /<поток>/dvr/clip?from=<мс>&to=<мс> |
выгрузка фрагмента файлом |
curl -sS "http://media.example.com:8080/cam1/dvr/api/timeline"
{"from":1787139386390,"to":1787142986390, "ranges":[{"start_ms":1787139151495,"end_ms":1787142986427, "segment_count":5,"bytes":4348353497,"tier":"local"}]}
Диапазоны склеены по ярусам: "tier":"local" — данные на диске, "tier":"s3" — в объектном
хранилище. Смешанного диапазона не бывает, поэтому по таймлайну сразу видно, какая часть архива
холодная. Разрыв между диапазонами — реальный пропуск в записи.
api/days возвращает UTC-сутки. В часовом поясе UTC+3 вечерние записи с 21:00 попадут в
следующие сутки календаря. Плеер учитывает смещение сервера сам; в собственных интерфейсах приводите
время явно.
Отдельного маршрута «список записей» нет — обнаружение строится из api/days (какие сутки есть)
и api/timeline (что внутри суток).
Встраиваемый плеер#
У пишущего потока во встраиваемом плеере автоматически появляются две дополнительные вкладки — DVR и Timeshift. У потока без записи их нет: набор вкладок выводится из того, что сервер реально умеет отдать для этого потока.
http://media.example.com:8080/cam1/embed.html?proto=dvr http://media.example.com:8080/cam1/embed.html?proto=timeshift
| Параметр запроса | Действие |
|---|---|
proto=dvr |
открыть режим просмотра архива |
proto=timeshift |
тот же режим, но с сохранённой ссылкой на живой край |
dvr=1 |
то же, что proto=dvr |
from=<мс> и to=<мс> |
открыть архив сразу на этом интервале (миллисекунды Unix) |
Режим DVR — это не другой медиапротокол, а другой интерфейс над теми же маршрутами: выбор суток из
api/days, масштабируемая шкала из api/timeline, превью при наведении из dvr/thumb и
воспроизведение конечным плейлистом dvr/playlist.m3u8?from=&to=.
Ссылку с интервалом можно передавать как есть — она открывает нужный момент:
http://media.example.com:8080/cam1/embed.html?from=1787142800000&to=1787142860000
Остальные параметры плеера (compact, chrome, autostart, token) действуют и в режиме архива —
см. Встраиваемый плеер.

Экспорт фрагмента файлом#
GET /<поток>/dvr/clip?from=<мс>&to=<мс> GET /<поток>/clip-<от-сек>-<длительность-сек>.mp4
Ответ — готовый video/mp4 с заголовком выгрузки:
FROM=$(date -u -d '2026-08-19 10:00:00' +%s000) # → 1787133600000 TO=$((FROM + 10000)) # 10 секунд # -OJ сохраняет файл под именем, которое предложил сервер; -D - печатает заголовки curl -sS -OJ -D - "http://media.example.com:8080/cam1/dvr/clip?from=$FROM&to=$TO"
HTTP/1.1 200 OK Content-Type: video/mp4 Content-Length: 13031529 Cache-Control: no-store Content-Disposition: attachment; filename="hastreamer-clip-1787133600000-1787133610000.mp4" X-HAS-Source: local
Заголовок X-HAS-Source показывает, откуда взялись данные: local — с диска, s3 — из
объектного хранилища.
Файл собирается без перекодирования: заголовок записи отдаётся как есть, к нему приставляется нужный отрезок медиаданных. Отсчёт времени в файле переносится в ноль по каждой дорожке отдельно, поэтому файл начинается с начала и синхронность звука с видео сохраняется; на диске архив остаётся с абсолютными метками времени. Выгружать можно и ещё не закрытую запись — вплоть до того момента, который уже надёжно записан.
Интервал может пересекать границы файлов записи. Хранилище режет архив на файлы по
segment_secs (15, 30 или 60 минут), и запрошенный час обычно состоит из нескольких таких файлов —
сервер склеивает их в один .mp4 со сквозной шкалой времени: скачка на стыке нет, звук от видео
не разъезжается. Живой хвост участвует в склейке наравне с закрытыми записями.
Файл отдаётся потоком, а не собирается в памяти целиком, поэтому длина клипа не ограничена
объёмом. Content-Length известна заранее, работает Range (докачка) и HEAD.
Запись с телефонным звуком G.711 браузер декодировать не умеет, поэтому при просмотре архива сервер честно отдаёт только видео. В экспортированный файл звук попадает целиком. Это частый вопрос: «в браузере тихо, а в скачанном файле звук есть» — так и задумано.
Пропуски в записи#
Если внутри интервала камера была недоступна, этих секунд в архиве нет — и взять их неоткуда. Сервер склеивает записи встык, сжимая паузу: файл получается короче запрошенного интервала ровно на невыписанное время, а шкала остаётся непрерывной. Так честнее, чем растягивать время: иначе плеер стоял бы на замершем кадре всю длительность аварии.
Считать длительность файла по to − from поэтому нельзя. Сколько записи реально есть в интервале,
показывает api/timeline (ranges): суммарная длительность его диапазонов и есть ожидаемая
длительность файла (плюс притяжка к ключевому кадру в начале).
Смена параметров кодирования — отказ#
Склеить в один файл можно только записи с одинаковыми параметрами: разрешение, кодек и набор
дорожек описываются одним заголовком moov. Если камеру переключили на другое разрешение, архив
хранит записи «до» и «после» с разными заголовками, и один файл из них не собрать. Сервер не отдаёт
ни обрезанный, ни битый результат, а отказывает с указанием момента смены:
422 clip crosses an encoding change at 1787227785573
(different resolution/codec/track set — export each side separately)
Выгрузите две части — до и после указанной метки.
Ограничения экспорта#
| # | Ограничение | Величина | Сообщение |
|---|---|---|---|
| 1 | Одинаковые параметры кодирования на всём интервале | init_hash записи |
clip crosses an encoding change at <мс> … |
| 2 | Предел длительности интервала | 24 часа | clip exceeds MAX_CLIP_MS (1440 min) (маршрут dvr/clip; алиас clip-…mp4 молча урезает) |
| 3 | Число склеиваемых записей | 512 | clip spans too many recording segments … |
| 4 | Окно плейлиста — относится к запросу интервала, а не к самому файлу | 6 часов | урезается молча |
Экспорт отдаётся потоком и стоит серверу одно окно памяти (несколько МиБ) независимо от длины клипа
и числа склеенных записей. Зато размер файла ничем не прикрыт: при 4 Мбит/с час записи — это
около 1.8 ГБ, сутки — около 43 ГБ. Оценивайте по bytes из api/timeline и предупреждайте
пользователей.
Одновременно выполняется не более шести выгрузок на весь сервер. Сверх этого:
503 clip export busy - retry shortly
В панели управления экспорт доступен на карточке потока, вкладка DVR, блок Export a clip: два поля выбора времени и кнопка с оценкой размера. Панель держит выделение внутри непрерывного диапазона архива — границы файлов записи внутри такого диапазона ей знать не нужно, их склеивает сервер.
Права доступа#
Когда авторизация просмотра включена, маршруты архива требуют разных прав:
| Маршрут | Требуемое право |
|---|---|
dvr/playlist.m3u8, dvr/api/*, dvr/init, dvr/seg, dvr/thumb, clip-….m3u8 |
dvr |
dvr/clip, clip-….mp4 |
export |
Право export не входит в dvr. Зритель, который может листать и смотреть архив, не сможет
скачать фрагмент, пока выгрузка не разрешена ему явно. Это позволяет открыть просмотр архива широкой
группе, оставив вынос материала за пределы системы узкой. Подробности —
Авторизация просмотра.
Учёт зрителей архива#
Плейлист и все запросы сегментов одного зрителя объединяются в одну сессию; сессия закрывается через 30 секунд без запросов. Зрители архива считаются отдельно от живых:
curl -sS "http://media.example.com:8080/api/v1/totals" \ -H "Authorization: Bearer $TOKEN"
{"bytes_in":7771909210,"bytes_out":578696551,"dvr_bytes_out":32833724,"dvr_sessions":1, "live_bytes_out":545862827,"live_sessions":3,"sessions":4,"streams":10}
На карточке потока те же величины называются dvr_viewers и dvr_bytes_out.
Диагностика#
| Что вернулось | Что это значит |
|---|---|
404 stream is not recorded |
у потока нет строки dvr — запись не настроена |
404 no such recording |
запрошенного момента в архиве нет: он вытеснен по ретенции или не записывался |
422 timeline requires from < to … |
окно шире 31 суток либо границы перепутаны местами |
422 clip crosses an encoding change at … |
внутри интервала менялись разрешение/кодек/набор дорожек: выгрузите части отдельно |
422 clip spans too many recording segments … |
интервал накрывает больше 512 файлов записи — сузьте его |
404 no recording in range |
в интервале нет ни одной записи (весь интервал попал в пропуск) |
503 |
архив временно недоступен: том не смонтирован, объектное хранилище не отвечает — повторите позже |
| Пустой плейлист | в запрошенном интервале записи нет; проверьте по api/timeline |
curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' \ "http://media.example.com:8080/cam1/dvr/playlist.m3u8?rewind=60"
200 application/vnd.apple.mpegurl
Если код 404, а тело — stream is not recorded, вернитесь к странице
Запись архива (DVR): в конфигурации потока нет строки dvr.
Куда двигаться дальше#
- Запись архива (DVR) — включение записи и глубина архива.
- Хранилища и S3 — где лежат записи и что задаёт границы файлов.
- Встраиваемый плеер — все параметры плеера.
- Авторизация просмотра — права
dvrиexport.