streamio-ffmpeg 0.8.5 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,27 @@
1
+ == 0.9.0 2012-07-24
2
+
3
+ New:
4
+ * Bumped target ffmpeg version to 0.11.1
5
+ * Add hung process detection with configurable timeout (thanks stakach)
6
+ * Raise FFMPEG::Error instead of generic RuntimeError on failed transcodings
7
+ * Movie#screenshot for more intuitive screenshotting (README has details)
8
+ * Movie#creation_time and Movie#rotation attributes when metadata is available (thanks Innonate)
9
+
10
+ Bugs:
11
+ * Fixed too many open files bug (thanks to akicho8)
12
+ * Fixed missing path escaping (thanks to mikesager)
13
+ * Fixed README typo (thanks to Linutux)
14
+ * Files outputing "could not find codec parameters" are now recognized as invalid
15
+
16
+ Deprecations:
17
+ * Removed Movie#uncertain_duration?
18
+ * Removed all the deprecated crop options (use :custom => '-vf crop=x:x:x:x' if you need it)
19
+
20
+ Refactorings:
21
+ * Removed the deprecated duration validation code
22
+ * Polish on the transcoder class
23
+ * Polish on the spec suite
24
+
1
25
  == 0.8.5 2011-03-05
2
26
 
3
27
  * If a clip has a DAR that doesn't make sense fall back to calculating aspect ratio from dimensions
data/README.md CHANGED
@@ -12,7 +12,7 @@ Installation
12
12
 
13
13
  (sudo) gem install streamio-ffmpeg
14
14
 
15
- This version is tested against ffmpeg 0.8. So no guarantees with earlier (or much later) versions. Output and input standards have inconveniently changed rather a lot between versions of ffmpeg. My goal is to keep this library in sync with new versions of ffmpeg as they come along.
15
+ This version is tested against ffmpeg 0.11.1. So no guarantees with earlier (or much later) versions. Output and input standards have inconveniently changed rather a lot between versions of ffmpeg. My goal is to keep this library in sync with new versions of ffmpeg as they come along.
16
16
 
17
17
  Usage
18
18
  -----
@@ -60,7 +60,7 @@ movie.transcode("tmp/movie.mp4") # Default ffmpeg settings for mp4 format
60
60
  Keep track of progress with an optional block.
61
61
 
62
62
  ``` ruby
63
- movie.transcode(movie.mp4") { |progress| puts progress } # 0.2 ... 0.5 ... 1.0
63
+ movie.transcode("movie.mp4") { |progress| puts progress } # 0.2 ... 0.5 ... 1.0
64
64
  ```
65
65
 
66
66
  Give custom command line options with a string.
@@ -73,10 +73,10 @@ Use the EncodingOptions parser for humanly readable transcoding options. Below y
73
73
 
74
74
  ``` ruby
75
75
  options = {:video_codec => "libx264", :frame_rate => 10, :resolution => "320x240", :video_bitrate => 300, :video_bitrate_tolerance => 100,
76
- :croptop => 60, :cropbottom => 60, :cropleft => 10, :cropright => 10, :aspect => 1.333333, :keyframe_interval => 90,
76
+ :aspect => 1.333333, :keyframe_interval => 90,
77
77
  :audio_codec => "libfaac", :audio_bitrate => 32, :audio_sample_rate => 22050, :audio_channels => 1,
78
78
  :threads => 2,
79
- :custom => "-flags +loop -cmp +chroma -partitions +parti4x4+partp8x8 -flags2 +mixed_refs -me_method umh -subq 6 -refs 6 -rc_eq 'blurCplx^(1-qComp)' -coder 0 -me_range 16 -g 250 -keyint_min 25 -sc_threshold 40 -i_qfactor 0.71 -qcomp 0.6 -qmin 10 -qmax 51 -qdiff 4 -level 21"}
79
+ :custom => "-vf crop=60:60:10:10"}
80
80
  movie.transcode("movie.mp4", options)
81
81
  ```
82
82
 
