active_encode 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +39 -21
- data/.rubocop_todo.yml +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +1 -14
- data/README.md +9 -3
- data/active_encode.gemspec +4 -2
- data/lib/active_encode/engine.rb +12 -0
- data/lib/active_encode/engine_adapters/ffmpeg_adapter/cleaner.rb +5 -1
- data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +64 -6
- data/lib/active_encode/engine_adapters/media_convert_adapter.rb +95 -48
- data/lib/active_encode/engine_adapters/media_convert_output.rb +35 -14
- data/lib/active_encode/engine_adapters/pass_through_adapter.rb +6 -1
- data/lib/active_encode/engine_adapters.rb +0 -2
- data/lib/active_encode/filename_sanitizer.rb +3 -2
- data/lib/active_encode/technical_metadata.rb +13 -1
- data/lib/active_encode/version.rb +1 -1
- metadata +34 -11
- data/lib/active_encode/engine_adapters/matterhorn_adapter.rb +0 -300
- data/lib/active_encode/engine_adapters/zencoder_adapter.rb +0 -156
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f90e9c3cedb039d22d5576047ff97f27007e1ab0a4a42be49a8af6332152237
|
4
|
+
data.tar.gz: cd525b293617c059017568d2bd9d70817213f4b81e9331cf08b330412c8f35c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cdfba6f2f6f5712ecb9c54f8f7fd61c679559ca07930c43430cc26a85d96e7469d6b1d33061922dc05abf2a287b92676a7c4cf9bc66f4216607cafdc7fa86b3
|
7
|
+
data.tar.gz: 9c7ce5b39d81c746eda7bf54e023c6893300aa6a5bd4743f5afefe430189e62cd7419ff065d86bd3be4368b44bf4005ba18241472d1374f74744700905272ed5
|
data/.circleci/config.yml
CHANGED
@@ -46,6 +46,16 @@ jobs:
|
|
46
46
|
|
47
47
|
- samvera/cached_checkout
|
48
48
|
|
49
|
+
- run:
|
50
|
+
name: Check for a branch named 'master'
|
51
|
+
command: |
|
52
|
+
git fetch --all --quiet --prune --prune-tags
|
53
|
+
if [[ -n "$(git branch --all --list master */master)" ]]; then
|
54
|
+
echo "A branch named 'master' was found. Please remove it."
|
55
|
+
echo "$(git branch --all --list master */master)"
|
56
|
+
fi
|
57
|
+
[[ -z "$(git branch --all --list master */master)" ]]
|
58
|
+
|
49
59
|
- samvera/bundle:
|
50
60
|
ruby_version: << parameters.ruby_version >>
|
51
61
|
bundler_version: << parameters.bundler_version >>
|
@@ -65,30 +75,38 @@ workflows:
|
|
65
75
|
ci:
|
66
76
|
jobs:
|
67
77
|
- bundle_and_test:
|
68
|
-
name: "ruby3-
|
69
|
-
ruby_version: "3.
|
70
|
-
rails_version: "
|
78
|
+
name: "ruby3-4_rails8-0"
|
79
|
+
ruby_version: "3.4.1"
|
80
|
+
rails_version: "8.0.1"
|
81
|
+
- bundle_and_test:
|
82
|
+
name: "ruby3-4_rails7-2"
|
83
|
+
ruby_version: "3.4.1"
|
84
|
+
rails_version: "7.2.2.1"
|
85
|
+
- bundle_and_test:
|
86
|
+
name: "ruby3-4_rails7-1"
|
87
|
+
ruby_version: "3.4.1"
|
88
|
+
rails_version: "7.1.5.1"
|
71
89
|
- bundle_and_test:
|
72
|
-
name: "ruby3-
|
73
|
-
ruby_version: "3.
|
74
|
-
rails_version: "
|
90
|
+
name: "ruby3-3_rails8-0"
|
91
|
+
ruby_version: "3.3.7"
|
92
|
+
rails_version: "8.0.1"
|
75
93
|
- bundle_and_test:
|
76
|
-
name: "ruby3-
|
77
|
-
ruby_version: "3.
|
78
|
-
rails_version: "7.
|
94
|
+
name: "ruby3-3_rails7-2"
|
95
|
+
ruby_version: "3.3.7"
|
96
|
+
rails_version: "7.2.2.1"
|
79
97
|
- bundle_and_test:
|
80
|
-
name: "ruby3-
|
81
|
-
ruby_version: "3.
|
82
|
-
rails_version: "
|
98
|
+
name: "ruby3-3_rails7-1"
|
99
|
+
ruby_version: "3.3.7"
|
100
|
+
rails_version: "7.1.5.1"
|
83
101
|
- bundle_and_test:
|
84
|
-
name: "ruby3-
|
85
|
-
ruby_version: "3.
|
86
|
-
rails_version: "
|
102
|
+
name: "ruby3-2_rails8-0"
|
103
|
+
ruby_version: "3.2.7"
|
104
|
+
rails_version: "8.0.1"
|
87
105
|
- bundle_and_test:
|
88
|
-
name: "
|
89
|
-
ruby_version: "2.7
|
90
|
-
rails_version: "
|
106
|
+
name: "ruby3-2_rails7-2"
|
107
|
+
ruby_version: "3.2.7"
|
108
|
+
rails_version: "7.2.2.1"
|
91
109
|
- bundle_and_test:
|
92
|
-
name: "
|
93
|
-
ruby_version: "2.7
|
94
|
-
rails_version: "
|
110
|
+
name: "ruby3-2_rails7-1"
|
111
|
+
ruby_version: "3.2.7"
|
112
|
+
rails_version: "7.1.5.1"
|
data/.rubocop_todo.yml
CHANGED
@@ -8,6 +8,7 @@ Metrics/AbcSize:
|
|
8
8
|
Metrics/BlockLength:
|
9
9
|
Exclude:
|
10
10
|
- 'lib/active_encode/spec/shared_specs/*'
|
11
|
+
- 'lib/active_encode/engine_adapters/media_convert_adapter.rb'
|
11
12
|
- 'spec/**/*'
|
12
13
|
|
13
14
|
Metrics/BlockNesting:
|
@@ -26,12 +27,11 @@ Metrics/CyclomaticComplexity:
|
|
26
27
|
Exclude:
|
27
28
|
- 'lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb'
|
28
29
|
- 'lib/active_encode/engine_adapters/ffmpeg_adapter.rb'
|
29
|
-
- 'lib/active_encode/engine_adapters/zencoder_adapter.rb'
|
30
30
|
- 'lib/active_encode/engine_adapters/media_convert_adapter.rb'
|
31
|
+
- 'lib/active_encode/engine_adapters/media_convert_output.rb'
|
31
32
|
|
32
33
|
Layout/LineLength:
|
33
34
|
Exclude:
|
34
|
-
- 'lib/active_encode/engine_adapters/matterhorn_adapter.rb'
|
35
35
|
- 'spec/**/*'
|
36
36
|
|
37
37
|
Metrics/MethodLength:
|
@@ -44,6 +44,7 @@ Metrics/PerceivedComplexity:
|
|
44
44
|
Exclude:
|
45
45
|
- 'lib/active_encode/engine_adapters/ffmpeg_adapter.rb'
|
46
46
|
- 'lib/active_encode/engine_adapters/media_convert_adapter.rb'
|
47
|
+
- 'lib/active_encode/engine_adapters/media_convert_output.rb'
|
47
48
|
- 'lib/file_locator.rb'
|
48
49
|
|
49
50
|
RSpec/AnyInstance:
|
@@ -61,7 +62,6 @@ RSpec/InstanceVariable:
|
|
61
62
|
|
62
63
|
RSpec/MessageSpies:
|
63
64
|
Exclude:
|
64
|
-
- 'spec/integration/matterhorn_adapter_spec.rb'
|
65
65
|
- 'spec/integration/ffmpeg_adapter_spec.rb'
|
66
66
|
|
67
67
|
RSpec/NamedSubject:
|
data/CONTRIBUTING.md
CHANGED
@@ -68,7 +68,7 @@ further details.
|
|
68
68
|
* Fork the repository on GitHub
|
69
69
|
* Create a topic branch from where you want to base your work.
|
70
70
|
* This is usually the `main` branch.
|
71
|
-
* To quickly create a topic branch based on `main`; `git branch fix/
|
71
|
+
* To quickly create a topic branch based on `main`; `git branch fix/main/my_contribution main`
|
72
72
|
* Then checkout the new branch with `git checkout fix/main/my_contribution`.
|
73
73
|
* Please avoid working directly on the `main` branch.
|
74
74
|
* Please do not create a branch called `master`. (See note below.)
|
data/Gemfile
CHANGED
@@ -1,15 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
source 'https://rubygems.org'
|
3
3
|
|
4
|
-
# Specify your gem's dependencies in
|
4
|
+
# Specify your gem's dependencies in active_encode.gemspec
|
5
5
|
gemspec
|
6
6
|
|
7
7
|
gem 'aws-sdk-elastictranscoder'
|
8
8
|
gem 'aws-sdk-s3'
|
9
9
|
gem 'byebug'
|
10
|
-
gem 'rubyhorn', git: "https://github.com/avalonmediasystem/rubyhorn.git"
|
11
|
-
gem 'shingoncoder'
|
12
|
-
gem 'zencoder'
|
13
10
|
|
14
11
|
# BEGIN ENGINE_CART BLOCK
|
15
12
|
# engine_cart: 2.4.0
|
@@ -33,16 +30,6 @@ else
|
|
33
30
|
else
|
34
31
|
gem 'rails', ENV['RAILS_VERSION']
|
35
32
|
end
|
36
|
-
|
37
|
-
case ENV['RAILS_VERSION']
|
38
|
-
when /^6.0/
|
39
|
-
gem 'sass-rails', '>= 6'
|
40
|
-
gem 'webpacker', '~> 4.0'
|
41
|
-
when /^5.[12]/
|
42
|
-
gem 'sass-rails', '~> 5.0'
|
43
|
-
gem 'sprockets', '~> 3.7'
|
44
|
-
gem 'thor', '~> 0.20'
|
45
|
-
end
|
46
33
|
end
|
47
34
|
end
|
48
35
|
# END ENGINE_CART BLOCK
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# ActiveEncode
|
2
2
|
|
3
3
|
Code: [](http://badge.fury.io/rb/active_encode)
|
4
|
-
[](https://dl.circleci.com/status-badge/redirect/gh/samvera-labs/active_encode/tree/main)
|
5
|
+
[](https://coveralls.io/github/samvera-labs/active_encode?branch=main)
|
6
6
|
|
7
7
|
Docs: [](./CONTRIBUTING.md)
|
8
8
|
[](./LICENSE)
|
@@ -11,7 +11,7 @@ Jump in: [](http://slack.samv
|
|
11
11
|
|
12
12
|
# What is ActiveEncode?
|
13
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/), [AWS Elemental MediaConvert](https://aws.amazon.com/mediaconvert/)
|
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 [AWS Elemental MediaConvert](https://aws.amazon.com/mediaconvert/).
|
15
15
|
|
16
16
|
# Help
|
17
17
|
|
@@ -247,6 +247,12 @@ RSpec.describe MyCustomAdapter do
|
|
247
247
|
end
|
248
248
|
```
|
249
249
|
|
250
|
+
## Contributing
|
251
|
+
|
252
|
+
If you're working on PR for this project, create a feature branch off of `main`.
|
253
|
+
|
254
|
+
This repository follows the [Samvera Community Code of Conduct](https://samvera.atlassian.net/wiki/spaces/samvera/pages/405212316/Code+of+Conduct) and [language recommendations](https://github.com/samvera/maintenance/blob/main/templates/CONTRIBUTING.md#language). Please ***do not*** create a branch called `master` for this repository or as part of your pull request; the branch will either need to be removed or renamed before it can be considered for inclusion in the code base and history of this repository.
|
255
|
+
|
250
256
|
# Acknowledgments
|
251
257
|
|
252
258
|
This software has been developed by and is brought to you by the Samvera community. Learn more at the
|
data/active_encode.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.authors = ["Michael Klein, Chris Colvard, Phuong Dinh"]
|
12
12
|
spec.email = ["mbklein@gmail.com, chris.colvard@gmail.com, phuongdh@gmail.com"]
|
13
13
|
spec.summary = 'Declare encode job classes that can be run by a variety of encoding services'
|
14
|
-
spec.description = 'This gem provides an interface to transcoding services such as Ffmpeg, Amazon Elastic Transcoder, or
|
14
|
+
spec.description = 'This gem provides an interface to transcoding services such as Ffmpeg, Amazon Elastic Transcoder, or Amazon Elemental MediaConvert.'
|
15
15
|
spec.homepage = "https://github.com/samvera-labs/active_encode"
|
16
16
|
spec.license = "Apache-2.0"
|
17
17
|
|
@@ -22,11 +22,13 @@ Gem::Specification.new do |spec|
|
|
22
22
|
|
23
23
|
spec.add_dependency "rails"
|
24
24
|
spec.add_dependency "addressable", "~> 2.8"
|
25
|
+
spec.add_dependency "retriable"
|
25
26
|
|
26
27
|
spec.add_development_dependency "aws-sdk-cloudwatchevents"
|
27
28
|
spec.add_development_dependency "aws-sdk-cloudwatchlogs"
|
29
|
+
spec.add_development_dependency "aws-sdk-core", "<= 3.220.0"
|
28
30
|
spec.add_development_dependency "aws-sdk-elastictranscoder"
|
29
|
-
spec.add_development_dependency "aws-sdk-mediaconvert"
|
31
|
+
spec.add_development_dependency "aws-sdk-mediaconvert", ">= 1.157.0"
|
30
32
|
spec.add_development_dependency "aws-sdk-s3"
|
31
33
|
spec.add_development_dependency "bixby", '~> 5.0', '>= 5.0.2'
|
32
34
|
spec.add_development_dependency "bundler"
|
data/lib/active_encode/engine.rb
CHANGED
@@ -4,5 +4,17 @@ require 'rails'
|
|
4
4
|
module ActiveEncode
|
5
5
|
class Engine < ::Rails::Engine
|
6
6
|
isolate_namespace ActiveEncode
|
7
|
+
|
8
|
+
config.before_configuration do
|
9
|
+
# rubocop:disable Style/IfUnlessModifier
|
10
|
+
# see https://github.com/fxn/zeitwerk#for_gem
|
11
|
+
# Blacklight puts a generator into LOCAL APP lib/generators, so tell
|
12
|
+
# zeitwerk to ignore the whole directory? If we're using zeitwerk
|
13
|
+
#
|
14
|
+
# See: https://github.com/cbeer/engine_cart/issues/117
|
15
|
+
if ::Rails.try(:autoloaders).try(:main).respond_to?(:ignore)
|
16
|
+
::Rails.autoloaders.main.ignore(::Rails.root.join('lib', 'generators'))
|
17
|
+
end
|
18
|
+
end
|
7
19
|
end
|
8
20
|
end
|
@@ -53,7 +53,11 @@ module ActiveEncode
|
|
53
53
|
def remove_empty_directories(directories)
|
54
54
|
directories_to_delete = directories.select { |d| Dir.empty?(d) }
|
55
55
|
non_empty_directories = directories - directories_to_delete
|
56
|
-
directories_to_delete += non_empty_directories.select
|
56
|
+
directories_to_delete += non_empty_directories.select do |ned|
|
57
|
+
Dir.children(ned).sort == ["outputs", "supplemental_files"] &&
|
58
|
+
directories_to_delete.include?(File.join(ned, "outputs")) &&
|
59
|
+
directories_to_delete.include?(File.join(ned, "supplemental_files"))
|
60
|
+
end
|
57
61
|
FileUtils.rmdir(directories_to_delete) unless directories_to_delete.empty?
|
58
62
|
end
|
59
63
|
|
@@ -42,6 +42,7 @@ module ActiveEncode
|
|
42
42
|
# Create a working directory that holds all output files related to the encode
|
43
43
|
FileUtils.mkdir_p working_path("", new_encode.id)
|
44
44
|
FileUtils.mkdir_p working_path("outputs", new_encode.id)
|
45
|
+
FileUtils.mkdir_p working_path("supplemental_files", new_encode.id)
|
45
46
|
|
46
47
|
# Extract technical metadata from input file
|
47
48
|
curl_option = if options && options[:headers]
|
@@ -91,12 +92,14 @@ module ActiveEncode
|
|
91
92
|
new_encode.input.duration = fixed_duration(working_path("duration_input_metadata", new_encode.id))
|
92
93
|
end
|
93
94
|
|
95
|
+
subtitle_count = new_encode.input.subtitles.length if new_encode.input.subtitles.present?
|
96
|
+
|
94
97
|
new_encode.state = :running
|
95
98
|
new_encode.percent_complete = 1
|
96
99
|
new_encode.errors = []
|
97
100
|
|
98
101
|
# Run the ffmpeg command and save its pid
|
99
|
-
command = ffmpeg_command(input_url, new_encode.id, options)
|
102
|
+
command = ffmpeg_command(input_url, new_encode.id, options, subtitle_count)
|
100
103
|
# Capture the exit status in a file in order to differentiate warning output in stderr between real process failure
|
101
104
|
exit_status_file = working_path("exit_status.code", new_encode.id)
|
102
105
|
command = "#{command}; echo $? > #{exit_status_file}"
|
@@ -150,6 +153,7 @@ module ActiveEncode
|
|
150
153
|
end
|
151
154
|
|
152
155
|
encode.output = build_outputs encode if encode.completed?
|
156
|
+
encode.output += build_supplemental_outputs encode if encode.completed?
|
153
157
|
|
154
158
|
encode
|
155
159
|
end
|
@@ -198,6 +202,7 @@ module ActiveEncode
|
|
198
202
|
|
199
203
|
def write_errors(encode)
|
200
204
|
File.write(working_path("error.log", encode.id), encode.errors.join("\n"))
|
205
|
+
File.write(working_path("exit_status.code", encode.id), "1") unless File.exist?(working_path("exit_status.code", encode.id))
|
201
206
|
end
|
202
207
|
|
203
208
|
def read_errors(id)
|
@@ -251,17 +256,51 @@ module ActiveEncode
|
|
251
256
|
outputs
|
252
257
|
end
|
253
258
|
|
254
|
-
def
|
259
|
+
def build_supplemental_outputs(encode)
|
260
|
+
id = encode.id
|
261
|
+
files = []
|
262
|
+
Dir["#{File.absolute_path(working_path('supplemental_files', id))}/*"].each_with_index do |file_path, index|
|
263
|
+
file = ActiveEncode::Output.new
|
264
|
+
file.url = "file://#{file_path}"
|
265
|
+
file.id = "#{encode.input.id}-#{File.basename(file_path)}"
|
266
|
+
file.created_at = encode.created_at
|
267
|
+
file.updated_at = File.mtime file_path
|
268
|
+
file.label = encode.input.subtitles[index][:label]
|
269
|
+
file.language = encode.input.subtitles[index][:language]
|
270
|
+
file.format = 'vtt'
|
271
|
+
|
272
|
+
files << file
|
273
|
+
end
|
274
|
+
|
275
|
+
files
|
276
|
+
end
|
277
|
+
|
278
|
+
def ffmpeg_command(input_url, id, opts, subtitle_count = nil)
|
279
|
+
sanitized_filename = ActiveEncode.sanitize_base input_url
|
255
280
|
output_opt = opts[:outputs].collect do |output|
|
256
|
-
sanitized_filename = ActiveEncode.sanitize_base input_url
|
257
281
|
file_name = "outputs/#{sanitized_filename}-#{output[:label]}.#{output[:extension]}"
|
258
282
|
" #{output[:ffmpeg_opt]} #{working_path(file_name, id)}"
|
259
283
|
end.join(" ")
|
284
|
+
|
285
|
+
supplemental_file_opt = if opts[:extract_subtitles] && subtitle_count.present?
|
286
|
+
caption_extraction_options(sanitized_filename, subtitle_count, id)
|
287
|
+
end
|
288
|
+
|
260
289
|
header_opt = Array(opts[:headers]).map do |k, v|
|
261
290
|
"#{k}: #{v}\r\n"
|
262
291
|
end.join
|
263
292
|
header_opt = "-headers '#{header_opt}'" if header_opt.present?
|
264
|
-
"#{FFMPEG_PATH} #{header_opt} -y -loglevel level+fatal -progress #{working_path('progress', id)} -i \"#{input_url}\" #{output_opt}"
|
293
|
+
"#{FFMPEG_PATH} #{header_opt} -y -loglevel level+fatal -progress #{working_path('progress', id)} -i \"#{input_url}\" #{supplemental_file_opt} #{output_opt}"
|
294
|
+
end
|
295
|
+
|
296
|
+
def caption_extraction_options(filename, count, id)
|
297
|
+
opts = ""
|
298
|
+
(0..count - 1).each do |i|
|
299
|
+
subtitle_filename = "supplemental_files/#{filename}-caption#{i}.vtt"
|
300
|
+
opts += " -map 0:s:#{i} -c:s webvtt #{working_path(subtitle_filename, id)}"
|
301
|
+
end
|
302
|
+
|
303
|
+
opts
|
265
304
|
end
|
266
305
|
|
267
306
|
def get_pid(id)
|
@@ -273,6 +312,8 @@ module ActiveEncode
|
|
273
312
|
end
|
274
313
|
|
275
314
|
def running?(pid)
|
315
|
+
return false if pid.nil?
|
316
|
+
|
276
317
|
Process.getpgid pid.to_i
|
277
318
|
true
|
278
319
|
rescue Errno::ESRCH
|
@@ -314,22 +355,39 @@ module ActiveEncode
|
|
314
355
|
doc.remove_namespaces!
|
315
356
|
duration = get_xpath_text(doc, '//Duration/text()', :to_f)
|
316
357
|
duration *= 1000 unless duration.nil? # Convert to milliseconds
|
358
|
+
audio_codec = get_xpath_text(doc, '//track[@type="Audio"]/CodecID/text()', :to_s)
|
359
|
+
if get_xpath_text(doc, '//track[@type="Audio"]/Format/text()', :to_s) == "MPEG Audio" &&
|
360
|
+
get_xpath_text(doc, '//track[@type="Audio"]/Format_Profile/text()', :to_s) == "Layer 3"
|
361
|
+
audio_codec ||= "mp3"
|
362
|
+
end
|
363
|
+
|
317
364
|
{ url: get_xpath_text(doc, '//media/@ref', :to_s),
|
318
365
|
width: get_xpath_text(doc, '//Width/text()', :to_f),
|
319
366
|
height: get_xpath_text(doc, '//Height/text()', :to_f),
|
320
367
|
frame_rate: get_xpath_text(doc, '//FrameRate/text()', :to_f),
|
321
368
|
duration: duration,
|
322
369
|
file_size: get_xpath_text(doc, '//FileSize/text()', :to_i),
|
323
|
-
audio_codec:
|
370
|
+
audio_codec: audio_codec,
|
324
371
|
audio_bitrate: get_xpath_text(doc, '//track[@type="Audio"]/BitRate/text()', :to_i),
|
325
372
|
video_codec: get_xpath_text(doc, '//track[@type="Video"]/CodecID/text()', :to_s),
|
326
|
-
video_bitrate: get_xpath_text(doc, '//track[@type="Video"]/BitRate/text()', :to_i)
|
373
|
+
video_bitrate: get_xpath_text(doc, '//track[@type="Video"]/BitRate/text()', :to_i),
|
374
|
+
subtitles: get_subtitle_tech_metadata(doc).compact }
|
327
375
|
end
|
328
376
|
|
329
377
|
def get_xpath_text(doc, xpath, cast_method)
|
330
378
|
doc.xpath(xpath).first&.text&.send(cast_method)
|
331
379
|
end
|
332
380
|
|
381
|
+
def get_subtitle_tech_metadata(doc)
|
382
|
+
doc.xpath("//track[@type='Text' and Format='Timed Text']").collect do |track|
|
383
|
+
{
|
384
|
+
format: get_xpath_text(track, "./CodecID/text()", :to_s),
|
385
|
+
label: get_xpath_text(track, "./Title/text()", :to_s),
|
386
|
+
language: get_xpath_text(track, "./Language/text()", :to_s)
|
387
|
+
}
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
333
391
|
def fixed_duration(path)
|
334
392
|
get_tech_metadata(path)[:duration]
|
335
393
|
end
|
@@ -6,6 +6,7 @@ require 'aws-sdk-cloudwatchevents'
|
|
6
6
|
require 'aws-sdk-cloudwatchlogs'
|
7
7
|
require 'aws-sdk-mediaconvert'
|
8
8
|
require 'file_locator'
|
9
|
+
require 'retriable'
|
9
10
|
|
10
11
|
require 'active_support/json'
|
11
12
|
require 'active_support/time'
|
@@ -46,12 +47,16 @@ module ActiveEncode
|
|
46
47
|
#
|
47
48
|
# ActiveEncode::Base.engine_adapter.setup!
|
48
49
|
#
|
49
|
-
# **OR
|
50
|
-
#
|
51
|
-
#
|
50
|
+
# **OR** There is functionality to get what we can directly from the job without requiring
|
51
|
+
# a CloudWatch log -- this only works for HLS and file outputs at present.
|
52
|
+
# To opt-in, and not require CloudWatch logs:
|
52
53
|
#
|
53
54
|
# ActiveEncode::Base.engine_adapter.direct_output_lookup = true
|
54
55
|
#
|
56
|
+
# MediaConvert also provides a probe endpoint to extract technical metadata about a file stored
|
57
|
+
# in s3. This can be used to gather output metadata when using direct_output_lookup instead of
|
58
|
+
# having to look in the CloudWatch logs.
|
59
|
+
#
|
55
60
|
# ## Example
|
56
61
|
#
|
57
62
|
# ActiveEncode::Base.engine_adapter = :media_convert
|
@@ -102,16 +107,42 @@ module ActiveEncode
|
|
102
107
|
end
|
103
108
|
end
|
104
109
|
|
110
|
+
class RetriableClient
|
111
|
+
def initialize
|
112
|
+
@retry_params = { on: Aws::MediaConvert::Errors::ServiceError, base_interval: 1 }
|
113
|
+
endpoint = Retriable.retriable(@retry_params) { Aws::MediaConvert::Client.new.describe_endpoints.endpoints.first.url }
|
114
|
+
@client = Aws::MediaConvert::Client.new(endpoint: endpoint)
|
115
|
+
end
|
116
|
+
|
117
|
+
def method_missing(method, *args, &block)
|
118
|
+
if @client.respond_to?(method)
|
119
|
+
Retriable.retriable(@retry_params) { @client.send(method, *args, &block) }
|
120
|
+
else
|
121
|
+
super
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def respond_to_missing?(method_name, include_private = false)
|
126
|
+
@client.respond_to?(method_name) || super
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
105
130
|
# @!attribute [rw] role simple name of AWS role to pass to MediaConvert, eg `my-role-name`
|
106
131
|
# @!attribute [rw] output_bucket simple bucket name to write output to
|
107
132
|
# @!attribute [rw] direct_output_lookup if true, do NOT get output information from cloudwatch,
|
108
133
|
# instead retrieve and construct it only from job itself. Currently
|
109
|
-
# working only for HLS output. default false
|
110
|
-
|
134
|
+
# working only for HLS output. (default: false)
|
135
|
+
# @!attribute [rw] use_probe if true use probe endpoint to get technical metadata about input and outputs (default: false)
|
136
|
+
attr_accessor :role, :output_bucket, :direct_output_lookup, :use_probe
|
111
137
|
|
112
138
|
# @!attribute [w] log_group log_group_name that is being used to capture output
|
113
139
|
# @!attribute [w] queue name of MediaConvert queue to use.
|
114
|
-
|
140
|
+
# @!attribute [w] output_id_format sprintf format for output ids (default: "%{job_id}-output%{suffix}")
|
141
|
+
# Available string variables are job_id and the keys in the MediaConvertOutput tech meatdata hash
|
142
|
+
# @!attribute [w] output_label_format sprintf format for output ids (default: "%{basename}" if output_url is preset
|
143
|
+
# otherwise "%{suffix}")
|
144
|
+
# Available string variables are job_id, basename, and the keys in the MediaConvertOutput tech metadata hash
|
145
|
+
attr_writer :log_group, :queue, :output_id_format, :output_label_format
|
115
146
|
|
116
147
|
# Creates a [CloudWatch Logs]
|
117
148
|
# (https://aws.amazon.com/cloudwatch/) log group and an EventBridge rule to forward status
|
@@ -274,6 +305,10 @@ module ActiveEncode
|
|
274
305
|
|
275
306
|
encode.input.created_at = encode.created_at
|
276
307
|
encode.input.updated_at = encode.updated_at
|
308
|
+
if use_probe
|
309
|
+
tech_md = MediaConvertOutput.tech_metadata_from_probe(url: encode.input.url, probe_response: probe(encode.input.url))
|
310
|
+
encode.input.assign_tech_metadata(tech_md)
|
311
|
+
end
|
277
312
|
|
278
313
|
encode = complete_encode(encode, job) if encode.state == :completed
|
279
314
|
encode
|
@@ -338,35 +373,42 @@ module ActiveEncode
|
|
338
373
|
|
339
374
|
output_group_details = job.dig("output_group_details", 0, "output_details")
|
340
375
|
file_input_url = job.dig("settings", "inputs", 0, "file_input")
|
376
|
+
output_group_type = output_group_settings.type.downcase.to_sym
|
377
|
+
output_destination = output_group_settings[output_group_type].destination
|
341
378
|
|
342
379
|
outputs = output_group_details.map.with_index do |output_group_detail, index|
|
343
|
-
#
|
344
|
-
|
345
|
-
if output_group_settings.type == "HLS_GROUP_SETTINGS"
|
380
|
+
# Only HLS and file output groups have been tested with this approach
|
381
|
+
if [:hls_group_settings, :file_group_settings].include? output_group_type
|
346
382
|
output_url = MediaConvertOutput.construct_output_url(
|
347
|
-
destination:
|
383
|
+
destination: output_destination,
|
348
384
|
file_input_url: file_input_url,
|
349
385
|
name_modifier: output_settings[index].name_modifier,
|
350
|
-
file_suffix:
|
386
|
+
file_suffix: output_settings[index].container_settings.container.downcase
|
351
387
|
)
|
352
388
|
end
|
353
389
|
|
354
|
-
tech_md =
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
390
|
+
tech_md = if use_probe
|
391
|
+
MediaConvertOutput.tech_metadata_from_probe(
|
392
|
+
url: output_url,
|
393
|
+
output_settings: output_settings[index],
|
394
|
+
probe_response: probe(output_url)
|
395
|
+
)
|
396
|
+
else
|
397
|
+
MediaConvertOutput.tech_metadata_from_settings(
|
398
|
+
output_url: output_url,
|
399
|
+
output_settings: output_settings[index],
|
400
|
+
output_detail_settings: output_group_detail
|
401
|
+
)
|
402
|
+
end
|
359
403
|
|
360
404
|
output = ActiveEncode::Output.new
|
361
|
-
|
405
|
+
output.url = output_url
|
362
406
|
output.created_at = job.timing.submit_time
|
363
407
|
output.updated_at = job.timing.finish_time || job.timing.start_time || output.created_at
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
end
|
369
|
-
output.id ||= "#{job.id}-output#{tech_md[:suffix]}"
|
408
|
+
output.assign_tech_metadata(tech_md)
|
409
|
+
file_basename = File.basename(output.url)
|
410
|
+
output.id = format(output_id_format, tech_md.merge(job_id: job.id))
|
411
|
+
output.label = format(output_label_format(file_basename), tech_md.merge(job_id: job.id, basename: file_basename))
|
370
412
|
output
|
371
413
|
end
|
372
414
|
|
@@ -382,15 +424,13 @@ module ActiveEncode
|
|
382
424
|
)
|
383
425
|
|
384
426
|
output = ActiveEncode::Output.new
|
427
|
+
output.url = adaptive_playlist_url
|
385
428
|
output.created_at = job.timing.submit_time
|
386
429
|
output.updated_at = job.timing.finish_time || job.timing.start_time || output.created_at
|
387
|
-
output.
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
end
|
392
|
-
output.label = File.basename(adaptive_playlist_url)
|
393
|
-
output.url = adaptive_playlist_url
|
430
|
+
output.assign_tech_metadata(outputs.first.tech_metadata)
|
431
|
+
file_basename = File.basename(output.url)
|
432
|
+
output.id = format(output_id_format, output.tech_metadata.merge(job_id: job.id, suffix: '-auto'))
|
433
|
+
output.label = format(output_label_format(file_basename), output.tech_metadata.merge(job_id: job.id, suffix: '-auto', basename: file_basename))
|
394
434
|
outputs << output
|
395
435
|
end
|
396
436
|
|
@@ -411,30 +451,26 @@ module ActiveEncode
|
|
411
451
|
outputs = logged_results.dig('detail', 'outputGroupDetails', 0, 'outputDetails').map.with_index do |logged_detail, index|
|
412
452
|
tech_md = MediaConvertOutput.tech_metadata_from_logged(output_settings[index], logged_detail)
|
413
453
|
output = ActiveEncode::Output.new
|
414
|
-
|
454
|
+
output.url = tech_md[:url]
|
415
455
|
output.created_at = job.timing.submit_time
|
416
456
|
output.updated_at = job.timing.finish_time || job.timing.start_time || output.created_at
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
end
|
422
|
-
output.id ||= "#{job.id}-output#{tech_md[:suffix]}"
|
457
|
+
output.assign_tech_metadata(tech_md)
|
458
|
+
file_basename = File.basename(output.url)
|
459
|
+
output.id = format(output_id_format, tech_md.merge(job_id: job.id))
|
460
|
+
output.label = format(output_label_format(file_basename), tech_md.merge(job_id: job.id, basename: file_basename))
|
423
461
|
output
|
424
462
|
end
|
425
463
|
|
426
464
|
adaptive_playlist = logged_results.dig('detail', 'outputGroupDetails', 0, 'playlistFilePaths', 0)
|
427
465
|
unless adaptive_playlist.nil?
|
428
466
|
output = ActiveEncode::Output.new
|
467
|
+
output.url = adaptive_playlist
|
429
468
|
output.created_at = job.timing.submit_time
|
430
469
|
output.updated_at = job.timing.finish_time || job.timing.start_time || output.created_at
|
431
|
-
output.
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
end
|
436
|
-
output.label = File.basename(adaptive_playlist)
|
437
|
-
output.url = adaptive_playlist
|
470
|
+
output.assign_tech_metadata(outputs.first.tech_metadata)
|
471
|
+
file_basename = File.basename(output.url)
|
472
|
+
output.id = format(output_id_format, output.tech_metadata.merge(job_id: job.id, suffix: '-auto'))
|
473
|
+
output.label = format(output_label_format(file_basename), output.tech_metadata.merge(job_id: job.id, suffix: '-auto', basename: file_basename))
|
438
474
|
outputs << output
|
439
475
|
end
|
440
476
|
outputs
|
@@ -476,10 +512,21 @@ module ActiveEncode
|
|
476
512
|
end
|
477
513
|
|
478
514
|
def mediaconvert
|
479
|
-
@mediaconvert ||=
|
480
|
-
|
481
|
-
|
482
|
-
|
515
|
+
@mediaconvert ||= RetriableClient.new
|
516
|
+
end
|
517
|
+
|
518
|
+
def probe(url)
|
519
|
+
# NOTE: certain audio files don't return any track information
|
520
|
+
@probe_cache ||= {}
|
521
|
+
@probe_cache[url] ||= mediaconvert.probe({ input_files: [{ file_url: url }] })&.probe_results&.first
|
522
|
+
end
|
523
|
+
|
524
|
+
def output_id_format
|
525
|
+
@output_id_format || "%{job_id}-output%{suffix}"
|
526
|
+
end
|
527
|
+
|
528
|
+
def output_label_format(file_basename)
|
529
|
+
@output_label_format || (file_basename.present? ? "%{basename}" : "%{suffix}")
|
483
530
|
end
|
484
531
|
|
485
532
|
def s3client
|