d-streamio-ffmpeg 3.0.3

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.
@@ -0,0 +1,200 @@
1
+ module FFMPEG
2
+ class EncodingOptions < Hash
3
+ def initialize(options = {})
4
+ merge!(options)
5
+ end
6
+
7
+ def params_order(k)
8
+ if k =~ /watermark$/
9
+ 0
10
+ elsif k =~ /watermark/
11
+ 1
12
+ elsif k =~ /codec/
13
+ 2
14
+ elsif k =~ /preset/
15
+ 3
16
+ else
17
+ 4
18
+ end
19
+ end
20
+
21
+ def to_a
22
+ params = []
23
+
24
+ # codecs should go before the presets so that the files will be matched successfully
25
+ # all other parameters go after so that we can override whatever is in the preset
26
+ keys.sort_by{|k| params_order(k) }.each do |key|
27
+
28
+ value = self[key]
29
+ a = send("convert_#{key}", value) if value && supports_option?(key)
30
+ params += a unless a.nil?
31
+ end
32
+
33
+ params += convert_aspect(calculate_aspect) if calculate_aspect?
34
+ params.map(&:to_s)
35
+ end
36
+
37
+ def width
38
+ self[:resolution].split("x").first.to_i rescue nil
39
+ end
40
+
41
+ def height
42
+ self[:resolution].split("x").last.to_i rescue nil
43
+ end
44
+
45
+ private
46
+ def supports_option?(option)
47
+ option = RUBY_VERSION < "1.9" ? "convert_#{option}" : "convert_#{option}".to_sym
48
+ private_methods.include?(option)
49
+ end
50
+
51
+ def convert_aspect(value)
52
+ ["-aspect", value]
53
+ end
54
+
55
+ def calculate_aspect
56
+ width, height = self[:resolution].split("x")
57
+ width.to_f / height.to_f
58
+ end
59
+
60
+ def calculate_aspect?
61
+ self[:aspect].nil? && self[:resolution]
62
+ end
63
+
64
+ def convert_video_codec(value)
65
+ ["-vcodec", value]
66
+ end
67
+
68
+ def convert_frame_rate(value)
69
+ ["-r", value]
70
+ end
71
+
72
+ def convert_resolution(value)
73
+ ["-s", value]
74
+ end
75
+
76
+ def convert_video_bitrate(value)
77
+ ["-b:v", k_format(value)]
78
+ end
79
+
80
+ def convert_audio_codec(value)
81
+ ["-acodec", value]
82
+ end
83
+
84
+ def convert_audio_bitrate(value)
85
+ ["-b:a", k_format(value)]
86
+ end
87
+
88
+ def convert_audio_sample_rate(value)
89
+ ["-ar", value]
90
+ end
91
+
92
+ def convert_audio_channels(value)
93
+ ["-ac", value]
94
+ end
95
+
96
+ def convert_video_max_bitrate(value)
97
+ ["-maxrate", k_format(value)]
98
+ end
99
+
100
+ def convert_video_min_bitrate(value)
101
+ ["-minrate", k_format(value)]
102
+ end
103
+
104
+ def convert_buffer_size(value)
105
+ ["-bufsize", k_format(value)]
106
+ end
107
+
108
+ def convert_video_bitrate_tolerance(value)
109
+ ["-bt", k_format(value)]
110
+ end
111
+
112
+ def convert_threads(value)
113
+ ["-threads", value]
114
+ end
115
+
116
+ def convert_target(value)
117
+ ['-target', value]
118
+ end
119
+
120
+ def convert_duration(value)
121
+ ["-t", value]
122
+ end
123
+
124
+ def convert_video_preset(value)
125
+ ["-vpre", value]
126
+ end
127
+
128
+ def convert_audio_preset(value)
129
+ ["-apre", value]
130
+ end
131
+
132
+ def convert_file_preset(value)
133
+ ["-fpre", value]
134
+ end
135
+
136
+ def convert_keyframe_interval(value)
137
+ ["-g", value]
138
+ end
139
+
140
+ def convert_seek_time(value)
141
+ ["-ss", value]
142
+ end
143
+
144
+ def convert_screenshot(value)
145
+ result = []
146
+ unless self[:vframes]
147
+ result << '-vframes'
148
+ result << 1
149
+ end
150
+ result << '-f'
151
+ result << 'image2'
152
+ value ? result : []
153
+ end
154
+
155
+ def convert_quality(value)
156
+ ['-q:v', value]
157
+ end
158
+
159
+ def convert_vframes(value)
160
+ ['-vframes', value]
161
+ end
162
+
163
+ def convert_x264_vprofile(value)
164
+ ["-vprofile", value]
165
+ end
166
+
167
+ def convert_x264_preset(value)
168
+ ["-preset", value]
169
+ end
170
+
171
+ def convert_watermark(value)
172
+ ["-i", value]
173
+ end
174
+
175
+ def convert_watermark_filter(value)
176
+ position = value[:position]
177
+ padding_x = value[:padding_x] || 10
178
+ padding_y = value[:padding_y] || 10
179
+ case position.to_s
180
+ when "LT"
181
+ ["-filter_complex", "scale=#{self[:resolution]},overlay=x=#{padding_x}:y=#{padding_y}"]
182
+ when "RT"
183
+ ["-filter_complex", "scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{padding_x}:y=#{padding_y}"]
184
+ when "LB"
185
+ ["-filter_complex", "scale=#{self[:resolution]},overlay=x=#{padding_x}:y=main_h-overlay_h-#{padding_y}"]
186
+ when "RB"
187
+ ["-filter_complex", "scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{padding_x}:y=main_h-overlay_h-#{padding_y}"]
188
+ end
189
+ end
190
+
191
+ def convert_custom(value)
192
+ raise ArgumentError unless value.class <= Array
193
+ value
194
+ end
195
+
196
+ def k_format(value)
197
+ value.to_s.include?("k") ? value : "#{value}k"
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,6 @@
1
+ module FFMPEG
2
+ class Error < StandardError
3
+ end
4
+ class HTTPTooManyRequests < StandardError
5
+ end
6
+ 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,257 @@
1
+ require 'time'
2
+ require 'multi_json'
3
+ require 'uri'
4
+
5
+ module FFMPEG
6
+ class Movie
7
+ attr_reader :path, :duration, :time, :bitrate, :rotation, :creation_time
8
+ attr_reader :video_stream, :video_codec, :video_bitrate, :colorspace, :width, :height, :sar, :dar, :frame_rate
9
+ attr_reader :audio_streams, :audio_stream, :audio_codec, :audio_bitrate, :audio_sample_rate, :audio_channels, :audio_tags, :audio_sample_fmt
10
+ attr_reader :container
11
+ attr_reader :metadata, :format_tags
12
+
13
+ UNSUPPORTED_CODEC_PATTERN = /^Unsupported codec with id (\d+) for input stream (\d+)$/
14
+
15
+ def initialize(path)
16
+ @path = path
17
+
18
+ if remote?
19
+ @head = head
20
+ raise Errno::ENOENT, "the URL '#{path}' does not exist" unless @head.is_a?(Net::HTTPSuccess)
21
+ else
22
+ raise Errno::ENOENT, "the file '#{path}' does not exist" unless File.exist?(path)
23
+ end
24
+
25
+ @path = path
26
+
27
+ # ffmpeg will output to stderr
28
+ command = [FFMPEG.ffprobe_binary, '-i', path, *%w(-print_format json -show_format -show_streams -show_error)]
29
+ std_output = ''
30
+ std_error = ''
31
+
32
+ Open3.popen3(*command) do |stdin, stdout, stderr|
33
+ std_output = stdout.read unless stdout.nil?
34
+ std_error = stderr.read unless stderr.nil?
35
+ end
36
+
37
+ fix_encoding(std_output)
38
+ fix_encoding(std_error)
39
+
40
+ begin
41
+ @metadata = MultiJson.load(std_output, symbolize_keys: true)
42
+ rescue MultiJson::ParseError
43
+ raise "Could not parse output from FFProbe:\n#{ std_output }"
44
+ end
45
+
46
+ if @metadata.key?(:error)
47
+
48
+ @duration = 0
49
+
50
+ else
51
+ video_streams = @metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'video' }
52
+ audio_streams = @metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'audio' }
53
+
54
+ @container = @metadata[:format][:format_name]
55
+
56
+ @duration = @metadata[:format][:duration].to_f
57
+
58
+ @time = @metadata[:format][:start_time].to_f
59
+
60
+ @format_tags = @metadata[:format][:tags]
61
+
62
+ @creation_time = if @format_tags and @format_tags.key?(:creation_time)
63
+ begin
64
+ Time.parse(@format_tags[:creation_time])
65
+ rescue ArgumentError
66
+ nil
67
+ end
68
+ else
69
+ nil
70
+ end
71
+
72
+ @bitrate = @metadata[:format][:bit_rate].to_i
73
+
74
+ # TODO: Handle multiple video codecs (is that possible?)
75
+ video_stream = video_streams.first
76
+ unless video_stream.nil?
77
+ @video_codec = video_stream[:codec_name]
78
+ @colorspace = video_stream[:pix_fmt]
79
+ @width = video_stream[:width]
80
+ @height = video_stream[:height]
81
+ @video_bitrate = video_stream[:bit_rate].to_i
82
+ @sar = video_stream[:sample_aspect_ratio]
83
+ @dar = video_stream[:display_aspect_ratio]
84
+
85
+ @frame_rate = unless video_stream[:avg_frame_rate] == '0/0'
86
+ Rational(video_stream[:avg_frame_rate])
87
+ else
88
+ nil
89
+ end
90
+
91
+ @video_stream = "#{video_stream[:codec_name]} (#{video_stream[:profile]}) (#{video_stream[:codec_tag_string]} / #{video_stream[:codec_tag]}), #{colorspace}, #{resolution} [SAR #{sar} DAR #{dar}]"
92
+
93
+ @rotation = if video_stream.key?(:tags) and video_stream[:tags].key?(:rotate)
94
+ video_stream[:tags][:rotate].to_i
95
+ else
96
+ nil
97
+ end
98
+ end
99
+
100
+ @audio_streams = audio_streams.map do |stream|
101
+ {
102
+ :index => stream[:index],
103
+ :channels => stream[:channels].to_i,
104
+ :codec_name => stream[:codec_name],
105
+ :sample_rate => stream[:sample_rate].to_i,
106
+ :bitrate => stream[:bit_rate].to_i,
107
+ :channel_layout => stream[:channel_layout],
108
+ :tags => stream[:streams],
109
+ :sample_fmt => stream[:sample_fmt],
110
+ :overview => "#{stream[:codec_name]} (#{stream[:codec_tag_string]} / #{stream[:codec_tag]}), #{stream[:sample_rate]} Hz, #{stream[:channel_layout]}, #{stream[:sample_fmt]}, #{stream[:bit_rate]} bit/s"
111
+ }
112
+ end
113
+
114
+ audio_stream = @audio_streams.first
115
+ unless audio_stream.nil?
116
+ @audio_channels = audio_stream[:channels]
117
+ @audio_codec = audio_stream[:codec_name]
118
+ @audio_sample_rate = audio_stream[:sample_rate]
119
+ @audio_bitrate = audio_stream[:bitrate]
120
+ @audio_channel_layout = audio_stream[:channel_layout]
121
+ @audio_tags = audio_stream[:audio_tags]
122
+ @audio_sample_fmt = audio_stream[:sample_fmt]
123
+ @audio_stream = audio_stream[:overview]
124
+ end
125
+
126
+ end
127
+
128
+ unsupported_stream_ids = unsupported_streams(std_error)
129
+ nil_or_unsupported = -> (stream) { stream.nil? || unsupported_stream_ids.include?(stream[:index]) }
130
+
131
+ @invalid = true if nil_or_unsupported.(video_stream) && nil_or_unsupported.(audio_stream)
132
+ @invalid = true if @metadata.key?(:error)
133
+ @invalid = true if std_error.include?("could not find codec parameters")
134
+ end
135
+
136
+ def unsupported_streams(std_error)
137
+ [].tap do |stream_indices|
138
+ std_error.each_line do |line|
139
+ match = line.match(UNSUPPORTED_CODEC_PATTERN)
140
+ stream_indices << match[2].to_i if match
141
+ end
142
+ end
143
+ end
144
+
145
+ def valid?
146
+ not @invalid
147
+ end
148
+
149
+ def remote?
150
+ @path =~ URI::regexp(%w(http https))
151
+ end
152
+
153
+ def local?
154
+ not remote?
155
+ end
156
+
157
+ def width
158
+ rotation.nil? || rotation == 180 ? @width : @height;
159
+ end
160
+
161
+ def height
162
+ rotation.nil? || rotation == 180 ? @height : @width;
163
+ end
164
+
165
+ def resolution
166
+ unless width.nil? or height.nil?
167
+ "#{width}x#{height}"
168
+ end
169
+ end
170
+
171
+ def calculated_aspect_ratio
172
+ aspect_from_dar || aspect_from_dimensions
173
+ end
174
+
175
+ def calculated_pixel_aspect_ratio
176
+ aspect_from_sar || 1
177
+ end
178
+
179
+ def size
180
+ if local?
181
+ File.size(@path)
182
+ else
183
+ @head.content_length
184
+ end
185
+ end
186
+
187
+ def audio_channel_layout
188
+ # TODO Whenever support for ffmpeg/ffprobe 1.2.1 is dropped this is no longer needed
189
+ @audio_channel_layout || case(audio_channels)
190
+ when 1
191
+ 'stereo'
192
+ when 2
193
+ 'stereo'
194
+ when 6
195
+ '5.1'
196
+ else
197
+ 'unknown'
198
+ end
199
+ end
200
+
201
+ def transcode(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
202
+ Transcoder.new(self, output_file, options, transcoder_options).run &block
203
+ end
204
+
205
+ def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
206
+ Transcoder.new(self, output_file, options.merge(screenshot: true), transcoder_options).run &block
207
+ end
208
+
209
+ protected
210
+ def aspect_from_dar
211
+ calculate_aspect(dar)
212
+ end
213
+
214
+ def aspect_from_sar
215
+ calculate_aspect(sar)
216
+ end
217
+
218
+ def calculate_aspect(ratio)
219
+ return nil unless ratio
220
+ w, h = ratio.split(':')
221
+ return nil if w == '0' || h == '0'
222
+ @rotation.nil? || (@rotation == 180) ? (w.to_f / h.to_f) : (h.to_f / w.to_f)
223
+ end
224
+
225
+ def aspect_from_dimensions
226
+ aspect = width.to_f / height.to_f
227
+ aspect.nan? ? nil : aspect
228
+ end
229
+
230
+ def fix_encoding(output)
231
+ output[/test/] # Running a regexp on the string throws error if it's not UTF-8
232
+ rescue ArgumentError
233
+ output.force_encoding("ISO-8859-1")
234
+ end
235
+
236
+ def head(location=@path, limit=FFMPEG.max_http_redirect_attempts)
237
+ url = URI(location)
238
+ return unless url.path
239
+
240
+ http = Net::HTTP.new(url.host, url.port)
241
+ http.use_ssl = url.port == 443
242
+ response = http.request_head(url.request_uri)
243
+
244
+ case response
245
+ when Net::HTTPRedirection then
246
+ raise FFMPEG::HTTPTooManyRequests if limit == 0
247
+ new_uri = url + URI(response['Location'])
248
+
249
+ head(new_uri, limit - 1)
250
+ else
251
+ response
252
+ end
253
+ rescue SocketError, Errno::ECONNREFUSED => e
254
+ nil
255
+ end
256
+ end
257
+ end