@@ -116,11 +116,24 @@ options = {:video_min_bitrate => 600, :video_max_bitrate => 600, :buffer_size =>
116
116
  movie.transcode("movie.flv", options)
117
117
  ```
118
118
 
119
- Use ffpreset files to avoid headaches when encoding with libx264 (http://www.ffmpeg.org/ffmpeg-doc.html#SEC13).
119
+ ### Taking Screenshots
120
+
121
+ You can use the screenshot method to make taking screenshots a bit simpler.
122
+
123
+ ``` ruby
124
+ movie.screenshot("screenshot.jpg")
125
+ ```
126
+
127
+ The screenshot method has the very same API as transcode so the same options will work.
128
+
129
+ ``` ruby
130
+ movie.screenshot("screenshot.bmp", :seek_time => 5, :resolution => '320x240')
131
+ ```
132
+
133
+ You can preserve aspect ratio the same way as when using transcode.
120
134
 
121
135
  ``` ruby
122
- options = {:video_codec => "libx264", :video_preset => "medium"} # audio_preset and file_preset also availible
123
- movie.transcode("movie.mp4", options) # encodes video using libx264-medium.ffpreset
136
+ movie.screenshot("screenshot.png", {:seek_time => 2, :resolution => '200x120'}, :preserve_aspect_ratio => :width)
124
137
  ```
125
138
 
126
139
  Specify the path to ffmpeg
@@ -134,6 +147,22 @@ FFMPEG.ffmpeg_binary = '/usr/local/bin/ffmpeg'
134
147
 
135
148
  This will cause the same command to run as "/usr/local/bin/ffmpeg -i /path/to/input.file ..." instead.
136
149
 
150
+
151
+ Automatically kill hung processes
152
+ ---------------------------------
153
+
154
+ 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.
155
+ It is possible to modify this behaviour by setting a new default:
156
+
157
+ ``` ruby
158
+ # Change the timeout
159
+ Transcoder.timeout = 30
160
+
161
+ # Disable the timeout altogether
162
+ Transcoder.timeout = false
163
+ ```
164
+
165
+
137
166
  Copyright
138
167
  ---------
139
168
 
@@ -48,22 +48,6 @@ module FFMPEG
48
48
  self[:aspect].nil? && self[:resolution]
49
49
  end
50
50
 
51
- def convert_croptop(value)
52
- "-croptop #{value}"
53
- end
54
-
55
- def convert_cropbottom(value)
56
- "-cropbottom #{value}"
57
- end
58
-
59
- def convert_cropleft(value)
60
- "-cropleft #{value}"
61
- end
62
-
63
- def convert_cropright(value)
64
- "-cropright #{value}"
65
- end
66
-
67
51
  def convert_video_codec(value)
68
52
  "-vcodec #{value}"
69
53
  end
@@ -77,7 +61,7 @@ module FFMPEG
77
61
  end
78
62
 
79
63
  def convert_video_bitrate(value)
80
- "-b #{k_format(value)}"
64
+ "-b:v #{k_format(value)}"
81
65
  end
82
66
 
83
67
  def convert_audio_codec(value)
@@ -85,7 +69,7 @@ module FFMPEG
85
69
  end
86
70
 
87
71
  def convert_audio_bitrate(value)
88
- "-ab #{k_format(value)}"
72
+ "-b:a #{k_format(value)}"
89
73
  end
90
74
 
91
75
  def convert_audio_sample_rate(value)
@@ -140,6 +124,10 @@ module FFMPEG
140
124
  "-ss #{value}"
141
125
  end
142
126
 
127
+ def convert_screenshot(value)
128
+ value ? "-vframes 1 -f image2" : ""
129
+ end
130
+
143
131
  def convert_custom(value)
144
132
  value
145
133
  end
@@ -0,0 +1,4 @@
1
+ module FFMPEG
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,59 @@
1
+ if RUBY_VERSION =~ /1\.8/
2
+ # Useful when `timeout.rb`, which, on M.R.I 1.8, relies on green threads, does not work consistently.
3
+ begin
4
+ require 'system_timer'
5
+ FFMPEG::Timer = SystemTimer
6
+ rescue LoadError
7
+ require 'timeout'
8
+ FFMPEG::Timer = Timeout
9
+ end
10
+ else
11
+ require 'timeout'
12
+ FFMPEG::Timer = Timeout
13
+ end
14
+
15
+ require 'win32/process' if RUBY_PLATFORM =~ /(win|w)(32|64)$/
16
+
17
+ #
18
+ # Monkey Patch timeout support into the IO class
19
+ #
20
+ class IO
21
+ def each_with_timeout(pid, seconds, sep_string=$/)
22
+ sleeping_queue = Queue.new
23
+ thread = nil
24
+
25
+ timer_set = lambda do
26
+ thread = new_thread(pid) { FFMPEG::Timer.timeout(seconds) { sleeping_queue.pop } }
27
+ end
28
+
29
+ timer_cancel = lambda do
30
+ thread.kill if thread rescue nil
31
+ end
32
+
33
+ timer_set.call
34
+ each(sep_string) do |buffer|
35
+ timer_cancel.call
36
+ yield buffer
37
+ timer_set.call
38
+ end
39
+ ensure
40
+ timer_cancel.call
41
+ end
42
+
43
+ private
44
+ def new_thread(pid, &block)
45
+ current_thread = Thread.current
46
+ Thread.new do
47
+ begin
48
+ block.call
49
+ rescue Exception => e
50
+ current_thread.raise e
51
+ if RUBY_PLATFORM =~ /(win|w)(32|64)$/
52
+ Process.kill(1, pid)
53
+ else
54
+ Process.kill('SIGKILL', pid)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/ffmpeg/movie.rb CHANGED
@@ -1,6 +1,8 @@
1
+ require 'time'
2
+
1
3
  module FFMPEG
2
4
  class Movie
3
- attr_reader :path, :duration, :time, :bitrate
5
+ attr_reader :path, :duration, :time, :bitrate, :rotation, :creation_time
4
6
  attr_reader :video_stream, :video_codec, :video_bitrate, :colorspace, :resolution, :dar
5
7
  attr_reader :audio_stream, :audio_codec, :audio_bitrate, :audio_sample_rate
6
8
 
@@ -9,8 +11,9 @@ module FFMPEG
9
11
 
10
12
  @path = path
11
13
 
12
- stdin, stdout, stderr = Open3.popen3("#{FFMPEG.ffmpeg_binary} -i '#{path}'") # Output will land in stderr
13
- output = stderr.read
14
+ # ffmpeg will output to stderr
15
+ command = "#{FFMPEG.ffmpeg_binary} -i #{Shellwords.escape(path)}"
16
+ output = Open3.popen3(command) { |stdin, stdout, stderr| stderr.read }
14
17
 
15
18
  fix_encoding(output)
16
19
 
@@ -19,18 +22,22 @@ module FFMPEG
19
22
 
20
23
  output[/start: (\d*\.\d*)/]
21
24
  @time = $1 ? $1.to_f : 0.0
25
+
26
+ output[/creation_time {1,}: {1,}(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/]
27
+ @creation_time = $1 ? Time.parse("#{$1}") : nil
22
28
 
23
29
  output[/bitrate: (\d*)/]
24
30
  @bitrate = $1 ? $1.to_i : nil
25
31
 
32
+ output[/rotate\ {1,}:\ {1,}(\d*)/]
33
+ @rotation = $1 ? $1.to_i : nil
34
+
26
35
  output[/Video: (.*)/]
27
36
  @video_stream = $1
28
37
 
29
38
  output[/Audio: (.*)/]
30
39
  @audio_stream = $1
31
40
 
32
- @uncertain_duration = true #output.include?("Estimating duration from bitrate, this may be inaccurate") || @time > 0
33
-
34
41
  if video_stream
35
42
  @video_codec, @colorspace, resolution, video_bitrate = video_stream.split(/\s?,\s?/)
36
43
  @video_bitrate = video_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil
@@ -46,16 +53,13 @@ module FFMPEG
46
53
 
47
54
  @invalid = true if @video_stream.to_s.empty? && @audio_stream.to_s.empty?
48
55
  @invalid = true if output.include?("is not supported")
56
+ @invalid = true if output.include?("could not find codec parameters")
49
57
  end
50
58
 
51
59
  def valid?
52
60
  not @invalid
53
61
  end
54
62
 
55
- def uncertain_duration?
56
- @uncertain_duration
57
- end
58
-
59
63
  def width
60
64
  resolution.split("x")[0].to_i rescue nil
61
65
  end
@@ -88,6 +92,10 @@ module FFMPEG
88
92
  Transcoder.new(self, output_file, options, transcoder_options).run &block
89
93
  end
90
94
 
95
+ def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
96
+ Transcoder.new(self, output_file, options.merge(:screenshot => true), transcoder_options).run &block
97
+ end
98
+
91
99
  protected
92
100
  def aspect_from_dar
93
101
  return nil unless dar
@@ -3,6 +3,16 @@ require 'shellwords'
3
3
 
4
4
  module FFMPEG
5
5
  class Transcoder
6
+ @@timeout = 200
7
+
8
+ def self.timeout=(time)
9
+ @@timeout = time
10
+ end
11
+
12
+ def self.timeout
13
+ @@timeout
14
+ end
15
+
6
16
  def initialize(movie, output_file, options = EncodingOptions.new, transcoder_options = {})
7
17
  @movie = movie
8
18
  @output_file = output_file
@@ -24,30 +34,42 @@ module FFMPEG
24
34
  # ffmpeg < 0.8: frame= 413 fps= 48 q=31.0 size= 2139kB time=16.52 bitrate=1060.6kbits/s
25
35
  # ffmpeg >= 0.8: frame= 4855 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/
26
36
  def run
27
- command = "#{FFMPEG.ffmpeg_binary} -y -i #{Shellwords.escape(@movie.path)} #{@raw_options} '#{@output_file}'"
37
+ command = "#{FFMPEG.ffmpeg_binary} -y -i #{Shellwords.escape(@movie.path)} #{@raw_options} #{Shellwords.escape(@output_file)}"
28
38
  FFMPEG.logger.info("Running transcoding...\n#{command}\n")
29
39
  output = ""
30
40
  last_output = nil
31
- Open3.popen3(command) do |stdin, stdout, stderr|
32
- yield(0.0) if block_given?
33
- stderr.each("r") do |line|
34
- fix_encoding(line)
35
- output << line
36
- if line.include?("time=")
37
- if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
38
- time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
39
- elsif line =~ /time=(\d+.\d+)/ # ffmpeg 0.7 and below style
40
- time = $1.to_f
41
- else # better make sure it wont blow up in case of unexpected output
42
- time = 0.0
41
+ Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
42
+ begin
43
+ yield(0.0) if block_given?
44
+ next_line = Proc.new do |line|
45
+ fix_encoding(line)
46
+ output << line
47
+ if line.include?("time=")
48
+ if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
49
+ time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
50
+ elsif line =~ /time=(\d+.\d+)/ # ffmpeg 0.7 and below style
51
+ time = $1.to_f
52
+ else # better make sure it wont blow up in case of unexpected output
53
+ time = 0.0
54
+ end
55
+ progress = time / @movie.duration
56
+ yield(progress) if block_given?
57
+ end
58
+ if line =~ /Unsupported codec/
59
+ FFMPEG.logger.error "Failed encoding...\nCommand\n#{command}\nOutput\n#{output}\n"
60
+ raise "Failed encoding: #{line}"
43
61
  end
44
- progress = time / @movie.duration
45
- yield(progress) if block_given?
46
62
  end
47
- if line =~ /Unsupported codec/
48
- FFMPEG.logger.error "Failed encoding...\nCommand\n#{command}\nOutput\n#{output}\n"
49
- raise "Failed encoding: #{line}"
63
+
64
+ if @@timeout
65
+ stderr.each_with_timeout(wait_thr.pid, @@timeout, "r", &next_line)
66
+ else
67
+ stderr.each("r", &next_line)
50
68
  end
69
+
70
+ rescue Timeout::Error => e
71
+ FFMPEG.logger.error "Process hung...\nCommand\n#{command}\nOutput\n#{output}\n"
72
+ raise FFMPEG::Error, "Process hung. Full output: #{output}"
51
73
  end
52
74
  end
53
75
 
@@ -55,34 +77,17 @@ module FFMPEG
55
77
  yield(1.0) if block_given?
56
78
  FFMPEG.logger.info "Transcoding of #{@movie.path} to #{@output_file} succeeded\n"
57
79
  else
58
- errors = @errors.empty? ? "" : " Errors: #{@errors.join(", ")}. "
80
+ errors = "Errors: #{@errors.join(", ")}. "
59
81
  FFMPEG.logger.error "Failed encoding...\n#{command}\n\n#{output}\n#{errors}\n"
60
- raise "Failed encoding.#{errors}Full output: #{output}"
82
+ raise FFMPEG::Error, "Failed encoding.#{errors}Full output: #{output}"
61
83
  end
62
84
 
63
85
  encoded
64
86
  end
65
87
 
66
88
  def encoding_succeeded?
67
- unless File.exists?(@output_file)
68
- @errors << "no output file created"
69
- return false
70
- end
71
-
72
- unless encoded.valid?
73
- @errors << "encoded file is invalid"
74
- return false
75
- end
76
-
77
- if validate_duration?
78
- precision = @raw_options[:duration] ? 1.5 : 1.1
79
- desired_duration = @raw_options[:duration] && @raw_options[:duration] < @movie.duration ? @raw_options[:duration] : @movie.duration
80
- if (encoded.duration >= (desired_duration * precision) or encoded.duration <= (desired_duration / precision))
81
- @errors << "encoded file duration differed from original/specified duration (wanted: #{desired_duration}sec, got: #{encoded.duration}sec)"
82
- return false
83
- end
84
- end
85
-
89
+ @errors << "no output file created" and return false unless File.exists?(@output_file)
90
+ @errors << "encoded file is invalid" and return false unless encoded.valid?
86
91
  true
87
92
  end
88
93
 
@@ -107,13 +112,6 @@ module FFMPEG
107
112
  end
108
113
  end
109
114
 
110
- def validate_duration?
111
- return false if @movie.uncertain_duration?
112
- return false if %w(.jpg .png).include?(File.extname(@output_file))
113
- return false if @raw_options.is_a?(String)
114
- true
115
- end
116
-
117
115
  def fix_encoding(output)
118
116
  output[/test/]
119
117
  rescue ArgumentError
@@ -1,3 +1,3 @@
1
1
  module FFMPEG
2
- VERSION = "0.8.5"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -4,10 +4,12 @@ require 'logger'
4
4
  require 'stringio'
5
5
 
6
6
  require 'ffmpeg/version'
7
+ require 'ffmpeg/errors'
7
8
  require 'ffmpeg/movie'
9
+ require 'ffmpeg/io_monkey'
8
10
  require 'ffmpeg/transcoder'
9
11
  require 'ffmpeg/encoding_options'
10
-
12
+
11
13
  module FFMPEG
12
14
  # FFMPEG logs information about its progress when it's transcoding.
13
15
  # Jack in your own logger through this method if you wish to.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: streamio-ffmpeg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.5
4
+ version: 0.9.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-05 00:00:00.000000000 Z
12
+ date: 2012-07-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &70155720045540 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,15 @@ dependencies:
21
21
  version: '2.7'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70155720045540
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '2.7'
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: rake
27
- requirement: &70155720044980 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ~>
@@ -32,7 +37,12 @@ dependencies:
32
37
  version: 0.9.2
33
38
  type: :development
34
39
  prerelease: false
35
- version_requirements: *70155720044980
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.9.2
36
46
  description: Simple yet powerful wrapper around ffmpeg to get metadata from movies
37
47
  and do transcoding.
38
48
  email:
@@ -42,6 +52,8 @@ extensions: []
42
52
  extra_rdoc_files: []
43
53
  files:
44
54
  - lib/ffmpeg/encoding_options.rb
55
+ - lib/ffmpeg/errors.rb
56
+ - lib/ffmpeg/io_monkey.rb
45
57
  - lib/ffmpeg/movie.rb
46
58
  - lib/ffmpeg/transcoder.rb
47
59
  - lib/ffmpeg/version.rb
@@ -63,7 +75,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
75
  version: '0'
64
76
  segments:
65
77
  - 0
66
- hash: -4488954591217010798
78
+ hash: 190048295991981084
67
79
  required_rubygems_version: !ruby/object:Gem::Requirement
68
80
  none: false
69
81
  requirements:
@@ -72,10 +84,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
84
  version: '0'
73
85
  segments:
74
86
  - 0
75
- hash: -4488954591217010798
87
+ hash: 190048295991981084
76
88
  requirements: []
77
89
  rubyforge_project:
78
- rubygems_version: 1.8.17
90
+ rubygems_version: 1.8.24
79
91
  signing_key:
80
92
  specification_version: 3
81
93
  summary: Reads metadata and transcodes movies.