active_encode 1.3.0 → 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: a55a4e3f05065b6f69afc4a7d097f9c5c15926aada0fdb5915c3b19e09d024a2
4
- data.tar.gz: 6c3e3b9f78fb430ed77f658a6f2be986dee227b642ce92a0c156998a1d35663c
3
+ metadata.gz: 4f90e9c3cedb039d22d5576047ff97f27007e1ab0a4a42be49a8af6332152237
4
+ data.tar.gz: cd525b293617c059017568d2bd9d70817213f4b81e9331cf08b330412c8f35c7
5
5
  SHA512:
6
- metadata.gz: 1124cbecacf61ec7fa05a5eaf109599d4957e6865c0bc7e8938dc0a5d09f1912c5c93d49764c60301b7d1de0917b6d1af923b3e322812a7cc7f4b1dacf571257
7
- data.tar.gz: 69695a33b2af42d9b19f101e2030209c2483d8bed919fc76542593f599a603bcba720341f0826fdd350154ac4feb62733a7a8453e11b935a28016889dc41e79a
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 >>
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,18 +30,6 @@ else
33
30
  else
34
31
  gem 'rails', ENV['RAILS_VERSION']
35
32
  end
36
-
37
- case ENV['RAILS_VERSION']
38
- when /^[56]/, /^7.0/
39
- gem 'concurrent-ruby', '1.3.4'
40
- when /^6.0/
41
- gem 'sass-rails', '>= 6'
42
- gem 'webpacker', '~> 4.0'
43
- when /^5.[12]/
44
- gem 'sass-rails', '~> 5.0'
45
- gem 'sprockets', '~> 3.7'
46
- gem 'thor', '~> 0.20'
47
- end
48
33
  end
49
34
  end
50
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,12 +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"
28
29
  spec.add_development_dependency "aws-sdk-core", "<= 3.220.0"
29
30
  spec.add_development_dependency "aws-sdk-elastictranscoder"
30
- spec.add_development_dependency "aws-sdk-mediaconvert"
31
+ spec.add_development_dependency "aws-sdk-mediaconvert", ">= 1.157.0"
31
32
  spec.add_development_dependency "aws-sdk-s3"
32
33
  spec.add_development_dependency "bixby", '~> 5.0', '>= 5.0.2'
33
34
  spec.add_development_dependency "bundler"
@@ -202,6 +202,7 @@ module ActiveEncode
202
202
 
203
203
  def write_errors(encode)
204
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))
205
206
  end
206
207
 
207
208
  def read_errors(id)
@@ -311,6 +312,8 @@ module ActiveEncode
311
312
  end
312
313
 
313
314
  def running?(pid)
315
+ return false if pid.nil?
316
+
314
317
  Process.getpgid pid.to_i
315
318
  true
316
319
  rescue Errno::ESRCH
@@ -352,13 +355,19 @@ module ActiveEncode
352
355
  doc.remove_namespaces!
353
356
  duration = get_xpath_text(doc, '//Duration/text()', :to_f)
