staugaard-transcoding_machine 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README +6 -0
- data/VERSION.yml +4 -0
- data/bin/transcoding_machine +43 -0
- data/bin/transcoding_machine_ec2_server +26 -0
- data/lib/transcoding_machine.rb +45 -0
- data/lib/transcoding_machine/client/job_queue.rb +17 -0
- data/lib/transcoding_machine/client/result_queue.rb +47 -0
- data/lib/transcoding_machine/client/server_manager.rb +107 -0
- data/lib/transcoding_machine/media_format.rb +91 -0
- data/lib/transcoding_machine/media_format_criterium.rb +50 -0
- data/lib/transcoding_machine/media_player.rb +17 -0
- data/lib/transcoding_machine/server.rb +6 -0
- data/lib/transcoding_machine/server/ec2_environment.rb +79 -0
- data/lib/transcoding_machine/server/file_storage.rb +23 -0
- data/lib/transcoding_machine/server/media_file_attributes.rb +582 -0
- data/lib/transcoding_machine/server/s3_storage.rb +35 -0
- data/lib/transcoding_machine/server/transcoder.rb +157 -0
- data/lib/transcoding_machine/server/transcoding_event_listener.rb +59 -0
- data/lib/transcoding_machine/server/worker.rb +137 -0
- data/test/deserialze_test.rb +7 -0
- data/test/fixtures/serialized_models.json +52 -0
- data/test/media_format_criterium_test.rb +55 -0
- data/test/media_format_test.rb +42 -0
- data/test/media_player_test.rb +7 -0
- data/test/test_helper.rb +9 -0
- metadata +106 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'transcoding_machine/media_format'
|
2
|
+
|
3
|
+
module TranscodingMachine
|
4
|
+
class MediaPlayer
|
5
|
+
attr_reader :formats
|
6
|
+
def initialize(args)
|
7
|
+
if args[:formats]
|
8
|
+
@formats = args[:formats].sort {|f1, f2| f2.priority <=> f1.priority}
|
9
|
+
end
|
10
|
+
@formats ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def best_format_for(media_file_attributes)
|
14
|
+
@formats.find {|f| f.can_transcode?(media_file_attributes)}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'yaml'
|
3
|
+
require 'right_aws'
|
4
|
+
|
5
|
+
module TranscodingMachine
|
6
|
+
module Server
|
7
|
+
class Ec2Environment
|
8
|
+
@@logger = STDOUT
|
9
|
+
@@data = nil
|
10
|
+
@@transcoding_settings = nil
|
11
|
+
|
12
|
+
def self.logger=(new_logger)
|
13
|
+
@@logger = new_logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.logger
|
17
|
+
@@logger || STDOUT
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.load
|
21
|
+
!data.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Load user data
|
25
|
+
def self.data
|
26
|
+
return @@data if @@data
|
27
|
+
begin
|
28
|
+
logger.puts "Getting EC2 instance ID"
|
29
|
+
iid = open('http://169.254.169.254/latest/meta-data/instance-id').read(200)
|
30
|
+
logger.puts "Getting EC2 user data"
|
31
|
+
user_data = open('http://169.254.169.254/latest/user-data').read(2000)
|
32
|
+
data = YAML.load(user_data)
|
33
|
+
aws = { :aws_env => data[:aws_env],
|
34
|
+
:aws_access_key => data[:aws_access_key],
|
35
|
+
:aws_secret_key => data[:aws_secret_key]
|
36
|
+
}
|
37
|
+
|
38
|
+
ENV['AWS_ACCESS_KEY_ID'] = data[:aws_access_key]
|
39
|
+
ENV['AWS_SECRET_ACCESS_KEY'] = data[:aws_secret_key]
|
40
|
+
rescue
|
41
|
+
# when running locally, use fake iid
|
42
|
+
iid = "unknown"
|
43
|
+
user_data = nil
|
44
|
+
aws = {}
|
45
|
+
end
|
46
|
+
@@data = {:aws => aws, :iid => iid, :user_data => data}
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.keys
|
50
|
+
[data[:aws][:aws_access_key], data[:aws][:aws_secret_key]]
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.instance_id
|
54
|
+
data[:iid]
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.transcoding_settings
|
58
|
+
return @@transcoding_settings if @@transcoding_settings
|
59
|
+
|
60
|
+
bucket = data[:user_data][:transcoding_settings][:bucket]
|
61
|
+
key = data[:user_data][:transcoding_settings][:key]
|
62
|
+
|
63
|
+
logger.puts "Getting transcoding settings from S3 #{bucket}/#{key}"
|
64
|
+
|
65
|
+
s3 = RightAws::S3.new
|
66
|
+
|
67
|
+
@@transcoding_settings ||= YAML.load(s3.bucket(bucket).key(key).data)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.work_queue_name
|
71
|
+
data[:user_data][:work_queue_name]
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.status_queue_name
|
75
|
+
data[:user_data][:status_queue_name]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module TranscodingMachine
|
2
|
+
module Server
|
3
|
+
class FileStorage
|
4
|
+
def initialize(root_directory = '.')
|
5
|
+
@root = root_directory
|
6
|
+
end
|
7
|
+
|
8
|
+
def get_file(source_file_name, destination_file_path, options)
|
9
|
+
puts File.expand_path(source_file_name, @root)
|
10
|
+
puts destination_file_path
|
11
|
+
FileUtils.cp(File.expand_path(source_file_name, @root), destination_file_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def put_file(source_file_path, destination_file_name, media_format, options)
|
15
|
+
FileUtils.mv(source_file_path, File.expand_path(destination_file_name, @root))
|
16
|
+
end
|
17
|
+
|
18
|
+
def put_thumbnail_file(thumbnail_file_path, source_file, options)
|
19
|
+
FileUtils.mv(thumbnail_file_path.path, @root)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,582 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'open3'
|
3
|
+
require 'pathname'
|
4
|
+
require 'timeout'
|
5
|
+
require 'unicode'
|
6
|
+
|
7
|
+
module TranscodingMachine
|
8
|
+
module Server
|
9
|
+
class MediaFileAttributes < Hash
|
10
|
+
ASPECT_RATIO_2_35_BY_1 = 2.35
|
11
|
+
ASPECT_RATIO_16_BY_9 = 16.0 / 9.0
|
12
|
+
ASPECT_RATIO_4_BY_3 = 4.0 / 3.0
|
13
|
+
ASPECT_RATIO_NAMES = {ASPECT_RATIO_2_35_BY_1 => '2.35/1', ASPECT_RATIO_16_BY_9 => '16/9', ASPECT_RATIO_4_BY_3 => '4/3'}
|
14
|
+
ASPECT_RATIO_VALUES = ASPECT_RATIO_NAMES.invert
|
15
|
+
|
16
|
+
CODECS = {'ffmp3' => :mp3, 'mp3' => :mp3, 'faad' => :aac, 'ffh264' => :h264, 'h264' => :h264, 'ffvp6f' => :flash_video}
|
17
|
+
|
18
|
+
TRACK_FIELD_TYPES = {
|
19
|
+
:codec => :codec,
|
20
|
+
:width => :integer,
|
21
|
+
:height => :integer,
|
22
|
+
:format => :string,
|
23
|
+
:aspect => :float,
|
24
|
+
:id => :integer,
|
25
|
+
:bitrate => :integer,
|
26
|
+
:fps => :float,
|
27
|
+
:file_name => :string,
|
28
|
+
:length => :float,
|
29
|
+
:demuxer => :string,
|
30
|
+
:rate => :integer,
|
31
|
+
:channels => :integer
|
32
|
+
}
|
33
|
+
|
34
|
+
FIELD_TYPES = {
|
35
|
+
:audio => :boolean,
|
36
|
+
:audio_format => :string,
|
37
|
+
:audio_rate => :integer,
|
38
|
+
:audio_bitrate => :integer,
|
39
|
+
:audio_codec => :codec,
|
40
|
+
:audio_channels => :integer,
|
41
|
+
:video => :boolean,
|
42
|
+
:video_format => :string,
|
43
|
+
:video_codec => :codec,
|
44
|
+
:width => :integer,
|
45
|
+
:height => :integer,
|
46
|
+
:aspect_ratio => :float,
|
47
|
+
:video_fps => :float,
|
48
|
+
:video_bitrate => :integer,
|
49
|
+
:ipod_uuid => :boolean,
|
50
|
+
:bitrate => :integer,
|
51
|
+
:length => :float,
|
52
|
+
:file_name => :string,
|
53
|
+
:file_extension => :string,
|
54
|
+
:demuxer => :string,
|
55
|
+
:poster_time => :float
|
56
|
+
}
|
57
|
+
|
58
|
+
def initialize(media_file_path)
|
59
|
+
super()
|
60
|
+
ffmpeg = FfmpegIntegrator.new(media_file_path)
|
61
|
+
mplayer = MplayerIntegrator.new(media_file_path)
|
62
|
+
|
63
|
+
puts "FFMPEG:\n#{ffmpeg.tracks.inspect}"
|
64
|
+
|
65
|
+
puts "MPLAYER:\n#{mplayer.tracks.inspect}"
|
66
|
+
|
67
|
+
store(:ipod_uuid, false)
|
68
|
+
|
69
|
+
merge!(get_video_info(get_video_track(ffmpeg.tracks), get_video_track(mplayer.tracks)))
|
70
|
+
merge!(get_audio_info(get_audio_track(ffmpeg.tracks), get_audio_track(mplayer.tracks)))
|
71
|
+
merge!(get_container_info(ffmpeg.tracks[:container], mplayer.tracks[:container]))
|
72
|
+
|
73
|
+
derive_values
|
74
|
+
|
75
|
+
if video?
|
76
|
+
atomic_parsley = AtomicParsleyIntegrator.new(media_file_path)
|
77
|
+
puts "ATOMIC_PARSLEY:\n#{atomic_parsley.tracks.inspect}"
|
78
|
+
store(:ipod_uuid, atomic_parsley.tracks[:container][:ipod_uuid])
|
79
|
+
|
80
|
+
exiftool = ExifToolIntegrator.new(media_file_path)
|
81
|
+
puts "EXIFTOOL:"
|
82
|
+
puts exiftool.inspect
|
83
|
+
store(:poster_time, exiftool.poster_time)
|
84
|
+
if exiftool.aspect_ratio and video_aspect != exiftool.aspect_ratio
|
85
|
+
store(:height, (width / exiftool.aspect_ratio).to_i)
|
86
|
+
store(:video_aspect, exiftool.aspect_ratio)
|
87
|
+
elsif exiftool.width && exiftool.height
|
88
|
+
store(:width, exiftool.width)
|
89
|
+
store(:height, exiftool.height)
|
90
|
+
derive_values
|
91
|
+
end
|
92
|
+
fix_dimensions
|
93
|
+
end
|
94
|
+
|
95
|
+
delete_if {|key, value| value.nil?}
|
96
|
+
end
|
97
|
+
|
98
|
+
def video?
|
99
|
+
video
|
100
|
+
end
|
101
|
+
|
102
|
+
def audio?
|
103
|
+
audio
|
104
|
+
end
|
105
|
+
|
106
|
+
def thumbnail_file
|
107
|
+
return @thumbnail_file if @thumbnail_file
|
108
|
+
|
109
|
+
return nil unless video? and height and width
|
110
|
+
|
111
|
+
time = poster_time
|
112
|
+
if time.nil? or time == 0 or time > 30
|
113
|
+
time = length / 10.0
|
114
|
+
time = 10 if time > 10
|
115
|
+
end
|
116
|
+
|
117
|
+
@thumbnail_file = FfmpegIntegrator.create_thumbnail(file_name, width, height, time)
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.parse_values(values)
|
121
|
+
values.values.each do |track_values|
|
122
|
+
track_values.each do |key, value|
|
123
|
+
case TRACK_FIELD_TYPES[key]
|
124
|
+
when :integer
|
125
|
+
track_values[key] = value.to_i
|
126
|
+
when :float
|
127
|
+
track_values[key] = value.to_f
|
128
|
+
when :codec
|
129
|
+
track_values[key] = CODECS[value] || value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
values
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def get_video_track(tracks)
|
140
|
+
get_track_by_type(tracks, :video)
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_audio_track(tracks)
|
144
|
+
get_track_by_type(tracks, :audio)
|
145
|
+
end
|
146
|
+
|
147
|
+
def get_track_by_type(tracks, type)
|
148
|
+
return nil if tracks.nil?
|
149
|
+
tracks.values.each do |track|
|
150
|
+
return track if track[:type] == type
|
151
|
+
end
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def has_real_video_track?(ffmpeg_video_track, mplayer_video_track)
|
156
|
+
return false if ffmpeg_video_track.nil? && mplayer_video_track.nil?
|
157
|
+
|
158
|
+
unless ffmpeg_video_track.nil?
|
159
|
+
return false if ffmpeg_video_track[:codec] == 'png'
|
160
|
+
return false if ffmpeg_video_track[:width] == 1
|
161
|
+
return false if ffmpeg_video_track[:height] == 1
|
162
|
+
return false if ffmpeg_video_track[:fps] && ffmpeg_video_track[:fps] > 1000
|
163
|
+
end
|
164
|
+
|
165
|
+
unless mplayer_video_track.nil?
|
166
|
+
return false if mplayer_video_track[:format] == 'jpeg'
|
167
|
+
return false if mplayer_video_track[:fps] == 0
|
168
|
+
end
|
169
|
+
|
170
|
+
ffmpeg_video_track ||= {}
|
171
|
+
mplayer_video_track ||= {}
|
172
|
+
|
173
|
+
return false if (ffmpeg_video_track[:codec].nil? && mplayer_video_track[:codec].nil?)
|
174
|
+
return false if (ffmpeg_video_track[:width].nil? && mplayer_video_track[:width].nil?)
|
175
|
+
return false if (ffmpeg_video_track[:height].nil? && mplayer_video_track[:height].nil?)
|
176
|
+
|
177
|
+
return true
|
178
|
+
end
|
179
|
+
|
180
|
+
def has_real_audio_track?(ffmpeg_audio_track, mplayer_audio_track)
|
181
|
+
unless ffmpeg_audio_track.nil?
|
182
|
+
return true if ffmpeg_audio_track[:codec]
|
183
|
+
return true if ffmpeg_audio_track[:format]
|
184
|
+
end
|
185
|
+
unless mplayer_audio_track.nil?
|
186
|
+
return true if mplayer_audio_track[:codec]
|
187
|
+
return true if mplayer_audio_track[:format]
|
188
|
+
end
|
189
|
+
return false
|
190
|
+
end
|
191
|
+
|
192
|
+
def get_video_info(ffmpeg_track, mplayer_track)
|
193
|
+
return {:video => false} unless has_real_video_track?(ffmpeg_track, mplayer_track)
|
194
|
+
|
195
|
+
ffmpeg_track ||= {}
|
196
|
+
mplayer_track ||= {}
|
197
|
+
|
198
|
+
output = {:video => true}
|
199
|
+
|
200
|
+
output[:video_format] = ffmpeg_track[:format] || mplayer_track[:format]
|
201
|
+
output[:width] = ffmpeg_track[:width] || mplayer_track[:width]
|
202
|
+
output[:height] = ffmpeg_track[:height] || mplayer_track[:height]
|
203
|
+
output[:video_aspect] = ffmpeg_track[:aspect] || mplayer_track[:aspect]
|
204
|
+
output[:video_fps] = ffmpeg_track[:fps] || mplayer_track[:fps]
|
205
|
+
output[:video_bitrate] = ffmpeg_track[:bitrate] || mplayer_track[:bitrate]
|
206
|
+
|
207
|
+
if ffmpeg_track[:codec].class == String && mplayer_track[:codec].class == Symbol
|
208
|
+
output[:video_codec] = mplayer_track[:codec]
|
209
|
+
else
|
210
|
+
output[:video_codec] = ffmpeg_track[:codec] || mplayer_track[:codec]
|
211
|
+
end
|
212
|
+
|
213
|
+
output
|
214
|
+
end
|
215
|
+
|
216
|
+
def get_audio_info(ffmpeg_track, mplayer_track)
|
217
|
+
return {:audio => false} unless has_real_audio_track?(ffmpeg_track, mplayer_track)
|
218
|
+
|
219
|
+
ffmpeg_track ||= {}
|
220
|
+
mplayer_track ||= {}
|
221
|
+
|
222
|
+
output = {:audio => true}
|
223
|
+
|
224
|
+
output[:audio_format] = ffmpeg_track[:format] || mplayer_track[:format]
|
225
|
+
output[:audio_rate] = ffmpeg_track[:rate] || mplayer_track[:rate]
|
226
|
+
output[:audio_bitrate] = ffmpeg_track[:bitrate] || mplayer_track[:bitrate]
|
227
|
+
output[:audio_channels] = ffmpeg_track[:channels] || mplayer_track[:channels]
|
228
|
+
|
229
|
+
if ffmpeg_track[:codec].class == String && mplayer_track[:codec].class == Symbol
|
230
|
+
output[:audio_codec] = mplayer_track[:codec]
|
231
|
+
else
|
232
|
+
output[:audio_codec] = ffmpeg_track[:codec] || mplayer_track[:codec]
|
233
|
+
end
|
234
|
+
|
235
|
+
output
|
236
|
+
end
|
237
|
+
|
238
|
+
def get_container_info(ffmpeg_track, mplayer_track)
|
239
|
+
output = Hash.new
|
240
|
+
|
241
|
+
high_prio = ffmpeg_track || {}
|
242
|
+
low_prio = mplayer_track || {}
|
243
|
+
|
244
|
+
output[:bitrate] = high_prio[:bitrate] || low_prio[:bitrate]
|
245
|
+
output[:length] = high_prio[:length] || low_prio[:length]
|
246
|
+
output[:file_name] = high_prio[:file_name] || low_prio[:file_name]
|
247
|
+
output[:demuxer] = high_prio[:demuxer] || low_prio[:demuxer]
|
248
|
+
output
|
249
|
+
end
|
250
|
+
|
251
|
+
def video_aspect_ratio
|
252
|
+
return nil unless video?
|
253
|
+
|
254
|
+
ratio = width.to_f / height.to_f
|
255
|
+
|
256
|
+
return nil if ratio.nan?
|
257
|
+
|
258
|
+
diffs = ASPECT_RATIO_NAMES.keys.map {|ar| [(ar - ratio).abs, ar]}
|
259
|
+
|
260
|
+
diffs.sort! {|x, y| x[0] <=> y[0]}
|
261
|
+
|
262
|
+
diffs.first[1]
|
263
|
+
end
|
264
|
+
|
265
|
+
def derive_values
|
266
|
+
store(:video_aspect, video_aspect_ratio) if (video? && has_key?(:width) && has_key?(:height))
|
267
|
+
|
268
|
+
if file_name && (m = file_name.match(/.+\.(\w+)/))
|
269
|
+
store(:file_extension, m[1])
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def fix_dimensions
|
274
|
+
[:width, :height].each do |key|
|
275
|
+
if has_key?(key) and (fetch(key) % 2 == 1)
|
276
|
+
store(key, fetch(key) - 1)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Intercept calls
|
282
|
+
def method_missing(method_name, *args)
|
283
|
+
if (FIELD_TYPES[method_name])
|
284
|
+
self[method_name]
|
285
|
+
else
|
286
|
+
super
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
class FfmpegIntegrator
|
292
|
+
attr_accessor :tracks
|
293
|
+
BINARY = ["ffmpeg"]
|
294
|
+
OPTIONS = ["-i"]
|
295
|
+
TIMEOUT = 60
|
296
|
+
|
297
|
+
def initialize(file_path)
|
298
|
+
commandline = []
|
299
|
+
commandline += BINARY
|
300
|
+
commandline += OPTIONS
|
301
|
+
commandline += [file_path]
|
302
|
+
puts "trying to run: #{commandline.join(' ')}"
|
303
|
+
result = begin
|
304
|
+
timeout(TIMEOUT) do
|
305
|
+
Open3.popen3(*commandline) do |i, o, e|
|
306
|
+
[o.read, e.read]
|
307
|
+
end
|
308
|
+
end
|
309
|
+
rescue Timeout::Error => e
|
310
|
+
puts "Timeout reached when inspecting #{file_path} with ffmpeg"
|
311
|
+
raise e
|
312
|
+
end
|
313
|
+
|
314
|
+
result = result.join("\n")
|
315
|
+
|
316
|
+
ffmpeg_values = Hash.new
|
317
|
+
|
318
|
+
start_index = result.index("Input #0, ")
|
319
|
+
@tracks = {}
|
320
|
+
|
321
|
+
unless start_index.nil?
|
322
|
+
result = result[start_index..-1]
|
323
|
+
|
324
|
+
result.split(/\n/).each do |line|
|
325
|
+
line.strip!
|
326
|
+
if match = line.match(/^Duration: ((\d\d):(\d\d):(\d\d(.\d)?)), .*, bitrate: (\d+) kb\/s/)
|
327
|
+
puts "found duration #{line}"
|
328
|
+
ffmpeg_values[:container] = {:type => :container}
|
329
|
+
if match[1]
|
330
|
+
hours = match[2] ? match[2].to_i : 0
|
331
|
+
minutes = match[3] ? match[3].to_i : 0
|
332
|
+
seconds = match[4] ? match[4].to_f : 0
|
333
|
+
ffmpeg_values[:container][:length] = (hours * 60 * 60) + (minutes * 60) + seconds
|
334
|
+
end
|
335
|
+
if match[6]
|
336
|
+
ffmpeg_values[:container][:bitrate] = match[6].to_i * 1000
|
337
|
+
end
|
338
|
+
elsif match = line.match(/^Stream #0.(\d).*: Video: ([^,]*)(, [^,]*)?(, (\d+)x(\d+))(, (\d+.\d+) fps)?/)
|
339
|
+
puts "found video #{line}"
|
340
|
+
track_info = ffmpeg_values["track_#{match[1]}".to_sym] = {:type => :video}
|
341
|
+
if match[2]
|
342
|
+
track_info[:codec] = match[2]
|
343
|
+
end
|
344
|
+
if match[5]
|
345
|
+
track_info[:width] = match[5]
|
346
|
+
end
|
347
|
+
if match[6]
|
348
|
+
track_info[:height] = match[6]
|
349
|
+
end
|
350
|
+
if match[8]
|
351
|
+
track_info[:fps] = match[8]
|
352
|
+
end
|
353
|
+
elsif match = line.match(/^Stream #0.(\d).*: Audio: ([^,]*)(, (\d+) Hz)?(, (mono|stereo))?(, (\d+) kb\/s)?/)
|
354
|
+
puts "found audio #{line}"
|
355
|
+
track_info = ffmpeg_values["track_#{match[1]}".to_sym] = {:type => :audio}
|
356
|
+
if match[2]
|
357
|
+
track_info[:codec] = match[2]
|
358
|
+
end
|
359
|
+
if match[4]
|
360
|
+
track_info[:rate] = match[4]
|
361
|
+
end
|
362
|
+
if match[6]
|
363
|
+
if match[6] == 'stereo'
|
364
|
+
track_info[:channels] = 2
|
365
|
+
elsif match[6] == 'mono'
|
366
|
+
track_info[:channels] = 1
|
367
|
+
end
|
368
|
+
end
|
369
|
+
if match[8]
|
370
|
+
track_info[:bitrate] = match[8].to_i * 1000
|
371
|
+
end
|
372
|
+
elsif match = line.match(/^Stream #0.(\d).*: Data: (.*)/)
|
373
|
+
puts "found data #{line}"
|
374
|
+
track_info = ffmpeg_values["track_#{match[1]}".to_sym] = {:type => :data}
|
375
|
+
else
|
376
|
+
puts "found other #{line}"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
@tracks = MediaFileAttributes.parse_values(ffmpeg_values)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def self.create_thumbnail(file_path, width, height, time)
|
385
|
+
thumbnail_file_path = "#{file_path}.jpg"
|
386
|
+
commandline = []
|
387
|
+
commandline += BINARY
|
388
|
+
commandline << '-ss'
|
389
|
+
commandline << time.to_s
|
390
|
+
commandline << '-i'
|
391
|
+
commandline << file_path
|
392
|
+
commandline += ['-f', 'mjpeg', '-deinterlace', '-vframes', '1', '-an', '-y', '-s']
|
393
|
+
commandline << "#{width}x#{height}"
|
394
|
+
commandline << thumbnail_file_path
|
395
|
+
|
396
|
+
puts "trying to run: #{commandline.join(' ')}"
|
397
|
+
result = begin
|
398
|
+
timeout(60) do
|
399
|
+
Open3.popen3(*commandline) do |i, o, e|
|
400
|
+
[o.read, e.read]
|
401
|
+
end
|
402
|
+
end
|
403
|
+
rescue Timeout::Error => e
|
404
|
+
puts "Timeout reached when inspecting #{file_path} with ffmpeg"
|
405
|
+
raise e
|
406
|
+
end
|
407
|
+
thumbnail_file = File.new(thumbnail_file_path)
|
408
|
+
if thumbnail_file.stat.size == 0
|
409
|
+
FileUtils.rm_f(thumbnail_file.path)
|
410
|
+
throw result.join
|
411
|
+
end
|
412
|
+
|
413
|
+
return thumbnail_file
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
class MplayerIntegrator
|
418
|
+
attr_accessor :tracks
|
419
|
+
BINARY = ["mplayer"]
|
420
|
+
OPTIONS = ["-identify", "-vo", "null", "-ao", "null", "-frames", "0", "-really-quiet", "-msgcharset", "utf8"]
|
421
|
+
TIMEOUT = 60
|
422
|
+
|
423
|
+
MPLAYER_TRACK_FIELD_MAP = {
|
424
|
+
'CODEC' => :codec,
|
425
|
+
'WIDTH' => :width,
|
426
|
+
'HEIGHT' => :height,
|
427
|
+
'FORMAT' => :format,
|
428
|
+
'ASPECT' => :aspect,
|
429
|
+
'ID' => :id,
|
430
|
+
'BITRATE' => :bitrate,
|
431
|
+
'FPS' => :fps,
|
432
|
+
'FILENAME' => :file_name,
|
433
|
+
'LENGTH' => :length,
|
434
|
+
'DEMUXER' => :demuxer,
|
435
|
+
'RATE' => :rate,
|
436
|
+
'NCH' => :channels
|
437
|
+
}
|
438
|
+
|
439
|
+
def initialize(file_path)
|
440
|
+
commandline = []
|
441
|
+
commandline += BINARY
|
442
|
+
commandline += OPTIONS
|
443
|
+
commandline += [file_path]
|
444
|
+
puts "trying to run: #{commandline.join(' ')}"
|
445
|
+
result = begin
|
446
|
+
timeout(TIMEOUT) do
|
447
|
+
Open3.popen3(*commandline) do |i, o, e|
|
448
|
+
[o.read, e.read]
|
449
|
+
end
|
450
|
+
end
|
451
|
+
rescue Timeout::Error => e
|
452
|
+
puts "Timeout reached when inspecting #{file_path} with mplayer"
|
453
|
+
raise e
|
454
|
+
end
|
455
|
+
|
456
|
+
raise "mplayer error when inspecting #{file_path}: #{result.last}" if result.first.empty? && !result.last.empty?
|
457
|
+
|
458
|
+
mplayer_values = {:container => {:type => :container}}
|
459
|
+
|
460
|
+
match = result.first.match(/.*ID_AUDIO_ID=(\d).*/)
|
461
|
+
audio_track = mplayer_values["track_#{match[1]}".to_sym] = {:type => :audio} unless match.nil?
|
462
|
+
|
463
|
+
match = result.first.match(/.*ID_VIDEO_ID=(\d).*/)
|
464
|
+
video_track = mplayer_values["track_#{match[1]}".to_sym] = {:type => :video} unless match.nil?
|
465
|
+
|
466
|
+
result.first.split(/\n/).each do |line|
|
467
|
+
#puts line
|
468
|
+
if (match = line.match(/ID_([^_]+)(_([^=].*))?=(.*)/))
|
469
|
+
if match[3]
|
470
|
+
key = MPLAYER_TRACK_FIELD_MAP[match[3]] || match[3]
|
471
|
+
else
|
472
|
+
key = MPLAYER_TRACK_FIELD_MAP[match[1]] || match[1]
|
473
|
+
end
|
474
|
+
case match[1]
|
475
|
+
when 'VIDEO'
|
476
|
+
video_track[key] = match[4]
|
477
|
+
when 'AUDIO'
|
478
|
+
audio_track[key] = match[4]
|
479
|
+
else
|
480
|
+
mplayer_values[:container][key] = match[4]
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
@tracks = MediaFileAttributes.parse_values(mplayer_values)
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
class AtomicParsleyIntegrator
|
490
|
+
attr_reader :values, :tracks
|
491
|
+
BINARY = ["AtomicParsley"]
|
492
|
+
OPTIONS = ['-T', '1']
|
493
|
+
TIMEOUT = 60
|
494
|
+
|
495
|
+
def initialize(file_path)
|
496
|
+
commandline = []
|
497
|
+
commandline += BINARY
|
498
|
+
commandline += [file_path]
|
499
|
+
commandline += OPTIONS
|
500
|
+
puts "trying to run: #{commandline.join(' ')}"
|
501
|
+
result = begin
|
502
|
+
timeout(TIMEOUT) do
|
503
|
+
Open3.popen3(*commandline) do |i, o, e|
|
504
|
+
o.read
|
505
|
+
end
|
506
|
+
end
|
507
|
+
rescue Timeout::Error => e
|
508
|
+
puts "Timeout reached when inspecting #{file_path} with AtomicParsley"
|
509
|
+
raise e
|
510
|
+
end
|
511
|
+
|
512
|
+
@tracks = {:container => {:type => :container, :ipod_uuid => false}}
|
513
|
+
|
514
|
+
atomic_parsley_values = Hash.new
|
515
|
+
|
516
|
+
if result =~ /Atom uuid=6b6840f2-5f24-4fc5-ba39-a51bcf0323f3/
|
517
|
+
@tracks[:container][:ipod_uuid] = true
|
518
|
+
atomic_parsley_values[:ipod_uuid] = true
|
519
|
+
end
|
520
|
+
@values = atomic_parsley_values
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
class ExifToolIntegrator
|
525
|
+
attr_accessor :aspect_ratio, :poster_time, :width, :height
|
526
|
+
BINARY = ["exiftool"]
|
527
|
+
OPTIONS = ["-q", "-q", "-s", "-t"]
|
528
|
+
TIMEOUT = 60
|
529
|
+
|
530
|
+
def initialize(file_path)
|
531
|
+
commandline = []
|
532
|
+
commandline += BINARY
|
533
|
+
commandline += OPTIONS
|
534
|
+
commandline += [file_path]
|
535
|
+
puts "trying to run: #{commandline.join(' ')}"
|
536
|
+
result = begin
|
537
|
+
timeout(TIMEOUT) do
|
538
|
+
Open3.popen3(*commandline) do |i, o, e|
|
539
|
+
o.read
|
540
|
+
end
|
541
|
+
end
|
542
|
+
rescue Timeout::Error => e
|
543
|
+
puts "Timeout reached when inspecting #{file_path} with ExifTool"
|
544
|
+
raise e
|
545
|
+
end
|
546
|
+
|
547
|
+
@values = Hash.new
|
548
|
+
|
549
|
+
result.split(/\n/).each do |line|
|
550
|
+
line.strip!
|
551
|
+
if match = line.match(/([^\t]+)\t(.+)/)
|
552
|
+
@values[match[1].underscore] = match[2]
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
unless @values['aspect_ratio'].blank?
|
557
|
+
aspect_ratio_match = @values['aspect_ratio'].match(/\d+:\d+/)
|
558
|
+
aspect_ratio_match = aspect_ratio_match[0] if aspect_ratio_match
|
559
|
+
case aspect_ratio_match
|
560
|
+
when "16:9", "16/9"
|
561
|
+
@aspect_ratio = MediaFileAttributes::ASPECT_RATIO_16_BY_9
|
562
|
+
when "4:3", "4/3"
|
563
|
+
@aspect_ratio = MediaFileAttributes::ASPECT_RATIO_4_BY_3
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
unless @values['poster_time'].blank?
|
568
|
+
poster_time_match = @values['poster_time'].match(/(\d+\.\d+)s/)
|
569
|
+
@poster_time = poster_time_match[1].to_f if poster_time_match
|
570
|
+
end
|
571
|
+
|
572
|
+
unless @values['image_height'].blank?
|
573
|
+
@height = @values['image_height'].to_i
|
574
|
+
end
|
575
|
+
|
576
|
+
unless @values['image_width'].blank?
|
577
|
+
@width = @values['image_width'].to_i
|
578
|
+
end
|
579
|
+
end
|
580
|
+
end
|
581
|
+
end
|
582
|
+
end
|