scottburton11-rvideo 0.9.5

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/History.txt ADDED
@@ -0,0 +1,56 @@
1
+ == 0.9.5 2008-09-09
2
+ Note:
3
+ * Moving hosting to GitHub.
4
+ http://github.com/zencoder/rvideo/tree
5
+
6
+ 2 major enhancements:
7
+ * Large files, or some tools, would dump out so much output that it could run a server out of memory.
8
+ Changed the way that output is parsed. Instead of pulling it all into memory, the log file is sent to
9
+ disk, and then post-processed.
10
+ * Added support for yamdi (http://yamdi.sourceforge.net/) for FLV metadata injection. This is much faster
11
+ than flvtool2 on huge files, and uses less memory as well.
12
+
13
+
14
+ == 0.9.4 2007-12-12
15
+
16
+ 3 major enhancements:
17
+ * Changed transcoder interface. The preferred interface is now to pass the input file to RVideo::Transcoder.new. This allows you to create multiple output files from the same Transcoder object, and inspects the input file metadata before the job is transcoded.
18
+ * Added screengrab functionality to the Inspector class. Fire up an inspector instance on a file, and you can take one or more screenshots through inspected_file.capture_frame("50%").
19
+ * Added resolution/aspect support. Pass :width and :height parameters to a Transcoder instance, and include $resolution$ or $aspect_ratio$ in the recipe.
20
+
21
+ 4 minor enhancements:
22
+ * Remove old/unnecessary files and features
23
+ * Three additional ffmpeg results: unsupported codec and no output streams
24
+ * One additional flvtool2 result: empty input file
25
+ * Check that input file exists earlier in the transcoding process
26
+
27
+ == 0.9.3 2007-10-30
28
+
29
+ One minor enhancement:
30
+ * Reraise all unhandled RVideo exceptions as TranscoderErrors
31
+
32
+ == 0.9.2 2007-10-30
33
+
34
+ One minor bug fix:
35
+ * Correctly parse invalid files, where duration and bitrate are N/A, but start: 0.000 is included
36
+
37
+ == 0.9.1 2007-10-11
38
+
39
+ One major enhancement:
40
+ * Added Mencoder support. (Andre Medeiros)
41
+
42
+ Two minor enhancements:
43
+ * Added total_time method to RVideo::Transcoder instances.
44
+ * Added another error condition for ffmpeg - codec not found.
45
+
46
+ Two notes:
47
+ * Tried and tested using open3 and open4 for command execution, but mencoder would unexpectedly hang with certain files when using these. Reverted these changes.
48
+ * Mencoder has basic unit tests, but needs more tests. In particular, example output should be added for a variety of cases (especially failures and errors).
49
+
50
+ == 0.9.0 2007-09-27
51
+
52
+ * Public RVideo release.
53
+
54
+ == 0.8.0 2007-09-27
55
+
56
+ * RVideo rewrite.
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Jonathan Dahl and Slantwise Design
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.txt ADDED
@@ -0,0 +1,100 @@
1
+ = RVideo
2
+
3
+ == DESCRIPTION:
4
+
5
+ RVideo allows you to inspect and process video files.
6
+
7
+ == INSTALL:
8
+
9
+ Installation is a little involved. First, install the gem:
10
+
11
+ sudo gem install rvideo
12
+
13
+ Next, install ffmpeg and (possibly) other related libraries. This is
14
+ documented elsewhere on the web, and can be a headache. If you are on OS X,
15
+ the Darwinports build is reasonably good (though not perfect). Install with:
16
+
17
+ sudo port install ffmpeg
18
+
19
+ Or, for a better build (recommended), add additional video- and audio-related
20
+ libraries, like this:
21
+
22
+ sudo port install ffmpeg +lame +libogg +vorbis +faac +faad +xvid +x264 +a52
23
+
24
+ Most package management systems include a build of ffmpeg, but many include a
25
+ poor build. So you may need to compile from scratch.
26
+
27
+ If you want to create Flash Video files, also install flvtool2:
28
+
29
+ sudo gem install flvtool2
30
+
31
+ Once ffmpeg and RVideo are installed, you're set.
32
+
33
+ == SYNOPSIS:
34
+
35
+ To inspect a file, initialize an RVideo file inspector object. See the
36
+ documentation for details.
37
+
38
+ A few examples:
39
+
40
+ file = RVideo::Inspector.new(:file => "#{APP_ROOT}/files/input.mp4")
41
+
42
+ file = RVideo::Inspector.new(:raw_response => @existing_response)
43
+
44
+ file = RVideo::Inspector.new(:file => "#{APP_ROOT}/files/input.mp4",
45
+ :ffmpeg_binary => "#{APP_ROOT}/bin/ffmpeg")
46
+
47
+ file.fps # "29.97"
48
+ file.duration # "00:05:23.4"
49
+
50
+ To transcode a video, initialize a Transcoder object.
51
+
52
+ transcoder = RVideo::Transcoder.new
53
+
54
+ Then pass a command and valid options to the execute method
55
+
56
+ recipe = "ffmpeg -i $input_file$ -ar 22050 -ab 64 -f flv -r 29.97 -s"
57
+ recipe += " $resolution$ -y $output_file$"
58
+ recipe += "\nflvtool2 -U $output_file$"
59
+ begin
60
+ transcoder.execute(recipe, {:input_file => "/path/to/input.mp4",
61
+ :output_file => "/path/to/output.flv", :resolution => "640x360"})
62
+ rescue TranscoderError => e
63
+ puts "Unable to transcode file: #{e.class} - #{e.message}"
64
+ end
65
+
66
+ If the job succeeds, you can access the metadata of the input and output
67
+ files with:
68
+
69
+ transcoder.original # RVideo::Inspector object
70
+ transcoder.processed # RVideo::Inspector object
71
+
72
+ If the transcoding succeeds, the file may still have problems. RVideo
73
+ will populate an errors array if the duration of the processed video
74
+ differs from the duration of the original video, or if the processed
75
+ file is unreadable.
76
+
77
+ == FEATURES/PROBLEMS:
78
+
79
+ == REQUIREMENTS:
80
+
81
+ Thanks to Peter Boling for early work on RVideo.
82
+
83
+ Contribute to RVideo! If you want to help out, there are a few things you can
84
+ do.
85
+
86
+ - Use, test, and submit bugs/patches
87
+ - We need a RVideo::Tools::Mencoder class to add mencoder support.
88
+ - Other tool classes would be great - On2, mp4box, Quicktime (?), etc.
89
+ - Submit other fixes, features, optimizations, and refactorings
90
+
91
+ If RVideo is useful to you, you may also be interested in RMovie, another Ruby
92
+ video library. See http://rmovie.rubyforge.org/ for more.
93
+
94
+ Finally, watch for Zencoder, a commercial video transcoder built by Slantwise
95
+ Design. Zencoder uses RVideo for its video processing, but adds file queuing,
96
+ distributed transcoding, a web-based transcoder dashboard, and more. See
97
+ http://zencoder.tv or http://slantwisedesign.com for more.
98
+
99
+ Copyright (c) 2007 Jonathan Dahl and Slantwise Design. Released under the MIT
100
+ license.
data/RULES ADDED
@@ -0,0 +1,11 @@
1
+ Collection of transcoding edge cases and rules
2
+ ------------------------
3
+
4
+ * mpeg4 output errors out if frame rate is not supplied
5
+
6
+ [mpeg4 @ 0x149e810]timebase not supported by mpeg 4 standard
7
+ Error while opening codec for output stream #0.0 - maybe incorrect parameters such as bit_rate, rate, width or height
8
+
9
+ Solution: provide a frame rate with -r (any frame rate will do)
10
+
11
+
data/Rakefile ADDED
@@ -0,0 +1,178 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+ begin
12
+ require 'spec/rake/spectask'
13
+ rescue LoadError
14
+ puts 'To use rspec for testing you must install rspec gem:'
15
+ puts '$ sudo gem install rspec'
16
+ exit
17
+ end
18
+
19
+ include FileUtils
20
+ require File.join(File.dirname(__FILE__), 'lib', 'rvideo', 'version')
21
+
22
+ AUTHOR = 'Jonathan Dahl (Slantwise Design)' # can also be an array of Authors
23
+ EMAIL = "jon@slantwisedesign.com"
24
+ DESCRIPTION = "Inspect and process video or audio files"
25
+ GEM_NAME = 'rvideo' # what ppl will type to install your gem
26
+
27
+ @config_file = "~/.rubyforge/user-config.yml"
28
+ @config = nil
29
+ def rubyforge_username
30
+ unless @config
31
+ begin
32
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
33
+ rescue
34
+ puts <<-EOS
35
+ ERROR: No rubyforge config file found: #{@config_file}"
36
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
37
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
38
+ EOS
39
+ exit
40
+ end
41
+ end
42
+ @rubyforge_username ||= @config["username"]
43
+ end
44
+
45
+ RUBYFORGE_PROJECT = 'rvideo' # The unix name for your project
46
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
47
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
48
+
49
+ NAME = "rvideo"
50
+ REV = nil
51
+ # UNCOMMENT IF REQUIRED:
52
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
53
+ VERS = Rvideo::VERSION::STRING + (REV ? ".#{REV}" : "")
54
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store']
55
+ RDOC_OPTS = ['--quiet', '--title', 'rvideo documentation',
56
+ "--opname", "index.html",
57
+ "--line-numbers",
58
+ "--main", "README",
59
+ "--inline-source"]
60
+
61
+ class Hoe
62
+ def extra_deps
63
+ @extra_deps.reject { |x| Array(x).first == 'hoe' }
64
+ end
65
+ end
66
+
67
+ # Generate all the Rake tasks
68
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
69
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
70
+ p.author = AUTHOR
71
+ p.description = DESCRIPTION
72
+ p.email = EMAIL
73
+ p.summary = DESCRIPTION
74
+ p.url = HOMEPATH
75
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
76
+ p.test_globs = ["test/**/test_*.rb"]
77
+ p.clean_globs |= CLEAN #An array of file patterns to delete on clean.
78
+
79
+ # == Optional
80
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
81
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
82
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
83
+ end
84
+
85
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n")
86
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
87
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
88
+
89
+ desc 'Generate website files'
90
+ task :website_generate do
91
+ Dir['website/**/*.txt'].each do |txt|
92
+ sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
93
+ end
94
+ end
95
+
96
+ desc 'Upload website files to rubyforge'
97
+ task :website_upload do
98
+ host = "#{rubyforge_username}@rubyforge.org"
99
+ remote_dir = "/var/www/gforge-projects/#{PATH}/"
100
+ local_dir = 'website'
101
+ sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}}
102
+ end
103
+
104
+ desc 'Generate and upload website files'
105
+ task :website => [:website_generate, :website_upload, :publish_docs]
106
+
107
+ desc 'Release the website and new gem version'
108
+ task :deploy => [:check_version, :website, :release] do
109
+ puts "Remember to create SVN tag:"
110
+ puts "svn copy svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/trunk " +
111
+ "svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
112
+ puts "Suggested comment:"
113
+ puts "Tagging release #{CHANGES}"
114
+ end
115
+
116
+ desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
117
+ task :local_deploy => [:website_generate, :install_gem]
118
+
119
+ task :check_version do
120
+ unless ENV['VERSION']
121
+ puts 'Must pass a VERSION=x.y.z release version'
122
+ exit
123
+ end
124
+ unless ENV['VERSION'] == VERS
125
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
126
+ exit
127
+ end
128
+ end
129
+
130
+ #require 'rake'
131
+ #require 'spec/rake/spectask'
132
+ require File.dirname(__FILE__) + '/lib/rvideo'
133
+
134
+ namespace :spec do
135
+ desc "Run Unit Specs"
136
+ Spec::Rake::SpecTask.new("units") do |t|
137
+ t.spec_files = FileList['spec/units/**/*.rb']
138
+ end
139
+
140
+ desc "Run Integration Specs"
141
+ Spec::Rake::SpecTask.new("integrations") do |t|
142
+ t.spec_files = FileList['spec/integrations/**/*.rb']
143
+ end
144
+ end
145
+
146
+ desc "Process a file"
147
+ task(:transcode) do
148
+ RVideo::Transcoder.logger = Logger.new(STDOUT)
149
+ transcode_single_job(ENV['RECIPE'], ENV['FILE'])
150
+ end
151
+
152
+ desc "Batch transcode files"
153
+ task(:batch_transcode) do
154
+ RVideo::Transcoder.logger = Logger.new(File.dirname(__FILE__) + '/test/output.log')
155
+ f = YAML::load(File.open(File.dirname(__FILE__) + '/test/batch_transcode.yml'))
156
+ recipes = f['recipes']
157
+ files = f['files']
158
+ files.each do |f|
159
+ file = "#{File.dirname(__FILE__)}/test/files/#{f}"
160
+ recipes.each do |recipe|
161
+ transcode_single_job(recipe, file)
162
+ end
163
+ end
164
+ end
165
+
166
+ def transcode_single_job(recipe, input_file)
167
+ puts "Transcoding #{File.basename(input_file)} to #{recipe}"
168
+ r = YAML::load(File.open(File.dirname(__FILE__) + '/test/recipes.yml'))[recipe]
169
+ transcoder = RVideo::Transcoder.new(input_file)
170
+ output_file = "#{TEMP_PATH}/#{File.basename(input_file, ".*")}-#{recipe}.#{r['extension']}"
171
+ FileUtils.mkdir_p(File.dirname(output_file))
172
+ begin
173
+ transcoder.execute(r['command'], {:output_file => output_file}.merge(r))
174
+ puts "Finished #{File.basename(output_file)} in #{transcoder.total_time}"
175
+ rescue StandardError => e
176
+ puts "Error transcoding #{File.basename(output_file)} - #{e.class} (#{e.message}\n#{e.backtrace})"
177
+ end
178
+ end
data/lib/rvideo.rb ADDED
@@ -0,0 +1,22 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/rvideo'
2
+
3
+ require 'inspector'
4
+ require 'float'
5
+ require 'tools/abstract_tool'
6
+ require 'tools/ffmpeg'
7
+ require 'tools/mencoder'
8
+ require 'tools/flvtool2'
9
+ require 'tools/mp4box'
10
+ require 'tools/mplayer'
11
+ require 'tools/mp4creator'
12
+ require 'tools/ffmpeg2theora'
13
+ require 'tools/yamdi'
14
+ require 'errors'
15
+ require 'transcoder'
16
+ require 'active_support'
17
+
18
+ TEMP_PATH = File.expand_path(File.dirname(__FILE__) + '/../tmp')
19
+ FIXTURE_PATH = File.expand_path(File.dirname(__FILE__) + '/../spec/fixtures')
20
+ TEST_FILE_PATH = File.expand_path(File.dirname(__FILE__) + '/../spec/files')
21
+ REPORT_PATH = File.expand_path(File.dirname(__FILE__) + '/../report')
22
+
@@ -0,0 +1,24 @@
1
+ module RVideo
2
+ class TranscoderError < RuntimeError
3
+ class InvalidCommand < TranscoderError
4
+ end
5
+
6
+ class InvalidFile < TranscoderError
7
+ end
8
+
9
+ class InputFileNotFound < TranscoderError
10
+ end
11
+
12
+ class UnexpectedResult < TranscoderError
13
+ end
14
+
15
+ class ParameterError < TranscoderError
16
+ end
17
+
18
+ class UnknownError < TranscoderError
19
+ end
20
+
21
+ class UnknownTool < TranscoderError
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ # Add a rounding method to the Float class.
2
+ class Float
3
+ def round_to(x)
4
+ (self * 10**x).round.to_f / 10**x
5
+ end
6
+ end
7
+
@@ -0,0 +1,558 @@
1
+ module RVideo # :nodoc:
2
+ class Inspector
3
+
4
+ attr_reader :filename, :path, :full_filename, :raw_response, :raw_metadata
5
+
6
+ attr_accessor :ffmpeg_binary
7
+
8
+ #
9
+ # To inspect a video or audio file, initialize an Inspector object.
10
+ #
11
+ # file = RVideo::Inspector.new(options_hash)
12
+ #
13
+ # Inspector accepts three options: file, raw_response, and ffmpeg_binary.
14
+ # Either raw_response or file is required; ffmpeg binary is optional.
15
+ #
16
+ # :file is a path to a file to be inspected.
17
+ #
18
+ # :raw_response is the full output of "ffmpeg -i [file]". If the
19
+ # :raw_response option is used, RVideo will not actually inspect a file;
20
+ # it will simply parse the provided response. This is useful if your
21
+ # application has already collected the ffmpeg -i response, and you don't
22
+ # want to call it again.
23
+ #
24
+ # :ffmpeg_binary is an optional argument that specifies the path to the
25
+ # ffmpeg binary to be used. If a path is not explicitly declared, RVideo
26
+ # will assume that ffmpeg exists in the Unix path. Type "which ffmpeg" to
27
+ # check if ffmpeg is installed and exists in your operating system's path.
28
+ #
29
+
30
+ def initialize(options = {})
31
+ if options[:raw_response]
32
+ @raw_response = options[:raw_response]
33
+ elsif options[:file]
34
+ if options[:ffmpeg_binary]
35
+ @ffmpeg_binary = options[:ffmpeg_binary]
36
+ raise RuntimeError, "ffmpeg could not be found (trying #{@ffmpeg_binary})" unless FileTest.exist?(@ffmpeg_binary)
37
+ else
38
+ # assume it is in the unix path
39
+ raise RuntimeError, 'ffmpeg could not be found (expected ffmpeg to be found in the Unix path)' unless FileTest.exist?(`which ffmpeg`.chomp)
40
+ @ffmpeg_binary = "ffmpeg"
41
+ end
42
+
43
+ file = options[:file]
44
+ @filename = File.basename(file)
45
+ @path = File.dirname(file)
46
+ @full_filename = file
47
+ raise TranscoderError::InputFileNotFound, "File not found (#{file})" unless FileTest.exist?(file.gsub("\"",""))
48
+ @raw_response = `#{@ffmpeg_binary} -i #{@full_filename} 2>&1`
49
+ else
50
+ raise ArgumentError, "Must supply either an input file or a pregenerated response" if options[:raw_response].nil? and file.nil?
51
+ end
52
+
53
+ metadata = metadata_regexp.match(@raw_response)
54
+
55
+ if /Unknown format/i.match(@raw_response) || metadata.nil?
56
+ @unknown_format = true
57
+ elsif /Duration: N\/A/im.match(@raw_response)
58
+ # elsif /Duration: N\/A|bitrate: N\/A/im.match(@raw_response)
59
+ @unreadable_file = true
60
+ @raw_metadata = metadata[1] # in this case, we can at least still get the container type
61
+ else
62
+ @raw_metadata = metadata[1]
63
+ end
64
+ end
65
+
66
+ #
67
+ # Returns true if the file can be read successfully. Returns false otherwise.
68
+ #
69
+
70
+ def valid?
71
+ if @unknown_format or @unreadable_file
72
+ false
73
+ else
74
+ true
75
+ end
76
+ end
77
+
78
+ #
79
+ # Returns false if the file can be read successfully. Returns false otherwise.
80
+ #
81
+
82
+ def invalid?
83
+ !valid?
84
+ end
85
+
86
+ #
87
+ # True if the format is not understood ("Unknown Format")
88
+ #
89
+
90
+ def unknown_format?
91
+ if @unknown_format
92
+ true
93
+ else
94
+ false
95
+ end
96
+ end
97
+
98
+ #
99
+ # True if the file is not readable ("Duration: N/A, bitrate: N/A")
100
+ #
101
+
102
+ def unreadable_file?
103
+ if @unreadable_file
104
+ true
105
+ else
106
+ false
107
+ end
108
+ end
109
+
110
+ #
111
+ # Does the file have an audio stream?
112
+ #
113
+
114
+ def audio?
115
+ if audio_match.nil?
116
+ false
117
+ else
118
+ true
119
+ end
120
+ end
121
+
122
+ #
123
+ # Does the file have a video stream?
124
+ #
125
+
126
+ def video?
127
+ if video_match.nil?
128
+ false
129
+ else
130
+ true
131
+ end
132
+ end
133
+
134
+ #
135
+ # Returns a match regexp appropriate to the version of ffmpeg in use
136
+ #
137
+
138
+ def metadata_regexp
139
+ case ffmpeg_version
140
+ when "SVN-r18077"
141
+ /(Input \#.*)\nAt/m
142
+ else
143
+ /(Input \#.*)\nMust/m
144
+ end
145
+ end
146
+
147
+ #
148
+ # Take a screengrab of a movie. Requires an input file and a time parameter, and optionally takes an output filename. If no output filename is specfied, constructs one.
149
+ #
150
+ # Three types of time parameters are accepted - percentage (e.g. 3%), time in seconds (e.g. 60 seconds), and raw frame (e.g. 37). Will raise an exception if the time in seconds or the frame are out of the bounds of the input file.
151
+ #
152
+ # Types:
153
+ # 37s (37 seconds)
154
+ # 37f (frame 37)
155
+ # 37% (37 percent)
156
+ # 37 (default to seconds)
157
+ #
158
+ # If a time is outside of the duration of the file, it will choose a frame at the 99% mark.
159
+ #
160
+ # Example:
161
+ #
162
+ # t = RVideo::Transcoder.new('path/to/input_file.mp4')
163
+ # t.capture_frame('10%') # => '/path/to/screenshot/input-10p.jpg'
164
+ #
165
+
166
+ def capture_frame(timecode, output_file = nil)
167
+ t = calculate_time(timecode)
168
+ unless output_file
169
+ output_file = "#{TEMP_PATH}/#{File.basename(@full_filename, ".*")}-#{timecode.gsub("%","p")}.jpg"
170
+ end
171
+ # do the work
172
+ # mplayer $input_file$ -ss $start_time$ -frames 1 -vo jpeg -o $output_file$
173
+ # ffmpeg -i $input_file$ -v nopb -ss $start_time$ -b $bitrate$ -an -vframes 1 -y $output_file$
174
+ command = "ffmpeg -i #{@full_filename} -ss #{t} -t 00:00:01 -r 1 -vframes 1 -f image2 #{output_file}"
175
+ Transcoder.logger.info("\nCreating Screenshot: #{command}\n")
176
+ frame_result = `#{command} 2>&1`
177
+ Transcoder.logger.info("\nScreenshot results: #{frame_result}")
178
+ output_file
179
+ end
180
+
181
+ def calculate_time(timecode)
182
+ m = /\A([0-9\.\,]*)(s|f|%)?\Z/.match(timecode)
183
+ if m.nil? or m[1].nil? or m[1].empty?
184
+ raise TranscoderError::ParameterError, "Invalid timecode for frame capture: #{timecode}. Must be a number, optionally followed by s, f, or %."
185
+ end
186
+
187
+ case m[2]
188
+ when "s", nil
189
+ t = m[1].to_f
190
+ when "f"
191
+ t = m[1].to_f / fps.to_f
192
+ when "%"
193
+ # milliseconds / 1000 * percent / 100
194
+ t = (duration.to_i / 1000.0) * (m[1].to_f / 100.0)
195
+ else
196
+ raise TranscoderError::ParameterError, "Invalid timecode for frame capture: #{timecode}. Must be a number, optionally followed by s, f, or p."
197
+ end
198
+
199
+ if (t * 1000) > duration
200
+ calculate_time("99%")
201
+ else
202
+ t
203
+ end
204
+ end
205
+
206
+ #
207
+ # Returns the version of ffmpeg used, In practice, this may or may not be
208
+ # useful.
209
+ #
210
+ # Examples:
211
+ #
212
+ # SVN-r6399
213
+ # CVS
214
+ #
215
+
216
+ def ffmpeg_version
217
+ @ffmpeg_version = @raw_response.split("\n").first.split("version").last.split(",").first.strip
218
+ end
219
+
220
+ #
221
+ # Returns the configuration options used to build ffmpeg.
222
+ #
223
+ # Example:
224
+ #
225
+ # --enable-mp3lame --enable-gpl --disable-ffplay --disable-ffserver
226
+ # --enable-a52 --enable-xvid
227
+ #
228
+
229
+ def ffmpeg_configuration
230
+ /(\s*configuration:)(.*)\n/.match(@raw_response)[2].strip
231
+ end
232
+
233
+ #
234
+ # Returns the versions of libavutil, libavcodec, and libavformat used by
235
+ # ffmpeg.
236
+ #
237
+ # Example:
238
+ #
239
+ # libavutil version: 49.0.0
240
+ # libavcodec version: 51.9.0
241
+ # libavformat version: 50.4.0
242
+ #
243
+
244
+ def ffmpeg_libav
245
+ /^(\s*lib.*\n)+/.match(@raw_response)[0].split("\n").each {|l| l.strip! }
246
+ end
247
+
248
+ #
249
+ # Returns the build description for ffmpeg.
250
+ #
251
+ # Example:
252
+ #
253
+ # built on Apr 15 2006 04:58:19, gcc: 4.0.1 (Apple Computer, Inc. build
254
+ # 5250)
255
+ #
256
+
257
+ def ffmpeg_build
258
+ /(\n\s*)(built on.*)(\n)/.match(@raw_response)[2]
259
+ end
260
+
261
+ #
262
+ # Returns the container format for the file. Instead of returning a single
263
+ # format, this may return a string of related formats.
264
+ #
265
+ # Examples:
266
+ #
267
+ # "avi"
268
+ #
269
+ # "mov,mp4,m4a,3gp,3g2,mj2"
270
+ #
271
+
272
+ def container
273
+ return nil if @unknown_format
274
+
275
+ /Input \#\d+\,\s*(\S+),\s*from/.match(@raw_metadata)[1]
276
+ end
277
+
278
+ #
279
+ # The duration of the movie, as a string.
280
+ #
281
+ # Example:
282
+ #
283
+ # "00:00:24.4" # 24.4 seconds
284
+ #
285
+ def raw_duration
286
+ return nil unless valid?
287
+
288
+ /Duration:\s*([0-9\:\.]+),/.match(@raw_metadata)[1]
289
+ end
290
+
291
+ #
292
+ # The duration of the movie in milliseconds, as an integer.
293
+ #
294
+ # Example:
295
+ #
296
+ # 24400 # 24.4 seconds
297
+ #
298
+ # Note that the precision of the duration is in tenths of a second, not
299
+ # thousandths, but milliseconds are a more standard unit of time than
300
+ # deciseconds.
301
+ #
302
+
303
+ def duration
304
+ return nil unless valid?
305
+
306
+ units = raw_duration.split(":")
307
+ (units[0].to_i * 60 * 60 * 1000) + (units[1].to_i * 60 * 1000) + (units[2].to_f * 1000).to_i
308
+ end
309
+
310
+ #
311
+ # The bitrate of the movie.
312
+ #
313
+ # Example:
314
+ #
315
+ # 3132
316
+ #
317
+
318
+ def bitrate
319
+ return nil unless valid?
320
+
321
+ bitrate_match[1].to_i
322
+ end
323
+
324
+ #
325
+ # The bitrate units used. In practice, this may always be kb/s.
326
+ #
327
+ # Example:
328
+ #
329
+ # "kb/s"
330
+ #
331
+
332
+ def bitrate_units
333
+ return nil unless valid?
334
+
335
+ bitrate_match[2]
336
+ end
337
+
338
+ def audio_bit_rate # :nodoc:
339
+ nil
340
+ end
341
+
342
+ def audio_stream
343
+ return nil unless valid?
344
+
345
+ #/\n\s*Stream.*Audio:.*\n/.match(@raw_response)[0].strip
346
+ match = /\n\s*Stream.*Audio:.*\n/.match(@raw_response)
347
+ return match[0].strip if match
348
+ end
349
+
350
+ #
351
+ # The audio codec used.
352
+ #
353
+ # Example:
354
+ #
355
+ # "aac"
356
+ #
357
+
358
+ def audio_codec
359
+ return nil unless audio?
360
+
361
+ audio_match[2]
362
+ end
363
+
364
+ #
365
+ # The sampling rate of the audio stream.
366
+ #
367
+ # Example:
368
+ #
369
+ # 44100
370
+ #
371
+
372
+ def audio_sample_rate
373
+ return nil unless audio?
374
+
375
+ audio_match[3].to_i
376
+ end
377
+
378
+ #
379
+ # The units used for the sampling rate. May always be Hz.
380
+ #
381
+ # Example:
382
+ #
383
+ # "Hz"
384
+ #
385
+
386
+ def audio_sample_units
387
+ return nil unless audio?
388
+
389
+ audio_match[4]
390
+ end
391
+
392
+ #
393
+ # The channels used in the audio stream.
394
+ #
395
+ # Examples:
396
+ # "stereo"
397
+ # "mono"
398
+ # "5:1"
399
+ #
400
+
401
+ def audio_channels_string
402
+ return nil unless audio?
403
+
404
+ audio_match[5]
405
+ end
406
+
407
+ def audio_channels
408
+ return nil unless audio?
409
+
410
+ case audio_match[5]
411
+ when "mono"
412
+ 1
413
+ when "stereo"
414
+ 2
415
+ else
416
+ raise RuntimeError, "Unknown number of channels: #{audio_channels}"
417
+ end
418
+ end
419
+
420
+ #
421
+ # The ID of the audio stream (useful for troubleshooting).
422
+ #
423
+ # Example:
424
+ # #0.1
425
+ #
426
+
427
+ def audio_stream_id
428
+ return nil unless audio?
429
+
430
+ audio_match[1]
431
+ end
432
+
433
+ def video_stream
434
+ return nil unless valid?
435
+
436
+ match = /\n\s*Stream.*Video:.*\n/.match(@raw_response)
437
+ return match[0].strip unless match.nil?
438
+ nil
439
+ end
440
+
441
+ #
442
+ # The ID of the video stream (useful for troubleshooting).
443
+ #
444
+ # Example:
445
+ # #0.0
446
+ #
447
+
448
+ def video_stream_id
449
+ return nil unless video?
450
+
451
+ video_match[1]
452
+ end
453
+
454
+ #
455
+ # The video codec used.
456
+ #
457
+ # Example:
458
+ #
459
+ # "mpeg4"
460
+ #
461
+
462
+ def video_codec
463
+ return nil unless video?
464
+
465
+ video_match[2]
466
+ end
467
+
468
+ #
469
+ # The colorspace of the video stream.
470
+ #
471
+ # Example:
472
+ #
473
+ # "yuv420p"
474
+ #
475
+
476
+ def video_colorspace
477
+ return nil unless video?
478
+
479
+ video_match[3]
480
+ end
481
+
482
+ #
483
+ # The width of the video in pixels.
484
+ #
485
+
486
+ def width
487
+ return nil unless video?
488
+
489
+ video_match[4].to_i
490
+ end
491
+
492
+ #
493
+ # The height of the video in pixels.
494
+ #
495
+
496
+ def height
497
+ return nil unless video?
498
+
499
+ video_match[5].to_i
500
+ end
501
+
502
+ #
503
+ # width x height, as a string.
504
+ #
505
+ # Examples:
506
+ # 320x240
507
+ # 1280x720
508
+ #
509
+
510
+ def resolution
511
+ return nil unless video?
512
+
513
+ "#{width}x#{height}"
514
+ end
515
+
516
+ #
517
+ # The frame rate of the video in frames per second
518
+ #
519
+ # Example:
520
+ #
521
+ # "29.97"
522
+ #
523
+
524
+ def fps
525
+ return nil unless video?
526
+
527
+ /([0-9\.]+) (fps|tb)/.match(video_stream)[1]
528
+ end
529
+
530
+ private
531
+
532
+ def bitrate_match
533
+ /bitrate: ([0-9\.]+)\s*(.*)\s+/.match(@raw_metadata)
534
+ end
535
+
536
+ def audio_match
537
+ return nil unless valid?
538
+
539
+ /Stream\s*(.*?)[,|:|\(|\[].*?\s*Audio:\s*(.*?),\s*([0-9\.]*) (\w*),\s*([a-zA-Z:]*)/.match(audio_stream)
540
+ end
541
+
542
+ def video_match
543
+ return nil unless valid?
544
+
545
+ match = /Stream\s*(.*?)[,|:|\(|\[].*?\s*Video:\s*(.*?),\s*(.*?),\s*(\d*)x(\d*)/.match(video_stream)
546
+
547
+ # work-around for Apple Intermediate format, which does not have a color space
548
+ # I fake up a match data object (yea, duck typing!) with an empty spot where
549
+ # the color space would be.
550
+ if match.nil?
551
+ match = /Stream\s*(.*?)[,|:|\(|\[].*?\s*Video:\s*(.*?),\s*(\d*)x(\d*)/.match(video_stream)
552
+ match = [nil, match[1], match[2], nil, match[3], match[4]] unless match.nil?
553
+ end
554
+
555
+ match
556
+ end
557
+ end
558
+ end