active_encode 0.4.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +80 -0
- data/.rubocop.yml +9 -70
- data/.rubocop_todo.yml +68 -0
- data/CODE_OF_CONDUCT.md +36 -0
- data/CONTRIBUTING.md +23 -21
- data/Gemfile +5 -4
- data/LICENSE +11 -199
- data/README.md +135 -24
- data/SUPPORT.md +5 -0
- data/active_encode.gemspec +13 -3
- data/app/controllers/active_encode/encode_record_controller.rb +13 -0
- data/app/jobs/active_encode/polling_job.rb +1 -1
- data/app/models/active_encode/encode_record.rb +1 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20180822021048_create_active_encode_encode_records.rb +1 -0
- data/db/migrate/20190702153755_add_create_options_to_active_encode_encode_records.rb +6 -0
- data/db/migrate/20190712174821_add_progress_to_active_encode_encode_records.rb +6 -0
- data/lib/active_encode.rb +1 -0
- data/lib/active_encode/base.rb +2 -2
- data/lib/active_encode/callbacks.rb +1 -0
- data/lib/active_encode/core.rb +4 -3
- data/lib/active_encode/engine.rb +1 -0
- data/lib/active_encode/engine_adapter.rb +1 -0
- data/lib/active_encode/engine_adapters.rb +4 -1
- data/lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb +116 -38
- data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +141 -87
- data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +5 -4
- data/lib/active_encode/engine_adapters/media_convert_adapter.rb +372 -0
- data/lib/active_encode/engine_adapters/media_convert_output.rb +104 -0
- data/lib/active_encode/engine_adapters/pass_through_adapter.rb +239 -0
- data/lib/active_encode/engine_adapters/test_adapter.rb +5 -4
- data/lib/active_encode/engine_adapters/zencoder_adapter.rb +3 -2
- data/lib/active_encode/errors.rb +6 -0
- data/lib/active_encode/global_id.rb +2 -1
- data/lib/active_encode/input.rb +3 -2
- data/lib/active_encode/output.rb +3 -2
- data/lib/active_encode/persistence.rb +11 -5
- data/lib/active_encode/polling.rb +3 -2
- data/lib/active_encode/spec/shared_specs.rb +2 -0
- data/{spec/shared_specs/engine_adapter_specs.rb → lib/active_encode/spec/shared_specs/engine_adapter.rb} +37 -38
- data/lib/active_encode/status.rb +1 -0
- data/lib/active_encode/technical_metadata.rb +3 -2
- data/lib/active_encode/version.rb +2 -1
- data/lib/file_locator.rb +93 -0
- data/spec/controllers/encode_record_controller_spec.rb +53 -0
- data/spec/fixtures/ffmpeg/cancelled-id/cancelled +0 -0
- data/spec/fixtures/file with space.low.mp4 +0 -0
- data/spec/fixtures/file with space.mp4 +0 -0
- data/spec/fixtures/fireworks.low.mp4 +0 -0
- data/spec/fixtures/media_convert/endpoints.json +1 -0
- data/spec/fixtures/media_convert/job_canceled.json +412 -0
- data/spec/fixtures/media_convert/job_canceling.json +1 -0
- data/spec/fixtures/media_convert/job_completed.json +359 -0
- data/spec/fixtures/media_convert/job_completed_detail.json +1 -0
- data/spec/fixtures/media_convert/job_completed_detail_query.json +1 -0
- data/spec/fixtures/media_convert/job_created.json +408 -0
- data/spec/fixtures/media_convert/job_failed.json +406 -0
- data/spec/fixtures/media_convert/job_progressing.json +414 -0
- data/spec/fixtures/pass_through/cancelled-id/cancelled +0 -0
- data/spec/fixtures/pass_through/cancelled-id/input_metadata +90 -0
- data/spec/fixtures/pass_through/completed-id/completed +0 -0
- data/spec/fixtures/pass_through/completed-id/input_metadata +102 -0
- data/spec/fixtures/pass_through/completed-id/output_metadata-high +90 -0
- data/spec/fixtures/pass_through/completed-id/output_metadata-low +90 -0
- data/spec/fixtures/pass_through/completed-id/video-high.mp4 +0 -0
- data/spec/fixtures/pass_through/completed-id/video-low.mp4 +0 -0
- data/spec/fixtures/pass_through/failed-id/error.log +1 -0
- data/spec/fixtures/pass_through/failed-id/input_metadata +90 -0
- data/spec/fixtures/pass_through/running-id/input_metadata +90 -0
- data/spec/integration/elastic_transcoder_adapter_spec.rb +63 -29
- data/spec/integration/ffmpeg_adapter_spec.rb +96 -24
- data/spec/integration/matterhorn_adapter_spec.rb +45 -44
- data/spec/integration/media_convert_adapter_spec.rb +126 -0
- data/spec/integration/pass_through_adapter_spec.rb +151 -0
- data/spec/integration/zencoder_adapter_spec.rb +210 -209
- data/spec/rails_helper.rb +1 -0
- data/spec/routing/encode_record_controller_routing_spec.rb +10 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -12
- data/spec/units/callbacks_spec.rb +3 -2
- data/spec/units/core_spec.rb +26 -25
- data/spec/units/engine_adapter_spec.rb +1 -0
- data/spec/units/file_locator_spec.rb +129 -0
- data/spec/units/global_id_spec.rb +12 -11
- data/spec/units/input_spec.rb +8 -5
- data/spec/units/output_spec.rb +8 -5
- data/spec/units/persistence_spec.rb +15 -11
- data/spec/units/polling_job_spec.rb +7 -6
- data/spec/units/polling_spec.rb +1 -0
- data/spec/units/status_spec.rb +3 -3
- metadata +184 -18
- data/.travis.yml +0 -19
data/README.md
CHANGED
@@ -1,6 +1,21 @@
|
|
1
1
|
# ActiveEncode
|
2
2
|
|
3
|
-
|
3
|
+
Code: [](http://badge.fury.io/rb/active_encode)
|
4
|
+
[](https://travis-ci.org/samvera-labs/active_encode)
|
5
|
+
[](https://coveralls.io/github/samvera-labs/active_encode?branch=master)
|
6
|
+
|
7
|
+
Docs: [](./CONTRIBUTING.md)
|
8
|
+
[](./LICENSE)
|
9
|
+
|
10
|
+
Jump in: [](http://slack.samvera.org/)
|
11
|
+
|
12
|
+
# What is ActiveEncode?
|
13
|
+
|
14
|
+
ActiveEncode serves as the basis for the interface between a Ruby (Rails) application and a provider of encoding services such as [FFmpeg](https://www.ffmpeg.org/), [Amazon Elastic Transcoder](http://aws.amazon.com/elastictranscoder/), and [Zencoder](http://zencoder.com).
|
15
|
+
|
16
|
+
# Help
|
17
|
+
|
18
|
+
The Samvera community is here to help. Please see our [support guide](./SUPPORT.md).
|
4
19
|
|
5
20
|
## Installation
|
6
21
|
|
@@ -18,41 +33,76 @@ Or install it yourself as:
|
|
18
33
|
|
19
34
|
$ gem install active_encode
|
20
35
|
|
36
|
+
## Prerequisites
|
37
|
+
|
38
|
+
FFmpeg (tested with version 4+) and mediainfo (version 17.10+) need to be installed to use the FFmpeg engine adapter.
|
39
|
+
|
21
40
|
## Usage
|
22
41
|
|
23
42
|
Set the engine adapter (default: test), configure it (if neccessary), then submit encoding jobs!
|
24
43
|
|
25
44
|
```ruby
|
26
|
-
ActiveEncode::Base.engine_adapter = :
|
27
|
-
|
45
|
+
ActiveEncode::Base.engine_adapter = :ffmpeg
|
46
|
+
file = "file://#{File.absolute_path "spec/fixtures/fireworks.mp4"}"
|
47
|
+
ActiveEncode::Base.create(file, { outputs: [{ label: "low", ffmpeg_opt: "-s 640x480", extension: "mp4"}, { label: "high", ffmpeg_opt: "-s 1280x720", extension: "mp4"}] })
|
28
48
|
```
|
29
|
-
Create returns an encoding job that has been submitted to the
|
49
|
+
Create returns an encoding job that has been submitted to the adapter for processing. At this point it will have an id, a state, the input, and any additional information the adapter returns.
|
30
50
|
|
31
51
|
```ruby
|
32
|
-
#<ActiveEncode::Base:
|
52
|
+
#<ActiveEncode::Base:0x007f8ef3b2ae88 @input=#<ActiveEncode::Input:0x007f8ef3b23188 @url="file:///Users/cjcolvar/Documents/Code/samvera-labs/active_encode/spec/fixtures/fireworks.mp4", @width=960.0, @height=540.0, @frame_rate=29.671, @duration=6024, @file_size=1629578, @audio_codec="mp4a-40-2", @video_codec="avc1", @audio_bitrate=69737, @video_bitrate=2092780, @created_at=2018-12-03 14:22:05 -0500, @updated_at=2018-12-03 14:22:05 -0500, @id=7653>, @options={:outputs=>[{:label=>"low", :ffmpeg_opt=>"-s 640x480", :extension=>"mp4"}, {:label=>"high", :ffmpeg_opt=>"-s 1280x720", :extension=>"mp4"}]}, @id="1e4a907a-ccff-494f-ad70-b1c5072c2465", @created_at=2018-12-03 14:22:05 -0500, @updated_at=2018-12-03 14:22:05 -0500, @current_operations=[], @output=[], @state=:running, @percent_complete=1, @errors=[]>
|
33
53
|
```
|
34
54
|
```ruby
|
35
|
-
encode.id # "
|
55
|
+
encode.id # "1e4a907a-ccff-494f-ad70-b1c5072c2465"
|
36
56
|
encode.state # :running
|
37
57
|
```
|
38
58
|
|
39
|
-
This encode can be looked back up later using #find. Alternatively, use #reload to refresh an instance with the latest information from the
|
59
|
+
This encode can be looked back up later using #find. Alternatively, use #reload to refresh an instance with the latest information from the adapter:
|
40
60
|
|
41
61
|
```ruby
|
42
|
-
encode = ActiveEncode::Base.find("
|
62
|
+
encode = ActiveEncode::Base.find("1e4a907a-ccff-494f-ad70-b1c5072c2465")
|
43
63
|
encode.reload
|
44
64
|
```
|
45
65
|
|
46
|
-
Progress of a running encode is shown with
|
66
|
+
Progress of a running encode is shown with current operations (multiple are possible when outputs are generated in parallel) and percent complete. Technical metadata about the input file may be added by the adapter. This should include a mime type, checksum, duration, and basic technical details of the audio and video content of the file (codec, audio channels, bitrate, frame rate, and dimensions). Outputs are added once they are created and should include the same technical metadata along with an id, label, and url.
|
47
67
|
|
48
|
-
If the encoding job
|
68
|
+
If you want to stop the encoding job call cancel:
|
49
69
|
|
50
70
|
```ruby
|
51
71
|
encode.cancel!
|
52
72
|
encode.cancelled? # true
|
53
73
|
```
|
54
74
|
|
55
|
-
An encoding job is meant to be the record of the work of the encoding engine and not the current state of the outputs. Therefore
|
75
|
+
An encoding job is meant to be the record of the work of the encoding engine and not the current state of the outputs. Therefore moved or deleted outputs will not be reflected in the encoding job.
|
76
|
+
|
77
|
+
### AWS ElasticTranscoder
|
78
|
+
|
79
|
+
To use active_encode with the AWS ElasticTransoder, the following are required:
|
80
|
+
- An S3 bucket to store master files
|
81
|
+
- An S3 bucket to store derivatives (recommended to be separate)
|
82
|
+
- An ElasticTranscoder pipeline
|
83
|
+
- Some transcoding presets for the pipeline
|
84
|
+
|
85
|
+
Set the adapter:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
ActiveEncode::Base.engine_adapter = :elastic_transcoder
|
89
|
+
```
|
90
|
+
|
91
|
+
Construct the options hash:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
outputs = [{ key: "quality-low/hls/fireworks", preset_id: '1494429796844-aza6zh', segment_duration: '2' },
|
95
|
+
{ key: "quality-medium/hls/fireworks", preset_id: '1494429797061-kvg9ki', segment_duration: '2' },
|
96
|
+
{ key: "quality-high/hls/fireworks", preset_id: '1494429797265-9xi831', segment_duration: '2' }]
|
97
|
+
options = {pipeline_id: 'my-pipeline-id', masterfile_bucket: 'my-master-files', outputs: outputs}
|
98
|
+
```
|
99
|
+
|
100
|
+
Create the job:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
file = 'file:///path/to/file/fireworks.mp4' # or 's3://my-bucket/fireworks.mp4'
|
104
|
+
encode = ActiveEncode::Base.create(file, options)
|
105
|
+
```
|
56
106
|
|
57
107
|
### Custom jobs
|
58
108
|
|
@@ -72,19 +122,80 @@ end
|
|
72
122
|
|
73
123
|
### Engine Adapters
|
74
124
|
|
75
|
-
Engine adapters are shims between ActiveEncode and the back end encoding service.
|
125
|
+
Engine adapters are shims between ActiveEncode and the back end encoding service. You can add an additional engine by creating an engine adapter class that implements `:create`, `:find`, and `:cancel` and passes the shared specs.
|
126
|
+
|
127
|
+
For example:
|
128
|
+
```ruby
|
129
|
+
# In your application at:
|
130
|
+
# lib/active_encode/engine_adapters/my_custom_adapter.rb
|
131
|
+
module ActiveEncode
|
132
|
+
module EngineAdapters
|
133
|
+
class MyCustomAdapter
|
134
|
+
def create(input_url, options = {})
|
135
|
+
# Start a new encoding job. This may be an external service, or a
|
136
|
+
# locally queued job.
|
137
|
+
|
138
|
+
# Return an instance ActiveEncode::Base (or subclass) that represents
|
139
|
+
# the encoding job that was just started.
|
140
|
+
end
|
141
|
+
|
142
|
+
def find(id, opts = {})
|
143
|
+
# Find the encoding job for the given parameters.
|
144
|
+
|
145
|
+
# Return an instance of ActiveEncode::Base (or subclass) that represents
|
146
|
+
# the found encoding job.
|
147
|
+
end
|
148
|
+
|
149
|
+
def cancel(id)
|
150
|
+
# Cancel the encoding job for the given id.
|
151
|
+
|
152
|
+
# Return an instance of ActiveEncode::Base (or subclass) that represents
|
153
|
+
# the canceled job.
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
Then, use the shared specs...
|
160
|
+
```ruby
|
161
|
+
# In your application at...
|
162
|
+
# spec/lib/active_encode/engine_adapters/my_custom_adapter_spec.rb
|
163
|
+
require 'spec_helper'
|
164
|
+
require 'active_encode/spec/shared_specs'
|
165
|
+
RSpec.describe MyCustomAdapter do
|
166
|
+
let(:created_job) {
|
167
|
+
# an instance of ActiveEncode::Base represented a newly created encode job
|
168
|
+
}
|
169
|
+
let(:running_job) {
|
170
|
+
# an instance of ActiveEncode::Base represented a running encode job
|
171
|
+
}
|
172
|
+
let(:canceled_job) {
|
173
|
+
# an instance of ActiveEncode::Base represented a canceled encode job
|
174
|
+
}
|
175
|
+
let(:completed_job) {
|
176
|
+
# an instance of ActiveEncode::Base represented a completed encode job
|
177
|
+
}
|
178
|
+
let(:failed_job) {
|
179
|
+
# an instance of ActiveEncode::Base represented a failed encode job
|
180
|
+
}
|
181
|
+
let(:completed_tech_metadata) {
|
182
|
+
# a hash representing completed technical metadata
|
183
|
+
}
|
184
|
+
let(:completed_output) {
|
185
|
+
# data representing completed output
|
186
|
+
}
|
187
|
+
let(:failed_tech_metadata) {
|
188
|
+
# a hash representing failed technical metadata
|
189
|
+
}
|
190
|
+
|
191
|
+
# Run the shared specs.
|
192
|
+
it_behaves_like 'an ActiveEncode::EngineAdapter'
|
193
|
+
end
|
194
|
+
```
|
76
195
|
|
77
|
-
|
78
|
-
|--------------------------|--------|------|--------|--------|------------------|
|
79
|
-
| Test | X | X | X | | |
|
80
|
-
| AWS Elastic Transcoder | X | X | X | | |
|
81
|
-
| Zencoder | X | X | X | | |
|
82
|
-
| Matterhorn | X | X | X | X | X |
|
196
|
+
# Acknowledgments
|
83
197
|
|
84
|
-
|
198
|
+
This software has been developed by and is brought to you by the Samvera community. Learn more at the
|
199
|
+
[Samvera website](http://samvera.org/).
|
85
200
|
|
86
|
-
|
87
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
88
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
89
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
90
|
-
5. Create a new Pull Request
|
201
|
+

|
data/SUPPORT.md
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
If you would like to report an issue, first search [the list of issues](https://github.com/samvera-labs/active_encode/issues/) to see if someone else has already reported it, and then feel free to [create a new issue](https://github.com/samvera-labs/active_encode/issues/new).
|
2
|
+
|
3
|
+
If you have questions or need help, please email [the Samvera community tech list](https://groups.google.com/forum/#!forum/samvera-tech) or stop by the #dev channel in [the Samvera community Slack team](https://wiki.duraspace.org/pages/viewpage.action?pageId=87460391#Getintouch!-Slack).
|
4
|
+
|
5
|
+
You can learn more about the various Samvera communication channels on the [Get in touch!](https://wiki.duraspace.org/pages/viewpage.action?pageId=87460391) wiki page.
|
data/active_encode.gemspec
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
lib = File.expand_path('../lib', __FILE__)
|
4
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
@@ -10,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
10
11
|
spec.authors = ["Michael Klein, Chris Colvard, Phuong Dinh"]
|
11
12
|
spec.email = ["mbklein@gmail.com, chris.colvard@gmail.com, phuongdh@gmail.com"]
|
12
13
|
spec.summary = 'Declare encode job classes that can be run by a variety of encoding services'
|
13
|
-
spec.description = 'This gem
|
14
|
+
spec.description = 'This gem provides an interface to transcoding services such as Ffmpeg, Amazon Elastic Transcoder, or Zencoder.'
|
14
15
|
spec.homepage = "https://github.com/samvera-labs/active_encode"
|
15
16
|
spec.license = "Apache-2.0"
|
16
17
|
|
@@ -21,12 +22,21 @@ Gem::Specification.new do |spec|
|
|
21
22
|
|
22
23
|
spec.add_dependency "rails"
|
23
24
|
|
25
|
+
spec.add_development_dependency "aws-sdk-cloudwatchevents"
|
26
|
+
spec.add_development_dependency "aws-sdk-cloudwatchlogs"
|
27
|
+
spec.add_development_dependency "aws-sdk-elastictranscoder"
|
28
|
+
spec.add_development_dependency "aws-sdk-mediaconvert"
|
29
|
+
spec.add_development_dependency "aws-sdk-s3"
|
30
|
+
spec.add_development_dependency "bixby", '~> 1.0.0'
|
24
31
|
spec.add_development_dependency "bundler"
|
25
32
|
spec.add_development_dependency "coveralls"
|
26
33
|
spec.add_development_dependency "database_cleaner"
|
27
|
-
spec.add_development_dependency "engine_cart"
|
34
|
+
spec.add_development_dependency "engine_cart", "~> 2.2"
|
28
35
|
spec.add_development_dependency "rake"
|
29
36
|
spec.add_development_dependency "rspec"
|
30
|
-
spec.add_development_dependency
|
37
|
+
spec.add_development_dependency 'rspec_junit_formatter'
|
31
38
|
spec.add_development_dependency "rspec-rails"
|
39
|
+
|
40
|
+
# Pin sprockets to < 4 so it works with ruby 2.5+
|
41
|
+
spec.add_dependency 'sprockets', '< 4'
|
32
42
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ActiveEncode
|
3
|
+
class EncodeRecordController < ActionController::Base
|
4
|
+
rescue_from ActiveRecord::RecordNotFound do |e|
|
5
|
+
render json: { message: e.message }, status: :not_found
|
6
|
+
end
|
7
|
+
|
8
|
+
def show
|
9
|
+
@encode_record = ActiveEncode::EncodeRecord.find(params[:id])
|
10
|
+
render json: @encode_record.raw_object, status: :ok
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/config/routes.rb
ADDED
data/lib/active_encode.rb
CHANGED
data/lib/active_encode/base.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'active_encode/core'
|
2
3
|
require 'active_encode/engine_adapter'
|
4
|
+
require 'active_encode/errors'
|
3
5
|
require 'active_encode/status'
|
4
6
|
require 'active_encode/technical_metadata'
|
5
7
|
require 'active_encode/input'
|
@@ -17,6 +19,4 @@ module ActiveEncode #:nodoc:
|
|
17
19
|
include Callbacks
|
18
20
|
include GlobalID
|
19
21
|
end
|
20
|
-
|
21
|
-
class NotFound < RuntimeError; end
|
22
22
|
end
|
data/lib/active_encode/core.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'active_support'
|
2
3
|
require 'active_encode/callbacks'
|
3
4
|
|
@@ -44,19 +45,19 @@ module ActiveEncode
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def initialize(input_url, options = nil)
|
47
|
-
@input = Input.new.tap{ |input| input.url = input_url }
|
48
|
+
@input = Input.new.tap { |input| input.url = input_url }
|
48
49
|
@options = self.class.default_options(input_url).merge(Hash(options))
|
49
50
|
end
|
50
51
|
|
51
52
|
def create!
|
52
53
|
run_callbacks :create do
|
53
|
-
merge!(self.class.engine_adapter.create(
|
54
|
+
merge!(self.class.engine_adapter.create(input.url, options))
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
57
58
|
def cancel!
|
58
59
|
run_callbacks :cancel do
|
59
|
-
merge!(self.class.engine_adapter.cancel(
|
60
|
+
merge!(self.class.engine_adapter.cancel(id))
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
data/lib/active_encode/engine.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module ActiveEncode
|
2
3
|
# == Active Encode adapters
|
3
4
|
#
|
@@ -13,8 +14,10 @@ module ActiveEncode
|
|
13
14
|
autoload :ElasticTranscoderAdapter
|
14
15
|
autoload :TestAdapter
|
15
16
|
autoload :FfmpegAdapter
|
17
|
+
autoload :MediaConvertAdapter
|
18
|
+
autoload :PassThroughAdapter
|
16
19
|
|
17
|
-
ADAPTER = 'Adapter'
|
20
|
+
ADAPTER = 'Adapter'
|
18
21
|
private_constant :ADAPTER
|
19
22
|
|
20
23
|
class << self
|
@@ -1,12 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'addressable/uri'
|
3
|
+
require 'aws-sdk-elastictranscoder'
|
4
|
+
require 'file_locator'
|
5
|
+
|
1
6
|
module ActiveEncode
|
2
7
|
module EngineAdapters
|
3
8
|
class ElasticTranscoderAdapter
|
4
|
-
|
9
|
+
JOB_STATES = {
|
10
|
+
"Submitted" => :running, "Progressing" => :running, "Canceled" => :cancelled,
|
11
|
+
"Error" => :failed, "Complete" => :completed
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
# Require options to include :pipeline_id, :masterfile_bucket and :outputs
|
15
|
+
# Example :outputs value:
|
16
|
+
# [{ key: "quality-low/hls/fireworks", preset_id: '1494429796844-aza6zh', segment_duration: '2' },
|
17
|
+
# { key: "quality-medium/hls/fireworks", preset_id: '1494429797061-kvg9ki', segment_duration: '2' },
|
18
|
+
# { key: "quality-high/hls/fireworks", preset_id: '1494429797265-9xi831', segment_duration: '2' }]
|
5
19
|
def create(input_url, options = {})
|
20
|
+
s3_key = copy_to_input_bucket input_url, options[:masterfile_bucket]
|
6
21
|
job = client.create_job(
|
7
|
-
input: { key:
|
22
|
+
input: { key: s3_key },
|
8
23
|
pipeline_id: options[:pipeline_id],
|
9
|
-
output_key_prefix: options[:output_key_prefix],
|
24
|
+
output_key_prefix: options[:output_key_prefix] || "#{SecureRandom.uuid}/",
|
10
25
|
outputs: options[:outputs],
|
11
26
|
user_metadata: options[:user_metadata]
|
12
27
|
).job
|
@@ -14,7 +29,7 @@ module ActiveEncode
|
|
14
29
|
build_encode(job)
|
15
30
|
end
|
16
31
|
|
17
|
-
def find(id,
|
32
|
+
def find(id, _opts = {})
|
18
33
|
build_encode(get_job_details(id))
|
19
34
|
end
|
20
35
|
|
@@ -31,28 +46,33 @@ module ActiveEncode
|
|
31
46
|
@client ||= Aws::ElasticTranscoder::Client.new
|
32
47
|
end
|
33
48
|
|
49
|
+
def s3client
|
50
|
+
Aws::S3::Client.new
|
51
|
+
end
|
52
|
+
|
34
53
|
def get_job_details(job_id)
|
35
|
-
client.read_job(id: job_id)
|
54
|
+
client.read_job(id: job_id)&.job
|
36
55
|
end
|
37
56
|
|
38
57
|
def build_encode(job)
|
39
58
|
return nil if job.nil?
|
40
|
-
encode = ActiveEncode::Base.new(convert_input(job),
|
59
|
+
encode = ActiveEncode::Base.new(convert_input(job), {})
|
41
60
|
encode.id = job.id
|
42
|
-
encode.state =
|
43
|
-
encode.current_operations =
|
61
|
+
encode.state = JOB_STATES[job.status]
|
62
|
+
encode.current_operations = []
|
44
63
|
encode.percent_complete = convert_percent_complete(job)
|
45
64
|
encode.created_at = convert_time(job.timing["submit_time_millis"])
|
46
|
-
encode.updated_at = convert_time(job.timing["finish_time_millis"] || job.timing["start_time_millis"]) || encode.created_at
|
65
|
+
encode.updated_at = convert_time(job.timing["finish_time_millis"]) || convert_time(job.timing["start_time_millis"]) || encode.created_at
|
66
|
+
|
47
67
|
encode.output = convert_output(job)
|
48
|
-
encode.errors =
|
68
|
+
encode.errors = job.outputs.select { |o| o.status == "Error" }.collect(&:status_detail).compact
|
49
69
|
|
50
|
-
encode.input.id = job.input.key
|
51
70
|
tech_md = convert_tech_metadata(job.input.detected_properties)
|
52
|
-
[:width, :height, :frame_rate, :duration, :
|
53
|
-
:audio_bitrate, :video_bitrate, :file_size].each do |field|
|
71
|
+
[:width, :height, :frame_rate, :duration, :file_size].each do |field|
|
54
72
|
encode.input.send("#{field}=", tech_md[field])
|
55
73
|
end
|
74
|
+
|
75
|
+
encode.input.id = job.id
|
56
76
|
encode.input.state = encode.state
|
57
77
|
encode.input.created_at = encode.created_at
|
58
78
|
encode.input.updated_at = encode.updated_at
|
@@ -62,7 +82,12 @@ module ActiveEncode
|
|
62
82
|
|
63
83
|
def convert_time(time_millis)
|
64
84
|
return nil if time_millis.nil?
|
65
|
-
Time.at(time_millis / 1000)
|
85
|
+
Time.at(time_millis / 1000).utc
|
86
|
+
end
|
87
|
+
|
88
|
+
def convert_bitrate(rate)
|
89
|
+
return nil if rate.nil?
|
90
|
+
(rate.to_f * 1024).to_s
|
66
91
|
end
|
67
92
|
|
68
93
|
def convert_state(job)
|
@@ -78,13 +103,12 @@ module ActiveEncode
|
|
78
103
|
end
|
79
104
|
end
|
80
105
|
|
81
|
-
def
|
82
|
-
|
83
|
-
current_ops
|
106
|
+
def convert_percent_complete(job)
|
107
|
+
job.outputs.inject(0) { |sum, output| sum + output_percentage(output) } / job.outputs.length
|
84
108
|
end
|
85
109
|
|
86
|
-
def
|
87
|
-
case
|
110
|
+
def output_percentage(output)
|
111
|
+
case output.status
|
88
112
|
when "Submitted"
|
89
113
|
10
|
90
114
|
when "Progressing", "Canceled", "Error"
|
@@ -97,32 +121,72 @@ module ActiveEncode
|
|
97
121
|
end
|
98
122
|
|
99
123
|
def convert_input(job)
|
100
|
-
job.input
|
124
|
+
job.input.key
|
101
125
|
end
|
102
126
|
|
103
|
-
def
|
104
|
-
|
127
|
+
def copy_to_input_bucket(input_url, bucket)
|
128
|
+
case Addressable::URI.parse(input_url).scheme
|
129
|
+
when nil, 'file'
|
130
|
+
upload_to_s3 input_url, bucket
|
131
|
+
when 's3'
|
132
|
+
check_s3_bucket input_url, bucket
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def check_s3_bucket(input_url, source_bucket)
|
137
|
+
# logger.info("Checking `#{input_url}'")
|
138
|
+
s3_object = FileLocator::S3File.new(input_url).object
|
139
|
+
if s3_object.bucket_name == source_bucket
|
140
|
+
# logger.info("Already in bucket `#{source_bucket}'")
|
141
|
+
s3_object.key
|
142
|
+
else
|
143
|
+
s3_key = File.join(SecureRandom.uuid, s3_object.key)
|
144
|
+
# logger.info("Copying to `#{source_bucket}/#{input_url}'")
|
145
|
+
target = Aws::S3::Object.new(bucket_name: source_bucket, key: input_url)
|
146
|
+
target.copy_from(s3_object, multipart_copy: s3_object.size > 15_728_640) # 15.megabytes
|
147
|
+
s3_key
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def upload_to_s3(input_url, source_bucket)
|
152
|
+
# original_input = input_url
|
153
|
+
bucket = Aws::S3::Resource.new(client: s3client).bucket(source_bucket)
|
154
|
+
filename = FileLocator.new(input_url).location
|
155
|
+
s3_key = File.join(SecureRandom.uuid, File.basename(filename))
|
156
|
+
# logger.info("Copying `#{original_input}' to `#{source_bucket}/#{input_url}'")
|
157
|
+
obj = bucket.object(s3_key)
|
158
|
+
obj.upload_file filename
|
159
|
+
|
160
|
+
s3_key
|
161
|
+
end
|
162
|
+
|
163
|
+
def read_preset(id)
|
164
|
+
@presets ||= {}
|
165
|
+
@presets[id] ||= client.read_preset(id: id).preset
|
105
166
|
end
|
106
167
|
|
107
168
|
def convert_output(job)
|
108
|
-
job.
|
109
|
-
|
110
|
-
|
169
|
+
@pipeline ||= client.read_pipeline(id: job.pipeline_id).pipeline
|
170
|
+
job.outputs.collect do |joutput|
|
171
|
+
preset = read_preset(joutput.preset_id)
|
172
|
+
extension = preset.container == 'ts' ? '.m3u8' : ''
|
173
|
+
additional_metadata = {
|
174
|
+
managed: false,
|
175
|
+
id: joutput.id,
|
176
|
+
label: joutput.key.split("/", 2).first,
|
177
|
+
url: "s3://#{@pipeline.output_bucket}/#{job.output_key_prefix}#{joutput.key}#{extension}"
|
178
|
+
}
|
179
|
+
tech_md = convert_tech_metadata(joutput, preset).merge(additional_metadata)
|
180
|
+
|
111
181
|
output = ActiveEncode::Output.new
|
112
|
-
output.
|
113
|
-
output.
|
114
|
-
output.
|
115
|
-
|
116
|
-
# TODO: If HLS is not considered distinct from this output then this should be handled by a method on a ActiveEncode::Base subclass or consuming client
|
117
|
-
# extras[:hls_url] = url + ".m3u8" if url.include?("/hls/") # TODO: find a better way to signal hls
|
118
|
-
tech_md = convert_tech_metadata(o)
|
182
|
+
output.state = convert_state(joutput)
|
183
|
+
output.created_at = convert_time(job.timing["submit_time_millis"])
|
184
|
+
output.updated_at = convert_time(job.timing["finish_time_millis"] || job.timing["start_time_millis"]) || output.created_at
|
185
|
+
|
119
186
|
[:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
|
120
|
-
:audio_bitrate, :video_bitrate, :file_size].each do |field|
|
187
|
+
:audio_bitrate, :video_bitrate, :file_size, :label, :url, :id].each do |field|
|
121
188
|
output.send("#{field}=", tech_md[field])
|
122
189
|
end
|
123
|
-
output.state = convert_state(o)
|
124
|
-
output.created_at = convert_time(job.timing["submit_time_millis"])
|
125
|
-
output.updated_at = convert_time(job.timing["finish_time_millis"] || job.timing["start_time_millis"]) || output.created_at
|
126
190
|
|
127
191
|
output
|
128
192
|
end
|
@@ -132,12 +196,13 @@ module ActiveEncode
|
|
132
196
|
job.outputs.select { |o| o.status == "Error" }.collect(&:status_detail).compact
|
133
197
|
end
|
134
198
|
|
135
|
-
def convert_tech_metadata(props)
|
136
|
-
return {} if props.
|
199
|
+
def convert_tech_metadata(props, preset = nil)
|
200
|
+
return {} if props.nil? || props.empty?
|
137
201
|
metadata_fields = {
|
138
202
|
file_size: { key: :file_size, method: :itself },
|
139
203
|
duration_millis: { key: :duration, method: :to_i },
|
140
204
|
frame_rate: { key: :frame_rate, method: :to_i },
|
205
|
+
segment_duration: { key: :segment_duration, method: :itself },
|
141
206
|
width: { key: :width, method: :itself },
|
142
207
|
height: { key: :height, method: :itself }
|
143
208
|
}
|
@@ -149,6 +214,19 @@ module ActiveEncode
|
|
149
214
|
next if conversion.nil?
|
150
215
|
metadata[conversion[:key]] = value.send(conversion[:method])
|
151
216
|
end
|
217
|
+
|
218
|
+
unless preset.nil?
|
219
|
+
audio = preset.audio
|
220
|
+
video = preset.video
|
221
|
+
metadata.merge!(
|
222
|
+
audio_codec: audio&.codec,
|
223
|
+
audio_channels: audio&.channels,
|
224
|
+
audio_bitrate: convert_bitrate(audio&.bit_rate),
|
225
|
+
video_codec: video&.codec,
|
226
|
+
video_bitrate: convert_bitrate(video&.bit_rate)
|
227
|
+
)
|
228
|
+
end
|
229
|
+
|
152
230
|
metadata
|
153
231
|
end
|
154
232
|
end
|