Задать вопрос
nefone
@nefone
Junior PHP разработчик

Конвертация видео с помощью FFMpeg в Yii2?

В приложении на Yii2 фреймворке есть Job, который в очереди запускает на сервере кодирование видео из MP4 в HLS.
С небольшими видео длительностью до 11 мин и весом до 2 Гб, справляется хорошо, но если пробовать загрузить видео допустим 20 мин, процесс кодирования не доходит до конца (всё указывает на то, что FFmpeg завершается).

Лимиты время выполнения и использования памяти были увеличены: set_time_limit(14400) и ini_set('memory_limit', '2048M');

Может подскажите куда копать и как можно решить такую проблему?

Сам код команды в методе execute() выглядит так:
$cmd = "ffmpeg -i {$originalPathEscaped} " .
                "-c:v libx264 -c:a aac " .
                "-preset medium -crf 23 -g 48 -keyint_min 48 -threads 0 " .
                "-hls_time 6 " .
                "-hls_list_size 0 " .
                "-hls_segment_filename {$hlsPathEscaped}segment_%03d.ts " .
                "-f hls " .
                "-stats " .
                "{$hlsMasterEscaped} 2>&1" ;

            // Запускаем процесс с мониторингом
            $this->runWithMonitoring($cmd, $logFile, $duration, $video, $startTime);


Сам метод который запускает процесс FFMpeg для кодирования видео:
/**
     * Запускает процесс FFmpeg и отслеживает его прогресс в реальном времени.
     *
     * Читает вывод процесса построчно, парсит прогресс (`out_time_ms`), обновляет логи,
     * проверяет таймаут выполнения и отсутствие активности (возможное зависание).
     *
     * @param string $cmd Команда для запуска через proc_open
     * @param string $logFile Путь к файлу лога FFmpeg
     * @param float $duration Общая длительность видео в секундах
     * @param Videos $video Модель видео для возможного обновления прогресса
     * @param int $startTime Время начала выполнения задания (метка времени)
     *
     * @return void
     *
     * @throws \Exception Если процесс не запустился, завершился с ошибкой, завис или превысил лимит времени
     */
    private function runWithMonitoring(string $cmd, string $logFile, float $duration, Videos $video, int $startTime): void
    {
        // Запускаем процесс
        $descriptors = [
            1 => ['pipe', 'w'], // stdout
            2 => ['pipe', 'w'], // stderr
        ];

        $this->process = proc_open($cmd, $descriptors, $pipes);

        if (!is_resource($this->process)) {
            throw new \Exception("Failed to start FFmpeg process");
        }

        $lastProgress = 0;
        $lastActivity = time();
        $output = '';

        // Читаем вывод в реальном времени
        while (!feof($pipes[1])) {
            $line = fgets($pipes[1], 4096);
            if ($line !== false) {
                $output .= $line;
                $lastActivity = time();

                // Парсим прогресс
                if (preg_match('/out_time_ms=(\d+)/', $line, $matches)) {
                    $progress = $this->calculateProgress((int)$matches[1], $duration);
                    if ($progress > $lastProgress + 2) {
                        $lastProgress = $progress;
                        Yii::info("Conversion progress: {$progress}% for video #{$this->videoId}", 'video-convert');
                    }
                }

                // Проверяем на ошибки
                if (stripos($line, 'error') !== false) {
                    Yii::warning("FFmpeg: " . trim($line), 'video-convert');
                }
            }

            // Проверяем таймаут
            if (time() - $startTime > $this->maxExecutionTime) {
                $this->killProcess();
                throw new \Exception("Conversion timeout exceeded {$this->maxExecutionTime}s");
            }

            // Проверяем активность (если 2 минуты нет вывода - возможно зависло)
            if (time() - $lastActivity > 120) {
                $this->killProcess();
                throw new \Exception("FFmpeg process stalled - no output for 2 minutes");
            }
        }

        while (!feof($pipes[1]) || !feof($pipes[2])) {
            // Читаем оба потока
            if (!feof($pipes[1])) {
                $line = fgets($pipes[1], 4096);
                if ($line !== false) {
                    $output .= $line;
                    $lastActivity = time();
                    $this->parseProgressLine($line, $duration);
                }
            }
            if (!feof($pipes[2])) {
                $line = fgets($pipes[2], 4096);
                if ($line !== false) {
                    $output .= $line;
                    $lastActivity = time();
                    Yii::warning("FFmpeg stderr: " . trim($line), 'video-convert');
                }
            }
            if (time() - $startTime > $this->maxExecutionTime) {
                $this->killProcess();
                throw new \Exception("Conversion timeout exceeded {$this->maxExecutionTime}s");
            }
        }

        fclose($pipes[1]);
        fclose($pipes[2]);

        // Ждём завершения процесса
        $returnCode = proc_close($this->process);

        if ($returnCode !== 0) {
            // Логируем последние строки вывода
            $lines = explode("\n", $output);
            $lastLines = implode("\n", array_slice($lines, -50));
            throw new \Exception("FFmpeg exited with code {$returnCode}. Last output:\n{$lastLines}");
        }

        // Сохраняем лог для отладки
        file_put_contents($logFile, $output);
    }
  • Вопрос задан
  • 129 просмотров
Подписаться 1 Сложный 2 комментария
Помогут разобраться в теме Все курсы
  • Onskills
    PHP: базовый курс
    1 месяц
    Далее
  • HTML Academy
    Бэкенд-разработчик для Addamant
    6 месяцев
    Далее
  • teamcoding
    TC200PY Разработка PHP веб-приложений на Yii2. Шаблон приложения advanced
    3 месяца
    Далее
Решения вопроса 1
opium
@opium
Просто люблю качественно работать
Похоже, stall-чек убивает процесс: -stats пишет прогресс в stderr, а 2>> logfile стоит ПОСЛЕ 2>&1 и перебивает его — stderr идёт в файл, не в pipes[1]. PHP видит пустой stdout 120+ секунд и думает что процесс завис.

Добавь -progress pipe:1 (вместо -stats) — тогда out_time_ms=N пойдёт в stdout, stall-check получит активность и процесс не убьют зря.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы