Trick Play, Seek и DVR: Незаметные Тяжёлые Задачи

Автор: Николай СапуновОбновлено: август 202621 мин чтения
Содержание статьи +

TL;DR

Три функции стриминга, которым никто не аплодирует и которые все замечают, когда они ломаются – скраббинг, переход к таймкоду и пауза в прямом эфире, чтобы пересмотреть гол – это также три функции, которые тише всего раздувают расходы на хранение, повышают нагрузку на CPU origin-сервера и унижают плеер при сбое. Trick play (превью-миниатюры под скраббером плюс ускоренная перемотка вперёд и назад), seek (прыжок плеера на любую точку таймлайна) и DVR (возможность отмотать прямой эфир в прошлое) – каждая требует структурных компромиссов на этапе энкодера, упаковщика, манифеста, CDN и плеера. Сложные части в 2026 те же, что и в 2010: закодированное видео полно кадров, которые нельзя декодировать в изоляции, а HTTP-кэши никогда не проектировались для зрителя, который прыгает назад во времени. Эта статья объясняет структурные причины сложности каждой функции, механизмы HLS и DASH на уровне манифеста, и практические решения, которые определяют, будет ли ваше DVR-окно длиной в две минуты или в двое суток.

Почему Это Важно

Три функции из заголовка – те, что инженерные менеджеры недооценивают на этапе планирования и в которых продакт-менеджеры обвиняют инженеров на запуске. 90-минутный фильм, на seek в который уходит восемь секунд, теряет зрителей; 24-часовой прямой эфир, который нельзя отмотать дальше последней рекламной паузы, теряет подписчиков; скраббер без миниатюр выглядит как приложение из 2008 года. Ни один из этих сбоев не возникает в первоначальном плане «доставить HLS на телефоны» – они возникают за две недели до релиза, когда команда плеера обнаруживает, что энкодер никогда не создавал I-frame playlist, а строка «storage» в бюджете следующего квартала должна вырасти втрое. Эта статья даёт нетехническим лидерам достаточно механики, чтобы принимать решения заранее, и даёт инженерным руководителям чек-лист тегов манифеста, флагов энкодера и правил кэша CDN, которые тихо решают, заработают ли функции на запуске или сломаются.

Почему Эти Три Функции Так Сложны

Чтобы увидеть, почему скраббинг, seek и DVR структурно сложнее, чем «играть поток вперёд», нужен один контекстный факт: не каждый закодированный кадр можно декодировать отдельно. Энкодер сжимает поток, храня большинство кадров как разницы относительно соседних – только небольшая часть кадров, называемых I-frames (от «intra-coded frames», внутрикадрового кодирования), содержит достаточно информации, чтобы быть декодированной независимо. Всё остальное – P-frames (predicted, предсказанные из более ранних кадров) и B-frames (предсказанные из обоих направлений) – зависит от соседей.

Полезная аналогия: представьте комикс, где каждая пятая страница – это полный рисунок, а каждая страница между ними – просто список отличий от предыдущей: «добавьте шляпу, сдвиньте дерево на пять сантиметров влево, поменяйте цвет неба». Вы можете начать читать с любого полного рисунка, но не можете начать с середины – отличия имеют смысл только относительно полного рисунка, за которым они следовали. Декодеры работают так же. Чтобы начать воспроизведение в любой точке, декодер должен отмотать к ближайшему I-frame и проиграть каждый зависимый кадр до запрошенного таймкода.

Этот единственный факт – корневая причина всех проблем в этой статье. Seek сложен, потому что плеер не может просто прыгнуть на байт X – он должен сначала найти правильный I-frame. Trick play сложен, потому что fast forward на 8× потребовал бы декодирования 240 кадров в секунду обычного контента, что большинство телефонов не выдержит – поэтому trick play использует альтернативный поток, содержащий только I-frames. DVR сложен, потому что манифест, хранилище и кэш CDN – все должны помнить прошлое, а HTTP-кэши никогда не были добры к URL, которые меняются каждые две секунды.

