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,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
|
+
}
|