active-encode 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +16 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE +202 -0
  7. data/README.md +106 -0
  8. data/Rakefile +34 -0
  9. data/active-encode.gemspec +29 -0
  10. data/lib/active-encode.rb +1 -0
  11. data/lib/active_encode.rb +2 -0
  12. data/lib/active_encode/base.rb +16 -0
  13. data/lib/active_encode/callbacks.rb +69 -0
  14. data/lib/active_encode/core.rb +81 -0
  15. data/lib/active_encode/engine_adapter.rb +52 -0
  16. data/lib/active_encode/engine_adapters.rb +26 -0
  17. data/lib/active_encode/engine_adapters/active_job_adapter.rb +23 -0
  18. data/lib/active_encode/engine_adapters/inline_adapter.rb +42 -0
  19. data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +290 -0
  20. data/lib/active_encode/engine_adapters/test_adapter.rb +38 -0
  21. data/lib/active_encode/engine_adapters/zencoder_adapter.rb +147 -0
  22. data/lib/active_encode/status.rb +38 -0
  23. data/lib/active_encode/technical_metadata.rb +11 -0
  24. data/lib/active_encode/version.rb +3 -0
  25. data/spec/fixtures/Bars_512kb.mp4 +0 -0
  26. data/spec/fixtures/matterhorn/cancelled_response.xml +323 -0
  27. data/spec/fixtures/matterhorn/completed_response.xml +4 -0
  28. data/spec/fixtures/matterhorn/create_response.xml +300 -0
  29. data/spec/fixtures/matterhorn/delete_track_response.xml +2 -0
  30. data/spec/fixtures/matterhorn/failed_response.xml +4 -0
  31. data/spec/fixtures/matterhorn/purged_response.xml +342 -0
  32. data/spec/fixtures/matterhorn/running_response.xml +1 -0
  33. data/spec/fixtures/matterhorn/stop_completed_response.xml +228 -0
  34. data/spec/fixtures/matterhorn/stop_running_response.xml +339 -0
  35. data/spec/fixtures/zencoder/job_create.json +1 -0
  36. data/spec/fixtures/zencoder/job_details_cancelled.json +1 -0
  37. data/spec/fixtures/zencoder/job_details_completed.json +1 -0
  38. data/spec/fixtures/zencoder/job_details_create.json +1 -0
  39. data/spec/fixtures/zencoder/job_details_failed.json +73 -0
  40. data/spec/fixtures/zencoder/job_details_running.json +1 -0
  41. data/spec/fixtures/zencoder/job_progress_cancelled.json +13 -0
  42. data/spec/fixtures/zencoder/job_progress_completed.json +13 -0
  43. data/spec/fixtures/zencoder/job_progress_create.json +1 -0
  44. data/spec/fixtures/zencoder/job_progress_failed.json +13 -0
  45. data/spec/fixtures/zencoder/job_progress_running.json +1 -0
  46. data/spec/integration/matterhorn_adapter_spec.rb +186 -0
  47. data/spec/integration/zencoder_adapter_spec.rb +153 -0
  48. data/spec/spec_helper.rb +12 -0
  49. data/spec/units/callbacks_spec.rb +67 -0
  50. data/spec/units/engine_adapter_spec.rb +68 -0
  51. data/spec/units/status_spec.rb +64 -0
  52. metadata +221 -0
