active_encode 0.8.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +26 -17
  3. data/.rubocop.yml +7 -3
  4. data/.rubocop_todo.yml +8 -1
  5. data/CONTRIBUTING.md +42 -12
  6. data/Gemfile +11 -11
  7. data/README.md +64 -10
  8. data/active_encode.gemspec +2 -4
  9. data/app/controllers/active_encode/encode_record_controller.rb +1 -1
  10. data/app/jobs/active_encode/polling_job.rb +1 -1
  11. data/app/models/active_encode/encode_record.rb +1 -1
  12. data/guides/media_convert_adapter.md +208 -0
  13. data/lib/active_encode/base.rb +1 -1
  14. data/lib/active_encode/core.rb +14 -14
  15. data/lib/active_encode/engine_adapter.rb +13 -13
  16. data/lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb +158 -158
  17. data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +14 -3
  18. data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +204 -202
  19. data/lib/active_encode/engine_adapters/media_convert_adapter.rb +421 -217
  20. data/lib/active_encode/engine_adapters/media_convert_output.rb +67 -5
  21. data/lib/active_encode/engine_adapters/pass_through_adapter.rb +3 -3
  22. data/lib/active_encode/engine_adapters/zencoder_adapter.rb +114 -114
  23. data/lib/active_encode/errors.rb +1 -1
  24. data/lib/active_encode/persistence.rb +19 -19
  25. data/lib/active_encode/version.rb +1 -1
  26. data/lib/file_locator.rb +6 -6
  27. data/spec/fixtures/ffmpeg/cancelled-id/exit_status.code +1 -0
  28. data/spec/fixtures/ffmpeg/completed-id/exit_status.code +1 -0
  29. data/spec/fixtures/ffmpeg/completed-with-warnings-id/error.log +3 -0
  30. data/spec/fixtures/ffmpeg/completed-with-warnings-id/exit_status.code +1 -0
  31. data/spec/fixtures/ffmpeg/completed-with-warnings-id/input_metadata +102 -0
  32. data/spec/fixtures/ffmpeg/completed-with-warnings-id/output_metadata-high +90 -0
  33. data/spec/fixtures/ffmpeg/completed-with-warnings-id/output_metadata-low +90 -0
  34. data/spec/fixtures/ffmpeg/completed-with-warnings-id/pid +1 -0
  35. data/spec/fixtures/ffmpeg/completed-with-warnings-id/progress +11 -0
  36. data/spec/fixtures/ffmpeg/completed-with-warnings-id/video-high.mp4 +0 -0
  37. data/spec/fixtures/ffmpeg/completed-with-warnings-id/video-low.mp4 +0 -0
  38. data/spec/fixtures/ffmpeg/failed-id/exit_status.code +1 -0
  39. data/spec/integration/ffmpeg_adapter_spec.rb +50 -1
  40. data/spec/integration/matterhorn_adapter_spec.rb +1 -2
  41. data/spec/integration/media_convert_adapter_spec.rb +91 -0
  42. data/spec/integration/pass_through_adapter_spec.rb +2 -2
  43. data/spec/integration/zencoder_adapter_spec.rb +3 -3
  44. data/spec/units/core_spec.rb +1 -1
  45. data/spec/units/file_locator_spec.rb +3 -3
  46. data/spec/units/status_spec.rb +1 -1
  47. metadata +50 -19
@@ -2,6 +2,7 @@
2
2
  require 'fileutils'
3
3
  require 'nokogiri'
4
4
  require 'shellwords'
5
+ require 'addressable/uri'
5
6
 
6
7
  module ActiveEncode
7
8
  module EngineAdapters
@@ -14,7 +15,7 @@ module ActiveEncode
14
15
  # Decode file uris for ffmpeg (mediainfo works either way)
15
16
  case input_url
16
17
  when /^file\:\/\/\//
17
- input_url = URI.decode(input_url)
18
+ input_url = Addressable::URI.unencode(input_url)
18
19
  when /^s3\:\/\//
19
20
  require 'file_locator'
