powerand-ffmpeg 2.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8b54f1d85f8f8eb48cd261d420774e194c4514ce
4
+ data.tar.gz: 00230f6e7783dc8c8bacc87c497daad354f8aa05
5
+ SHA512:
6
+ metadata.gz: b1bcd7442f6adf143d6e36ddd3681340d3481f4cf79a56c4b59039550bcde2c91f2b332a1863959a28fed7d015f7017bef46ba7af0c545800f24eb93fcc5a07c
7
+ data.tar.gz: 8d3133f63f559295eeb865d8a2df124531b71aa16c3839b3c444fe15ea3c2c0d23b24604c26fc32e01cb9c245e49548e464e1385598f23a056363d054e5a64ec
data/CHANGELOG ADDED
@@ -0,0 +1,225 @@
1
+ == Master
2
+
3
+ Current with 2.1.0
4
+
5
+ == 2.1.0 2016-03-04
6
+
7
+ Improvements:
8
+ * Silent mode support, example:
9
+ FFMPEG::Movie.new('path/name.ext').transcode('new.ext', { silent: true })
10
+ * Added gemspec
11
+
12
+ == 2.0.0 2016-01-14
13
+
14
+ New:
15
+ * Support watermarking (thanks smoothdvd)
16
+ * Bumped target ffmpeg version to 2.8
17
+
18
+ Improvements:
19
+ * Allow parenthesis in colorspace (thanks walterdavis for initial code and rociiu for finding a bug with it)
20
+ * Width and height now switched if video is filmed in portrait mode. ffmpeg 2.7 or later now automatically rotates output
21
+ * Movie metadata now provided with ffprobe (#114). Thanks to Ryan Lovelett for the contributions!
22
+ * Ability to create multiple screenshots at consistent intervals in one pass (#113)
23
+
24
+ == 1.0.0 2013-07-08
25
+
26
+ New:
27
+ * Bumped target ffmpeg version to 1.2.1
28
+
29
+ Improvements:
30
+ * Simpler implementation for timeouts.
31
+ Should be far less cpu and memory dependent (don't spawn a thread for every line of output)
32
+ Timeout spec now passes in Rubinius (using 1.9 mode)
33
+ * Give helpful error message for windows users lacking the win32-process gem (thanks casoetan)
34
+ * Add Movie#container (thanks vitalis)
35
+ * Support vprofile and preset encoding options (thanks vitalis)
36
+
37
+ Changes:
38
+ * Default timeout lowered to 30 seconds
39
+
40
+ Bugs:
41
+ * Avoid crash if asking for frame_rate of a video without video stream (thanks squidarth)
42
+ * Fix crash when doing audio transcoding on ffmpeg >= 1.0.1 (thanks vitalis)
43
+
44
+ Deprecations:
45
+ * Removed support for Ruby 1.8
46
+ * Removed support for ffmpeg 0.7
47
+
48
+ Refactorings:
49
+ * Quite a few, see commit history for details.
50
+
51
+ == 0.9.0 2012-07-24
52
+
53
+ New:
54
+ * Bumped target ffmpeg version to 0.11.1
55
+ * Add hung process detection with configurable timeout (thanks stakach)
56
+ * Raise FFMPEG::Error instead of generic RuntimeError on failed transcodings
57
+ * Movie#screenshot for more intuitive screenshotting (README has details)
58
+ * Movie#creation_time and Movie#rotation attributes when metadata is available (thanks Innonate)
59
+
60
+ Bugs:
61
+ * Fixed too many open files bug (thanks to akicho8)
62
+ * Fixed missing path escaping (thanks to mikesager)
63
+ * Fixed README typo (thanks to Linutux)
64
+ * Files outputing "could not find codec parameters" are now recognized as invalid
65
+
66
+ Deprecations:
67
+ * Removed Movie#uncertain_duration?
68
+ * Removed all the deprecated crop options (use :custom => '-vf crop=x:x:x:x' if you need it)
69
+
70
+ Refactorings:
71
+ * Removed the deprecated duration validation code
72
+ * Polish on the transcoder class
73
+ * Polish on the spec suite
74
+
75
+ == 0.8.5 2011-03-05
76
+
77
+ * If a clip has a DAR that doesn't make sense fall back to calculating aspect ratio from dimensions
78
+ * Allow filenames with single quote characters (thanks to youpy)
79
+
80
+ == 0.8.4 2011-11-30
81
+
82
+ * Duration now one decimal more accurate (thanks to Russel Brooks)
83
+ * Added encoding option seek_time (thanks to Misty De Meo)
84
+
85
+ == 0.8.3 2011-09-01
86
+
87
+ * Parameters now come in the order of codecs, presets, others so that we can override the presets
88
+ * Added encoding option keyframe_interval to set number of frames between i-frames (aka GOP size)
89
+ * Streamio (sponsor of this project) have launched new awesome pricing @ http://streamio.com
90
+
91
+ == 0.8.2 2011-08-19
92
+
93
+ * Path to ffmpeg binary can now be specified (thanks jonathandean)
94
+ * If ffmpeg output contains "is not supported" the Movie will be considered invalid
95
+
96
+ == 0.8.1 2011-07-28
97
+
98
+ * Fix progress yielding with ffmpeg 0.8
99
+ * Updated specs to pass with ffmpeg 0.8
100
+
101
+ == 0.8.0 2011-05-26
102
+
103
+ * Duration is now ALWAYS considered uncertain (we've noticed that ffmpeg is not always correct)
104
+ * This means that the duration check will normally never run (unless you manually hack @uncertain_duration to false)
105
+ * Movie#audio_channels now returns nil if there is no audio stream (instead of crashing)
106
+ * Development: Use Bundler
107
+ * Development: Update RSpec to 2.6
108
+
109
+ == 0.7.8 2011-04-04
110
+
111
+ * Fixed number of audio channels on files with 5.1 audio
112
+
113
+ == 0.7.7 2011-02-01
114
+
115
+ * Movies with starttime are now considered as having uncertain duration as its behavior is not consistent across formats
116
+ * Upgrade development environment to RSpec 2.4
117
+
118
+ == 0.7.6 2011-01-14
119
+
120
+ * Another ruby 1.9 encoding fix
121
+
122
+ == 0.7.5 2011-01-14
123
+
124
+ * Fixed some ruby 1.9 issues
125
+ * Added Movie#video_bitrate and Movie#audio_bitrate (thanks to mbj)
126
+
127
+ == 0.7.4 2010-12-07
128
+
129
+ * Fixed broken duration on movies with start times over 0 by reducing duration with start-time
130
+
131
+ == 0.7.3 2010-08-26
132
+
133
+ * Replaced Jewler with simple dynamic gemspec file
134
+ * Spec files now not in published gem to make it a lot smaller in size
135
+ * Full output from ffmpeg command in error raised during transcoding
136
+
137
+ == 0.7.2 2010-08-11
138
+
139
+ * Added encoding option duration
140
+ * Avoid crashing when ffmpeg can't find resolution of a movie
141
+
142
+ == 0.7.1 2010-07-08
143
+
144
+ * Make sure preset parameters are always put last to avoid them ending up before any codec assignments
145
+ * Testing against a fresh ffmpeg build (r24069)
146
+
147
+ == 0.7.0 2010-07-07
148
+
149
+ * Support for ffpresets through video_preset, audio_preset and file_preset encoding options
150
+ * Added encoding option video_bitrate_tolerance
151
+
152
+ == 0.6.8.1 2010-07-06
153
+
154
+ * Bugfix - aspect ratio was not calculated properly on movies with no DAR
155
+
156
+ == 0.6.8 2010-07-06
157
+
158
+ * Don't use encoding options with nil values
159
+ * Added encoding options video_max_bitrate, video_min_bitrate and buffer_size for constant bitrate encoding
160
+
161
+ == 0.6.7 2010-06-10
162
+
163
+ * Bugfix - aspect ratio preserver could suggest non even resolutions in certain circumstances
164
+
165
+ == 0.6.6 2010-06-10
166
+
167
+ * Transcodings to .jpg and .png will now work as they will skip duration validation
168
+
169
+ == 0.6.5 2010-05-19
170
+
171
+ * Movie#size method to get file size.
172
+
173
+ == 0.6.4 2010-05-12
174
+
175
+ * Ruby 1.9 compatibility fix for EncodingOptions (thanks michalf!)
176
+
177
+ == 0.6.3 2010-05-05
178
+
179
+ * Use DAR to calculate aspect ratio if available
180
+
181
+ == 0.6.2 2010-05-05
182
+
183
+ * Added Movie#uncertain_duration? which is true if ffmpeg is guessing duration from bitrate
184
+ * Skipping the transcoders duration validation if original file has uncertain duration
185
+ * Made sure aspect ratio preservation always rounds new size to an even number to avoid "not divisible by 2" errors
186
+ * Changed Movie#valid? logic to accept any movie with either a readable audio or video stream
187
+
188
+ == 0.6.0 2010-05-04
189
+
190
+ * Cropping options now handled by EncodingOptions (croptop, cropbottom, cropleft and cropright)
191
+ * Aspect ratio parameter calculated and added by default
192
+ * Added transcoder options to preserve original aspect ratio on width or height
193
+
194
+ == 0.5.0 2010-04-28
195
+
196
+ * Added logging capabilities
197
+
198
+ == 0.4.3 2010-04-06
199
+
200
+ * Correctly identify invalid movies on latest ffmpeg build (r22811)
201
+
202
+ == 0.4.2 2010-04-06
203
+
204
+ * Escape the path to handle spaces in filenames and avoid CLI injection attacks (thanks J. Weir!)
205
+
206
+ == 0.4.1 2010-02-10
207
+
208
+ * Forgot to change the transcoding shortcut from Movie
209
+
210
+ == 0.4.0 2010-02-10
211
+
212
+ * Transcoding API changed to make use of more humanly readable options (see README for examples)
213
+ * Fixed frame rate parsing for integer frame rates
214
+
215
+ == 0.3.0 2010-02-07
216
+
217
+ * Simple transcoding
218
+
219
+ == 0.2.0 2010-02-06
220
+
221
+ * Some more metadata parsing
222
+
223
+ == 0.1.0 2010-02-05
224
+
225
+ * Some basic parsing of metadata added
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 powerand AB
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.md ADDED
@@ -0,0 +1,218 @@
1
+ powerand FFMPEG
2
+ ===============
3
+
4
+ Simple yet powerful wrapper around the ffmpeg command for reading metadata and transcoding movies.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ (sudo) gem install powerand-ffmpeg
10
+
11
+ Compatibility
12
+ -------------
13
+
14
+ ### Ruby
15
+
16
+ Only guaranteed to work with MRI Ruby 1.9.3 or later.
17
+ Should work with rubinius head in 1.9 mode.
18
+ Will not work in jruby until they fix: http://goo.gl/Z4UcX (should work in the upcoming 1.7.5)
19
+
20
+ ### ffmpeg
21
+
22
+ The current gem is tested against ffmpeg 2.8.4. So no guarantees with earlier (or much later)
23
+ versions. Output and input standards have inconveniently changed rather a lot between versions
24
+ of ffmpeg. My goal is to keep this library in sync with new versions of ffmpeg as they come along.
25
+
26
+ Usage
27
+ -----
28
+
29
+ ### Require the gem
30
+
31
+ ``` ruby
32
+ require 'rubygems'
33
+ require 'powerand-ffmpeg'
34
+ ```
35
+
36
+ ### Reading Metadata
37
+
38
+ ``` ruby
39
+ movie = FFMPEG::Movie.new("path/to/movie.mov")
40
+
41
+ movie.duration # 7.5 (duration of the movie in seconds)
42
+ movie.bitrate # 481 (bitrate in kb/s)
43
+ movie.size # 455546 (filesize in bytes)
44
+
45
+ movie.video_stream # "h264, yuv420p, 640x480 [PAR 1:1 DAR 4:3], 371 kb/s, 16.75 fps, 15 tbr, 600 tbn, 1200 tbc" (raw video stream info)
46
+ movie.video_codec # "h264"
47
+ movie.colorspace # "yuv420p"
48
+ movie.resolution # "640x480"
49
+ movie.width # 640 (width of the movie in pixels)
50
+ movie.height # 480 (height of the movie in pixels)
51
+ movie.frame_rate # 16.72 (frames per second)
52
+
53
+ movie.audio_stream # "aac, 44100 Hz, stereo, s16, 75 kb/s" (raw audio stream info)
54
+ movie.audio_codec # "aac"
55
+ movie.audio_sample_rate # 44100
56
+ movie.audio_channels # 2
57
+
58
+ movie.valid? # true (would be false if ffmpeg fails to read the movie)
59
+ ```
60
+
61
+ ### Transcoding
62
+
63
+ First argument is the output file path.
64
+
65
+ ``` ruby
66
+ movie.transcode("tmp/movie.mp4") # Default ffmpeg settings for mp4 format
67
+ ```
68
+
69
+ Keep track of progress with an optional block.
70
+
71
+ ``` ruby
72
+ movie.transcode("movie.mp4") { |progress| puts progress } # 0.2 ... 0.5 ... 1.0
73
+ ```
74
+
75
+ Give custom command line options with a string.
76
+
77
+ ``` ruby
78
+ movie.transcode("movie.mp4", "-ac aac -vc libx264 -ac 2 ...")
79
+ ```
80
+
81
+ 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.
82
+
83
+ ``` ruby
84
+ options = {video_codec: "libx264", frame_rate: 10, resolution: "320x240", video_bitrate: 300, video_bitrate_tolerance: 100,
85
+ aspect: 1.333333, keyframe_interval: 90,
86
+ x264_vprofile: "high", x264_preset: "slow",
87
+ audio_codec: "libfaac", audio_bitrate: 32, audio_sample_rate: 22050, audio_channels: 1,
88
+ threads: 2,
89
+ custom: "-vf crop=60:60:10:10"}
90
+ movie.transcode("movie.mp4", options)
91
+ ```
92
+
93
+ The transcode function returns a Movie object for the encoded file.
94
+
95
+ ``` ruby
96
+ transcoded_movie = movie.transcode("tmp/movie.flv")
97
+
98
+ transcoded_movie.video_codec # "flv"
99
+ transcoded_movie.audio_codec # "mp3"
100
+ ```
101
+
102
+ Aspect ratio is added to encoding options automatically if none is specified.
103
+
104
+ ``` ruby
105
+ options = { resolution: "320x180" } # Will add -aspect 1.77777777777778 to ffmpeg
106
+ ```
107
+
108
+ Preserve aspect ratio on width or height by using the preserve_aspect_ratio transcoder option.
109
+
110
+ ``` ruby
111
+ widescreen_movie = FFMPEG::Movie.new("path/to/widescreen_movie.mov")
112
+
113
+ options = { resolution: "320x240" }
114
+
115
+ transcoder_options = { preserve_aspect_ratio: :width }
116
+ widescreen_movie.transcode("movie.mp4", options, transcoder_options) # Output resolution will be 320x180
117
+
118
+ transcoder_options = { preserve_aspect_ratio: :height }
119
+ widescreen_movie.transcode("movie.mp4", options, transcoder_options) # Output resolution will be 426x240
120
+ ```
121
+
122
+ For constant bitrate encoding use video_min_bitrate and video_max_bitrate with buffer_size.
123
+
124
+ ``` ruby
125
+ options = {video_min_bitrate: 600, video_max_bitrate: 600, buffer_size: 2000}
126
+ movie.transcode("movie.flv", options)
127
+ ```
128
+
129
+ Add watermark image on the video.
130
+
131
+ For example, you want to add a watermark on the video at right top corner with 10px padding.
132
+
133
+ ``` ruby
134
+ options = { watermark: "full_path_of_watermark.png", resolution: "640x360", watermark_filter: { position: "RT", padding_x: 10, padding_y: 10 } }
135
+ ```
136
+
137
+ Position can be "LT" (Left Top Corner), "RT" (Right Top Corner), "LB" (Left Bottom Corner), "RB" (Right Bottom Corner).
138
+
139
+ ### Taking Screenshots
140
+
141
+ You can use the screenshot method to make taking screenshots a bit simpler.
142
+
143
+ ``` ruby
144
+ movie.screenshot("screenshot.jpg")
145
+ ```
146
+
147
+ The screenshot method has the very same API as transcode so the same options will work.
148
+
149
+ ``` ruby
150
+ movie.screenshot("screenshot.bmp", seek_time: 5, resolution: '320x240')
151
+ ```
152
+
153
+ To generate multiple screenshots in a single pass, specify `vframes`. The following code
154
+ generates up to 20 screenshots every 10 seconds:
155
+
156
+ ``` ruby
157
+ movie.screenshot("screenshot.jpg", vframes: 20, frame_rate: '1/6')
158
+ ```
159
+
160
+ To specify the quality when generating compressed screenshots (.jpg), use `quality` which specifies
161
+ ffmpeg `-v:q` option. Quality is an integer between 1 and 31, where lower is better quality:
162
+
163
+ ``` ruby
164
+ movie.screenshot("screenshot.jpg", quality: 3)
165
+ ```
166
+
167
+ You can preserve aspect ratio the same way as when using transcode.
168
+
169
+ ``` ruby
170
+ movie.screenshot("screenshot.png", { seek_time: 2, resolution: '200x120' }, preserve_aspect_ratio: :width)
171
+ ```
172
+
173
+ Specify the path to ffmpeg
174
+ --------------------------
175
+
176
+ By default, the gem assumes that the ffmpeg binary is available in the execution path and named ffmpeg and so will run commands that look something like "ffmpeg -i /path/to/input.file ...". Use the FFMPEG.ffmpeg_binary setter to specify the full path to the binary if necessary:
177
+
178
+ ``` ruby
179
+ FFMPEG.ffmpeg_binary = '/usr/local/bin/ffmpeg'
180
+ ```
181
+
182
+ This will cause the same command to run as "/usr/local/bin/ffmpeg -i /path/to/input.file ..." instead.
183
+
184
+
185
+ Automatically kill hung processes
186
+ ---------------------------------
187
+
188
+ By default, the gem will wait for 30 seconds between IO feedback from the FFMPEG process. After which an error is logged and the process killed.
189
+ It is possible to modify this behaviour by setting a new default:
190
+
191
+ ``` ruby
192
+ # Change the timeout
193
+ Transcoder.timeout = 10
194
+
195
+ # Disable the timeout altogether
196
+ Transcoder.timeout = false
197
+ ```
198
+
199
+ Disabling output file validation
200
+ ------------------------------
201
+
202
+ By default Transcoder validates the output file, in case you use FFMPEG for HLS
203
+ format that creates multiple outputs you can disable the validation by passing
204
+ `validate: false` to transcoder_options.
205
+
206
+ Note that transcode will not return the encoded movie object in this case since
207
+ attempting to open a (possibly) invalid output file might result in an error being raised.
208
+
209
+ ```ruby
210
+ transcoder_options = { validate: false }
211
+ movie.transcode("movie.mp4", options, transcoder_options) # returns nil
212
+ ```
213
+
214
+
215
+ Copyright
216
+ ---------
217
+
218
+ Copyright (c) powerand AB. See LICENSE for details.
@@ -0,0 +1,179 @@
1
+ module FFMPEG
2
+ class EncodingOptions < Hash
3
+ def initialize(options = {})
4
+ merge!(options)
5
+ end
6
+
7
+ def to_s
8
+ params = collect do |key, value|
9
+ send("convert_#{key}", value) if value && supports_option?(key)
10
+ end
11
+
12
+ # codecs should go before the presets so that the files will be matched successfully
13
+ # all other parameters go after so that we can override whatever is in the preset
14
+ codecs = params.select { |p| p =~ /codec/ }
15
+ presets = params.select { |p| p =~ /\-.pre/ }
16
+ watermarkoptions = params.select { |p| p =~ /i / || p=~ /filter_complex/ }
17
+ other = params - codecs - presets - watermarkoptions
18
+ params = watermarkoptions + codecs + presets + other
19
+
20
+ params_string = params.join(" ")
21
+ params_string << " #{convert_aspect(calculate_aspect)}" if calculate_aspect?
22
+ params_string
23
+ end
24
+
25
+ def width
26
+ self[:resolution].split("x").first.to_i rescue nil
27
+ end
28
+
29
+ def height
30
+ self[:resolution].split("x").last.to_i rescue nil
31
+ end
32
+
33
+ private
34
+
35
+ def supports_option?(option)
36
+ option = RUBY_VERSION < "1.9" ? "convert_#{option}" : "convert_#{option}".to_sym
37
+ private_methods.include?(option)
38
+ end
39
+
40
+ def convert_aspect(value)
41
+ "-aspect #{value}"
42
+ end
43
+
44
+ def calculate_aspect
45
+ width, height = self[:resolution].split("x")
46
+ width.to_f / height.to_f
47
+ end
48
+
49
+ def calculate_aspect?
50
+ self[:aspect].nil? && self[:resolution]
51
+ end
52
+
53
+ def convert_video_codec(value)
54
+ "-vcodec #{value}"
55
+ end
56
+
57
+ def convert_frame_rate(value)
58
+ "-r #{value}"
59
+ end
60
+
61
+ def convert_resolution(value)
62
+ "-s #{value}"
63
+ end
64
+
65
+ def convert_video_bitrate(value)
66
+ "-b:v #{k_format(value)}"
67
+ end
68
+
69
+ def convert_audio_codec(value)
70
+ "-acodec #{value}"
71
+ end
72
+
73
+ def convert_audio_bitrate(value)
74
+ "-b:a #{k_format(value)}"
75
+ end
76
+
77
+ def convert_audio_sample_rate(value)
78
+ "-ar #{value}"
79
+ end
80
+
81
+ def convert_audio_channels(value)
82
+ "-ac #{value}"
83
+ end
84
+
85
+ def convert_video_max_bitrate(value)
86
+ "-maxrate #{k_format(value)}"
87
+ end
88
+
89
+ def convert_video_min_bitrate(value)
90
+ "-minrate #{k_format(value)}"
91
+ end
92
+
93
+ def convert_buffer_size(value)
94
+ "-bufsize #{k_format(value)}"
95
+ end
96
+
97
+ def convert_video_bitrate_tolerance(value)
98
+ "-bt #{k_format(value)}"
99
+ end
100
+
101
+ def convert_threads(value)
102
+ "-threads #{value}"
103
+ end
104
+
105
+ def convert_target(value)
106
+ "-target #{value}"
107
+ end
108
+
109
+ def convert_duration(value)
110
+ "-t #{value}"
111
+ end
112
+
113
+ def convert_video_preset(value)
114
+ "-vpre #{value}"
115
+ end
116
+
117
+ def convert_audio_preset(value)
118
+ "-apre #{value}"
119
+ end
120
+
121
+ def convert_file_preset(value)
122
+ "-fpre #{value}"
123
+ end
124
+
125
+ def convert_keyframe_interval(value)
126
+ "-g #{value}"
127
+ end
128
+
129
+ def convert_seek_time(value)
130
+ "-ss #{value}"
131
+ end
132
+
133
+ def convert_screenshot(value)
134
+ vframes = '-vframes 1 ' unless self[:vframes]
135
+ value ? "#{vframes}-f image2" : ""
136
+ end
137
+
138
+ def convert_quality(value)
139
+ "-q:v #{value}"
140
+ end
141
+
142
+ def convert_vframes(value)
143
+ "-vframes #{value}"
144
+ end
145
+
146
+ def convert_x264_vprofile(value)
147
+ "-vprofile #{value}"
148
+ end
149
+
150
+ def convert_x264_preset(value)
151
+ "-preset #{value}"
152
+ end
153
+
154
+ def convert_watermark(value)
155
+ "-i #{value}"
156
+ end
157
+
158
+ def convert_watermark_filter(value)
159
+ case value[:position].to_s
160
+ when "LT"
161
+ "-filter_complex 'scale=#{self[:resolution]},overlay=x=#{value[:padding_x]}:y=#{value[:padding_y]}'"
162
+ when "RT"
163
+ "-filter_complex 'scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{value[:padding_x]}:y=#{value[:padding_y]}'"
164
+ when "LB"
165
+ "-filter_complex 'scale=#{self[:resolution]},overlay=x=#{value[:padding_x]}:y=main_h-overlay_h-#{value[:padding_y]}'"
166
+ when "RB"
167
+ "-filter_complex 'scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{value[:padding_x]}:y=main_h-overlay_h-#{value[:padding_y]}'"
168
+ end
169
+ end
170
+
171
+ def convert_custom(value)
172
+ value
173
+ end
174
+
175
+ def k_format(value)
176
+ value.to_s.include?("k") ? value : "#{value}k"
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,4 @@
1
+ module FFMPEG
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,42 @@
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
9
+ end
10
+
11
+ #
12
+ # Monkey Patch timeout support into the IO class
13
+ #
14
+ class IO
15
+ def each_with_timeout(pid, seconds, sep_string=$/)
16
+ last_update = Time.now
17
+
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
27
+
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
+ ensure
40
+ check_update_thread.kill
41
+ end
42
+ end
@@ -0,0 +1,176 @@
1
+ require 'time'
2
+ require 'multi_json'
3
+
4
+ module FFMPEG
5
+ class Movie
6
+ attr_reader :path, :duration, :time, :bitrate, :rotation, :creation_time
7
+ attr_reader :video_stream, :video_codec, :video_bitrate, :colorspace, :width, :height, :sar, :dar, :frame_rate
8
+ attr_reader :audio_stream, :audio_codec, :audio_bitrate, :audio_sample_rate, :audio_channels
9
+ attr_reader :container
10
+
11
+ def initialize(path)
12
+ raise Errno::ENOENT, "the file '#{path}' does not exist" unless File.exist?(path)
13
+
14
+ @path = path
15
+
16
+ # ffmpeg will output to stderr
17
+ command = "#{FFMPEG.ffprobe_binary} -i #{Shellwords.escape(path)} -print_format json -show_format -show_streams -show_error"
18
+ std_output = ''
19
+ std_error = ''
20
+
21
+ Open3.popen3(command) do |stdin, stdout, stderr|
22
+ std_output = stdout.read unless stdout.nil?
23
+ std_error = stderr.read unless stderr.nil?
24
+ end
25
+
26
+ fix_encoding(std_output)
27
+
28
+ metadata = MultiJson.load(std_output, symbolize_keys: true)
29
+
30
+ if metadata.key?(:error)
31
+
32
+ @duration = 0
33
+
34
+ else
35
+
36
+ video_streams = metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'video' }
37
+ audio_streams = metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'audio' }
38
+
39
+ @container = metadata[:format][:format_name]
40
+
41
+ @duration = metadata[:format][:duration].to_f
42
+
43
+ @time = metadata[:format][:start_time].to_f
44
+
45
+ @creation_time = if metadata[:format].key?(:tags) and metadata[:format][:tags].key?(:creation_time)
46
+ Time.parse(metadata[:format][:tags][:creation_time])
47
+ else
48
+ nil
49
+ end
50
+
51
+ @bitrate = metadata[:format][:bit_rate].to_i
52
+
53
+ unless video_streams.empty?
54
+ # TODO: Handle multiple video codecs (is that possible?)
55
+ video_stream = video_streams.first
56
+ @video_codec = video_stream[:codec_name]
57
+ @colorspace = video_stream[:pix_fmt]
58
+ @width = video_stream[:width]
59
+ @height = video_stream[:height]
60
+ @video_bitrate = video_stream[:bit_rate].to_i
61
+ @sar = video_stream[:sample_aspect_ratio]
62
+ @dar = video_stream[:display_aspect_ratio]
63
+
64
+ @frame_rate = unless video_stream[:avg_frame_rate] == '0/0'
65
+ Rational(video_stream[:avg_frame_rate])
66
+ else
67
+ nil
68
+ end
69
+
70
+ @video_stream = "#{video_stream[:codec_name]} (#{video_stream[:profile]}) (#{video_stream[:codec_tag_string]} / #{video_stream[:codec_tag]}), #{colorspace}, #{resolution} [SAR #{sar} DAR #{dar}]"
71
+
72
+ @rotation = if video_stream.key?(:tags) and video_stream[:tags].key?(:rotate)
73
+ video_stream[:tags][:rotate].to_i
74
+ else
75
+ nil
76
+ end
77
+ end
78
+
79
+ unless audio_streams.empty?
80
+ # TODO: Handle multiple audio codecs
81
+ audio_stream = audio_streams.first
82
+ @audio_channels = audio_stream[:channels].to_i
83
+ @audio_codec = audio_stream[:codec_name]
84
+ @audio_sample_rate = audio_stream[:sample_rate].to_i
85
+ @audio_bitrate = audio_stream[:bit_rate].to_i
86
+ @audio_channel_layout = audio_stream[:channel_layout]
87
+ @audio_stream = "#{audio_codec} (#{audio_stream[:codec_tag_string]} / #{audio_stream[:codec_tag]}), #{audio_sample_rate} Hz, #{audio_channel_layout}, #{audio_stream[:sample_fmt]}, #{audio_bitrate} bit/s"
88
+ end
89
+
90
+ end
91
+
92
+ @invalid = true if metadata.key?(:error)
93
+ @invalid = true if std_error.include?("Unsupported codec")
94
+ @invalid = true if std_error.include?("is not supported")
95
+ @invalid = true if std_error.include?("could not find codec parameters")
96
+ end
97
+
98
+ def valid?
99
+ not @invalid
100
+ end
101
+
102
+ def width
103
+ rotation.nil? || rotation == 180 ? @width : @height;
104
+ end
105
+
106
+ def height
107
+ rotation.nil? || rotation == 180 ? @height : @width;
108
+ end
109
+
110
+ def resolution
111
+ unless width.nil? or height.nil?
112
+ "#{width}x#{height}"
113
+ end
114
+ end
115
+
116
+ def calculated_aspect_ratio
117
+ aspect_from_dar || aspect_from_dimensions
118
+ end
119
+
120
+ def calculated_pixel_aspect_ratio
121
+ aspect_from_sar || 1
122
+ end
123
+
124
+ def size
125
+ File.size(@path)
126
+ end
127
+
128
+ def audio_channel_layout
129
+ # TODO Whenever support for ffmpeg/ffprobe 1.2.1 is dropped this is no longer needed
130
+ @audio_channel_layout || case(audio_channels)
131
+ when 1
132
+ 'stereo'
133
+ when 2
134
+ 'stereo'
135
+ when 6
136
+ '5.1'
137
+ else
138
+ 'unknown'
139
+ end
140
+ end
141
+
142
+ def transcode(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
143
+ Transcoder.new(self, output_file, options, transcoder_options).run &block
144
+ end
145
+
146
+ def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
147
+ Transcoder.new(self, output_file, options.merge(screenshot: true), transcoder_options).run &block
148
+ end
149
+
150
+ protected
151
+ def aspect_from_dar
152
+ return nil unless dar
153
+ w, h = dar.split(":")
154
+ aspect = w.to_f / h.to_f
155
+ aspect.zero? ? nil : aspect
156
+ end
157
+
158
+ def aspect_from_sar
159
+ return nil unless sar
160
+ w, h = sar.split(":")
161
+ aspect = w.to_f / h.to_f
162
+ aspect.zero? ? nil : aspect
163
+ end
164
+
165
+ def aspect_from_dimensions
166
+ aspect = width.to_f / height.to_f
167
+ aspect.nan? ? nil : aspect
168
+ end
169
+
170
+ def fix_encoding(output)
171
+ output[/test/] # Running a regexp on the string throws error if it's not UTF-8
172
+ rescue ArgumentError
173
+ output.force_encoding("ISO-8859-1")
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,130 @@
1
+ require 'open3'
2
+ require 'shellwords'
3
+
4
+ module FFMPEG
5
+ class Transcoder
6
+ @@timeout = 30
7
+
8
+ def self.timeout=(time)
9
+ @@timeout = time
10
+ end
11
+
12
+ def self.timeout
13
+ @@timeout
14
+ end
15
+
16
+ def initialize(movie, output_file, options = EncodingOptions.new, transcoder_options = {})
17
+ @movie = movie
18
+ @output_file = output_file
19
+
20
+ FFMPEG.logger.level = Logger::INFO
21
+
22
+ if options.is_a?(String) || options.is_a?(EncodingOptions)
23
+ @raw_options = options
24
+ elsif options.is_a?(Hash)
25
+ FFMPEG.logger.level = Logger::ERROR if options[:silent]
26
+ @raw_options = EncodingOptions.new(options)
27
+ else
28
+ raise ArgumentError, "Unknown options format '#{options.class}', should be either EncodingOptions, Hash or String."
29
+ end
30
+
31
+ @transcoder_options = transcoder_options
32
+ @errors = []
33
+
34
+ apply_transcoder_options
35
+ end
36
+
37
+ def run(&block)
38
+ transcode_movie(&block)
39
+ if @transcoder_options[:validate]
40
+ validate_output_file(&block)
41
+ return encoded
42
+ else
43
+ return nil
44
+ end
45
+ end
46
+
47
+ def encoding_succeeded?
48
+ @errors << "no output file created" and return false unless File.exist?(@output_file)
49
+ @errors << "encoded file is invalid" and return false unless encoded.valid?
50
+ true
51
+ end
52
+
53
+ def encoded
54
+ @encoded ||= Movie.new(@output_file)
55
+ end
56
+
57
+ private
58
+ # frame= 4855 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/
59
+ def transcode_movie
60
+ @command = "#{FFMPEG.ffmpeg_binary} -y -i #{Shellwords.escape(@movie.path)} #{@raw_options} #{Shellwords.escape(@output_file)}"
61
+ FFMPEG.logger.info("Running transcoding...\n#{@command}\n")
62
+ @output = ""
63
+
64
+ Open3.popen3(@command) do |_stdin, _stdout, stderr, wait_thr|
65
+ begin
66
+ yield(0.0) if block_given?
67
+ next_line = Proc.new do |line|
68
+ fix_encoding(line)
69
+ @output << line
70
+ if line.include?("time=")
71
+ if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
72
+ time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
73
+ else # better make sure it wont blow up in case of unexpected output
74
+ time = 0.0
75
+ end
76
+ progress = time / @movie.duration
77
+ yield(progress) if block_given?
78
+ end
79
+ end
80
+
81
+ if @@timeout
82
+ stderr.each_with_timeout(wait_thr.pid, @@timeout, 'size=', &next_line)
83
+ else
84
+ stderr.each('size=', &next_line)
85
+ end
86
+
87
+ rescue Timeout::Error => e
88
+ FFMPEG.logger.error "Process hung...\n@command\n#{@command}\nOutput\n#{@output}\n"
89
+ raise Error, "Process hung. Full output: #{@output}"
90
+ end
91
+ end
92
+ end
93
+
94
+ def validate_output_file(&block)
95
+ if encoding_succeeded?
96
+ yield(1.0) if block_given?
97
+ FFMPEG.logger.info "Transcoding of #{@movie.path} to #{@output_file} succeeded\n"
98
+ else
99
+ errors = "Errors: #{@errors.join(", ")}. "
100
+ FFMPEG.logger.error "Failed encoding...\n#{@command}\n\n#{@output}\n#{errors}\n"
101
+ raise Error, "Failed encoding.#{errors}Full output: #{@output}"
102
+ end
103
+ end
104
+
105
+ def apply_transcoder_options
106
+ # if true runs #validate_output_file
107
+ @transcoder_options[:validate] = @transcoder_options.fetch(:validate) { true }
108
+
109
+ return if @movie.calculated_aspect_ratio.nil?
110
+ case @transcoder_options[:preserve_aspect_ratio].to_s
111
+ when "width"
112
+ new_height = @raw_options.width / @movie.calculated_aspect_ratio
113
+ new_height = new_height.ceil.even? ? new_height.ceil : new_height.floor
114
+ new_height += 1 if new_height.odd? # needed if new_height ended up with no decimals in the first place
115
+ @raw_options[:resolution] = "#{@raw_options.width}x#{new_height}"
116
+ when "height"
117
+ new_width = @raw_options.height * @movie.calculated_aspect_ratio
118
+ new_width = new_width.ceil.even? ? new_width.ceil : new_width.floor
119
+ new_width += 1 if new_width.odd?
120
+ @raw_options[:resolution] = "#{new_width}x#{@raw_options.height}"
121
+ end
122
+ end
123
+
124
+ def fix_encoding(output)
125
+ output[/test/]
126
+ rescue ArgumentError
127
+ output.force_encoding("ISO-8859-1")
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,94 @@
1
+ require 'logger'
2
+ require 'stringio'
3
+ require 'byebug'
4
+
5
+ Proc.new do |this_dir|
6
+ %w(
7
+ errors
8
+ movie
9
+ io_monkey
10
+ transcoder
11
+ encoding_options
12
+ ).each do |lib_name|
13
+ require File.join(this_dir, 'ffmpeg', lib_name)
14
+ end
15
+ end.call(File.dirname(__FILE__))
16
+
17
+ module FFMPEG
18
+ # FFMPEG logs information about its progress when it's transcoding.
19
+ # Jack in your own logger through this method if you wish to.
20
+ #
21
+ # @param [Logger] log your own logger
22
+ # @return [Logger] the logger you set
23
+ def self.logger=(log)
24
+ @logger = log
25
+ end
26
+
27
+ # Get FFMPEG logger.
28
+ #
29
+ # @return [Logger]
30
+ def self.logger
31
+ return @logger if @logger
32
+ logger = Logger.new(STDOUT)
33
+ logger.level = Logger::INFO
34
+ @logger = logger
35
+ end
36
+
37
+ # Set the path of the ffmpeg binary.
38
+ # Can be useful if you need to specify a path such as /usr/local/bin/ffmpeg
39
+ #
40
+ # @param [String] path to the ffmpeg binary
41
+ # @return [String] the path you set
42
+ # @raise Errno::ENOENT if the ffmpeg binary cannot be found
43
+ def self.ffmpeg_binary=(bin)
44
+ if bin.is_a?(String) && !File.executable?(bin)
45
+ raise Errno::ENOENT, "the ffmpeg binary, \'#{bin}\', is not executable"
46
+ end
47
+ @ffmpeg_binary = bin
48
+ end
49
+
50
+ # Get the path to the ffmpeg binary, defaulting to 'ffmpeg'
51
+ #
52
+ # @return [String] the path to the ffmpeg binary
53
+ # @raise Errno::ENOENT if the ffmpeg binary cannot be found
54
+ def self.ffmpeg_binary
55
+ @ffmpeg_binary || which('ffmpeg')
56
+ end
57
+
58
+ # Get the path to the ffprobe binary, defaulting to what is on ENV['PATH']
59
+ #
60
+ # @return [String] the path to the ffprobe binary
61
+ # @raise Errno::ENOENT if the ffprobe binary cannot be found
62
+ def self.ffprobe_binary
63
+ @ffprobe_binary || which('ffprobe')
64
+ end
65
+
66
+ # Set the path of the ffprobe binary.
67
+ # Can be useful if you need to specify a path such as /usr/local/bin/ffprobe
68
+ #
69
+ # @param [String] path to the ffprobe binary
70
+ # @return [String] the path you set
71
+ # @raise Errno::ENOENT if the ffprobe binary cannot be found
72
+ def self.ffprobe_binary=(bin)
73
+ if bin.is_a?(String) && !File.executable?(bin)
74
+ raise Errno::ENOENT, "the ffprobe binary, \'#{bin}\', is not executable"
75
+ end
76
+ @ffprobe_binary = bin
77
+ end
78
+
79
+ # Cross-platform way of finding an executable in the $PATH.
80
+ #
81
+ # which('ruby') #=> /usr/bin/ruby
82
+ # see: http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
83
+ def self.which(cmd)
84
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
85
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
86
+ exts.each { |ext|
87
+ exe = File.join(path, "#{cmd}#{ext}")
88
+ return exe if File.executable? exe
89
+ }
90
+ end
91
+ raise Errno::ENOENT, "the #{cmd} binary could not be found in #{ENV['PATH']}"
92
+ end
93
+
94
+ end
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "powerand-ffmpeg"
3
+ s.version = "2.1.0"
4
+ s.date = "2016-03-05"
5
+ s.summary = "powerand FFMPEG"
6
+ s.description = %q{
7
+ Simple yet powerful wrapper around the ffmpeg command
8
+ for reading metadata and transcoding movies}
9
+ s.authors = ["Yudin Artur"]
10
+ s.email = ["artur.yudin.88@gmail.com"]
11
+ s.files = `git ls-files`.split($/)
12
+ s.homepage = "http://rubygems.org/gems/powerand-ffmpeg"
13
+ s.license = "MIT"
14
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: powerand-ffmpeg
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudin Artur
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2-
14
+
15
+ Simple yet powerful wrapper around the ffmpeg command
16
+ for reading metadata and transcoding movies
17
+ email:
18
+ - artur.yudin.88@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - CHANGELOG
24
+ - LICENSE
25
+ - README.md
26
+ - lib/ffmpeg/encoding_options.rb
27
+ - lib/ffmpeg/errors.rb
28
+ - lib/ffmpeg/io_monkey.rb
29
+ - lib/ffmpeg/movie.rb
30
+ - lib/ffmpeg/transcoder.rb
31
+ - lib/powerand-ffmpeg.rb
32
+ - powerand-ffmpeg.gemspec
33
+ homepage: http://rubygems.org/gems/powerand-ffmpeg
34
+ licenses:
35
+ - MIT
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.4.8
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: powerand FFMPEG
57
+ test_files: []