Главный результат этой оптимизации теперь можно выразить одним нормальным числом: один конкретный файл из рабочей системы после новой H.264-политики уменьшился примерно с 280 МБ до 50 МБ. Это примерно в 5,6 раза меньше: экономия около 230 МБ, или примерно 82% исходного размера.
Я получил это не переходом на AV1, HEVC или VP9. Выходной файл остался обычным H.264 внутри MP4. Изменилась политика вокруг кодека: меньше лишних пикселей, меньше ненужных временных отсчётов, гораздо менее консервативная цель качества, больше вычислений на одноразовом этапе кодирования и намеренно ограниченный декодерный контракт.
Нагрузка была очень конкретной: короткие иллюстрированные и анимированные ролики, примерно 80% мобильного трафика, сетевой трафик как постоянная повторяющаяся стоимость и предварительное кодирование, где время процессора намного дешевле, чем годами раздавать слишком большие файлы.
Старая и новая политики в упрощённом виде выглядели так:
старая схема
H.264 Main @ Level 4.0
CRF 19
предустановка slow
до 1920x1080 / 1080x1920
30 fps
refs = 3
B-frames = 3
GOP ≈ 2 секунды
VBV ≈ 10M / 20M
новая схема
H.264 Main @ Level 3.1
CRF 28
предустановка veryslow
потолок 720p-класса, без увеличения
полезный CFR, обычно <= 30 fps
refs = 4
B-frames = 5
GOP ≈ 5 секунд
VBV ≈ 4M / 8M
yuv420p / avc1 / faststart
Измеренный результат: примерно 280 МБ превратились в 50 МБ
В логах есть несколько реальных измерений, но это не один и тот же эксперимент. Я специально не смешиваю их в одну красивую цифру, потому что так легко получить впечатляющий, но нечестный вывод.
| Измерение | До | После | Сокращение |
|---|---|---|---|
| Один и тот же конкретный файл | ~280 МБ | ~50 МБ | ~5,6× меньше / ~82% |
| Более ранний этап того же файла | ~350 МБ | ~238 МБ | ~1,47× меньше / 32% |
Первая строка — самое чистое доказательство заголовка. Это один конкретный файл до и после более новой политики: примерно 280 МБ превратились примерно в 50 МБ. Для именно этой пары в восстановленных старых логах не сохранилась строка с длительностью и битрейтом, поэтому я не придумываю её задним числом. Самого изменения размера достаточно: этот файл стал примерно в 5,6 раза меньше.
Кейс ~350→238 МБ относится к более раннему этапу оптимизации одного и того же файла на той стадии цепочки обработки. Получившееся видео было примерно 1264×720, 30 fps, около 500 секунд, без звука и примерно 3,8 Mbps. Арифметика сходится: 3,8 Mbps на ~500 секунд дают примерно 238 МБ. Это уже экономило 32% относительно ~350 МБ, но для моей цели по сетевому трафику 238 МБ всё равно были слишком большими.
Старый корпус также показывал, что огромные H.264 были не одним случайным выбросом. В одном аудите было 238 видео из рабочей системы общим объёмом 6,37 ГБ: 117 H.264 и 121 AV1. 101 файл весил не меньше 20 МБ, 34 — не меньше 50 МБ. Несколько крупных H.264 выглядели так:
| Размер | Длительность | Средний битрейт |
|---|---|---|
| 121,0 МБ | 4:25 | 3,83 Mbps |
| 101,2 МБ | 5:19 | 2,659 Mbps |
| 92,78 МБ | 4:44 | 2,735 Mbps |
| 90,10 МБ | 3:55 | 3,206 Mbps |
| 89,74 МБ | 4:41 | 2,676 Mbps |
Это разные видео, поэтому таблица даёт контекст, а не прямое сравнение до и после. Но она показывает важную вещь: старый пример с 3,8 Мбит/с не был случайностью. У нескольких крупных H.264 из прежней библиотеки действительно встречался диапазон примерно 2,6–3,8 Мбит/с.
Главная оптимизация была не во флаге FFmpeg
Самое важное изменение было в том, как я стал считать стоимость.
Кодирование происходит один раз. Передача файла — при каждом запросе.
Для видео в реальном времени тратить намного больше CPU ради небольшой экономии битрейта может быть невыгодно. Мои файлы кодируются офлайн, а затем отдаются снова и снова. В такой модели сэкономленные десять минут кодирования почти ничего не стоят, если более быстрый вариант делает каждый последующий запрос тяжелее.
Поэтому -preset veryslow для меня имеет смысл. Я готов один раз потратить больше CPU, если x264 благодаря этому найдёт более эффективное представление. Браузер не повторяет работу кодировщика по поиску оптимального варианта — он только декодирует готовый битовый поток.
Правило стало простым: не жалеть вычислений на этапе, который выполняется один раз, и экономить байты на этапе, который повторяется постоянно.
Почему я остался на H.264, а не погнался за новым кодеком
Я не утверждаю, что H.264 лучше всех сжимает видео. Это не так. Более новые кодеки особенно интересны, когда система доставки может хранить несколько вариантов файла и выбирать подходящий для конкретного устройства.
У меня другое ограничение: один URL, один файл, один кодек и как можно меньше проблем с воспроизведением у аудитории, которая в основном смотрит с мобильных устройств.
Для такой задачи H.264 в MP4 по-прежнему остаётся очень надёжной базой. Apple сейчас рекомендует веб-разработчикам использовать MP4-файлы с H.264 для статического видео в Safari. В актуальной документации Android H.264 поддерживается в MP4, а декодер профиля Main обязателен начиная с Android 6.0; в рекомендациях по воспроизведению H.264 также указаны 1280×720 при 30 fps для HD, при этом отдельно отмечено, что HD доступно не на всех устройствах. См. поддерживаемые Android форматы мультимедиа.
Это не означает, что современные устройства ограничены профилем Main или уровнем 3.1. Например, в рекомендациях Apple для HLS обычно предпочтителен профиль High, а не Main или Baseline. Main@3.1 я выбрал потому, что хотел сознательно умеренные требования к декодеру для одного статического MP4, а не потому, что Apple требует именно этот профиль.
Я перестал кодировать пиксели, которых не должно было быть
Разрешение оказалось одним из самых сильных рычагов. Верхняя граница у меня — примерно 1280×720 для горизонтального видео, 720×1280 для вертикального и около 960×960 для квадратного или смешанного по ориентации материала.
Но важнее другое правило: никогда не увеличивать исходник только ради того, чтобы дотянуть его до верхней границы.
Если исходник имеет размер 900×600, увеличение до 1280×720 не вернёт детали. Оно лишь создаст дополнительные отсчёты, которые кодировщику придётся описывать. Исходник 1920×1080 можно уменьшить до класса 720p, а 900×600 вполне может остаться примерно 900×600. Верхняя граница — это максимум, а не цель.
Звучит очевидно, но удаление ненужных пикселей может дать больше, чем многие экзотические настройки кодировщика.
Этот потолок выбран не ради красивого круглого числа. Кадр 1920×1080 содержит 2 073 600 пикселей, а 1280×720 — 921 600. Переход с 1080p на 720p ещё до работы кодировщика убирает примерно 55,6% пространственных отсчётов.
Я рассматривал и 540p как общий вариант. Но 960×540 — это всего 518 400 пикселей: на 43,75% меньше, чем 1280×720, то есть остаётся 56,25% отсчётов 720p. Для рисованного материала эти отсчёты описывают тонкие линии, глаза, волосы, пальцы, лица и резкие контуры. Если мне всё ещё нужно уменьшить размер, я сначала проверю немного более высокий CRF, а не стану вслепую выбрасывать ещё 43,75% пространственной информации. Квантование можно изменить при следующем кодировании; детали, удалённые уменьшением разрешения, уже не вернуть.
Поэтому 720p-класс — мой безопасный общий потолок, а не утверждение, что 540p плох. Для конкретного файла измеренный вариант 540p вполне может оказаться лучшим. Я лишь не делаю такое необратимое уменьшение разрешения правилом для всей библиотеки без данных.
Я перестал платить за кадры, которых на самом деле нет в исходнике
Частота кадров — ещё один множитель. Если в анимации примерно 16 реально отличающихся визуальных состояний в секунду, хранение её в 30 или 60 fps само по себе не сделает движение лучше. Часто это лишь добавляет повторяющиеся или синтезированные временные отсчёты, которые всё равно нужно закодировать.
Моё правило — сохранять полезный ритм исходника и обычно не выходить за 30 fps. Для такого материала 12, 15, 16, 18, 20, 24, 25 или 30 fps могут быть нормальным выбором, если именно такая частота соответствует исходнику.
В готовом файле я также предпочитаю аккуратную постоянную частоту кадров. VFR сам по себе не является проблемой; CFR просто упрощает в моей обработке временные метки, подсчёт кадров, проверку длительности, перемотку и последующую проверку.
Общий принцип важнее любого конкретного FPS: не платить трафиком за временную информацию, которой нет в исходнике.
CRF 28 — выбор под мою задачу, а не магическое число
Я не хотел загонять каждый ролик в один и тот же целевой битрейт. Почти неподвижной иллюстрации и сцене со сложным движением не нужно одинаковое количество битов, чтобы выглядеть приемлемо.
Поэтому я использую режим CRF в x264 и для своего иллюстрированного контента, где трафик важнее всего, остановился примерно на -crf 28. В документации FFmpeg CRF для libx264 описан как режим управления скоростью потока с постоянным качеством; см. документацию FFmpeg по кодекам.
CRF 28 — намеренно агрессивное значение. Я бы не переносил его вслепую на материал с плёночным зерном, шумное видео с камеры, очень мелкий текст на экране или задачу, где точность изображения важнее трафика.
У меня нет универсальной перцептивной метрики, которая доказывала бы, что CRF 28 незаметен для глаза. Про свой материал я могу сказать гораздо уже: файлы стали существенно меньше и при обычном воспроизведении всё ещё выглядели для меня нормально. Это практическое наблюдение, а не утверждение, что CRF 28 визуально без потерь.
veryslow дорог для кодировщика, но не обязательно для декодера
Мой пресет — -preset veryslow. Более медленный пресет даёт x264 больше времени на поиск эффективных вариантов предсказания и кодирования. Цена — CPU и время на стороне кодировщика.
Здесь важно разделять две вещи: вычислительная работа кодировщика и сложность декодирования — не одно и то же.
Я могу позволить x264 очень долго искать лучший вариант и при этом отдельно ограничить готовый поток. Мои консервативные требования к результату такие:
H.264, профиль Main
Level 3.1
8-битный yuv420p
avc1
refs = 4
B-кадры = 5
B-pyramid = normal
открытый GOP = отключён
FFmpeg позволяет независимо настраивать CRF, предустановки, настройку под тип контента, ограничения профиля, опорные кадры и B-кадры. Я и рассматриваю их отдельно: пусть кодировщик ищет тщательно, а воспроизведение остаётся обычным и предсказуемым.
GOP и VBV — ограничители, а не основной способ управлять качеством
Для этих коротких прогрессивных роликов я использую максимальный GOP примерно в пять секунд: около -g 150 при 30 fps, -g 120 при 24 fps или -g 80 при 16 fps.
Это решение под мою задачу, а не универсальное правило. У адаптивной потоковой передачи другие ограничения: например, рекомендации Apple по HLS предлагают IDR каждые две секунды. Я не переношу это требование HLS автоматически на короткие статические MP4 с прогрессивной загрузкой.
Кроме того, я использую примерно такие значения:
-maxrate:v 4M
-bufsize:v 8M
Это верхние ограничения на необычные всплески битрейта. Они не означают «кодировать всё в 4 Mbps». Обычное распределение битов по-прежнему определяет CRF, поэтому простые ролики могут получаться очень маленькими.
Контейнер MP4 я тоже сделал максимально обычным
Я явно задаю avc1. В актуальной документации Apple по HLS рекомендуется использовать форматы выборок вроде avc1 вместо avc3. На размер моих файлов это заметно не повлияло, но соответствует цели получать обычный H.264 в MP4.
Также я использую -movflags +faststart. В документации FFmpeg по форматам сказано, что faststart переносит индекс moov MP4 в начало файла. В требованиях Android к потоковой передаче по HTTP для MPEG-4 также указано, что moov должен идти после ftyp, но до mdat.
ftyp
moov
mdat
Faststart не улучшает сжатие. Он делает прогрессивное воспроизведение по HTTP менее проблемным.
Для обычного SDR я использую 8-битный yuv420p и сигнализирую BT.709 с ограниченным видеодиапазоном. Если в ролике нет звука, я не создаю пустую аудиодорожку. Для этого иллюстрированного контента я также использую -tune animation; это настройка под тип материала, а не часть универсальных требований совместимости.
Основной профиль FFmpeg
Для иллюстрированного исходника с 30 fps центральная часть команды выглядит примерно так:
ffmpeg -i input \
-c:v libx264 \
-preset veryslow \
-tune animation \
-crf 28 \
-profile:v main \
-level:v 3.1 \
-pix_fmt yuv420p \
-tag:v avc1 \
-refs 4 \
-bf 5 \
-g 150 \
-maxrate:v 4M \
-bufsize:v 8M \
-x264-params "open-gop=0:b-pyramid=normal:nal-hrd=none" \
-color_range tv \
-color_primaries bt709 \
-color_trc bt709 \
-colorspace bt709 \
-an \
-movflags +faststart \
output.mp4
Масштабирование и частота кадров здесь намеренно не зашиты жёстко. Исходник 900×600 не нужно увеличивать только потому, что верхняя граница — 1280×720, а анимацию с естественно низкой частотой кадров не нужно принудительно переводить в 30 fps только потому, что в примере используется -g 150.
Команда — это реализация правил, а не сами правила.
Почему файлы стали в несколько раз меньше
Никакого чудо-флага не было.
Файлы уменьшились благодаря нескольким решениям, каждое из которых убрало свой тип лишних данных: ненужные пиксели, ненужные кадры, слишком консервативную цель по качеству, настройки кодировщика в пользу скорости вместо эффективности, слишком частые ключевые кадры и ненужные мне потоки.
Поэтому фраза «этот файл — H.264» на удивление мало говорит о его размере. Два H.264-кодирования одного исходника могут отличаться очень сильно, потому что название кодека ничего не говорит о разрешении, частоте кадров, управлении битрейтом, пресете, структуре GOP, профиле или подготовке исходника.
В моём случае решения вокруг кодека оказались важнее смены самого кодека.
Чего этот результат не доказывает
Я не проверял каждую настройку отдельно в контролируемом эксперименте, поэтому не могу честно сказать, какой точный процент экономии дали по отдельности veryslow, CRF 28, уменьшение разрешения и уменьшение частоты кадров.
Я также не могу утверждать, что любой результат при CRF 28 будет визуально неотличим от исходника. «Без заметной потери качества» — моё наблюдение для этого иллюстрированного материала при обычном размере просмотра, а не научная гарантия для любого видео.
И я не утверждаю, что один H.264-файл — правильная архитектура для каждого сайта. Несколько вариантов одного видео, адаптивная потоковая передача, HDR, 4K и согласование кодеков меняют компромиссы.
Я могу утверждать более узкую вещь: для библиотеки коротких иллюстрированных и анимированных роликов, где главное — трафик, аудитория в основном мобильная, время кодирования стоит дёшево, а предсказуемое воспроизведение важно, этот профиль уменьшил мои файлы в несколько раз, сохранив нормальный вид при обычном просмотре.
Как этот результат изменил мой подход к оптимизации
Раньше я воспринимал оптимизацию видео в основном как задачу по настройке кодировщика. Теперь я смотрю на неё как на стоимость файла за весь срок его жизни.
Кодировщик может сработать один раз. Байты могут пройти по сети тысячи или миллионы раз.
Из-за этого слово «дорого» начинает означать другое.
Я спокойно потрачу CPU один раз. Но мне гораздо меньше хочется при каждом последующем запросе отправлять пиксели, созданные увеличением изображения, кадры без дополнительной полезной информации о движении или битрейт, который этому контенту не нужен.
Кодек остался скучным и обычным: H.264 в MP4. Оптимизация произошла вокруг него.
Для этой задачи вывод важнее любого отдельного флага FFmpeg: оптимизировать нужно ту стоимость, которую платишь постоянно, а не ту, которую платишь один раз.