active_encode 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|