354
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
+
355
364
  { url: get_xpath_text(doc, '//media/@ref', :to_s),
356
365
  width: get_xpath_text(doc, '//Width/text()', :to_f),
357
366
  height: get_xpath_text(doc, '//Height/text()', :to_f),
358
367
  frame_rate: get_xpath_text(doc, '//FrameRate/text()', :to_f),
359
368
  duration: duration,
360
369
  file_size: get_xpath_text(doc, '//FileSize/text()', :to_i),
361
- audio_codec: get_xpath_text(doc, '//track[@type="Audio"]/CodecID/text()', :to_s),
370
+ audio_codec: audio_codec,
362
371
  audio_bitrate: get_xpath_text(doc, '//track[@type="Audio"]/BitRate/text()', :to_i),
363
372
  video_codec: get_xpath_text(doc, '//track[@type="Video"]/CodecID/text()', :to_s),
364
373
  video_bitrate: get_xpath_text(doc, '//track[@type="Video"]/BitRate/text()', :to_i),
@@ -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
@@ -35,8 +35,8 @@ module ActiveEncode
35
35
  # @param output_detail_settings [Aws::MediaConvert::Types::OutputDetail]
36
36
  def tech_metadata_from_settings(output_url:, output_settings:, output_detail_settings:)
37
37
  {
38
- width: output_detail_settings.video_details.width_in_px,
39
- height: output_detail_settings.video_details.height_in_px,
38
+ width: output_detail_settings.video_details&.width_in_px,
39
+ height: output_detail_settings.video_details&.height_in_px,
40
40
  frame_rate: extract_video_frame_rate(output_settings),
41
41
  duration: output_detail_settings.duration_in_ms,
42
42
  audio_codec: extract_audio_codec(output_settings),
@@ -44,7 +44,6 @@ module ActiveEncode
44
44
  audio_bitrate: extract_audio_bitrate(output_settings),
45
45
  video_bitrate: extract_video_bitrate(output_settings),
46
46
  url: output_url,
47
- label: (output_url ? File.basename(output_url) : output_settings.name_modifier),
48
47
  suffix: output_settings.name_modifier
49
48
  }
50
49
  end
@@ -61,11 +60,33 @@ module ActiveEncode
61
60
  audio_bitrate: extract_audio_bitrate(settings),
62
61
  video_bitrate: extract_video_bitrate(settings),
63
62
  url: url,
64
- label: File.basename(url),
65
63
  suffix: settings.name_modifier
66
64
  }
67
65
  end
68
66
 
67
+ def tech_metadata_from_probe(url:, probe_response:, output_settings: nil)
68
+ tech_md = { url: url, suffix: output_settings&.name_modifier }
69
+ return tech_md unless probe_response
70
+
71
+ # Need to determine which track has video/audio
72
+ video_track = probe_response.container.tracks&.find { |track| track.track_type == "video" }
73
+ audio_track = probe_response.container.tracks&.find { |track| track.track_type == "audio" }
74
+ frame_rate = (video_track.video_properties.frame_rate.numerator / video_track.video_properties.frame_rate.denominator.to_f).round(2) if video_track
75
+ duration = probe_response.container.duration * 1000 if probe_response.container.duration.present?
76
+
77
+ tech_md.merge({
78
+ width: video_track&.video_properties&.width,
79
+ height: video_track&.video_properties&.height,
80
+ frame_rate: frame_rate,
81
+ duration: duration, # milliseconds
82
+ audio_codec: audio_track&.codec,
83
+ video_codec: video_track&.codec,
84
+ audio_bitrate: audio_track&.audio_properties&.bit_rate,
85
+ video_bitrate: video_track&.video_properties&.bit_rate,
86
+ file_size: probe_response.metadata.file_size
87
+ })
88
+ end
89
+
69
90
  # constructs an `s3:` output URL from the MediaConvert job params, the same
70
91
  # way MediaConvert will.
71
92
  #
@@ -110,27 +131,27 @@ module ActiveEncode
110
131
  end
111
132
 
112
133
  def extract_audio_codec(settings)
113
- settings.audio_descriptions.first.codec_settings.codec
114
- rescue
115
- nil
134
+ settings.audio_descriptions&.first&.codec_settings&.codec
116
135
  end
117
136
 
118
137
  def extract_audio_codec_settings(settings)
119
- codec_key = AUDIO_SETTINGS[extract_audio_codec(settings)]
138
+ codec = extract_audio_codec(settings)
139
+ return nil if codec.nil?
140
+
141
+ codec_key = AUDIO_SETTINGS[codec]
120
142
  settings.audio_descriptions.first.codec_settings[codec_key]
121
143
  end
122
144
 
123
145
  def extract_video_codec(settings)
124
- settings.video_description.codec_settings.codec
125
- rescue
126
- nil
146
+ settings.video_description&.codec_settings&.codec
127
147
  end
128
148
 
129
149
  def extract_video_codec_settings(settings)
130
- codec_key = VIDEO_SETTINGS[extract_video_codec(settings)]
150
+ codec = extract_video_codec(settings)
151
+ return nil if codec.nil?
152
+
153
+ codec_key = VIDEO_SETTINGS[codec]
131
154
  settings.video_description.codec_settings[codec_key]
132
- rescue
133
- nil
134
155
  end
135
156
 
136
157
  def extract_audio_bitrate(settings)
@@ -234,13 +234,18 @@ module ActiveEncode
234
234
  doc.remove_namespaces!
