Кодирование медиа-файлов в Ruby при помощи ffmpeg/mencoder с отслеживанием статуса процесса

(Ruby & Rails) · English (44,896 views)

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

Для начала, необходимо решить, как будет отображаться прогресс. Я не нашел ничего проще, чем вывод простого текста в стандартный поток вывода PROGRESS: 56, где 56 – это статус кодирования в процентах, и ERROR в случае ошибки. Каждый выходной формат будет обрабатываться отдельным скриптом (например, для получения видео для iPod будет скрипт ipod.rb).

Мне необходимо использовать две программы кодирования – mencoder and ffmpeg (не считая дополнительных инструментов вроде flvtool2 и других, так как их выполнение не занимает много времени). Это означает, что все, что мне нужно – это найти общий метод выполнения этих программ и обработки их вывода (который уже содержит информацию, достаточную для вычисления статуса кодирования).

Ну что ж, начнем. Для начала запустим mencoder:

1
mencoder input.avi -o output.avi -oac lavc -ovc lavc -lavcopts vcodec=xvid:acodec=mp3 > output.txt

Это очень сильно упрощенная команда, и я надеюсь, вы не будете использовать ее в реальных проекта :-) Как можно заметить, output.txt содержит строки, заканчивающиеся r:

1
2
3
4
5
--skipped--
Pos:   0.1s      3f ( 1%)  0.00fps Trem:   0min   0mb  A-V:0.008 [0:0]
Pos:   0.2s      4f ( 2%)  0.00fps Trem:   0min   0mb  A-V:0.012 [0:0]
Pos:   0.2s      5f ( 3%)  0.00fps Trem:   0min   0mb  A-V:0.016 [0:0]
--skipped--

Я буду использовать метод Ruby IO.popen для того, чтобы разобрать вывод:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MediaFormatException < StandardError
end

def execute_mencoder(command)
  progress = nil
  IO.popen(command) do |pipe|
    pipe.each("r") do |line|
      if line =~ /Pos:[^(]*(s*(d+)%)/
        p = $1.to_i
        p = 100 if p > 100
        if progress != p
          progress = p
          print "PROGRESS: #{progress}n"
          $defout.flush
        end
      end
    end
  end
  raise MediaFormatException if $?.exitstatus != 0
end

Сначала я определил класс MediaFormatException, который будет использоваться для выбрасывания исключения для уведомления вызывающего кода об ошибках (мы поговорим об этом позже). Далее можно увидеть метод execute_mencoder, который принимает команду. Статус процесса будет выводиться в поток стандартного вывода, и прогресс не будет выводиться дважды.

Продолжим с ffmpeg:

1
ffmpeg -y -i input.avi -vcodec xvid -acodec mp3 -ab 96 output.avi > output.txt

Упс! Он производит вывод в поток ошибок!

1
ffmpeg -y -i input.avi -vcodec xvid -acodec mp3 -ab 96 output.avi &> output.txt

Как можно заметить, он показывает текущий фрейм и время, а не проценты. Но в начале вывода выводится Duration: 00:00:24.9, потому мы можем высчитать прогресс самостоятельно:

1
2
3
4
5
6
7
--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--

Так сделаем же это!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def execute_ffmpeg(command)
  progress = nil
  IO.popen(command) do |pipe|
    pipe.each("r") do |line|
      if line =~ /Duration: (d{2}):(d{2}):(d{2}).(d{1})/
        duration = (($1.to_i * 60 + $2.to_i) * 60 + $3.to_i) * 10 + $4.to_i
      end
      if line =~ /time=(d+).(d+)/
        if not duration.nil? and duration != 0
          p = ($1.to_i * 10 + $2.to_i) * 100 / duration
        else
          p = 0
        end
        p = 100 if p > 100
        if progress != p
          progress = p
          print "PROGRESS: #{progress}n"
          $defout.flush
        end
      end
    end
  end
  raise MediaFormatException if $?.exitstatus != 0
end

Здесь мы использовали тот же класс MediaFormatException для исключений. Теперь у нас есть оба метода, можно начать тестирование.

1
2
3
4
5
6
7
8
9
10
command_mencoder = "mencoder input.avi -o output.avi -oac lavc -ovc lavc -lavcopts vcodec=xvid:acodec=mp3"
command_ffmpeg = "ffmpeg -y -i input.avi -vcodec xvid -acodec mp3 -ab 96 output.avi 2>&1"

begin
  execute_mencoder(command_mencoder)
  execute_ffmpeg(command_ffmpeg)
rescue
  print "ERRORn"
  exit 1
end

Обратите внимание, что поток ошибок перенаправлен в поток стандартного вывода, чтобы обработать статус процесса для ffmpeg.

Выглядит не очень красиво, поскольку нам нужно обрабатывать исключения в каждом скрипте. Напишем метод, который сделает это за нас:

1
2
3
4
5
6
def safe_execute
  yield
rescue
  print "ERRORn"
  exit 1
end

И пример использования:

1
2
3
4
5
6
7
command_mencoder = "mencoder input.avi -o output.avi -oac lavc -ovc lavc -lavcopts vcodec=xvid:acodec=mp3"
command_ffmpeg = "ffmpeg -y -i input.avi -vcodec xvid -acodec mp3 -ab 96 output.avi 2>&1"

safe_execute do
  execute_mencoder(command_mencoder)
  execute_ffmpeg(command_ffmpeg)
end

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

23 Responses to this entry

Subscribe to comments with RSS

said on 11.10.2006 at 12.22 · Permalink

Интересная статья, добавляем в закладки. Спасибо!

erka
said on 12.10.2006 at 10.31 · Permalink

Поддерживаю. Спасибо за статью. Хорошо, что ты нашел на нее время, которого у тебя щас нет. Тем не менее я жду еще одну про проблемы с кодированием.

Chad @
said on 03.01.2007 at 9.11 · Permalink

to get the duration, change all the “d”s in the regex to “\d”s

said on 03.01.2007 at 16.55 · Permalink

Oh, thanks! Of course, you right. This is my syntax highlighter killed \ :-)
I have updated post.

