active_encode 1.2.3 → 2.0.0
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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +39 -21
- data/.rubocop_todo.yml +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +1 -14
- data/README.md +9 -3
- data/active_encode.gemspec +4 -2
- data/lib/active_encode/engine.rb +12 -0
- data/lib/active_encode/engine_adapters/ffmpeg_adapter/cleaner.rb +5 -1
- data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +64 -6
- data/lib/active_encode/engine_adapters/media_convert_adapter.rb +95 -48
- data/lib/active_encode/engine_adapters/media_convert_output.rb +35 -14
- data/lib/active_encode/engine_adapters/pass_through_adapter.rb +6 -1
- data/lib/active_encode/engine_adapters.rb +0 -2
- data/lib/active_encode/filename_sanitizer.rb +3 -2
- data/lib/active_encode/technical_metadata.rb +13 -1
- data/lib/active_encode/version.rb +1 -1
- metadata +34 -11
- data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +0 -300
- data/lib/active_encode/engine_adapters/zencoder_adapter.rb +0 -156
@@ -35,8 +35,8 @@ module ActiveEncode
|
|
35
35
|
# @param output_detail_settings [Aws::MediaConvert::Types::OutputDetail]
|
36
36
|
def tech_metadata_from_settings(output_url:, output_settings:, output_detail_settings:)
|
37
37
|
{
|
38
|
-
width: output_detail_settings.video_details
|
39
|
-
height: output_detail_settings.video_details
|
38
|
+
width: output_detail_settings.video_details&.width_in_px,
|
39
|
+
height: output_detail_settings.video_details&.height_in_px,
|
40
40
|
frame_rate: extract_video_frame_rate(output_settings),
|
41
41
|
duration: output_detail_settings.duration_in_ms,
|
42
42
|
audio_codec: extract_audio_codec(output_settings),
|
@@ -44,7 +44,6 @@ module ActiveEncode
|
|
44
44
|
audio_bitrate: extract_audio_bitrate(output_settings),
|
45
45
|
video_bitrate: extract_video_bitrate(output_settings),
|
46
46
|
url: output_url,
|
47
|
-
label: (output_url ? File.basename(output_url) : output_settings.name_modifier),
|
48
47
|
suffix: output_settings.name_modifier
|
49
48
|
}
|
50
49
|
end
|
@@ -61,11 +60,33 @@ module ActiveEncode
|
|
61
60
|
audio_bitrate: extract_audio_bitrate(settings),
|
62
61
|
video_bitrate: extract_video_bitrate(settings),
|
63
62
|
url: url,
|
64
|
-
label: File.basename(url),
|
65
63
|
suffix: settings.name_modifier
|
66
64
|
}
|
67
65
|
end
|
68
66
|
|
67
|
+
def tech_metadata_from_probe(url:, probe_response:, output_settings: nil)
|
68
|
+
tech_md = { url: url, suffix: output_settings&.name_modifier }
|
69
|
+
return tech_md unless probe_response
|
70
|
+
|
71
|
+
# Need to determine which track has video/audio
|
72
|
+
video_track = probe_response.container.tracks&.find { |track| track.track_type == "video" }
|
73
|
+
audio_track = probe_response.container.tracks&.find { |track| track.track_type == "audio" }
|
74
|
+
frame_rate = (video_track.video_properties.frame_rate.numerator / video_track.video_properties.frame_rate.denominator.to_f).round(2) if video_track
|
75
|
+
duration = probe_response.container.duration * 1000 if probe_response.container.duration.present?
|
76
|
+
|
77
|
+
tech_md.merge({
|
78
|
+
width: video_track&.video_properties&.width,
|
79
|
+
height: video_track&.video_properties&.height,
|
80
|
+
frame_rate: frame_rate,
|
81
|
+
duration: duration, # milliseconds
|
82
|
+
audio_codec: audio_track&.codec,
|
83
|
+
video_codec: video_track&.codec,
|
84
|
+
audio_bitrate: audio_track&.audio_properties&.bit_rate,
|
85
|
+
video_bitrate: video_track&.video_properties&.bit_rate,
|
86
|
+
file_size: probe_response.metadata.file_size
|
87
|
+
})
|
88
|
+
end
|
89
|
+
|
69
90
|
# constructs an `s3:` output URL from the MediaConvert job params, the same
|
70
91
|
# way MediaConvert will.
|
71
92
|
#
|
@@ -110,27 +131,27 @@ module ActiveEncode
|
|
110
131
|
end
|
111
132
|
|
112
133
|
def extract_audio_codec(settings)
|
113
|
-
settings.audio_descriptions
|
114
|
-
rescue
|
115
|
-
nil
|
134
|
+
settings.audio_descriptions&.first&.codec_settings&.codec
|
116
135
|
end
|
117
136
|
|
118
137
|
def extract_audio_codec_settings(settings)
|
119
|
-
|
138
|
+
codec = extract_audio_codec(settings)
|
139
|
+
return nil if codec.nil?
|
140
|
+
|
141
|
+
codec_key = AUDIO_SETTINGS[codec]
|
120
142
|
settings.audio_descriptions.first.codec_settings[codec_key]
|
121
143
|
end
|
122
144
|
|
123
145
|
def extract_video_codec(settings)
|
124
|
-
settings.video_description
|
125
|
-
rescue
|
126
|
-
nil
|
146
|
+
settings.video_description&.codec_settings&.codec
|
127
147
|
end
|
128
148
|
|
129
149
|
def extract_video_codec_settings(settings)
|
130
|
-
|
150
|
+
codec = extract_video_codec(settings)
|
151
|
+
return nil if codec.nil?
|
152
|
+
|
153
|
+
codec_key = VIDEO_SETTINGS[codec]
|
131
154
|
settings.video_description.codec_settings[codec_key]
|
132
|
-
rescue
|
133
|
-
nil
|
134
155
|
end
|
135
156
|
|
136
157
|
def extract_audio_bitrate(settings)
|
@@ -234,13 +234,18 @@ module ActiveEncode
|
|
234
234
|
doc.remove_namespaces!
|
235
235
|
duration = get_xpath_text(doc, '//Duration/text()', :to_f)
|
236
236
|
duration *= 1000 unless duration.nil? # Convert to milliseconds
|
237
|
+
audio_codec = get_xpath_text(doc, '//track[@type="Audio"]/CodecID/text()', :to_s)
|
238
|
+
if get_xpath_text(doc, '//track[@type="Audio"]/Format/text()', :to_s) == "MPEG Audio" &&
|
239
|
+
get_xpath_text(doc, '//track[@type="Audio"]/Format_Profile/text()', :to_s) == "Layer 3"
|
240
|
+
audio_codec ||= "mp3"
|
241
|
+
end
|
237
242
|
{ url: get_xpath_text(doc, '//media/@ref', :to_s),
|
238
243
|
width: get_xpath_text(doc, '//Width/text()', :to_f),
|
239
244
|
height: get_xpath_text(doc, '//Height/text()', :to_f),
|
240
245
|
frame_rate: get_xpath_text(doc, '//FrameRate/text()', :to_f),
|
241
246
|
duration: duration,
|
242
247
|
file_size: get_xpath_text(doc, '//FileSize/text()', :to_i),
|
243
|
-
audio_codec:
|
248
|
+
audio_codec: audio_codec,
|
244
249
|
audio_bitrate: get_xpath_text(doc, '//track[@type="Audio"]/BitRate/text()', :to_i),
|
245
250
|
video_codec: get_xpath_text(doc, '//track[@type="Video"]/CodecID/text()', :to_s),
|
246
251
|
video_bitrate: get_xpath_text(doc, '//track[@type="Video"]/BitRate/text()', :to_i) }
|
@@ -10,7 +10,8 @@ module ActiveEncode
|
|
10
10
|
filepath = input_url.is_a?(URI::HTTP) ? input_url.path : input_url
|
11
11
|
# Replace special characters with underscores and remove excess periods.
|
12
12
|
# This removes the extension before processing so it is safe to delete all detected periods.
|
13
|
-
|
13
|
+
# This explicitly handles percent encoded spaces.
|
14
|
+
File.basename(filepath, File.extname(filepath)).gsub(/%20/, "_").gsub(/[^0-9A-Za-z.\-\/]/, '_').delete('.')
|
14
15
|
end
|
15
16
|
|
16
17
|
def sanitize_filename(input_url)
|
@@ -24,7 +25,7 @@ module ActiveEncode
|
|
24
25
|
when /^file\:\/\/\//
|
25
26
|
input_url.to_s.gsub(/file:\/\//, '')
|
26
27
|
when /^s3\:\/\//
|
27
|
-
input_url.to_s.gsub(/#{Addressable::URI.parse(input_url).
|
28
|
+
input_url.to_s.gsub(/#{Addressable::URI.parse(input_url).site}/, '')
|
28
29
|
when /^https?:\/\//
|
29
30
|
input_url
|
30
31
|
end
|
@@ -22,13 +22,25 @@ module ActiveEncode
|
|
22
22
|
attr_accessor :video_codec
|
23
23
|
attr_accessor :audio_bitrate
|
24
24
|
attr_accessor :video_bitrate
|
25
|
+
|
26
|
+
# Array of hashes
|
27
|
+
attr_accessor :subtitles
|
28
|
+
attr_accessor :format
|
29
|
+
attr_accessor :language
|
25
30
|
end
|
26
31
|
|
27
32
|
def assign_tech_metadata(metadata)
|
28
33
|
[:width, :height, :frame_rate, :duration, :file_size, :checksum,
|
29
|
-
:audio_codec, :video_codec, :audio_bitrate, :video_bitrate
|
34
|
+
:audio_codec, :video_codec, :audio_bitrate, :video_bitrate, :subtitles,
|
35
|
+
:format, :language].each do |field|
|
30
36
|
send("#{field}=", metadata[field]) if metadata.key?(field)
|
31
37
|
end
|
32
38
|
end
|
39
|
+
|
40
|
+
def tech_metadata
|
41
|
+
[:width, :height, :frame_rate, :duration, :file_size, :checksum,
|
42
|
+
:audio_codec, :video_codec, :audio_bitrate, :video_bitrate, :subtitles,
|
43
|
+
:format, :language].index_with { |field| send(field) }
|
44
|
+
end
|
33
45
|
end
|
34
46
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_encode
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Klein, Chris Colvard, Phuong Dinh
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rails
|
@@ -38,6 +37,20 @@ dependencies:
|
|
38
37
|
- - "~>"
|
39
38
|
- !ruby/object:Gem::Version
|
40
39
|
version: '2.8'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: retriable
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
41
54
|
- !ruby/object:Gem::Dependency
|
42
55
|
name: aws-sdk-cloudwatchevents
|
43
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +79,20 @@ dependencies:
|
|
66
79
|
- - ">="
|
67
80
|
- !ruby/object:Gem::Version
|
68
81
|
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: aws-sdk-core
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "<="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 3.220.0
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "<="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 3.220.0
|
69
96
|
- !ruby/object:Gem::Dependency
|
70
97
|
name: aws-sdk-elastictranscoder
|
71
98
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,14 +113,14 @@ dependencies:
|
|
86
113
|
requirements:
|
87
114
|
- - ">="
|
88
115
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
116
|
+
version: 1.157.0
|
90
117
|
type: :development
|
91
118
|
prerelease: false
|
92
119
|
version_requirements: !ruby/object:Gem::Requirement
|
93
120
|
requirements:
|
94
121
|
- - ">="
|
95
122
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
123
|
+
version: 1.157.0
|
97
124
|
- !ruby/object:Gem::Dependency
|
98
125
|
name: aws-sdk-s3
|
99
126
|
requirement: !ruby/object:Gem::Requirement
|
@@ -241,7 +268,7 @@ dependencies:
|
|
241
268
|
- !ruby/object:Gem::Version
|
242
269
|
version: '0'
|
243
270
|
description: This gem provides an interface to transcoding services such as Ffmpeg,
|
244
|
-
Amazon Elastic Transcoder, or
|
271
|
+
Amazon Elastic Transcoder, or Amazon Elemental MediaConvert.
|
245
272
|
email:
|
246
273
|
- mbklein@gmail.com, chris.colvard@gmail.com, phuongdh@gmail.com
|
247
274
|
executables: []
|
@@ -279,12 +306,10 @@ files:
|
|
279
306
|
- lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb
|
280
307
|
- lib/active_encode/engine_adapters/ffmpeg_adapter.rb
|
281
308
|
- lib/active_encode/engine_adapters/ffmpeg_adapter/cleaner.rb
|
282
|
-
- lib/active_encode/engine_adapters/matterhorn_adapter.rb
|
283
309
|
- lib/active_encode/engine_adapters/media_convert_adapter.rb
|
284
310
|
- lib/active_encode/engine_adapters/media_convert_output.rb
|
285
311
|
- lib/active_encode/engine_adapters/pass_through_adapter.rb
|
286
312
|
- lib/active_encode/engine_adapters/test_adapter.rb
|
287
|
-
- lib/active_encode/engine_adapters/zencoder_adapter.rb
|
288
313
|
- lib/active_encode/errors.rb
|
289
314
|
- lib/active_encode/filename_sanitizer.rb
|
290
315
|
- lib/active_encode/global_id.rb
|
@@ -303,7 +328,6 @@ licenses:
|
|
303
328
|
- Apache-2.0
|
304
329
|
metadata:
|
305
330
|
rubygems_mfa_required: 'true'
|
306
|
-
post_install_message:
|
307
331
|
rdoc_options: []
|
308
332
|
require_paths:
|
309
333
|
- lib
|
@@ -318,8 +342,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
318
342
|
- !ruby/object:Gem::Version
|
319
343
|
version: '0'
|
320
344
|
requirements: []
|
321
|
-
rubygems_version: 3.
|
322
|
-
signing_key:
|
345
|
+
rubygems_version: 3.6.9
|
323
346
|
specification_version: 4
|
324
347
|
summary: Declare encode job classes that can be run by a variety of encoding services
|
325
348
|
test_files: []
|
@@ -1,300 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require 'rubyhorn'
|
3
|
-
|
4
|
-
module ActiveEncode
|
5
|
-
module EngineAdapters
|
6
|
-
class MatterhornAdapter
|
7
|
-
DEFAULT_ARGS = { 'flavor' => 'presenter/source' }.freeze
|
8
|
-
|
9
|
-
def create(input_url, options = {})
|
10
|
-
workflow_id = options[:preset] || "full"
|
11
|
-
workflow_om = Rubyhorn.client.addMediaPackageWithUrl(DEFAULT_ARGS.merge('workflow' => workflow_id, 'url' => input_url, 'filename' => File.basename(input_url), 'title' => File.basename(input_url)))
|
12
|
-
build_encode(get_workflow(workflow_om))
|
13
|
-
end
|
14
|
-
|
15
|
-
def find(id, _opts = {})
|
16
|
-
build_encode(fetch_workflow(id))
|
17
|
-
end
|
18
|
-
|
19
|
-
def cancel(id)
|
20
|
-
workflow_om = Rubyhorn.client.stop(id)
|
21
|
-
build_encode(get_workflow(workflow_om))
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
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)
|
40
|
-
end
|
41
|
-
|
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
|
49
|
-
end
|
50
|
-
|
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
|
74
|
-
|
75
|
-
encode
|
76
|
-
end
|
77
|
-
|
78
|
-
def convert_id(workflow)
|
79
|
-
workflow.attribute('id').to_s
|
80
|
-
end
|
81
|
-
|
82
|
-
def get_workflow_state(workflow)
|
83
|
-
workflow.attribute('state').to_s
|
84
|
-
end
|
85
|
-
|
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
|
98
|
-
|
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
|
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
|
-
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
|
134
|
-
end
|
135
|
-
outputs
|
136
|
-
end
|
137
|
-
|
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
|
142
|
-
|
143
|
-
def convert_errors(workflow)
|
144
|
-
workflow.xpath('//errors/error/text()').map(&:to_s)
|
145
|
-
end
|
146
|
-
|
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
|
151
|
-
|
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
|
156
|
-
|
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
|
162
|
-
|
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
|
168
|
-
|
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
|
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
|
198
|
-
|
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
|
252
|
-
begin
|
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"
|
257
|
-
end
|
258
|
-
end
|
259
|
-
end
|
260
|
-
end
|
261
|
-
|
262
|
-
class MatterhornRtmpUrl
|
263
|
-
class_attribute :members
|
264
|
-
self.members = %i[application prefix media_id stream_id filename extension]
|
265
|
-
attr_accessor(*members)
|
266
|
-
REGEX = %r{^
|
267
|
-
/(?<application>.+) # application (avalon)
|
268
|
-
/(?:(?<prefix>.+):)? # prefix (mp4:)
|
269
|
-
(?<media_id>[^\/]+) # media_id (98285a5b-603a-4a14-acc0-20e37a3514bb)
|
270
|
-
/(?<stream_id>[^\/]+) # stream_id (b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3)
|
271
|
-
/(?<filename>.+?) # filename (MVI_0057)
|
272
|
-
(?:\.(?<extension>.+))?$ # extension (mp4)
|
273
|
-
}x.freeze
|
274
|
-
|
275
|
-
# @param [MatchData] match_data
|
276
|
-
def initialize(match_data)
|
277
|
-
self.class.members.each do |key|
|
278
|
-
send("#{key}=", match_data[key])
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
def self.parse(url_string)
|
283
|
-
# Example input: /avalon/mp4:98285a5b-603a-4a14-acc0-20e37a3514bb/b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3/MVI_0057.mp4
|
284
|
-
|
285
|
-
uri = URI.parse(url_string)
|
286
|
-
match_data = REGEX.match(uri.path)
|
287
|
-
MatterhornRtmpUrl.new match_data
|
288
|
-
end
|
289
|
-
|
290
|
-
alias _binding binding
|
291
|
-
def binding
|
292
|
-
_binding
|
293
|
-
end
|
294
|
-
|
295
|
-
def to_path
|
296
|
-
File.join(media_id, stream_id, "#{filename}.#{extension || prefix}")
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|
300
|
-
end
|