Хорошая новость в том, что каждый современный протокол стриминга – HLS, MPEG-DASH, CMAF – отшлифован так, чтобы все три функции работали чисто. Плохая новость в том, что они работают только если энкодер, упаковщик, манифест и плеер все настроены под них. Пропустите один шаг – и функция тихо деградирует.

Рисунок 1. Группа изображений (GOP) начинается с ключевого кадра – I-frame – за которым следуют P- и B-frames, ссылающиеся на него. Seek приземляется на ближайший предшествующий I-frame; интервал ключевых кадров (обычно 2 секунды для стриминга) задаёт минимальную гранулярность seek.

Seek: Анатомия Прыжка На Таймкод

Когда зритель тащит скраббер с 12-й минуты на 47-ю, плеер выполняет цепочку шагов, которые большинство людей представляют себе мгновенными. Это не так. Это примерно следующее.

Плеер читает свой манифест – HLS multi-variant playlist или DASH MPD – и находит сегмент, чей диапазон presentation time содержит 47-ю минуту. Манифест перечисляет сегменты либо явно, по URL и длительности (HLS media playlist; DASH SegmentList), либо неявно, через шаблон, генерирующий URL сегмента из его номера и start-number offset (DASH SegmentTemplate; доминирующий паттерн). В любом случае плеер вычисляет номер сегмента из таймкода: при длительности сегмента 4 секунды 47-я минута (2820 секунд) лежит в сегменте 706 (2820 ÷ 4 = 705, округлено вверх).

Плеер отправляет HTTP-запрос на сегмент 706. Сегмент – это короткий fragmented-MP4 файл, обычно 2–6 секунд, который плеер может декодировать независимо от любого другого сегмента. Это свойство и делает возможным HTTP-стриминг: каждый сегмент самодостаточен, начинается с I-frame, заканчивается там, где начинается следующий.

Внутри сегмента 706 плеер всё ещё должен найти правильный кадр. Сегмент 706 представляет 4 секунды wall-clock времени, но зритель хотел именно 47-ю минуту, что может быть 1,7 секунды в сегменте. Декодер начинает с открывающего I-frame сегмента, декодирует P- и B-frames до кадра 51 (при 30 fps 1,7 с ≈ 51 кадр) и только тогда рисует картинку на экране.

Гранулярность seek потока, таким образом, равна длительности сегмента в секундах, делённой на интервал ключевых кадров внутри сегмента. HLS и DASH традиционно ставят ровно один I-frame в начале каждого сегмента, так что гранулярность seek равна длительности сегмента: примерно 2–6 секунд. Чтобы выполнять seek с субсекундной точностью, энкодер должен вставлять дополнительные I-frames внутрь каждого сегмента, что повышает битрейт на 10–30%. Большинство платформ принимают гранулярность 2–4 секунды и проектируют UX вокруг неё – скраббер защёлкивает play head на начале сегмента, содержащего запрошенный таймкод, и зритель воспринимает это как мгновенный переход.

Распространённая ошибка – несогласованные границы сегментов между rendition-ами. Если поток 1080p обрезает новый сегмент каждые 4 секунды на границах кадров N, N+120, N+240, а поток 480p – на N+10, N+130, N+250, плеер не может чисто переключить rendition во время seek. Каждая ступенька битрейтной лесенки должна использовать те же границы сегментов, заданные тем же closed-GOP интервалом ключевых кадров. Упаковщик обеспечивает это при нарезке выхода энкодера, но энкодер должен сначала производить I-frames в правильном ритме. Классический сбой – per-title энкодер, решающий оптимальное размещение ключевых кадров под контент и в итоге дающий несогласованные границы GOP между rendition-ами; плеер переключает rendition, номера сегментов больше не совпадают, и следующий seek приземляется не на ту секунду.

Trick Play: Fast Forward Без Поджаривания Декодера

Trick play – это семейство поведений, которых зритель ожидает от «пульта»: fast forward на 2×, 4×, 8×, миниатюры под скраббером, превью-кадр при ховере, и то же самое в реверсе. Ничего из этого нельзя реализовать, просто играя основной поток быстрее. На 8× скорости поток 30 fps требовал бы подавать в декодер 240 кадров в секунду, что мобильные декодеры не выдержат и что съест батарею за минуты. На любой скорости миниатюры скраббера появляются на каждом пикселе, через который перетаскивает пользователь, что означает, что плееру нужны сотни превью-кадров загружены впереди позиции скраббера – гораздо больше, чем может предоставить ритм сегментов основного потока.

