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.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +80 -0
  3. data/.rubocop.yml +9 -70
  4. data/.rubocop_todo.yml +68 -0
  5. data/CODE_OF_CONDUCT.md +36 -0
  6. data/CONTRIBUTING.md +23 -21
  7. data/Gemfile +5 -4
  8. data/LICENSE +11 -199
  9. data/README.md +135 -24
  10. data/SUPPORT.md +5 -0
  11. data/active_encode.gemspec +13 -3
  12. data/app/controllers/active_encode/encode_record_controller.rb +13 -0
  13. data/app/jobs/active_encode/polling_job.rb +1 -1
  14. data/app/models/active_encode/encode_record.rb +1 -0
  15. data/config/routes.rb +4 -0
  16. data/db/migrate/20180822021048_create_active_encode_encode_records.rb +1 -0
  17. data/db/migrate/20190702153755_add_create_options_to_active_encode_encode_records.rb +6 -0
  18. data/db/migrate/20190712174821_add_progress_to_active_encode_encode_records.rb +6 -0
  19. data/lib/active_encode.rb +1 -0
  20. data/lib/active_encode/base.rb +2 -2
  21. data/lib/active_encode/callbacks.rb +1 -0
  22. data/lib/active_encode/core.rb +4 -3
  23. data/lib/active_encode/engine.rb +1 -0
  24. data/lib/active_encode/engine_adapter.rb +1 -0
  25. data/lib/active_encode/engine_adapters.rb +4 -1
  26. data/lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb +116 -38
  27. data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +141 -87
  28. data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +5 -4
  29. data/lib/active_encode/engine_adapters/media_convert_adapter.rb +372 -0
  30. data/lib/active_encode/engine_adapters/media_convert_output.rb +104 -0
  31. data/lib/active_encode/engine_adapters/pass_through_adapter.rb +239 -0
  32. data/lib/active_encode/engine_adapters/test_adapter.rb +5 -4
  33. data/lib/active_encode/engine_adapters/zencoder_adapter.rb +3 -2
  34. data/lib/active_encode/errors.rb +6 -0
  35. data/lib/active_encode/global_id.rb +2 -1
  36. data/lib/active_encode/input.rb +3 -2
  37. data/lib/active_encode/output.rb +3 -2
  38. data/lib/active_encode/persistence.rb +11 -5
  39. data/lib/active_encode/polling.rb +3 -2
  40. data/lib/active_encode/spec/shared_specs.rb +2 -0
  41. data/{spec/shared_specs/engine_adapter_specs.rb → lib/active_encode/spec/shared_specs/engine_adapter.rb} +37 -38
  42. data/lib/active_encode/status.rb +1 -0
  43. data/lib/active_encode/technical_metadata.rb +3 -2
  44. data/lib/active_encode/version.rb +2 -1
  45. data/lib/file_locator.rb +93 -0
  46. data/spec/controllers/encode_record_controller_spec.rb +53 -0
  47. data/spec/fixtures/ffmpeg/cancelled-id/cancelled +0 -0
  48. data/spec/fixtures/file with space.low.mp4 +0 -0
  49. data/spec/fixtures/file with space.mp4 +0 -0
  50. data/spec/fixtures/fireworks.low.mp4 +0 -0
  51. data/spec/fixtures/media_convert/endpoints.json +1 -0
  52. data/spec/fixtures/media_convert/job_canceled.json +412 -0
  53. data/spec/fixtures/media_convert/job_canceling.json +1 -0
  54. data/spec/fixtures/media_convert/job_completed.json +359 -0
  55. data/spec/fixtures/media_convert/job_completed_detail.json +1 -0
  56. data/spec/fixtures/media_convert/job_completed_detail_query.json +1 -0
  57. data/spec/fixtures/media_convert/job_created.json +408 -0
  58. data/spec/fixtures/media_convert/job_failed.json +406 -0
  59. data/spec/fixtures/media_convert/job_progressing.json +414 -0
  60. data/spec/fixtures/pass_through/cancelled-id/cancelled +0 -0
  61. data/spec/fixtures/pass_through/cancelled-id/input_metadata +90 -0
  62. data/spec/fixtures/pass_through/completed-id/completed +0 -0
  63. data/spec/fixtures/pass_through/completed-id/input_metadata +102 -0
  64. data/spec/fixtures/pass_through/completed-id/output_metadata-high +90 -0
  65. data/spec/fixtures/pass_through/completed-id/output_metadata-low +90 -0
  66. data/spec/fixtures/pass_through/completed-id/video-high.mp4 +0 -0
  67. data/spec/fixtures/pass_through/completed-id/video-low.mp4 +0 -0
  68. data/spec/fixtures/pass_through/failed-id/error.log +1 -0
  69. data/spec/fixtures/pass_through/failed-id/input_metadata +90 -0
  70. data/spec/fixtures/pass_through/running-id/input_metadata +90 -0
  71. data/spec/integration/elastic_transcoder_adapter_spec.rb +63 -29
  72. data/spec/integration/ffmpeg_adapter_spec.rb +96 -24
  73. data/spec/integration/matterhorn_adapter_spec.rb +45 -44
  74. data/spec/integration/media_convert_adapter_spec.rb +126 -0
  75. data/spec/integration/pass_through_adapter_spec.rb +151 -0
  76. data/spec/integration/zencoder_adapter_spec.rb +210 -209
  77. data/spec/rails_helper.rb +1 -0
  78. data/spec/routing/encode_record_controller_routing_spec.rb +10 -0
  79. data/spec/spec_helper.rb +2 -2
  80. data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -12
  81. data/spec/units/callbacks_spec.rb +3 -2
  82. data/spec/units/core_spec.rb +26 -25
  83. data/spec/units/engine_adapter_spec.rb +1 -0
  84. data/spec/units/file_locator_spec.rb +129 -0
  85. data/spec/units/global_id_spec.rb +12 -11
  86. data/spec/units/input_spec.rb +8 -5
  87. data/spec/units/output_spec.rb +8 -5
  88. data/spec/units/persistence_spec.rb +15 -11
  89. data/spec/units/polling_job_spec.rb +7 -6
  90. data/spec/units/polling_spec.rb +1 -0
  91. data/spec/units/status_spec.rb +3 -3
  92. metadata +184 -18
  93. data/.travis.yml +0 -19
