amagi_transcode 0.1.14
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 +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +48 -0
- data/README.md +140 -0
- data/amagi_transcode.gemspec +26 -0
- data/bin/console +14 -0
- data/bin/mediainfo +0 -0
- data/lib/.DS_Store +0 -0
- data/lib/xmorph/.DS_Store +0 -0
- data/lib/xmorph/base.rb +261 -0
- data/lib/xmorph/customers/cbn/cbn/transcode.rb +51 -0
- data/lib/xmorph/customers/cinedigm/Cinedigm/transcode.rb +107 -0
- data/lib/xmorph/customers/curiosity/curiosity/transcode.rb +36 -0
- data/lib/xmorph/customers/dogtv/dogtv/transcode.rb +44 -0
- data/lib/xmorph/customers/gusto/gusto/transcode.rb +75 -0
- data/lib/xmorph/customers/hkitv/hkitv/transcode.rb +44 -0
- data/lib/xmorph/customers/hungama/Hungama/transcode.rb +36 -0
- data/lib/xmorph/customers/kalpnik/kalpnik/transcode.rb +40 -0
- data/lib/xmorph/customers/lightning/LIG/transcode.rb +36 -0
- data/lib/xmorph/customers/peopletv/peopletv/transcode.rb +36 -0
- data/lib/xmorph/customers/rewind/rewind/transcode.rb +50 -0
- data/lib/xmorph/customers/rooster-teeth/roosterteeth/transcode.rb +36 -0
- data/lib/xmorph/customers/sabatv/sabatv/transcode.rb +37 -0
- data/lib/xmorph/customers/scripps-cp/scpbu/transcode.rb +36 -0
- data/lib/xmorph/customers/shoutfactory/shoutfactory/transcode.rb +68 -0
- data/lib/xmorph/customers/tastemade/tastemade/transcode.rb +36 -0
- data/lib/xmorph/customers/tern-cp/TCP/transcode.rb +36 -0
- data/lib/xmorph/customers/turner-nordic/TurnerNordic/transcode.rb +117 -0
- data/lib/xmorph/customers/tyt/TYT/transcode.rb +52 -0
- data/lib/xmorph/customers/zsports/asn/transcode.rb +50 -0
- data/lib/xmorph/error.rb +3 -0
- data/lib/xmorph/util.rb +43 -0
- data/lib/xmorph/version.rb +3 -0
- data/lib/xmorph.rb +6 -0
- metadata +133 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f29849e1f25ecd2da85971c1bad0c57eedf75fef968c0cf3cf1367ce3c24d8d7
|
4
|
+
data.tar.gz: a1fa79528c6e2f61b5995c04cfdd74d8c2d9f840c4dce58299cb50e453ac1796
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c38ab0488e1ebaee04d0e47303ababbe7421b53e164867cf3108d6ffc0e3c40c652639229824355b543421d7a38103f27bd81f4e30fcd141dbf5048f8e3bc02c
|
7
|
+
data.tar.gz: ceba158e5877c60d0765bde41362f4f438eb844ded9a578ab73839636050326fc48d93a86c119cafeb25b0bcae9e3e6a4cfe0d39e38675f959e96b9e5ac7f193
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
xmorph (0.1.12)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activesupport (5.2.1.1)
|
10
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
11
|
+
i18n (>= 0.7, < 2)
|
12
|
+
minitest (~> 5.1)
|
13
|
+
tzinfo (~> 1.1)
|
14
|
+
concurrent-ruby (1.1.3)
|
15
|
+
diff-lcs (1.3)
|
16
|
+
i18n (1.1.1)
|
17
|
+
concurrent-ruby (~> 1.0)
|
18
|
+
minitest (5.11.3)
|
19
|
+
rake (10.5.0)
|
20
|
+
rspec (3.8.0)
|
21
|
+
rspec-core (~> 3.8.0)
|
22
|
+
rspec-expectations (~> 3.8.0)
|
23
|
+
rspec-mocks (~> 3.8.0)
|
24
|
+
rspec-core (3.8.0)
|
25
|
+
rspec-support (~> 3.8.0)
|
26
|
+
rspec-expectations (3.8.2)
|
27
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
+
rspec-support (~> 3.8.0)
|
29
|
+
rspec-mocks (3.8.0)
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
31
|
+
rspec-support (~> 3.8.0)
|
32
|
+
rspec-support (3.8.0)
|
33
|
+
thread_safe (0.3.6)
|
34
|
+
tzinfo (1.2.5)
|
35
|
+
thread_safe (~> 0.1)
|
36
|
+
|
37
|
+
PLATFORMS
|
38
|
+
ruby
|
39
|
+
|
40
|
+
DEPENDENCIES
|
41
|
+
activesupport
|
42
|
+
bundler (~> 1.13)
|
43
|
+
rake (~> 10.0)
|
44
|
+
rspec (~> 3.8.0)
|
45
|
+
xmorph!
|
46
|
+
|
47
|
+
BUNDLED WITH
|
48
|
+
1.16.2
|
data/README.md
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# XMorph
|
2
|
+
|
3
|
+
Morphs things from one kind to another. Transcodes, in local speak, for example..
|
4
|
+
|
5
|
+
# Features!
|
6
|
+
|
7
|
+
- XMorph can get validate an asset against given set of validations (set of video and audio parameters).
|
8
|
+
- Can choose a pre-defined proifile based on asset's mediainfo, and transcode using the profiles corresponding command.
|
9
|
+
|
10
|
+
# Installation:
|
11
|
+
```sh
|
12
|
+
$ gem install xmorph
|
13
|
+
```
|
14
|
+
> For a specific version:
|
15
|
+
```sh
|
16
|
+
$ gem install xmorph -v 0.1.5
|
17
|
+
```
|
18
|
+
|
19
|
+
# Usage
|
20
|
+
|
21
|
+
Once you have written XMorph profile for a customer, test if it's working fine.
|
22
|
+
Get base class object for a given customer
|
23
|
+
```ruby
|
24
|
+
transcoder = XMorph::Base.get_transcode_template(HOST, ACCOUNT_DOMAIN, ACCOUNT_NAME, ASSET_PATH)
|
25
|
+
|
26
|
+
transcoder = XMorph::Base.get_transcode_template("cinedigm", "Cinedigm", "amagi", "/home/tejaswini/volt/cinedigm/PRAYERLINK112718CC.mxf")
|
27
|
+
```
|
28
|
+
transcoder is object for a customer class, in case XMorph is able to find corresponding customers transcoding setup, else it'll raise TranscoderError exception.
|
29
|
+
You can also pass extra param, which will return the filepath it loaded inorder to pick customer specific transcode setup.
|
30
|
+
```ruby
|
31
|
+
transcoder, loaded_filepath = XMorph::Base.get_transcode_template("cinedigm", "Cinedigm", "amagi", "/home/tejaswini/volt/cinedigm/PRAYERLINK112718CC.mxf", true)
|
32
|
+
```
|
33
|
+
Here loaded_filepath will be
|
34
|
+
```ruby
|
35
|
+
/home/tejaswini/src/xmorph/lib/xmorph/customers/cinedigm/Cinedigm/transcode.rb
|
36
|
+
```
|
37
|
+
Get mediainfo for the asset with:
|
38
|
+
```ruby
|
39
|
+
transcoder.get_asset_details
|
40
|
+
```
|
41
|
+
If XMorph is not able to get mediainfo of the asset, it will raise exception. If it was success you can access mediainfo with, which will be a hash.
|
42
|
+
```ruby
|
43
|
+
transcoder.mediainfo_output
|
44
|
+
```
|
45
|
+
Perform default validations on the asset once you get mediainfo.
|
46
|
+
```ruby
|
47
|
+
transcoder.validate_asset
|
48
|
+
```
|
49
|
+
This function will validate the asset against the requirements mentioned in video_checks and audio_checks methods for a given customer. It will either return true or raise exception incase of failure.
|
50
|
+
|
51
|
+
Once the asset is validated, try to get suitable profile for the same.
|
52
|
+
```ruby ffmpeg_command = transcoder.get_profile``` or ```profile_name, ffmpeg_cmd = transcoder.get_profile``` to get profile name as well.
|
53
|
+
|
54
|
+
The ffmpeg command come with %{IN} and %{OUT} replace them with input filepath and output filepath and pass as parameter for, ```transcoder.transcode(updated_ffmpeg_command)```
|
55
|
+
|
56
|
+
# Execution
|
57
|
+
```sh
|
58
|
+
$ cd xmorph
|
59
|
+
$ ./bin/console
|
60
|
+
$ transcoder = XMorph::Base.get_transcode_template("cinedigm", "Cinedigm", "amagi", "/home/tejaswini/volt/cbn/PRAYERLINK112718CC.mxf")
|
61
|
+
$ transcoder, loaded_filepath = XMorph::Base.get_transcode_template("cinedigm", "Cinedigm", "amagi", "/home/tejaswini/volt/cbn/PRAYERLINK112718CC.mxf", true)
|
62
|
+
$ transcoder.get_asset_details
|
63
|
+
$ transcoder.validate_asset
|
64
|
+
$ ffmpeg_cmd = transcoder.get_profile
|
65
|
+
$ profile_name, ffmpeg_cmd = transcoder.get_profile(true)
|
66
|
+
$ transcoder.transcode(ffmpeg_cmd % {IN: asset_path, OUT: tmp_file.path})
|
67
|
+
```
|
68
|
+
|
69
|
+
# Adding new XMorph profile for a customer
|
70
|
+
```sh
|
71
|
+
$ cd xmorph/lib/xmorph/customers
|
72
|
+
$ mkdir -p HOST/ACCOUNT_DOMAIN/
|
73
|
+
$ cd HOST/ACCOUNT_DOMAIN/
|
74
|
+
$ vi transcode.rb
|
75
|
+
```
|
76
|
+
> Here, HOST is customer in customer.amagi.tv and ACCOUNT_DOMAIN is account.domain
|
77
|
+
> EX: for cinedigm.amagi.tv HOST is cinedigm and account's domain is Cinedigm
|
78
|
+
> Interfaces to be implemented in transcodee.rb,
|
79
|
+
```ruby
|
80
|
+
class Transcode < XMorph::Base
|
81
|
+
#define constants for set of profiles which says what the corresponding command does, or what the input asset is
|
82
|
+
SCALE_TO_720x480_4_3 = "720x480_4_3"
|
83
|
+
SCALE_TO_720x480_16_9 = "720x480_16_9"
|
84
|
+
SCALE_TO_1920x1080_16_9 = "1920x1080_16_9"
|
85
|
+
SCALE_TO_720x480_3_2 = "720x480_3_2"
|
86
|
+
#set the profiles using the constants defined earlier
|
87
|
+
def set_profiles
|
88
|
+
self.profiles = {
|
89
|
+
SCALE_TO_720x480_4_3 => "ffmpeg -y -i %{IN} $options to perform transcoding/encoding$ %{OUT} 2>&1",
|
90
|
+
SCALE_TO_720x480_16_9 => "ffmpeg -y -i %{IN} $options to perform transcoding/encoding$ %{OUT} 2>&1",
|
91
|
+
SCALE_TO_1920x1080_16_9 => "ffmpeg -y -i %{IN} $options to perform transcoding/encoding$ %{OUT} 2>&1",
|
92
|
+
SCALE_TO_720x480_3_2 => "ffmpeg -y -i %{IN} $options to perform transcoding/encoding$ %{OUT} 2>&1",
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
#values for the sollowing validations can be Range, Array or IGNORE(upcase)
|
97
|
+
#Range - (n1..n2) - applicable if the values are numbers, validates if value x is between n1 and n2, it also includes fractions. ex 5: in (1..10) -> true and 29.97 in (25..30) -> true
|
98
|
+
#Array - [n1,n2] - validates if value x is in the given array. ex: 5 in [1,10] -> false and 1 in [1,10] -> true
|
99
|
+
#IGNORE will skip validation for the corresponding parameter. it will not even check if media has that parameter. i.e if mediainfo is not able to read parameter x for the asset, its validation will still return true.
|
100
|
+
#These methods are to be implemented in the sub class, there's no defaut method defined in the base class, exception is raised if these methods are not found.
|
101
|
+
|
102
|
+
def video_checks
|
103
|
+
{
|
104
|
+
ALLOWED_ASPECT_RATIO => ["4:3", "16:9"] || IGNORE,
|
105
|
+
ALLOWED_HEIGHT => (400..1080) || [720, 1080, 546] || IGNORE,
|
106
|
+
ALLOWED_WIDTH => (640..720) || [720, 1920] || IGNORE,
|
107
|
+
ALLOWED_FRAME_RATE => (25..30) || [25, 29.970, 24] || IGNORE,
|
108
|
+
ALLOWED_VIDEO_BIT_RATE => (8..32) || [8, 32] || IGNORE,
|
109
|
+
ALLOWED_SCAN_TYPE => ['progressive', 'interlaced'] || IGNORE,
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def audio_checks
|
114
|
+
{
|
115
|
+
PRESENCE_OF_AUDIO_TRACK => IGNORE || VALIDATE,
|
116
|
+
ALLOWED_NUMBER_OF_AUDIO_TRACKS => (1..16)|| [2, 4, 6, 8] || IGNORE,
|
117
|
+
ALLOWED_AUDIO_CODECS => ['aac', 'ac-3', 'mp2', 'mp4', 'dolby e', 'mpeg audio'] || IGNORE,
|
118
|
+
ALLOWED_AUDIO_BIT_RATE => (120..317) || [192, 317] || IGNORE,
|
119
|
+
ALLOWED_NUMBER_OF_AUDIO_CHANNELS => (1..16) || [1, 2] || IGNORE,
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
#Write summary as what are the combinations of profiles we get for this customer, and based on which parameters we choose the profile.
|
124
|
+
|
125
|
+
# have a set of conditional statements to choose a profile defined above.
|
126
|
+
def set_profile_name
|
127
|
+
self.profile_name = nil
|
128
|
+
self.error = nil
|
129
|
+
mediainfo = self.mediainfo_output
|
130
|
+
video_info = mediainfo["Video"]
|
131
|
+
audio_tracks = mediainfo["Audio"]
|
132
|
+
#Once you have videoinfo and audiotracks info, write a set of if else statements to choose a profile from the above defined
|
133
|
+
#Assign profile name to self.profile_name
|
134
|
+
#Assign any error messages you want to display on UI to self.error, make sure error messages are unambiguous
|
135
|
+
#EX: self.error = "Got unexpected width-#{width} for video with AR-#{aspect_ratio}, expected width: 640 or 720"
|
136
|
+
#Ensure there's only if..elseif statements rather than if..else
|
137
|
+
XMorph::Base.logger.debug("XMorph#set_profile_name#Cinedigm: using profile #{self.profile_name}") unless self.profile_name.nil?
|
138
|
+
return true
|
139
|
+
end
|
140
|
+
```
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'xmorph/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "amagi_transcode"
|
8
|
+
spec.version = XMorph::VERSION
|
9
|
+
spec.authors = ["tejaswini"]
|
10
|
+
spec.email = ["tejaswini@amagi.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Morphs things from one kind to another.}
|
13
|
+
spec.description = %q{Morphs things from one kind to another. Transcodes, in local speak, for example.}
|
14
|
+
spec.homepage = "https://github.com/amagimedia/xmorph"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "rspec", "~> 3.8.0"
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "activesupport"
|
26
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require_relative '../lib/xmorph'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/mediainfo
ADDED
Binary file
|
data/lib/.DS_Store
ADDED
Binary file
|
Binary file
|
data/lib/xmorph/base.rb
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
module XMorph
|
2
|
+
class Base
|
3
|
+
|
4
|
+
IGNORE = "ignore"
|
5
|
+
VALIDATE = "validate"
|
6
|
+
|
7
|
+
ALLOWED_ASPECT_RATIO = "allowed_aspect_ratio"
|
8
|
+
ALLOWED_HEIGHT = "allowed_height"
|
9
|
+
ALLOWED_WIDTH = "allowed_width"
|
10
|
+
ALLOWED_FRAME_RATE = "allowed_frame_rate"
|
11
|
+
ALLOWED_VIDEO_BIT_RATE = "allowed_video_bit_rate"
|
12
|
+
ALLOWED_SCAN_TYPE = "allowed_scan_type"
|
13
|
+
|
14
|
+
PRESENCE_OF_AUDIO_TRACK = "presence_of_audio_track"
|
15
|
+
ALLOWED_NUMBER_OF_AUDIO_TRACKS = "allowed_number_of_audio_tracks"
|
16
|
+
ALLOWED_AUDIO_CODECS = "allowed_audio_codecs"
|
17
|
+
ALLOWED_AUDIO_BIT_RATE = "allowed_audio_bit_rate"
|
18
|
+
ALLOWED_NUMBER_OF_AUDIO_CHANNELS = "allowed_number_of_audio_channels"
|
19
|
+
|
20
|
+
attr_accessor :logger, :asset_path, :mediainfo_output, :profiles, :profile_name, :error, :default_video_checks, :default_audio_checks
|
21
|
+
|
22
|
+
def self.root
|
23
|
+
File.dirname __dir__
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.logger
|
27
|
+
@@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.logger=(logger)
|
31
|
+
@@logger = logger
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(asset_path)
|
35
|
+
self.asset_path = asset_path
|
36
|
+
self.set_profiles
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.get_transcode_template(host, domain, name, asset_path, return_loaded_filepath=false)
|
40
|
+
return nil unless domain
|
41
|
+
file_path = File.join(self.root, "xmorph", "customers", host, domain, "transcode.rb")
|
42
|
+
raise TranscoderError.new("Transcoding profile doesn't exist for account, #{name}.") unless File.exist? file_path
|
43
|
+
XMorph::Base.logger.debug("XMorph#get_transcode_template: loading file #{file_path} to transcode #{asset_path}")
|
44
|
+
load file_path
|
45
|
+
return Transcode.new(asset_path), file_path if return_loaded_filepath
|
46
|
+
return Transcode.new(asset_path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_asset_details
|
50
|
+
mediainfo_command = "#{File.dirname(self.class.root)}/bin/mediainfo --output=XML #{self.asset_path}"
|
51
|
+
success, response = Util.run_cmd_with_response(mediainfo_command)
|
52
|
+
raise TranscoderError.new("Failed to get mediainfo for the asset.") unless success
|
53
|
+
begin
|
54
|
+
self.mediainfo_output = Util.mediainfo_xml_to_hash(response)[1]
|
55
|
+
rescue => e
|
56
|
+
raise TranscoderError.new("Failed to get mediainfo for the asset.")
|
57
|
+
end
|
58
|
+
set_validations
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_validations
|
62
|
+
begin
|
63
|
+
self.default_video_checks = self.video_checks
|
64
|
+
rescue => e
|
65
|
+
raise TranscoderError.new("Default video validation requirements are not defined.")
|
66
|
+
end
|
67
|
+
begin
|
68
|
+
self.default_audio_checks = self.audio_checks
|
69
|
+
rescue => e
|
70
|
+
raise TranscoderError.new("Default audio validation requirements are not defined.")
|
71
|
+
end
|
72
|
+
return true
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_asset
|
76
|
+
raise TranscoderError.new(self.error || "Failed to perform default video validations.") unless perform_default_video_validations
|
77
|
+
raise TranscoderError.new(self.error || "Failed to perform default audio validations.") unless perform_default_audio_validations
|
78
|
+
return true
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_profile(return_profile_name=false)
|
82
|
+
self.set_profile_name
|
83
|
+
if (self.profile_name.blank? or self.profiles[self.profile_name].blank?)
|
84
|
+
raise TranscoderError.new(self.error || "Media doesnt match any transcoding profiles.")
|
85
|
+
end
|
86
|
+
XMorph::Base.logger.debug("XMorph#get_profile: Using ffmpeg command: #{self.profiles[self.profile_name]} to transcode #{asset_path}")
|
87
|
+
return self.profile_name, self.profiles[self.profile_name] if return_profile_name
|
88
|
+
return self.profiles[self.profile_name]
|
89
|
+
end
|
90
|
+
|
91
|
+
def transcode(cmd)
|
92
|
+
raise TranscoderError.new("Command passed to transcode is empty.") unless cmd
|
93
|
+
status, response = Util.run_cmd_with_response(cmd)
|
94
|
+
raise TranscoderError.new(self.error || response.split("\n").last) unless status
|
95
|
+
return status
|
96
|
+
end
|
97
|
+
|
98
|
+
def post_process
|
99
|
+
#do some post processing
|
100
|
+
end
|
101
|
+
|
102
|
+
#These are the default validations performed on assets
|
103
|
+
#override in sub class with new configs
|
104
|
+
#allowed values: Range, Array, IGNORE(presnce of that config will not be checked)
|
105
|
+
#def video_checks
|
106
|
+
# {
|
107
|
+
# ALLOWED_ASPECT_RATIO => ["4:3", "16:9"] || IGNORE,
|
108
|
+
# ALLOWED_HEIGHT => (400..1080) || [720, 1080, 546] || IGNORE,
|
109
|
+
# ALLOWED_WIDTH => (640..720) || [720, 1920] || IGNORE,
|
110
|
+
# ALLOWED_FRAME_RATE => (25..30) || [25, 29.970, 24] || IGNORE,
|
111
|
+
# ALLOWED_VIDEO_BIT_RATE => (8..32) || [8, 32] || IGNORE,
|
112
|
+
# ALLOWED_SCAN_TYPE => ['progressive', 'interlaced', 'interlaced_mbaff'] || IGNORE,
|
113
|
+
# }
|
114
|
+
#end
|
115
|
+
|
116
|
+
#def audio_checks
|
117
|
+
# {
|
118
|
+
# PRESENCE_OF_AUDIO_TRACK => IGNORE || VALIDATE,
|
119
|
+
# ALLOWED_NUMBER_OF_AUDIO_TRACKS => (1..16)|| [2, 4, 6, 8] || IGNORE,
|
120
|
+
# ALLOWED_AUDIO_CODECS => ['aac', 'ac3', 'mp2', 'mp4'] || IGNORE,
|
121
|
+
# ALLOWED_AUDIO_BIT_RATE => (120..317) || [192, 317] || IGNORE,
|
122
|
+
# ALLOWED_NUMBER_OF_AUDIO_CHANNELS => (1..16) || [1, 2] || IGNORE,
|
123
|
+
# }
|
124
|
+
#end
|
125
|
+
|
126
|
+
def perform_default_video_validations
|
127
|
+
mediainfo = self.mediainfo_output
|
128
|
+
video_info = mediainfo["Video"]
|
129
|
+
default_checks = self.default_video_checks
|
130
|
+
|
131
|
+
aspect_ratio = video_info["Display_aspect_ratio"]
|
132
|
+
height = (video_info["Original_height"] || video_info["Height"])
|
133
|
+
width = (video_info["Original_width"] || video_info["Width"])
|
134
|
+
frame_rate = video_info["Frame_rate"]
|
135
|
+
bit_rate = video_info["Bit_rate"] || video_info["Nominal_bit_rate"]
|
136
|
+
scan_type = video_info["Scan_type"]
|
137
|
+
|
138
|
+
errors = []
|
139
|
+
missing = []
|
140
|
+
if default_checks[ALLOWED_ASPECT_RATIO] != IGNORE
|
141
|
+
if aspect_ratio.present?
|
142
|
+
errors << "Unexpected Aspect ratio #{aspect_ratio}. We support #{default_checks[ALLOWED_ASPECT_RATIO]}" unless default_checks[ALLOWED_ASPECT_RATIO].include? aspect_ratio
|
143
|
+
else
|
144
|
+
missing << "Aspect ratio"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
if default_checks[ALLOWED_HEIGHT] != IGNORE
|
148
|
+
if height.present?
|
149
|
+
height = height.split("pixels")[0].gsub(/ /,"").to_i
|
150
|
+
errors << "Unexpected Height #{height}. We support #{default_checks[ALLOWED_HEIGHT]}" unless default_checks[ALLOWED_HEIGHT].include? height
|
151
|
+
else
|
152
|
+
missing << "Height"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
if default_checks[ALLOWED_WIDTH] != IGNORE
|
156
|
+
if width.present?
|
157
|
+
width = width.split("pixels")[0].gsub(/ /,"").to_i
|
158
|
+
errors << "Unexpected Width #{width}. We support #{default_checks[ALLOWED_WIDTH]}" unless default_checks[ALLOWED_WIDTH].include? width
|
159
|
+
else
|
160
|
+
missing << "Width"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
if default_checks[ALLOWED_FRAME_RATE] != IGNORE
|
164
|
+
if frame_rate.present?
|
165
|
+
frame_rate = frame_rate.split(" ")[0].to_f
|
166
|
+
errors << "Unexpected Frame rate #{frame_rate}. We support #{default_checks[ALLOWED_FRAME_RATE]}" unless default_checks[ALLOWED_FRAME_RATE].include? frame_rate
|
167
|
+
else
|
168
|
+
missing << "Frame rate"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
if default_checks[ALLOWED_VIDEO_BIT_RATE] != IGNORE
|
172
|
+
if bit_rate.present?
|
173
|
+
bit_rate = bit_rate.split(" ")[0].to_f
|
174
|
+
errors << "Unexpected Bit rate #{bit_rate}. We support #{default_checks[ALLOWED_VIDEO_BIT_RATE]}" unless default_checks[ALLOWED_VIDEO_BIT_RATE].include? bit_rate
|
175
|
+
else
|
176
|
+
missing << "Bit rate"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
if default_checks[ALLOWED_SCAN_TYPE] != IGNORE
|
180
|
+
if scan_type.present?
|
181
|
+
errors << "Unexpected Scan type #{scan_type}. We support #{default_checks[ALLOWED_SCAN_TYPE]}" unless default_checks[ALLOWED_SCAN_TYPE].include? scan_type.downcase
|
182
|
+
else
|
183
|
+
missing << "Scan type"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
unless missing.empty?
|
188
|
+
self.error = "Couldn't find #{missing.join(",")} of the Video correctly"
|
189
|
+
return false
|
190
|
+
end
|
191
|
+
unless errors.empty?
|
192
|
+
self.error = errors.join("\n")
|
193
|
+
return false
|
194
|
+
end
|
195
|
+
return true
|
196
|
+
end
|
197
|
+
|
198
|
+
def perform_default_audio_validations
|
199
|
+
mediainfo = self.mediainfo_output
|
200
|
+
audio_tracks = mediainfo["Audio"]
|
201
|
+
default_checks = self.default_audio_checks
|
202
|
+
|
203
|
+
number_of_audio_tracks = audio_tracks.count
|
204
|
+
if number_of_audio_tracks > 0
|
205
|
+
audio_codecs = []; bit_rate = []; number_of_audio_channels = []
|
206
|
+
audio_tracks.each do |a|
|
207
|
+
audio_codecs << a["Format"]
|
208
|
+
bit_rate << (a["Bit_rate"] || a["Nominal_bit_rate"]).split(" ")[0].to_f if (a["Bit_rate"] || a["Nominal_bit_rate"]).present?
|
209
|
+
number_of_audio_channels << a["Channel_s_"].split(" ")[0].to_i if a["Channel_s_"].present?
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
if (default_checks[PRESENCE_OF_AUDIO_TRACK] != IGNORE) and (number_of_audio_tracks <= 0)
|
214
|
+
self.error = "No audio tracks found"
|
215
|
+
return false
|
216
|
+
end
|
217
|
+
|
218
|
+
errors = []
|
219
|
+
missing = []
|
220
|
+
if number_of_audio_tracks > 0
|
221
|
+
if default_checks[ALLOWED_NUMBER_OF_AUDIO_TRACKS] != IGNORE
|
222
|
+
errors << "Unexpected number of audio tracks #{number_of_audio_tracks}. We support #{default_checks[ALLOWED_NUMBER_OF_AUDIO_TRACKS]} tracks" unless default_checks[ALLOWED_NUMBER_OF_AUDIO_TRACKS].include? number_of_audio_tracks
|
223
|
+
end
|
224
|
+
if default_checks[ALLOWED_AUDIO_BIT_RATE] != IGNORE
|
225
|
+
if bit_rate.empty? || (bit_rate.uniq.include? nil)
|
226
|
+
missing << "Bit rate"
|
227
|
+
else
|
228
|
+
unsupported_bit_rate = bit_rate.map{|b| b unless default_checks[ALLOWED_AUDIO_BIT_RATE].include? b}
|
229
|
+
errors << "Unexpected Bit rate #{unsupported_bit_rate}. We support #{default_checks[ALLOWED_AUDIO_BIT_RATE]}" unless unsupported_bit_rate.empty?
|
230
|
+
end
|
231
|
+
end
|
232
|
+
if default_checks[ALLOWED_AUDIO_CODECS] != IGNORE
|
233
|
+
if audio_codecs.empty? || (audio_codecs.uniq.include? nil)
|
234
|
+
missing << "audio codecs"
|
235
|
+
else
|
236
|
+
audio_codecs = audio_codecs.map{|a| a.downcase}
|
237
|
+
unsupported_audio_codecs = audio_codecs - default_checks[ALLOWED_AUDIO_CODECS].to_a
|
238
|
+
errors << "Unexpected audio codecs #{unsupported_audio_codecs}. We support #{default_checks[ALLOWED_AUDIO_CODECS]}" unless unsupported_audio_codecs.empty?
|
239
|
+
end
|
240
|
+
end
|
241
|
+
if default_checks[ALLOWED_NUMBER_OF_AUDIO_CHANNELS] != IGNORE
|
242
|
+
if number_of_audio_channels.empty? || (number_of_audio_channels.uniq.include? nil)
|
243
|
+
missing << "number of audio channels"
|
244
|
+
else
|
245
|
+
unsupported_number_of_audio_channels = number_of_audio_channels - default_checks[ALLOWED_NUMBER_OF_AUDIO_CHANNELS].to_a
|
246
|
+
errors << "Unexpected number of audio channels #{unsupported_number_of_audio_channels}. We support #{default_checks[ALLOWED_NUMBER_OF_AUDIO_CHANNELS]} channels" unless unsupported_number_of_audio_channels.empty?
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
unless missing.empty?
|
251
|
+
self.error = "Couldn't find #{missing.join(",")} of the Video correctly"
|
252
|
+
return false
|
253
|
+
end
|
254
|
+
unless errors.empty?
|
255
|
+
self.error = errors.join("\n")
|
256
|
+
return false
|
257
|
+
end
|
258
|
+
return true
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class Transcode < XMorph::Base
|
2
|
+
|
3
|
+
DEFAULT_COMMAND = "DEFAULT_COMMAND"
|
4
|
+
|
5
|
+
def set_profiles
|
6
|
+
self.profiles = {
|
7
|
+
DEFAULT_COMMAND => "docker run --rm -v %{IP_MOUNT_PATH}:%{IP_MOUNT_PATH} -v %{OP_MOUNT_PATH}:%{OP_MOUNT_PATH} %{VOLT_TAG} ./volt/transcoder -if %{IN} -of %{OUT} -vr 1080i60 -acm \"1,2;1,2\" -ac \"ac3;aac\" -ac3_dialnorm -24 -lt -25 -lm DOLBY",
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def video_checks
|
12
|
+
{
|
13
|
+
ALLOWED_ASPECT_RATIO => ["16:9"],
|
14
|
+
ALLOWED_HEIGHT => [1080],
|
15
|
+
ALLOWED_WIDTH => [1440, 1920],
|
16
|
+
ALLOWED_FRAME_RATE => [29.97],
|
17
|
+
ALLOWED_VIDEO_BIT_RATE => IGNORE, #Mbps
|
18
|
+
ALLOWED_SCAN_TYPE => ["interlaced"], #all downcased
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def audio_checks
|
23
|
+
{
|
24
|
+
PRESENCE_OF_AUDIO_TRACK => VALIDATE,
|
25
|
+
ALLOWED_NUMBER_OF_AUDIO_TRACKS => [1, 2],
|
26
|
+
ALLOWED_AUDIO_CODECS => ["pcm"],
|
27
|
+
ALLOWED_AUDIO_BIT_RATE => IGNORE, #kbps
|
28
|
+
ALLOWED_NUMBER_OF_AUDIO_CHANNELS => [1, 2],
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_profile_name
|
33
|
+
self.profile_name = nil
|
34
|
+
self.error = nil
|
35
|
+
|
36
|
+
audio_tracks = self.mediainfo_output["Audio"]
|
37
|
+
sample_rate = audio_tracks.map{|a| a["Sampling_rate"]}
|
38
|
+
self.error = "Unexpected audio sample rate #{sample_rate.uniq}. We support 48.0 KHz" and return if sample_rate.uniq != ["48.0 KHz"]
|
39
|
+
|
40
|
+
number_of_audio_tracks = audio_tracks.count
|
41
|
+
number_of_audio_channels = audio_tracks.map{|a| a["Channel_s_"].split(" ")[0].to_i if a["Channel_s_"].present? }
|
42
|
+
|
43
|
+
if (number_of_audio_tracks == 2 and number_of_audio_channels.uniq == [1]) or (number_of_audio_tracks == 1 and number_of_audio_channels.uniq == [2])
|
44
|
+
self.profile_name = DEFAULT_COMMAND
|
45
|
+
return true
|
46
|
+
end
|
47
|
+
|
48
|
+
XMorph::Base.logger.debug("XMorph#set_profile_name#cbn: using profile #{self.profile_name}") unless self.profile_name.nil?
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class Transcode < XMorph::Base
|
2
|
+
|
3
|
+
SCALE_TO_720x480_4_3 = "720x480_4_3"
|
4
|
+
SCALE_TO_720x480_16_9 = "720x480_16_9"
|
5
|
+
SCALE_TO_1920x1080_16_9 = "1920x1080_16_9"
|
6
|
+
SCALE_TO_720x480_3_2 = "720x480_3_2"
|
7
|
+
|
8
|
+
def set_profiles
|
9
|
+
self.profiles = {
|
10
|
+
SCALE_TO_720x480_4_3 => "ffmpeg -y -i %{IN} -map 0:v:0 -map 0:a:0 -acodec libfdk_aac -profile:a aac_low -ac 2 -ar 48000 -ab 192k -vcodec libx264 -vf \"fps=fps=29.970000,scale=720x480,setdar=dar=4/3\" -pix_fmt yuv420p -g 13 -bf 2 -profile:v high -vb 12000000 -minrate:v 12000k -maxrate:v 12000k -bufsize:v 24000k -muxrate 13000k -x264opts nal-hrd=cbr -pes_payload_size 16 -streamid 0:2064 -streamid 1:2068 -vsync 1 -async 1 %{OUT} 2>&1",
|
11
|
+
SCALE_TO_720x480_16_9 => "ffmpeg -y -i %{IN} -map 0:v:0 -map 0:a:0 -acodec libfdk_aac -profile:a aac_low -ac 2 -ar 48000 -ab 192k -vcodec libx264 -vf \"fps=fps=29.970000,scale=720x480,setdar=dar=16/9\" -pix_fmt yuv420p -g 13 -bf 2 -profile:v high -vb 12000000 -minrate:v 12000k -maxrate:v 12000k -bufsize:v 24000k -muxrate 13000k -x264opts nal-hrd=cbr -pes_payload_size 16 -streamid 0:2064 -streamid 1:2068 -vsync 1 -async 1 %{OUT} 2>&1",
|
12
|
+
SCALE_TO_1920x1080_16_9 => "ffmpeg -y -i %{IN} -map 0:v:0 -map 0:a:0 -acodec libfdk_aac -profile:a aac_low -ac 2 -ar 48000 -ab 192k -vcodec libx264 -vf \"fps=fps=29.970000,scale=1920x1080,setdar=dar=16/9\" -pix_fmt yuv420p -g 13 -bf 2 -profile:v high -vb 12000000 -minrate:v 12000k -maxrate:v 12000k -bufsize:v 24000k -muxrate 13000k -x264opts nal-hrd=cbr -pes_payload_size 16 -streamid 0:2064 -streamid 1:2068 -vsync 1 -async 1 %{OUT} 2>&1",
|
13
|
+
SCALE_TO_720x480_3_2 => "ffmpeg -y -i %{IN} -map 0:v:0 -map 0:a:0 -acodec libfdk_aac -profile:a aac_low -ac 2 -ar 48000 -ab 192k -vcodec libx264 -vf \"fps=fps=29.970000,scale=720x480,setdar=dar=4/3\" -pix_fmt yuv420p -g 13 -bf 2 -profile:v high -vb 12000000 -minrate:v 12000k -maxrate:v 12000k -bufsize:v 24000k -muxrate 13000k -x264opts nal-hrd=cbr -pes_payload_size 16 -streamid 0:2064 -streamid 1:2068 -vsync 1 -async 1 %{OUT} 2>&1",
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def video_checks
|
18
|
+
{
|
19
|
+
ALLOWED_ASPECT_RATIO => ["4:3", "16:9", "16:10", "3:2"],
|
20
|
+
ALLOWED_HEIGHT => (400..1080),
|
21
|
+
ALLOWED_WIDTH => (640..1920),
|
22
|
+
ALLOWED_FRAME_RATE => IGNORE,
|
23
|
+
ALLOWED_VIDEO_BIT_RATE => IGNORE, #Mbps
|
24
|
+
ALLOWED_SCAN_TYPE => IGNORE, #all downcased
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def audio_checks
|
29
|
+
{
|
30
|
+
PRESENCE_OF_AUDIO_TRACK => VALIDATE,
|
31
|
+
ALLOWED_NUMBER_OF_AUDIO_TRACKS => [1, 2],
|
32
|
+
ALLOWED_AUDIO_CODECS => ["pcm", "aac"],
|
33
|
+
ALLOWED_AUDIO_BIT_RATE => IGNORE, #kbps
|
34
|
+
ALLOWED_NUMBER_OF_AUDIO_CHANNELS => [2, 6],
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Classification is based on Height, Width and aspect ratio
|
39
|
+
# 4:3 Aspect Ratio & SD (720 * (xyz)) & SD (640 * 480): scaled to 720x480 wih 4:3
|
40
|
+
# 16:9 Aspect Ratio & SD (720 * (xyz)): scaled to 720x480 wih 16:9
|
41
|
+
# 16:9 Aspect Ratio & HD ( 1080p & 720p ): scaled to 1920x1080 wih 16:9
|
42
|
+
# 3:2 Aspect Ratio and (720 * (xyz)): scaled to 720x480 with 4:3
|
43
|
+
def set_profile_name
|
44
|
+
self.profile_name = nil
|
45
|
+
self.error = nil
|
46
|
+
|
47
|
+
mediainfo = self.mediainfo_output
|
48
|
+
video_info = mediainfo["Video"]
|
49
|
+
aspect_ratio = video_info["Display_aspect_ratio"]
|
50
|
+
height = (video_info["Original_height"] || video_info["Height"]).split("pixels")[0].gsub(/ /,"").to_i
|
51
|
+
width = (video_info["Original_width"] || video_info["Width"]).split("pixels")[0].gsub(/ /,"").to_i
|
52
|
+
|
53
|
+
if aspect_ratio == "4:3"
|
54
|
+
if width == 640
|
55
|
+
if height == 480
|
56
|
+
self.profile_name = SCALE_TO_720x480_4_3
|
57
|
+
else
|
58
|
+
self.error = "Got unexpected height #{height} for video with width-#{width} and AR-#{aspect_ratio}, expected height: 480"
|
59
|
+
end
|
60
|
+
elsif width == 720
|
61
|
+
if (400..600).include? height
|
62
|
+
self.profile_name = SCALE_TO_720x480_4_3
|
63
|
+
else
|
64
|
+
self.error = "Got unexpected height #{height} for video with width-#{width} and AR-#{aspect_ratio}, expected height to be 400-600"
|
65
|
+
end
|
66
|
+
else
|
67
|
+
self.error = "Got unexpected width-#{width} for video with AR-#{aspect_ratio}, expected width: 640 or 720"
|
68
|
+
end
|
69
|
+
elsif ["16:9", "16:10"].include? aspect_ratio
|
70
|
+
if width == 720 and aspect_ratio == "16:9"
|
71
|
+
if (400..600).include? height
|
72
|
+
self.profile_name = SCALE_TO_720x480_16_9
|
73
|
+
else
|
74
|
+
self.error = "Got unexpected height #{height} for video with width-#{width} and AR-#{aspect_ratio}, expected height to be 400-600"
|
75
|
+
end
|
76
|
+
elsif (width == 1920 and height == 1080) or (width == 1280 and height == 720)
|
77
|
+
self.profile_name = SCALE_TO_1920x1080_16_9
|
78
|
+
elsif [854].include? width and aspect_ratio == "16:9"
|
79
|
+
if [480].include? height
|
80
|
+
self.profile_name = SCALE_TO_720x480_16_9
|
81
|
+
else
|
82
|
+
self.error = "Got unexpected height #{height} for video with width-#{width} and AR-#{aspect_ratio}, expected height to be 480"
|
83
|
+
end
|
84
|
+
elsif [688].include? width and aspect_ratio == "16:10"
|
85
|
+
if [440].include? height
|
86
|
+
self.profile_name = SCALE_TO_720x480_16_9
|
87
|
+
else
|
88
|
+
self.error = "Got unexpected height #{height} for video with width-#{width} and AR-#{aspect_ratio}, expected height to be 480"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
self.error = "Got unexpected width #{width} and height #{height} for video with AR-#{aspect_ratio}, expected : 720x(*) or 1920x1080 or 1280x720"
|
92
|
+
end
|
93
|
+
elsif aspect_ratio == "3:2"
|
94
|
+
if width == 720
|
95
|
+
if (400..600).include? height
|
96
|
+
self.profile_name = SCALE_TO_720x480_3_2
|
97
|
+
else
|
98
|
+
self.error = "Got unexpected height #{height} for video with width-#{width} and AR-#{aspect_ratio}, expected height to be 400-600"
|
99
|
+
end
|
100
|
+
else
|
101
|
+
self.error = "Got unexpected width-#{width} for video with AR-#{aspect_ratio}, expected width: 720"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
XMorph::Base.logger.debug("XMorph#set_profile_name#Cinedigm: using profile #{self.profile_name}") unless self.profile_name.nil?
|
105
|
+
return true
|
106
|
+
end
|
107
|
+
end
|