20
21
 
@@ -63,6 +64,9 @@ module ActiveEncode
63
64
 
64
65
  # Run the ffmpeg command and save its pid
65
66
  command = ffmpeg_command(input_url, new_encode.id, options)
67
+ # Capture the exit status in a file in order to differentiate warning output in stderr between real process failure
68
+ exit_status_file = working_path("exit_status.code", new_encode.id)
69
+ command = "#{command}; echo $? > #{exit_status_file}"
66
70
  pid = Process.spawn(command, err: working_path('error.log', new_encode.id))
67
71
  File.open(working_path("pid", new_encode.id), 'w') { |file| file.write pid }
68
72
  new_encode.input.id = pid
@@ -73,7 +77,7 @@ module ActiveEncode
73
77
  new_encode.percent_complete = 1
74
78
  new_encode.errors = [e.full_message]
75
79
  write_errors new_encode
76
- return new_encode
80
+ new_encode
77
81
  ensure
78
82
  # Prevent zombie process
79
83
  Process.detach(pid) if pid.present?
@@ -96,7 +100,8 @@ module ActiveEncode
96
100
  encode.current_operations = []
97
101
  encode.created_at, encode.updated_at = get_times encode.id
98
102
  encode.errors = read_errors(id)
99
- if encode.errors.present?
103
+ exit_code = read_exit_code(id)
104
+ if exit_code.present? && exit_code != 0 && exit_code != 143
100
105
  encode.state = :failed
101
106
  elsif running? pid
102
107
  encode.state = :running
@@ -171,6 +176,12 @@ module ActiveEncode
171
176
  end
172
177
  end
173
178
 
179
+ def read_exit_code(id)
180
+ exit_path = working_path("exit_status.code", id)
181
+ exit_status = File.read(exit_path) if File.file? exit_path
182
+ exit_status.present? ? exit_status.to_i : nil # process still running
183
+ end
184
+
174
185
  def build_input(encode)
175
186
  input = ActiveEncode::Input.new
176
187
  metadata = get_tech_metadata(working_path("input_metadata", encode.id))
@@ -23,238 +23,240 @@ module ActiveEncode
23
23
 
24
24
  private
25
25
 
26
- def fetch_workflow(id)
27
- workflow_om = begin
28
- Rubyhorn.client.instance_xml(id)
29
- rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
30
- nil
31
- end
32
-
33
- workflow_om ||= begin
34
- Rubyhorn.client.get_stopped_workflow(id)
35
- rescue
36
- nil
37
- end
38
-
39
- get_workflow(workflow_om)
26
+ def fetch_workflow(id)
27
+ workflow_om = begin
28
+ Rubyhorn.client.instance_xml(id)
29
+ rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
30
+ nil
40
31
  end
41
32
 
42
- def get_workflow(workflow_om)
43
- return nil if workflow_om.nil?
44
- if workflow_om.ng_xml.is_a? Nokogiri::XML::Document
45
- workflow_om.ng_xml.remove_namespaces!.root
46
- else
47
- workflow_om.ng_xml
48
- end
33
+ workflow_om ||= begin
34
+ Rubyhorn.client.get_stopped_workflow(id)
35
+ rescue
36
+ nil
49
37
  end
50
38
 
51
- def build_encode(workflow)
52
- return nil if workflow.nil?
53
- input_url = convert_input(workflow)
54
- input_url = get_workflow_title(workflow) if input_url.blank?
55
- encode = ActiveEncode::Base.new(input_url, convert_options(workflow))
56
- encode.id = convert_id(workflow)
57
- encode.state = convert_state(workflow)
58
- encode.current_operations = convert_current_operations(workflow)
59
- encode.percent_complete = calculate_percent_complete(workflow)
60
- encode.created_at = convert_created_at(workflow)
61
- encode.updated_at = convert_updated_at(workflow) || encode.created_at
62
- encode.output = convert_output(workflow, encode.options)
63
- encode.errors = convert_errors(workflow)
64
-
65
- encode.input.id = "presenter/source"
66
- encode.input.state = encode.state
67
- encode.input.created_at = encode.created_at
68
- encode.input.updated_at = encode.updated_at
69
- tech_md = convert_tech_metadata(workflow)
70
- [:width, :height, :duration, :frame_rate, :checksum, :audio_codec, :video_codec,
71
- :audio_bitrate, :video_bitrate].each do |field|
72
- encode.input.send("#{field}=", tech_md[field])
73
- end
39
+ get_workflow(workflow_om)
40
+ end
74
41
 
