Может ли ffmpeg показать индикатор выполнения?
Я конвертирую файл.avi в файл.flv с помощью ffmpeg. Поскольку преобразование файла занимает много времени, я хотел бы отобразить индикатор выполнения. Может кто-нибудь, пожалуйста, наставьте меня, как поступить примерно так же.
Я знаю, что ffmpeg каким-то образом должен выводить прогресс в текстовом файле, и я должен прочитать его с помощью вызовов ajax. Но как мне получить ffmpeg для вывода прогресса в текстовый файл?
Большое спасибо.
18 ответов
Я играл с этим в течение нескольких дней. Эта штука с "ffmpegprogress" помогла, но мне было очень трудно начать работать с моими настройками, и трудно было прочитать код.
Чтобы показать прогресс ffmpeg, вам нужно сделать следующее:
- запустите команду ffmpeg из php, не ожидая ответа (для меня это была самая сложная часть)
- скажите ffmpeg отправить его вывод в файл
- из внешнего интерфейса (AJAX, Flash и т. д.) попадайте либо в этот файл напрямую, либо в файл php, который может извлекать информацию о прогрессе из вывода ffmpeg.
Вот как я решил каждую часть:
1. Я получил следующую идею от "ffmpegprogress". Вот что он сделал: один файл PHP вызывает другой через сокет http. Второй фактически запускает exec, и первый файл просто зависает на нем. Для меня его реализация была слишком сложной. Он использовал "fsockopen". Мне нравится CURL. Итак, вот что я сделал:
$url = "http://".$_SERVER["HTTP_HOST"]."/path/to/exec/exec.php";
curl_setopt($curlH, CURLOPT_URL, $url);
$postData = "&cmd=".urlencode($cmd);
$postData .= "&outFile=".urlencode("path/to/output.txt");
curl_setopt($curlH, CURLOPT_POST, TRUE);
curl_setopt($curlH, CURLOPT_POSTFIELDS, $postData);
curl_setopt($curlH, CURLOPT_RETURNTRANSFER, TRUE);
// # this is the key!
curl_setopt($curlH, CURLOPT_TIMEOUT, 1);
$result = curl_exec($curlH);
Установка CURLOPT_TIMEOUT в 1 означает, что ответ будет ждать 1 секунду. Желательно, чтобы это было ниже. Существует также CURLOPT_TIMEOUT_MS, который занимает миллисекунды, но у меня это не сработало.
Через 1 секунду CURL зависает, но команда exec все еще выполняется. Часть 1 решена.
Кстати, несколько человек предлагали использовать команду nohup для этого. Но это не сработало для меня.
*ТАКЖЕ! Наличие php-файла на вашем сервере, который может выполнять код непосредственно из командной строки, является очевидной угрозой безопасности. У вас должен быть пароль или каким-либо образом закодировать данные поста.
2. Приведенный выше скрипт "exec.php" также должен указывать ffmpeg выводить в файл. Вот код для этого:
exec("ffmpeg -i path/to/input.mov path/to/output.flv 1> path/to/output.txt 2>&1");
Обратите внимание на "1> path/to/output.txt 2>&1". Я не эксперт по командной строке, но из того, что я могу сказать, в этой строке написано "отправлять нормальный вывод в этот файл и отправлять ошибки в одно и то же место". Проверьте этот URL для получения дополнительной информации: http://tldp.org/LDP/abs/html/io-redirection.html
3. Из внешнего интерфейса вызовите скрипт php, указав в нем местоположение файла output.txt. Этот php-файл будет извлекать прогресс из текстового файла. Вот как я это сделал:
// # get duration of source
preg_match("/Duration: (.*?), start:/", $content, $matches);
$rawDuration = $matches[1];
// # rawDuration is in 00:00:00.00 format. This converts it to seconds.
$ar = array_reverse(explode(":", $rawDuration));
$duration = floatval($ar[0]);
if (!empty($ar[1])) $duration += intval($ar[1]) * 60;
if (!empty($ar[2])) $duration += intval($ar[2]) * 60 * 60;
// # get the current time
preg_match_all("/time=(.*?) bitrate/", $content, $matches);
$last = array_pop($matches);
// # this is needed if there is more than one match
if (is_array($last)) {
$last = array_pop($last);
}
$curTime = floatval($last);
// # finally, progress is easy
$progress = $curTime/$duration;
Надеюсь, это кому-нибудь поможет.
На русском есть статья, в которой рассказывается, как решить вашу проблему.
Дело в том, чтобы поймать Duration
значение до кодирования и ловить time=...
значения во время кодирования.
--skipped--
Duration: 00:00:24.9, start: 0.000000, bitrate: 331 kb/s
--skipped--
frame= 41 q=7.0 size= 116kB time=1.6 bitrate= 579.7kbits/s
frame= 78 q=12.0 size= 189kB time=3.1 bitrate= 497.2kbits/s
frame= 115 q=13.0 size= 254kB time=4.6 bitrate= 452.3kbits/s
--skipped--
Это очень просто, если вы используете команду pipeview. Для этого нужно преобразовать
ffmpeg -i input.avi {arguments}
в
pv input.avi | ffmpeg -i pipe:0 -v warning {arguments}
Не нужно заниматься кодированием!
Вы можете сделать это с ffmpeg
"s -progress
аргумент и nc
WATCHER_PORT=9998
DURATION= $(ffprobe -select_streams v:0 -show_entries "stream=duration" \
-of compact $INPUT_FILE | sed 's!.*=\(.*\)!\1!g')
nc -l $WATCHER_PORT | while read; do
sed -n 's/out_time=\(.*\)/\1 of $DURATION/p')
done &
ffmpeg -y -i $INPUT_FILE -progress localhost:$WATCHER_PORT $OUTPUT_ARGS
FFmpeg использует stdout для вывода медиа-данных и stderr для регистрации / информации о прогрессе. Вам просто нужно перенаправить stderr в файл или в stdin процесса, способного его обработать.
С оболочкой Unix это что-то вроде:
ffmpeg {ffmpeg arguments} 2> logFile
или же
ffmpeg {ffmpeg arguments} 2| processFFmpegLog
В любом случае, вы должны запустить ffmpeg как отдельный поток или процесс.
К сожалению, ffmpeg
сам по себе все еще не может отображать индикатор выполнения, а также многие из вышеупомянутых решений на основе bash- или python-решений для пробелов устарели и перестали функционировать.
Таким образом, я рекомендую попробовать совершенно новый https://github.com/sidneys/ffmpeg-progressbar-cli:
Это обертка для ffmpeg
исполняемый файл, показывающий цветной индикатор по центру и оставшееся время.
Кроме того, это открытый исходный код, основанный на Node.js и активно развивающийся, позволяющий справиться с большинством из упомянутых причуд (полное раскрытие информации: я являюсь его нынешним ведущим разработчиком).
Если вам просто нужно скрыть всю информацию и показать прогресс по умолчанию, как ffmpeg в последней строке, вы можете использовать -stats
опция:
ffmpeg -v warning -hide_banner -stats ${your_params}
Я нашел ffpb
Пакет Python (pip install ffpb
), который прозрачно передает аргументы в FFmpeg. Благодаря своей прочности он не требует особого ухода. Последний выпуск от 29 апреля 2019 г.
javascript должен сказать php начать конвертацию [1], а затем сделать [2]...
[1] php: начать преобразование и записать статус в файл (см. Выше):
exec("ffmpeg -i path/to/input.mov path/to/output.flv 1>path/to/output.txt 2>&1");
Для второй части нам нужно просто JavaScript, чтобы прочитать файл. В следующем примере используется dojo.request для AJAX, но вы также можете использовать jQuery, vanilla или что-то еще:
[2] js: захватить прогресс из файла:
var _progress = function(i){
i++;
// THIS MUST BE THE PATH OF THE .txt FILE SPECIFIED IN [1] :
var logfile = 'path/to/output.txt';
/* (example requires dojo) */
request.post(logfile).then( function(content){
// AJAX success
var duration = 0, time = 0, progress = 0;
var resArr = [];
// get duration of source
var matches = (content) ? content.match(/Duration: (.*?), start:/) : [];
if( matches.length>0 ){
var rawDuration = matches[1];
// convert rawDuration from 00:00:00.00 to seconds.
var ar = rawDuration.split(":").reverse();
duration = parseFloat(ar[0]);
if (ar[1]) duration += parseInt(ar[1]) * 60;
if (ar[2]) duration += parseInt(ar[2]) * 60 * 60;
// get the time
matches = content.match(/time=(.*?) bitrate/g);
console.log( matches );
if( matches.length>0 ){
var rawTime = matches.pop();
// needed if there is more than one match
if (lang.isArray(rawTime)){
rawTime = rawTime.pop().replace('time=','').replace(' bitrate','');
} else {
rawTime = rawTime.replace('time=','').replace(' bitrate','');
}
// convert rawTime from 00:00:00.00 to seconds.
ar = rawTime.split(":").reverse();
time = parseFloat(ar[0]);
if (ar[1]) time += parseInt(ar[1]) * 60;
if (ar[2]) time += parseInt(ar[2]) * 60 * 60;
//calculate the progress
progress = Math.round((time/duration) * 100);
}
resArr['status'] = 200;
resArr['duration'] = duration;
resArr['current'] = time;
resArr['progress'] = progress;
console.log(resArr);
/* UPDATE YOUR PROGRESSBAR HERE with above values ... */
if(progress==0 && i>20){
// TODO err - giving up after 8 sec. no progress - handle progress errors here
console.log('{"status":-400, "error":"there is no progress while we tried to encode the video" }');
return;
} else if(progress<100){
setTimeout(function(){ _progress(i); }, 400);
}
} else if( content.indexOf('Permission denied') > -1) {
// TODO - err - ffmpeg is not executable ...
console.log('{"status":-400, "error":"ffmpeg : Permission denied, either for ffmpeg or upload location ..." }');
}
},
function(err){
// AJAX error
if(i<20){
// retry
setTimeout(function(){ _progress(0); }, 400);
} else {
console.log('{"status":-400, "error":"there is no progress while we tried to encode the video" }');
console.log( err );
}
return;
});
}
setTimeout(function(){ _progress(0); }, 800);
Эти ответы, в которых используется несколько инструментов/консолей, слишком усложняют ситуацию.
pv
является хорошим вариантом, но имеет отмеченные недостатки, связанные с отсутствием неупорядоченных данных.
Просто используйте
progress
утилита: запустите ffmpeg как обычно, затем на другом мониторе консоли с помощью
progress -m -c ffmpeg
Были проблемы со второй частью php. Поэтому я использую это вместо:
$log = @file_get_contents($txt);
preg_match("/Duration:([^,]+)/", $log, $matches);
list($hours,$minutes,$seconds,$mili) = split(":",$matches[1]);
$seconds = (($hours * 3600) + ($minutes * 60) + $seconds);
$seconds = round($seconds);
$page = join("",file("$txt"));
$kw = explode("time=", $page);
$last = array_pop($kw);
$values = explode(' ', $last);
$curTime = round($values[0]);
$percent_extracted = round((($curTime * 100)/($seconds)));
Выходы отлично.
Хотелось бы увидеть что-то для нескольких загрузок для другого индикатора выполнения. Этот переход для текущего файла на один процент. Затем общий индикатор выполнения. Почти готово.
Кроме того, если людям трудно получить:
exec("ffmpeg -i path/to/input.mov path/to/output.flv 1> path/to/output.txt 2>&1");
Работать.
Пытаться:
exec("ffmpeg -i path/to/input.mov path/to/output.flv 1>path/to/output.txt 2>&1");
"1> путь" к "1> пути" ИЛИ "2> путь" к "2> пути"
Мне понадобилось время, чтобы понять это. FFMPEG продолжал терпеть неудачу. Сработало, когда я сменил пространство.
Взяв индикатор выполнения из этой ссылки, я создаю простой скрипт, который показывает фактический кадр кодируемого видео и показывает индикатор выполнения внизу... в то же время, когда ffmpeg кодирует видео, конечно.
Во-первых, мы должны получить продолжительность, ширину и высоту видео, чтобы создать полосу. Но, как
color
фильтр не может получить эту информацию из файла, мы должны сначала получить их с помощью
ffprobe
. Затем мы используем их с ffmpeg.
#!/bin/bash
video_duration=`ffprobe -v quiet -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$1"`
video_width=`ffprobe -v quiet -select_streams v -show_entries stream=width -of csv=p=0:s=x "$1"`
video_height=`ffprobe -v quiet -select_streams v -show_entries stream=height -of csv=p=0:s=x "$1"`
five_percent=`expr $video_height / 20`
#echo $video_duration
#echo $video_width
#echo $video_height
#echo $five_percent
ffmpeg -i "$1" -filter_complex "color=c=red:s='$video_width'x$five_percent[bar];[0][bar]overlay=-w+(w/$video_duration)*t:H-h:shortest=1[bar]" "$2" -map [bar] -f xv display
Затем используйте скрипт как:
sh encode_with_bar.sh video_in.mkv video_out.mp4
Производительность: используемый фильтр очень прост... но все добавленное потребляет дополнительный процессор. Тестирование видеофайла размером 10 МБ на моем компьютере, вот разница:
- Без сценария: 14,46 секунды
- Со сценарием: 17,05 секунды (на 18% больше)
Да, почти на 20% больше. Для коротких видео это хорошо. Для больших файлов, вероятно, это не очень хорошая идея.
Вызов системной функции php блокирует этот поток, поэтому вам нужно создать 1 HTTP-запрос для выполнения преобразования и еще один запрос на чтение txt-файла, который генерируется.
Или, что еще лучше, клиенты отправляют видео на конвертацию, а затем другой процесс становится ответственным за выполнение конвертации. Таким образом, клиентское соединение не будет иметь тайм-аут при ожидании завершения системного вызова. Опрос производится так же, как указано выше.
Это мое решение:
Я использую подпроцессы ffpb и python для отслеживания прогресса ffmpeg. Затем я отправляю статус в базу данных (например, Redis) для отображения индикатора выполнения на веб-сайте.
import subprocess
cmd = 'ffpb -i 400MB.mp4 400MB.avi'
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
encoding='utf-8',
errors='replace'
)
while True:
realtime_output = process.stdout.readline()
if realtime_output == '' and process.poll() is not None:
break
if realtime_output:
print(realtime_output.strip(), flush=True)
print('Push status to Redis...')
Докер
Чтобы преобразовать файлы WMA/MP3 в статический фильм (чтобы загрузить их на YouTube), я пытаюсь использовать docker jrottenberg/ffmpeg. Вначале кажется, что ни одно из вышеперечисленных решений не работает, но когда я запускаю Docker Compose, используяrun
вместоup
тогда решение Geograph работает :)
version: '3.7'
services:
musicToVideo:
image: jrottenberg/ffmpeg
volumes:
- ./music:/data
command: ' -loop 1 -framerate 2 -i "/data/image.jpeg" -i "/data/myAudio.WMA" -preset medium -tune stillimage -crf 18 -c:a aac -b:a 128k -shortest -pix_fmt yuv420p "/data/myVideo.mp4" -stats'
# command: ' -loop 1 -framerate 2 -i "/data/image.jpeg" -i "/data/myAudio.mp3" -c:v libx264 -preset medium -tune stillimage -crf 18 -c:a copy -shortest -pix_fmt yuv420p "/data/myVideo.mp4" -stats'
Я оставил свою работуdocker-dompose.yml
файл для будущих читателей, которые используют Docker — просто запустите его с помощьюdocker compose run musicToVideo
(Я предполагаю, что каталог ./music находится в том же каталоге, что и файл .yml выше, и содержит файл WMA/MP3 и image.jpeg) (для mp3 прокомментируйте «команду» с помощью WMA и раскомментируйте с помощью mp3)
Все просто и… Вот краткий обзор возможностей использования индикатора выполнения.
Нет проблем с pv ( предложение istefani) для преобразования, например, видео на YouTube. Однако не работает для видео, загруженных с Odysee. У меня есть это сообщение об ошибке:
[mov,mp4,m4a,3gp,3g2,mj2 @ 0x7fee39005400] stream 0, offset 0x30: partial file [mov,mp4,m4a,3gp,3g2,mj2 @ 0x7fee39005400] Could not find codec parameters for stream 0 (Video: h264 (avc1 / 0x31637661), none(tv, bt709), 1280x720, 604 kb/s): unspecified pixel format Consider increasing the value for the 'analyzeduration' (0) and 'probesize' (5000000) options
В итоге я использовал ffpb , который до сих пор отлично работает для меня; пока проблем нет.
С использованиемprogress -m -c ffmpeg …
интересно, но нужно открыть другую консоль, чтобы запустить ее после выполнения обычной команды ffmpeg в первой консоли (не удобно при запуске из сценария оболочки).
ffmpeg-progress-yield кажется отличной альтернативой ffpb , но не показывает (по крайней мере, для меня) название конвертируемого видео; он показывает «тест» вместо фактического имени файла.
Наконец, ffmpeg-progressbar-cli очень похож на ffpb и ffmpeg-progress-yield , но, видимо, больше не поддерживается; Я не пробовал.
Решение для FFMPEG.WASM
Этот код можно разместить после загрузки ffmpeg
ffmpeg.setProgress(({ ratio }) => {
//ratio is a float number between 0 to 1.
const progress = Math.round(ratio * 100);
// Update your progress indicator here
console.log(`Progress: ${progress}%`);
});
После некоторого исследования этой библиотеки я вижу, что реализация должна выполнять следующие шаги:
- Длительность результата расчета. Например, получите начальную продолжительность (вычтите любые триммерные секунды, если это необходимо):
ffprobe -v error -show_entries format=duration -of csv=p=0 input.mkv
Результат должен быть проанализирован как число с плавающей запятой и сохранен, например, в JavaScript:
const duration = parseFloat(result);
- В задании укажите дополнительный флаг, чтобы ffmpeg выдавал длительность обработки:
ffmpeg -i input.mkv -progress pipe:1 ...
Или то же самое, но с передачей в stderr:
ffmpeg -i input.mkv -progress pipe:2 ...
- Извлечь обработанную продолжительность (из stdout или stderr), например, в JavaScript:
const matchedResult = chunkResult.match(/out_time_ms=(\d+)/);
if (Array.isArray(matchedResult)) {
const currentTimeMs = Math.round(Number(matchedResult[1]) / 1000000);
}
- Сравните обработанную продолжительность с общей продолжительностью
currentTimeMs/duration