plugins/html5_video/lib/video_processor/ffmpeg.rb
require "rmagick"
require "yaml"
# Works for ffmpeg version 2.8.6-1~bpo8 shiped by Debian Jessie Backports
# https://packages.debian.org/jessie-backports/ffmpeg
# Add this line to your /etc/apt/sources.list:
# deb http://http.debian.net/debian jessie-backports main
# then: aptitude install ffmpeg
class VideoProcessor::Ffmpeg
def _(str)
str
end
def run(*parameters)
parameters = ([:threads, config["num_of_threads"]] + parameters).flatten
cmd = ["ffmpeg"] + parameters.map do |p|
p.kind_of?(Symbol) ? "-" + p.to_s : p.to_s
end
io = IO.popen({ "LANG" => "C.UTF-8" }, cmd, err: [:child, :out])
output = io.read
io.close
response = {
error: { code: 0, message: "" },
parameters: parameters,
output: output
}
if $?.exitstatus != 0 then
if $?.exitstatus == 127 then
throw "There is no FFmpeg installed!"
end
response[:error][:code] = -1
response[:error][:message] = _("Unknow error")
if match = /\n\s*([^\n]*): No such file or directory\s*\n/i.match(output)
response[:error][:code] = 1
response[:error][:message] = _('No such file or directory "%s".') % match[1]
elsif output =~ /At least one output file must be specified/i
response[:error][:code] = 2
response[:error][:message] = _("No output defined.")
elsif match = /\n\s*Unknown encoder[\s']+([^\s']+)/i.match(output)
response[:error][:code] = 3
response[:error][:message] = _('Unknown encoder "%s".') % match[1]
elsif output =~ /\n\s*Error while opening encoder for output/i
response[:error][:code] = 4
response[:error][:message] = _("Error while opening encoder for output - maybe incorrect parameters such as bit rate, frame rate, width or height.")
elsif match = /\n\s*Could not open '([^\s']+)/i.match(output)
response[:error][:code] = 5
response[:error][:message] = _('Could not open "%s".') % match[1]
elsif match = /\n\s*Unsupported codec (.*) for (.*) stream (.*)/i.match(output)
response[:error][:code] = 6
response[:error][:message] = _("Unsupported codec %{codec} for %{act} stream %{id}.") %
{ codec: match[1], act: match[2], id: match[3] }
elsif output =~ /Unable to find a suitable output format/i
response[:error][:code] = 7
response[:error][:message] = _("Unable to find a suitable output format for %{file}.") %
{ file: parameters[-1] }
elsif output =~ /Invalid data found when processing input/i
response[:error][:code] = 8
response[:error][:message] = _("Invalid data found when processing input.")
end
end
return response
end
def config
@config ||= YAML.load_file(File.join(__dir__, "../../config.yml"))["ffmpeg"]
end
def register_information
response = self.run(:formats)[:output]
@@version = /^\s*FFmpeg version ([0-9.]+)/i.match(response)[1]
@@formats = {}
response.split("--")[-1].strip.split("\n").each do |line|
if pieces = / (.)(.) ([^\s]+)\s+([^\s].*)/.match(line)
@@formats[pieces[3].to_sym] = {
demux: (pieces[1] == "D"),
mux: (pieces[2] == "E"),
description: pieces[4].strip
}
end
end
response = self.run(:codecs)[:output]
@@codecs = {}
response.split("--")[-1].strip.split("\n").each do |line|
if pieces = / (.)(.)(.)(.)(.)(.) ([^\s]+)\s+([^\s].*)/.match(line)
@@codecs[pieces[7].to_sym] = {
decode: (pieces[1] == "D"),
encode: (pieces[2] == "E"),
draw_horiz_band: (pieces[4] == "S"),
direct_rendering: (pieces[5] == "D"),
wf_trunc: (pieces[6] == "T"),
type: (
if pieces[3] == "V"; :video
elsif pieces[3] == "A"; :audio
elsif pieces[3] == "S"; :subtitle
else :unknown; end
),
description: pieces[8].strip
}
end
end
{
version: @@version,
formats: @@formats,
codecs: @@codecs
}
end
def timestr_to_secs(str)
return nil if !/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/.match str
t = str.split(":").map(&:to_i)
t[0] * 60 * 60 + t[1] * 60 + t[2]
end
def get_stream_info(stream_str)
stream_info = { type: "undefined" }
stream_info[:type] = "video" if / Video:/.match(stream_str)
stream_info[:type] = "audio" if / Audio:/.match(stream_str)
{
id: [/^\s*Stream ([^:(]+)/],
codec: [/ (?:Audio|Video):\s*([^,\s]+)/x],
bitrate: [/ ([0-9]+) kb\/s\b/, :to_i],
frequency: [/ ([0-9]+) Hz\b/, :to_i],
channels: [/ ([0-9]+) channels\b/, :to_i],
framerate: [/ ([0-9.]+) (fps|tbr)\b/, :to_f],
size: [/ ([0-9]+x[0-9]+)[, ]/],
}.each do |att, params|
re = params[0]
method = params[1] || :strip
if match = re.match(stream_str) then
stream_info[att] = match[1].send(method)
end
end
if stream_info[:size]
size_array = stream_info[:size].split("x").map { |s| s.to_i }
stream_info[:size] = { w: size_array[0], h: size_array[1] }
end
stream_info
end
def webdir_for_original_video(video)
webdir = File.dirname(video) + "/web"
Dir.mkdir(webdir) if !File.exist?(webdir)
webdir
end
def valid_abrate_for_web(info)
return 0 if info.audio_stream.empty?
if brate = info.audio_stream[0][:bitrate]
brate = 8 if brate < 8
(brate > 128) ? 128 : brate
else
48
end
end
def valid_vbrate_for_web(info)
if brate = info.video_stream[0][:bitrate] || info[:global_bitrate]
brate = 128 if brate < 128
(brate > 1024) ? 1024 : brate
else
400
end
end
def valid_size_for_web(info)
orig_size = info.video_stream[0][:size]
if info.video_stream[0][:size][:w] > 640
h = 640.0 / (orig_size[:w].to_f / orig_size[:h].to_f)
{ w: 640, h: h.to_i }
else
{ w: orig_size[:w].to_i, h: orig_size[:h].to_i }
end
end
def validate_conversion_conf_for_web(conf, file_type)
conf = conf.clone
if conf[:abrate].nil? || conf[:vbrate].nil? || conf[:size].nil?
info = get_video_info(conf[:in])
if info[:error][:code] == 0
conf[:abrate] ||= valid_abrate_for_web info
conf[:vbrate] ||= valid_vbrate_for_web info
conf[:size] ||= valid_size_for_web info
end
end
result_dir = webdir_for_original_video conf[:in]
size_str = "#{conf[:size][:w]}x#{conf[:size][:h]}"
unless conf[:file_name]
conf[:file_name] = "#{size_str}_#{conf[:vbrate]}.#{file_type}"
end
conf[:out] = result_dir + "/" + conf[:file_name]
conf
end
public
def get_video_info(file)
response = self.run :i, file
if response[:error][:code] == 2
# No output is not an error on this context
response[:error][:code] = 0
response[:error][:message] = _("Success.")
end
response[:metadata] = {}
{
author: /\n\s*author[\t ]*:([^\n]*)\n/i,
title: /\n\s*title[\t ]*:([^\n]*)\n/i,
comment: /\n\s*comment[\t ]*:([^\n]*)\n/i
}.each do |att, re|
if match = re.match(response[:output]) then
response[:metadata][att] = match[1].strip
end
end
{
type: /\nInput #0, ([a-z0-9]+), from/,
duration: /\n\s*Duration:[\t ]*([0-9:]+)[^\n]* bitrate:/,
global_bitrate: /\n\s*Duration:[^\n]* bitrate:[\t ]*([0-9]+)/
}.each do |att, re|
if match = re.match(response[:output]) then
response[att] = match[1].strip
end
end
response[:duration] = timestr_to_secs response[:duration]
response[:global_bitrate] = response[:global_bitrate].to_i
response[:streams] = []
response[:output].split("\n").grep(/^\s*Stream /).each do |stream|
response[:streams] << get_stream_info(stream)
end
def response.video_stream
self[:streams].select { |s| s[:type] == "video" }
end
def response.audio_stream
self[:streams].select { |s| s[:type] == "audio" }
end
return response
end
def convert2ogv(conf)
conf[:type] = :OGV
conf[:vbrate] ||= 600
parameters = [:i, conf[:in], :y, :'b:v', "#{conf[:vbrate]}k",
:f, "ogg", :acodec, "libvorbis", :vcodec, "libtheora"]
parameters << :s << "#{conf[:size][:w]}x#{conf[:size][:h]}" if conf[:size]
parameters << :'b:a' << "#{conf[:abrate]}k" if conf[:abrate]
# Vorbis dá pau com -ar 8000Hz ???
parameters << :r << conf[:fps] if conf[:fps]
parameters << conf[:out]
response = self.run parameters
response[:conf] = conf
response
end
def convert2mp4(conf)
conf[:type] = :MP4
conf[:vbrate] ||= 600
parameters = [:i, conf[:in], :y, :'b:v', "#{conf[:vbrate]}k",
:preset, "slow", :f, "mp4", :acodec, "aac", :vcodec, "libx264",
:strict, "-2"]
parameters << :s << "#{conf[:size][:w]}x#{conf[:size][:h]}" if conf[:size]
parameters << :'b:a' << "#{conf[:abrate]}k" if conf[:abrate]
parameters << :r << conf[:fps] if conf[:fps]
parameters << conf[:out]
response = self.run parameters
response[:conf] = conf
response
end
def convert2webm(conf)
conf[:type] = :WEBM
conf[:vbrate] ||= 600
parameters = [:i, conf[:in], :y, :'b:v', "#{conf[:vbrate]}k",
:f, "webm", :acodec, "libvorbis", :vcodec, "libvpx"]
parameters << :s << "#{conf[:size][:w]}x#{conf[:size][:h]}" if conf[:size]
parameters << :'b:a' << "#{conf[:abrate]}k" if conf[:abrate]
parameters << :r << conf[:fps] if conf[:fps]
parameters << conf[:out]
response = self.run parameters
response[:conf] = conf
response
end
def make_ogv_for_web(conf)
conf = validate_conversion_conf_for_web conf, :ogv
convert2ogv(conf)
end
def make_mp4_for_web(conf)
conf = validate_conversion_conf_for_web conf, :mp4
convert2mp4(conf)
end
def make_webm_for_web(conf)
conf = validate_conversion_conf_for_web conf, :webm
convert2webm(conf)
end
# video_thumbnail creates 2 preview images on the sub directory web
# from the video file parent dir. This preview images are six concatenated
# frames in one image each. The frames have fixed dimension. The bigger
# preview has frames with in 160x120, and smaller has frames whit in 107x80.
# Use this helper only on the original movie to have only one "web" sub-dir.
def video_thumbnail(video)
result_dir = webdir_for_original_video video
info = get_video_info(video)
if info[:duration] < 15
pos = 1
duration = info[:duration] - 2
frate = (7.0 / duration).ceil
else
pos = (info[:duration] / 2.5).ceil
duration = 7
frate = 1
end
response = self.run :i, video, :ss, pos, :t, duration, :r, frate,
:s, "320x240", result_dir + "/f%d.png"
img_names = ["/preview_160x120.jpg", "/preview_107x80.jpg"]
if response[:error][:code] == 0
imgs = (2..7).map { |num|
img = result_dir + "/f#{num}.png"
File.exists?(img) ? img : nil
}.compact
imgs = Magick::ImageList.new *imgs
imgs.montage {
self.geometry = "160x120+0+0"
self.tile = "1x#{imgs.size}"
self.frame = "0x0+0+0"
}.write result_dir + img_names[0]
imgs.montage {
self.geometry = "107x80+0+0"
self.tile = "1x#{imgs.size}"
self.frame = "0x0+0+0"
}.write result_dir + img_names[1]
end
f_num = 1
while File.exists? result_dir + "/f#{f_num}.png" do
File.delete result_dir + "/f#{f_num}.png"
f_num += 1
end
if response[:error][:code] == 0
return { big: "/web" + img_names[0], thumb: "/web" + img_names[1] }
else
return response
end
end
def version
@@version ||= register_information[:version]
end
def formats
@@formats ||= register_information[:formats]
end
def codecs
@@codecs ||= register_information[:codecs]
end
end