75
- encode
42
+ def get_workflow(workflow_om)
43
+ return nil if workflow_om.nil?
44
+ if workflow_om.ng_xml.is_a? Nokogiri::XML::Document
45
+ workflow_om.ng_xml.remove_namespaces!.root
46
+ else
47
+ workflow_om.ng_xml
76
48
  end
49
+ end
77
50
 
78
- def convert_id(workflow)
79
- workflow.attribute('id').to_s
51
+ def build_encode(workflow)
52
+ return nil if workflow.nil?
53
+ input_url = convert_input(workflow)
54
+ input_url = get_workflow_title(workflow) if input_url.blank?
55
+ encode = ActiveEncode::Base.new(input_url, convert_options(workflow))
56
+ encode.id = convert_id(workflow)
57
+ encode.state = convert_state(workflow)
58
+ encode.current_operations = convert_current_operations(workflow)
59
+ encode.percent_complete = calculate_percent_complete(workflow)
60
+ encode.created_at = convert_created_at(workflow)
61
+ encode.updated_at = convert_updated_at(workflow) || encode.created_at
62
+ encode.output = convert_output(workflow, encode.options)
63
+ encode.errors = convert_errors(workflow)
64
+
65
+ encode.input.id = "presenter/source"
66
+ encode.input.state = encode.state
67
+ encode.input.created_at = encode.created_at
68
+ encode.input.updated_at = encode.updated_at
69
+ tech_md = convert_tech_metadata(workflow)
70
+ [:width, :height, :duration, :frame_rate, :checksum, :audio_codec, :video_codec,
71
+ :audio_bitrate, :video_bitrate].each do |field|
72
+ encode.input.send("#{field}=", tech_md[field])
80
73
  end
81
74
 
82
- def get_workflow_state(workflow)
83
- workflow.attribute('state').to_s
84
- end
75
+ encode
76
+ end
85
77
 
86
- def convert_state(workflow)
87
- case get_workflow_state(workflow)
88
- when "INSTANTIATED", "RUNNING" # Should there be a queued state?
89
- :running
90
- when "STOPPED"
91
- :cancelled
92
- when "FAILED"
93
- workflow.xpath('//operation[@state="FAILED"]').empty? ? :cancelled : :failed
94
- when "SUCCEEDED", "SKIPPED" # Should there be a errored state?
95
- :completed
96
- end
97
- end
78
+ def convert_id(workflow)
79
+ workflow.attribute('id').to_s
80
+ end
98
81
 
99
- def convert_input(workflow)
100
- # Need to do anything else since this is a MH url? and this disappears when a workflow is cleaned up
101
- workflow.xpath('mediapackage/media/track[@type="presenter/source"]/url/text()').to_s.strip
102
- end
82
+ def get_workflow_state(workflow)
83
+ workflow.attribute('state').to_s
84
+ end
103
85
 
104
- def get_workflow_title(workflow)
105
- workflow.xpath('mediapackage/title/text()').to_s.strip
86
+ def convert_state(workflow)
87
+ case get_workflow_state(workflow)
88
+ when "INSTANTIATED", "RUNNING" # Should there be a queued state?
89
+ :running
90
+ when "STOPPED"
91
+ :cancelled
92
+ when "FAILED"
93
+ workflow.xpath('//operation[@state="FAILED"]').empty? ? :cancelled : :failed
94
+ when "SUCCEEDED", "SKIPPED" # Should there be a errored state?
95
+ :completed
106
96
  end