235
235
  duration = get_xpath_text(doc, '//Duration/text()', :to_f)
236
236
  duration *= 1000 unless duration.nil? # Convert to milliseconds
237
+ audio_codec = get_xpath_text(doc, '//track[@type="Audio"]/CodecID/text()', :to_s)
238
+ if get_xpath_text(doc, '//track[@type="Audio"]/Format/text()', :to_s) == "MPEG Audio" &&
239
+ get_xpath_text(doc, '//track[@type="Audio"]/Format_Profile/text()', :to_s) == "Layer 3"
240
+ audio_codec ||= "mp3"
241
+ end
237
242
  { url: get_xpath_text(doc, '//media/@ref', :to_s),
238
243
  width: get_xpath_text(doc, '//Width/text()', :to_f),
239
244
  height: get_xpath_text(doc, '//Height/text()', :to_f),
240
245
  frame_rate: get_xpath_text(doc, '//FrameRate/text()', :to_f),
241
246
  duration: duration,
242
247
  file_size: get_xpath_text(doc, '//FileSize/text()', :to_i),
243
- audio_codec: get_xpath_text(doc, '//track[@type="Audio"]/CodecID/text()', :to_s),
248
+ audio_codec: audio_codec,
244
249
  audio_bitrate: get_xpath_text(doc, '//track[@type="Audio"]/BitRate/text()', :to_i),
245
250
  video_codec: get_xpath_text(doc, '//track[@type="Video"]/CodecID/text()', :to_s),
246
251
  video_bitrate: get_xpath_text(doc, '//track[@type="Video"]/BitRate/text()', :to_i) }
@@ -9,8 +9,6 @@ module ActiveEncode
9
9
  module EngineAdapters
10
10
  extend ActiveSupport::Autoload
11
11
 
12
- autoload :MatterhornAdapter
13
- autoload :ZencoderAdapter
14
12
  autoload :ElasticTranscoderAdapter
15
13
  autoload :TestAdapter
16
14
  autoload :FfmpegAdapter
@@ -36,5 +36,11 @@ module ActiveEncode
36
36
  send("#{field}=", metadata[field]) if metadata.key?(field)
37
37
  end
38
38
  end
39
+
40
+ def tech_metadata
41
+ [:width, :height, :frame_rate, :duration, :file_size, :checksum,
42
+ :audio_codec, :video_codec, :audio_bitrate, :video_bitrate, :subtitles,
43
+ :format, :language].index_with { |field| send(field) }
44
+ end
39
45
  end
40
46
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ActiveEncode
3
- VERSION = '1.3.0'
3
+ VERSION = '2.0.0'
4
4
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_encode
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Klein, Chris Colvard, Phuong Dinh
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-07-30 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - "~>"
39
38
  - !ruby/object:Gem::Version
40
39
  version: '2.8'
40
+ - !ruby/object:Gem::Dependency
41
+ name: retriable
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: aws-sdk-cloudwatchevents
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +113,14 @@ dependencies:
100
113
  requirements:
101
114
  - - ">="
102
115
  - !ruby/object:Gem::Version
103
- version: '0'
116
+ version: 1.157.0
104
117
  type: :development
105
118
  prerelease: false
106
119
  version_requirements: !ruby/object:Gem::Requirement
107
120
  requirements:
108
121
  - - ">="
109
122
  - !ruby/object:Gem::Version
110
- version: '0'
123
+ version: 1.157.0
111
124
  - !ruby/object:Gem::Dependency
112
125
  name: aws-sdk-s3
113
126
  requirement: !ruby/object:Gem::Requirement
@@ -255,7 +268,7 @@ dependencies:
255
268
  - !ruby/object:Gem::Version
256
269
  version: '0'
257
270
  description: This gem provides an interface to transcoding services such as Ffmpeg,
258
- Amazon Elastic Transcoder, or Zencoder.
271
+ Amazon Elastic Transcoder, or Amazon Elemental MediaConvert.
259
272
  email:
260
273
  - mbklein@gmail.com, chris.colvard@gmail.com, phuongdh@gmail.com
261
274
  executables: []
@@ -293,12 +306,10 @@ files:
293
306
  - lib/active_encode/engine_adapters/elastic_transcoder_adapter.rb