Структурное решение – вторичный поток только из I-frames. Энкодер производит, рядом с основным потоком, альтернативный поток, содержащий только I-frames основного контента, упакованный на гораздо меньшем битрейте, потому что между ними нет P- и B-frames. Манифест HLS объявляет этот поток тегом EXT-X-I-FRAME-STREAM-INF – отличным от обычного EXT-X-STREAM-INF, объявляющего проигрываемые rendition-ы. Внутри I-frame playlist каждый «сегмент» – это один I-frame, помеченный EXT-X-I-FRAMES-ONLY в начале media playlist для сигнализации необычной формы. DASH решает ту же проблему через AdaptationSet с атрибутом @codecs, или выставляя тот же I-frame поток как low-bitrate representation с пометкой trick-mode track в MPD.

Когда зритель запускает fast forward, плеер переключается с основного потока на I-frame поток, вычисляет, какие I-frames рендерить на выбранной скорости (каждый второй на 4×, каждый четвёртый на 8× и так далее), и подаёт эти кадры в декодер на нормальной частоте кадров воспроизведения. Декодеру не приходится рендерить больше 30 кадров в секунду независимо от скорости trick-play; только источник прыгает по таймлайну. Реверс работает на том же механизме в обратную сторону – плеер декодирует I-frames от поздних к ранним и показывает их в убывающем порядке.

Миниатюры под скраббером – связанная, меньшая версия того же трюка. Стандартное решение – I-frame thumbnail track: отдельный поток низкого разрешения – обычно 160×90 пикселей, одно изображение в 2 секунды – публикуемый как последовательность JPEG или WebP изображений, организованных в sprite sheet для эффективного HTTP-фетча. HLS выражает их через тег EXT-X-IMAGE-STREAM-INF (добавленный в драфт RFC 8216bis и доступный в основных упаковщиках с 2021 года). DASH выражает их через codec image/jpeg в AdaptationSet, определённый в DASH-IF image-thumbnail guidance. В обоих случаях плеер скачивает thumbnail track поверх основного воспроизведения, индексирует его по таймкоду и показывает правильную плитку при ховере на скраббере.

Самый частый баг миниатюр – рассогласование ритма: thumbnail track закодирован с интервалом 10 секунд, а манифест заявляет 2 секунды, и превью при скраббинге не соответствуют тому, что играет после отпускания скраббера. Доверяй манифесту, но проверяй фактический ритм миниатюр против заявления манифеста перед релизом.

Рисунок 2. Плеер несёт три потока: основной media-поток для нормального воспроизведения, I-frame-only поток для fast forward и реверса, и image rendition для миниатюр под скраббером. Переключение управляется действием пользователя, не условиями сети.

DVR: Пауза В Прямом Эфире

DVR – сокращение от «digital video recorder», но в стриминге означает любую возможность отматывать назад в прямом эфире – это функция, которая позволяет зрителю поставить на паузу баскетбольный матч, взять напиток, вернуться через десять минут и продолжить с места паузы. Это же позволяет зрителю отмотать на тридцать секунд назад, чтобы пересмотреть гол. Хорошая реализация отделяет отшлифованный стриминговый сервис от любительского.

Структурная задача в том, что прямой эфир по определению – это скользящее окно недавних сегментов. Энкодер производит новый сегмент каждые 2–6 секунд. Упаковщик обновляет манифест, добавляя новый сегмент и (в большинстве live-конфигураций) удаляя самый старый. Если манифест всегда перечисляет только пять последних сегментов, зритель может отмотать назад на десять-двадцать секунд; более старых сегментов уже нет. Это поведение по умолчанию «live» HLS playlist без тега EXT-X-ENDLIST – плеер опрашивает манифест каждые несколько секунд для обновлений, и реверс ограничен тем, что манифест сейчас перечисляет.