97
+ end
107
98
 
108
- def convert_tech_metadata(workflow)
109
- convert_track_metadata(workflow.xpath('//track[@type="presenter/source"]').first)
110
- end
99
+ def convert_input(workflow)
100
+ # Need to do anything else since this is a MH url? and this disappears when a workflow is cleaned up
101
+ workflow.xpath('mediapackage/media/track[@type="presenter/source"]/url/text()').to_s.strip
102
+ end
103
+
104
+ def get_workflow_title(workflow)
105
+ workflow.xpath('mediapackage/title/text()').to_s.strip
106
+ end
111
107
 
112
- def convert_output(workflow, options)
113
- outputs = []
114
- workflow.xpath('//track[@type="presenter/delivery" and tags/tag[text()="streaming"]]').each do |track|
115
- output = ActiveEncode::Output.new
116
- output.label = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
117
- output.url = track.at("url/text()").to_s
118
- if output.url.start_with? "rtmp"
119
- output.url = File.join(options[:stream_base], MatterhornRtmpUrl.parse(output.url).to_path) if options[:stream_base]
120
- end
121
- output.id = track.at("@id").to_s
122
-
123
- tech_md = convert_track_metadata(track)
124
- [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
125
- :audio_bitrate, :video_bitrate, :file_size].each do |field|
126
- output.send("#{field}=", tech_md[field])
127
- end
128
-
129
- output.state = :completed
130
- output.created_at = convert_output_created_at(track, workflow)
131
- output.updated_at = convert_output_updated_at(track, workflow)
132
-
133
- outputs << output
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
+ outputs = []
114
+ workflow.xpath('//track[@type="presenter/delivery" and tags/tag[text()="streaming"]]').each do |track|
115
+ output = ActiveEncode::Output.new
116
+ output.label = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
117
+ output.url = track.at("url/text()").to_s
118
+ if output.url.start_with? "rtmp"
119
+ output.url = File.join(options[:stream_base], MatterhornRtmpUrl.parse(output.url).to_path) if options[:stream_base]
134
120
  end
135
- outputs
136
- end
121
+ output.id = track.at("@id").to_s
137
122
 
138
- def convert_current_operations(workflow)
139
- current_op = workflow.xpath('//operation[@state!="INSTANTIATED"]/@description').last.to_s
140
- current_op.present? ? [current_op] : []
141
- end
123
+ tech_md = convert_track_metadata(track)
124
+ [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
125
+ :audio_bitrate, :video_bitrate, :file_size].each do |field|
126
+ output.send("#{field}=", tech_md[field])
127
+ end
142
128
 
143
- def convert_errors(workflow)
144
- workflow.xpath('//errors/error/text()').map(&:to_s)
145
- end
129
+ output.state = :completed
130
+ output.created_at = convert_output_created_at(track, workflow)
131
+ output.updated_at = convert_output_updated_at(track, workflow)
146
132
 
147
- def convert_created_at(workflow)
148
- created_at = workflow.xpath('mediapackage/@start').last.to_s
149
- created_at.present? ? Time.parse(created_at).utc : nil
133
+ outputs << output
150
134
  end
135
+ outputs
136
+ end
151
137
 
152
- def convert_updated_at(workflow)
153
- updated_at = workflow.xpath('//operation[@state!="INSTANTIATED"]/completed/text()').last.to_s
154
- updated_at.present? ? Time.strptime(updated_at, "%Q") : nil
155
- end
138
+ def convert_current_operations(workflow)
139
+ current_op = workflow.xpath('//operation[@state!="INSTANTIATED"]/@description').last.to_s
140
+ current_op.present? ? [current_op] : []
141
+ end
156
142
 
157
- def convert_output_created_at(track, workflow)
158
- quality = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
159
- created_at = workflow.xpath("//operation[@id=\"compose\"][configurations/configuration[@key=\"target-tags\" and contains(text(), \"#{quality}\")]]/started/text()").to_s
160
- created_at.present? ? Time.at(created_at.to_i / 1000.0).utc : nil
161
- end
143
+ def convert_errors(workflow)
144
+ workflow.xpath('//errors/error/text()').map(&:to_s)
145
+ end
162
146
 
