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 +4 -4
- data/.circleci/config.yml +10 -0
- data/.rubocop_todo.yml +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +1 -16
- data/README.md +9 -3
- data/active_encode.gemspec +3 -2
- data/lib/active_encode/engine_adapters/ffmpeg_adapter.rb +10 -1
- 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/technical_metadata.rb +6 -0
- data/lib/active_encode/version.rb +1 -1
- metadata +20 -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 >>
|
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,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: [](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,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:
|
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
|
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
|
@@ -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
|
39
|
-
height: output_detail_settings.video_details
|
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
|
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
|
-
|
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
|
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
|
-
|
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:
|
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) }
|
@@ -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
|
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:
|
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:
|
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:
|
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:
|
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
|
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.
|
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
|