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.
@@ -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
+ }