163
- def convert_output_updated_at(track, workflow)
164
- quality = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
165
- updated_at = workflow.xpath("//operation[@id=\"compose\"][configurations/configuration[@key=\"target-tags\" and contains(text(), \"#{quality}\")]]/completed/text()").to_s
166
- updated_at.present? ? Time.at(updated_at.to_i / 1000.0).utc : nil
167
- end
147
+ def convert_created_at(workflow)
148
+ created_at = workflow.xpath('mediapackage/@start').last.to_s
149
+ created_at.present? ? Time.parse(created_at).utc : nil
150
+ end
168
151
 
169
- def convert_options(workflow)
170
- options = {}
171
- options[:preset] = workflow.xpath('template/text()').to_s
172
- 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
173
- options
174
- end
152
+ def convert_updated_at(workflow)
153
+ updated_at = workflow.xpath('//operation[@state!="INSTANTIATED"]/completed/text()').last.to_s
154
+ updated_at.present? ? Time.strptime(updated_at, "%Q") : nil
155
+ end
175
156
 
176
- def convert_track_metadata(track)
177
- return {} if track.nil?
178
- metadata = {}
179
- # metadata[:mime_type] = track.at("mimetype/text()").to_s if track.at('mimetype')
180
- metadata[:checksum] = track.at("checksum/text()").to_s.strip if track.at('checksum')
181
- metadata[:duration] = track.at("duration/text()").to_s.to_i if track.at('duration')
182
- if track.at('audio')
183
- metadata[:audio_codec] = track.at("audio/encoder/@type").to_s
184
- metadata[:audio_channels] = track.at("audio/channels/text()").to_s
185
- metadata[:audio_bitrate] = track.at("audio/bitrate/text()").to_s.to_f
186
- end
187
- if track.at('video')
188
- metadata[:video_codec] = track.at("video/encoder/@type").to_s
189
- metadata[:video_bitrate] = track.at("video/bitrate/text()").to_s.to_f
190
- metadata[:frame_rate] = track.at("video/framerate/text()").to_s.to_f
191
- metadata[:width] = track.at("video/resolution/text()").to_s.split('x')[0].to_i
192
- metadata[:height] = track.at("video/resolution/text()").to_s.split('x')[1].to_i
193
- end
194
- metadata
195
- end
157
+ def convert_output_created_at(track, workflow)
158
+ quality = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
159
+ created_at = workflow.xpath("//operation[@id=\"compose\"][configurations/configuration[@key=\"target-tags\" and contains(text(), \"#{quality}\")]]/started/text()").to_s
160
+ created_at.present? ? Time.at(created_at.to_i / 1000.0).utc : nil
161
+ end
196
162
 
197
- def get_media_package(workflow)
198
- mp = workflow.xpath('//mediapackage')
199
- first_node = mp.first
200
- first_node['xmlns'] = 'http://mediapackage.opencastproject.org'
201
- mp
202
- end
163
+ def convert_output_updated_at(track, workflow)
164
+ quality = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
165
+ updated_at = workflow.xpath("//operation[@id=\"compose\"][configurations/configuration[@key=\"target-tags\" and contains(text(), \"#{quality}\")]]/completed/text()").to_s
166
+ updated_at.present? ? Time.at(updated_at.to_i / 1000.0).utc : nil
167
+ end
203
168
 