data/README.md CHANGED
@@ -1,6 +1,21 @@
1
1
  # ActiveEncode
2
2
 
3
- This gem serves as the basis for the interface between a Ruby (Rails) application and a provider of transcoding services such as [Opencast Matterhorn](http://opencast.org), [Zencoder](http://zencoder.com), and [Amazon Elastic Transcoder](http://aws.amazon.com/elastictranscoder/).
3
+ Code: [![Version](https://badge.fury.io/rb/active_encode.png)](http://badge.fury.io/rb/active_encode)
4
+ [![Build Status](https://travis-ci.org/samvera-labs/active_encode.png?branch=master)](https://travis-ci.org/samvera-labs/active_encode)
5
+ [![Coverage Status](https://coveralls.io/repos/github/samvera-labs/active_encode/badge.svg?branch=master)](https://coveralls.io/github/samvera-labs/active_encode?branch=master)
6
+
7
+ Docs: [![Contribution Guidelines](http://img.shields.io/badge/CONTRIBUTING-Guidelines-blue.svg)](./CONTRIBUTING.md)
8
+ [![Apache 2.0 License](http://img.shields.io/badge/APACHE2-license-blue.svg)](./LICENSE)
9
+
10
+ Jump in: [![Slack Status](http://slack.samvera.org/badge.svg)](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 = :matterhorn
27
- ActiveEncode::Base.create(File.open('spec/fixtures/Bars_512kb.mp4'))
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 encoding engine for processing. At this point it will have an id, a state, the input, and any additional information the encoding engine returns.
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:0x00000003f3cd90 @input="http://localhost:8080/files/mediapackage/edcac316-1f98-44b1-88ca-0ce6f80aebc0/ff43c56f-7b8f-4d9c-a846-6e51de2e8cb4/Bars_512kb.mp4", @options={:preset=>"avalon", :stream_base=>"file:///home/cjcolvar/Code/avalon/avalon/red5/webapps/avalon/streams"}, @id="12154", @state=:running, @current_operations=[], @percent_complete=0.0, @output=[], @errors=[], @tech_metadata={}>
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 # "12103"
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("12103")
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 a current operation (multiple possible if outputs are generated in parallel) and percent complete. Technical metadata about the input file is added by the encoding engine. 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, framerate, and dimensions). Outputs are added once they are created and should include the same technical metadata along with an id, label, and url.
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 should be stopped, call cancel:
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 removed outputs will not be reflected in the encoding job.
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. Each service has its own API and idiosyncracies so consult the table below to see what features are supported by each adapter. Add an additional engines by creating an engine adapter class that implements :create, :find, and :cancel.
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
- | Adapter/Feature | Create | Find | Cancel | Preset | Multiple Outputs |
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
- ## Contributing
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
- 1. Fork it ( https://github.com/projecthydra-labs/active_encode/fork )
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
+ ![Samvera Logo](https://wiki.duraspace.org/download/thumbnails/87459292/samvera-fall-font2-200w.png?version=1&modificationDate=1498550535816&api=v2)
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.
@@ -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 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.'
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 "rspec-its"
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
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveEncode
2
3
  class PollingJob < ActiveJob::Base
3
-
4
4
  def perform(encode)
5
5
  encode.run_callbacks(:status_update) { encode }
6
6
  case encode.state
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveEncode
2
3
  class EncodeRecord < ActiveRecord::Base
3
4
  # sql id, globalid, state, adapter, input filename/job title, timestamps
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ ActiveEncode::Engine.routes.draw do
3
+ resources :encode_record, only: [:show]
4
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class CreateActiveEncodeEncodeRecords < ActiveRecord::Migration[5.0]
2
3
  def change
3
4
  create_table :active_encode_encode_records do |t|
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ class AddCreateOptionsToActiveEncodeEncodeRecords < ActiveRecord::Migration[5.0]
3
+ def change
4
+ add_column :active_encode_encode_records, :create_options, :text
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ class AddProgressToActiveEncodeEncodeRecords < ActiveRecord::Migration[5.1]
3
+ def change
4
+ add_column :active_encode_encode_records, :progress, :float
5
+ end
6
+ end
data/lib/active_encode.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_encode/version'
2
3
  require 'active_encode/base'
3
4
  require 'active_encode/engine'
@@ -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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_model/callbacks'
2
3
 
3
4
  module ActiveEncode
@@ -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(self.input.url, self.options))
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(self.id))
60
+ merge!(self.class.engine_adapter.cancel(id))
60
61
  end
61
62
  end
62
63
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails'
2
3
 
3
4
  module ActiveEncode
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_encode/engine_adapters'
2
3
  require 'active_support/core_ext/class/attribute'
3
4
  require 'active_support/core_ext/string/inflections'
@@ -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'.freeze
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
- # TODO: add a stub for an input helper (supplied by an initializer) that transforms encode.input into a zencoder accepted url
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: input_url },
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, opts = {})
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).job
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), convert_options(job))
59
+ encode = ActiveEncode::Base.new(convert_input(job), {})
41
60
  encode.id = job.id
42
- encode.state = convert_state(job)
43
- encode.current_operations = convert_current_operations(job)
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 = convert_errors(job)
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, :checksum, :audio_codec, :video_codec,
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 convert_current_operations(_job)
82
- current_ops = []
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 convert_percent_complete(job)
87
- case job.status
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 convert_options(_job_details)
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.outputs.collect do |o|
109
- # It is assumed that the first part of the output key can be used to label the output
110
- # e.g. "quality-medium/somepath/filename.flv"
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.id = o.id
113
- output.label = o.key.split("/", 2).first
114
- output.url = job.output_key_prefix + o.key
115
- # TODO: If HLS is considered distinct from this output then it should be a different output
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.blank?
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