Назад в блог
13 августа 2026 г.Sergei Solod9 мин чтения

Как я уменьшил H.264-видео в несколько раз без заметной потери качества

Я оставил H.264, но пересобрал профиль кодирования вокруг CRF 28, x264 veryslow, ограничения разрешения до класса 720p, полезного FPS и консервативной сложности декодирования. Главный вывод оказался экономическим: кодирование оплачивается один раз, а трафик — при каждом просмотре.

H.264FFmpegСжатие видеоВеб-производительностьx264

Сначала результат выглядел так, будто я перешёл на более новый кодек: видео стали в несколько раз меньше, при обычном просмотре всё продолжало выглядеть нормально, а заметной потери качества на привычном размере я не видел.

Но я не переходил на AV1, HEVC или VP9. Я всё ещё использовал H.264 внутри MP4.

Изменился не кодек, а всё вокруг него. Я пересобрал политику кодирования под конкретную нагрузку: короткие иллюстрированные и анимированные ролики, большая доля мобильной аудитории, трафик как главный повторяющийся расход и почти полное отсутствие требований к скорости однократного офлайн-кодирования.

В итоге baseline получился намеренно консервативным для воспроизведения и намеренно дорогим для энкодера: H.264 Main Profile @ Level 3.1, avc1, 8-bit yuv420p, CRF 28, x264 veryslow, ограничение разрешения примерно классом 720p, только полезный FPS обычно не выше 30, ограниченные refs и B-frames и faststart для прогрессивной отдачи MP4.

Главная оптимизация была не в параметре FFmpeg

Самое важное изменение произошло в том, как я вообще считал стоимость.

Кодирование происходит один раз. Передача файла — при каждом просмотре.

Для real-time видео тратить намного больше CPU ради небольшой экономии bitrate может быть плохим обменом. Мои файлы кодируются офлайн, а затем раздаются снова и снова. В такой модели сэкономить десять минут на encode почти бессмысленно, если более быстрый encode делает каждый будущий запрос тяжелее.

Поэтому для меня имеет смысл -preset veryslow. Я готов один раз потратить CPU, если x264 за счёт этого найдёт более эффективное представление. Браузер не повторяет дорогой поиск энкодера — он только декодирует готовый битстрим.

Правило стало простым: тратить вычисления на однократном этапе и жадно экономить байты на повторяющемся.

Почему я оставил H.264, а не погнался за самым новым кодеком

Я не утверждаю, что H.264 — самый эффективный кодек по степени сжатия. Это не так. Более новые кодеки интересны, когда система доставки может хранить несколько renditions и выбирать подходящую для клиента.

У меня другое ограничение: один URL, один файл, один кодек и как можно меньше сюрпризов при воспроизведении у mobile-heavy аудитории.

Для такой задачи H.264 в MP4 остаётся очень безопасным baseline. Apple сейчас прямо рекомендует веб-разработчикам использовать MP4 с H.264 для статического видео в Safari. В актуальной документации Android H.264 поддерживается в MP4, а декодер Main Profile обязателен начиная с Android 6.0; в рекомендациях Android также есть 1280×720 при 30 fps как HD-конфигурация H.264. См. поддерживаемые форматы Android.

Это не значит, что современные устройства ограничены Main Profile или Level 3.1. Например, Apple для HLS обычно рекомендует High Profile вместо Main или Baseline. Main@3.1 я выбрал потому, что хотел намеренно умеренные требования к декодеру для одного статического MP4, а не потому, что этого требует Apple.

Я перестал кодировать пиксели, которые не нужны

Разрешение оказалось одним из самых сильных рычагов. Мой верхний предел стал примерно 1280×720 для горизонтального видео, 720×1280 для вертикального и около 960×960 для квадратного или смешанного контента.

Но важнее другое правило: никогда не апскейлить исходник только ради достижения этого потолка.

Если источник 900×600, превращение его в 1280×720 не возвращает деталей. Оно лишь создаёт дополнительные сэмплы, которые потом приходится описывать энкодеру. Исходник 1920×1080 можно уменьшить до класса 720p, а 900×600 оставить примерно 900×600. Потолок — это максимум, а не target.

Звучит просто, но удаление ненужных пикселей часто даёт больше, чем множество редких encoder tweaks.

Я перестал платить за кадры, которых по сути нет в исходнике

FPS — ещё один множитель. Если в анимации реально около 16 полезных визуальных состояний в секунду, хранение её как 30 или 60 fps само по себе не создаёт более хорошего движения. Оно может в основном добавить повторённые или синтезированные временные сэмплы, которые всё равно нужно кодировать.

Моё правило — сохранять полезный темп исходника и обычно не подниматься выше 30 fps. Для такого материала 12, 15, 16, 18, 20, 24, 25 или 30 fps могут быть нормальными значениями, если именно они адекватно описывают источник.

В готовом output я также предпочитаю чистый CFR. VFR сам по себе не сломан; просто CFR упрощает timestamps, число кадров, проверку длительности, seeking и последующую валидацию в моём pipeline.

Обобщаемый принцип важнее конкретного FPS: не платить трафиком за временную информацию, которой нет в исходнике.

CRF 28 — решение под workload, а не магическое число

Я не хотел загонять каждый ролик в одинаковый target bitrate. Почти статичная иллюстрация и сцена со сложным движением требуют разного количества битов, чтобы выглядеть приемлемо.

Поэтому я использую CRF-режим x264 и для этого bandwidth-first иллюстрированного контента остановился примерно на -crf 28. FFmpeg описывает CRF в libx264 как constant-quality rate control; см. официальную документацию FFmpeg по кодекам.

