staugaard-transcoding_machine 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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,35 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
|
3
|
+
module TranscodingMachine
|
4
|
+
module Server
|
5
|
+
class S3Storage
|
6
|
+
def initialize
|
7
|
+
@s3 = RightAws::S3.new(nil, nil, :server => 's3.amazonaws.com', :port => 80, :protocol => 'http')
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_file(key_name, destination_file_path, options)
|
11
|
+
file = File.new(destination_file_path, File::CREAT|File::RDWR)
|
12
|
+
rhdr = @s3.interface.get(options[:bucket], key_name) do |chunk|
|
13
|
+
file.write(chunk)
|
14
|
+
end
|
15
|
+
file.close
|
16
|
+
end
|
17
|
+
|
18
|
+
def put_file(source_file_path, destination_file_name, media_format, options)
|
19
|
+
destination_key = RightAws::S3::Key.create(@s3.bucket(options[:bucket]), destination_file_name)
|
20
|
+
s3_headers = { 'Content-Type' => media_format.mime_type,
|
21
|
+
'Content-Disposition' => "attachment; filename=#{File.basename(destination_file_name)}"
|
22
|
+
}
|
23
|
+
|
24
|
+
destination_key.put(File.new(source_file_path), 'public-read', s3_headers)
|
25
|
+
FileUtils.rm(source_file_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def put_thumbnail_file(thumbnail_file, source_file_key_name, options)
|
29
|
+
destination_key = RightAws::S3::Key.create(@s3.bucket(options[:bucket]), "#{source_file_key_name}.thumb.jpg")
|
30
|
+
destination_key.put(thumbnail_file, 'public-read', 'Content-Type' => 'image/jpg')
|
31
|
+
FileUtils.rm(thumbnail_file.path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'transcoding_machine/server/media_file_attributes'
|
2
|
+
require 'transcoding_machine/server/file_storage'
|
3
|
+
|
4
|
+
module TranscodingMachine
|
5
|
+
module Server
|
6
|
+
class Transcoder
|
7
|
+
@@options = {:work_directory => '/tmp', :storage => FileStorage.new}
|
8
|
+
def self.options
|
9
|
+
@@options
|
10
|
+
end
|
11
|
+
|
12
|
+
def work_directory
|
13
|
+
self.class.options[:work_directory]
|
14
|
+
end
|
15
|
+
|
16
|
+
def storage
|
17
|
+
self.class.options[:storage]
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(source_file_name, media_players, media_formats, event_handler = nil, storage_options = {})
|
21
|
+
@source_file_name = source_file_name
|
22
|
+
@event_handler = event_handler
|
23
|
+
@media_players = media_players
|
24
|
+
@media_formats = media_formats
|
25
|
+
@storage_options = storage_options
|
26
|
+
end
|
27
|
+
|
28
|
+
def source_file_path
|
29
|
+
@source_file_path ||= File.expand_path(@source_file_name, work_directory)
|
30
|
+
end
|
31
|
+
|
32
|
+
def source_file_directory
|
33
|
+
@source_file_directory ||= File.dirname(source_file_path)
|
34
|
+
end
|
35
|
+
|
36
|
+
def destination_file_name(media_format)
|
37
|
+
@source_file_name + media_format.suffix
|
38
|
+
end
|
39
|
+
|
40
|
+
def destination_file_path(media_format)
|
41
|
+
File.expand_path(destination_file_name(media_format), work_directory)
|
42
|
+
end
|
43
|
+
|
44
|
+
def start
|
45
|
+
prepare_working_directory
|
46
|
+
get_source_file
|
47
|
+
source_file_attributes, source_media_format = analyze_source_file
|
48
|
+
|
49
|
+
thumbnail_file_path = generate_thumbnail(source_file_attributes)
|
50
|
+
storage.put_thumbnail_file(thumbnail_file_path, @source_file_name, @storage_options) if thumbnail_file_path
|
51
|
+
|
52
|
+
media_formats = @media_players.map {|mp| mp.best_format_for(source_file_attributes)}.compact.uniq
|
53
|
+
|
54
|
+
media_formats -= [source_media_format]
|
55
|
+
|
56
|
+
media_formats.each do |media_format|
|
57
|
+
destination_file_path = transcode(source_file_attributes, media_format)
|
58
|
+
put_destination_file(destination_file_path, media_format)
|
59
|
+
end
|
60
|
+
clear
|
61
|
+
end
|
62
|
+
|
63
|
+
def prepare_working_directory
|
64
|
+
if !File.exist?(source_file_directory)
|
65
|
+
puts "creating directory #{source_file_directory}"
|
66
|
+
FileUtils.mkdir_p(source_file_directory)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_source_file
|
71
|
+
@event_handler.getting_source_file if @event_handler
|
72
|
+
storage.get_file(@source_file_name, source_file_path, @storage_options)
|
73
|
+
@event_handler.got_source_file if @event_handler
|
74
|
+
end
|
75
|
+
|
76
|
+
def analyze_source_file
|
77
|
+
@event_handler.analyzing_source_file if @event_handler
|
78
|
+
|
79
|
+
source_file_attributes = MediaFileAttributes.new(source_file_path)
|
80
|
+
|
81
|
+
source_media_format = @media_formats.find {|mf| mf.matches(source_file_attributes)}
|
82
|
+
|
83
|
+
@event_handler.analyzed_source_file(source_file_attributes, source_media_format) if @event_handler
|
84
|
+
[source_file_attributes, source_media_format]
|
85
|
+
end
|
86
|
+
|
87
|
+
def generate_thumbnail(source_file_attributes)
|
88
|
+
if source_file_attributes.video?
|
89
|
+
@event_handler.generating_thumbnail_file if @event_handler
|
90
|
+
file = source_file_attributes.thumbnail_file
|
91
|
+
@event_handler.generated_thumbnail_file if @event_handler
|
92
|
+
file
|
93
|
+
else
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def transcode(source_file_attributes, media_format)
|
99
|
+
@event_handler.transcoding(media_format) if @event_handler
|
100
|
+
|
101
|
+
dst_file_path = destination_file_path(media_format)
|
102
|
+
cmd = command_for(source_file_attributes, media_format, dst_file_path)
|
103
|
+
commands = cmd.split(' && ')
|
104
|
+
puts "Number of commands to run: #{commands.size}"
|
105
|
+
|
106
|
+
commands.each do |command|
|
107
|
+
puts "Running command: #{command}"
|
108
|
+
result = begin
|
109
|
+
timeout(1000 * 60 * 55) do
|
110
|
+
puts IO.popen(command).read
|
111
|
+
end
|
112
|
+
rescue Timeout::Error => e
|
113
|
+
puts "TIMEOUT REACHED WHEN RUNNING COMMAND"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
@event_handler.transcoded(media_format) if @event_handler
|
118
|
+
dst_file_path
|
119
|
+
end
|
120
|
+
|
121
|
+
def put_destination_file(file_path, media_format)
|
122
|
+
@event_handler.putting_destination_file(file_path, media_format) if @event_handler
|
123
|
+
storage.put_file(destination_file_path(media_format), destination_file_name(media_format), media_format, @storage_options)
|
124
|
+
@event_handler.put_destination_file(file_path, media_format) if @event_handler
|
125
|
+
end
|
126
|
+
|
127
|
+
def clear
|
128
|
+
FileUtils.rm(source_file_path)
|
129
|
+
end
|
130
|
+
|
131
|
+
def command_for(source_file_attributes, media_format, destination_file_path)
|
132
|
+
command_variables = {
|
133
|
+
'in_file_name' => "\"#{source_file_path}\"",
|
134
|
+
'in_directory' => "#{source_file_directory}",
|
135
|
+
'out_file_name' => "\"#{destination_file_path}\"",
|
136
|
+
'out_directory' => "#{source_file_directory}"
|
137
|
+
}
|
138
|
+
|
139
|
+
command_variables['fps'] = target_fps(source_file_attributes) if source_file_attributes.video_fps
|
140
|
+
|
141
|
+
cmd = media_format.command.strip
|
142
|
+
|
143
|
+
command_variables.each {|key, value| cmd.gsub!("{{#{key}}}", value.to_s) }
|
144
|
+
|
145
|
+
cmd
|
146
|
+
end
|
147
|
+
|
148
|
+
def target_fps(source_file_attributes)
|
149
|
+
if fps = source_file_attributes.video_fps
|
150
|
+
fps > 30 ? 25 : fps
|
151
|
+
else
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
|
3
|
+
module TranscodingMachine
|
4
|
+
module Server
|
5
|
+
class TranscodingEventListener
|
6
|
+
def initialize(message_properties)
|
7
|
+
@message_properties = message_properties
|
8
|
+
@result_queue = RightAws::SqsGen2.new.queue(message_properties[:result_queue])
|
9
|
+
end
|
10
|
+
|
11
|
+
def getting_source_file
|
12
|
+
push_status(:downloading)
|
13
|
+
end
|
14
|
+
|
15
|
+
def got_source_file
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def analyzing_source_file
|
20
|
+
push_status(:analyzing)
|
21
|
+
end
|
22
|
+
|
23
|
+
def analyzed_source_file(source_file_attributes, source_media_format)
|
24
|
+
push_status(:analyzed, :media_format => source_media_format, :media_attributes => source_file_attributes)
|
25
|
+
end
|
26
|
+
|
27
|
+
def generating_thumbnail_file
|
28
|
+
push_status(:creating_thumbnail)
|
29
|
+
end
|
30
|
+
|
31
|
+
def generated_thumbnail_file
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def transcoding(media_format)
|
36
|
+
push_status(:transcoding, :media_format => media_format)
|
37
|
+
end
|
38
|
+
|
39
|
+
def transcoded(media_format)
|
40
|
+
push_status(:transcoded, :media_format => media_format)
|
41
|
+
end
|
42
|
+
|
43
|
+
def putting_destination_file(file_path, media_format)
|
44
|
+
push_status(:uploading, :media_format => media_format, :destination_key => file_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def put_destination_file(file_path, media_format)
|
48
|
+
push_status(:uploaded, :media_format => media_format, :destination_key => file_path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def push_status(status, options = {})
|
52
|
+
msg = @message_properties.clone
|
53
|
+
msg[:status] = status
|
54
|
+
msg.merge!(options)
|
55
|
+
@result_queue.push(msg.to_yaml)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require File.expand_path('../server', File.dirname(__FILE__))
|
2
|
+
require 'right_aws'
|
3
|
+
require 'transcoding_machine/server/ec2_environment'
|
4
|
+
require 'transcoding_machine/server/transcoding_event_listener'
|
5
|
+
|
6
|
+
module TranscodingMachine
|
7
|
+
module Server
|
8
|
+
class Worker
|
9
|
+
# initialize queues from names
|
10
|
+
def initialize(log)
|
11
|
+
@log = log
|
12
|
+
@shutdown = false
|
13
|
+
@state = "none"
|
14
|
+
@last_status_time = Time.now
|
15
|
+
@sqs = RightAws::SqsGen2.new
|
16
|
+
@s3 = RightAws::S3.new(nil, nil, :server => 's3.amazonaws.com', :port => 80, :protocol => 'http')
|
17
|
+
|
18
|
+
begin
|
19
|
+
@work_queue = @sqs.queue(Ec2Environment.work_queue_name)
|
20
|
+
if @work_queue.nil?
|
21
|
+
@log.puts "error #{$!} #{Ec2Environment.work_queue_name}"
|
22
|
+
raise "no work queue"
|
23
|
+
end
|
24
|
+
@status_queue = @sqs.queue(Ec2Environment.status_queue_name)
|
25
|
+
if @status_queue.nil?
|
26
|
+
@log.puts "error #{$!} #{Ec2Environment.status_queue_name}"
|
27
|
+
raise "no status queue"
|
28
|
+
end
|
29
|
+
rescue
|
30
|
+
@log.puts "error #{$!}"
|
31
|
+
raise "cannot list queues"
|
32
|
+
end
|
33
|
+
|
34
|
+
@media_players, @media_formats = TranscodingMachine.load_models_from_hash(Ec2Environment.transcoding_settings)
|
35
|
+
Transcoder.options[:storage] = S3Storage.new
|
36
|
+
|
37
|
+
set_state("idle")
|
38
|
+
end
|
39
|
+
|
40
|
+
def send_status_message(status)
|
41
|
+
now = Time.now
|
42
|
+
msg = { :type => 'status',
|
43
|
+
:instance_id => Ec2Environment.instance_id,
|
44
|
+
:state => 'active',
|
45
|
+
:load_estimate => status == 'busy' ? 1 : 0,
|
46
|
+
:timestamp => now}
|
47
|
+
@status_queue.push(msg.to_yaml)
|
48
|
+
@last_status_time = now
|
49
|
+
end
|
50
|
+
|
51
|
+
def send_log_message(message)
|
52
|
+
@log.puts message
|
53
|
+
msg = { :type => 'log',
|
54
|
+
:instance_id => Ec2Environment.instance_id,
|
55
|
+
:message => message,
|
56
|
+
:timestamp => Time.now}
|
57
|
+
@status_queue.push(msg.to_yaml)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Send status when state changes, when state becomes busy, or
|
61
|
+
# every minute (even if there is no state change).
|
62
|
+
def set_state(new_state)
|
63
|
+
if new_state != @state ||
|
64
|
+
new_state == "busy" ||
|
65
|
+
@last_status_time + 60 < Time.now
|
66
|
+
@state = new_state
|
67
|
+
send_status_message(new_state)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_message(msg)
|
72
|
+
#pp msg
|
73
|
+
set_state("busy")
|
74
|
+
|
75
|
+
start_time = Time.now
|
76
|
+
|
77
|
+
send_log_message "Transcoding: BLA BLA BLA"
|
78
|
+
|
79
|
+
message_properties = YAML.load(msg.body)
|
80
|
+
puts "consuming message #{message_properties.inspect}"
|
81
|
+
selected_media_players = find_media_players(message_properties[:media_players])
|
82
|
+
if selected_media_players.any?
|
83
|
+
if bucket = @s3.bucket(message_properties[:bucket].to_s)
|
84
|
+
key = bucket.key(message_properties[:key].to_s)
|
85
|
+
if key.exists?
|
86
|
+
transcoder = Transcoder.new(key.name,
|
87
|
+
selected_media_players,
|
88
|
+
@media_formats,
|
89
|
+
TranscodingEventListener.new(message_properties),
|
90
|
+
:bucket => bucket.name)
|
91
|
+
transcoder.start
|
92
|
+
else
|
93
|
+
send_log_message "Input file not found (bucket: #{message_properties[:bucket]} key: #{message_properties[:key]})"
|
94
|
+
end
|
95
|
+
else
|
96
|
+
send_log_message "Input bucket not found (bucket: #{message_properties[:bucket]})"
|
97
|
+
end
|
98
|
+
else
|
99
|
+
send_log_message "No media players found #{message_properties[:media_players].inspect}"
|
100
|
+
end
|
101
|
+
|
102
|
+
end_time = Time.now
|
103
|
+
|
104
|
+
if true #test if transcode was successful
|
105
|
+
msg.delete
|
106
|
+
send_log_message "Transcoded: BLA BLA BLA"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_media_players(media_player_ids)
|
111
|
+
@media_players.slice(*media_player_ids).values
|
112
|
+
end
|
113
|
+
|
114
|
+
def message_loop
|
115
|
+
msg = @work_queue.pop
|
116
|
+
if msg.nil?
|
117
|
+
#@log.puts "no messages"
|
118
|
+
set_state("idle")
|
119
|
+
sleep 5
|
120
|
+
else
|
121
|
+
handle_message(msg)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def shutdown
|
126
|
+
@shutdown = true
|
127
|
+
end
|
128
|
+
|
129
|
+
def run
|
130
|
+
while ! @shutdown
|
131
|
+
message_loop
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
{
|
2
|
+
"media_formats": {
|
3
|
+
"1": {
|
4
|
+
"name": "h264 Video 640x352",
|
5
|
+
"type": "video",
|
6
|
+
"priority": 3000,
|
7
|
+
"width": 640,
|
8
|
+
"height": 352,
|
9
|
+
"aspect_ratio": "16/9",
|
10
|
+
"mime_type": "video/mp4",
|
11
|
+
"suffix": "_h264_640_480.mp4",
|
12
|
+
"command": "cp {{in_file_name}} {{out_file_name}}",
|
13
|
+
"criteria": [
|
14
|
+
{"key": "video_codec", "operator": "equals", "value": "h264"},
|
15
|
+
{"key": "width", "operator": "equals", "value": 640},
|
16
|
+
{"key": "height", "operator": "equals", "value": 352},
|
17
|
+
{"key": "ipod_uuid", "operator": "equals", "value": true},
|
18
|
+
{"key": "audio_codec", "operator": "equals", "value": "aac"}
|
19
|
+
]
|
20
|
+
},
|
21
|
+
"2": {
|
22
|
+
"name": "h264 HD 1280x720p",
|
23
|
+
"type": "video",
|
24
|
+
"priority": 10000,
|
25
|
+
"width": 1280,
|
26
|
+
"height": 720,
|
27
|
+
"aspect_ratio": "16/9",
|
28
|
+
"mime_type": "video/mp4",
|
29
|
+
"suffix": "_h264_HD1280_720p.mp4",
|
30
|
+
"command": "cp {{in_file_name}} {{out_file_name}}",
|
31
|
+
"criteria": [
|
32
|
+
{"key": "video_codec", "operator": "equals", "value": "h264"},
|
33
|
+
{"key": "width", "operator": "equals", "value": 1280},
|
34
|
+
{"key": "height", "operator": "equals", "value": 720},
|
35
|
+
{"key": "audio_codec", "operator": "equals", "value": "aac"}
|
36
|
+
]
|
37
|
+
}
|
38
|
+
},
|
39
|
+
|
40
|
+
"media_players": {
|
41
|
+
"appletv": {
|
42
|
+
"name": "AppleTV",
|
43
|
+
"description": "Damn cool media player",
|
44
|
+
"formats": ["1", "2"]
|
45
|
+
},
|
46
|
+
"ipod": {
|
47
|
+
"name": "iPod",
|
48
|
+
"description": "you know this thing!",
|
49
|
+
"formats": ["1"]
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|