active_encode 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +16 -0
- data/.rubocop.yml +76 -0
- data/.travis.yml +10 -0
- data/Gemfile +11 -0
- data/LICENSE +202 -0
- data/README.md +106 -0
- data/Rakefile +38 -0
- data/active_encode.gemspec +28 -0
- data/lib/active_encode.rb +2 -0
- data/lib/active_encode/base.rb +16 -0
- data/lib/active_encode/callbacks.rb +69 -0
- data/lib/active_encode/core.rb +79 -0
- data/lib/active_encode/engine_adapter.rb +51 -0
- data/lib/active_encode/engine_adapters.rb +27 -0
- data/lib/active_encode/engine_adapters/active_job_adapter.rb +23 -0
- data/lib/active_encode/engine_adapters/inline_adapter.rb +42 -0
- data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +312 -0
- data/lib/active_encode/engine_adapters/shingoncoder_adapter.rb +56 -0
- data/lib/active_encode/engine_adapters/test_adapter.rb +38 -0
- data/lib/active_encode/engine_adapters/zencoder_adapter.rb +143 -0
- data/lib/active_encode/status.rb +38 -0
- data/lib/active_encode/technical_metadata.rb +11 -0
- data/lib/active_encode/version.rb +3 -0
- data/spec/fixtures/Bars_512kb.mp4 +0 -0
- data/spec/fixtures/matterhorn/cancelled_response.xml +323 -0
- data/spec/fixtures/matterhorn/completed_response.xml +4 -0
- data/spec/fixtures/matterhorn/create_response.xml +300 -0
- data/spec/fixtures/matterhorn/delete_track_response.xml +2 -0
- data/spec/fixtures/matterhorn/failed_response.xml +4 -0
- data/spec/fixtures/matterhorn/purged_response.xml +342 -0
- data/spec/fixtures/matterhorn/running_response.xml +1 -0
- data/spec/fixtures/matterhorn/stop_completed_response.xml +228 -0
- data/spec/fixtures/matterhorn/stop_running_response.xml +339 -0
- data/spec/fixtures/zencoder/job_create.json +1 -0
- data/spec/fixtures/zencoder/job_details_cancelled.json +1 -0
- data/spec/fixtures/zencoder/job_details_completed.json +1 -0
- data/spec/fixtures/zencoder/job_details_create.json +1 -0
- data/spec/fixtures/zencoder/job_details_failed.json +73 -0
- data/spec/fixtures/zencoder/job_details_running.json +1 -0
- data/spec/fixtures/zencoder/job_progress_cancelled.json +13 -0
- data/spec/fixtures/zencoder/job_progress_completed.json +13 -0
- data/spec/fixtures/zencoder/job_progress_create.json +1 -0
- data/spec/fixtures/zencoder/job_progress_failed.json +13 -0
- data/spec/fixtures/zencoder/job_progress_running.json +1 -0
- data/spec/integration/matterhorn_adapter_spec.rb +186 -0
- data/spec/integration/shingoncoder_adapter_spec.rb +152 -0
- data/spec/integration/zencoder_adapter_spec.rb +152 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/units/callbacks_spec.rb +66 -0
- data/spec/units/engine_adapter_spec.rb +78 -0
- data/spec/units/status_spec.rb +62 -0
- 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,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
|