LINUX.ORG.RU

bash, ffmpeg, какой-то трындец и непонятки

 , ,


0

3

Как то давно я написал себе скрипт-конвертер мультимедийных файлов, всё как положено, многопоточный с контролем потоков, с рекурсивным сбором из папки, и давно им пользуюсь иногда внося изменения. Он отлично работает на RPi3 с дебиан8 и ffmpeg 3.4. Но вот я решил ускорить кодирование сериала на нескольких машинах, отдал один сезон RPi3 а второй попробовал кодировать на других компах с помощью самбы. И на них начался трешак и падения в части многопоточности - 1-2 файла отправлялись на кодирование, медленная работа из за того, что ffmpeg срёт ошибками (вывод подавлен, но он это делает и вешает себе главный поток), и обрыв собственно скрипта-распределителя потоков.

Вот скрипт. Общий принцип: чистка мусора, подготовка рабочей папки, затем создание скрипта-модуля convert.bash, который принимает данные о кодируемом файле и дёргает собственно ffmpeg. В коде convert.bash захардкожена строка команды. Затем собираерся рекурсивный список файлов в папке и для него начинает крутиться цикл while read со встроенными замедлителями потоков через файлы блокировки. При запуске convert.bash файл-блокировка создаётся, а затем convert.bash его удаляет когда ffmpeg закончит работу. Обратите внимание: в нормальном состоянии вывод ffmpeg подавлен через > /dev/null 2>&1, но я его отключал для диагностики - на поведение и на глюк не влияет.

#!/bin/bash
CORE="1" # Создавать потоков

if [ "$1" = "-h" ]; then
echo 'Использование: paket_convert.bash <каталог для поиска файлов> <каталог для помещения результатов>'
echo 'Задействовано потоков: '"$CORE"
exit 0
fi

rm -R /tmp/ffmpeg/
rm /tmp/convert.bash
mkdir /tmp/ffmpeg/
id='1' # Начальный индекс файла
cd "$1"
mkdir "$2"
ALL=$( find -P ./ -type f | wc -l )

	# Создание второстепенного скрипта /tmp/convert.bash
	# $1 - id файла. $2 - путь к папке, куда надо положить результат. Файлы блокировок расположены в /tmp/ffmpeg/, имя = id, содержится строка с отн. адресом файла на перекодирование.
echo 'ALL=$( find -P ./ -type f | wc -l )' >> /tmp/convert.bash
echo 'FILE=$( cat /tmp/ffmpeg/$1 )' >> /tmp/convert.bash
echo 'DIR=${FILE%/*}' >> /tmp/convert.bash
echo 'LONG_DIR=${#DIR}+1' >> /tmp/convert.bash
echo 'NAME=${FILE:LONG_DIR}' >> /tmp/convert.bash
echo 'FILENAME=${NAME%.*}' >> /tmp/convert.bash

#
# здесь всякие варианты строк ffmpeg на все случаи жизни
#
echo 'ffmpeg -i "$FILE" -map 0:0 -map 0:1 -s 634x360 -acodec copy -vcodec libx264 -profile high -level 42 -qmax 22 "$2"/"$FILENAME".mp4 > /dev/null 2>&1' >> /tmp/convert.bash


echo 'rm /tmp/ffmpeg/"$1"' >> /tmp/convert.bash
echo 'echo $1 из $ALL завершено' >> /tmp/convert.bash
chmod +x /tmp/convert.bash
	# Конец создания второстепенного скрипта

# Начинаю крутить цикл
		find -P ./ -type f | while read FILE
		do
while [ $( ls -1A /tmp/ffmpeg | wc -l ) -ge "$CORE" ]; do
	sleep 20
done

echo "$FILE" >> /tmp/ffmpeg/"$id"
/tmp/convert.bash "$id" "$2" &
echo $id'/'$ALL" кодируется ""$FILE"
let id++
sleep 2
		done

# жду завершения задач
while [ $( ls -1A /tmp/ffmpeg | wc -l ) -gt "0" ]; do
	sleep 3
done

Собственно что произошло на компах с 10 и 11 дебианом и ffmpeg 4.1 и ещё каким то: в выводе ffmpeg начинают появляться ошибки о неправильных фреймах, битых данных, между ними проскакивают сообщения вида «кусок_имени_реально_существующего_файла не найден» ну и собственно куски имён. Через примерно минуту (2-3 цикла ожидания) основной скрипт наворачивается и перестаёт корректно считать потоки - запускает копии sleep, но не запускает convert.bash. После переборки определённого числа обрубков имён - заканчивает список и ждёт завершения задач.

