twinge-rvideo 0.9.6
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 +70 -0
- data/ENV +100 -0
- data/ENV2 +129 -0
- data/LICENSE +20 -0
- data/Manifest +67 -0
- data/README +91 -0
- data/RULES +11 -0
- data/Rakefile +63 -0
- data/config/boot.rb +25 -0
- data/lib/rvideo.rb +44 -0
- data/lib/rvideo/errors.rb +24 -0
- data/lib/rvideo/float.rb +7 -0
- data/lib/rvideo/frame_capturer.rb +126 -0
- data/lib/rvideo/inspector.rb +481 -0
- data/lib/rvideo/reporter.rb +176 -0
- data/lib/rvideo/reporter/views/index.html.erb +27 -0
- data/lib/rvideo/reporter/views/report.css +27 -0
- data/lib/rvideo/reporter/views/report.html.erb +81 -0
- data/lib/rvideo/reporter/views/report.js +9 -0
- data/lib/rvideo/string.rb +5 -0
- data/lib/rvideo/tools/abstract_tool.rb +401 -0
- data/lib/rvideo/tools/ffmpeg.rb +277 -0
- data/lib/rvideo/tools/ffmpeg2theora.rb +42 -0
- data/lib/rvideo/tools/flvtool2.rb +50 -0
- data/lib/rvideo/tools/mencoder.rb +103 -0
- data/lib/rvideo/tools/mp4box.rb +21 -0
- data/lib/rvideo/tools/mp4creator.rb +35 -0
- data/lib/rvideo/tools/mplayer.rb +31 -0
- data/lib/rvideo/tools/qtfaststart.rb +37 -0
- data/lib/rvideo/tools/yamdi.rb +44 -0
- data/lib/rvideo/transcoder.rb +120 -0
- data/lib/rvideo/version.rb +9 -0
- data/rvideo.gemspec +37 -0
- data/scripts/txt2html +67 -0
- data/setup.rb +1585 -0
- data/spec/files/boat.avi +0 -0
- data/spec/files/kites.mp4 +0 -0
- data/spec/fixtures/ffmpeg_builds.yml +28 -0
- data/spec/fixtures/ffmpeg_results.yml +608 -0
- data/spec/fixtures/files.yml +398 -0
- data/spec/fixtures/recipes.yml +58 -0
- data/spec/integrations/formats_spec.rb +315 -0
- data/spec/integrations/frame_capturer_spec.rb +26 -0
- data/spec/integrations/inspection_spec.rb +112 -0
- data/spec/integrations/recipes_spec.rb +0 -0
- data/spec/integrations/rvideo_spec.rb +17 -0
- data/spec/integrations/transcoder_integration_spec.rb +29 -0
- data/spec/integrations/transcoding_spec.rb +9 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support.rb +36 -0
- data/spec/units/abstract_tool_spec.rb +111 -0
- data/spec/units/ffmpeg_spec.rb +323 -0
- data/spec/units/flvtool2_spec.rb +324 -0
- data/spec/units/frame_capturer_spec.rb +72 -0
- data/spec/units/inspector_spec.rb +59 -0
- data/spec/units/mencoder_spec.rb +4994 -0
- data/spec/units/mp4box_spec.rb +34 -0
- data/spec/units/mp4creator_spec.rb +34 -0
- data/spec/units/mplayer_spec.rb +34 -0
- data/spec/units/qtfaststart_spec.rb +35 -0
- data/spec/units/string_spec.rb +8 -0
- data/spec/units/transcoder_spec.rb +156 -0
- data/tasks/deployment.rake +5 -0
- data/tasks/testing.rake +27 -0
- data/tasks/transcoding.rake +40 -0
- data/tasks/website.rake +8 -0
- metadata +175 -0
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,63 @@
|
|
|
1
|
+
require "rubygems"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "echoe"
|
|
4
|
+
|
|
5
|
+
__HERE__ = File.dirname(__FILE__)
|
|
6
|
+
require File.join(__HERE__, 'lib', 'rvideo', 'version')
|
|
7
|
+
require File.join(__HERE__, 'lib', 'rvideo')
|
|
8
|
+
|
|
9
|
+
###
|
|
10
|
+
|
|
11
|
+
AUTHOR = [
|
|
12
|
+
"Peter Boling",
|
|
13
|
+
"Jonathan Dahl (Slantwise Design)",
|
|
14
|
+
"Seth Thomas Rasmussen"
|
|
15
|
+
]
|
|
16
|
+
EMAIL = "sethrasmussen@gmail.com"
|
|
17
|
+
DESCRIPTION = "Inspect and transcode video and audio files."
|
|
18
|
+
|
|
19
|
+
NAME = "rvideo"
|
|
20
|
+
|
|
21
|
+
REV = `git log -n1 --pretty=oneline | cut -d' ' -f1`.strip
|
|
22
|
+
BRANCH = `git branch | grep '*' | cut -d' ' -f2`.strip
|
|
23
|
+
|
|
24
|
+
# This is not the version used for the gem.
|
|
25
|
+
# That is parsed from the CHANGELOG by Echoe.
|
|
26
|
+
VERS = "#{RVideo::VERSION::STRING} (#{BRANCH} @ #{REV})"
|
|
27
|
+
|
|
28
|
+
Echoe.new NAME do |p|
|
|
29
|
+
p.author = AUTHOR
|
|
30
|
+
p.description = DESCRIPTION
|
|
31
|
+
p.email = EMAIL
|
|
32
|
+
p.summary = DESCRIPTION
|
|
33
|
+
p.url = "http://github.com/greatseth/rvideo"
|
|
34
|
+
|
|
35
|
+
p.runtime_dependencies = ["activesupport"]
|
|
36
|
+
p.development_dependencies = ["rspec"]
|
|
37
|
+
|
|
38
|
+
p.ignore_pattern = [
|
|
39
|
+
"spec/files/boat.mpg",
|
|
40
|
+
"spec/files/dinner.3g2",
|
|
41
|
+
"spec/files/foo $ bar & baz",
|
|
42
|
+
"spec/files/hats.3gp",
|
|
43
|
+
"spec/files/quads.wmv",
|
|
44
|
+
"spec/files/sword.3gp",
|
|
45
|
+
"website/**/*",
|
|
46
|
+
"tmp/**/*",
|
|
47
|
+
"scripts/test_progress.rb"
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
p.rdoc_options = [
|
|
51
|
+
"--quiet",
|
|
52
|
+
"--title", "rvideo documentation",
|
|
53
|
+
"--opname", "index.html",
|
|
54
|
+
"--line-numbers",
|
|
55
|
+
"--main", "README",
|
|
56
|
+
"--inline-source"
|
|
57
|
+
]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Load supporting Rake files
|
|
61
|
+
Dir[File.join(__HERE__, "tasks", "*.rake")].each { |t| load t }
|
|
62
|
+
|
|
63
|
+
puts "#{NAME} #{VERS}"
|
data/config/boot.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# I don't know what this file was intended for originally, but I've been
|
|
2
|
+
# using it to setup RVideo to play with in an IRB session. - Seth
|
|
3
|
+
|
|
4
|
+
$LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
|
5
|
+
require "lib/rvideo"
|
|
6
|
+
require "spec/support"
|
|
7
|
+
|
|
8
|
+
include RVideo
|
|
9
|
+
|
|
10
|
+
###
|
|
11
|
+
|
|
12
|
+
class Inspector
|
|
13
|
+
public :video_match
|
|
14
|
+
public :audio_match
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inspector(filename)
|
|
18
|
+
options = if filename.is_a? Symbol
|
|
19
|
+
{ :raw_response => files(filename) }
|
|
20
|
+
else
|
|
21
|
+
{ :file => spec_file(filename) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Inspector.new options
|
|
25
|
+
end
|
data/lib/rvideo.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
|
2
|
+
|
|
3
|
+
# core extensions
|
|
4
|
+
require 'rvideo/float'
|
|
5
|
+
require 'rvideo/string'
|
|
6
|
+
|
|
7
|
+
# gems
|
|
8
|
+
require 'rubygems'
|
|
9
|
+
require 'active_support'
|
|
10
|
+
|
|
11
|
+
# rvideo
|
|
12
|
+
require 'rvideo/inspector'
|
|
13
|
+
require 'rvideo/frame_capturer'
|
|
14
|
+
require 'rvideo/errors'
|
|
15
|
+
require 'rvideo/transcoder'
|
|
16
|
+
require 'rvideo/tools/abstract_tool'
|
|
17
|
+
require 'rvideo/tools/ffmpeg'
|
|
18
|
+
require 'rvideo/tools/mencoder'
|
|
19
|
+
require 'rvideo/tools/flvtool2'
|
|
20
|
+
require 'rvideo/tools/mp4box'
|
|
21
|
+
require 'rvideo/tools/mplayer'
|
|
22
|
+
require 'rvideo/tools/mp4creator'
|
|
23
|
+
require 'rvideo/tools/ffmpeg2theora'
|
|
24
|
+
require 'rvideo/tools/yamdi'
|
|
25
|
+
require 'rvideo/tools/qtfaststart'
|
|
26
|
+
|
|
27
|
+
TEMP_PATH = File.expand_path(File.dirname(__FILE__) + '/../tmp')
|
|
28
|
+
REPORT_PATH = File.expand_path(File.dirname(__FILE__) + '/../report')
|
|
29
|
+
|
|
30
|
+
module RVideo
|
|
31
|
+
# Configure logging. Assumes that the logger object has an
|
|
32
|
+
# interface similar to stdlib's Logger class.
|
|
33
|
+
#
|
|
34
|
+
# RVideo.logger = Logger.new(STDOUT)
|
|
35
|
+
#
|
|
36
|
+
def self.logger=(logger)
|
|
37
|
+
@logger = logger
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.logger
|
|
41
|
+
@logger = Logger.new("/dev/null") unless @logger
|
|
42
|
+
@logger
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -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
|
data/lib/rvideo/float.rb
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module RVideo
|
|
2
|
+
# FrameCapturer uses ffmpeg to capture frames from a movie in JPEG format.
|
|
3
|
+
#
|
|
4
|
+
# You can capture one or many frames in a variety of ways:
|
|
5
|
+
#
|
|
6
|
+
# - one frame at a given offset
|
|
7
|
+
# - multiple frames every n seconds from a given offset
|
|
8
|
+
#
|
|
9
|
+
# TODO
|
|
10
|
+
#
|
|
11
|
+
# - n frames total, evenly distributed across the duration of the movie
|
|
12
|
+
#
|
|
13
|
+
# For the offset options, three types of values are accepted:
|
|
14
|
+
# - percentage e.g. '37%'
|
|
15
|
+
# - seconds e.g. '37s' or simply '37'
|
|
16
|
+
# - frame e.g. '37f'
|
|
17
|
+
#
|
|
18
|
+
# If a time is outside of the duration of the file, it will choose a frame at the
|
|
19
|
+
# 99% mark.
|
|
20
|
+
#
|
|
21
|
+
# Example:
|
|
22
|
+
#
|
|
23
|
+
# RVideo::FrameCapturer.capture! :input => 'path/to/input.mp4', :offset => '10%'
|
|
24
|
+
# # => ['/path/to/screenshot/input-10p.jpg']
|
|
25
|
+
#
|
|
26
|
+
# In the case where you specify an :interval, e.g. :interval => 5 for a frame every
|
|
27
|
+
# 5 seconds, you will generally get a few more images that you might expect.
|
|
28
|
+
# Typically there will be at least two extra, one for the very start and one for
|
|
29
|
+
# the very end of the video. Then, depending on how close to a simple integer of
|
|
30
|
+
# seconds the duration of the video is, you may get one or two more.
|
|
31
|
+
#
|
|
32
|
+
# # Assuming input.mp4 is 19.6 seconds long..
|
|
33
|
+
# RVideo::FrameCapturer.capture! :input => 'path/to/input.mp4', :interval => 5
|
|
34
|
+
# # => ['/path/to/input-1.jpg','/path/to/input-2.jpg','/path/to/input-3.jpg',
|
|
35
|
+
# '/path/to/input-4.jpg','/path/to/input-5.jpg','/path/to/input-6.jpg']
|
|
36
|
+
#
|
|
37
|
+
# For more precision, you can try multiple capture commands, each getting
|
|
38
|
+
# a single frame but with increasing offsets.
|
|
39
|
+
class FrameCapturer
|
|
40
|
+
attr_reader :input, :output, :offset, :rate, :limit, :inspector, :command
|
|
41
|
+
|
|
42
|
+
def self.capture!(options)
|
|
43
|
+
new(options).capture!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(options)
|
|
47
|
+
@input = options[:input] || raise(ArgumentError, "need :input => /path/to/movie")
|
|
48
|
+
|
|
49
|
+
@inspector = Inspector.new :file => @input
|
|
50
|
+
|
|
51
|
+
@offset, @rate, @limit, @output = parse_options options
|
|
52
|
+
|
|
53
|
+
@command = "ffmpeg -i #{@input.shell_quoted} -ss #{@offset} -r #{@rate} #{@output.shell_quoted} -vframes #{@limit}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def capture!
|
|
57
|
+
RVideo.logger.info("\nCreating Screenshot: #{@command}\n")
|
|
58
|
+
frame_result = `#{@command} 2>&1`
|
|
59
|
+
RVideo.logger.info("\nScreenshot results: #{frame_result}")
|
|
60
|
+
|
|
61
|
+
Dir[File.expand_path(@output).sub("%d", "*")].entries
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
VALID_TIMECODE_FORMAT = /\A([0-9.,]*)(s|f|%)?\Z/
|
|
65
|
+
|
|
66
|
+
# TODO This method should not be public, but I'm too lazy to update the specs right now..
|
|
67
|
+
def calculate_time(timecode)
|
|
68
|
+
m = VALID_TIMECODE_FORMAT.match(timecode.to_s)
|
|
69
|
+
if m.nil? or m[1].nil? or m[1].empty?
|
|
70
|
+
raise TranscoderError::ParameterError,
|
|
71
|
+
"Invalid timecode for frame capture: #{timecode}. " <<
|
|
72
|
+
"Must be a number, optionally followed by s, f, or %."
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
case m[2]
|
|
76
|
+
when "s", nil
|
|
77
|
+
t = m[1].to_f
|
|
78
|
+
when "f"
|
|
79
|
+
t = m[1].to_f / @inspector.fps.to_f
|
|
80
|
+
when "%"
|
|
81
|
+
# milliseconds / 1000 * percent / 100
|
|
82
|
+
t = (@inspector.duration.to_i / 1000.0) * (m[1].to_f / 100.0)
|
|
83
|
+
else
|
|
84
|
+
raise TranscoderError::ParameterError,
|
|
85
|
+
"Invalid timecode for frame capture: #{timecode}. " <<
|
|
86
|
+
"Must be a number, optionally followed by s, f, or p."
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if (t * 1000) > @inspector.duration
|
|
90
|
+
calculate_time("99%")
|
|
91
|
+
else
|
|
92
|
+
t
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
def parse_options(options)
|
|
98
|
+
offset = options[:offset] ? calculate_time(options[:offset]) : 0
|
|
99
|
+
rate = options[:interval] ? (1 / options[:interval].to_f) : 1
|
|
100
|
+
|
|
101
|
+
limit = if options[:limit]
|
|
102
|
+
options[:limit]
|
|
103
|
+
elsif not options[:interval]
|
|
104
|
+
1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
output = if options[:output]
|
|
108
|
+
options[:output]
|
|
109
|
+
else
|
|
110
|
+
path = File.dirname File.expand_path(options[:input])
|
|
111
|
+
|
|
112
|
+
name = File.basename(options[:input], ".*")
|
|
113
|
+
if options[:interval]
|
|
114
|
+
name << "-%d"
|
|
115
|
+
else
|
|
116
|
+
name << "-#{offset}"
|
|
117
|
+
end
|
|
118
|
+
name << ".jpg"
|
|
119
|
+
|
|
120
|
+
File.join path, name
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
[offset, rate, limit, output]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
module RVideo # :nodoc:
|
|
2
|
+
# To inspect a video or audio file, initialize an Inspector object.
|
|
3
|
+
#
|
|
4
|
+
# file = RVideo::Inspector.new(options_hash)
|
|
5
|
+
#
|
|
6
|
+
# Inspector accepts three options: file, raw_response, and ffmpeg_binary.
|
|
7
|
+
# Either raw_response or file is required; ffmpeg binary is optional.
|
|
8
|
+
#
|
|
9
|
+
# :file is a path to a file to be inspected.
|
|
10
|
+
#
|
|
11
|
+
# :raw_response is the full output of "ffmpeg -i [file]". If the
|
|
12
|
+
# :raw_response option is used, RVideo will not actually inspect a file;
|
|
13
|
+
# it will simply parse the provided response. This is useful if your
|
|
14
|
+
# application has already collected the ffmpeg -i response, and you don't
|
|
15
|
+
# want to call it again.
|
|
16
|
+
#
|
|
17
|
+
# :ffmpeg_binary is an optional argument that specifies the path to the
|
|
18
|
+
# ffmpeg binary to be used. If a path is not explicitly declared, RVideo
|
|
19
|
+
# will assume that ffmpeg exists in the Unix path. Type "which ffmpeg" to
|
|
20
|
+
# check if ffmpeg is installed and exists in your operating system's path.
|
|
21
|
+
class Inspector
|
|
22
|
+
attr_reader :filename, :path, :full_filename, :raw_response, :raw_metadata
|
|
23
|
+
|
|
24
|
+
attr_accessor :ffmpeg_binary
|
|
25
|
+
|
|
26
|
+
def initialize(options = {})
|
|
27
|
+
if not (options[:raw_response] or options[:file])
|
|
28
|
+
raise ArgumentError, "Must supply either an input file or a pregenerated response"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if options[:raw_response]
|
|
32
|
+
initialize_with_raw_response(options[:raw_response])
|
|
33
|
+
elsif options[:file]
|
|
34
|
+
initialize_with_file(options[:file], options[:ffmpeg_binary])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
metadata = /(Input \#.*)\n.+\n\Z/m.match(@raw_response)
|
|
38
|
+
|
|
39
|
+
if /Unknown format/i.match(@raw_response) || metadata.nil?
|
|
40
|
+
@unknown_format = true
|
|
41
|
+
elsif /Duration: N\/A/im.match(@raw_response)
|
|
42
|
+
# in this case, we can at least still get the container type
|
|
43
|
+
@unreadable_file = true
|
|
44
|
+
@raw_metadata = metadata[1]
|
|
45
|
+
else
|
|
46
|
+
@raw_metadata = metadata[1]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initialize_with_raw_response(raw_response)
|
|
51
|
+
@raw_response = raw_response
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize_with_file(file, ffmpeg_binary = nil)
|
|
55
|
+
if ffmpeg_binary
|
|
56
|
+
@ffmpeg_binary = ffmpeg_binary
|
|
57
|
+
if not FileTest.exist?(@ffmpeg_binary)
|
|
58
|
+
raise "ffmpeg could not be found (trying #{@ffmpeg_binary})"
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
# assume it is in the unix path
|
|
62
|
+
if not FileTest.exist?(`which ffmpeg`.chomp)
|
|
63
|
+
raise "ffmpeg could not be found (expected ffmpeg to be found in the Unix path)"
|
|
64
|
+
end
|
|
65
|
+
@ffmpeg_binary = "ffmpeg"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if not FileTest.exist?(file.gsub('"',''))
|
|
69
|
+
raise TranscoderError::InputFileNotFound, "File not found (#{file})"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@full_filename = file
|
|
73
|
+
@filename = File.basename(@full_filename)
|
|
74
|
+
@path = File.dirname(@full_filename)
|
|
75
|
+
|
|
76
|
+
@raw_response = `#{@ffmpeg_binary} -i #{@full_filename.shell_quoted} 2>&1`
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns true if the file can be read successfully. Returns false otherwise.
|
|
80
|
+
def valid?
|
|
81
|
+
not (@unknown_format or @unreadable_file)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns false if the file can be read successfully. Returns false otherwise.
|
|
85
|
+
def invalid?
|
|
86
|
+
not valid?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# True if the format is not understood ("Unknown Format")
|
|
90
|
+
def unknown_format?
|
|
91
|
+
@unknown_format ? true : false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# True if the file is not readable ("Duration: N/A, bitrate: N/A")
|
|
95
|
+
def unreadable_file?
|
|
96
|
+
@unreadable_file ? true : false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Does the file have an audio stream?
|
|
100
|
+
def audio?
|
|
101
|
+
not audio_match.nil?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Does the file have a video stream?
|
|
105
|
+
def video?
|
|
106
|
+
not video_match.nil?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns the version of ffmpeg used, In practice, this may or may not be
|
|
110
|
+
# useful.
|
|
111
|
+
#
|
|
112
|
+
# Examples:
|
|
113
|
+
#
|
|
114
|
+
# SVN-r6399
|
|
115
|
+
# CVS
|
|
116
|
+
#
|
|
117
|
+
def ffmpeg_version
|
|
118
|
+
@ffmpeg_version = @raw_response.split("\n").first.split("version").last.split(",").first.strip
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns the configuration options used to build ffmpeg.
|
|
122
|
+
#
|
|
123
|
+
# Example:
|
|
124
|
+
#
|
|
125
|
+
# --enable-mp3lame --enable-gpl --disable-ffplay --disable-ffserver
|
|
126
|
+
# --enable-a52 --enable-xvid
|
|
127
|
+
#
|
|
128
|
+
def ffmpeg_configuration
|
|
129
|
+
/(\s*configuration:)(.*)\n/.match(@raw_response)[2].strip
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns the versions of libavutil, libavcodec, and libavformat used by
|
|
133
|
+
# ffmpeg.
|
|
134
|
+
#
|
|
135
|
+
# Example:
|
|
136
|
+
#
|
|
137
|
+
# libavutil version: 49.0.0
|
|
138
|
+
# libavcodec version: 51.9.0
|
|
139
|
+
# libavformat version: 50.4.0
|
|
140
|
+
#
|
|
141
|
+
def ffmpeg_libav
|
|
142
|
+
/^(\s*lib.*\n)+/.match(@raw_response)[0].split("\n").each {|l| l.strip! }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Returns the build description for ffmpeg.
|
|
146
|
+
#
|
|
147
|
+
# Example:
|
|
148
|
+
#
|
|
149
|
+
# built on Apr 15 2006 04:58:19, gcc: 4.0.1 (Apple Computer, Inc. build
|
|
150
|
+
# 5250)
|
|
151
|
+
#
|
|
152
|
+
def ffmpeg_build
|
|
153
|
+
/(\n\s*)(built on.*)(\n)/.match(@raw_response)[2]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns the container format for the file. Instead of returning a single
|
|
157
|
+
# format, this may return a string of related formats.
|
|
158
|
+
#
|
|
159
|
+
# Examples:
|
|
160
|
+
#
|
|
161
|
+
# "avi"
|
|
162
|
+
#
|
|
163
|
+
# "mov,mp4,m4a,3gp,3g2,mj2"
|
|
164
|
+
#
|
|
165
|
+
def container
|
|
166
|
+
return nil if @unknown_format
|
|
167
|
+
/Input \#\d+\,\s*(\S+),\s*from/.match(@raw_metadata)[1]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The duration of the movie, as a string.
|
|
171
|
+
#
|
|
172
|
+
# Example:
|
|
173
|
+
#
|
|
174
|
+
# "00:00:24.4" # 24.4 seconds
|
|
175
|
+
#
|
|
176
|
+
def raw_duration
|
|
177
|
+
return nil unless valid?
|
|
178
|
+
/Duration:\s*([0-9\:\.]+),/.match(@raw_metadata)[1]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The duration of the movie in milliseconds, as an integer.
|
|
182
|
+
#
|
|
183
|
+
# Example:
|
|
184
|
+
#
|
|
185
|
+
# 24400 # 24.4 seconds
|
|
186
|
+
#
|
|
187
|
+
# Note that the precision of the duration is in tenths of a second, not
|
|
188
|
+
# thousandths, but milliseconds are a more standard unit of time than
|
|
189
|
+
# deciseconds.
|
|
190
|
+
#
|
|
191
|
+
def duration
|
|
192
|
+
return nil unless valid?
|
|
193
|
+
|
|
194
|
+
units = raw_duration.split(":")
|
|
195
|
+
(units[0].to_i * 60 * 60 * 1000) + (units[1].to_i * 60 * 1000) + (units[2].to_f * 1000).to_i
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# The bitrate of the movie.
|
|
199
|
+
#
|
|
200
|
+
# Example:
|
|
201
|
+
#
|
|
202
|
+
# 3132
|
|
203
|
+
#
|
|
204
|
+
def bitrate
|
|
205
|
+
return nil unless valid?
|
|
206
|
+
bitrate_match[1].to_i
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The bitrate units used. In practice, this may always be kb/s.
|
|
210
|
+
#
|
|
211
|
+
# Example:
|
|
212
|
+
#
|
|
213
|
+
# "kb/s"
|
|
214
|
+
#
|
|
215
|
+
def bitrate_units
|
|
216
|
+
return nil unless valid?
|
|
217
|
+
bitrate_match[2]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def bitrate_with_units
|
|
221
|
+
"#{bitrate} #{bitrate_units}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def audio_bit_rate
|
|
225
|
+
return nil unless audio?
|
|
226
|
+
audio_match[7].to_i
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def audio_bit_rate_units
|
|
230
|
+
return nil unless audio?
|
|
231
|
+
audio_match[8]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def audio_bit_rate_with_units
|
|
235
|
+
"#{audio_bit_rate} #{audio_bit_rate_units}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def audio_stream
|
|
239
|
+
return nil unless valid?
|
|
240
|
+
|
|
241
|
+
match = /\n\s*Stream.*Audio:.*\n/.match(@raw_response)
|
|
242
|
+
match[0].strip if match
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# The audio codec used.
|
|
246
|
+
#
|
|
247
|
+
# Example:
|
|
248
|
+
#
|
|
249
|
+
# "aac"
|
|
250
|
+
#
|
|
251
|
+
def audio_codec
|
|
252
|
+
return nil unless audio?
|
|
253
|
+
audio_match[2]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The sampling rate of the audio stream.
|
|
257
|
+
#
|
|
258
|
+
# Example:
|
|
259
|
+
#
|
|
260
|
+
# 44100
|
|
261
|
+
#
|
|
262
|
+
def audio_sample_rate
|
|
263
|
+
return nil unless audio?
|
|
264
|
+
audio_match[3].to_i
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# The units used for the sampling rate. May always be Hz.
|
|
268
|
+
#
|
|
269
|
+
# Example:
|
|
270
|
+
#
|
|
271
|
+
# "Hz"
|
|
272
|
+
#
|
|
273
|
+
def audio_sample_rate_units
|
|
274
|
+
return nil unless audio?
|
|
275
|
+
audio_match[4]
|
|
276
|
+
end
|
|
277
|
+
alias_method :audio_sample_units, :audio_sample_rate_units
|
|
278
|
+
|
|
279
|
+
def audio_sample_rate_with_units
|
|
280
|
+
"#{audio_sample_rate} #{audio_sample_rate_units}"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# The channels used in the audio stream.
|
|
284
|
+
#
|
|
285
|
+
# Examples:
|
|
286
|
+
# "stereo"
|
|
287
|
+
# "mono"
|
|
288
|
+
# "5:1"
|
|
289
|
+
#
|
|
290
|
+
def audio_channels_string
|
|
291
|
+
return nil unless audio?
|
|
292
|
+
audio_match[5]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def audio_channels
|
|
296
|
+
return nil unless audio?
|
|
297
|
+
|
|
298
|
+
case audio_match[5]
|
|
299
|
+
when "mono" then 1
|
|
300
|
+
when "stereo" then 2
|
|
301
|
+
else
|
|
302
|
+
raise RuntimeError, "Unknown number of channels: #{audio_channels}"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# This should almost always return 16,
|
|
307
|
+
# as the vast majority of audio is 16 bit.
|
|
308
|
+
def audio_sample_bit_depth
|
|
309
|
+
return nil unless audio?
|
|
310
|
+
audio_match[6].to_i
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The ID of the audio stream (useful for troubleshooting).
|
|
314
|
+
#
|
|
315
|
+
# Example:
|
|
316
|
+
# #0.1
|
|
317
|
+
#
|
|
318
|
+
def audio_stream_id
|
|
319
|
+
return nil unless audio?
|
|
320
|
+
audio_match[1]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def video_stream
|
|
324
|
+
return nil unless valid?
|
|
325
|
+
|
|
326
|
+
match = /\n\s*Stream.*Video:.*\n/.match(@raw_response)
|
|
327
|
+
match[0].strip unless match.nil?
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# The ID of the video stream (useful for troubleshooting).
|
|
331
|
+
#
|
|
332
|
+
# Example:
|
|
333
|
+
# #0.0
|
|
334
|
+
#
|
|
335
|
+
def video_stream_id
|
|
336
|
+
return nil unless video?
|
|
337
|
+
video_match[1]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# The video codec used.
|
|
341
|
+
#
|
|
342
|
+
# Example:
|
|
343
|
+
#
|
|
344
|
+
# "mpeg4"
|
|
345
|
+
#
|
|
346
|
+
def video_codec
|
|
347
|
+
return nil unless video?
|
|
348
|
+
video_match[3]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# The colorspace of the video stream.
|
|
352
|
+
#
|
|
353
|
+
# Example:
|
|
354
|
+
#
|
|
355
|
+
# "yuv420p"
|
|
356
|
+
#
|
|
357
|
+
def video_colorspace
|
|
358
|
+
return nil unless video?
|
|
359
|
+
video_match[4]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# The width of the video in pixels.
|
|
363
|
+
def width
|
|
364
|
+
return nil unless video?
|
|
365
|
+
video_match[5].to_i
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# The height of the video in pixels.
|
|
369
|
+
def height
|
|
370
|
+
return nil unless video?
|
|
371
|
+
video_match[6].to_i
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# width x height, as a string.
|
|
375
|
+
#
|
|
376
|
+
# Examples:
|
|
377
|
+
# 320x240
|
|
378
|
+
# 1280x720
|
|
379
|
+
#
|
|
380
|
+
def resolution
|
|
381
|
+
return nil unless video?
|
|
382
|
+
"#{width}x#{height}"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def pixel_aspect_ratio
|
|
386
|
+
return nil unless video?
|
|
387
|
+
video_match[7]
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def display_aspect_ratio
|
|
391
|
+
return nil unless video?
|
|
392
|
+
video_match[8]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# The portion of the overall bitrate the video is responsible for.
|
|
396
|
+
def video_bit_rate
|
|
397
|
+
return nil unless video?
|
|
398
|
+
video_match[9]
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def video_bit_rate_units
|
|
402
|
+
return nil unless video?
|
|
403
|
+
video_match[10]
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# The frame rate of the video in frames per second
|
|
407
|
+
#
|
|
408
|
+
# Example:
|
|
409
|
+
#
|
|
410
|
+
# "29.97"
|
|
411
|
+
#
|
|
412
|
+
def fps
|
|
413
|
+
return nil unless video?
|
|
414
|
+
video_match[2] or video_match[11]
|
|
415
|
+
end
|
|
416
|
+
alias_method :framerate, :fps
|
|
417
|
+
|
|
418
|
+
def time_base
|
|
419
|
+
return nil unless video?
|
|
420
|
+
video_match[12]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def codec_time_base
|
|
424
|
+
return nil unless video?
|
|
425
|
+
video_match[13]
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
private
|
|
429
|
+
|
|
430
|
+
def bitrate_match
|
|
431
|
+
/bitrate: ([0-9\.]+)\s*(.*)\s+/.match(@raw_metadata)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
###
|
|
435
|
+
# I am wondering how reliable it would be to simplify a lot
|
|
436
|
+
# of this regexp parsery by using split(/\s*,\s*/) - Seth
|
|
437
|
+
|
|
438
|
+
SEP = '(?:,\s*)'
|
|
439
|
+
VAL = '([^,]+)'
|
|
440
|
+
|
|
441
|
+
RATE = '([\d.]+k?)'
|
|
442
|
+
|
|
443
|
+
AUDIO_MATCH_PATTERN = /
|
|
444
|
+
Stream\s+(.*?)[,:\(\[].*?\s*
|
|
445
|
+
Audio:\s+
|
|
446
|
+
#{VAL}#{SEP} # codec
|
|
447
|
+
#{RATE}\s+(\w*)#{SEP}? # sample rate
|
|
448
|
+
([a-zA-Z:]*)#{SEP}? # channels
|
|
449
|
+
(?:s(\d+)#{SEP}?)? # audio sample bit depth
|
|
450
|
+
(?:(\d+)\s+(\S+))? # audio bit rate
|
|
451
|
+
/x
|
|
452
|
+
|
|
453
|
+
def audio_match
|
|
454
|
+
return nil unless valid?
|
|
455
|
+
AUDIO_MATCH_PATTERN.match(audio_stream)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
FPS = 'fps(?:\(r\))?'
|
|
459
|
+
|
|
460
|
+
VIDEO_MATCH_PATTERN = /
|
|
461
|
+
Stream\s*(\#[\d.]+)(?:[\(\[].+?[\)\]])?\s* # stream id
|
|
462
|
+
[,:]\s*
|
|
463
|
+
(?:#{RATE}\s*#{FPS}[,:]\s*)? # frame rate, older builds
|
|
464
|
+
Video:\s*
|
|
465
|
+
#{VAL}#{SEP} # codec
|
|
466
|
+
(?:#{VAL}#{SEP})? # color space
|
|
467
|
+
(\d+)x(\d+) # resolution
|
|
468
|
+
(?:\s*\[?(?:PAR\s*(\d+:\d+))?\s*(?:DAR\s*(\d+:\d+))?\]?)? # pixel and display aspect ratios
|
|
469
|
+
#{SEP}?
|
|
470
|
+
(?:#{RATE}\s*(kb\/s)#{SEP}?)? # video bit rate
|
|
471
|
+
(?:#{RATE}\s*(?:tb\(?r\)?|#{FPS})#{SEP}?)? # frame rate
|
|
472
|
+
(?:#{RATE}\s*tbn#{SEP}?)? # time base
|
|
473
|
+
(?:#{RATE}\s*tbc#{SEP}?)? # codec time base
|
|
474
|
+
/x
|
|
475
|
+
|
|
476
|
+
def video_match
|
|
477
|
+
return nil unless valid?
|
|
478
|
+
VIDEO_MATCH_PATTERN.match(video_stream)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|