over-video/doc/coniuga.py

967 lines
30 KiB
Python
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#! /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