streamio-ffmpeg 0.9.0 → 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG +31 -4
- data/README.md +43 -16
- data/lib/ffmpeg/encoding_options.rb +38 -30
- data/lib/ffmpeg/io_monkey.rb +30 -47
- data/lib/ffmpeg/movie.rb +31 -26
- data/lib/ffmpeg/transcoder.rb +49 -43
- data/lib/ffmpeg/version.rb +1 -1
- data/lib/streamio-ffmpeg.rb +2 -2
- metadata +14 -27
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b2f7f026d79edbb50b3cc2a727970b24ddc14e35
|
4
|
+
data.tar.gz: 1eb3016af722ac698e9ffa27515b515f7683bf7a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bd2bde719ec750a394f4a9143d117355e615d7302d9da8c02d58300b59fed78c7e7e85c2d8b4821221cc21fc402c6bc94982596db37df85f121915aa3b889265
|
7
|
+
data.tar.gz: 448a6304a0c99eb13a1540bf8a99f45db9d8a08f12d7430a08a6f4b7a6a0a12746f9b1211587ff226a5c37184d81438ab3fd18780a393ba7439c9e30a44a472b
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,30 @@
|
|
1
|
+
== 1.0.0 2013-07-08
|
2
|
+
|
3
|
+
New:
|
4
|
+
* Bumped target ffmpeg version to 1.2.1
|
5
|
+
|
6
|
+
Improvements:
|
7
|
+
* Simpler implementation for timeouts.
|
8
|
+
Should be far less cpu and memory dependent (don't spawn a thread for every line of output)
|
9
|
+
Timeout spec now passes in Rubinius (using 1.9 mode)
|
10
|
+
* Give helpful error message for windows users lacking the win32-process gem (thanks casoetan)
|
11
|
+
* Add Movie#container (thanks vitalis)
|
12
|
+
* Support vprofile and preset encoding options (thanks vitalis)
|
13
|
+
|
14
|
+
Changes:
|
15
|
+
* Default timeout lowered to 30 seconds
|
16
|
+
|
17
|
+
Bugs:
|
18
|
+
* Avoid crash if asking for frame_rate of a video without video stream (thanks squidarth)
|
19
|
+
* Fix crash when doing audio transcoding on ffmpeg >= 1.0.1 (thanks vitalis)
|
20
|
+
|
21
|
+
Deprecations:
|
22
|
+
* Removed support for Ruby 1.8
|
23
|
+
* Removed support for ffmpeg 0.7
|
24
|
+
|
25
|
+
Refactorings:
|
26
|
+
* Quite a few, see commit history for details.
|
27
|
+
|
1
28
|
== 0.9.0 2012-07-24
|
2
29
|
|
3
30
|
New:
|
@@ -76,7 +103,7 @@ Refactorings:
|
|
76
103
|
|
77
104
|
== 0.7.4 2010-12-07
|
78
105
|
|
79
|
-
* Fixed broken duration on movies with start times over 0 by reducing duration with start-time
|
106
|
+
* Fixed broken duration on movies with start times over 0 by reducing duration with start-time
|
80
107
|
|
81
108
|
== 0.7.3 2010-08-26
|
82
109
|
|
@@ -110,7 +137,7 @@ Refactorings:
|
|
110
137
|
|
111
138
|
== 0.6.7 2010-06-10
|
112
139
|
|
113
|
-
* Bugfix - aspect ratio preserver could suggest non even resolutions in certain circumstances
|
140
|
+
* Bugfix - aspect ratio preserver could suggest non even resolutions in certain circumstances
|
114
141
|
|
115
142
|
== 0.6.6 2010-06-10
|
116
143
|
|
@@ -143,7 +170,7 @@ Refactorings:
|
|
143
170
|
|
144
171
|
== 0.5.0 2010-04-28
|
145
172
|
|
146
|
-
* Added logging capabilities
|
173
|
+
* Added logging capabilities
|
147
174
|
|
148
175
|
== 0.4.3 2010-04-06
|
149
176
|
|
@@ -172,4 +199,4 @@ Refactorings:
|
|
172
199
|
|
173
200
|
== 0.1.0 2010-02-05
|
174
201
|
|
175
|
-
* Some basic parsing of metadata added
|
202
|
+
* Some basic parsing of metadata added
|
data/README.md
CHANGED
@@ -12,7 +12,18 @@ Installation
|
|
12
12
|
|
13
13
|
(sudo) gem install streamio-ffmpeg
|
14
14
|
|
15
|
-
|
15
|
+
Compatibility
|
16
|
+
-------------
|
17
|
+
|
18
|
+
### Ruby
|
19
|
+
|
20
|
+
Only guaranteed to work with MRI Ruby 1.9.3 or later.
|
21
|
+
Should work with rubinius head in 1.9 mode.
|
22
|
+
Will not work in jruby until they fix: http://goo.gl/Z4UcX (should work in the upcoming 1.7.5)
|
23
|
+
|
24
|
+
### ffmpeg
|
25
|
+
|
26
|
+
The current gem is tested against ffmpeg 1.2.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
27
|
|
17
28
|
Usage
|
18
29
|
-----
|
@@ -72,11 +83,12 @@ movie.transcode("movie.mp4", "-ac aac -vc libx264 -ac 2 ...")
|
|
72
83
|
Use the EncodingOptions parser for humanly readable transcoding options. Below you'll find most of the supported options. Note that the :custom key will be used as is without modification so use it for any tricky business you might need.
|
73
84
|
|
74
85
|
``` ruby
|
75
|
-
options = {:
|
76
|
-
:
|
77
|
-
:
|
78
|
-
:
|
79
|
-
:
|
86
|
+
options = {video_codec: "libx264", frame_rate: 10, resolution: "320x240", video_bitrate: 300, video_bitrate_tolerance: 100,
|
87
|
+
aspect: 1.333333, keyframe_interval: 90,
|
88
|
+
x264_profile: "high", x264_preset: "slow",
|
89
|
+
audio_codec: "libfaac", audio_bitrate: 32, audio_sample_rate: 22050, audio_channels: 1,
|
90
|
+
threads: 2,
|
91
|
+
custom: "-vf crop=60:60:10:10"}
|
80
92
|
movie.transcode("movie.mp4", options)
|
81
93
|
```
|
82
94
|
|
@@ -92,7 +104,7 @@ transcoded_movie.audio_codec # "mp3"
|
|
92
104
|
Aspect ratio is added to encoding options automatically if none is specified.
|
93
105
|
|
94
106
|
``` ruby
|
95
|
-
options = {:
|
107
|
+
options = { resolution: "320x180" } # Will add -aspect 1.77777777777778 to ffmpeg
|
96
108
|
```
|
97
109
|
|
98
110
|
Preserve aspect ratio on width or height by using the preserve_aspect_ratio transcoder option.
|
@@ -100,19 +112,19 @@ Preserve aspect ratio on width or height by using the preserve_aspect_ratio tran
|
|
100
112
|
``` ruby
|
101
113
|
widescreen_movie = FFMPEG::Movie.new("path/to/widescreen_movie.mov")
|
102
114
|
|
103
|
-
options = {:
|
115
|
+
options = { resolution: "320x240" }
|
104
116
|
|
105
|
-
transcoder_options = {:
|
117
|
+
transcoder_options = { preserve_aspect_ratio: :width }
|
106
118
|
widescreen_movie.transcode("movie.mp4", options, transcoder_options) # Output resolution will be 320x180
|
107
119
|
|
108
|
-
transcoder_options = {:
|
120
|
+
transcoder_options = { preserve_aspect_ratio: :height }
|
109
121
|
widescreen_movie.transcode("movie.mp4", options, transcoder_options) # Output resolution will be 426x240
|
110
122
|
```
|
111
123
|
|
112
124
|
For constant bitrate encoding use video_min_bitrate and video_max_bitrate with buffer_size.
|
113
125
|
|
114
126
|
``` ruby
|
115
|
-
options = {:
|
127
|
+
options = {video_min_bitrate: 600, video_max_bitrate: 600, buffer_size: 2000}
|
116
128
|
movie.transcode("movie.flv", options)
|
117
129
|
```
|
118
130
|
|
@@ -127,13 +139,13 @@ movie.screenshot("screenshot.jpg")
|
|
127
139
|
The screenshot method has the very same API as transcode so the same options will work.
|
128
140
|
|
129
141
|
``` ruby
|
130
|
-
movie.screenshot("screenshot.bmp", :
|
142
|
+
movie.screenshot("screenshot.bmp", seek_time: 5, resolution: '320x240')
|
131
143
|
```
|
132
144
|
|
133
145
|
You can preserve aspect ratio the same way as when using transcode.
|
134
146
|
|
135
147
|
``` ruby
|
136
|
-
movie.screenshot("screenshot.png", {:
|
148
|
+
movie.screenshot("screenshot.png", { seek_time: 2, resolution: '200x120' }, preserve_aspect_ratio: :width)
|
137
149
|
```
|
138
150
|
|
139
151
|
Specify the path to ffmpeg
|
@@ -151,19 +163,34 @@ This will cause the same command to run as "/usr/local/bin/ffmpeg -i /path/to/in
|
|
151
163
|
Automatically kill hung processes
|
152
164
|
---------------------------------
|
153
165
|
|
154
|
-
By default, streamio will wait for
|
166
|
+
By default, streamio will wait for 30 seconds between IO feedback from the FFMPEG process. After which an error is logged and the process killed.
|
155
167
|
It is possible to modify this behaviour by setting a new default:
|
156
168
|
|
157
169
|
``` ruby
|
158
170
|
# Change the timeout
|
159
|
-
Transcoder.timeout =
|
171
|
+
Transcoder.timeout = 10
|
160
172
|
|
161
173
|
# Disable the timeout altogether
|
162
174
|
Transcoder.timeout = false
|
163
175
|
```
|
164
176
|
|
177
|
+
Disabling output file validation
|
178
|
+
------------------------------
|
179
|
+
|
180
|
+
By default Transcoder validates the output file, in case you use FFMPEG for HLS
|
181
|
+
format that creates multiple outputs you can disable the validation by passing
|
182
|
+
`validate: false` to transcoder_options.
|
183
|
+
|
184
|
+
Note that transcode will not return the encoded movie object in this case since
|
185
|
+
attempting to open a (possibly) invalid output file might result in an error being raised.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
transcoder_options = { validate: false }
|
189
|
+
movie.transcode("movie.mp4", options, transcoder_options) # returns nil
|
190
|
+
```
|
191
|
+
|
165
192
|
|
166
193
|
Copyright
|
167
194
|
---------
|
168
195
|
|
169
|
-
Copyright (c)
|
196
|
+
Copyright (c) Streamio AB. See LICENSE for details.
|
@@ -3,119 +3,119 @@ module FFMPEG
|
|
3
3
|
def initialize(options = {})
|
4
4
|
merge!(options)
|
5
5
|
end
|
6
|
-
|
6
|
+
|
7
7
|
def to_s
|
8
8
|
params = collect do |key, value|
|
9
9
|
send("convert_#{key}", value) if value && supports_option?(key)
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
# codecs should go before the presets so that the files will be matched successfully
|
13
13
|
# all other parameters go after so that we can override whatever is in the preset
|
14
14
|
codecs = params.select { |p| p =~ /codec/ }
|
15
15
|
presets = params.select { |p| p =~ /\-.pre/ }
|
16
16
|
other = params - codecs - presets
|
17
17
|
params = codecs + presets + other
|
18
|
-
|
18
|
+
|
19
19
|
params_string = params.join(" ")
|
20
20
|
params_string << " #{convert_aspect(calculate_aspect)}" if calculate_aspect?
|
21
21
|
params_string
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def width
|
25
25
|
self[:resolution].split("x").first.to_i rescue nil
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def height
|
29
29
|
self[:resolution].split("x").last.to_i rescue nil
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
private
|
33
33
|
def supports_option?(option)
|
34
34
|
option = RUBY_VERSION < "1.9" ? "convert_#{option}" : "convert_#{option}".to_sym
|
35
35
|
private_methods.include?(option)
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def convert_aspect(value)
|
39
39
|
"-aspect #{value}"
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
def calculate_aspect
|
43
43
|
width, height = self[:resolution].split("x")
|
44
44
|
width.to_f / height.to_f
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
def calculate_aspect?
|
48
48
|
self[:aspect].nil? && self[:resolution]
|
49
49
|
end
|
50
|
-
|
50
|
+
|
51
51
|
def convert_video_codec(value)
|
52
52
|
"-vcodec #{value}"
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
def convert_frame_rate(value)
|
56
56
|
"-r #{value}"
|
57
57
|
end
|
58
|
-
|
58
|
+
|
59
59
|
def convert_resolution(value)
|
60
60
|
"-s #{value}"
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
def convert_video_bitrate(value)
|
64
64
|
"-b:v #{k_format(value)}"
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
def convert_audio_codec(value)
|
68
68
|
"-acodec #{value}"
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def convert_audio_bitrate(value)
|
72
72
|
"-b:a #{k_format(value)}"
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
def convert_audio_sample_rate(value)
|
76
76
|
"-ar #{value}"
|
77
77
|
end
|
78
|
-
|
78
|
+
|
79
79
|
def convert_audio_channels(value)
|
80
80
|
"-ac #{value}"
|
81
81
|
end
|
82
|
-
|
82
|
+
|
83
83
|
def convert_video_max_bitrate(value)
|
84
84
|
"-maxrate #{k_format(value)}"
|
85
85
|
end
|
86
|
-
|
86
|
+
|
87
87
|
def convert_video_min_bitrate(value)
|
88
88
|
"-minrate #{k_format(value)}"
|
89
89
|
end
|
90
|
-
|
90
|
+
|
91
91
|
def convert_buffer_size(value)
|
92
92
|
"-bufsize #{k_format(value)}"
|
93
93
|
end
|
94
|
-
|
94
|
+
|
95
95
|
def convert_video_bitrate_tolerance(value)
|
96
96
|
"-bt #{k_format(value)}"
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
def convert_threads(value)
|
100
100
|
"-threads #{value}"
|
101
101
|
end
|
102
|
-
|
102
|
+
|
103
103
|
def convert_duration(value)
|
104
104
|
"-t #{value}"
|
105
105
|
end
|
106
|
-
|
106
|
+
|
107
107
|
def convert_video_preset(value)
|
108
108
|
"-vpre #{value}"
|
109
109
|
end
|
110
|
-
|
110
|
+
|
111
111
|
def convert_audio_preset(value)
|
112
112
|
"-apre #{value}"
|
113
113
|
end
|
114
|
-
|
114
|
+
|
115
115
|
def convert_file_preset(value)
|
116
116
|
"-fpre #{value}"
|
117
117
|
end
|
118
|
-
|
118
|
+
|
119
119
|
def convert_keyframe_interval(value)
|
120
120
|
"-g #{value}"
|
121
121
|
end
|
@@ -123,15 +123,23 @@ module FFMPEG
|
|
123
123
|
def convert_seek_time(value)
|
124
124
|
"-ss #{value}"
|
125
125
|
end
|
126
|
-
|
126
|
+
|
127
127
|
def convert_screenshot(value)
|
128
128
|
value ? "-vframes 1 -f image2" : ""
|
129
129
|
end
|
130
|
-
|
130
|
+
|
131
|
+
def convert_x264_vprofile(value)
|
132
|
+
"-vprofile #{value}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def convert_x264_preset(value)
|
136
|
+
"-preset #{value}"
|
137
|
+
end
|
138
|
+
|
131
139
|
def convert_custom(value)
|
132
140
|
value
|
133
141
|
end
|
134
|
-
|
142
|
+
|
135
143
|
def k_format(value)
|
136
144
|
value.to_s.include?("k") ? value : "#{value}k"
|
137
145
|
end
|
data/lib/ffmpeg/io_monkey.rb
CHANGED
@@ -1,59 +1,42 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
else
|
11
|
-
require 'timeout'
|
12
|
-
FFMPEG::Timer = Timeout
|
1
|
+
require 'timeout'
|
2
|
+
require 'thread'
|
3
|
+
if RUBY_PLATFORM =~ /(win|w)(32|64)$/
|
4
|
+
begin
|
5
|
+
require 'win32/process'
|
6
|
+
rescue LoadError
|
7
|
+
"Warning: streamio-ffmpeg is missing the win32-process gem to properly handle hung transcodings. Install the gem (in Gemfile if using bundler) to avoid errors."
|
8
|
+
end
|
13
9
|
end
|
14
10
|
|
15
|
-
require 'win32/process' if RUBY_PLATFORM =~ /(win|w)(32|64)$/
|
16
|
-
|
17
11
|
#
|
18
12
|
# Monkey Patch timeout support into the IO class
|
19
13
|
#
|
20
14
|
class IO
|
21
15
|
def each_with_timeout(pid, seconds, sep_string=$/)
|
22
|
-
|
23
|
-
thread = nil
|
24
|
-
|
25
|
-
timer_set = lambda do
|
26
|
-
thread = new_thread(pid) { FFMPEG::Timer.timeout(seconds) { sleeping_queue.pop } }
|
27
|
-
end
|
16
|
+
last_update = Time.now
|
28
17
|
|
29
|
-
|
30
|
-
|
31
|
-
|
18
|
+
current_thread = Thread.current
|
19
|
+
check_update_thread = Thread.new do
|
20
|
+
loop do
|
21
|
+
sleep 0.1
|
22
|
+
if last_update - Time.now < -seconds
|
23
|
+
current_thread.raise Timeout::Error.new('output wait time expired')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
32
27
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
28
|
+
each(sep_string) do |buffer|
|
29
|
+
last_update = Time.now
|
30
|
+
yield buffer
|
31
|
+
end
|
32
|
+
rescue Timeout::Error
|
33
|
+
if RUBY_PLATFORM =~ /(win|w)(32|64)$/
|
34
|
+
Process.kill(1, pid)
|
35
|
+
else
|
36
|
+
Process.kill('SIGKILL', pid)
|
37
|
+
end
|
38
|
+
raise
|
39
39
|
ensure
|
40
|
-
|
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
|
40
|
+
check_update_thread.kill
|
58
41
|
end
|
59
42
|
end
|
data/lib/ffmpeg/movie.rb
CHANGED
@@ -5,77 +5,81 @@ module FFMPEG
|
|
5
5
|
attr_reader :path, :duration, :time, :bitrate, :rotation, :creation_time
|
6
6
|
attr_reader :video_stream, :video_codec, :video_bitrate, :colorspace, :resolution, :dar
|
7
7
|
attr_reader :audio_stream, :audio_codec, :audio_bitrate, :audio_sample_rate
|
8
|
-
|
8
|
+
attr_reader :container
|
9
|
+
|
9
10
|
def initialize(path)
|
10
11
|
raise Errno::ENOENT, "the file '#{path}' does not exist" unless File.exists?(path)
|
11
|
-
|
12
|
+
|
12
13
|
@path = path
|
13
14
|
|
14
15
|
# ffmpeg will output to stderr
|
15
16
|
command = "#{FFMPEG.ffmpeg_binary} -i #{Shellwords.escape(path)}"
|
16
17
|
output = Open3.popen3(command) { |stdin, stdout, stderr| stderr.read }
|
17
|
-
|
18
|
+
|
18
19
|
fix_encoding(output)
|
19
|
-
|
20
|
+
|
21
|
+
output[/Input \#\d+\,\s*(\S+),\s*from/]
|
22
|
+
@container = $1
|
23
|
+
|
20
24
|
output[/Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})/]
|
21
25
|
@duration = ($1.to_i*60*60) + ($2.to_i*60) + $3.to_f
|
22
|
-
|
26
|
+
|
23
27
|
output[/start: (\d*\.\d*)/]
|
24
28
|
@time = $1 ? $1.to_f : 0.0
|
25
29
|
|
26
30
|
output[/creation_time {1,}: {1,}(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/]
|
27
31
|
@creation_time = $1 ? Time.parse("#{$1}") : nil
|
28
|
-
|
32
|
+
|
29
33
|
output[/bitrate: (\d*)/]
|
30
34
|
@bitrate = $1 ? $1.to_i : nil
|
31
|
-
|
35
|
+
|
32
36
|
output[/rotate\ {1,}:\ {1,}(\d*)/]
|
33
37
|
@rotation = $1 ? $1.to_i : nil
|
34
38
|
|
35
|
-
output[/Video
|
39
|
+
output[/Video:\ (.*)/]
|
36
40
|
@video_stream = $1
|
37
|
-
|
38
|
-
output[/Audio
|
41
|
+
|
42
|
+
output[/Audio:\ (.*)/]
|
39
43
|
@audio_stream = $1
|
40
|
-
|
44
|
+
|
41
45
|
if video_stream
|
42
46
|
@video_codec, @colorspace, resolution, video_bitrate = video_stream.split(/\s?,\s?/)
|
43
47
|
@video_bitrate = video_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil
|
44
48
|
@resolution = resolution.split(" ").first rescue nil # get rid of [PAR 1:1 DAR 16:9]
|
45
49
|
@dar = $1 if video_stream[/DAR (\d+:\d+)/]
|
46
50
|
end
|
47
|
-
|
51
|
+
|
48
52
|
if audio_stream
|
49
53
|
@audio_codec, audio_sample_rate, @audio_channels, unused, audio_bitrate = audio_stream.split(/\s?,\s?/)
|
50
54
|
@audio_bitrate = audio_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil
|
51
55
|
@audio_sample_rate = audio_sample_rate[/\d*/].to_i
|
52
56
|
end
|
53
|
-
|
57
|
+
|
54
58
|
@invalid = true if @video_stream.to_s.empty? && @audio_stream.to_s.empty?
|
55
59
|
@invalid = true if output.include?("is not supported")
|
56
60
|
@invalid = true if output.include?("could not find codec parameters")
|
57
61
|
end
|
58
|
-
|
62
|
+
|
59
63
|
def valid?
|
60
64
|
not @invalid
|
61
65
|
end
|
62
|
-
|
66
|
+
|
63
67
|
def width
|
64
68
|
resolution.split("x")[0].to_i rescue nil
|
65
69
|
end
|
66
|
-
|
70
|
+
|
67
71
|
def height
|
68
72
|
resolution.split("x")[1].to_i rescue nil
|
69
73
|
end
|
70
|
-
|
74
|
+
|
71
75
|
def calculated_aspect_ratio
|
72
76
|
aspect_from_dar || aspect_from_dimensions
|
73
77
|
end
|
74
|
-
|
78
|
+
|
75
79
|
def size
|
76
80
|
File.size(@path)
|
77
81
|
end
|
78
|
-
|
82
|
+
|
79
83
|
def audio_channels
|
80
84
|
return nil unless @audio_channels
|
81
85
|
return @audio_channels[/\d*/].to_i if @audio_channels["channels"]
|
@@ -83,19 +87,20 @@ module FFMPEG
|
|
83
87
|
return 2 if @audio_channels["stereo"]
|
84
88
|
return 6 if @audio_channels["5.1"]
|
85
89
|
end
|
86
|
-
|
90
|
+
|
87
91
|
def frame_rate
|
92
|
+
return nil unless video_stream
|
88
93
|
video_stream[/(\d*\.?\d*)\s?fps/] ? $1.to_f : nil
|
89
94
|
end
|
90
|
-
|
95
|
+
|
91
96
|
def transcode(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
|
92
97
|
Transcoder.new(self, output_file, options, transcoder_options).run &block
|
93
98
|
end
|
94
|
-
|
99
|
+
|
95
100
|
def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
|
96
|
-
Transcoder.new(self, output_file, options.merge(:
|
101
|
+
Transcoder.new(self, output_file, options.merge(screenshot: true), transcoder_options).run &block
|
97
102
|
end
|
98
|
-
|
103
|
+
|
99
104
|
protected
|
100
105
|
def aspect_from_dar
|
101
106
|
return nil unless dar
|
@@ -103,12 +108,12 @@ module FFMPEG
|
|
103
108
|
aspect = w.to_f / h.to_f
|
104
109
|
aspect.zero? ? nil : aspect
|
105
110
|
end
|
106
|
-
|
111
|
+
|
107
112
|
def aspect_from_dimensions
|
108
113
|
aspect = width.to_f / height.to_f
|
109
114
|
aspect.nan? ? nil : aspect
|
110
115
|
end
|
111
|
-
|
116
|
+
|
112
117
|
def fix_encoding(output)
|
113
118
|
output[/test/] # Running a regexp on the string throws error if it's not UTF-8
|
114
119
|
rescue ArgumentError
|
data/lib/ffmpeg/transcoder.rb
CHANGED
@@ -3,7 +3,7 @@ require 'shellwords'
|
|
3
3
|
|
4
4
|
module FFMPEG
|
5
5
|
class Transcoder
|
6
|
-
@@timeout =
|
6
|
+
@@timeout = 30
|
7
7
|
|
8
8
|
def self.timeout=(time)
|
9
9
|
@@timeout = time
|
@@ -16,7 +16,7 @@ module FFMPEG
|
|
16
16
|
def initialize(movie, output_file, options = EncodingOptions.new, transcoder_options = {})
|
17
17
|
@movie = movie
|
18
18
|
@output_file = output_file
|
19
|
-
|
19
|
+
|
20
20
|
if options.is_a?(String) || options.is_a?(EncodingOptions)
|
21
21
|
@raw_options = options
|
22
22
|
elsif options.is_a?(Hash)
|
@@ -24,79 +24,85 @@ module FFMPEG
|
|
24
24
|
else
|
25
25
|
raise ArgumentError, "Unknown options format '#{options.class}', should be either EncodingOptions, Hash or String."
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
@transcoder_options = transcoder_options
|
29
29
|
@errors = []
|
30
|
-
|
30
|
+
|
31
31
|
apply_transcoder_options
|
32
32
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
33
|
+
|
34
|
+
def run(&block)
|
35
|
+
transcode_movie(&block)
|
36
|
+
if @transcoder_options[:validate]
|
37
|
+
validate_output_file(&block)
|
38
|
+
return encoded
|
39
|
+
else
|
40
|
+
return nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def encoding_succeeded?
|
45
|
+
@errors << "no output file created" and return false unless File.exists?(@output_file)
|
46
|
+
@errors << "encoded file is invalid" and return false unless encoded.valid?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def encoded
|
51
|
+
@encoded ||= Movie.new(@output_file)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
# frame= 4855 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/
|
56
|
+
def transcode_movie
|
57
|
+
@command = "#{FFMPEG.ffmpeg_binary} -y -i #{Shellwords.escape(@movie.path)} #{@raw_options} #{Shellwords.escape(@output_file)}"
|
58
|
+
FFMPEG.logger.info("Running transcoding...\n#{@command}\n")
|
59
|
+
@output = ""
|
60
|
+
|
61
|
+
Open3.popen3(@command) do |stdin, stdout, stderr, wait_thr|
|
42
62
|
begin
|
43
63
|
yield(0.0) if block_given?
|
44
64
|
next_line = Proc.new do |line|
|
45
65
|
fix_encoding(line)
|
46
|
-
output << line
|
66
|
+
@output << line
|
47
67
|
if line.include?("time=")
|
48
68
|
if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
|
49
69
|
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
70
|
else # better make sure it wont blow up in case of unexpected output
|
53
71
|
time = 0.0
|
54
72
|
end
|
55
73
|
progress = time / @movie.duration
|
56
74
|
yield(progress) if block_given?
|
57
75
|
end
|
58
|
-
if line =~ /Unsupported codec/
|
59
|
-
FFMPEG.logger.error "Failed encoding...\nCommand\n#{command}\nOutput\n#{output}\n"
|
60
|
-
raise "Failed encoding: #{line}"
|
61
|
-
end
|
62
76
|
end
|
63
|
-
|
77
|
+
|
64
78
|
if @@timeout
|
65
|
-
stderr.each_with_timeout(wait_thr.pid, @@timeout,
|
79
|
+
stderr.each_with_timeout(wait_thr.pid, @@timeout, 'size=', &next_line)
|
66
80
|
else
|
67
|
-
stderr.each(
|
81
|
+
stderr.each('size=', &next_line)
|
68
82
|
end
|
69
|
-
|
83
|
+
|
70
84
|
rescue Timeout::Error => e
|
71
|
-
FFMPEG.logger.error "Process hung...\
|
72
|
-
raise
|
85
|
+
FFMPEG.logger.error "Process hung...\n@command\n#{@command}\nOutput\n#{@output}\n"
|
86
|
+
raise Error, "Process hung. Full output: #{@output}"
|
73
87
|
end
|
74
88
|
end
|
89
|
+
end
|
75
90
|
|
91
|
+
def validate_output_file(&block)
|
76
92
|
if encoding_succeeded?
|
77
93
|
yield(1.0) if block_given?
|
78
94
|
FFMPEG.logger.info "Transcoding of #{@movie.path} to #{@output_file} succeeded\n"
|
79
95
|
else
|
80
96
|
errors = "Errors: #{@errors.join(", ")}. "
|
81
|
-
FFMPEG.logger.error "Failed encoding...\n#{command}\n\n#{output}\n#{errors}\n"
|
82
|
-
raise
|
97
|
+
FFMPEG.logger.error "Failed encoding...\n#{@command}\n\n#{@output}\n#{errors}\n"
|
98
|
+
raise Error, "Failed encoding.#{errors}Full output: #{@output}"
|
83
99
|
end
|
84
|
-
|
85
|
-
encoded
|
86
|
-
end
|
87
|
-
|
88
|
-
def encoding_succeeded?
|
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?
|
91
|
-
true
|
92
|
-
end
|
93
|
-
|
94
|
-
def encoded
|
95
|
-
@encoded ||= Movie.new(@output_file)
|
96
100
|
end
|
97
|
-
|
98
|
-
private
|
101
|
+
|
99
102
|
def apply_transcoder_options
|
103
|
+
# if true runs #validate_output_file
|
104
|
+
@transcoder_options[:validate] = @transcoder_options.fetch(:validate) { true }
|
105
|
+
|
100
106
|
return if @movie.calculated_aspect_ratio.nil?
|
101
107
|
case @transcoder_options[:preserve_aspect_ratio].to_s
|
102
108
|
when "width"
|
@@ -111,7 +117,7 @@ module FFMPEG
|
|
111
117
|
@raw_options[:resolution] = "#{new_width}x#{@raw_options.height}"
|
112
118
|
end
|
113
119
|
end
|
114
|
-
|
120
|
+
|
115
121
|
def fix_encoding(output)
|
116
122
|
output[/test/]
|
117
123
|
rescue ArgumentError
|
data/lib/ffmpeg/version.rb
CHANGED
data/lib/streamio-ffmpeg.rb
CHANGED
@@ -19,7 +19,7 @@ module FFMPEG
|
|
19
19
|
def self.logger=(log)
|
20
20
|
@logger = log
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
# Get FFMPEG logger.
|
24
24
|
#
|
25
25
|
# @return [Logger]
|
@@ -43,6 +43,6 @@ module FFMPEG
|
|
43
43
|
#
|
44
44
|
# @return [String] the path to the ffmpeg binary
|
45
45
|
def self.ffmpeg_binary
|
46
|
-
@ffmpeg_binary
|
46
|
+
@ffmpeg_binary || 'ffmpeg'
|
47
47
|
end
|
48
48
|
end
|
metadata
CHANGED
@@ -1,52 +1,46 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: streamio-ffmpeg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- David Backeus
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2013-07-08 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: rspec
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - ~>
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: '2.
|
19
|
+
version: '2.14'
|
22
20
|
type: :development
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - ~>
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: '2.
|
26
|
+
version: '2.14'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: rake
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
31
|
- - ~>
|
36
32
|
- !ruby/object:Gem::Version
|
37
|
-
version:
|
33
|
+
version: '10.1'
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
38
|
- - ~>
|
44
39
|
- !ruby/object:Gem::Version
|
45
|
-
version:
|
46
|
-
description:
|
47
|
-
and do transcoding.
|
40
|
+
version: '10.1'
|
41
|
+
description:
|
48
42
|
email:
|
49
|
-
- david@streamio.
|
43
|
+
- david@streamio.com
|
50
44
|
executables: []
|
51
45
|
extensions: []
|
52
46
|
extra_rdoc_files: []
|
@@ -63,32 +57,25 @@ files:
|
|
63
57
|
- CHANGELOG
|
64
58
|
homepage: http://github.com/streamio/streamio-ffmpeg
|
65
59
|
licenses: []
|
60
|
+
metadata: {}
|
66
61
|
post_install_message:
|
67
62
|
rdoc_options: []
|
68
63
|
require_paths:
|
69
64
|
- lib
|
70
65
|
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
-
none: false
|
72
66
|
requirements:
|
73
|
-
- -
|
67
|
+
- - '>='
|
74
68
|
- !ruby/object:Gem::Version
|
75
69
|
version: '0'
|
76
|
-
segments:
|
77
|
-
- 0
|
78
|
-
hash: 190048295991981084
|
79
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
-
none: false
|
81
71
|
requirements:
|
82
|
-
- -
|
72
|
+
- - '>='
|
83
73
|
- !ruby/object:Gem::Version
|
84
74
|
version: '0'
|
85
|
-
segments:
|
86
|
-
- 0
|
87
|
-
hash: 190048295991981084
|
88
75
|
requirements: []
|
89
76
|
rubyforge_project:
|
90
|
-
rubygems_version:
|
77
|
+
rubygems_version: 2.0.3
|
91
78
|
signing_key:
|
92
|
-
specification_version:
|
93
|
-
summary:
|
79
|
+
specification_version: 4
|
80
|
+
summary: Wraps ffmpeg to read metadata and transcodes videos.
|
94
81
|
test_files: []
|