staugaard-transcoding_machine 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class DeserializeTest < Test::Unit::TestCase
4
+ def test_deserialization
5
+ puts TranscodingMachine.load_models_from_json(File.read('test/fixtures/serialized_models.json')).inspect
6
+ end
7
+ 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
+ }