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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa6172de1470349b8dd8c2422c51049402168ca45eba77aa62ab6b7d9637f4ae
4
- data.tar.gz: 999ec2e97a500f3bb258db4d4fbfa6e5fa4132dfefb4a82da1667eefa6875cff
3
+ metadata.gz: 4f90e9c3cedb039d22d5576047ff97f27007e1ab0a4a42be49a8af6332152237
4
+ data.tar.gz: cd525b293617c059017568d2bd9d70817213f4b81e9331cf08b330412c8f35c7
5
5
  SHA512:
6
- metadata.gz: a78df8af781cf5f4f198a9b4d9e994108294fd0248725d9b6553a96fb829de41afc7a7ccc69cadacafe8d6cb4ac809cb4331c2331c968b7429b7306e19cf3f51
7
- data.tar.gz: 358557321351790685a68c4604d6ca43592c928523085e96f5353603ffe8ad0bfc70b6bcde119b2275cb3d3bbec7b6be78c74b84c3f65900dcd2f0e8f3c38421
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-2_rails7-0"
69
- ruby_version: "3.2.0"
70
- rails_version: "7.0.4.2"
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-1_rails7-0"
73
- ruby_version: "3.1.3"
74
- rails_version: "7.0.4.2"
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-0_rails7-0"
77
- ruby_version: "3.0.5"
78
- rails_version: "7.0.4.2"
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-0_rails6-1"
81
- ruby_version: "3.0.5"
82
- rails_version: "6.1.7.2"
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-0_rails6-0"
85
- ruby_version: "3.0.5"
86
- rails_version: "6.0.6.1"
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: "ruby2-7_rails6-0"
89
- ruby_version: "2.7.7"
90
- rails_version: "6.0.6.1"
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: "ruby2-7_rails5-2"
93
- ruby_version: "2.7.7"
94
- rails_version: "5.2.8.1"
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/master/my_contribution main`
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 hydra-transcoder.gemspec
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: [![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)
4
+ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/samvera-labs/active_encode/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/samvera-labs/active_encode/tree/main)
5
+ [![Coverage Status](https://coveralls.io/repos/github/samvera-labs/active_encode/badge.svg?branch=main)](https://coveralls.io/github/samvera-labs/active_encode?branch=main)
6
6
 
7
7
  Docs: [![Contribution Guidelines](http://img.shields.io/badge/CONTRIBUTING-Guidelines-blue.svg)](./CONTRIBUTING.md)
8
8
  [![Apache 2.0 License](http://img.shields.io/badge/APACHE2-license-blue.svg)](./LICENSE)
@@ -11,7 +11,7 @@ Jump in: [![Slack Status](http://slack.samvera.org/badge.svg)](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/), and [Zencoder](http://zencoder.com).
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
@@ -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 Zencoder.'
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"
@@ -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 { |ned| Dir.children(ned) == ["outputs"] && directories_to_delete.include?(File.join(ned, "outputs")) }
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 ffmpeg_command(input_url, id, opts)
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: get_xpath_text(doc, '//track[@type="Audio"]/CodecID/text()', :to_s),
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**, there is experimental functionality to get what we can directly from the job without
50
- # requiring a CloudWatch log -- this is expected to be complete only for HLS output at present.
51
- # It seems to work well for HLS output. To opt-in, and not require CloudWatch logs:
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
- attr_accessor :role, :output_bucket, :direct_output_lookup
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
- attr_writer :log_group, :queue
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
- # Right now we only know how to get a URL for hls output, although
344
- # the others should be possible and very analagous, just not familiar with them.
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: output_group_settings.hls_group_settings.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: "m3u8"
386
+ file_suffix: output_settings[index].container_settings.container.downcase
351
387
  )
352
388
  end
353
389
 
354
- tech_md = MediaConvertOutput.tech_metadata_from_settings(
355
- output_url: output_url,
356
- output_settings: output_settings[index],
357
- output_detail_settings: output_group_detail
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
- [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
366
- :audio_bitrate, :video_bitrate, :file_size, :label, :url, :id].each do |field|
367
- output.send("#{field}=", tech_md[field])
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.id = "#{job.id}-output-auto"
388
-
389
- [:duration, :audio_codec, :video_codec].each do |field|
390
- output.send("#{field}=", outputs.first.send(field))
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
- [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
419
- :audio_bitrate, :video_bitrate, :file_size, :label, :url, :id].each do |field|
420
- output.send("#{field}=", tech_md[field])
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.id = "#{job.id}-output-auto"
432
-
433
- [:duration, :audio_codec, :video_codec].each do |field|
434
- output.send("#{field}=", outputs.first.send(field))
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 ||= begin
480
- endpoint = Aws::MediaConvert::Client.new.describe_endpoints.endpoints.first.url
481
- Aws::MediaConvert::Client.new(endpoint: endpoint)
482
- end
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