streamio-ffmpeg 0.8.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.