DVR расширяет это окно. Вместо удаления сегментов после их сползания с live edge упаковщик их сохраняет, и манифест продолжает их перечислять – иногда час, иногда полные сутки, иногда всю длительность трансляции. В HLS DVR-окно выражается неявно – теми сегментами, которые media playlist всё ещё перечисляет. В DASH оно выражается явно атрибутом MPD@timeShiftBufferDepth, который указывает в секундах продолжительность доступного для seek прошлого – PT1H для 1-часового DVR-окна, PT4H для 4 часов. ISO/IEC 23009-1:2022, раздел 5.3.1.2, определяет timeShiftBufferDepth как длительность time-shift буфера, гарантированно доступного начиная от live edge назад.

Имплементационная работа в трёх местах. Упаковщик должен продолжать генерировать новые сегменты без удаления старых. Origin должен сохранять старые сегменты доступными – обычно на том же хранилище, что и live-сегменты, иногда на отдельном медленном tier для часовых архивов. CDN должен эффективно их кэшировать – DVR-сегменты отличные кандидаты для CDN, потому что как только они сползли несколько минут от live edge, их содержимое больше не меняется, и длинный TTL (1 час, 6 часов, 1 день) на CDN edge означает, что большинство DVR-повторов отдаются из кэша.

Компромисс – это storage. 1080p-канал на 5 Mbps композитного битрейта тратит 2,25 GB в час – на шести rendition-ах лесенки это 13,5 GB на channel-hour. 4-часовое DVR-окно стоит 54 GB live-хранилища на канал. 24-часовое DVR-окно стоит 324 GB на канал. Платформе с 200 live-каналами и 24-часовым DVR-окном нужно примерно 65 TB always-on, low-latency хранилища только под DVR – пятизначная ежемесячная строка storage почти у любого облачного провайдера.

Решение на уровне упаковщика – будет ли DVR-окно rolling (старые сегменты удаляются по мере поступления новых – простейший паттерн) или catch-up (каждый сегмент с начала трансляции сохраняется до её конца, затем архивируется в VOD-актив). Rolling DVR используют новостные каналы и спортивные вещатели в нормальном режиме. Catch-up DVR используют подписочные сервисы, когда ценность трансляции переживает live-момент – матч Премьер-Лиги должен быть пересматриваемым хотя бы 24 часа после финального свистка, в идеале неделю.

Решение на уровне плеера – какой UI экспонировать. Кнопка «отмотать на 30 секунд» – самый безопасный универсальный контрол: она работает независимо от длины DVR-окна, независимо от того, началась ли трансляция 10 минут или 4 часа назад. Скраббер, показывающий полное DVR-окно – мощнее, но запутаннее: зрители регулярно перетаскивают скраббер за live edge, получают обратно пилюлю «Live» и думают, что что-то сломали. Конвенция 2026 года – показывать таймлайн, заканчивающийся на «Live» с видимой меткой, со скраббером, который может перетаскиваться влево к любому сегменту в DVR-окне и защёлкиваться обратно на live при перетаскивании к правому краю.

Где HTTP-Кэширование Перестаёт Помогать

Trick play и seek дружелюбны к HTTP-кэшированию. Сегменты и I-frame playlists статичны после публикации; CDN может кэшировать их с длинным TTL, и большинство повторов попадают на тёплый edge. Главное усложнение – сам манифест, который для live-потоков обновляется каждые несколько секунд – большинство CDN ставят short-TTL на манифест (1–3 с) и long-TTL на сегменты (часы-дни). LL-HLS добавляет дальнейшее усложнение, отдавая partial segments, которые сшиваются в финальный сегмент через несколько секунд, но сплит TTL манифест-vs-сегмент сохраняется.

DVR seek – там, где история кэширования становится сложнее. Зритель, прыгнувший назад на 47-ю минуту 4-часовой трансляции, вызывает cache miss для того rendition-а, что он смотрит, на том номере сегмента, что отображается на 47-ю минуту. Если сегмент последний раз запрашивался час назад и устарел в edge cache, запрос идёт через origin shield к origin. Первый зритель, делающий seek на холодный сегмент, платит латентность полного похода до origin; следующие зрители выигрывают от прогрева кэша.