Thanks again

Chad @
said on 23.01.2007 at 7.52 · Permalink

I am using your method to process ffmpeg commands with images as my input, eg “ffmpeg -i %03d.jpg movie.mp4″ and I keep receiving errors that “%03d.jpg: I/O error occured
Usually that means that input file is truncated and/or corrupted.”

I know the images are good, I can issue the same exact command via bash (using the same images) and it builds the movie with no probs.

Of course, my first assumption was that the % is messing something up so I tried doing “ffmpeg -i ‘%03d.jpg’ movie.mp4″ but it did not change the out come any and I got the same error.

Any suggestions?

Chad @
said on 23.01.2007 at 8.07 · Permalink

I just solved my previous problem. It was simply an issue of incorrect paths. Since my code is in a model it was trying to load images from my app/models/ directory. I just gave it a full path, eg

1
2
3
datadir = RAILS_ROOT + "/tmp/images/"
videodir = RAILS_ROOT + "/public/videos/"
"ffmpeg -i #{datadir}%03d.jpg #{videodir}mymovie.mp4"

For anyone else who may have this problem… it’s a silly little time consumer…

said on 23.01.2007 at 13.16 · Permalink

Yeah, you right. I’m using absolute path for input and output files too. Thanks for the notice.

Alex @
said on 15.05.2007 at 20.47 · Permalink

Хорошая статья, только у меня вопрос, а как заставить ffmpeg работать с xvid-ом?
у меня FreeBSD и на команду вроде

1
ffmpeg -y -i input.avi -vcodec xvid -acodec mp3 -ab 96 output.avi

говорит:

1
Unknown video codec 'xvid'

хотя ‘xvid’ установлен и mencoder с ним прекрасно работает.
Что делать?

said on 15.05.2007 at 21.31 · Permalink

Наиболее вероятное объяснение — ffmpeg собран без поддержки xvid (ключ --enable-xvid). Скорее всего придется его пересобрать.

Посмотреть список форматов, поддерживаемых ffmpeg:

1
ffmpeg -formats

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

Alex @
said on 16.05.2007 at 10.40 · Permalink

Спасибо за ответ, но не получается!
При установке из портов:
make config
No options to configure

