tivohmo-streamio-ffmpeg 2.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 +210 -0
- data/LICENSE +20 -0
- data/README.md +6 -0
- data/lib/ffmpeg/encoding_options.rb +164 -0
- data/lib/ffmpeg/errors.rb +4 -0
- data/lib/ffmpeg/io_monkey.rb +42 -0
- data/lib/ffmpeg/movie.rb +168 -0
- data/lib/ffmpeg/transcoder.rb +127 -0
- data/lib/ffmpeg/version.rb +3 -0
- data/lib/streamio-ffmpeg.rb +90 -0
- data/lib/tivohmo-streamio-ffmpeg.rb +1 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b496d2d98d1135036fd67e133db072313c9b1eee
|
4
|
+
data.tar.gz: 503db0a5d379c21887857d64ee9609d276308ddd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c1d537bbb5873762cad2564f1b8eb5f3f6a9c41fee65df16fea3bfd759e8d554e5f495578f60d8bd67363fb6af9e8fe5772f7fbd5ac9b686e50eb241d9128381
|
7
|
+
data.tar.gz: edc7d421a80f227ff331066926c7e0aab0bffe3b7f263b29b73e79126ae0707b1e0413d527d9fef115f3a876aba86b0f6e7b92db49a37520487265a0fb3a37c2
|
data/CHANGELOG
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
== Master
|
2
|
+
|
3
|
+
New:
|
4
|
+
* Support watermarking (thanks smoothdvd)
|
5
|
+
|
6
|
+
Improvements:
|
7
|
+
* Allow parenthesis in colorspace (thanks walterdavis for initial code and rociiu for finding a bug with it)
|
8
|
+
|
9
|
+
== 1.0.0 2013-07-08
|
10
|
+
|
11
|
+
New:
|
12
|
+
* Bumped target ffmpeg version to 1.2.1
|
13
|
+
|
14
|
+
Improvements:
|
15
|
+
* Simpler implementation for timeouts.
|
16
|
+
Should be far less cpu and memory dependent (don't spawn a thread for every line of output)
|
17
|
+
Timeout spec now passes in Rubinius (using 1.9 mode)
|
18
|
+
* Give helpful error message for windows users lacking the win32-process gem (thanks casoetan)
|
19
|
+
* Add Movie#container (thanks vitalis)
|
20
|
+
* Support vprofile and preset encoding options (thanks vitalis)
|
21
|
+
|
22
|
+
Changes:
|
23
|
+
* Default timeout lowered to 30 seconds
|
24
|
+
|
25
|
+
Bugs:
|
26
|
+
* Avoid crash if asking for frame_rate of a video without video stream (thanks squidarth)
|
27
|
+
* Fix crash when doing audio transcoding on ffmpeg >= 1.0.1 (thanks vitalis)
|
28
|
+
|
29
|
+
Deprecations:
|
30
|
+
* Removed support for Ruby 1.8
|
31
|
+
* Removed support for ffmpeg 0.7
|
32
|
+
|
33
|
+
Refactorings:
|
34
|
+
* Quite a few, see commit history for details.
|
35
|
+
|
36
|
+
== 0.9.0 2012-07-24
|
37
|
+
|
38
|
+
New:
|
39
|
+
* Bumped target ffmpeg version to 0.11.1
|
40
|
+
* Add hung process detection with configurable timeout (thanks stakach)
|
41
|
+
* Raise FFMPEG::Error instead of generic RuntimeError on failed transcodings
|
42
|
+
* Movie#screenshot for more intuitive screenshotting (README has details)
|
43
|
+
* Movie#creation_time and Movie#rotation attributes when metadata is available (thanks Innonate)
|
44
|
+
|
45
|
+
Bugs:
|
46
|
+
* Fixed too many open files bug (thanks to akicho8)
|
47
|
+
* Fixed missing path escaping (thanks to mikesager)
|
48
|
+
* Fixed README typo (thanks to Linutux)
|
49
|
+
* Files outputing "could not find codec parameters" are now recognized as invalid
|
50
|
+
|
51
|
+
Deprecations:
|
52
|
+
* Removed Movie#uncertain_duration?
|
53
|
+
* Removed all the deprecated crop options (use :custom => '-vf crop=x:x:x:x' if you need it)
|
54
|
+
|
55
|
+
Refactorings:
|
56
|
+
* Removed the deprecated duration validation code
|
57
|
+
* Polish on the transcoder class
|
58
|
+
* Polish on the spec suite
|
59
|
+
|
60
|
+
== 0.8.5 2011-03-05
|
61
|
+
|
62
|
+
* If a clip has a DAR that doesn't make sense fall back to calculating aspect ratio from dimensions
|
63
|
+
* Allow filenames with single quote characters (thanks to youpy)
|
64
|
+
|
65
|
+
== 0.8.4 2011-11-30
|
66
|
+
|
67
|
+
* Duration now one decimal more accurate (thanks to Russel Brooks)
|
68
|
+
* Added encoding option seek_time (thanks to Misty De Meo)
|
69
|
+
|
70
|
+
== 0.8.3 2011-09-01
|
71
|
+
|
72
|
+
* Parameters now come in the order of codecs, presets, others so that we can override the presets
|
73
|
+
* Added encoding option keyframe_interval to set number of frames between i-frames (aka GOP size)
|
74
|
+
* Streamio (sponsor of this project) have launched new awesome pricing @ http://streamio.com
|
75
|
+
|
76
|
+
== 0.8.2 2011-08-19
|
77
|
+
|
78
|
+
* Path to ffmpeg binary can now be specified (thanks jonathandean)
|
79
|
+
* If ffmpeg output contains "is not supported" the Movie will be considered invalid
|
80
|
+
|
81
|
+
== 0.8.1 2011-07-28
|
82
|
+
|
83
|
+
* Fix progress yielding with ffmpeg 0.8
|
84
|
+
* Updated specs to pass with ffmpeg 0.8
|
85
|
+
|
86
|
+
== 0.8.0 2011-05-26
|
87
|
+
|
88
|
+
* Duration is now ALWAYS considered uncertain (we've noticed that ffmpeg is not always correct)
|
89
|
+
* This means that the duration check will normally never run (unless you manually hack @uncertain_duration to false)
|
90
|
+
* Movie#audio_channels now returns nil if there is no audio stream (instead of crashing)
|
91
|
+
* Development: Use Bundler
|
92
|
+
* Development: Update RSpec to 2.6
|
93
|
+
|
94
|
+
== 0.7.8 2011-04-04
|
95
|
+
|
96
|
+
* Fixed number of audio channels on files with 5.1 audio
|
97
|
+
|
98
|
+
== 0.7.7 2011-02-01
|
99
|
+
|
100
|
+
* Movies with starttime are now considered as having uncertain duration as its behavior is not consistent across formats
|
101
|
+
* Upgrade development environment to RSpec 2.4
|
102
|
+
|
103
|
+
== 0.7.6 2011-01-14
|
104
|
+
|
105
|
+
* Another ruby 1.9 encoding fix
|
106
|
+
|
107
|
+
== 0.7.5 2011-01-14
|
108
|
+
|
109
|
+
* Fixed some ruby 1.9 issues
|
110
|
+
* Added Movie#video_bitrate and Movie#audio_bitrate (thanks to mbj)
|
111
|
+
|
112
|
+
== 0.7.4 2010-12-07
|
113
|
+
|
114
|
+
* Fixed broken duration on movies with start times over 0 by reducing duration with start-time
|
115
|
+
|
116
|
+
== 0.7.3 2010-08-26
|
117
|
+
|
118
|
+
* Replaced Jewler with simple dynamic gemspec file
|
119
|
+
* Spec files now not in published gem to make it a lot smaller in size
|
120
|
+
* Full output from ffmpeg command in error raised during transcoding
|
121
|
+
|
122
|
+
== 0.7.2 2010-08-11
|
123
|
+
|
124
|
+
* Added encoding option duration
|
125
|
+
* Avoid crashing when ffmpeg can't find resolution of a movie
|
126
|
+
|
127
|
+
== 0.7.1 2010-07-08
|
128
|
+
|
129
|
+
* Make sure preset parameters are always put last to avoid them ending up before any codec assignments
|
130
|
+
* Testing against a fresh ffmpeg build (r24069)
|
131
|
+
|
132
|
+
== 0.7.0 2010-07-07
|
133
|
+
|
134
|
+
* Support for ffpresets through video_preset, audio_preset and file_preset encoding options
|
135
|
+
* Added encoding option video_bitrate_tolerance
|
136
|
+
|
137
|
+
== 0.6.8.1 2010-07-06
|
138
|
+
|
139
|
+
* Bugfix - aspect ratio was not calculated properly on movies with no DAR
|
140
|
+
|
141
|
+
== 0.6.8 2010-07-06
|
142
|
+
|
143
|
+
* Don't use encoding options with nil values
|
144
|
+
* Added encoding options video_max_bitrate, video_min_bitrate and buffer_size for constant bitrate encoding
|
145
|
+
|
146
|
+
== 0.6.7 2010-06-10
|
147
|
+
|
148
|
+
* Bugfix - aspect ratio preserver could suggest non even resolutions in certain circumstances
|
149
|
+
|
150
|
+
== 0.6.6 2010-06-10
|
151
|
+
|
152
|
+
* Transcodings to .jpg and .png will now work as they will skip duration validation
|
153
|
+
|
154
|
+
== 0.6.5 2010-05-19
|
155
|
+
|
156
|
+
* Movie#size method to get file size.
|
157
|
+
|
158
|
+
== 0.6.4 2010-05-12
|
159
|
+
|
160
|
+
* Ruby 1.9 compatibility fix for EncodingOptions (thanks michalf!)
|
161
|
+
|
162
|
+
== 0.6.3 2010-05-05
|
163
|
+
|
164
|
+
* Use DAR to calculate aspect ratio if available
|
165
|
+
|
166
|
+
== 0.6.2 2010-05-05
|
167
|
+
|
168
|
+
* Added Movie#uncertain_duration? which is true if ffmpeg is guessing duration from bitrate
|
169
|
+
* Skipping the transcoders duration validation if original file has uncertain duration
|
170
|
+
* Made sure aspect ratio preservation always rounds new size to an even number to avoid "not divisible by 2" errors
|
171
|
+
* Changed Movie#valid? logic to accept any movie with either a readable audio or video stream
|
172
|
+
|
173
|
+
== 0.6.0 2010-05-04
|
174
|
+
|
175
|
+
* Cropping options now handled by EncodingOptions (croptop, cropbottom, cropleft and cropright)
|
176
|
+
* Aspect ratio parameter calculated and added by default
|
177
|
+
* Added transcoder options to preserve original aspect ratio on width or height
|
178
|
+
|
179
|
+
== 0.5.0 2010-04-28
|
180
|
+
|
181
|
+
* Added logging capabilities
|
182
|
+
|
183
|
+
== 0.4.3 2010-04-06
|
184
|
+
|
185
|
+
* Correctly identify invalid movies on latest ffmpeg build (r22811)
|
186
|
+
|
187
|
+
== 0.4.2 2010-04-06
|
188
|
+
|
189
|
+
* Escape the path to handle spaces in filenames and avoid CLI injection attacks (thanks J. Weir!)
|
190
|
+
|
191
|
+
== 0.4.1 2010-02-10
|
192
|
+
|
193
|
+
* Forgot to change the transcoding shortcut from Movie
|
194
|
+
|
195
|
+
== 0.4.0 2010-02-10
|
196
|
+
|
197
|
+
* Transcoding API changed to make use of more humanly readable options (see README for examples)
|
198
|
+
* Fixed frame rate parsing for integer frame rates
|
199
|
+
|
200
|
+
== 0.3.0 2010-02-07
|
201
|
+
|
202
|
+
* Simple transcoding
|
203
|
+
|
204
|
+
== 0.2.0 2010-02-06
|
205
|
+
|
206
|
+
* Some more metadata parsing
|
207
|
+
|
208
|
+
== 0.1.0 2010-02-05
|
209
|
+
|
210
|
+
* Some basic parsing of metadata added
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Streamio 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,6 @@
|
|
1
|
+
TivoHMO FFMPEG
|
2
|
+
===============
|
3
|
+
|
4
|
+
A fork of [streamio-ffmpeg](https://github.com/streamio/streamio-ffmpeg) including [PR73](https://github.com/streamio/streamio-ffmpeg/pull/73) by RLovelett to allow it to work with ffmoeg 2.4.3. Forked to allow releasing to rubygems for use as a dependency in the [tivohmo gem](https://github.com/wr0ngway/tivohmo).
|
5
|
+
|
6
|
+
See the [streamio-ffmpeg readme](https://github.com/streamio/streamio-ffmpeg/blob/master/README.md) for more details.
|
@@ -0,0 +1,164 @@
|
|
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
|
+
other = params - codecs - presets
|
17
|
+
params = codecs + presets + other
|
18
|
+
|
19
|
+
params_string = params.join(" ")
|
20
|
+
params_string << " #{convert_aspect(calculate_aspect)}" if calculate_aspect?
|
21
|
+
params_string
|
22
|
+
end
|
23
|
+
|
24
|
+
def width
|
25
|
+
self[:resolution].split("x").first.to_i rescue nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def height
|
29
|
+
self[:resolution].split("x").last.to_i rescue nil
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def supports_option?(option)
|
34
|
+
option = RUBY_VERSION < "1.9" ? "convert_#{option}" : "convert_#{option}".to_sym
|
35
|
+
private_methods.include?(option)
|
36
|
+
end
|
37
|
+
|
38
|
+
def convert_aspect(value)
|
39
|
+
"-aspect #{value}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def calculate_aspect
|
43
|
+
width, height = self[:resolution].split("x")
|
44
|
+
width.to_f / height.to_f
|
45
|
+
end
|
46
|
+
|
47
|
+
def calculate_aspect?
|
48
|
+
self[:aspect].nil? && self[:resolution]
|
49
|
+
end
|
50
|
+
|
51
|
+
def convert_video_codec(value)
|
52
|
+
"-vcodec #{value}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def convert_frame_rate(value)
|
56
|
+
"-r #{value}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def convert_resolution(value)
|
60
|
+
"-s #{value}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def convert_video_bitrate(value)
|
64
|
+
"-b:v #{k_format(value)}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def convert_audio_codec(value)
|
68
|
+
"-acodec #{value}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def convert_audio_bitrate(value)
|
72
|
+
"-b:a #{k_format(value)}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def convert_audio_sample_rate(value)
|
76
|
+
"-ar #{value}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def convert_audio_channels(value)
|
80
|
+
"-ac #{value}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def convert_video_max_bitrate(value)
|
84
|
+
"-maxrate #{k_format(value)}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def convert_video_min_bitrate(value)
|
88
|
+
"-minrate #{k_format(value)}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def convert_buffer_size(value)
|
92
|
+
"-bufsize #{k_format(value)}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def convert_video_bitrate_tolerance(value)
|
96
|
+
"-bt #{k_format(value)}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def convert_threads(value)
|
100
|
+
"-threads #{value}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def convert_duration(value)
|
104
|
+
"-t #{value}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def convert_video_preset(value)
|
108
|
+
"-vpre #{value}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def convert_audio_preset(value)
|
112
|
+
"-apre #{value}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def convert_file_preset(value)
|
116
|
+
"-fpre #{value}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def convert_keyframe_interval(value)
|
120
|
+
"-g #{value}"
|
121
|
+
end
|
122
|
+
|
123
|
+
def convert_seek_time(value)
|
124
|
+
"-ss #{value}"
|
125
|
+
end
|
126
|
+
|
127
|
+
def convert_screenshot(value)
|
128
|
+
value ? "-vframes 1 -f image2" : ""
|
129
|
+
end
|
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
|
+
|
139
|
+
def convert_watermark(value)
|
140
|
+
"-i #{value}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def convert_watermark_filter(value)
|
144
|
+
case value[:position].to_s
|
145
|
+
when "LT"
|
146
|
+
"-filter_complex 'scale=#{self[:resolution]},overlay=x=#{value[:padding_x]}:y=#{value[:padding_y]}'"
|
147
|
+
when "RT"
|
148
|
+
"-filter_complex 'scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{value[:padding_x]}:y=#{value[:padding_y]}'"
|
149
|
+
when "LB"
|
150
|
+
"-filter_complex 'scale=#{self[:resolution]},overlay=x=#{value[:padding_x]}:y=main_h-overlay_h-#{value[:padding_y]}'"
|
151
|
+
when "RB"
|
152
|
+
"-filter_complex 'scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{value[:padding_x]}:y=main_h-overlay_h-#{value[:padding_y]}'"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def convert_custom(value)
|
157
|
+
value
|
158
|
+
end
|
159
|
+
|
160
|
+
def k_format(value)
|
161
|
+
value.to_s.include?("k") ? value : "#{value}k"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
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
|
data/lib/ffmpeg/movie.rb
ADDED
@@ -0,0 +1,168 @@
|
|
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.exists?(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 resolution
|
103
|
+
unless width.nil? or height.nil?
|
104
|
+
"#{width}x#{height}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def calculated_aspect_ratio
|
109
|
+
aspect_from_dar || aspect_from_dimensions
|
110
|
+
end
|
111
|
+
|
112
|
+
def calculated_pixel_aspect_ratio
|
113
|
+
aspect_from_sar || 1
|
114
|
+
end
|
115
|
+
|
116
|
+
def size
|
117
|
+
File.size(@path)
|
118
|
+
end
|
119
|
+
|
120
|
+
def audio_channel_layout
|
121
|
+
# TODO Whenever support for ffmpeg/ffprobe 1.2.1 is dropped this is no longer needed
|
122
|
+
@audio_channel_layout || case(audio_channels)
|
123
|
+
when 1
|
124
|
+
'stereo'
|
125
|
+
when 2
|
126
|
+
'stereo'
|
127
|
+
when 6
|
128
|
+
'5.1'
|
129
|
+
else
|
130
|
+
'unknown'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def transcode(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
|
135
|
+
Transcoder.new(self, output_file, options, transcoder_options).run &block
|
136
|
+
end
|
137
|
+
|
138
|
+
def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
|
139
|
+
Transcoder.new(self, output_file, options.merge(screenshot: true), transcoder_options).run &block
|
140
|
+
end
|
141
|
+
|
142
|
+
protected
|
143
|
+
def aspect_from_dar
|
144
|
+
return nil unless dar
|
145
|
+
w, h = dar.split(":")
|
146
|
+
aspect = w.to_f / h.to_f
|
147
|
+
aspect.zero? ? nil : aspect
|
148
|
+
end
|
149
|
+
|
150
|
+
def aspect_from_sar
|
151
|
+
return nil unless sar
|
152
|
+
w, h = sar.split(":")
|
153
|
+
aspect = w.to_f / h.to_f
|
154
|
+
aspect.zero? ? nil : aspect
|
155
|
+
end
|
156
|
+
|
157
|
+
def aspect_from_dimensions
|
158
|
+
aspect = width.to_f / height.to_f
|
159
|
+
aspect.nan? ? nil : aspect
|
160
|
+
end
|
161
|
+
|
162
|
+
def fix_encoding(output)
|
163
|
+
output[/test/] # Running a regexp on the string throws error if it's not UTF-8
|
164
|
+
rescue ArgumentError
|
165
|
+
output.force_encoding("ISO-8859-1")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,127 @@
|
|
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
|
+
if options.is_a?(String) || options.is_a?(EncodingOptions)
|
21
|
+
@raw_options = options
|
22
|
+
elsif options.is_a?(Hash)
|
23
|
+
@raw_options = EncodingOptions.new(options)
|
24
|
+
else
|
25
|
+
raise ArgumentError, "Unknown options format '#{options.class}', should be either EncodingOptions, Hash or String."
|
26
|
+
end
|
27
|
+
|
28
|
+
@transcoder_options = transcoder_options
|
29
|
+
@errors = []
|
30
|
+
|
31
|
+
apply_transcoder_options
|
32
|
+
end
|
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|
|
62
|
+
begin
|
63
|
+
yield(0.0) if block_given?
|
64
|
+
next_line = Proc.new do |line|
|
65
|
+
fix_encoding(line)
|
66
|
+
@output << line
|
67
|
+
if line.include?("time=")
|
68
|
+
if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
|
69
|
+
time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
|
70
|
+
else # better make sure it wont blow up in case of unexpected output
|
71
|
+
time = 0.0
|
72
|
+
end
|
73
|
+
progress = time / @movie.duration
|
74
|
+
yield(progress) if block_given?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if @@timeout
|
79
|
+
stderr.each_with_timeout(wait_thr.pid, @@timeout, 'size=', &next_line)
|
80
|
+
else
|
81
|
+
stderr.each('size=', &next_line)
|
82
|
+
end
|
83
|
+
|
84
|
+
rescue Timeout::Error => e
|
85
|
+
FFMPEG.logger.error "Process hung...\n@command\n#{@command}\nOutput\n#{@output}\n"
|
86
|
+
raise Error, "Process hung. Full output: #{@output}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_output_file(&block)
|
92
|
+
if encoding_succeeded?
|
93
|
+
yield(1.0) if block_given?
|
94
|
+
FFMPEG.logger.info "Transcoding of #{@movie.path} to #{@output_file} succeeded\n"
|
95
|
+
else
|
96
|
+
errors = "Errors: #{@errors.join(", ")}. "
|
97
|
+
FFMPEG.logger.error "Failed encoding...\n#{@command}\n\n#{@output}\n#{errors}\n"
|
98
|
+
raise Error, "Failed encoding.#{errors}Full output: #{@output}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def apply_transcoder_options
|
103
|
+
# if true runs #validate_output_file
|
104
|
+
@transcoder_options[:validate] = @transcoder_options.fetch(:validate) { true }
|
105
|
+
|
106
|
+
return if @movie.calculated_aspect_ratio.nil?
|
107
|
+
case @transcoder_options[:preserve_aspect_ratio].to_s
|
108
|
+
when "width"
|
109
|
+
new_height = @raw_options.width / @movie.calculated_aspect_ratio
|
110
|
+
new_height = new_height.ceil.even? ? new_height.ceil : new_height.floor
|
111
|
+
new_height += 1 if new_height.odd? # needed if new_height ended up with no decimals in the first place
|
112
|
+
@raw_options[:resolution] = "#{@raw_options.width}x#{new_height}"
|
113
|
+
when "height"
|
114
|
+
new_width = @raw_options.height * @movie.calculated_aspect_ratio
|
115
|
+
new_width = new_width.ceil.even? ? new_width.ceil : new_width.floor
|
116
|
+
new_width += 1 if new_width.odd?
|
117
|
+
@raw_options[:resolution] = "#{new_width}x#{@raw_options.height}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def fix_encoding(output)
|
122
|
+
output[/test/]
|
123
|
+
rescue ArgumentError
|
124
|
+
output.force_encoding("ISO-8859-1")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
require 'ffmpeg/version'
|
7
|
+
require 'ffmpeg/errors'
|
8
|
+
require 'ffmpeg/movie'
|
9
|
+
require 'ffmpeg/io_monkey'
|
10
|
+
require 'ffmpeg/transcoder'
|
11
|
+
require 'ffmpeg/encoding_options'
|
12
|
+
|
13
|
+
module FFMPEG
|
14
|
+
# FFMPEG logs information about its progress when it's transcoding.
|
15
|
+
# Jack in your own logger through this method if you wish to.
|
16
|
+
#
|
17
|
+
# @param [Logger] log your own logger
|
18
|
+
# @return [Logger] the logger you set
|
19
|
+
def self.logger=(log)
|
20
|
+
@logger = log
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get FFMPEG logger.
|
24
|
+
#
|
25
|
+
# @return [Logger]
|
26
|
+
def self.logger
|
27
|
+
return @logger if @logger
|
28
|
+
logger = Logger.new(STDOUT)
|
29
|
+
logger.level = Logger::INFO
|
30
|
+
@logger = logger
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the path of the ffmpeg binary.
|
34
|
+
# Can be useful if you need to specify a path such as /usr/local/bin/ffmpeg
|
35
|
+
#
|
36
|
+
# @param [String] path to the ffmpeg binary
|
37
|
+
# @return [String] the path you set
|
38
|
+
# @raise Errno::ENOENT if the ffmpeg binary cannot be found
|
39
|
+
def self.ffmpeg_binary=(bin)
|
40
|
+
if bin.is_a?(String) && !File.executable?(bin)
|
41
|
+
raise Errno::ENOENT, "the ffmpeg binary, \'#{bin}\', is not executable"
|
42
|
+
end
|
43
|
+
@ffmpeg_binary = bin
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get the path to the ffmpeg binary, defaulting to 'ffmpeg'
|
47
|
+
#
|
48
|
+
# @return [String] the path to the ffmpeg binary
|
49
|
+
# @raise Errno::ENOENT if the ffmpeg binary cannot be found
|
50
|
+
def self.ffmpeg_binary
|
51
|
+
@ffmpeg_binary || which('ffmpeg')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get the path to the ffprobe binary, defaulting to what is on ENV['PATH']
|
55
|
+
#
|
56
|
+
# @return [String] the path to the ffprobe binary
|
57
|
+
# @raise Errno::ENOENT if the ffprobe binary cannot be found
|
58
|
+
def self.ffprobe_binary
|
59
|
+
@ffprobe_binary || which('ffprobe')
|
60
|
+
end
|
61
|
+
|
62
|
+
# Set the path of the ffprobe binary.
|
63
|
+
# Can be useful if you need to specify a path such as /usr/local/bin/ffprobe
|
64
|
+
#
|
65
|
+
# @param [String] path to the ffprobe binary
|
66
|
+
# @return [String] the path you set
|
67
|
+
# @raise Errno::ENOENT if the ffprobe binary cannot be found
|
68
|
+
def self.ffprobe_binary=(bin)
|
69
|
+
if bin.is_a?(String) && !File.executable?(bin)
|
70
|
+
raise Errno::ENOENT, "the ffprobe binary, \'#{bin}\', is not executable"
|
71
|
+
end
|
72
|
+
@ffprobe_binary = bin
|
73
|
+
end
|
74
|
+
|
75
|
+
# Cross-platform way of finding an executable in the $PATH.
|
76
|
+
#
|
77
|
+
# which('ruby') #=> /usr/bin/ruby
|
78
|
+
# see: http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
|
79
|
+
def self.which(cmd)
|
80
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
81
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
82
|
+
exts.each { |ext|
|
83
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
84
|
+
return exe if File.executable? exe
|
85
|
+
}
|
86
|
+
end
|
87
|
+
raise Errno::ENOENT, "the #{cmd} binary could not be found in #{ENV['PATH']}"
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'streamio-ffmpeg'
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tivohmo-streamio-ffmpeg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Backeus
|
8
|
+
- Ryan Lovelett
|
9
|
+
- Matt Conway
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2014-11-23 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: multi_json
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - "~>"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.8'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - "~>"
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '1.8'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: rspec
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - "~>"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '2.14'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '2.14'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: rake
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '10.1'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - "~>"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '10.1'
|
57
|
+
description:
|
58
|
+
email:
|
59
|
+
- david@streamio.com
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- CHANGELOG
|
65
|
+
- LICENSE
|
66
|
+
- README.md
|
67
|
+
- lib/ffmpeg/encoding_options.rb
|
68
|
+
- lib/ffmpeg/errors.rb
|
69
|
+
- lib/ffmpeg/io_monkey.rb
|
70
|
+
- lib/ffmpeg/movie.rb
|
71
|
+
- lib/ffmpeg/transcoder.rb
|
72
|
+
- lib/ffmpeg/version.rb
|
73
|
+
- lib/streamio-ffmpeg.rb
|
74
|
+
- lib/tivohmo-streamio-ffmpeg.rb
|
75
|
+
homepage: http://github.com/wr0ngway/tivohmo-streamio-ffmpeg
|
76
|
+
licenses: []
|
77
|
+
metadata: {}
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 2.2.2
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: Forked from streamio-ffmpeg for use by tivohmo gem
|
98
|
+
test_files: []
|