active_encode 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +16 -0
  4. data/.rubocop.yml +76 -0
  5. data/.travis.yml +10 -0
  6. data/Gemfile +11 -0
  7. data/LICENSE +202 -0
  8. data/README.md +106 -0
  9. data/Rakefile +38 -0
  10. data/active_encode.gemspec +28 -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 +79 -0
  15. data/lib/active_encode/engine_adapter.rb +51 -0
  16. data/lib/active_encode/engine_adapters.rb +27 -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 +312 -0
  20. data/lib/active_encode/engine_adapters/shingoncoder_adapter.rb +56 -0
  21. data/lib/active_encode/engine_adapters/test_adapter.rb +38 -0
  22. data/lib/active_encode/engine_adapters/zencoder_adapter.rb +143 -0
  23. data/lib/active_encode/status.rb +38 -0
  24. data/lib/active_encode/technical_metadata.rb +11 -0
  25. data/lib/active_encode/version.rb +3 -0
  26. data/spec/fixtures/Bars_512kb.mp4 +0 -0
  27. data/spec/fixtures/matterhorn/cancelled_response.xml +323 -0
  28. data/spec/fixtures/matterhorn/completed_response.xml +4 -0
  29. data/spec/fixtures/matterhorn/create_response.xml +300 -0
  30. data/spec/fixtures/matterhorn/delete_track_response.xml +2 -0
  31. data/spec/fixtures/matterhorn/failed_response.xml +4 -0
  32. data/spec/fixtures/matterhorn/purged_response.xml +342 -0
  33. data/spec/fixtures/matterhorn/running_response.xml +1 -0
  34. data/spec/fixtures/matterhorn/stop_completed_response.xml +228 -0
  35. data/spec/fixtures/matterhorn/stop_running_response.xml +339 -0
  36. data/spec/fixtures/zencoder/job_create.json +1 -0
  37. data/spec/fixtures/zencoder/job_details_cancelled.json +1 -0
  38. data/spec/fixtures/zencoder/job_details_completed.json +1 -0
  39. data/spec/fixtures/zencoder/job_details_create.json +1 -0
  40. data/spec/fixtures/zencoder/job_details_failed.json +73 -0
  41. data/spec/fixtures/zencoder/job_details_running.json +1 -0
  42. data/spec/fixtures/zencoder/job_progress_cancelled.json +13 -0
  43. data/spec/fixtures/zencoder/job_progress_completed.json +13 -0
  44. data/spec/fixtures/zencoder/job_progress_create.json +1 -0
  45. data/spec/fixtures/zencoder/job_progress_failed.json +13 -0
  46. data/spec/fixtures/zencoder/job_progress_running.json +1 -0
  47. data/spec/integration/matterhorn_adapter_spec.rb +186 -0
  48. data/spec/integration/shingoncoder_adapter_spec.rb +152 -0
  49. data/spec/integration/zencoder_adapter_spec.rb +152 -0
  50. data/spec/spec_helper.rb +12 -0
  51. data/spec/units/callbacks_spec.rb +66 -0
  52. data/spec/units/engine_adapter_spec.rb +78 -0
  53. data/spec/units/status_spec.rb +62 -0
  54. metadata +210 -0
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'active_encode/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "active_encode"
8
+ spec.version = ActiveEncode::VERSION
9
+ spec.authors = ["Michael Klein, Chris Colvard"]
10
+ spec.email = ["mbklein@gmail.com, chris.colvard@gmail.com"]
11
+ spec.summary = %q{Declare encode job classes that can be run by a variety of encoding services}
12
+ spec.description = %q{This gem serves as the basis for the interface between a Ruby (Rails) application and a provider of transcoding services such as Opencast Matterhorn, Zencoder, and Amazon Elastic Transcoder.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "coveralls"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency "rspec-its", "~> 1.2"
28
+ end
@@ -0,0 +1,2 @@
1
+ require 'active_encode/version'
2
+ require 'active_encode/base'
@@ -0,0 +1,16 @@
1
+ require 'active_encode/core'
2
+ require 'active_encode/engine_adapter'
3
+ require 'active_encode/status'
4
+ require 'active_encode/technical_metadata'
5
+ require 'active_encode/callbacks'
6
+ # require 'active_encode/logging'
7
+
8
+ module ActiveEncode #:nodoc:
9
+ class Base
10
+ include Core
11
+ include Status
12
+ include TechnicalMetadata
13
+ include EngineAdapter
14
+ include Callbacks
15
+ end
16
+ end
@@ -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,79 @@
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
+ delegate :list, to: :engine_adapter
37
+ end
38
+
39
+ def initialize(input, options = nil)
40
+ @input = input
41
+ @options = options || self.class.default_options(input)
42
+ end
43
+
44
+ def create!
45
+ run_callbacks :create do
46
+ self.class.engine_adapter.create self
47
+ end
48
+ end
49
+
50
+ def cancel!
51
+ run_callbacks :cancel do
52
+ self.class.engine_adapter.cancel self
53
+ end
54
+ end
55
+
56
+ def purge!
57
+ run_callbacks :purge do
58
+ self.class.engine_adapter.purge self
59
+ end
60
+ end
61
+
62
+ def remove_output!(output_id)
63
+ self.class.engine_adapter.remove_output self, output_id
64
+ end
65
+
66
+ def reload
67
+ fresh_encode = self.class.engine_adapter.find(id, cast: self.class)
68
+ @id = fresh_encode.id
69
+ @input = fresh_encode.input
70
+ @output = fresh_encode.output
71
+ @state = fresh_encode.state
72
+ @current_operations = fresh_encode.current_operations
73
+ @errors = fresh_encode.errors
74
+ @tech_metadata = fresh_encode.tech_metadata
75
+
76
+ self
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,51 @@
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
+ fail 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
+ end
50
+ end
51
+ end
@@ -0,0 +1,27 @@
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 :ShingoncoderAdapter
16
+ autoload :TestAdapter
17
+
18
+ ADAPTER = 'Adapter'.freeze
19
+ private_constant :ADAPTER
20
+
21
+ class << self
22
+ def lookup(name)
23
+ const_get(name.to_s.camelize << ADAPTER)
24
+ end
25
+ end
26
+ end
27
+ 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
+ fail 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,312 @@
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
+ create_multiple_files(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
+ fail 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 = begin
33
+ Rubyhorn.client.stop(encode.id)
34
+ rescue
35
+ nil
36
+ end
37
+ workflow_om ||= begin
38
+ Rubyhorn.client.get_stopped_workflow(encode.id)
39
+ rescue
40
+ nil
41
+ end
42
+ purged_workflow = purge_outputs(get_workflow(workflow_om))
43
+ # Rubyhorn.client.delete_instance(encode.id) #Delete is not working so workflow instances can always be retrieved later!
44
+ build_encode(purged_workflow, encode.class)
45
+ end
46
+
47
+ def remove_output(encode, output_id)
48
+ workflow = fetch_workflow(encode.id)
49
+ output = encode.output.find { |o| o[:id] == output_id }
50
+ return if output.nil?
51
+ purge_output(workflow, output_id)
52
+ output
53
+ end
54
+
55
+ private
56
+
57
+ def fetch_workflow(id)
58
+ workflow_om = begin
59
+ Rubyhorn.client.instance_xml(id)
60
+ rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
61
+ nil
62
+ end
63
+
64
+ workflow_om ||= begin
65
+ Rubyhorn.client.get_stopped_workflow(id)
66
+ rescue
67
+ nil
68
+ end
69
+
70
+ get_workflow(workflow_om)
71
+ end
72
+
73
+ def get_workflow(workflow_om)
74
+ return nil if workflow_om.nil?
75
+ if workflow_om.ng_xml.is_a? Nokogiri::XML::Document
76
+ workflow_om.ng_xml.remove_namespaces!.root
77
+ else
78
+ workflow_om.ng_xml
79
+ end
80
+ end
81
+
82
+ def build_encode(workflow, cast)
83
+ return nil if workflow.nil?
84
+ encode = cast.new(convert_input(workflow), convert_options(workflow))
85
+ encode.id = convert_id(workflow)
86
+ encode.state = convert_state(workflow)
87
+ encode.current_operations = convert_current_operations(workflow)
88
+ encode.percent_complete = calculate_percent_complete(workflow)
89
+ encode.output = convert_output(workflow, encode.options)
90
+ encode.errors = convert_errors(workflow)
91
+ encode.tech_metadata = convert_tech_metadata(workflow)
92
+ encode
93
+ end
94
+
95
+ def convert_id(workflow)
96
+ workflow.attribute('id').to_s
97
+ end
98
+
99
+ def convert_state(workflow)
100
+ case workflow.attribute('state').to_s
101
+ when "INSTANTIATED", "RUNNING" # Should there be a queued state?
102
+ :running
103
+ when "STOPPED"
104
+ :cancelled
105
+ when "FAILED"
106
+ workflow.xpath('//operation[@state="FAILED"]').empty? ? :cancelled : :failed
107
+ when "SUCCEEDED", "SKIPPED" # Should there be a errored state?
108
+ :completed
109
+ end
110
+ end
111
+
112
+ def convert_input(workflow)
113
+ # Need to do anything else since this is a MH url? and this disappears when a workflow is cleaned up
114
+ workflow.xpath('mediapackage/media/track[@type="presenter/source"]/url/text()').to_s
115
+ end
116
+
117
+ def convert_tech_metadata(workflow)
118
+ convert_track_metadata(workflow.xpath('//track[@type="presenter/source"]').first)
119
+ end
120
+
121
+ def convert_output(workflow, options)
122
+ output = []
123
+ workflow.xpath('//track[@type="presenter/delivery" and tags/tag[text()="streaming"]]').each do |track|
124
+ label = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
125
+ url = track.at("url/text()").to_s
126
+ if url.start_with? "rtmp"
127
+ url = File.join(options[:stream_base], MatterhornRtmpUrl.parse(url).to_path) if options[:stream_base]
128
+ end
129
+ track_id = track.at("@id").to_s
130
+ output << convert_track_metadata(track).merge(id: track_id, url: url, label: label)
131
+ end
132
+ output
133
+ end
134
+
135
+ def convert_current_operations(workflow)
136
+ current_op = workflow.xpath('//operation[@state!="INSTANTIATED"]/@description').last.to_s
137
+ current_op.present? ? [current_op] : []
138
+ end
139
+
140
+ def convert_errors(workflow)
141
+ workflow.xpath('//errors/error/text()').map(&:to_s)
142
+ end
143
+
144
+ def convert_options(workflow)
145
+ options = {}
146
+ options[:preset] = workflow.xpath('template/text()').to_s
147
+ 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
148
+ options
149
+ end
150
+
151
+ def convert_track_metadata(track)
152
+ return {} if track.nil?
153
+ metadata = {}
154
+ metadata[:mime_type] = track.at("mimetype/text()").to_s if track.at('mimetype')
155
+ metadata[:checksum] = track.at("checksum/text()").to_s if track.at('checksum')
156
+ metadata[:duration] = track.at("duration/text()").to_s if track.at('duration')
157
+ if track.at('audio')
158
+ metadata[:audio_codec] = track.at("audio/encoder/@type").to_s
159
+ metadata[:audio_channels] = track.at("audio/channels/text()").to_s
160
+ metadata[:audio_bitrate] = track.at("audio/bitrate/text()").to_s
161
+ end
162
+ if track.at('video')
163
+ metadata[:video_codec] = track.at("video/encoder/@type").to_s
164
+ metadata[:video_bitrate] = track.at("video/bitrate/text()").to_s
165
+ metadata[:video_framerate] = track.at("video/framerate/text()").to_s
166
+ metadata[:width] = track.at("video/resolution/text()").to_s.split('x')[0]
167
+ metadata[:height] = track.at("video/resolution/text()").to_s.split('x')[1]
168
+ end
169
+ metadata
170
+ end
171
+
172
+ def get_media_package(workflow)
173
+ mp = workflow.xpath('//mediapackage')
174
+ first_node = mp.first
175
+ first_node['xmlns'] = 'http://mediapackage.opencastproject.org'
176
+ mp
177
+ end
178
+
179
+ def purge_outputs(workflow)
180
+ # Delete hls tracks first since the next, more general xpath matches them as well
181
+ 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|
182
+ begin
183
+ purge_output(workflow, hls_track_id)
184
+ rescue
185
+ nil
186
+ end
187
+ end
188
+ workflow.xpath('//track[@type="presenter/delivery" and tags/tag[text()="streaming"]]/@id').map(&:to_s).each do |track_id|
189
+ begin
190
+ purge_output(workflow, track_id)
191
+ rescue
192
+ nil
193
+ end
194
+ end
195
+
196
+ workflow
197
+ end
198
+
199
+ def purge_output(workflow, track_id)
200
+ media_package = get_media_package(workflow)
201
+ hls = workflow.xpath("//track[@id='#{track_id}']/tags/tag[text()='hls']").present?
202
+ job_url = if hls
203
+ Rubyhorn.client.delete_hls_track(media_package, track_id)
204
+ else
205
+ Rubyhorn.client.delete_track(media_package, track_id)
206
+ end
207
+ sleep(0.1)
208
+ job_status = Nokogiri::XML(Rubyhorn.client.get(URI(job_url).path)).root.attribute("status").value
209
+ # FIXME: have this return a boolean based upon result of operation
210
+ case job_status
211
+ when "FINISHED"
212
+ workflow.at_xpath("//track[@id=\"#{track_id}\"]").remove
213
+ when "FAILED"
214
+ workflow.at_xpath('//errors').add_child("<error>Output not purged: #{mp.at_xpath("//*[@id=\"#{track_id}\"]/tags/tag[starts-with(text(),\"quality\")]/text()")}</error>")
215
+ end
216
+ end
217
+
218
+ def calculate_percent_complete(workflow)
219
+ totals = {
220
+ transcode: 70,
221
+ distribution: 20,
222
+ other: 10
223
+ }
224
+
225
+ completed_transcode_operations = workflow.xpath('//operation[@id="compose" and (@state="SUCCEEDED" or @state="SKIPPED")]').size
226
+ total_transcode_operations = workflow.xpath('//operation[@id="compose"]').size
227
+ total_transcode_operations = 1 if total_transcode_operations == 0
228
+ completed_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute") and (@state="SUCCEEDED" or @state="SKIPPED")]').size
229
+ total_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute")]').size
230
+ total_distribution_operations = 1 if total_distribution_operations == 0
231
+ completed_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute")) and (@state="SUCCEEDED" or @state="SKIPPED")]').size
232
+ total_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute"))]').size
233
+ total_other_operations = 1 if total_other_operations == 0
234
+
235
+ ((totals[:transcode].to_f / total_transcode_operations) * completed_transcode_operations) +
236
+ ((totals[:distribution].to_f / total_distribution_operations) * completed_distribution_operations) +
237
+ ((totals[:other].to_f / total_other_operations) * completed_other_operations)
238
+ end
239
+
240
+ def create_multiple_files(input, workflow_id)
241
+ # Create empty media package xml document
242
+ mp = Rubyhorn.client.createMediaPackage
243
+
244
+ # Next line associates workflow title to avalon via masterfile pid
245
+ title = File.basename(input.values.first)
246
+ 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>')
247
+ mp = Rubyhorn.client.addDCCatalog('mediaPackage' => mp.to_xml, 'dublinCore' => dc.to_xml, 'flavor' => 'dublincore/episode')
248
+
249
+ # Add quality levels - repeated for each supplied file url
250
+ input.each_pair do |quality, url|
251
+ mp = Rubyhorn.client.addTrack('mediaPackage' => mp.to_xml, 'url' => url, 'flavor' => DEFAULT_ARGS['flavor'])
252
+ # Rewrite track to include quality tag
253
+ # Get the empty tags element under the newly added track
254
+ tags = mp.xpath('//xmlns:track/xmlns:tags[not(node())]', 'xmlns' => 'http://mediapackage.opencastproject.org').first
255
+ quality_tag = Nokogiri::XML::Node.new 'tag', mp
256
+ quality_tag.content = quality
257
+ tags.add_child quality_tag
258
+ end
259
+ # Finally ingest the media package
260
+ begin
261
+ Rubyhorn.client.start("definitionId" => workflow_id, "mediapackage" => mp.to_xml)
262
+ rescue Rubyhorn::RestClient::Exceptions::HTTPBadRequest
263
+ # 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
264
+ begin
265
+ workflow_definition_xml = Rubyhorn.client.definition_xml(workflow_id)
266
+ Rubyhorn.client.start("definition" => workflow_definition_xml, "mediapackage" => mp.to_xml)
267
+ rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
268
+ raise StandardError, "Unable to start workflow"
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ class MatterhornRtmpUrl
275
+ class_attribute :members
276
+ self.members = [:application, :prefix, :media_id, :stream_id, :filename, :extension]
277
+ attr_accessor(*members)
278
+ REGEX = %r{^
279
+ /(?<application>.+) # application (avalon)
280
+ /(?:(?<prefix>.+):)? # prefix (mp4:)
281
+ (?<media_id>[^\/]+) # media_id (98285a5b-603a-4a14-acc0-20e37a3514bb)
282
+ /(?<stream_id>[^\/]+) # stream_id (b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3)
283
+ /(?<filename>.+?) # filename (MVI_0057)
284
+ (?:\.(?<extension>.+))?$ # extension (mp4)
285
+ }x
286
+
287
+ # @param [MatchData] match_data
288
+ def initialize(match_data)
289
+ self.class.members.each do |key|
290
+ send("#{key}=", match_data[key])
291
+ end
292
+ end
293
+
294
+ def self.parse(url_string)
295
+ # Example input: /avalon/mp4:98285a5b-603a-4a14-acc0-20e37a3514bb/b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3/MVI_0057.mp4
296
+
297
+ uri = URI.parse(url_string)
298
+ match_data = REGEX.match(uri.path)
299
+ MatterhornRtmpUrl.new match_data
300
+ end
301
+
302
+ alias_method :_binding, :binding
303
+ def binding
304
+ _binding
305
+ end
306
+
307
+ def to_path
308
+ File.join(media_id, stream_id, "#{filename}.#{extension || prefix}")
309
+ end
310
+ end
311
+ end
312
+ end