Митигация – origin shield tier – небольшой набор mid-tier edges между листовыми edges и origin, настроенный с гораздо более длинными TTL и явным сохранением DVR-сегментов. Shield ловит большинство DVR cache miss и не даёт им попадать в origin; origin видит только малую долю miss, которой даже shield не имеет. (Полную архитектуру см. в Origin Shielding and Tiered Caching.)

Вторая митигация – дисциплина cache key. Удивительное количество DVR-багов происходит от cache key CDN, включающих query-параметры, которые плеер добавляет для аналитики или session identification. Если seg_00706.m4s?session=abc и seg_00706.m4s?session=xyz кэшируются отдельно, эффективный hit rate CDN – ноль. Удаляйте session-identifying query strings из cache key, или используйте signed-URL токены с долгоживущим cache key. (См. Cache Keys for Streaming.)

Live-to-VOD: Финальная Форма DVR

Самая чистая реализация дальнего DVR – это вовсе не DVR – это live-to-VOD конвейер. Когда live-трансляция заканчивается (или иногда продолжается), упаковщик закрывает манифест тегом EXT-X-ENDLIST (HLS) или переводит MPD type с dynamic на static (DASH), и актив становится обычным VOD-тайтлом. С этого момента применяются все обычные VOD-оптимизации – pre-packaged или JIT-origin, очень длинные CDN TTL, дешёвое object-store backing. DVR-окно перестаёт быть стоимостью live-хранилища и становится стоимостью archive-хранилища.

Большинство спортивных и новостных платформ работают в гибриде: rolling DVR на 4–6 часов во время live-трансляции, затем чистый переход в VOD-актив, живущий в каталоге дни-годы. Плееру не нужно сообщать – он опрашивает манифест, видит появление тега EXT-X-ENDLIST (или переключение DASH MPD type) и переключает внутреннее состояние с «live» на «on demand». Зритель ничего не замечает.

Подвох – переходное окно – несколько минут, в течение которых и live-сигнал, и VOD-актив ссылаются на те же сегменты. Если live-упаковщик продолжает писать пока VOD-упаковщик начинает публиковать, манифест может временно объявить больше сегментов, чем хранилище фактически содержит, или содержать URL сегмента, который переименовывается на лету. Решение – контракт координации live-to-VOD: live-упаковщик публикует финальный манифест с ENDLIST, VOD-упаковщик либо копирует live-сегменты в VOD origin, либо перенаправляет URL через redirect rule, и live-origin продолжает обслуживать старые URL с 301 на новые хотя бы в течение cache TTL.

Рабочий Пример: Спортивный DVR С Trick Play

Прогоним арифметику end-to-end для типового деплоя: стриминговый сервис, распределяющий 50 live-каналов спорта с 4-часовым rolling DVR, миниатюрами под скраббером и 8× fast forward в catch-up режиме.

Лесенка энкодера – обычная шестиступенчатая: 240p, 360p, 480p, 720p, 1080p, 4K. Композитный битрейт на полном качестве – примерно 9 Mbps. Добавьте I-frame-only вторичный поток на 600 kbps и thumbnail track 160×90 на 50 kbps. Каждый канал даёт около 9,65 Mbps в хранилище.

9,65 Mbps × 3600 с = 34 740 Mb/час = 4,34 GB/channel-hour
× 4 часа DVR-окна         = 17,37 GB на канал
× 50 каналов               = 868 GB live DVR-хранилища постоянно

При AWS S3 Standard pricing (~$0,023 / GB-месяц) это хранилище – около $20/месяц – мелочь. Реальная стоимость – это always-on object-store latency tier: большинство платформ держат DVR на чём-то типа EBS-backed origin storage, чтобы держать seek-into-DVR latency ниже 200 мс, что стоит примерно 5× S3 Standard. Строка DVR-хранилища для этой платформы выходит около $100/месяц – всё ещё мало по сравнению с CDN.

CDN-сторона несёт реальные деньги. При средней concurrency 50 000 зрителей по 50 каналам и среднем битрейте 5 Mbps live egress – 250 Gbps; за месяц это примерно 80 PB по стандартным delivery prices, или около $400 000/месяц по типовым CDN-расценкам крупного клиента. Использование DVR добавляет к этому около 5–8%, в основном catch-up зрители, переигрывающие сегменты через минуты после эфира – которые ещё тёплые на большинстве edges, так что маржинальная стоимость мала.

