Table of Contents

n900-encode.py: Create N900-friendly MP4-Videos

Today I present a (relatively) simple script that I wrote to convert videos to a format suitable for viewing on the Nokia N900. I know that there are many solutions for doing this, but none could handle subtitles (especially styled ones like SSA) well. Also I wanted the script to be scriptable, e.g. for converting a whole directory. Finally its a follow-up from my old n800-encode.sh script. So here is my solution to creating N900-friendly MP4:

The most recent version can always be found at my GitHub.

The Features

Version History

Known Limitations and Bugs

The Prequisites

The Usage

The simplest way is to just run this command:

n900-encode.py -i some_video.avi -o n900_video.mp4

for more available options see the script below or use the included help function (-h)

The Script

n900-encode.py
#!/usr/bin/env python
 
###########################################################################################
# n900-encode.py: Encode almost any Video to an Nokia N900-compatible format (h264,aac,mp4)
# Disclaimer: This program is provided without any warranty, USE AT YOUR OWN RISK!
#
# (C) 2010 Stefan Brand <seiichiro@seiichiro0185.org>
#
# Version 0.2
###########################################################################################
 
import sys, os, getopt, subprocess, re, atexit
from signal import signal, SIGTERM, SIGINT
from time import sleep
 
version = "0.2"
 
###########################################################################################
# Default values, feel free to adjust
###########################################################################################
 
_basewidth = 800         # Base width for widescreen Video
_basewidth43 = 640    # Base width for 4:3 Video
_maxheight = 480        # maximum height allowed
_abitrate = 112            # Audio Bitrate in kBit/s
_vbitrate = 22             # Video Bitrate, if set to value < 52 it is used as a CRF Value for Constant rate factor encoding
_threads = 0                # Use n Threads to encode (0 means use number of CPUs / Cores)
_mpbin = None                # mplayer binary, if set to None it is searched in your $PATH
_mcbin = None                # mencoder binary, if set to None it is searched in your $PATH
_m4bin = None             # MP4Box binary, if set to None it is searched in your $PATH
 
###########################################################################################
# Main Program, no changes needed below this line
###########################################################################################
 
def main(argv):
    """Main Function, cli argument processing and checking"""
 
    # CLI Argument Processing
    try:
        opts, args = getopt.getopt(argv, "i:o:m:v:a:t:hf", ["input=", "output=", "mpopts=", "abitrate=", "vbitrate=", "threads=", "help", "force-overwrite"])
    except getopt.GetoptError, err:
        print str(err)
        usage()
 
    input = None
    output = "n900encode.mp4"
    mpopts = ""
    abitrate = _abitrate
    vbitrate = _vbitrate
    threads = _threads
    overwrite = False
    for opt, arg in opts:
        if opt in ("-i", "--input"):
            input = arg
        elif opt in ("-o" "--output"):
            output = arg
        elif opt in ("-m" "--mpopts"):
            mpopts = arg
        elif opt in ("-a", "--abitrate"):
            abitrate = int(arg)
        elif opt in ("-v", "--vbitrate"):
            vbitrate = int(arg)
        elif opt in ("-t", "--threads"):
            threads = arg
        elif opt in ("-f", "--force-overwrite"):
            overwrite = True
        elif opt in ("-h", "--help"):
            usage()
 
    # Check for needed Programs
    global mpbin
    mpbin = None
    if not _mpbin == None and os.path.exists(_mpbin) and not os.path.isdir(_mpbin):
        mpbin = _mpbin
    else:
        mpbin = progpath("mplayer")
    if mpbin == None:
        print "Error: mplayer not found in PATH and no binary given, Aborting!"
        sys.exit(1)
 
    global mcbin
    mcbin = None
    if not _mcbin == None and os.path.exists(_mcbin) and not os.path.isdir(_mcbin):
        mcbin = _mcbin
    else:
        mcbin = progpath("mencoder")
    if mcbin == None:
        print "Error: mencoder not found in PATH and no binary given, Aborting!"
        sys.exit(1)
 
    global m4bin
    m4bin = None
    if not _m4bin == None and os.path.exists(_m4bin) and not os.path.isdir(_m4bin):
        m4bin = _m4bin
    else:
        m4bin = progpath("MP4Box")
    if m4bin == None:
        print "Error: MP4Box not found in PATH and no binary given, Aborting!"
        sys.exit(1)
 
    # Check input and output files
    if not os.path.isfile(input):
        print "Error: input file is not a valid File or doesn't exist"
        sys.exit(2)
 
    if os.path.isfile(output):
        if overwrite:
            os.remove(output)
        else:
            print "Error: output file " + output + " already exists, force overwrite with -f"
            sys.exit(1)
 
    # Start Processing
    res = calculate(input)
    convert(input, output, res, _abitrate, vbitrate, threads, mpopts)
    sys.exit(0)
 
 
def calculate(input):
    """Get Characteristics from input video and calculate resolution for output"""
 
    # Get characteristics using mplayer
    cmd=[mpbin, "-ao", "null", "-vo", "null", "-frames", "0", "-identify", input]
    mp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
 
    try:
        s = re.compile("^ID_VIDEO_ASPECT=(.*)$", re.M)
        m = s.search(mp[0])
        orig_aspect = m.group(1)
        s = re.compile("^ID_VIDEO_WIDTH=(.*)$", re.M)
        m = s.search(mp[0])
        orig_width = m.group(1)
        s = re.compile("^ID_VIDEO_HEIGHT=(.*)$", re.M)
        m = s.search(mp[0])
        orig_height = m.group(1)
        s = re.compile("^ID_VIDEO_FPS=(.*)$", re.M)
        m = s.search(mp[0])
        fps = m.group(1)
 
    except:
        print "Error: unable to identify source video, exiting!"
        sys.exit(2)
 
    # Calculate output resolution
    if float(orig_aspect) == 0 or orig_aspect == "":
        orig_aspect = float(orig_width)/float(orig_height)
    width = _basewidth
    height = int(round(_basewidth / float(orig_aspect) / 16) * 16)
    if (height > _maxheight):
        width = _basewidth43
        height = int(round(_basewidth43 / float(orig_aspect) / 16) * 16)
 
    return (width, height, fps)
 
 
