В приложении на 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);
}