Метод научного тыка показал, что в этом процессе ключевым является наличие в convert.bash строки ffmpeg -i "$FILE" {какие нибудь операции} "$2"/"$FILENAME".mp4. Самба/реальная ФС - не важно. 1 или много потоков - не важно. Аудио или видео - не важно. Кодек copy или реальное перекодирование - не важно. Наличие спецсимволов или пробелов в именах и путях - не важно.
В отрыве от ffmpeg все компоненты отрабатывают корректно, списки полные, имена целые, файлы-блокировки ставятся и удаляются, потоки считаются верно. Если оставить только ffmpeg -i "$FILE" без выходного файла - косяк не проявляется. Если вместо ffmpeg файлы дёргаются другими программами, например ffplay или vlc, или cp "$FILE" "$2"/"$FILENAME".mp4 - косяк не проявляется.

И самое интересное: изолирование ffmpeg в convert.bash в отдельном терминал командой xterm -e ffmpeg -i "$FILE" -acodec copy "$2"/"$FILENAME".mp4 вроде бы решает проблему! Но это же какой то хаос и трындец. было бы неплохо понять WTF тут происходит.

Перемещено hobbit из general

★★★★★

Последнее исправление: hobbit (всего исправлений: 1)
Ответ на: комментарий от firkax

Да я особо не разбирался. По умолчанию ffmpeg регулярно проверяет stdin на наличие данных и пытается оттуда читать. И это очень плохо взаимодействует с «something | while read». Возможно, эта конструкция в оболочке как-то меняет stdin, а ffmpeg как дочерний процесс его наследует и совсем такого подвоха не ожидает.

i-rinat ★★★★★
()
Последнее исправление: i-rinat (всего исправлений: 1)
Ответ на: комментарий от i-rinat

А, я не заметил что там конвеер с read для имён файлов. Тогда всё понятно.

Возможно, эта конструкция в оболочке как-то меняет stdin, а ffmpeg как дочерний процесс его наследует и совсем такого подвоха не ожидает.

Да нет, она всё делает предсказуемо - на stdin ставится вывод команды слева от |, а read из этого stdin читает. Но читает не только он а ещё и ffmpeg, портя файловый указатель, после чего следующий read получает мусор. Хотя непонятно причём тут битые фреймы. Может быть буквы из списка файлов оказываются валидными командами для ffmpeg которые нарушают его работу.

По идее ffmpeg должен делать isatty(0) и если там false то никакие команды с stdin не пытаться читать. Возможно, в старой версии ffmpeg так и было, и она не ломала ничего, а потом сломали.

firkax ★★★★★
()
Ответ на: комментарий от i-rinat

Странно как это у меня работало в версии 3.4 годами.

а ffmpeg как дочерний процесс его наследует и совсем такого подвоха не ожидает.

Так тут каким то макаром происходит подмена списка в while read в родительсском процессе. Он там реально строки на куски рубит.

kirill_rrr ★★★★★
() автор топика
Ответ на: комментарий от firkax

Насколько я понимаю, конвейр с read должен находиться в одном процессе bash, а stdin куда пишет ffmpeg - в другом. И при этом быть подавленым. Собственно так оно раньше и работало - ffmpeg мог срать сколько угодно, он же кучу статистики в процессе работы пишет, но в конвейр это не попадало.

kirill_rrr ★★★★★
() автор топика
Ответ на: комментарий от kirill_rrr

stdin наследуется потомками, как и все остальные дескрипторы. И так было всегда и во всех unix-ах.

он же кучу статистики в процессе работы пишет

Дело не в «пишет» а в «читает». Пишет он в stdout, и это обычно (но не всегда) можно безопасно для логики делать сколько угодно, поскольку stdout показывается юзеру и максимум что там можно так это замусорить его. У тебя stdout редиректится в /dev/null так что он уже не общий с башем и его (башу) даже замусорить не получится.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 2)

между ними проскакивают сообщения вида «кусок_имени_реально_существующего_файла не найден» ну и собственно куски имён

Очевидно же, что проблема не в debian или ffmpeg. А в том, что на Pi вы клали файлы в каталог, который по полному пути не содержит пробелов. А в debian у вас в пути пробелы. Я пытался в вашей каше найти где формируется имя файла, но это бесполезно. Суть в том, что в ffmpeg подается строка вида «ffmpeg xxx yyy», где «xxx yyy» - имя файла или полный путь до него. То что вы там обернули в кавычки - ничего не значит, он у вас несколько раз формируется без кавычек.

И что это за ужас вообще?

echo $id'/'$ALL" кодируется ""$FILE"

Зачем вы так странно делаете? Если надо чтобы переменные развернулись - оберните строку в ". Если не надо - в ’, если имя переменной сложное, то в ${name}:

echo "$id/$ALL кодируется $FILE"
PPP328 ★★★★★
()
Ответ на: комментарий от PPP328

Я же упоминал, что делал тестирование локально и без пробелов. Собственно изначально в сериале пробелов в именах нет.

То что вы там обернули в кавычки - ничего не значит

Как раз значит, потому что пробельные имена обрабатываются корректно.

Я пытался в вашей каше найти где формируется имя файла, но это бесполезно.

