Як генерувати прев'ю-кадри для відео за допомогою FFmpeg

Без якісного прев'ю відео втрачає кліки та погіршує користувацький досвід. Ми автоматизуємо генерацію прев'ю-кадрів за допомогою FFmpeg, створюючи інформативні постери та sprite sheet для прогресс-бара. Наша команда реалізує проєкт під ключ — від налаштування до подальшої підтримки.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Як генерувати прев'ю-кадри для відео за допомогою FFmpeg
Середній
від 1 дня до 3 днів
Часті запитання

Зауважте: коли користувач завантажує відео, перше, що він бачить — прев'ю. Якщо його немає або воно неінформативне, це знижує залученість та SEO-показники. Без прев'ю сторінка відео втрачає до 30% кліків у пошуковій видачі, а Core Web Vitals страждають через відсутність LCP-оптимізації. Ми автоматизуємо витягнення прев'ю-кадрів за допомогою FFmpeg, щоб кожне відео отримувало якісний постер та sprite sheet для прогрес-бару. Наш досвід у веб-розробці понад 8 років, реалізовано 50+ проектів з відеопрев'ю на платформах від Laravel до Next.js. Зв'яжіться з нами для консультації — оцінимо ваш проект.

Для генерації прев'ю-кадрів важливо обрати правильний час і параметри. Основна складність — обрати інформативний кадр. Випадковий кадр часто темний або розмитий. FFmpeg вирішує це фільтром thumbnail, який аналізує кадри за метрикою SAD (Sum of Absolute Differences) і знаходить найкращий. Друга проблема — продуктивність: при великій кількості відео неефективно генерувати прев'ю синхронно. Ми використовуємо черги Laravel з Job, запускаючи генерацію після завантаження відео. Завдяки фоновій обробці сервер не блокується, а користувач одразу бачить стандартний placeholder до завершення генерації. Процес генерації прев'ю кадрів включає вибір фільтра та оптимізацію розміру.

Як згенерувати постер відео за допомогою FFmpeg?

Найпростіший варіант — кадр на певній часовій мітці:

ffmpeg -i input.mp4 -ss 00:00:05 -vframes 1 -q:v 2 thumbnail.jpg 

-ss 00:00:05 — позиція (5 секунд від початку). -vframes 1 — один кадр. -q:v 2 — якість JPEG (1 = найкраще, 31 = найгірше). Краще ставити позначку не на 0 секунд (часто чорний кадр) і не на останню секунду. Хороший евристичний підхід — 10% від довжини відео, але не менше 3 секунд і не більше 30.

Як FFmpeg обирає найкращий кадр?

FFmpeg вміє обирати кадри за метрикою зміни через фільтр thumbnail:

ffmpeg -i input.mp4 -vf "thumbnail=300" -vframes 1 thumbnail.jpg 

Згідно з документацією FFmpeg, фільтр thumbnail аналізує кожен 300-й кадр, обчислюючи SAD відносно попереднього, і обирає кадр з максимальною зміною. Це приблизно один раз на 10–12 секунд при 24 fps. Повільніше прямої вибірки за часом, але результат інформативніший.

Вибір оптимальної точки для постера

Ми рекомендуємо гібридний підхід: якщо відео містить ключову сцену (наприклад, логотип на початку), використовуйте фіксовану мітку. Інакше — автовибір. В обох випадках застосовуйте масштабування з padding для єдиного розміру.

Порівняємо методи вибору кадру:

Параметр Випадковий кадр Thumbnail-фільтр
Швидкість Миттєво ~2–5 сек на хвилину відео
Якість Часто темний/розмитий Репрезентативний
Використання Постер за замовчуванням Постер для важливих відео

Чому sprite sheet кращий за одиночні прев'ю?

Sprite sheet — одне зображення з сіткою кадрів, яке плеєр використовує для показу прев'ю при наведенні на прогрес-бар. Це скорочує кількість HTTP-запитів до одного, знижуючи навантаження на сервер і прискорюючи відгук. При довгих відео (від 10 хвилин) це критично — одиночні прев'ю генерували б десятки файлів.

Для відео довжиною 10 хвилин сітка 10×10 дає інтервал 6 секунд. Ширина одного кадру 160px, сітка 1600×900px (при 16:9 thumb). Клієнтський код обчислює позицію спрайта залежно від часу:

function getThumbnailPosition(currentTime, spriteData) {
  const { columns, rows, thumbWidth, interval, duration } = spriteData;
  const thumbHeight = Math.round(thumbWidth * 9 / 16);
  const frameIndex = Math.min(Math.floor(currentTime / interval), columns * rows - 1);
  const col = frameIndex % columns;
  const row = Math.floor(frameIndex / columns);
  return { x: col * thumbWidth, y: row * thumbHeight, width: thumbWidth, height: thumbHeight };
}

При hover на прогрес-бар достатньо встановити background-image і змістити фон на обчислені координати. В середньому це зменшує кількість запитів на 90% порівняно із завантаженням окремих мініатюр.

PHP-сервіс генерації прев'ю

Повний код VideoThumbnailService
namespace App\Services;

