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

Oct 11

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

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

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

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

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

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

--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 для того, чтобы разобрать вывод:

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:

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

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

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

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

--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--

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

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 для исключений. Теперь у нас есть оба метода, можно начать тестирование.

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 "ERROR\n"
  exit 1
end

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

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

def safe_execute
  yield
rescue
  print "ERROR\n"
  exit 1
end

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

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 отзывов на 'Кодирование медиа-файлов в Ruby при помощи ffmpeg/mencoder с отслеживанием статуса процесса'

Подписаться на комментарии по RSS или TrackBack на 'Кодирование медиа-файлов в Ruby при помощи ffmpeg/mencoder с отслеживанием статуса процесса'.

1
сказал 11.10.2006 в 12.22

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

2
erka
сказал 12.10.2006 в 10.31

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

3
сказал 03.01.2007 в 9.11

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

4
сказал 03.01.2007 в 16.55

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

Thanks again

5
сказал 08.01.2007 в 22.33

[...] ffmpeg/mencoder for converting media [...]

6
сказал 23.01.2007 в 7.52

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?

7
сказал 23.01.2007 в 8.07

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

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…

8
сказал 23.01.2007 в 13.16

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

9
сказал 25.04.2007 в 3.28

[...] Encoding media files in Ruby using ffmpeg/mencoder with progress tracking (tags: ruby multimedia programming) [...]

10
Alex
сказал 15.05.2007 в 20.47

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

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

говорит:

Unknown video codec 'xvid'

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

11
сказал 15.05.2007 в 21.31

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

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

ffmpeg -formats

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

12
Alex
сказал 16.05.2007 в 10.40

Спасибо за ответ, но не получается!
При установке из портов:
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).
Может подскажешь как решить эту проблему?

13
сказал 16.05.2007 в 14.29

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

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

14
сказал 19.06.2007 в 12.20

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? :)

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
15
сказал 19.06.2007 в 12.28

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

16
сказал 19.06.2007 в 12.41

Hey, works :) Thanks…

17
сказал 19.08.2007 в 23.48

[...] going with a different guide [...]

18
сказал 19.10.2007 в 3.44

[...] Encoding media files in Ruby using ffmpeg/mencoder with progress tracking [...]

19
сказал 14.12.2007 в 11.59

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

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

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

20
Adam
сказал 23.01.2008 в 0.20

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”) ??

21
benok
сказал 16.02.2008 в 2.56

Hi, do you know this library?
It’s very handy.
http://0xcc.net/ruby-progressbar/index.html.en

22
Chris
сказал 22.06.2008 в 18.35

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.

23
Lele
сказал 23.06.2008 в 15.25

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

Оставить отзыв

Вы можете использовать простые теги форматирования HTML (вроде <a>, <ul> and others). Чтобы вставить пример код, используйте <code lang="php">$a = "hello";</code> (поддерживаемые языки: ruby, php, yaml, html, csharp, javascript). Также Вы можете использовать <code>$a = "hello";</code>, синтаксис не будет подсвечен. Если вы не хотите использовать тег <code>, замените символ < на &lt;.

Отправить

 
Copyright © 2005 - 2008, Dmytro Shteflyuk