amagi_transcode 0.1.14
Sign up to get free protection for your applications and to get access to all the features.
- 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
|