294
307
  - lib/active_encode/engine_adapters/ffmpeg_adapter.rb
295
308
  - lib/active_encode/engine_adapters/ffmpeg_adapter/cleaner.rb
296
- - lib/active_encode/engine_adapters/matterhorn_adapter.rb
297
309
  - lib/active_encode/engine_adapters/media_convert_adapter.rb
298
310
  - lib/active_encode/engine_adapters/media_convert_output.rb
299
311
  - lib/active_encode/engine_adapters/pass_through_adapter.rb
300
312
  - lib/active_encode/engine_adapters/test_adapter.rb
301
- - lib/active_encode/engine_adapters/zencoder_adapter.rb
302
313
  - lib/active_encode/errors.rb
303
314
  - lib/active_encode/filename_sanitizer.rb
304
315
  - lib/active_encode/global_id.rb
@@ -317,7 +328,6 @@ licenses:
317
328
  - Apache-2.0
318
329
  metadata:
319
330
  rubygems_mfa_required: 'true'
320
- post_install_message:
321
331
  rdoc_options: []
322
332
  require_paths:
323
333
  - lib
@@ -332,8 +342,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
332
342
  - !ruby/object:Gem::Version
333
343
  version: '0'
334
344
  requirements: []
335
- rubygems_version: 3.5.3
336
- signing_key:
345
+ rubygems_version: 3.6.9
337
346
  specification_version: 4
338
347
  summary: Declare encode job classes that can be run by a variety of encoding services
339
348
  test_files: []