def convert(input, output, res, abitrate, vbitrate, threads, mpopts):
    """Convert the Video"""
 
    # Needed for cleanup function
    global h264, aac
 
    # define some localvariables
    pid = os.getpid()
    h264 = output + ".h264"
    aac = output + ".aac"
    width = str(res[0])
    height = str(res[1])
    fps = str(res[2])
 
    if (vbitrate < 52):
        vbr = "crf=" + str(vbitrate)
    else:
        vbr = "bitrate=" + str(vbitrate)
 
    # Define mencoder command for video encoding
    mencvideo = [ mcbin,
            "-ovc", "x264",
            "-x264encopts", vbr + ":bframes=0:trellis=0:nocabac:no8x8dct:level_idc=30:frameref=4:me=umh:weightp=0:vbv_bufsize=2000:vbv_maxrate=1800:threads=" + str(threads),
            "-sws", "9",
            "-vf", "scale=" + width + ":" + height + ",dsize=" + width + ":" + height + ",fixpts=fps=" + fps + ",ass,fixpts,harddup",
            "-of", "rawvideo", 
            "-o", h264,
            "-nosound",
            "-noskip",
            "-ass",
            input ]
    if (mpopts != ""):
        for mpopt in mpopts.split(" "):
            mencvideo.append(mpopt)
 
    # Define mencoder command for audio encoding
    mencaudio = [ mcbin,
            "-oac", "faac",
            "-faacopts", "br=" + str(abitrate) + ":mpeg=4:object=2",
            "-ovc", "copy",
            "-mc", "0",
            "-vf", "softskip",
            "-of", "rawaudio",
            "-o", aac,
            "-noskip",
            input ]
    if (mpopts != ""):
        for mpopt in mpopts.split(" "):
            mencaudio.append(mpopt)
 
 
    # Define MB4Box Muxing Command
    mp4mux = [ m4bin,
            "-fps", fps,
            "-new", "-add", h264,
            "-add", aac,
            output ]
 
    # Encode Video
    print "### Starting Video Encode ###"
    try:
        subprocess.check_call(mencvideo)
        print "### Video Encoding Finished. ###"
    except subprocess.CalledProcessError:
        print "Error: Video Encoding Failed!"
        sys.exit(3)
 
    print "### Starting Audio Encode ###"
    try:
        subprocess.check_call(mencaudio)
        print "### Audio Encode Finished. ###"
    except subprocess.CalledProcessError:
        print "Error: Audio Encoding Failed!"
        sys.exit(3)
 
    # Mux Video and Audio to MP4
    print "### Starting MP4 Muxing ###"
    try:
        subprocess.check_call(mp4mux)
        print "### MP4 Muxing Finished. Video is now ready! ###"
    except subprocess.CalledProcessError:
        print "Error: Encoding thread failed!"
        sys.exit(4)
 
 
def progpath(program):
    """Get Full path for given Program"""
 
    for path in os.environ.get('PATH', '').split(':'):
        if os.path.exists(os.path.join(path, program)) and not os.path.isdir(os.path.join(path, program)):
            return os.path.join(path, program)
    return None
 
def cleanup():
    """Clean up when killed"""
 
    # Cleanup
    try:
        os.remove(h264)
        os.remove(aac)
    finally:
        sys.exit(0)
 
 
def usage():
    """Print avaiable commandline arguments"""
 
    print "This is n900-encode.py Version" + version + "(C) 2010 Stefan Brand <seiichiro0185 AT tol DOT ch>"
    print "Usage:"
    print "  n900-encode.py --input <file> [opts]\n"
    print "Options:"
    print "  --input <file>    [-i]: Video to Convert"
    print "  --output <file>   [-o]: Name of the converted Video"
    print "  --mpopts \"<opts>\" [-m]: Additional options for mplayer (eg -sid 1 or -aid 1) Must be enclosed in \"\""
    print "  --abitrate <br>   [-a]: Audio Bitrate in KBit/s"
    print "  --vbitrate <br>   [-v]: Video Bitrate in kBit/s, values < 52 activate h264 CRF-Encoding, given value is used as CRF Factor"
    print "  --threads <num>   [-t]: Use <num> Threads to encode, giving 0 will autodetect number of CPUs"
    print "  --force-overwrite [-f]: Overwrite output-file if existing"
    print "  --help            [-h]: Print this Help"
    sys.exit(0)
 
 
# Start the Main Function
if __name__ == "__main__":
    # Catch kill and clean up
    atexit.register(cleanup)
 
    signal(SIGTERM, lambda signum, stack_frame: exit(1))
    signal(SIGINT, lambda signum, stack_frame: exit(1))
 
    # Check min params and start if sufficient
    if len(sys.argv) > 1:
        main(sys.argv[1:])
    else:
        print "Error: You have to give an input file at least!"
        usage()

Simply use the download link above the code to get the script and put it into your PATH.

If you have questions or suggestions, or if you find bugs feel free to use the comment section below or drop me an email.