CRF 28 — намеренно агрессивное значение. Я бы не копировал его вслепую для film grain, шумного видео с камеры, мелкого текста на screen recording или workload, где fidelity важнее bandwidth.

У меня также нет универсального perceptual score, доказывающего прозрачность CRF 28. Я могу утверждать более узкую вещь: на моём контенте файлы стали значительно меньше и при обычном просмотре продолжали выглядеть нормально. Это практическое наблюдение, а не утверждение, что CRF 28 визуально lossless.

Veryslow дорог для энкодера, но не автоматически для декодера

Мой preset — -preset veryslow. Более медленный preset даёт x264 больше возможностей искать эффективные варианты prediction и кодирования. Цена — CPU и время энкодера.

Ключевое различие: сложность энкодера и сложность декодера — не одно и то же.

Я могу заставить x264 очень долго работать и при этом отдельно ограничить готовый поток. Мой консервативный output contract:

H.264 Main Profile
Level 3.1
8-bit yuv420p
avc1
refs = 4
B-frames = 5
B-pyramid = normal
open GOP = disabled

FFmpeg позволяет независимо задавать CRF, presets, tuning, profile restrictions, reference frames и B-frames. Именно так я на них и смотрю: пусть encoder ищет максимально тщательно, а playback side остаётся обычным.

GOP и VBV — ограничители, а не основной контроль качества

Для этих коротких progressive роликов я использую максимальный GOP примерно в пять секунд: около -g 150 при 30 fps, -g 120 при 24 fps или -g 80 при 16 fps.

Это выбор под конкретную нагрузку, а не универсальное правило. У adaptive streaming другие ограничения; например, Apple для HLS рекомендует IDR каждые две секунды. Я не переношу это HLS-правило автоматически на короткие статические progressive MP4.

Также я использую примерно:

-maxrate:v 4M
-bufsize:v 8M

Эти значения — потолок против необычных всплесков bitrate. Они не означают «кодировать всё в 4 Mbps». Обычным распределением битов продолжает управлять CRF, поэтому лёгкие ролики могут становиться очень маленькими.

Сам MP4-контейнер я тоже сделал максимально скучным

Я явно использую avc1. В актуальной документации Apple для HLS рекомендуются sample formats вроде avc1, а не avc3. Это не причина уменьшения файлов, но соответствует общей цели — обычный H.264-in-MP4.

Ещё я использую -movflags +faststart. В документации FFmpeg по форматам сказано, что faststart переносит MP4-индекс moov в начало файла. В требованиях Android к HTTP streaming также указано, что для MPEG-4 moov должен идти перед mdat, после ftyp.

ftyp
moov
mdat

Faststart не улучшает сжатие. Он делает прогрессивное HTTP-воспроизведение менее проблемным.

Для обычного SDR я использую 8-bit yuv420p и сигнализирую BT.709 с limited/video range. Если звук не нужен, я не создаю аудиотрек. Для иллюстрированного контента я также использую -tune animation, но считаю это настройкой под тип контента, а не частью универсального compatibility-контракта.

Основной профиль 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

Scaling и frame-rate stage здесь специально не захардкожены. Исходник 900×600 не нужно растягивать только потому, что потолок 1280×720, а естественно низко-FPS анимацию не нужно насильно превращать в 30 fps только потому, что в примере стоит -g 150.

Команда — реализация политики, а не сама политика.

Почему файлы стали в несколько раз меньше

Одной чудо-настройки не было.

Уменьшение получилось из суммы решений, каждое из которых убирало свой тип отходов: ненужные пиксели, ненужные кадры, мышление фиксированным bitrate, дешёвые encoder settings, слишком частые keyframes и потоки, которые мне не нужны.

Поэтому фраза «это H.264» на удивление мало говорит о размере. Два H.264 encode одного источника могут весить очень по-разному, потому что название кодека не описывает разрешение, FPS, rate control, preset, GOP, profile или подготовку исходника.

В моём случае эти окружающие решения оказались важнее, чем замена самого кодека.

Чего этот результат не доказывает

Я не изолировал каждую настройку в отдельном контролируемом эксперименте, поэтому не могу честно приписать точный процент экономии отдельно veryslow, CRF 28, уменьшению разрешения или FPS.

Я также не могу утверждать, что любой output при CRF 28 perceptually transparent. «Без заметной потери качества» — моё наблюдение для этого иллюстрированного workload при обычном размере просмотра, а не научная гарантия для любого видео.

И я не утверждаю, что один H.264-файл — правильная архитектура для каждого сайта. Multiple renditions, adaptive streaming, HDR, 4K и codec negotiation меняют trade-offs.

Результат уже и полезнее: для bandwidth-first, mobile-heavy библиотеки коротких иллюстрированных и анимированных роликов, где время кодирования дёшево, а предсказуемое воспроизведение важно, этот профиль сделал мои файлы в несколько раз меньше, сохранив нормальный вид при обычном просмотре.

Правило, которым я пользуюсь теперь

Раньше я думал об оптимизации видео в основном как о настройке энкодера. Теперь я думаю о стоимости всего жизненного цикла файла.

Encoder может отработать один раз. Байты могут пройти по сети тысячи или миллионы раз.

После этого слово «дорого» означает совсем другое.

Я готов один раз потратить CPU. Но я гораздо меньше готов при каждом будущем запросе отправлять пиксели, появившиеся из-за апскейла, кадры, которые не добавляют полезного движения, или bitrate, который контенту не нужен.

Кодек остался скучным: H.264 в MP4. Оптимизация произошла вокруг него.

Для моего workload вывод оказался полезнее любого отдельного FFmpeg flag: оптимизировать нужно стоимость, которую платишь постоянно, а не ту, которую платишь один раз.