amagi_transcode 0.1.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +48 -0
  4. data/README.md +140 -0
  5. data/amagi_transcode.gemspec +26 -0
  6. data/bin/console +14 -0
  7. data/bin/mediainfo +0 -0
  8. data/lib/.DS_Store +0 -0
  9. data/lib/xmorph/.DS_Store +0 -0
  10. data/lib/xmorph/base.rb +261 -0
  11. data/lib/xmorph/customers/cbn/cbn/transcode.rb +51 -0
  12. data/lib/xmorph/customers/cinedigm/Cinedigm/transcode.rb +107 -0
  13. data/lib/xmorph/customers/curiosity/curiosity/transcode.rb +36 -0
  14. data/lib/xmorph/customers/dogtv/dogtv/transcode.rb +44 -0
  15. data/lib/xmorph/customers/gusto/gusto/transcode.rb +75 -0
  16. data/lib/xmorph/customers/hkitv/hkitv/transcode.rb +44 -0
  17. data/lib/xmorph/customers/hungama/Hungama/transcode.rb +36 -0
  18. data/lib/xmorph/customers/kalpnik/kalpnik/transcode.rb +40 -0
  19. data/lib/xmorph/customers/lightning/LIG/transcode.rb +36 -0
  20. data/lib/xmorph/customers/peopletv/peopletv/transcode.rb +36 -0
  21. data/lib/xmorph/customers/rewind/rewind/transcode.rb +50 -0
  22. data/lib/xmorph/customers/rooster-teeth/roosterteeth/transcode.rb +36 -0
  23. data/lib/xmorph/customers/sabatv/sabatv/transcode.rb +37 -0
  24. data/lib/xmorph/customers/scripps-cp/scpbu/transcode.rb +36 -0
  25. data/lib/xmorph/customers/shoutfactory/shoutfactory/transcode.rb +68 -0
  26. data/lib/xmorph/customers/tastemade/tastemade/transcode.rb +36 -0
  27. data/lib/xmorph/customers/tern-cp/TCP/transcode.rb +36 -0
  28. data/lib/xmorph/customers/turner-nordic/TurnerNordic/transcode.rb +117 -0
  29. data/lib/xmorph/customers/tyt/TYT/transcode.rb +52 -0
  30. data/lib/xmorph/customers/zsports/asn/transcode.rb +50 -0
  31. data/lib/xmorph/error.rb +3 -0
  32. data/lib/xmorph/util.rb +43 -0
  33. data/lib/xmorph/version.rb +3 -0
  34. data/lib/xmorph.rb +6 -0
  35. 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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in morph.gemspec
4
+ gemspec
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
@@ -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