Trick play добавляет одноразовую стоимость кодирования (I-frame поток кодируется на ~5% битрейта основного, так что назовём 5% прирост encoding compute) и небольшую CDN-стоимость во время fast-forward сессий. Миниатюры скраббера добавляют, может быть, 0,5% к нагрузке энкодера и 0,1% к CDN egress.

Вывод: функции – не там, куда уходят деньги. Деньги уходят на CDN egress, доставляющий основные потоки. Функции стоят инженерного внимания, не капитала.

Где Фора Софт Подходит

Мы строим стриминговые платформы в OTT, спорте, e-learning, телемедицине и видеонаблюдении, и trick play / seek / DVR – проблемы, с которыми мы сталкиваемся почти в каждом проекте. В спорте мы поставляем 4-часовое rolling DVR с миниатюрами скраббера и 8× catch-up как baseline; в e-learning поставляем per-lesson seek с chapter markers в манифесте и оффлайн-скачивание I-frame thumbnail track для скраббинга на низкой полосе; в телемедицине поставляем recorded-session DVR с frame-accurate seek для клинического обзора. Паттерны различаются по вертикали, но механика на уровне манифеста – EXT-X-I-FRAMES-ONLY, EXT-X-IMAGE-STREAM-INF, DASH timeShiftBufferDepth, image adaptation sets – одна и та же во всех, и сбои тоже те же: GOP misalignment, дрейф cache key и недостаточный размер DVR-окна, обнаруженный на следующий день после релиза.

Распространённые Ошибки (Быстрая Справка)

Список ниже собирает паттерны сбоев, всплывающие почти в каждом проекте. Используйте как pre-launch чек-лист.

  • Границы GOP не выровнены между rendition-ами – каждая ступенька лесенки должна закрывать GOP на одном и том же wall-clock кадре, иначе seek ломается на переключении rendition.
  • I-frame playlist не сгенерирован – энкодер производит I-frames в правильном ритме, но упаковщик никогда не выставляет EXT-X-I-FRAMES-ONLY и EXT-X-I-FRAME-STREAM-INF, и плеер не может делать fast forward.
  • Thumbnail track закодирован с другим ритмом, чем заявлено в манифесте – превью под скраббером дрейфуют относительно фактического контента.
  • TTL live-манифеста слишком длинный – зрители видят устаревший «live edge», потому что CDN отдаёт 60-секундный манифест.
  • DVR-окно заявлено в секундах, но хранилище удерживает в сегментах – 1-часовое DVR-окно с 4-секундными сегментами требует удержания 900 сегментов на rendition, и реальное ограничение – лимит количества сегментов на упаковщике, не строка времени в манифесте.
  • Cache key содержит session ID – DVR-повторы идут на origin каждый раз, потому что никакие два cache key не совпадают.
  • Live-to-VOD переход без EXT-X-ENDLIST – плееры в режиме «live» продолжают опрашивать манифест, пропуская VOD-актив целиком.
Рисунок 3. Рабочая DVR-архитектура: live-упаковщик производит rolling window, origin shield tier держит DVR-сегменты на длинных TTL, листовые CDN edges кэшируют горячие live-сегменты на коротких TTL, и live-to-VOD bridge переводит завершённые трансляции в VOD-каталог.

Ключевые Выводы

  • Гранулярность seek равна длительности сегмента; субсекундный seek требует дополнительных I-frames и 10–30% повышения битрейта.
  • Trick play использует отдельный I-frame-only поток – без него нет fast forward на любой скорости.
  • DVR – это скользящее окно сохранённых live-сегментов; в HLS неявно через содержимое playlist, в DASH явно через timeShiftBufferDepth.
  • DVR-хранилище дёшево; латентность cache miss дорога – используйте origin shield tier и удаляйте session ID из cache key.
  • Самый чистый дальний DVR – это переход live-to-VOD, не бесконечное live-хранилище.

Что Читать Дальше

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.