echo 'ALL=$( find -P ./ -type f | wc -l )' >> /tmp/convert.bash
echo 'FILE=$( cat /tmp/ffmpeg/$1 )' >> /tmp/convert.bash
echo 'DIR=${FILE%/*}' >> /tmp/convert.bash
echo 'LONG_DIR=${#DIR}+1' >> /tmp/convert.bash
echo 'NAME=${FILE:LONG_DIR}' >> /tmp/convert.bash
echo 'FILENAME=${NAME%.*}' >> /tmp/convert.bash

Что в convert.bash превращается в

ALL=$( find -P ./ -type f | wc -l )
FILE=$( cat /tmp/ffmpeg/$1
DIR=${FILE%/*}
LONG_DIR=${#DIR}+1
NAME=${FILE:LONG_DIR}
FILENAME=${NAME%.*}

Здесь отрезание от строки с путём к исходнику одного расширения сзади и собственно пути спереди. Все пробелы сохраняются именно благодаря кавычкам.

Зачем вы так странно делаете?

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

kirill_rrr ★★★★★
() автор топика
Ответ на: комментарий от kirill_rrr

cat /tmp/ffmpeg/$1

Ну и где здесь обертка имени файла?

в сериале пробелов в именах нет.

Я говорю про путь, а не про только имя файла.

cat /tmp/ffmpeg/aaa bbb означает «прочитать файл /tmp/ffmpeg/aaa а затем прочитать файл bbb», что отвечает на ваш вопрос:

между ними проскакивают сообщения вида «кусок_имени_реально_существующего_файла не найден» ну и собственно куски имён

но загонять всё в одни мягкие кавычки я не стану.

Да как хотите, просто потом не удивляйтесь что никто вашу кашу из '\""\''" прочитать не может

PPP328 ★★★★★
()
Последнее исправление: PPP328 (всего исправлений: 1)
Ответ на: комментарий от PPP328

Тут проблема действительно не в пробелах, а именно в особенностях ffmpeg. Вот тебе короткий способ воспроизвести проблему локально:

$ convert -size 320x240 xc:blue frame.png
$ 
$ yes frame.png | head -2 | while read fn; do ffmpeg -y -i "$fn" -v error -stats out.webm; done
frame=    1 fps=0.0 q=10.0 Lsize=       1kB time=00:00:00.00 bitrate=N/A speed=   0x    eed=N/A    
rame.png: No such file or directory
$ 
$ yes frame.png | head -2 | while read fn; do ffmpeg -nostdin -y -i "$fn" -v error -stats out.webm; done
frame=    1 fps=0.0 q=10.0 Lsize=       1kB time=00:00:00.00 bitrate=N/A speed=   0x    eed=N/A    
frame=    1 fps=0.0 q=10.0 Lsize=       1kB time=00:00:00.00 bitrate=N/A speed=   0x    eed=N/A    
$ 

Обрати внимание на текст ошибки: «rame.png: No such file or directory». Если проследить за действиями ffmpeg через strace, будет видно, как ffmpeg действительно пытается открыть либо «rame.jpg», либо «ame.png». Точное имя зависит от того, сколько байт ffmpeg успеет сожрать из stdin.

i-rinat ★★★★★
()
Ответ на: комментарий от PPP328

Ну и где здесь обертка имени файла?

Разумеется имя и путь файла записан в файле-блокировке в /tmp/ffmpeg/$id, ссылка на который передаётся в convert.bash параметром $1.

Если вы про кавычки, то они там не нужны, т.к. $( ) направляет вывод cat сразу в переменную, а т.к. $id это просто число - там нечего экранировать.

Я говорю про путь, а не про только имя файла.

И там тоже не было никаких проблелов! Я специально проверил, а потом вообще начал использовать /home/user/test/.

Я же описал - скрипт сам по себе работает отлично, проблемы начинаются только когда конкретно ffmpeg начинает срать в stdin/stdout.

просто потом не удивляйтесь что никто вашу кашу из '\«»\"" прочитать не может

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

Я в принципе готов подробно объяснить что у меня где, но проблема уже решилась и она была точно не в этом.

kirill_rrr ★★★★★
() автор топика
Ответ на: комментарий от i-rinat

Использовал. Этот скрипт у меня с появления RPi3 и распбиан-8, если даже не раньше. Наваял его где то в институте, не позже 2014-2015, года, когда окончательно прекратил конвертировать музыку через aimp в вайне. Короче тогда очень остро встала многопоточность.

Да вот собственно, в заголовке же - исходный скрипт отлично и без всяких косяков отрабатывает на том же самом распбиан-8 на RPi3 и пережёвывает за ночь свой сезон.

kirill_rrr ★★★★★
() автор топика
Последнее исправление: kirill_rrr (всего исправлений: 2)
Ответ на: комментарий от kirill_rrr

Возможно там таки была проверка isatty(0) которую оказывается даже в том багрепорте предлагали сделать, а потом её почему-то не стало.

Проверка могла быть и из дебиановского патча например.

Попробуй этот тест погонять там где оно «работает».

firkax ★★★★★
()