если в ручную устанавливать:
./configure –help
особо выбора не предоставляет, не говоря уже об опции –enable-xvid :(
версия ffmpeg-0.4.9-pre1.tar.gz

в связи с этим другой вопрос, я решил использовать ffmpeg только потому что он делает нормальную копию звука (-acodec copy), в отличие от mencoder-а, который при опции -oac copy из дорожки “Dolby AC3 48000Hz 6ch 448Kbps” делает “DTS 48000Hz stereo 768Kbps” (так говорит Media Player Classic, приблизительно тоже и VirtualDub).
Может подскажешь как решить эту проблему?

said on 16.05.2007 at 14.29 · Permalink

Вот тут уж сорри, действительно не знаю, чем помочь. Никогда не работал с ffmpeg из портов фри. Как-то привычнее скачать официальные исходники и поставить куда-нить в /opt, чтобы не размазывались по системе.

С проблемой AC3 -> DTS тоже не сталкивался. У меня обычно задача из видео нормального качества сделать говно типа flv или mp4 с минимальными параметрами…

Baerz @
said on 19.06.2007 at 12.20 · Permalink

Hi, i tried to implement your Script, but it will not work for me. I get an Error: undefined method `execute_mencoder’ for main:Object (NoMethodError)

Why? :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class MediaFormatException < StandardError
end


command_mencoder = "/usr/bin/mencoder /home/download/medien_trailer.mov -o /home/robin/download/output.avi -oac lavc -ovc lavc -lavcopts vcodec=xvid:acodec=mp3"
command_ffmpeg = "/usr/bin/ffmpeg -y -i /home/download/medien_trailer.mov -vcodec xvid -acodec mp3 -ab 96 /home/robin/download/output.avi 2>&1"

begin
  execute_mencoder(command_mencoder)
  execute_ffmpeg(command_ffmpeg)
rescue
  print "ERROR\n"
  exit 1
end

def execute_mencoder(command)
  progress = nil
  Open3.popen3(command) do |pipe|
    pipe.each("\r") do |line|
      if line =~ /Pos:[^(]*(\s*(\d+)%)/
        p = $1.to_i
        p = 100 if p > 100
        if progress != p
          progress = p
          print "PROGRESS: #{progress}\n"
          $defout.flush
        end
      end
    end
  end
  raise MediaFormatException if $?.exitstatus != 0
end

def execute_ffmpeg(command)
  progress = nil
  Open3.popen3(command) do |pipe|
    pipe.each("\r") do |line|
      if line =~ /Duration: (\d{2}):(\d{2}):(\d{2}).(\d{1})/
        duration = (($1.to_i * 60 + $2.to_i) * 60 + $3.to_i) * 10 + $4.to_i
      end
      if line =~ /time=(\d+).(\d+)/
        if not duration.nil? and duration != 0
          p = ($1.to_i * 10 + $2.to_i) * 100 / duration
        else
          p = 0
        end
        p = 100 if p > 100
        if progress != p
          progress = p
          print "PROGRESS: #{progress}\n"
          $defout.flush
        end
      end
    end
  end
  raise MediaFormatException if $?.exitstatus != 0
end
said on 19.06.2007 at 12.28 · Permalink

Baerz, you should define methods execute_mencoder and execute_ffmpeg before executing them, for example, right after definition of the class MediaFormatException.

said on 14.12.2007 at 11.59 · Permalink

Помогите решить вот такую проблему:
конвертирую flv в mov
на входе 8 Мб
на выходе 51 Мб, т.е. в 6 раз больше.

команда такая:
ffmpeg -i sqlintro1.flv -sameq 01sql.mov

Подскажите, если не затруднит, какими ключами можно уменьшить размер выходного файла, чтобы он был примерно таким же как и входной.

Adam @
said on 23.01.2008 at 0.20 · Permalink

Hi , Thanks for this tutorial.
I am on leopard FFmpeg version SVN-r9102, ruby 1.8.6.
The above code works fine for me but the progress wont update sequentially as expected.

Any idea what this could be?? Is it something to do with pipe.each(“\r”) ??

Chris
said on 22.06.2008 at 18.35 · Permalink

The ffmpeg progress counter will never update with Ruby 1.8.6 because duration is defined out of scope (the first if clause) when processing each line from ffmpeg’s output (the second if clause) — to fix it, change the line: “progress = nil” to “duration = progress = nil” and it will work fine.

Lele @
said on 23.06.2008 at 15.25 · Permalink

Hi,
I’m trying to do something similar to what you did.
One thing I’m trying to add now is a timeout, what I would like to do is to check if I get some output from mencoder, if I don’t get any output on the stdour or error for more that 30 sec I want to quit the process.
Is that possible?
thanks very much in advance

Comments are closed

Comments for this entry are closed for a while. If you have anything to say – use a contact form. Thank you for your patience.