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.
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