Posted By

felipec on 02/28/08


Tagged

multimedia


Versions (?)

Who likes this?

3 people have marked this snippet as a favorite

wbowers
sergeizen
webstic


maemo transcoder


 / Published in: Ruby
 

  1. #!/usr/bin/env ruby
  2.  
  3. =begin
  4.  
  5. Copyright (C) 2007 Nokia Corporation. All rights reserved.
  6.  
  7. Contact: Felipe Contreras <[email protected]>
  8.  
  9. Permission is hereby granted, free of charge, to any person obtaining a copy of
  10. this software and associated documentation files (the "Software"), to deal in
  11. the Software without restriction, including without limitation the rights to
  12. use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  13. of the Software, and to permit persons to whom the Software is furnished to do
  14. so, subject to the following conditions:
  15.  
  16. The above copyright notice and this permission notice shall be included in all
  17. copies or substantial portions of the Software.
  18.  
  19. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  25. SOFTWARE.
  26.  
  27. =end
  28.  
  29. =begin
  30.  
  31. This script tries to intelligently find the best video frame size that would
  32. look good in the desired device (N770 or N800) while trying to keep the same
  33. aspect ratio as the original clip, as well as trying to fit the aspect ratio of
  34. the device.
  35.  
  36. It will try to change the framerate only to fit the capabilities of the device.
  37.  
  38. =end
  39.  
  40. require 'optparse'
  41.  
  42. class AppException < RuntimeError
  43. end
  44.  
  45. module Video
  46. class Aspect
  47. attr_reader :numerator, :denominator
  48.  
  49. def initialize(num, den)
  50. def greatest_common_divisor(a, b)
  51. while a % b != 0
  52. a, b = b.round, (a % b).round
  53. end
  54. return b
  55. end
  56.  
  57. gcd = greatest_common_divisor(num, den)
  58.  
  59. @numerator = num / gcd
  60. @denominator = den / gcd
  61. end
  62.  
  63. def to_s()
  64. return "%d:%d" % [numerator, denominator]
  65. end
  66.  
  67. def to_f()
  68. return @numerator.to_f / @denominator.to_f
  69. end
  70.  
  71. def /(b)
  72. self.to_f / b.to_f
  73. end
  74. end
  75.  
  76. class FrameSize
  77. attr_reader :width, :height
  78. attr_writer :width, :height
  79.  
  80. def initialize(width, height)
  81. @width = width
  82. @height = height
  83. end
  84.  
  85. def ==(size)
  86. return false if size.width != @width
  87. return false if size.height != @height
  88. return true
  89. end
  90.  
  91. def to_s()
  92. return "%dx%d" % [@width, @height]
  93. end
  94.  
  95. def aspect()
  96. return Aspect.new(@width, @height)
  97. end
  98.  
  99. def /(b)
  100. self.width.to_f / b.width.to_f
  101. end
  102. end
  103.  
  104. class Clip
  105. attr_reader :file_name, :size, :framerate, :bitrate
  106. attr_writer :file_name, :size, :framerate, :bitrate
  107.  
  108. def initialize(file)
  109. @file_name = file
  110. end
  111.  
  112. def to_s()
  113. return "%s [%s], %s fps, %s kbps" % [@size.to_s, @size.aspect.to_s, @framerate, @bitrate]
  114. end
  115. end
  116. end
  117.  
  118. module Device
  119. class Base
  120. attr_reader :screen_size, :basic_sizes, :macroblocks_per_second, :max_framerate
  121.  
  122. def initialize()
  123. @basic_sizes = []
  124. end
  125. end
  126.  
  127. class N770 < Base
  128. def initialize()
  129. super()
  130. @screen_size = Video::FrameSize.new(800, 480)
  131. @macroblocks_per_second = 22 * 18 * 15 # 352x288x15
  132. @max_framerate = 30
  133.  
  134. @basic_sizes << Video::FrameSize.new(240, 144)
  135. @basic_sizes << Video::FrameSize.new(352, 208)
  136. @basic_sizes << Video::FrameSize.new(352, 288)
  137. @basic_sizes << Video::FrameSize.new(176, 144)
  138.  
  139. @basic_sizes << Video::FrameSize.new(320, 240)
  140. end
  141. end
  142.  
  143. class N800 < N770
  144. def initialize()
  145. super()
  146. @screen_size = Video::FrameSize.new(800, 480)
  147. @macroblocks_per_second = 40 * 30 * 15 # 640x480x15
  148. @max_framerate = 30
  149.  
  150. @basic_sizes << Video::FrameSize.new(400, 240)
  151. @basic_sizes << Video::FrameSize.new(640, 480)
  152. end
  153. end
  154. end
  155.  
  156. module Transcoder
  157. class Base
  158. def initialize(input, output, device, bitrate)
  159. @input = input
  160. @output = output
  161. @device = device
  162.  
  163. output.bitrate = bitrate
  164. end
  165.  
  166. def run()
  167. raise "Run not implemented"
  168. end
  169. end
  170.  
  171. class Smart < Base
  172. class Evaluator
  173. class Variable
  174. def initialize(element, weight)
  175. @element = element
  176. @weight = weight
  177. end
  178.  
  179. def get(size)
  180. case @element
  181. when Video::FrameSize
  182. r = compare(@element, size)
  183. when Video::Aspect
  184. r = compare(@element, size.aspect)
  185. end
  186.  
  187. return r * @weight
  188. end
  189.  
  190. private
  191.  
  192. # Returns the amount of similarity from 0 to 1
  193. def compare(a, b)
  194. return ((1.0 / 100) ** (Math.log(a / b) ** 2))
  195. end
  196. end
  197.  
  198. attr_writer :framerate, :max_mbps
  199.  
  200. def initialize()
  201. @variables = []
  202. end
  203.  
  204. def add(element, weight)
  205. @variables << Variable.new(element, weight)
  206. end
  207.  
  208. def execute(size)
  209. value = @variables.inject(0) {|sum, n| sum + n.get(size)}
  210.  
  211. # We don't want a barely playable video
  212. mbps = (size.width / 16) * (size.height / 16) * @framerate
  213.  
  214. if mbps >= @max_mbps
  215. # print "Out of range\n"
  216. value /= 2
  217. end
  218.  
  219. # print "%3dx%3d: %f\n" % [size.width, size.height, value]
  220.  
  221. return value
  222. end
  223. end
  224.  
  225. def calculate()
  226. def nearest(num, mul)
  227. return (0.5 + num / mul).to_i * mul;
  228. end
  229.  
  230. max_value = nil
  231. new_size = nil
  232. new_framerate = @input.framerate
  233.  
  234. new_framerate /= 2 while new_framerate > @device.max_framerate
  235.  
  236. evaluator = Evaluator.new()
  237. evaluator.framerate = new_framerate
  238. evaluator.max_mbps = @device.macroblocks_per_second
  239.  
  240. # How similar to the original frame size?
  241. evaluator.add(@input.size, 50)
  242.  
  243. # How similar to the original aspect ratio?
  244. evaluator.add(@input.size.aspect, 100)
  245.  
  246. # How similar to the screen's aspect ratio?
  247. evaluator.add(@input.size.aspect, 75)
  248.  
  249. @device.basic_sizes.each do |size|
  250. # Evaluate this frame size
  251. value = evaluator.execute(size)
  252.  
  253. # Is this frame size the best or not?
  254. if not max_value or value > max_value
  255. new_size = size
  256. max_value = value
  257. end
  258. end
  259.  
  260. if @input.size.aspect / new_size.aspect > 1.2
  261. # Change height keep the aspect ratio
  262. new_size.height = nearest(new_size.width * (1 / @input.size.aspect.to_f), 16)
  263. end
  264.  
  265. # p @input.size.aspect / new_size.aspect
  266.  
  267. @output.framerate = new_framerate
  268. @output.size = new_size.clone()
  269. end
  270. end
  271.  
  272. class MEncoder < Smart
  273. def analyze()
  274. info_map = {}
  275. cmd = "mplayer -identify -quiet -frames 0 -vc null -vo null -ao null \"%s\"" % [@input.file_name]
  276. info_raw = %x[#{cmd} 2> /dev/null | grep "^ID_"]
  277. raise AppException, "Bad input file: \"%s\"" % [@input.file_name] if info_raw == ""
  278.  
  279. info_array = info_raw.map { |i| i.chomp().split("=")}
  280. info_array.each { |e| info_map[e[0]] = e[1] }
  281.  
  282. width = info_map["ID_VIDEO_WIDTH"].to_i
  283. height = info_map["ID_VIDEO_HEIGHT"].to_i
  284. @input.framerate = info_map["ID_VIDEO_FPS"].to_f
  285. @input.bitrate = info_map["ID_VIDEO_BITRATE"].to_i
  286.  
  287. @input.size = Video::FrameSize.new(width, height)
  288. end
  289.  
  290. def generate()
  291. messages = []
  292.  
  293. audio_options = []
  294. audio_options << "-srate 44100"
  295. audio_options << "-oac mp3lame"
  296. audio_options << "-lameopts vbr=0:br=128"
  297. audio_options << "-af volnorm"
  298.  
  299. video_options = []
  300. case $options[:output_format]
  301. when "mpeg4"
  302. video_options << "-ovc lavc"
  303. video_options << "-lavcopts vcodec=mpeg4:vbitrate=%d" % [@output.bitrate]
  304. when "h264"
  305. video_options << "-ovc x264"
  306. video_options << "-x264encopts bitrate=%d:nocabac" % [@output.bitrate]
  307. end
  308. video_options << "-ofps %f" % [@output.framerate]
  309.  
  310. messages << "Input: %s." % [@input.to_s]
  311. messages << "Output: %s." % [@output.to_s]
  312.  
  313. if @input.size != @output.size
  314. video_options << "-vf-add scale=%d:%d" % [@output.size.width, @output.size.height]
  315. end
  316.  
  317. # For the 770?
  318. # video_options << "-ffourcc DIVX"
  319. # video_options << "-ffourcc DX50"
  320. # video_options << "-noidx"
  321.  
  322. if $options[:verbose]
  323. messages.each do |m|
  324. print "* #{m}\n"
  325. end
  326. end
  327.  
  328. return "mencoder %s -o %s %s %s" % [@input.file_name, @output.file_name, audio_options.join(" "), video_options.join(" ")]
  329. end
  330.  
  331. def run()
  332. analyze()
  333. calculate()
  334. cmd = generate()
  335. print "#{cmd}\n"
  336. end
  337. end
  338. end
  339.  
  340. class App
  341. def initialize(args)
  342. @args = args
  343. @quality_presets = [80, 96, 200, 300, 400, 800, 1000, 1500, 2000]
  344.  
  345. @devices = {}
  346. @devices[:N770] = Device::N770
  347. @devices[:N800] = Device::N800
  348.  
  349. $options = {}
  350. $options[:quality] = 5
  351. $options[:device] = Device::N800
  352. $options[:output_format] = "mpeg4"
  353. end
  354.  
  355. def parse_options
  356. op = OptionParser.new do |opts|
  357. opts.banner = "Usage: transcode [options]"
  358.  
  359. opts.on("-i", "--input FILE",
  360. "Input file") do |i|
  361. $options[:input_file] = i
  362. end
  363.  
  364. opts.on("-o", "--output FILE",
  365. "Output file") do |o|
  366. $options[:output_file] = o
  367. end
  368.  
  369. opts.on("-f", "--format FORMAT",
  370. "Output format") do |f|
  371. $options[:output_format] = f
  372. end
  373.  
  374. opts.on("-q", "--quality N", Integer,
  375. "The quality of the output (1..%d) (default: %d)" % [@quality_presets.length, $options[:quality]]) do |q|
  376. $options[:quality] = q - 1
  377. end
  378.  
  379. opts.on("-d", "--device DEVICE", @devices,
  380. "Output compatible for this device {%s} (default: %s)" % [@devices.keys.join("|"), $options[:device]]) do |d|
  381. $options[:device] = d
  382. end
  383.  
  384. opts.on("-v", "--[no-]verbose",
  385. "Run verbosely") do |v|
  386. $options[:verbose] = v
  387. end
  388.  
  389. opts.on_tail("-h", "--help",
  390. "Show this message") do
  391. puts opts
  392. exit
  393. end
  394. end
  395.  
  396. op.parse!(@args)
  397.  
  398. if not $options[:input_file]
  399. raise ArgumentError, "You need to specify an input file"
  400. end
  401.  
  402. if not $options[:output_file]
  403. $options[:output_file] = File.basename($options[:input_file], ".*") + "_it.avi"
  404. end
  405. end
  406.  
  407. def transcode
  408. @bitrate = @quality_presets[$options[:quality]]
  409. @input = Video::Clip.new($options[:input_file])
  410. @output = Video::Clip.new($options[:output_file])
  411. @device = $options[:device].new()
  412.  
  413. trans = Transcoder::MEncoder.new(@input, @output, @device, @bitrate)
  414. trans.run()
  415. end
  416.  
  417. def run
  418. begin
  419. parse_options()
  420. rescue => msg
  421. print "Error: #{msg}\n"
  422. exit
  423. end
  424.  
  425. begin
  426. transcode()
  427. rescue AppException => msg
  428. print "Error: #{msg}\n"
  429. exit
  430. end
  431. end
  432. end
  433.  
  434. app = App.new(ARGV)
  435. app.run()

Report this snippet  

You need to login to post a comment.