#!/usr/bin/env python
"""
framerate.py
August 27, 2007
A little utility to convert the framerate of a video clip of arbitrary
format, using the yuvmotionfps and ffmpeg utilities.
The advantages of this script are:
- it's pretty simple
- its input and/or output can be piped
- it eliminates the need for typing manual commands, and creating huge
yuv files on your disk.
Prerequisites:
- ffmpeg - should be on your linux distro's feeds
- python version 2.4 or later - ditto
- yuvmotionfps - get from http://jcornet.free.fr/linux/yuvmotionfps.html
Installation:
- rename to a convenient name (I've just called it 'framerate')
- stick in your PATH
- change permissions to 744
Usage:
- run with '-h' for help
- note that audio gets ditched, and resulting video must be
re-muxed with audio afterwards
Example:
- framerate -f mov -vcodec mpeg4 -r 25 -b 2000 infile.avi outfile.mov
Author:
- David McNab rebirth AT orcon DOT net DOT nz
Date:
- August 12, 2007
License:
- GNU Lesser General Public License, version 3 - refer http://www.gnu.org
"""
version = "0.1.3"
import sys, os, time, popen2, commands, thread, threading
# set defaults
inputKbps = 5000 # convert input video to yuv at this bandwidth
inputFps = None # input framerate, if you need to set explicitly
outputKbps = 5000 # generate output video at this bandwidth
outputFps = 25 # framerate of output video
outputFormat = "avi" # output container format
outputVcodec = "mpeg4video" # output video codec
# interpolation defaults
interpBlockSize = 8
interpSearchRadius = 8 # search radius, best not go over 20
interpFrameThreshold = 50 # higher->better
interpSceneThreshold = 8 # 0 = disable scene change detection
interpVerbose = 0 # whether to spit verbose messages
procBufSize = 1024 ** 2
# globals
progname = sys.argv[0]
args = sys.argv[1:]
argc = len(args)
def help():
"""
spit help messages and exit without error
"""
print "%s: General framerate converter with motion interpolation" % progname
print "Usage: %s [options] [infile [outfile]]" % progname
print "Options:"
print " -start ss Set start point in source vid in seconds, default 0"
print " -length ss Set length of video to take from source vid"
print " -ib nnnn Set intermediate kbit/s for input video, default 5000"
print " -b nnnn Set output kbit/s for output video, default 5000"
print " -ir nnn Set input framerate, default 25 - may be unnecessary"
print " -r nnn Set intermediate framerate, default 25"
print " -or nnn Set final output framerate, defaults to -r value"
print " -iblk n Set block size for interpolation, default 8"
print " (lower is better, but slower)"
print " -irad n Set search radius for interpolation, default 8"
print " (higher is better, but slower, don't exceed 24)"
print " -ifrm n Set frame threshold for interpolation, default 8"
print " -iscn n Set scene detect threshold, default 8, 0=off"
print " -dry Do a dry run - only display the commands"
print " -nopipe Use intermediate files instead of pipes"
print " -stages Only perform given conversion stages - implies"
print " -nopipe. Argument is [1-3](,[1-3])* eg 1,2 or 2 or 2,3"
print " -v, --verbose Set verbose stderr messages during interpolate"
print " -V, --version Print version number and exit"
print " -genscript filename.sh - generate a shell script instead"
print "All other options get handed over to ffmpeg for output encoding"
print "Infile and outfile, if omitted or '-', will be stdin and stdout respectively"
print "NOTE: you must put all '-whatever' options before the input"
print "and output filenames, or else things won't work!"
print
sys.exit(0)
def usage(msg=None):
if msg:
sys.stderr.write(msg+"\n")
sys.stderr.write("Usage: %s [options] infile outfile\n" % progname)
sys.stderr.write("Type '%s -h' for help\n" % progname)
sys.exit(1)
def logthread(proc, errfile, lock):
"""
logs from process' error stream to given open file
"""
while True:
c = proc.read(1)
if not c:
errfile.close()
lock.release()
return
errfile.write(c)
def rateToFraction(rate):
"""
tries to convert an int or float fps rate to a fraction
"""
parts = str(rate).split(".")
fpsInt = int(parts[0])
if len(parts) > 1:
fpsDecStr = parts[1]
fpsDec = int(fpsDecStr)
mult = 10 ** len(fpsDecStr)
num = fpsInt * mult + fpsDec
denom = mult
else:
num = fpsInt
denom = 1
return num, denom
def convert(infile, outfile, **kw):
"""
perform conversion
Arguments:
- infile - a readable file object, or a pathname, or '-' to use stdin
- outfile - a writable file object, or a pathname, or '-' to use stdout
Keywords:
- as for help()
- any additional keywords will be passed to ffmpeg during the output conversion
"""
#print "kw=%s" % kw
start = kw.pop('start', None)
if start:
start = float(start)
length = kw.pop('length', None)
if length:
length = float(length)
# control options
dry = kw.pop('dry', False)
nopipe = kw.pop('nopipe', False)
stages = kw.pop('stages', None)
# allow the option to generate a script
genscript = kw.pop("genscript", None)
if genscript:
if not genscript.endswith(".sh"):
genscript += ".sh"
nopipe = True
if stages:
nopipe = True
if isinstance(stages, str):
stages = stages.split(",")
for stage in stages:
if str(stage) not in ['1', '2', '3']:
usage("Invalid stage argument %s" % repr(stage))
stages = [int(stage) for stage in stages]
else:
stages = [1,2,3]
# need to invent some filenames for intermediate files
if nopipe:
if outfile == '-':
outfile = "framerate.out"
fileYuvIn = infile + ".in.yuv"
fileYuvOut = infile + ".out.yuv"
# convert fps to fraction
fpsStr = str(kw.pop('r'))
fps = float(fpsStr)
fpsNum, fpsDenom = rateToFraction(fpsStr)
ofpsStr = kw.pop("or", None)
if ofpsStr:
ofpsNum, ofpsDenom = rateToFraction(ofpsStr)
else:
ofpsNum, ofpsDenom = fpsNum, fpsDenom
# create command to convert to yuv
toYuvCmd = " ".join([
"ffmpeg",
"-y",
])
if start:
toYuvCmd += " -ss %f" % start
if length:
toYuvCmd += " -t %f" % length
fpsIn = kw.pop("ir")
if fpsIn:
toYuvCmd += " -r %s" % fpsIn
toYuvCmd += " ".join([
"",
"-i %s" % infile,
"-f yuv4mpegpipe",
"-b %s" % kw.pop('ib'),
"-vcodec pgmyuv",
])
if nopipe:
toYuvCmd += " %s" % fileYuvIn
else:
toYuvCmd += " -" # pipe
# create framerate conversion command
convertCmd = " ".join([
"yuvmotionfps",
"-f",
"-r %s:%s" % (fpsNum, fpsDenom),
"-b %s" % kw.pop('iblk'),
"-p %s" % kw.pop('irad'),
"-t %s" % kw.pop('ifrm'),
"-s %s" % kw.pop('iscn'),
])
if kw.pop("v"):
convertCmd += " -v"
if nopipe:
convertCmd += " < %s" % fileYuvIn
convertCmd += " | yuvfps -r %s:%s -s %s:%s" % (ofpsNum, ofpsDenom, ofpsNum, ofpsDenom)
if nopipe:
convertCmd += " > %s" % fileYuvOut
#print "1a: kw=%s" % kw
# create ffmpeg output re-encode command
if nopipe:
finalInFile = fileYuvOut
else:
finalInFile = "-"
outputCmd = " ".join([
"ffmpeg",
"-y",
"-i %s" % finalInFile, # piping off yuvmotionfps' output
])
if not kw.get("sameq", False):
outputCmd += " -b %s" % kw.pop('b')
#print "2: kw=%s" % kw
for k,v in kw.items():
if v == True:
option = " -%s" % k
elif v == False:
continue
else:
option = " -%s %s" % (k,v)
#print "adding option: %s" % option
outputCmd += option
isYuvOut = outfile.endswith(".yuv")
if isYuvOut:
if nopipe:
outputCmd = "cat < %s > %s" % (finalInFile, outfile)
else:
outputCmd = "cat > %s" % outfile
else:
outputCmd += " %s" % outfile
if dry:
sys.stderr.write("Dry run commands:\n")
sys.stderr.write(" Convert to YUV:\n %s\n" % toYuvCmd)
sys.stderr.write(" Convert FPS:\n %s\n" % convertCmd)
sys.stderr.write(" Output:\n %s\n" % outputCmd)
return
elif genscript:
f = file(genscript, "w")
f.write("\n".join([
"#!/bin/sh",
"",
"# generated by framerate.py",
"",
"# stage 1 - convert input to YUV format",
toYuvCmd,
"",
"# stage 2 - convert framerate",
convertCmd,
"",
"# stage 3 - convert to final output",
outputCmd,
"",
"",
]))
f.close()
return
#fullCmd = "%s | %s | %s" % (toYuvCmd, convertCmd, outputCmd)
fullCmd1 = "%s | %s" % (toYuvCmd, convertCmd)
fullCmd2 = "%s" % outputCmd
sys.stderr.write("process 1 command:\n %s\n" % fullCmd1)
sys.stderr.write("process 2 command:\n %s\n" % fullCmd2)
#return
# break up into stages if needed
if nopipe:
if 1 in stages:
sys.stderr.write("Stage 1 - convert to YUV:\n %s\n" % toYuvCmd)
os.system(toYuvCmd)
if 2 in stages:
sys.stderr.write("Stage 2 - convert fps:\n %s\n" % convertCmd)
os.system(convertCmd)
if 3 in stages:
sys.stderr.write("Stage 3 - final output:\n %s\n" % outputCmd)
os.system(outputCmd)
return
if 0:
os.system(fullCmd1)
os.system(fullCmd2)
return
# create processes
proc1 = popen2.Popen3(fullCmd1, True, 16384)
proc2 = popen2.Popen3(fullCmd2, True, 16384)
# get file objects
p1Out = proc1.fromchild
p1In = proc1.tochild
p1Err = proc1.childerr
p2Out = proc2.fromchild
p2In = proc2.tochild
p2Err = proc2.childerr
# start up the error logging thread
errFile1 = outfile+".err1"
lock1 = threading.Lock()
lock1.acquire()
errFile2 = outfile+".err2"
lock2 = threading.Lock()
lock2.acquire()
thread.start_new_thread(logthread, (p1Err, file(errFile1, "w"), lock1))
thread.start_new_thread(logthread, (p2Err, file(errFile2, "w"), lock2))
# now shuffle the data from proc1 to proc2
done = False
sys.stderr.write("Converting video: ")
while not done:
chunks = []
size = 0
sys.stderr.write(".")
sys.stderr.flush()
while size < procBufSize:
#print "Reading %d bytes from cmd1" % (procBufSize-size)
buf = p1Out.read(procBufSize - size)
#print "Got it"
if buf == '':
done = True
break
chunks.append(buf)
size += len(buf)
# got our number of chars
buf = "".join(chunks)
p2In.write(buf)
sys.stderr.write("\n")
# proc1 is finished
#p1err = p1Err.read()
p1In.close()
p1Out.close()
# flush proc2 and wait for completion
p2In.flush()
p2In.close()
# now can display errors
lock1.acquire()
sys.stderr.write("____________\nProcess1 Error Output:\n_______________\n")
sys.stderr.write(file(errFile1).read()+"\n")
lock2.acquire()
sys.stderr.write("Process2 Error Output:\n_______________\n")
sys.stderr.write(file(errFile2).read()+"\n")
def main():
"""
CLI front-end
"""
# ensure requisite progs are present
if commands.getoutput("ffmpeg") == '':
sys.stderr.write("Sorry, ffmpeg is not installed\n")
sys.exit(1)
if commands.getoutput("yuvmotionfps") == '':
sys.stderr.write("\n".join([
"Can't find 'yuvmotionfps",
"Please get it from http://jcornet.free.fr/linux/yuvmotionfps.html",
""]))
sys.exit(1)
class MissingArg(Exception):
pass
def getarg(errstr=None):
if args:
arg = args.pop(0)
if errstr and arg == '-':
raise MissingArg(errstr)
return arg
else:
if errstr:
raise MissingArg(errstr)
return None
kw = {
"start" : None,
"length" : None,
"ib" : inputKbps,
"b" : outputKbps,
"ir": None,
"r" : outputFps,
"iblk" : interpBlockSize,
"irad" : interpSearchRadius,
"ifrm" : interpFrameThreshold,
"iscn" : interpSceneThreshold,
"v" : interpVerbose,
"dry" : False,
"nopipe" : False,
"stages" : None,
"sameq" : False,
}
infile = None
try:
while True:
opt = getarg()
if opt == '-h':
help()
if opt in ('-V', '--version'):
print "Version %s" % version
sys.exit(0)
if not opt:
break
if opt.startswith('-') and len(opt) > 1:
opt = opt[1:]
if opt in ['v', 'dry', 'nopipe', "sameq"]:
kw[opt] = True
if opt == 'sameq':
kw.pop("b", None)
else:
kw[opt] = getarg(opt)
#print "setting %s to %s" % (opt, kw[opt])
else:
# gave input filename
infile = opt
break
infile = infile or getarg() or '-'
outfile = getarg() or '-'
convert(infile, outfile, **kw)
except MissingArg, e:
usage("missing/invalid value for option -%s" % e.args[0])
if __name__ == '__main__':
main()