class VideoThumbnailService
{
    public function generatePoster(
        string $videoPath,
        string $outputPath,
        ?float $offsetSeconds = null,
        int $width = 1280,
        int $height = 720
    ): void {
        $duration = $this->getVideoDuration($videoPath);
        $offset = $offsetSeconds ?? max(3.0, $duration * 0.1);
        $offset = min($offset, $duration - 1.0);

        $scaleFilter = "scale={$width}:{$height}:force_original_aspect_ratio=decrease," .
            "pad={$width}:{$height}:(ow-iw)/2:(oh-ih)/2:black";

        $cmd = implode(' ', [
            'ffmpeg -y',
            '-ss ' . number_format($offset, 3, '.', ''),
            '-i ' . escapeshellarg($videoPath),
            '-vframes 1',
            "-vf " . escapeshellarg($scaleFilter),
            '-q:v 3',
            escapeshellarg($outputPath),
            '2>&1',
        ]);

        exec($cmd, $output, $exitCode);
        if ($exitCode !== 0 || !file_exists($outputPath)) {
            throw new \RuntimeException("Thumbnail generation failed: " . implode("\n", $output));
        }
    }

    public function generateSpriteSheet(
        string $videoPath,
        string $outputPath,
        int $columns = 10,
        int $rows = 10,
        int $thumbWidth = 160
    ): array {
        $duration = $this->getVideoDuration($videoPath);
        $totalFrames = $columns * $rows;
        $interval = $duration / $totalFrames;
        $fps = 1 / $interval;

        $filter = "fps={$fps},scale={$thumbWidth}:-1,tile={$columns}x{$rows}";

        $cmd = implode(' ', [
            'ffmpeg -y',
            '-i ' . escapeshellarg($videoPath),
            "-vf " . escapeshellarg($filter),
            '-vframes 1',
            '-q:v 5',
            escapeshellarg($outputPath),
            '2>&1',
        ]);

        exec($cmd, $output, $exitCode);
        if ($exitCode !== 0) {
            throw new \RuntimeException(implode("\n", $output));
        }

        return [
            'path' => $outputPath,
            'columns' => $columns,
            'rows' => $rows,
            'thumb_width' => $thumbWidth,
            'interval' => $interval,
            'duration' => $duration,
        ];
    }

    public function getVideoDuration(string $path): float
    {
        $cmd = "ffprobe -v error -show_entries format=duration -of csv=p=0 " . escapeshellarg($path);
        $output = shell_exec($cmd);
        return (float) trim($output ?? '0');
    }
}

Інтеграція в Laravel через Job

class GenerateVideoThumbnailsJob implements ShouldQueue {
    public int $timeout = 300;
    public int $tries = 2;

    public function __construct(private int $videoId) {}

    public function handle(VideoThumbnailService $service): void {
        $video = Video::findOrFail($this->videoId);
        $inputPath = Storage::disk('videos')->path($video->original_path);
        $dir = Storage::disk('videos')->path('thumbnails/' . $video->id);
        @mkdir($dir, 0755, true);

        $posterPath = "{$dir}/poster.jpg";
        $service->generatePoster($inputPath, $posterPath);

        $spritePath = "{$dir}/sprite.jpg";
        $spriteData = $service->generateSpriteSheet($inputPath, $spritePath);

        $video->update([
            'poster_path' => "thumbnails/{$video->id}/poster.jpg",
            'sprite_path' => "thumbnails/{$video->id}/sprite.jpg",
            'sprite_data' => $spriteData,
        ]);
    }
}

Метадані sprite (columns, rows, interval) зберігаються в JSONB-полі таблиці videos. Клієнт отримує їх разом з даними відео і використовує для рендерингу прев'ю на прогрес-барі.

Процес роботи

  1. Аналітика — вивчаємо структуру відео, вимоги до прев'ю (розмір, формат).
  2. Проектування — обираємо стек: FFmpeg + черга Laravel. Визначаємо параметри sprite sheet.
  3. Реалізація — пишемо сервіс і Job, інтегруємо з моделлю.
  4. Тестування — перевіряємо на реальних відео різної довжини, коригуємо евристики.
  5. Деплой — налаштовуємо чергу, моніторинг помилок.

Що входить в роботу

  • Налаштування FFmpeg з оптимальними параметрами для постера і sprite sheet.
  • Написання сервіс-класу VideoThumbnailService і Job для черги.
  • Інтеграція з моделлю відео (збереження метаданих в JSONB).
  • Оптимізація черг (черга high для пріоритетних відео, timeout 300 сек).
  • Тестування на відео різної тривалості та роздільної здатності.
  • Документація по API ендпоінтам та формату метаданих sprite.

Терміни та вартість

Терміни — від 3 до 5 днів під ключ. Вартість інтеграції починається від 200 євро. Економія на модерації контенту — до 70% витрат, що при 100 відео на місяць дає ~$500 економії. Вкладення окупаються за 2-3 місяці. Напишіть нам — оцінимо ваш проект.

Для наочності порівняємо підходи:

Параметр Одиночний постер Sprite sheet
Кількість файлів 1 1
HTTP-запитів 1 1
Генерація Швидко Довше (аналіз усіх кадрів)
Використання Обкладинка відео Прогрес-бар прев'ю
Розмір файлу Малий (до 100 КБ) Великий (до 5 МБ)

Ми гарантуємо якісну генерацію, що відповідає сучасним стандартам. Замовте впровадження генерації прев'ю для вашого відео-контенту — зв'яжіться з нами для консультації.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060