#!/usr/bin/python

# Copyright 2003 Tom Rothamel <tom-potw@rothamel.us>
# 
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


# Attempts to (smartly) turn a video file into an mpeg that is suitable
# for use in a DVD player.

# The input is expected to be encoded at either 29.97 fps or at 23.976
# fps. These are the NTSC tv and movie framerates, respectively.

# This requires that both transcode and mplayer are installed on the
# system. The latter is only needed because I have no idea how to use
# just transcode to detect pulldown in mpeg video.

import optparse
import os
import re
import signal
import sys

# A map from a possible frame size name to a tuple containing width, height,
# optional x-border, and optional y-border.

sizes = {
    'full' : ( 704, 480, 48, 80 ),
    'half' : ( 352, 480, 24, 80 ),
    'quarter' : ( 352, 240, 24, 40 ),
    }


def probe(input):
    """This probes the input video, and returns a tuple of things about
    it. In order, the members of this tuple are:

    pulldown -- Is pulldown needed?
    aspect -- The aspect ratio. Either '4:3' or '16:9', defaulting to the
    former.
    length -- The length of the movie in frames, or None if it could not
    be easily determined.
    """

    # We first use tcprobe to try and guess the aspect ratio and
    # framerate of the input file.

    pulldown = False
    aspect = "4:3"
    length = None

    f = os.popen("tcprobe -i '%s'" % input, "r")
    for l in f:

        # Frame size (guess aspect ratio.)
        m = re.search(r"-g (\d+)x(\d+)", l)
        if m:
            x = float(m.group(1))
            y = float(m.group(2))

            if x / y > 1.7:
                aspect = "16:9"
            else:
                aspect = "4:3"

        # Explicit aspect ratio.
        m = re.search(r"aspect ratio: ([\d:]+)", l)
        if m:
            aspect = m.group(1)

        # If the framerate is less than 25 fps, enable pulldown.
        m = re.search(r"-f ([\d\.]+)", l)
        if m:
            framerate = float(m.group(1))
            print "Detected framerate is:", framerate, "fps"
            if framerate < 25.0:
                pulldown = True

        # Try to find the length of the movie.
        m = re.search(r"length: (\d+) frames", l)
        if m:
            length = int(m.group(1))

    f.close()

    # The problem with that previous test is that tcprobe sometimes lies.
    # Mpeg video may have pulldown, the repetition of frames. This results
    # in a 24fps mpeg claiming to be 30fps. To fix this, we play 60 frames
    # of the video through mplayer, and see if it mentions detecting
    # telecine.

    f = os.popen("mplayer -vo null -ao null '%s' -frames 60 2>/dev/null", "r")
    for l in f:        
        if re.search(r"TELECINE", l):
            if not pulldown:
                print "TELECINE detected, enabling pulldown."
                pulldown = True
            
    f.close()

    return pulldown, aspect, length

# Displays a command on the screen, with all arguments escaped.
def quote_command(args):
    l = ""

    for w in ['"%s"' % i.replace("\\", "\\\\").replace('"', '\\"')
              for i in args]:

        if len(l + " " + w) > 78:
            print l[1:], "\\"
            l = " " + w
        else:
            l = l + " " + w

    print l[1:]

# Runs the command with the given list of arguments.
def run_command(args):
    print "Running %s." % args[0]

    # This is ugly. We have to go and remove the sigpipe handler
    # that python puts in for some reason.

    # Grr... Took me a few hours to find this one.

    pid = os.fork()

    if not pid:
        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
        os.execvp(args[0], args)
        os._exit(-1)

    pid, rv = os.waitpid(pid, 0)

    return rv
    
    
def main(): 

    # Parse the options.
    op = optparse.OptionParser(usage="%prog [options] <from> <to> [ -- <transcode options> ]",
                               version="%prog 1")

    op.add_option("--full", action="store_const", dest="size", const="full",
                  default="full",
                  help="encode full size frames (704x480)")
    op.add_option("--half", action="store_const", dest="size", const="half",
                  help="encode half size frames (352x480)")
    op.add_option("--quarter", action="store_const", dest="size", const="quarter",
                  help="encode quarter size frames (352x240)")
    op.add_option("--border", "--anime", action="store_true", dest="border", default=False,
                  help="add a border to the outside of the movie.")

    options, args = op.parse_args()

    if len(args) < 2:
        op.error("At least two arguments are required.")

    input = args[0]
    output = args[1]
    transcode_args = args[2:]

    # The name in tmp.
    tmp = "/tmp/encoding"

    # Probe for some things.
    pulldown, aspect, count = probe(input)

    # This contains the arguments to transcode that we are
    # building up.

    tcargs = [ "transcode",
               "-i", input,               # The input file.
               "-o", tmp,                 # The root name of the output files.
               "-y", "mpeg2enc,mp2enc",   # mpeg2 and mp2.
               "-F", "8",                 # Dvd video.
               "-w", "8000",              # Dvd bitrate.
               "-V",                      # Work in YUV, for a speedup.
               "-E", "48000",             # Dvd audio.
               ]

    # Ditto, for tcmplex.

    tmargs = [ "tcmplex",
               "-o", output,              # The output.
               "-m", "d",                 # DVD mode.
               "-i", tmp + ".m2v",        # The encoded mpeg video.
               "-p", tmp + ".mpa",        # The encoded mpeg audio.
               ]
               
    # Look up the sizes.
    xsize, ysize, xborder, yborder = sizes[options.size]

    # If we need a border, subtract it from the x and y sizes.
    if options.border:
        xsize -= xborder
        ysize -= yborder

    print "Encoding to size: %dx%d" % (xsize, ysize)
    tcargs += [ "-Z", "%dx%d,fast" % (xsize, ysize) ]

    # Add --pulldown, if needed.
    if pulldown:
        tcargs += [ "--pulldown" ]

        if options.size == "quarter":
            op.error("Can't have pulldown and a quarter-size image. Try --half.")

    # Add aspect ratio, if known.

    # A map from aspect ration to number in transcode.
    asrs = { '16:9' : '3', '4:3' : '2' }

    if aspect in asrs:
        tcargs += [ "--export_asr", asrs[aspect] ]

    # Add frame count, if known.
    if count:
        tcargs += [ "-c", "1-%d" % count ]

    # Add border, if necessary.
    if options.border:
        vborder = -(yborder/2)
        hborder = -(xborder/2)

        tcargs += [ "-Y", "%d,%d" % (vborder, hborder) ]
        

    # Add additional transcode arguments.
    tcargs += transcode_args

    # Show the various commands.
    print
    print "Transcode Command:"
    quote_command(tcargs)
    print
    print "Tcmplex Command:"
    quote_command(tmargs)
    print 

    os.nice(10)

    # Run the commands.
    ec = run_command(tcargs)

    if not ec:
        run_command(tmargs)

if __name__ == "__main__":
    main()
