added coniuga.py (with deps) to doc/ - contains usage paths for ffmpeg -map that might be implemented one day

This commit is contained in:
Overwatch 2014-08-29 11:48:06 +02:00
parent ae18e7a6ef
commit 65b0fe6144
2 changed files with 1333 additions and 0 deletions

967
doc/coniuga.py Executable file
View file

@ -0,0 +1,967 @@
#! /bin/env python
# encoding: utf-8
#
# Res Tranceliī
#
# X - ducentī sexāgintā quattuor videōnis audiōnisque convertator
#
# Usage: coniuga.py files [options]
#
import ellib, os, re, shutil, stat, sys, time
ellib._debug = True
###
# Actus Nullus: Parse command line, define commands, classes and methods
###
print
args = ellib.CmdParser([
"title", "t", 1, False, None,
"order", "O", 1, False, None, # semicolon-separated list of internal TIDs in the desired order
"force-duration", "D", 1, False, None, # force lenght of the video, used in local calculations only
"language", "l", 1, True, None, # TID:data format
"track-name", "ř", 1, True, None, # TID:data format
"sub-charset", "s", 1, True, None, # TID:data format
"fps", "f", 1, True, None, # TID:data format
"video-rate", "v", 1, True, None, # TID:data format
"audio-quality","a", 1, True, None, # TID:data format
"output", "o", 1, False, None,
"normalize", "n", 1, True, None, # TID:data format
"armed", "A", 0, False, False,
"resize", "r", 1, True, None, # TID:WxH
"crop", "C", 1, True, None, # TID:top:bottom:left:right (in px)
"aspect", "r", 1, True, None, # TID:data format
"recalc-aspect","+", 1, True, None, # track number to force aspect ratio recalculation for
"ffm-option", "P", 1, True, None, # additional params for ffmpeg, TID:data format
"downmix", "Ž", 1, True, None, # number of channels to downmix audio to, TID:data format
"mkv-option", "P", 1, True, None, # additional params for mkvmerge, TID:data format
"cut", "T", 1, False, None, # Start at:Duration format
"roundto", "d", 1, True, None, # TID:data
"normalize-all","N", 0, False, True, #
"video-rate-all", "V", 1, False, "600:200:800", # default values (overriden by specific values)
"audio-quality-all", "A", 1, False, "1", #
"roundto-all", "R", 1, False, "2", #
"cleanup", "c", 0, False, True,
"command-only", "X", 0, False, False,
"ignore", "e", 1, True, None, # track number to ignore
"chapters", "H", 1, False, None, # str
"log", "L", 0, False, True, # logging into ~/.coniuga/date - title.log
])
argc = args.config
if args.errors:
ellib.say("Exiting due to errors on command line.", 3)
for error in args.errors:
print " %s" %(error)
sys.exit(1)
get_demuxers_command = 'mkvmerge --list-types spoo'
tracklist_command = 'mkvmerge -i "%s"'
alternate_tracklist_command = 'ffmpeg -i "%s" -vn -an spoo'
media_identify_command = 'mplayer -endpos 0 -vo null -ao null -identify "%s"'
class world:
"""Global settings"""
types = {0: 'video', 1: 'audio', 2: 'subtitle'}
tracks = []
order = []
log = []
time = time.time()
def log(*lines):
"""Add data to logs."""
for line in lines:
world.log.append(re.sub("<C.>", "", line))
def write_log():
"""Dump everything we know into the log."""
logdir = "%s/.coniuga" %(os.getenv("HOME"))
if not os.path.isdir(logdir):
ellib.say("Creating log directory <CB>%s<C/>" %(logdir), 1)
os.mkdir(logdir)
name = re.sub(".mkv$", "", argc.output)
logfile = "%s/%s - %s.log" %(logdir, time.strftime("%Y%m%d.%H%M%S"), name) # FIXME
log = "%s\n\n%s" %(' '.join(sys.argv), '\n'.join(world.log))
file = open(logfile, 'w')
file.write(log)
file.close()
ellib.say("Log saved: <CB>%s<C/>" %(logfile), 1)
def get_demuxers():
pipe = os.popen(get_demuxers_command)
data = pipe.read()
pipe.close()
return re.findall(' (\w{2,4})\s{2,4}\w', data)[1:]
class Track:
def __init__(self, filename, id_in_file, type, original_filename=None):
self.active = True
self.filename = filename
self.original_filename = original_filename
self.id_in_file = id_in_file # (real, ffmpeg)
self.name = ""
self.language = None # str
self.type = type # 0 = video, 1 = audio, 2 = subtitle
self.duration = 0.0
self.proc_opts_hidden = [] # Primary options are added into these before a list is printed
self.cont_opts_hidden = [] # then they are merged into those below
self.proc_opts = []
self.cont_opts = []
self.cut = None # (start, duration)
# flags, data, etc.
temp_filename_basis = "%s_%s" %(self.filename.replace("/", "-"), self.id_in_file[0])
if self.type == 0:
self.temp_files = ["video-pass1-%s.x264" %(temp_filename_basis), "video-pass2-%s.x264" %(temp_filename_basis)]
self.resolution = None # [W, H]
self.orig_resolution = None # (W, H)
self.aspect = None # float
self.ro_aspect = False # True == readonly, don't recalc
self.fps = None # str
self.bitrate = tuple(map(int, argc.video_rate_all.split(":"))) # (nominal, variance, maximal)
self.resize = None # (W, H)
self.crop = None # (T, B, L, R)
if argc.roundto_all not in ["2", "16"]:
ellib.say("Option <Cr>roundto-all<C/> must be either <CB>2<C/> or <CB>16<C/>.", 3)
sys.exit(1)
self.roundto = int(argc.roundto_all)
elif self.type == 1:
self.temp_files = ["audio-%s.pcm" %(temp_filename_basis), "audio-%s.vorbis" %(temp_filename_basis)]
self.quality = float(argc.audio_quality_all) # float <-1; 10>
self.normalize = argc.normalize_all # bool
self.downmix = None # channels to downmix to; None means leave it be
elif self.type == 2:
self.charset = None # str
self.temp_file = "subtitle-%s.text" %(temp_filename_basis)
def __cmp__(self, other):
return cmp(self.type, other.type)
def get_tracks(filename):
# ffmpeg's an idiot incapable of grasping the preposterous idea that some filenames just HAVE a colon in their names, so we gotta hack around it
if ":" in filename and False:
original_filename = filename
filename = filename.replace(":", "_")
while os.path.exists(filename):
filename = "_" + filename
os.symlink(original_filename, filename)
else:
original_filename = None
suffix = filename.split(".")[-1].lower()
pipes = os.popen3(alternate_tracklist_command %(filename))
ffmpeg_data = pipes[2].read()
map(file.close, pipes)
tracks = []
with_ffmpeg = True
if suffix in get_demuxers():
# analyse with mkvmerge
pipe = os.popen(tracklist_command %(filename))
data = pipe.read()
pipe.close()
for tid, type in re.findall('Track ID (\d+): (\w+)', data):
with_ffmpeg = False
tid = int(tid)
if suffix.lower() in ["mkv", "mov"]:
tid -= 1
if original_filename:
tracks.append(Track(filename, (tid, tid), {'video': 0, 'audio': 1, 'subtitles': 2}[type], original_filename=original_filename))
else:
tracks.append(Track(filename, (tid, tid), {'video': 0, 'audio': 1, 'subtitles': 2}[type]))
# post pro: id_in_file for ffmpeg has to skip subtitles
for track in tracks:
if track.type == 2:
track.id_in_file = (track.id_in_file[0], None)
index = tracks.index(track)
for following_track in tracks[index+1:]:
following_track.id_in_file = (following_track.id_in_file[0], following_track.id_in_file[1] - 1)
if with_ffmpeg:
# analyse with ffmpeg
for tid, type in re.findall(' Stream #\d+.(\d+)[^:]*: ([^:]+):', ffmpeg_data):
if type in ['Audio', 'Video']:
tracks.append(Track(filename, (int(tid), int(tid)), {'Video': 0, 'Audio': 1}[type]))
else:
ellib.say("Unknown track <CB>%s<C/> type <Cr>%s<C/> from file <CB>%s<C/>" %(tid, type, filename))
for track in tracks:
if track.type in [0, 1]:
hours, mins, duration = re.findall("Duration: (.+?),", ffmpeg_data)[0].split(":")
track.duration = float(duration) + int(mins)*60 + int(hours)*3600
if track.type == 0:
#resolution, track.fps = re.findall("Stream #0.%s.+, (\d+x\d+).* (\d+.\d+)" %(track.id_in_file[1]), ffmpeg_data)[0]
#resolution, track.fps = re.findall("Stream #0.%s.+, (\d+x\d+),.* (\d+.\d+) fps" %(track.id_in_file[1]), ffmpeg_data)[0]
# =media-video/ffmpeg-0.4.9_p20081014
if "tbr" in ffmpeg_data and "tbn" in ffmpeg_data and "tbc" in ffmpeg_data:
resolution, track.fps = re.findall("Stream #0.%s.+, (\d+x\d+).* ([0-9./]+) tbr" %(track.id_in_file[1]), ffmpeg_data)[0]
else:
resolution, track.fps = re.findall("Stream #0.%s.+, (\d+x\d+).* (\d+.\d+)" %(track.id_in_file[1]), ffmpeg_data)[0]
if track.fps.startswith("29.9"):
track.fps = "30000/1001"
elif track.fps.startswith("23.9"):
track.fps = "24000/1001"
track.resolution = map(int, resolution.split("x"))
# we still need that state-of-the-junk MPlayer to capture aspect ratio
pipes = os.popen3(media_identify_command %(filename))
data = pipes[1].read()
map(file.close, pipes)
aspect = re.findall("ID_VIDEO_ASPECT=(.+)", data)
if (aspect and float(aspect[-1]) > 0) and not (str(tracks.index(track)) in argc.recalc_aspect):
track.aspect = float(aspect[-1])
else:
track.aspect = float(track.resolution[0]) / track.resolution[1]
return tracks
def list_track(tid, track, indent):
type = world.types[track.type].rjust(8)
filename = track.filename.ljust(indent)
id_in_file = track.id_in_file
if track.proc_opts:
proc_opts = "ffmpeg(<CB>%s<C/>)" %(' '.join(track.proc_opts))
else:
proc_opts = ""
if track.cont_opts:
cont_opts = "mkvmerge(<CB>%s<C/>)" %(' '.join(track.cont_opts))
else:
cont_opts = ""
if track.active:
flags = ""
if track.language:
flags += "lang:<Cb>%s<C/> " %(track.language)
if track.name:
flags += "name:<Cb>%s<C/> " %(track.name)
if track.type == 0:
flags += "bitrate:<Cb>%s<C/>:<Cb>%s<C/>:<Cb>%s<C/>" %(track.bitrate)
# check bitrate if sufficient
if "/" in track.fps:
fps = map(float, track.fps.split("/"))
fps = fps[0] / fps[1]
else:
print
print track.fps
fps = float(track.fps)
minimal_bitrate = (track.resolution[0] * track.resolution[1] * fps) / 18432
track_bitrate = (track.bitrate[0] + track.bitrate[1]) / 2
if minimal_bitrate - track_bitrate > 150: # insufficient
flags += ":<Cr>low<C/>:<Cr>+%s<C/> " %(int(minimal_bitrate - track_bitrate))
elif int(track_bitrate) - minimal_bitrate > 600: # excessive
flags += ":<Cy>high<C/>:<Cy>-%s<C/> "%(int(track_bitrate - minimal_bitrate))
else:
flags += ":<Cg>OK<C/> "
flags += "fps:<Cb>%s<C/> " %(track.fps)
if track.resolution:
x, y = track.orig_resolution[0], track.orig_resolution[1]
bad = False
if x % 2 == 1:
x = "<Cr>%s<C/>" %(x)
if y % 2 == 1:
y = "<Cr>%s<C/>" %(y)
flags += "size:<Cb>%s<C/>x<Cb>%s<C/> " %(x, y)
if track.crop:
flags += "crop:<Cb>%s<C/>:<Cb>%s<C/>:<Cb>%s<C/>:<Cb>%s<C/> " %(track.crop)
if track.resize or track.crop:
flags += "resize:<Cb>%s<C/>x<Cb>%s<C/> " %(track.resolution[0], track.resolution[1])
if track.aspect:
flags += "aspect:<Cb>%s<C/> " %(track.aspect)
elif track.type == 1:
flags += "quality:<Cb>%s<C/> " %(track.quality)
if track.normalize:
flags += "<Cb>normalize<C/> "
elif track.type == 2:
if track.charset:
flags += "char:<Cb>%s<C/> " %(track.charset)
if track.type in [0, 1]:
if track.cut:
flags += "cut:<Cb>%s<C/>:<Cb>%s<C/> " %(track.cut)
else:
flags = "<Cr>TRACK IGNORED<C/>"
output = "<Cy>%s<C/>: <CB>%s<C/> from <CB>%s<C/> <Cb>%s<C/>/<Cb>%s<C/> %s %s %s" %(str(tid).rjust(3), type, filename, id_in_file[0], id_in_file[1], flags, proc_opts, cont_opts)
log(output)
print ellib.style(output)
def list_tracks():
ellib.say("Duration: %s" %(world.tracks[0].duration))
print
print ellib.style("<Cy>TID<C/>: <CB>type<C/> from <CB>filename<C/> <Cb>position in file<C/>/<Cb>without subtitles<C/> flags")
filename_len = 0
for track in world.tracks:
if len(track.filename) > filename_len:
try:
filename_len = len(track.filename.decode(sys.stdin.encoding))
except:
filename_len = len(track.filename)
if world.order:
order = world.order
else:
order = range(len(world.tracks))
for tid in order:
try:
list_track(tid, world.tracks[tid], filename_len)
except IndexError:
ellib.say("Track <Cr>%s<C/> is not defined." %(tid), 3)
print
if argc.chapters:
ellib.say("Reading chapters from file <CB>%s<C/>." %(argc.chapters))
print
# estimates (in kB)
total_size = 0.0
for track in world.tracks:
if not track.active:
continue
if track.type == 0:
total_size += track.duration * (track.bitrate[0] + track.bitrate[2]) / 2 / 8
elif track.type == 1:
total_size += track.duration * 10 # approximate kB/s for vorbis q=1
if total_size > 40960:
total_size = "%s MB" %(int(total_size / 1024))
else:
total_size = "%s kB" %(int(total_size))
total_size_str = "Estimated file size is <CB>%s<C/>." %(total_size)
log("")
log(total_size_str)
ellib.say(total_size_str)
print
def parse_group(data, accepted=[0, 1, 2]):
tid, data = data.split(":", 1)
try:
tid = int(tid)
except ValueError:
ellib.say("<Cr>%s<C/> is not a valid Track ID." %(tid), 3)
sys.exit(1)
try:
track = world.tracks[tid]
except IndexError:
ellib.say("Track <Cr>%s<C/> is not defined." %(tid), 3)
sys.exit(1)
if track.type not in accepted:
accept_names = []
for type in accepted:
accept_names.append("<CB>%s<C/>" %(world.types[type]))
accept_names = ' or '.join(accept_names)
ellib.say("Track <Cr>%s<C/> is not a valid %s track." %(tid, accept_names), 3)
sys.exit(1)
return (tid, track, data)
def pad(n, padme = False, padto = 5):
if type(n) == float:
n = round(n, 2)
n = str(n)
if "." in n: # it's a float -> make sure it has two decimals. Pad if necessary.
if len(n.split(".")[1]) == 1:
n = n + "0"
elif len(n) == 1: # clock
n = "0" + n
if padme:
padtimes = padto - len(n)
n = "0"*padtimes + n
return n
def strftime(secs, return_string=True):
hours = int(secs) / 3600
secs = secs % 3600
mins = int(secs) / 60
secs = int(secs % 60)
if return_string:
return "%s:%s:%s" %(pad(hours), pad(mins), pad(secs))
else:
return (hours, mins, secs)
class ProgressBar:
def __init__(self, lenght = 30):
self.lenght = lenght - 3 # minus [, > and ]
self.filesize = 0
self.buffer = [] # stores file sizes, used to compute ETA
self.buffer_len = 8
self.start = time.time()
self.last_size = 0
def draw(self, percent, filesize = None, additional = ""):
# filesize == current filesize
# self.filesize == projected filesize
if not filesize or filesize > self.filesize: # assume 100%
filesize = self.filesize
if percent > 1:
percent = 1.0
current = int(self.lenght * percent)
# maintain a graph of speeds (kB/s for each update)
curr_time = time.time()
if not self.buffer:
last_time = self.start
else:
last_time = self.buffer[-1][1]
speed = (filesize - self.last_size) / (curr_time - last_time)
self.buffer.append([speed, curr_time])
self.last_size = filesize
if len(self.buffer) > self.buffer_len:
self.buffer.pop(0) # delete the oldest one
# average speed
suma = 0.0
for i in range(len(self.buffer)):
suma += self.buffer[i][0]
average = suma / len(self.buffer)
# come up with an ETA based on average speed
if average > 0:
remaining_size = self.filesize - filesize
ETA = strftime(remaining_size / average)
else:
ETA = "never"
if percent == 1:
arrow = "==="
else:
arrow = ">"
sys.stdout.write("\r%s%% [%s%s%s] %s/%sMB (%s kB/s) ETA: %s %s " %(pad(percent*100), current * "=", arrow, (self.lenght - current) * " ", pad(filesize/1048576.0), pad(self.filesize/1048576.0), pad(average/1024.0, True), ETA, additional))
sys.stdout.flush()
def ffmpeg(command, temp_file, track):
# purge temps, ffmpeg is kinda sensitive :o)
try:
os.remove(temp_file)
except:
pass
pipes = os.popen3(command)
bar = ProgressBar()
while True:
#time.sleep(1)
chunk = pipes[2].read(256)
if not chunk:
break # we're done I guess
# split output by '\r's, parse the one before last (it's surely complete and most up to date)
try:
line = chunk.split('\r')[-2]
# data: (frame, size in kB, time in seconds, bitrate in kb/s) - all floats
secs, bitrate = map(float, re.findall('size=\s*\d+kB time=\s*(\d+.\d+) bitrate=\s*(\d+.\d)kbits/s', line)[0])
# estimate target file size
percent = secs / track.duration
if percent == 0:
percent = 0.01
current_size = os.lstat(temp_file)[stat.ST_SIZE]
target_size = current_size / percent
bar.filesize = target_size
# draw progress bar
additional = "Kbps: %s" %(bitrate)
bar.draw(percent, current_size, additional)
except IndexError: # it's not converting yet
try:
#print line
pass
except:
pass
try:
bar.draw(1, target_size, bitrate)
except:
pass
map(file.close, pipes)
print
def oggenc(command, temp_file):
pipes = os.popen3(command)
bar = ProgressBar()
while True:
chunk = pipes[2].read(128)
if not chunk:
break # we're done I guess
try:
line = chunk.split('\r')[-2]
perc_units, perc_frac = re.findall("(\d+)[.,](\d+)%.+\d+m\d+s remaining", line)[0]
if len(perc_frac) == 1:
perc_frac += "0"
percent = (int(perc_units) + float(perc_frac)/100) / 100
if percent == 0:
percent = 0.01
current_size = os.lstat(temp_file)[stat.ST_SIZE]
target_size = current_size / percent
bar.filesize = target_size
# draw progress bar
bar.draw(percent, current_size)
except IndexError: # it's not converting yet
pass
try:
bar.draw(1, target_size)
except:
pass
map(file.close, pipes)
print
###
# Actus Primus: Analyse input files, create track objects, interpret gathered intel, bring order into chaos
###
# gather basic intel on our files
for filename in args.targets:
if os.path.exists(filename):
world.tracks += get_tracks(filename)
else:
ellib.say("File <Cr>%s<C/> does not exist." %(filename), 3)
sys.exit(1)
if not world.tracks:
ellib.say("Specify at least one file containing video, audio, or subtitles.", 3)
sys.exit(1)
# sort video -> audio -> subs
world.tracks.sort()
# React on user input
if argc.order:
world.order = map(int, argc.order.split(":"))
# Container options
if not argc.title:
if world.tracks[0].original_filename:
filename = world.tracks[0].original_filename
else:
filename = world.tracks[0].filename
if "." in filename:
argc.title = '.'.join(filename.split(".")[:-1])
else:
argc.title = filename
ellib.say("Setting title to <CB>%s<C/> as none was specified." %(argc.title))
if not argc.output:
argc.output = "%s.mkv" %(argc.title)
argc.output = ellib.charfilter(argc.output, {'/': '-'})
ellib.say("Setting output filename to <CB>%s<C/> as none was specified." %(argc.output))
for opt in argc.language:
tid, track, data = parse_group(opt)
track.language = data
for opt in argc.track_name:
tid, track, data = parse_group(opt)
track.name = data
for opt in argc.sub_charset:
tid, track, data = parse_group(opt, [2])
track.charset = data
for opt in argc.fps:
tid, track, data = parse_group(opt, [0])
track.fps = data
track.proc_opts_hidden.append("-r %s" %(data))
# Processor options
if argc.force_duration:
for track in world.tracks:
track.duration = float(argc.force_duration)
if argc.cut:
for track in world.tracks:
if track.type in [0, 1]:
track.cut = tuple(map(float, argc.cut.split(":")))
for opt in argc.normalize:
if ":" in opt:
tid, track, data = parse_group(opt, [1])
if data.lower() in ["true", "1", "yes"]:
track.normalize = True
else:
track.normalize = False
else:
if opt.lower() in ["true", "1", "yes"]:
data = True
else:
data = False
for track in world.tracks:
if track.type == 1:
track.normalize = data
for opt in argc.aspect:
tid, track, data = parse_group(opt, [0])
if ":" in data or "/" in data:
data = map(float, re.findall("(.+)[:/](.+)", data)[0])
aspect = data[0] / data[1]
else:
aspect = float(data)
track.aspect = aspect
track.ro_aspect = True # readonly, don't recalc
for opt in argc.ffm_option:
tid, track, data = parse_group(opt, [0, 1])
track.proc_opts.append(data)
for opt in argc.downmix:
tid, track, data = parse_group(opt, [1])
track.downmix = data
for opt in argc.mkv_option:
tid, track, data = parse_group(opt)
track.cont_opts.append(data)
for opt in argc.audio_quality:
tid, track, data = parse_group(opt, [1])
track.quality = float(data)
for opt in argc.video_rate:
tid, track, data = parse_group(opt, [0])
track.bitrate = tuple(map(int, data.split(":")))
for opt in argc.resize:
tid, track, data = parse_group(opt, [0])
if "x" not in data:
ellib.say("Resize parameter <Cr>%s<C/> of track <CB>%s<C/> is not valid." %(data, tid), 3)
sys.exit(1)
track.resize = tuple(map(int, data.split("x")))
for opt in argc.crop:
tid, track, data = parse_group(opt, [0])
if data.count(":") is not 3:
ellib.say("Crop parameter <Cr>%s<C/> of track <CB>%s<C/> is not valid." %(data, tid), 3)
sys.exit(1)
track.crop = tuple(map(int, data.split(":")))
for crop in track.crop:
if crop%2 == 1:
ellib.say("Crop values from track <CB>%s<C/> are not divisible by 2." %(tid), 3)
sys.exit(1)
for opt in argc.roundto:
tid, track, data = parse_group(opt, [0])
if data == "16":
track.roundto = 16
else:
ellib.say("Roundto parameter of track <Cr>%s<C/> must be either <CB>16<C/> or <CB>2<C/>." %(tid), 3)
for tid in argc.ignore:
try:
tid = int(tid)
except ValueError:
ellib.say("<Cr>%s<C/> is not a valid Track ID." %(tid), 3)
sys.exit(1)
if tid < len(world.tracks):
world.tracks[tid].active = False
if world.order:
order = world.order
else:
order = range(len(world.tracks))
# understand and apply all parameters
for tid in order:
track = world.tracks[tid]
if track.language:
#track.cont_opts_hidden.append("--language %s:%s" %(track.id_in_file[0], track.language))
track.cont_opts_hidden.append("--language 0:%s" %(track.language))
if track.type in [0, 1]:
if track.cut:
track.proc_opts_hidden.append("-ss %s -t %s" %(track.cut))
if track.type == 0:
## Size and aspect ratio
# 1) determine real size
if abs(float(track.resolution[0]) / track.resolution[1] - track.aspect) > 0.05:
if track.aspect >= 1:
track.resolution[0] = int(round(track.aspect * track.resolution[1]))
else:
track.resolution[1] = int(round(track.resolution[0] / track.aspect))
# save original sizes for the list
track.orig_resolution = tuple(track.resolution)
# 2) determine percentages - we'll use them to resize later
if track.resize:
perc_x = float(track.resize[0]) / track.resolution[0]
perc_y = float(track.resize[1]) / track.resolution[1]
# 3) substract crop values (comes in TBLR format)
if track.crop:
track.resolution[0] -= sum(track.crop[2:4])
track.resolution[1] -= sum(track.crop[0:2])
# 4) resize them if necessary
if track.resize:
track.resolution[0] *= perc_x
track.resolution[1] *= perc_y
# if we were resizing or cropping, calculate a new aspect ratio
if (track.resize or track.crop) and not track.ro_aspect:
track.aspect = track.resolution[0] / float(track.resolution[1])
track.proc_opts_hidden.append("-aspect %s" %(track.aspect))
# 5) round sizes
track.resolution[0] = int(round(track.resolution[0] / float(track.roundto))) * track.roundto
track.resolution[1] = int(round(track.resolution[1] / float(track.roundto))) * track.roundto
## ffmpeg options
# crop
if track.crop:
track.proc_opts_hidden.append('-vf crop=%d:%d:%d:%d' %(track.orig_resolution[0] - track.crop[2] - track.crop[3],
track.orig_resolution[1] - track.crop[0] - track.crop[1],
track.crop[2],
track.crop[0]))
# resize
if track.resize:
track.proc_opts_hidden.append("-s %sx%s" %(track.resolution[0], track.resolution[1]))
# mkvmerge options
if track.fps:
#track.cont_opts_hidden.append("--default-duration %s:%sfps" %(track.id_in_file[0], track.fps))
track.cont_opts_hidden.append("--default-duration 0:%sfps" %(track.fps))
elif track.type == 2:
if track.charset:
#track.cont_opts_hidden.append("--sub-charset %s:%s" %(track.id_in_file[0], track.charset))
track.cont_opts_hidden.append("--sub-charset 0:%s" %(track.charset))
# print all we know
list_tracks()
if not argc.armed: # if we're just planning we don't wanna go any further. Exiting nicely.
for track in world.tracks:# part 2A of our "Why ffmpeg's A Total Moron™" hack - remove the file if not armed
if track.original_filename and os.path.exists(track.filename): # in case a previous iteration removed the file
os.remove(track.filename)
sys.exit(0)
else:
# merge hidden and additional options
for track in world.tracks:
track.proc_opts = track.proc_opts_hidden + track.proc_opts
track.cont_opts = track.cont_opts_hidden + track.cont_opts
###
# Actus Secundus: The hard part - transcode (crop, resize, ...) video, transcode audio
###
ffmpeg_command = 'ffmpeg -i "%s" %s -map 0:%s:0 "%s"' # input parameters ffmpeg_stream_id output
ffmpeg_video_parameters = '-pass %s %s -an -vcodec libx264 -nr 400 -b %sk -bt %sk -maxrate %sk -coder 1 -flags +loop -partitions +parti4x4+partp8x8+partb8x8 -me_method 5 -subq 1 -trellis 0 -me_range 16 -g 250 -keyint_min 25 -sc_threshold 40 -i_qfactor 0.71 -rc_eq "blurCplx^(1-qComp)" -qmin 10 -qmax 51 -qdiff 4 -f rawvideo' # passnum additional_options bitrate_nominal bitrate_variance bitrate_maximal
ffmpeg_audio_parameters = '-vn -acodec pcm_s16le %s -f wav %s'
audio_normalize_command = 'normalize "%s"'
audio_encode_command = 'oggenc -q 1 "%s" -o "%s"'
# make us a nice temp directory
world.temp = "coniuga-temp_%s" %(argc.output)
if argc.command_only:
print 'mkdir "%s"' %(world.temp)
print 'cd "%s"' %(world.temp)
print
else:
ellib.say("Creating temp directory <CB>%s<C/>" %(world.temp), 1)
os.mkdir(world.temp)
os.chdir(world.temp)
# adjust filenames to reflect the change of current working directory
for track in world.tracks:
track.filename = "../%s" %(track.filename)
for tid in order:
track = world.tracks[tid]
if not track.active:
continue
if track.type == 2: # Subtitles do not require processing in this act. Just copy them over.
if argc.command_only:
print 'cp "%s" "%s"' %(track.filename, track.temp_file)
print
else:
shutil.copy(track.filename, track.temp_file)
if track.type == 0: # videō
for passnum in ["1", "2"]:
# compose the command
additional_processor_parameters = ' '.join(track.proc_opts)
# passnum additional_options bitrate_nominal bitrate_variance bitrate_maximal
processor_parameters = ffmpeg_video_parameters %((passnum, additional_processor_parameters) + track.bitrate)
main_command = ffmpeg_command %(track.filename, processor_parameters, track.id_in_file[1], track.temp_files[int(passnum) - 1])
if argc.command_only:
print main_command
print
else:
ellib.say("<CB>Transcoding<C/> <Cy>video<C/> track <Cy>%s<C/>, <Cb>pass %s<C/>..." %(tid, passnum), 1)
ffmpeg(main_command, track.temp_files[int(passnum) - 1], track)
elif track.type == 1: # audiō
additional_processor_parameters = ' '.join(track.proc_opts)
if track.downmix:
processor_parameters = ffmpeg_audio_parameters %("-ac %s" %(track.downmix), additional_processor_parameters)
else:
processor_parameters = ffmpeg_audio_parameters %("", additional_processor_parameters)
main_command = ffmpeg_command %(track.filename, processor_parameters, track.id_in_file[1], track.temp_files[0])
if argc.command_only:
print main_command
print
else:
ellib.say("<CB>Extracting<C/> <Cy>audio<C/> track <Cy>%s<C/>..." %(tid), 1)
ffmpeg(main_command, track.temp_files[0], track)
if track.normalize:
if argc.command_only:
print audio_normalize_command %(track.temp_files[0])
print
else:
ellib.say("<CB>Normalizing<C/> <Cy>audio<C/> track <Cy>%s<C/>..." %(tid), 1)
pipes = os.popen3(audio_normalize_command %(track.temp_files[0]))
pipes[2].read()
if argc.command_only:
print audio_encode_command %(track.temp_files[0], track.temp_files[1])
print
else:
ellib.say("<CB>Encoding<C/> <Cy>audio<C/> track <Cy>%s<C/>..." %(tid), 1)
oggenc(audio_encode_command %(track.temp_files[0], track.temp_files[1]), track.temp_files[1])
###
# Actus Tertius: Müxing.
###
mkvmerge_command = 'mkvmerge -o "../%s" --title "%s"' # output, title, tracks
merge_command = [mkvmerge_command %(argc.output, argc.title)]
for tid in order:
track = world.tracks[tid]
if not track.active:
continue
if track.type in [0, 1]:
file = track.temp_files[1]
else:
file = track.temp_file
if track.cont_opts:
track_command = '%s "%s"' %(' '.join(track.cont_opts), file)
else:
track_command = '"%s"' %(file)
merge_command.append(track_command)
if argc.chapters:
merge_command.append('--chapters "../%s"' %(argc.chapters))
merge_command = ' '.join(merge_command)
if argc.command_only:
print merge_command
print
else:
retval = os.system(merge_command)
if retval != 0:
ellib.say("mkvmerge, halting without cleanup (use manual merging)...", 3)
sys.exit(2)
###
# Actus Quartus: Cleanup.
###
if argc.cleanup:
files = ["ffmpeg2pass-0.log", "x264_2pass.log", "x264_2pass.log.mbtree"]
for track in world.tracks:
# part 2B of our "Why ffmpeg's A Total Moron™" hack - remove the file if armed
if track.original_filename:
try:
os.remove("../"+track.filename)
except:
pass
if not track.active:
continue
if track.type in [0, 1]:
files.extend(track.temp_files)
else:
files.append(track.temp_file)
to_remove = []
for file in files:
if argc.command_only:
to_remove.append(file)
else:
try:
os.remove(file)
ellib.say("Removed temp file <CB>%s<C/>" %(file), 1)
except OSError:
ellib.say("Expected temp file <CB>%s<C/> not present." %(file))
if argc.command_only:
print 'rm -f "%s"' %('" "'.join(to_remove))
print 'cd ..'
print 'rmdir "%s"' %(world.temp)
else:
os.chdir("..")
try:
os.rmdir(world.temp)
ellib.say("Removed temp directory <CB>%s<C/>" %(world.temp), 1)
except OSError:
ellib.say("Expected temp <Cr>dir<C/> <CB>%s<C/> not present, what's going on ???" %(file))
if not argc.command_only:
time_taken = time.time() - world.time
final_size = os.lstat(argc.output)[stat.ST_SIZE] / 1024
if final_size > 40960:
final_size = "%s MB" %(int(final_size / 1024))
else:
final_size = "%s kB" %(int(final_size))
final_size_str = "Resulting file size is <CB>%s<C/>." %(final_size)
ellib.say(final_size_str)
time_taken_str = "Process took <CB>%s<C/> hours, <CB>%s<C/> minutes and <CB>%s<C/> seconds." %(strftime(time_taken, False))
ellib.say(time_taken_str, 2)
log("")
log(time_taken_str, final_size_str)
write_log()
print