204
- def calculate_percent_complete(workflow)
205
- totals = {
206
- transcode: 70,
207
- distribution: 20,
208
- other: 10
209
- }
210
-
211
- completed_transcode_operations = workflow.xpath('//operation[@id="compose" and (@state="SUCCEEDED" or @state="SKIPPED")]').size
212
- total_transcode_operations = workflow.xpath('//operation[@id="compose"]').size
213
- total_transcode_operations = 1 if total_transcode_operations.zero?
214
- completed_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute") and (@state="SUCCEEDED" or @state="SKIPPED")]').size
215
- total_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute")]').size
216
- total_distribution_operations = 1 if total_distribution_operations.zero?
217
- completed_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute")) and (@state="SUCCEEDED" or @state="SKIPPED")]').size
218
- total_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute"))]').size
219
- total_other_operations = 1 if total_other_operations.zero?
220
-
221
- ((totals[:transcode].to_f / total_transcode_operations) * completed_transcode_operations) +
222
- ((totals[:distribution].to_f / total_distribution_operations) * completed_distribution_operations) +
223
- ((totals[:other].to_f / total_other_operations) * completed_other_operations)
169
+ def convert_options(workflow)
170
+ options = {}
171
+ options[:preset] = workflow.xpath('template/text()').to_s
172
+ if workflow.xpath('//properties/property[@key="avalon.stream_base"]/text()').present?
173
+ options[:stream_base] = workflow.xpath('//properties/property[@key="avalon.stream_base"]/text()').to_s
174
+ end # this is avalon-felix specific
175
+ options
176
+ end
177
+
178
+ def convert_track_metadata(track)
179
+ return {} if track.nil?
180
+ metadata = {}
181
+ # metadata[:mime_type] = track.at("mimetype/text()").to_s if track.at('mimetype')
182
+ metadata[:checksum] = track.at("checksum/text()").to_s.strip if track.at('checksum')
183
+ metadata[:duration] = track.at("duration/text()").to_s.to_i if track.at('duration')
184
+ if track.at('audio')
185
+ metadata[:audio_codec] = track.at("audio/encoder/@type").to_s
186
+ metadata[:audio_channels] = track.at("audio/channels/text()").to_s
187
+ metadata[:audio_bitrate] = track.at("audio/bitrate/text()").to_s.to_f
224
188
  end
189
+ if track.at('video')
190
+ metadata[:video_codec] = track.at("video/encoder/@type").to_s
191
+ metadata[:video_bitrate] = track.at("video/bitrate/text()").to_s.to_f
192
+ metadata[:frame_rate] = track.at("video/framerate/text()").to_s.to_f
193
+ metadata[:width] = track.at("video/resolution/text()").to_s.split('x')[0].to_i
194
+ metadata[:height] = track.at("video/resolution/text()").to_s.split('x')[1].to_i
195
+ end
196
+ metadata
197
+ end
225
198
 