@@ -0,0 +1,69 @@
1
+ require 'active_support/callbacks'
2
+
3
+ module ActiveEncode
4
+ # = Active Encode Callbacks
5
+ #
6
+ # Active Encode provides hooks during the life cycle of an encode. Callbacks allow you
7
+ # to trigger logic during the life cycle of an encode. Available callbacks are:
8
+ #
9
+ # * <tt>before_create</tt>
10
+ # * <tt>around_create</tt>
11
+ # * <tt>after_create</tt>
12
+ # * <tt>before_cancel</tt>
13
+ # * <tt>around_cancel</tt>
14
+ # * <tt>after_cancel</tt>
15
+ # * <tt>before_purge</tt>
16
+ # * <tt>around_purge</tt>
17
+ # * <tt>after_purge</tt>
18
+ #
19
+ module Callbacks
20
+ extend ActiveSupport::Concern
21
+ include ActiveSupport::Callbacks
22
+
23
+ included do
24
+ define_callbacks :create
25
+ define_callbacks :cancel
26
+ define_callbacks :purge
27
+ end
28
+
29
+ # These methods will be included into any Active Encode object, adding
30
+ # callbacks for +create+, +cancel+, and +purge+ methods.
31
+ module ClassMethods
32
+ def before_create(*filters, &blk)
33
+ set_callback(:create, :before, *filters, &blk)
34
+ end
35
+
36
+ def after_create(*filters, &blk)
37
+ set_callback(:create, :after, *filters, &blk)
38
+ end
39
+
40
+ def around_create(*filters, &blk)
41
+ set_callback(:create, :around, *filters, &blk)
42
+ end
43
+
44
+ def before_cancel(*filters, &blk)
45
+ set_callback(:cancel, :before, *filters, &blk)
46
+ end
47
+
48
+ def after_cancel(*filters, &blk)
49
+ set_callback(:cancel, :after, *filters, &blk)
50
+ end
51
+
52
+ def around_cancel(*filters, &blk)
53
+ set_callback(:cancel, :around, *filters, &blk)
54
+ end
55
+
56
+ def before_purge(*filters, &blk)
57
+ set_callback(:purge, :before, *filters, &blk)
58
+ end
59
+
60
+ def after_purge(*filters, &blk)
61
+ set_callback(:purge, :after, *filters, &blk)
62
+ end
63
+
64
+ def around_purge(*filters, &blk)
65
+ set_callback(:purge, :around, *filters, &blk)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,81 @@
1
+ require 'active_support'
2
+ require 'active_encode/callbacks'
3
+
4
+ module ActiveEncode
5
+ module Core
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Encode Identifier
10
+ attr_accessor :id
11
+
12
+ # Encode input
13
+ attr_accessor :input
14
+
15
+ # Encode output(s)
16
+ attr_accessor :output
17
+
18
+ # Encode options
19
+ attr_accessor :options
20
+ end
21
+
22
+ module ClassMethods
23
+ def default_options(input)
24
+ {}
25
+ end
26
+
27
+ def create(input, options = nil)
28
+ object = new(input, options)
29
+ object.create!
30
+ end
31
+
32
+ def find(id)
33
+ engine_adapter.find(id, cast: self)
34
+ end
35
+
36
+ def list(*filters)
37
+ engine_adapter.list(filters)
38
+ end
39
+ end
40
+
41
+ def initialize(input, options = nil)
42
+ @input = input
43
+ @options = options || self.class.default_options(input)
44
+ end
45
+
46
+ def create!
47
+ run_callbacks :create do
48
+ self.class.engine_adapter.create self
49
+ end
50
+ end
51
+
52
+ def cancel!
53
+ run_callbacks :cancel do
54
+ self.class.engine_adapter.cancel self
55
+ end
56
+ end
57
+
58
+ def purge!
59
+ run_callbacks :purge do
60
+ self.class.engine_adapter.purge self
61
+ end
62
+ end
63
+
64
+ def remove_output! output_id
65
+ self.class.engine_adapter.remove_output self, output_id
66
+ end
67
+
68
+ def reload
69
+ fresh_encode = self.class.engine_adapter.find(id, cast: self.class)
70
+ @id = fresh_encode.id
71
+ @input = fresh_encode.input
72
+ @output = fresh_encode.output
73
+ @state = fresh_encode.state
74
+ @current_operations = fresh_encode.current_operations
75
+ @errors = fresh_encode.errors
76
+ @tech_metadata = fresh_encode.tech_metadata
77
+
78
+ self
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,52 @@
1
+ require 'active_encode/engine_adapters'
2
+ require 'active_support/core_ext/class/attribute'
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ module ActiveEncode
6
+ # The <tt>ActiveEncode::EngineAdapter</tt> module is used to load the
7
+ # correct adapter. The default engine adapter is the :active_job engine.
8
+ module EngineAdapter #:nodoc:
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class_attribute :_engine_adapter, instance_accessor: false, instance_predicate: false
13
+ self.engine_adapter = :inline
14
+ end
15
+
16
+ # Includes the setter method for changing the active engine adapter.
17
+ module ClassMethods
18
+ def engine_adapter
19
+ _engine_adapter
20
+ end
21
+
22
+ # Specify the backend engine provider. The default engine adapter
23
+ # is the :inline engine. See QueueAdapters for more
24
+ # information.
25
+ def engine_adapter=(name_or_adapter_or_class)
26
+ self._engine_adapter = interpret_adapter(name_or_adapter_or_class)
27
+ end
28
+
29
+ private
30
+
31
+ def interpret_adapter(name_or_adapter_or_class)
32
+ case name_or_adapter_or_class
33
+ when Symbol, String
34
+ ActiveEncode::EngineAdapters.lookup(name_or_adapter_or_class).new
35
+ else
36
+ if engine_adapter?(name_or_adapter_or_class)
37
+ name_or_adapter_or_class
38
+ else
39
+ raise ArgumentError
40
+ end
41
+ end
42
+ end
43
+
44
+ ENGINE_ADAPTER_METHODS = [:create, :find, :list, :cancel, :purge, :remove_output].freeze
45
+
46
+ def engine_adapter?(object)
47
+ ENGINE_ADAPTER_METHODS.all? { |meth| object.respond_to?(meth) }
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveEncode
2
+ # == Active Encode adapters
3
+ #
4
+ # Active Encode has adapters for the following engines:
5
+ #
6
+ #
7
+ #
8
+ module EngineAdapters
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :ActiveJobAdapter
12
+ autoload :MatterhornAdapter
13
+ autoload :InlineAdapter
14
+ autoload :ZencoderAdapter
15
+ autoload :TestAdapter
16
+
17
+ ADAPTER = 'Adapter'.freeze
18
+ private_constant :ADAPTER
19
+
20
+ class << self
21
+ def lookup(name)
22
+ const_get(name.to_s.camelize << ADAPTER)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveEncode
2
+ module EngineAdapters
3
+ class ActiveJobAdapter
4
+ def create(encode)
5
+ end
6
+
7
+ def find(id, opts = {})
8
+ end
9
+
10
+ def list(*filters)
11
+ end
12
+
13
+ def cancel(encode)
14
+ end
15
+
16
+ def purge(encode)
17
+ end
18
+
19
+ def remove_output(encode, output_id)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ module ActiveEncode
2
+ module EngineAdapters
3
+ class InlineAdapter
4
+ class_attribute :encodes, instance_accessor: false, instance_predicate: false
5
+ InlineAdapter.encodes ||= {}
6
+
7
+ def create(encode)
8
+ encode.id = SecureRandom.uuid
9
+ self.class.encodes[encode.id] = encode
10
+ #start encode
11
+ encode.state = :running
12
+ encode
13
+ end
14
+
15
+ def find(id, opts = {})
16
+ self.class.encodes[id]
17
+ end
18
+
19
+ def list(*filters)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def cancel(encode)
24
+ inline_encode = self.class.encodes[encode.id]
25
+ return if inline_encode.nil?
26
+ inline_encode.state = :cancelled
27
+ #cancel encode
28
+ inline_encode
29
+ end
30
+
31
+ def purge(encode)
32
+ self.class.encodes.delete encode.id
33
+ end
34
+
35
+ def remove_output(encode, output_id)
36
+ inline_encode = self.class.encodes[encode.id]
37
+ return if inline_encode.nil?
38
+ inline_encode.output.delete(inline_encode.output.find {|o| o[:id] == output_id})
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,290 @@
1
+ require 'rubyhorn'
2
+
3
+ module ActiveEncode
4
+ module EngineAdapters
5
+ class MatterhornAdapter
6
+ DEFAULT_ARGS = {'flavor' => 'presenter/source'}
7
+
8
+ def create(encode)
9
+ workflow_id = encode.options[:preset] || "full"
10
+ workflow_om = if encode.input.is_a? Hash
11
+ createMultipleFiles(encode.input, workflow_id)
12
+ else
13
+ Rubyhorn.client.addMediaPackageWithUrl(DEFAULT_ARGS.merge({'workflow' => workflow_id, 'url' => encode.input, 'filename' => File.basename(encode.input), 'title' => File.basename(encode.input)}))
14
+ end
15
+ build_encode(get_workflow(workflow_om), encode.class)
16
+ end
17
+
18
+ def find(id, opts = {})
19
+ build_encode(fetch_workflow(id), opts[:cast])
20
+ end
21
+
22
+ def list(*filters)
23
+ raise NotImplementedError #TODO implement this
24
+ end
25
+
26
+ def cancel(encode)
27
+ workflow_om = Rubyhorn.client.stop(encode.id)
28
+ build_encode(get_workflow(workflow_om), encode.class)
29
+ end
30
+
31
+ def purge(encode)
32
+ workflow_om = Rubyhorn.client.stop(encode.id) rescue nil
33
+ workflow_om ||= Rubyhorn.client.get_stopped_workflow(encode.id) rescue nil
34
+ purged_workflow = purge_outputs(get_workflow(workflow_om))
35
+ #Rubyhorn.client.delete_instance(encode.id) #Delete is not working so workflow instances can always be retrieved later!
36
+ build_encode(purged_workflow, encode.class)
37
+ end
38
+
39
+ def remove_output(encode, output_id)
40
+ workflow = fetch_workflow(encode.id)
41
+ output = encode.output.find {|o| o[:id] == output_id}
42
+ return if output.nil?
43
+ purge_output(workflow, output_id)
44
+ output
45
+ end
46
+
47
+ private
48
+ def fetch_workflow(id)
49
+ workflow_om = begin
50
+ Rubyhorn.client.instance_xml(id)
51
+ rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
52
+ nil
53
+ end
54
+
55
+ workflow_om ||= begin
56
+ Rubyhorn.client.get_stopped_workflow(id)
57
+ rescue
58
+ nil
59
+ end
60
+
61
+ get_workflow(workflow_om)
62
+ end
63
+
64
+ def get_workflow(workflow_om)
65
+ return nil if workflow_om.nil?
66
+ if workflow_om.ng_xml.is_a? Nokogiri::XML::Document
67
+ workflow_om.ng_xml.remove_namespaces!.root
68
+ else
69
+ workflow_om.ng_xml
70
+ end
71
+ end
72
+
73
+ def build_encode(workflow, cast)
74
+ return nil if workflow.nil?
75
+ encode = cast.new(convert_input(workflow), convert_options(workflow))
76
+ encode.id = convert_id(workflow)
77
+ encode.state = convert_state(workflow)
78
+ encode.current_operations = convert_current_operations(workflow)
79
+ encode.percent_complete = calculate_percent_complete(workflow)
80
+ encode.output = convert_output(workflow, encode.options)
81
+ encode.errors = convert_errors(workflow)
82
+ encode.tech_metadata = convert_tech_metadata(workflow)
83
+ encode
84
+ end
85
+
86
+ def convert_id(workflow)
87
+ workflow.attribute('id').to_s
88
+ end
89
+
90
+ def convert_state(workflow)
91
+ case workflow.attribute('state').to_s
92
+ when "INSTANTIATED", "RUNNING" #Should there be a queued state?
93
+ :running
94
+ when "STOPPED"
95
+ :cancelled
96
+ when "FAILED"
97
+ workflow.xpath('//operation[@state="FAILED"]').empty? ? :cancelled : :failed
98
+ when "SUCCEEDED", "SKIPPED" #Should there be a errored state?
99
+ :completed
100
+ end
101
+ end
102
+
103
+ def convert_input(workflow)
104
+ #Need to do anything else since this is a MH url? and this disappears when a workflow is cleaned up
105
+ workflow.xpath('mediapackage/media/track[@type="presenter/source"]/url/text()').to_s
106
+ end
107
+
108
+ def convert_tech_metadata(workflow)
109
+ convert_track_metadata(workflow.xpath('//track[@type="presenter/source"]').first)
110
+ end
111
+
112
+ def convert_output(workflow, options)
113
+ output = []
114
+ workflow.xpath('//track[@type="presenter/delivery" and tags/tag[text()="streaming"]]').each do |track|
115
+ label = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
116
+ url = track.at("url/text()").to_s
117
+ if url.start_with? "rtmp"
118
+ url = File.join(options[:stream_base], MatterhornRtmpUrl.parse(url).to_path) if options[:stream_base]
119
+ end
120
+ track_id = track.at("@id").to_s
121
+ output << convert_track_metadata(track).merge({id: track_id, url: url, label: label})
122
+ end
123
+ output
124
+ end
125
+
126
+ def convert_current_operations(workflow)
127
+ current_op = workflow.xpath('//operation[@state!="INSTANTIATED"]/@description').last.to_s
128
+ current_op.present? ? [current_op] : []
129
+ end
130
+
131
+ def convert_errors(workflow)
132
+ workflow.xpath('//errors/error/text()').map(&:to_s)
133
+ end
134
+
135
+ def convert_options(workflow)
136
+ options = {}
137
+ options[:preset] = workflow.xpath('template/text()').to_s
138
+ options[:stream_base] = workflow.xpath('//properties/property[@key="avalon.stream_base"]/text()').to_s if workflow.xpath('//properties/property[@key="avalon.stream_base"]/text()').present? #this is avalon-felix specific
139
+ options
140
+ end
141
+
142
+ def convert_track_metadata(track)
143
+ return {} if track.nil?
144
+ metadata = {}
145
+ metadata[:mime_type] = track.at("mimetype/text()").to_s if track.at('mimetype')
146
+ metadata[:checksum] = track.at("checksum/text()").to_s if track.at('checksum')
147
+ metadata[:duration] = track.at("duration/text()").to_s if track.at('duration')
148
+ if track.at('audio')
149
+ metadata[:audio_codec] = track.at("audio/encoder/@type").to_s
150
+ metadata[:audio_channels] = track.at("audio/channels/text()").to_s
151
+ metadata[:audio_bitrate] = track.at("audio/bitrate/text()").to_s
152
+ end
153
+ if track.at('video')
154
+ metadata[:video_codec] = track.at("video/encoder/@type").to_s
155
+ metadata[:video_bitrate] = track.at("video/bitrate/text()").to_s
156
+ metadata[:video_framerate] = track.at("video/framerate/text()").to_s
157
+ metadata[:width] = track.at("video/resolution/text()").to_s.split('x')[0]
158
+ metadata[:height] = track.at("video/resolution/text()").to_s.split('x')[1]
159
+ end
160
+ metadata
161
+ end
162
+
163
+ def get_media_package(workflow)
164
+ mp = workflow.xpath('//mediapackage')
165
+ first_node = mp.first
166
+ first_node['xmlns'] = 'http://mediapackage.opencastproject.org'
167
+ mp
168
+ end
169
+
170
+ def purge_outputs(workflow)
171
+ #Delete hls tracks first since the next, more general xpath matches them as well
172
+ workflow.xpath('//track[@type="presenter/delivery" and tags/tag[text()="streaming"] and tags/tag[text()="hls"]]/@id').map(&:to_s).each do |hls_track_id|
173
+ purge_output(workflow, hls_track_id) rescue nil
174
+ end
175
+ workflow.xpath('//track[@type="presenter/delivery" and tags/tag[text()="streaming"]]/@id').map(&:to_s).each do |track_id|
176
+ purge_output(workflow, track_id) rescue nil
177
+ end
178
+
179
+ workflow
180
+ end
181
+
182
+ def purge_output(workflow, track_id)
183
+ media_package = get_media_package(workflow)
184
+ hls = workflow.xpath("//track[@id='#{track_id}']/tags/tag[text()='hls']").present?
185
+ job_url = if hls
186
+ Rubyhorn.client.delete_hls_track(media_package, track_id)
187
+ else
188
+ Rubyhorn.client.delete_track(media_package, track_id)
189
+ end
190
+ sleep(0.1)
191
+ job_status = Nokogiri::XML(Rubyhorn.client.get(URI(job_url).path)).root.attribute("status").value()
192
+ #FIXME have this return a boolean based upon result of operation
193
+ case job_status
194
+ when "FINISHED"
195
+ workflow.at_xpath("//track[@id=\"#{track_id}\"]").remove
196
+ when "FAILED"
197
+ workflow.at_xpath('//errors').add_child("<error>Output not purged: #{mp.at_xpath("//*[@id=\"#{track_id}\"]/tags/tag[starts-with(text(),\"quality\")]/text()").to_s}</error>")
198
+ end
199
+ end
200
+
201
+ def calculate_percent_complete workflow
202
+ totals = {
203
+ :transcode => 70,
204
+ :distribution => 20,
205
+ :other => 10
206
+ }
207
+
208
+ completed_transcode_operations = workflow.xpath('//operation[@id="compose" and (@state="SUCCEEDED" or @state="SKIPPED")]').size
209
+ total_transcode_operations = workflow.xpath('//operation[@id="compose"]').size
210
+ total_transcode_operations = 1 if total_transcode_operations == 0
211
+ completed_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute") and (@state="SUCCEEDED" or @state="SKIPPED")]').size
212
+ total_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute")]').size
213
+ total_distribution_operations = 1 if total_distribution_operations == 0
214
+ completed_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute")) and (@state="SUCCEEDED" or @state="SKIPPED")]').size
215
+ total_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute"))]').size
216
+ total_other_operations = 1 if total_other_operations == 0
217
+
218
+ ((totals[:transcode].to_f / total_transcode_operations) * completed_transcode_operations) +
219
+ ((totals[:distribution].to_f / total_distribution_operations) * completed_distribution_operations) +
220
+ ((totals[:other].to_f / total_other_operations) * completed_other_operations)
221
+ end
222
+
223
+ def createMultipleFiles(input, workflow_id)
224
+ #Create empty media package xml document
225
+ mp = Rubyhorn.client.createMediaPackage
226
+
227
+ #Next line associates workflow title to avalon via masterfile pid
228
+ title = File.basename(input.values.first)
229
+ dc = Nokogiri::XML('<dublincore xmlns="http://www.opencastproject.org/xsd/1.0/dublincore/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dcterms:title>' + title + '</dcterms:title></dublincore>')
230
+ mp = Rubyhorn.client.addDCCatalog({'mediaPackage' => mp.to_xml, 'dublinCore' => dc.to_xml, 'flavor' => 'dublincore/episode'})
231
+
232
+ #Add quality levels - repeated for each supplied file url
233
+ input.each_pair do |quality, url|
234
+ mp = Rubyhorn.client.addTrack({'mediaPackage' => mp.to_xml, 'url' => url, 'flavor' => DEFAULT_ARGS['flavor']})
235
+ #Rewrite track to include quality tag
236
+ #Get the empty tags element under the newly added track
237
+ tags = mp.xpath('//xmlns:track/xmlns:tags[not(node())]', 'xmlns' => 'http://mediapackage.opencastproject.org').first
238
+ qualityTag = Nokogiri::XML::Node.new 'tag', mp
239
+ qualityTag.content = quality
240
+ tags.add_child qualityTag
241
+ end
242
+ #Finally ingest the media package
243
+ begin
244
+ Rubyhorn.client.start({"definitionId" => workflow_id, "mediapackage" => mp.to_xml})
245
+ rescue Rubyhorn::RestClient::Exceptions::HTTPBadRequest
246
+ #make this two calls...one to get the workflow definition xml and then the second to submit it along with the mediapackage to start...due to unsolved issue with some MH installs
247
+ begin
248
+ workflow_definition_xml = Rubyhorn.client.definition_xml(workflow_id)
249
+ Rubyhorn.client.start({"definition" => workflow_definition_xml, "mediapackage" => mp.to_xml})
250
+ rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
251
+ raise StandardError.new("Unable to start workflow")
252
+ end
253
+ end
254
+ end
255
+ end
256
+
257
+ class MatterhornRtmpUrl < Struct.new(:application, :prefix, :media_id, :stream_id, :filename, :extension)
258
+
259
+ REGEX = %r{^
260
+ /(?<application>.+) # application (avalon)
261
+ /(?:(?<prefix>.+):)? # prefix (mp4:)
262
+ (?<media_id>[^\/]+) # media_id (98285a5b-603a-4a14-acc0-20e37a3514bb)
263
+ /(?<stream_id>[^\/]+) # stream_id (b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3)
264
+ /(?<filename>.+?) # filename (MVI_0057)
265
+ (?:\.(?<extension>.+))?$ # extension (mp4)
266
+ }x
267
+
268
+ def initialize(hash)
269
+ super(*members.map {|member| hash[member]})
270
+ end
271
+
272
+ def self.parse(url_string)
273
+ # Example input: /avalon/mp4:98285a5b-603a-4a14-acc0-20e37a3514bb/b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3/MVI_0057.mp4
274
+
275
+ uri = URI.parse(url_string)
276
+ match_data = REGEX.match(uri.path)
277
+ MatterhornRtmpUrl.new match_data
278
+ end
279
+
280
+ alias_method :'_binding', :'binding'
281
+ def binding
282
+ _binding
283
+ end
284
+
285
+ def to_path
286
+ File.join(media_id, stream_id, "#{filename}.#{extension||prefix}")
287
+ end
288
+ end
289
+ end
290
+ end