Permalink
Browse files

Merge pull request #27 from stakach/master

Added timeout option to detect hung ffmpeg process
  • Loading branch information...
2 parents 78b756a + 739688b commit 3b7053a228fb72fd2c440d2148ee8d3cb022643b @dbackeus dbackeus committed Jun 27, 2012
Showing with 146 additions and 20 deletions.
  1. +16 −0 README.md
  2. +72 −0 lib/ffmpeg/io_monkey.rb
  3. +44 −17 lib/ffmpeg/transcoder.rb
  4. +1 −1 lib/ffmpeg/version.rb
  5. +1 −0 lib/streamio-ffmpeg.rb
  6. +10 −0 spec/ffmpeg/transcoder_spec.rb
  7. +2 −2 streamio-ffmpeg.gemspec
View
16 README.md
@@ -154,6 +154,22 @@ FFMPEG.ffmpeg_binary = '/usr/local/bin/ffmpeg'
This will cause the same command to run as "/usr/local/bin/ffmpeg -i /path/to/input.file ..." instead.
+
+Automatically kill hung processes
+---------------------------------
+
+By default, streamio will wait for 200 seconds between IO feedback from the FFMPEG process. After which an error is logged and the process killed.
+It is possible to modify this behaviour by setting a new default:
+
+``` ruby
+# Change the timeout
+Transcoder.timeout = 30
+
+# Disable the timeout altogether
+Transcoder.timeout = false
+```
+
+
Copyright
---------
View
72 lib/ffmpeg/io_monkey.rb
@@ -0,0 +1,72 @@
+
+
+if RUBY_VERSION =~ /1\.8/
+ #
+ # This is useful when `timeout.rb`, which, on M.R.I 1.8, relies on green threads, does not work consistently.
+ #
+ begin
+ require 'system_timer'
+ MyTimer = SystemTimer
+ rescue LoadError
+ require 'timeout'
+ MyTimer = Timeout
+ end
+else
+ require 'timeout'
+ MyTimer = Timeout
+end
+
+
+#
+# Monkey Patch timeout support into the IO class
+#
+class IO
+ def each_with_timeout(pid, timeout, sep_string=$/)
+ q = Queue.new
+ th = nil
+
+ timer_set = lambda do |timeout|
+ th = new_thread(pid){ to(timeout){ q.pop } }
+ end
+
+ timer_cancel = lambda do |timeout|
+ th.kill if th rescue nil
+ end
+
+ timer_set[timeout]
+ begin
+ self.each(sep_string) do |buf|
+ timer_cancel[timeout]
+ yield buf
+ timer_set[timeout]
+ end
+ ensure
+ timer_cancel[timeout]
+ end
+ end
+
+
+ private
+
+
+ def new_thread(pid, *a, &b)
+ cur = Thread.current
+ Thread.new(*a) do |*a|
+ begin
+ b[*a]
+ rescue Exception => e
+ cur.raise e
+ if RUBY_PLATFORM =~ /(win|w)(32|64)$/
+ require 'win32/process'
+ Process.kill(1, pid)
+ else
+ Process.kill('INT', pid)
+ end
+ end
+ end
+ end
+
+ def to timeout = nil
+ MyTimer.timeout(timeout){ yield }
+ end
+end
View
61 lib/ffmpeg/transcoder.rb
@@ -1,8 +1,22 @@
require 'open3'
require 'shellwords'
+if RUBY_PLATFORM =~ /(win|w)(32|64)$/
+ require 'win32/process'
+end
+
module FFMPEG
class Transcoder
+ @@timeout = 200
+
+ def self.timeout=(time)
+ @@timeout = time == false ? false : time.to_i
+ end
+
+ def self.timeout
+ @@timeout
+ end
+
def initialize(movie, output_file, options = EncodingOptions.new, transcoder_options = {})
@movie = movie
@output_file = output_file
@@ -28,26 +42,39 @@ def run
FFMPEG.logger.info("Running transcoding...\n#{command}\n")
output = ""
last_output = nil
- Open3.popen3(command) do |stdin, stdout, stderr|
- yield(0.0) if block_given?
- stderr.each("r") do |line|
- fix_encoding(line)
- output << line
- if line.include?("time=")
- if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
- time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
- elsif line =~ /time=(\d+.\d+)/ # ffmpeg 0.7 and below style
- time = $1.to_f
- else # better make sure it wont blow up in case of unexpected output
- time = 0.0
+ Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
+ pid = wait_thr.pid
+ begin
+ yield(0.0) if block_given?
+ next_line = Proc.new do |line|
+ fix_encoding(line)
+ output << line
+ if line.include?("time=")
+ if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
+ time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
+ elsif line =~ /time=(\d+.\d+)/ # ffmpeg 0.7 and below style
+ time = $1.to_f
+ else # better make sure it wont blow up in case of unexpected output
+ time = 0.0
+ end
+ progress = time / @movie.duration
+ yield(progress) if block_given?
+ end
+ if line =~ /Unsupported codec/
+ FFMPEG.logger.error "Failed encoding...\nCommand\n#{command}\nOutput\n#{output}\n"
+ raise "Failed encoding: #{line}"
end
- progress = time / @movie.duration
- yield(progress) if block_given?
end
- if line =~ /Unsupported codec/
- FFMPEG.logger.error "Failed encoding...\nCommand\n#{command}\nOutput\n#{output}\n"
- raise "Failed encoding: #{line}"
+
+ if @@timeout != false
+ stderr.each_with_timeout(pid, @@timeout, "r", &next_line)
+ else
+ stderr.each("r", &next_line)
end
+
+ rescue Timeout::Error => e
+ FFMPEG.logger.error "Process hung...\nCommand\n#{command}\nOutput\n#{output}\n"
+ raise "Process hung"
end
end
View
2 lib/ffmpeg/version.rb
@@ -1,3 +1,3 @@
module FFMPEG
- VERSION = "0.8.5"
+ VERSION = "0.8.6"
end
View
1 lib/streamio-ffmpeg.rb
@@ -6,6 +6,7 @@
require 'ffmpeg/version'
require 'ffmpeg/errors'
require 'ffmpeg/movie'
+require 'ffmpeg/io_monkey'
require 'ffmpeg/transcoder'
require 'ffmpeg/encoding_options'
View
10 spec/ffmpeg/transcoder_spec.rb
@@ -29,6 +29,16 @@ module FFMPEG
FFMPEG.logger.should_receive(:info).at_least(:once)
end
+ it "should fail when IO timeout is exceeded" do
+ FFMPEG.logger.should_receive(:error)
+ movie = Movie.new("#{fixture_path}/movies/awesome_widescreen.mov")
+ Transcoder.timeout = 1
+ transcoder = Transcoder.new(movie, "#{tmp_path}/timeout.mp4")
+ lambda { transcoder.run }.should raise_error(RuntimeError, /Process hung/)
+ end
+
+ Transcoder.timeout = 200
+
it "should transcode the movie with progress given an awesome movie" do
FileUtils.rm_f "#{tmp_path}/awesome.flv"
View
4 streamio-ffmpeg.gemspec
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
s.summary = "Reads metadata and transcodes movies."
s.description = "Simple yet powerful wrapper around ffmpeg to get metadata from movies and do transcoding."
- s.add_development_dependency("rspec", "~> 2.7")
- s.add_development_dependency("rake", "~> 0.9.2")
+ s.add_development_dependency("rspec", ">= 2.7")
+ s.add_development_dependency("rake", ">= 0.9.2")
s.files = Dir.glob("lib/**/*") + %w(README.md LICENSE CHANGELOG)
end

0 comments on commit 3b7053a

Please sign in to comment.