226
- def create_multiple_files(input, workflow_id)
227
- # Create empty media package xml document
228
- mp = Rubyhorn.client.createMediaPackage
229
-
230
- # Next line associates workflow title to avalon via masterfile pid
231
- title = File.basename(input.values.first)
232
- 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>')
233
- mp = Rubyhorn.client.addDCCatalog('mediaPackage' => mp.to_xml, 'dublinCore' => dc.to_xml, 'flavor' => 'dublincore/episode')
234
-
235
- # Add quality levels - repeated for each supplied file url
236
- input.each_pair do |quality, url|
237
- mp = Rubyhorn.client.addTrack('mediaPackage' => mp.to_xml, 'url' => url, 'flavor' => DEFAULT_ARGS['flavor'])
238
- # Rewrite track to include quality tag
239
- # Get the empty tags element under the newly added track
240
- tags = mp.xpath('//xmlns:track/xmlns:tags[not(node())]', 'xmlns' => 'http://mediapackage.opencastproject.org').first
241
- quality_tag = Nokogiri::XML::Node.new 'tag', mp
242
- quality_tag.content = quality
243
- tags.add_child quality_tag
244
- end
245
- # Finally ingest the media package
199
+ def get_media_package(workflow)
200
+ mp = workflow.xpath('//mediapackage')
201
+ first_node = mp.first
202
+ first_node['xmlns'] = 'http://mediapackage.opencastproject.org'
203
+ mp
204
+ end
205
+
206
+ def calculate_percent_complete(workflow)
207
+ totals = {
208
+ transcode: 70,
209
+ distribution: 20,
210
+ other: 10
211
+ }
212
+
213
+ completed_transcode_operations = workflow.xpath('//operation[@id="compose" and (@state="SUCCEEDED" or @state="SKIPPED")]').size
214
+ total_transcode_operations = workflow.xpath('//operation[@id="compose"]').size
215
+ total_transcode_operations = 1 if total_transcode_operations.zero?
216
+ completed_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute") and (@state="SUCCEEDED" or @state="SKIPPED")]').size
217
+ total_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute")]').size
218
+ total_distribution_operations = 1 if total_distribution_operations.zero?
219
+ completed_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute")) and (@state="SUCCEEDED" or @state="SKIPPED")]').size
220
+ total_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute"))]').size
221
+ total_other_operations = 1 if total_other_operations.zero?
222
+
223
+ ((totals[:transcode].to_f / total_transcode_operations) * completed_transcode_operations) +
224
+ ((totals[:distribution].to_f / total_distribution_operations) * completed_distribution_operations) +
225
+ ((totals[:other].to_f / total_other_operations) * completed_other_operations)
226
+ end
227
+
228
+ def create_multiple_files(input, workflow_id)
229
+ # Create empty media package xml document
230
+ mp = Rubyhorn.client.createMediaPackage
231
+
232
+ # Next line associates workflow title to avalon via masterfile pid
233
+ title = File.basename(input.values.first)
234
+ 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>')
235
+ mp = Rubyhorn.client.addDCCatalog('mediaPackage' => mp.to_xml, 'dublinCore' => dc.to_xml, 'flavor' => 'dublincore/episode')
236
+
237
+ # Add quality levels - repeated for each supplied file url
238
+ input.each_pair do |quality, url|
239
+ mp = Rubyhorn.client.addTrack('mediaPackage' => mp.to_xml, 'url' => url, 'flavor' => DEFAULT_ARGS['flavor'])
240
+ # Rewrite track to include quality tag
241
+ # Get the empty tags element under the newly added track
242
+ tags = mp.xpath('//xmlns:track/xmlns:tags[not(node())]', 'xmlns' => 'http://mediapackage.opencastproject.org').first
243
+ quality_tag = Nokogiri::XML::Node.new 'tag', mp
244
+ quality_tag.content = quality
245
+ tags.add_child quality_tag
246
+ end
247
+ # Finally ingest the media package
248
+ begin
249
+ Rubyhorn.client.start("definitionId" => workflow_id, "mediapackage" => mp.to_xml)
250
+ rescue Rubyhorn::RestClient::Exceptions::HTTPBadRequest
251
+ # 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
246
252
  begin
247
- Rubyhorn.client.start("definitionId" => workflow_id, "mediapackage" => mp.to_xml)
248
- rescue Rubyhorn::RestClient::Exceptions::HTTPBadRequest
249
- # 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
250
- begin
251
- workflow_definition_xml = Rubyhorn.client.definition_xml(workflow_id)
252
- Rubyhorn.client.start("definition" => workflow_definition_xml, "mediapackage" => mp.to_xml)
253
- rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
254
- raise StandardError, "Unable to start workflow"
255
- end
253
+ workflow_definition_xml = Rubyhorn.client.definition_xml(workflow_id)
254
+ Rubyhorn.client.start("definition" => workflow_definition_xml, "mediapackage" => mp.to_xml)
255
+ rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
256
+ raise StandardError, "Unable to start workflow"
256
257
  end
257
- end
258
+ end
259
+ end
258
260
  end
259
261
 
260
262
  class MatterhornRtmpUrl
@@ -268,7 +270,7 @@ module ActiveEncode
268
270
  /(?<stream_id>[^\/]+) # stream_id (b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3)
269
271
  /(?<filename>.+?) # filename (MVI_0057)
270
272
  (?:\.(?<extension>.+))?$ # extension (mp4)
271
- }x
273
+ }x.freeze
272
274
 
273
275
  # @param [MatchData] match_data
274
276
  def initialize(match_data)