@@ -1,300 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'rubyhorn'
3
-
4
- module ActiveEncode
5
- module EngineAdapters
6
- class MatterhornAdapter
7
- DEFAULT_ARGS = { 'flavor' => 'presenter/source' }.freeze
8
-
9
- def create(input_url, options = {})
10
- workflow_id = options[:preset] || "full"
11
- workflow_om = Rubyhorn.client.addMediaPackageWithUrl(DEFAULT_ARGS.merge('workflow' => workflow_id, 'url' => input_url, 'filename' => File.basename(input_url), 'title' => File.basename(input_url)))
12
- build_encode(get_workflow(workflow_om))
13
- end
14
-
15
- def find(id, _opts = {})
16
- build_encode(fetch_workflow(id))
17
- end
18
-
19
- def cancel(id)
20
- workflow_om = Rubyhorn.client.stop(id)
21
- build_encode(get_workflow(workflow_om))
22
- end
23
-
24
- private
25
-
26
- def fetch_workflow(id)
27
- workflow_om = begin
28
- Rubyhorn.client.instance_xml(id)
29
- rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
30
- nil
31
- end
32
-
33
- workflow_om ||= begin
34
- Rubyhorn.client.get_stopped_workflow(id)
35
- rescue
36
- nil
37
- end
38
-
39
- get_workflow(workflow_om)
40
- end
41
-
42
- def get_workflow(workflow_om)
43
- return nil if workflow_om.nil?
44
- if workflow_om.ng_xml.is_a? Nokogiri::XML::Document
45
- workflow_om.ng_xml.remove_namespaces!.root
46
- else
47
- workflow_om.ng_xml
48
- end
49
- end
50
-
51
- def build_encode(workflow)
52
- return nil if workflow.nil?
53
- input_url = convert_input(workflow)
54
- input_url = get_workflow_title(workflow) if input_url.blank?
55
- encode = ActiveEncode::Base.new(input_url, convert_options(workflow))
56
- encode.id = convert_id(workflow)
57
- encode.state = convert_state(workflow)
58
- encode.current_operations = convert_current_operations(workflow)
59
- encode.percent_complete = calculate_percent_complete(workflow)
60
- encode.created_at = convert_created_at(workflow)
61
- encode.updated_at = convert_updated_at(workflow) || encode.created_at
62
- encode.output = convert_output(workflow, encode.options)
63
- encode.errors = convert_errors(workflow)
64
-
65
- encode.input.id = "presenter/source"
66
- encode.input.state = encode.state
67
- encode.input.created_at = encode.created_at
68
- encode.input.updated_at = encode.updated_at
69
- tech_md = convert_tech_metadata(workflow)
70
- [:width, :height, :duration, :frame_rate, :checksum, :audio_codec, :video_codec,
71
- :audio_bitrate, :video_bitrate].each do |field|
72
- encode.input.send("#{field}=", tech_md[field])
73
- end
74
-
75
- encode
76
- end
77
-
78
- def convert_id(workflow)
79
- workflow.attribute('id').to_s
80
- end
81
-
82
- def get_workflow_state(workflow)
83
- workflow.attribute('state').to_s
84
- end
85
-
86
- def convert_state(workflow)
87
- case get_workflow_state(workflow)
88
- when "INSTANTIATED", "RUNNING" # Should there be a queued state?
89
- :running
90
- when "STOPPED"
91
- :cancelled
92
- when "FAILED"
93
- workflow.xpath('//operation[@state="FAILED"]').empty? ? :cancelled : :failed
94
- when "SUCCEEDED", "SKIPPED" # Should there be a errored state?
95
- :completed
96
- end
97
- end
98
-
99
- def convert_input(workflow)
100
- # Need to do anything else since this is a MH url? and this disappears when a workflow is cleaned up
101
- workflow.xpath('mediapackage/media/track[@type="presenter/source"]/url/text()').to_s.strip
102
- end
103
-
104
- def get_workflow_title(workflow)
105
- workflow.xpath('mediapackage/title/text()').to_s.strip
106
- end
107
-
108
- def convert_tech_metadata(workflow)
109
- convert_track_metadata(workflow.xpath('//track[@type="presenter/source"]').first)
110
- end
111
-
112
- def convert_output(workflow, options)
113
- outputs = []
114
- workflow.xpath('//track[@type="presenter/delivery" and tags/tag[text()="streaming"]]').each do |track|
115
- output = ActiveEncode::Output.new
116
- output.label = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
117
- output.url = track.at("url/text()").to_s
118
- if output.url.start_with? "rtmp"
119
- output.url = File.join(options[:stream_base], MatterhornRtmpUrl.parse(output.url).to_path) if options[:stream_base]
120
- end
121
- output.id = track.at("@id").to_s
122
-
123
- tech_md = convert_track_metadata(track)
124
- [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
125
- :audio_bitrate, :video_bitrate, :file_size].each do |field|
126
- output.send("#{field}=", tech_md[field])
127
- end
128
-
129
- output.state = :completed
130
- output.created_at = convert_output_created_at(track, workflow)
131
- output.updated_at = convert_output_updated_at(track, workflow)
132
-
133
- outputs << output
134
- end
135
- outputs
136
- end
137
-
138
- def convert_current_operations(workflow)
139
- current_op = workflow.xpath('//operation[@state!="INSTANTIATED"]/@description').last.to_s
140
- current_op.present? ? [current_op] : []
141
- end
142
-
143
- def convert_errors(workflow)
144
- workflow.xpath('//errors/error/text()').map(&:to_s)
145
- end
146
-
147
- def convert_created_at(workflow)
148
- created_at = workflow.xpath('mediapackage/@start').last.to_s
149
- created_at.present? ? Time.parse(created_at).utc : nil
150
- end
151
-
152
- def convert_updated_at(workflow)
153
- updated_at = workflow.xpath('//operation[@state!="INSTANTIATED"]/completed/text()').last.to_s
154
- updated_at.present? ? Time.strptime(updated_at, "%Q") : nil
155
- end
156
-
157
- def convert_output_created_at(track, workflow)
158
- quality = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
159
- created_at = workflow.xpath("//operation[@id=\"compose\"][configurations/configuration[@key=\"target-tags\" and contains(text(), \"#{quality}\")]]/started/text()").to_s
160
- created_at.present? ? Time.at(created_at.to_i / 1000.0).utc : nil
161
- end
162
-
163
- def convert_output_updated_at(track, workflow)
164
- quality = track.xpath('tags/tag[starts-with(text(),"quality")]/text()').to_s
165
- updated_at = workflow.xpath("//operation[@id=\"compose\"][configurations/configuration[@key=\"target-tags\" and contains(text(), \"#{quality}\")]]/completed/text()").to_s
166
- updated_at.present? ? Time.at(updated_at.to_i / 1000.0).utc : nil
167
- end
168
-
169
- def convert_options(workflow)
170
- options = {}
171
- options[:preset] = workflow.xpath('template/text()').to_s
172
- if workflow.xpath('//properties/property[@key="avalon.stream_base"]/text()').present?
173
- options[:stream_base] = workflow.xpath('//properties/property[@key="avalon.stream_base"]/text()').to_s
174
- end # this is avalon-felix specific
175
- options
176
- end
177
-
178
- def convert_track_metadata(track)
179
- return {} if track.nil?
180
- metadata = {}
181
- # metadata[:mime_type] = track.at("mimetype/text()").to_s if track.at('mimetype')
182
- metadata[:checksum] = track.at("checksum/text()").to_s.strip if track.at('checksum')
183
- metadata[:duration] = track.at("duration/text()").to_s.to_i if track.at('duration')
184
- if track.at('audio')
185
- metadata[:audio_codec] = track.at("audio/encoder/@type").to_s
186
- metadata[:audio_channels] = track.at("audio/channels/text()").to_s
187
- metadata[:audio_bitrate] = track.at("audio/bitrate/text()").to_s.to_f
188
- end
189
- if track.at('video')
190
- metadata[:video_codec] = track.at("video/encoder/@type").to_s
191
- metadata[:video_bitrate] = track.at("video/bitrate/text()").to_s.to_f
192
- metadata[:frame_rate] = track.at("video/framerate/text()").to_s.to_f
193
- metadata[:width] = track.at("video/resolution/text()").to_s.split('x')[0].to_i
194
- metadata[:height] = track.at("video/resolution/text()").to_s.split('x')[1].to_i
195
- end
196
- metadata
197
- end
198
-
199
- def get_media_package(workflow)
200
- mp = workflow.xpath('//mediapackage')
201
- first_node = mp.first
202
- first_node['xmlns'] = 'http://mediapackage.opencastproject.org'
203
- mp
204
- end
205
-
206
- def calculate_percent_complete(workflow)
207
- totals = {
208
- transcode: 70,
209
- distribution: 20,
210
- other: 10
211
- }
212
-
213
- completed_transcode_operations = workflow.xpath('//operation[@id="compose" and (@state="SUCCEEDED" or @state="SKIPPED")]').size
214
- total_transcode_operations = workflow.xpath('//operation[@id="compose"]').size
215
- total_transcode_operations = 1 if total_transcode_operations.zero?
216
- completed_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute") and (@state="SUCCEEDED" or @state="SKIPPED")]').size
217
- total_distribution_operations = workflow.xpath('//operation[starts-with(@id,"distribute")]').size
218
- total_distribution_operations = 1 if total_distribution_operations.zero?
219
- completed_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute")) and (@state="SUCCEEDED" or @state="SKIPPED")]').size
220
- total_other_operations = workflow.xpath('//operation[@id!="compose" and not(starts-with(@id,"distribute"))]').size
221
- total_other_operations = 1 if total_other_operations.zero?
222
-
223
- ((totals[:transcode].to_f / total_transcode_operations) * completed_transcode_operations) +
224
- ((totals[:distribution].to_f / total_distribution_operations) * completed_distribution_operations) +
225
- ((totals[:other].to_f / total_other_operations) * completed_other_operations)
226
- end
227
-
228
- def create_multiple_files(input, workflow_id)
229
- # Create empty media package xml document
230
- mp = Rubyhorn.client.createMediaPackage
231
-
232
- # Next line associates workflow title to avalon via masterfile pid
233
- title = File.basename(input.values.first)
234
- dc = Nokogiri::XML('<dublincore xmlns="http://www.opencastproject.org/xsd/1.0/dublincore/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dcterms:title>' + title + '</dcterms:title></dublincore>')
235
- mp = Rubyhorn.client.addDCCatalog('mediaPackage' => mp.to_xml, 'dublinCore' => dc.to_xml, 'flavor' => 'dublincore/episode')
236
-
237
- # Add quality levels - repeated for each supplied file url
238
- input.each_pair do |quality, url|
239
- mp = Rubyhorn.client.addTrack('mediaPackage' => mp.to_xml, 'url' => url, 'flavor' => DEFAULT_ARGS['flavor'])
240
- # Rewrite track to include quality tag
241
- # Get the empty tags element under the newly added track
242
- tags = mp.xpath('//xmlns:track/xmlns:tags[not(node())]', 'xmlns' => 'http://mediapackage.opencastproject.org').first
243
- quality_tag = Nokogiri::XML::Node.new 'tag', mp
244
- quality_tag.content = quality
245
- tags.add_child quality_tag
246
- end
247
- # Finally ingest the media package
248
- begin
249
- Rubyhorn.client.start("definitionId" => workflow_id, "mediapackage" => mp.to_xml)
250
- rescue Rubyhorn::RestClient::Exceptions::HTTPBadRequest
251
- # make this two calls...one to get the workflow definition xml and then the second to submit it along with the mediapackage to start...due to unsolved issue with some MH installs
252
- begin
253
- workflow_definition_xml = Rubyhorn.client.definition_xml(workflow_id)
254
- Rubyhorn.client.start("definition" => workflow_definition_xml, "mediapackage" => mp.to_xml)
255
- rescue Rubyhorn::RestClient::Exceptions::HTTPNotFound
256
- raise StandardError, "Unable to start workflow"
257
- end
258
- end
259
- end
260
- end
261
-
262
- class MatterhornRtmpUrl
263
- class_attribute :members
264
- self.members = %i[application prefix media_id stream_id filename extension]
265
- attr_accessor(*members)
266
- REGEX = %r{^
267
- /(?<application>.+) # application (avalon)
268
- /(?:(?<prefix>.+):)? # prefix (mp4:)
269
- (?<media_id>[^\/]+) # media_id (98285a5b-603a-4a14-acc0-20e37a3514bb)
270
- /(?<stream_id>[^\/]+) # stream_id (b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3)
271
- /(?<filename>.+?) # filename (MVI_0057)
272
- (?:\.(?<extension>.+))?$ # extension (mp4)
273
- }x.freeze
274
-
275
- # @param [MatchData] match_data
276
- def initialize(match_data)
277
- self.class.members.each do |key|
278
- send("#{key}=", match_data[key])
279
- end
280
- end
281
-
282
- def self.parse(url_string)
283
- # Example input: /avalon/mp4:98285a5b-603a-4a14-acc0-20e37a3514bb/b3d5663d-53f1-4f7d-b7be-b52fd5ca50a3/MVI_0057.mp4
284
-
285
- uri = URI.parse(url_string)
286
- match_data = REGEX.match(uri.path)
287
- MatterhornRtmpUrl.new match_data
288
- end
289
-
290
- alias _binding binding
291
- def binding
292
- _binding
293
- end
294
-
295
- def to_path
296
- File.join(media_id, stream_id, "#{filename}.#{extension || prefix}")
297
- end
298
- end
299
- end
300
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
- module ActiveEncode
3
- module EngineAdapters
4
- class ZencoderAdapter
5
- # TODO: add a stub for an input helper (supplied by an initializer) that transforms encode.input.url into a zencoder accepted url
6
- def create(input_url, _options = {})
7
- response = Zencoder::Job.create(input: input_url.to_s)
8
- build_encode(get_job_details(response.body["id"]))
9
- end
10
-
11
- def find(id, _opts = {})
12
- build_encode(get_job_details(id))
13
- end
14
-
15
- def cancel(id)
16
- response = Zencoder::Job.cancel(id)
17
- build_encode(get_job_details(id)) if response.success?
18
- end
19
-
20
- private
21
-
22
- def get_job_details(job_id)
23
- Zencoder::Job.details(job_id)
24
- end
25
-
26
- def get_job_progress(job_id)
27
- Zencoder::Job.progress(job_id)
28
- end
29
-
30
- def build_encode(job_details)
31
- return nil if job_details.nil?
32
- encode = ActiveEncode::Base.new(convert_input(job_details), convert_options(job_details))
33
- encode.id = job_details.body["job"]["id"].to_s
34
- encode.state = convert_state(get_job_state(job_details))
35
- job_progress = get_job_progress(encode.id)
36
- encode.current_operations = convert_current_operations(job_progress)
37
- encode.percent_complete = convert_percent_complete(job_progress, job_details)
38
- encode.created_at = job_details.body["job"]["created_at"]
39
- encode.updated_at = job_details.body["job"]["updated_at"]
40
- encode.errors = []
41
-
42
- encode.output = convert_output(job_details, job_progress)
43
-
44
- encode.input.id = job_details.body["job"]["input_media_file"]["id"].to_s
45
- encode.input.errors = convert_input_errors(job_details)
46
- tech_md = convert_tech_metadata(job_details.body["job"]["input_media_file"])
47
- [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
48
- :audio_bitrate, :video_bitrate, :file_size].each do |field|
49
- encode.input.send("#{field}=", tech_md[field])
50
- end
51
- encode.input.state = convert_state(job_details.body["job"]["input_media_file"]["state"])
52
- encode.input.created_at = job_details.body["job"]["input_media_file"]["created_at"]
53
- encode.input.updated_at = job_details.body["job"]["input_media_file"]["updated_at"]
54
-
55
- encode
56
- end
57
-
58
- def convert_state(state)
59
- case state
60
- when "assigning", "pending", "waiting", "processing" # Should there be a queued state?
61
- :running
62
- when "cancelled"
63
- :cancelled
64
- when "failed", "no_input"
65
- :failed
66
- when "finished"
67
- :completed
68
- end
69
- end
70
-
71
- def get_job_state(job_details)
72
- job_details.body["job"]["state"]
73
- end
74
-
75
- def convert_current_operations(job_progress)
76
- current_ops = []
77
- job_progress.body["outputs"].each { |output| current_ops << output["current_event"] unless output["current_event"].nil? }
78
- current_ops
79
- end
80
-
81
- def convert_percent_complete(job_progress, job_details)
82
- percent = job_progress.body["progress"]
83
- percent ||= 100 if convert_state(get_job_state(job_details)) == :completed
84
- percent ||= 0
85
- percent
86
- end
87
-
88
- def convert_input(job_details)
89
- job_details.body["job"]["input_media_file"]["url"]
90
- end
91
-
92
- def convert_options(_job_details)
93
- {}
94
- end
95
-
96
- def convert_output(job_details, job_progress)
97
- job_details.body["job"]["output_media_files"].collect do |o|
98
- output = ActiveEncode::Output.new
99
- output.id = o["id"].to_s
100
- output.label = o["label"]
101
- output.url = o["url"]
102
- output.errors = Array(o["error_message"])
103
-
104
- tech_md = convert_tech_metadata(o)
105
- [:width, :height, :frame_rate, :duration, :checksum, :audio_codec, :video_codec,
106
- :audio_bitrate, :video_bitrate, :file_size].each do |field|
107
- output.send("#{field}=", tech_md[field])
108
- end
109
- output_progress = job_progress.body["outputs"].find { |out_prog| out_prog["id"] = output.id }
110
- output.state = convert_state(output_progress["state"])
111
- output.created_at = o["created_at"]
112
- output.updated_at = o["updated_at"]
113
- output
114
- end
115
- end
116
-
117
- def convert_input_errors(job_details)
118
- Array(job_details.body["job"]["input_media_file"]["error_message"])
119
- end
120
-
121
- def convert_tech_metadata(media_file)
122
- return {} if media_file.nil?
123
-
124
- metadata = {}
125
- media_file.each_pair do |key, value|
126
- next if value.blank?
127
- case key
128
- when "md5_checksum"
129
- metadata[:checksum] = value
130
- when "format"
131
- metadata[:mime_type] = value
132
- when "duration_in_ms"
133
- metadata[:duration] = value
134
- when "audio_codec"
135
- metadata[:audio_codec] = value
136
- when "channels"
137
- metadata[:audio_channels] = value
138
- when "audio_bitrate_in_kbps"
139
- metadata[:audio_bitrate] = value
140
- when "video_codec"
141
- metadata[:video_codec] = value
142
- when "frame_rate"
143
- metadata[:frame_rate] = value
144
- when "video_bitrate_in_kbps"
145
- metadata[:video_bitrate] = value
146
- when "width"
147
- metadata[:width] = value
148
- when "height"
149
- metadata[:height] = value
150
- end
151
- end
152
- metadata
153
- end
154
- end
155
- end
156
- end