1 #!/usr/bin/python
    2 
    3 # Copyright 2003 Tom Rothamel <tom-potw@rothamel.us>
    4 # 
    5 # Permission is hereby granted, free of charge, to any person
    6 # obtaining a copy of this software and associated documentation files
    7 # (the "Software"), to deal in the Software without restriction,
    8 # including without limitation the rights to use, copy, modify, merge,
    9 # publish, distribute, sublicense, and/or sell copies of the Software,
   10 # and to permit persons to whom the Software is furnished to do so,
   11 # subject to the following conditions:
   12 # 
   13 # The above copyright notice and this permission notice shall be
   14 # included in all copies or substantial portions of the Software.
   15 # 
   16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
   17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
   18 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
   19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
   20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
   21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
   22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   23 
   24 
   25 # Attempts to (smartly) turn a video file into an mpeg that is suitable
   26 # for use in a DVD player.
   27 
   28 # The input is expected to be encoded at either 29.97 fps or at 23.976
   29 # fps. These are the NTSC tv and movie framerates, respectively.
   30 
   31 # This requires that both transcode and mplayer are installed on the
   32 # system. The latter is only needed because I have no idea how to use
   33 # just transcode to detect pulldown in mpeg video.
   34 
   35 import optparse
   36 import os
   37 import re
   38 import signal
   39 import sys
   40 
   41 # A map from a possible frame size name to a tuple containing width, height,
   42 # optional x-border, and optional y-border.
   43 
   44 sizes = {
   45     'full' : ( 704, 480, 48, 80 ),
   46     'half' : ( 352, 480, 24, 80 ),
   47     'quarter' : ( 352, 240, 24, 40 ),
   48     }
   49 
   50 
   51 def probe(input):
   52     """This probes the input video, and returns a tuple of things about
   53     it. In order, the members of this tuple are:
   54 
   55     pulldown -- Is pulldown needed?
   56     aspect -- The aspect ratio. Either '4:3' or '16:9', defaulting to the
   57     former.
   58     length -- The length of the movie in frames, or None if it could not
   59     be easily determined.
   60     """
   61 
   62     # We first use tcprobe to try and guess the aspect ratio and
   63     # framerate of the input file.
   64 
   65     pulldown = False
   66     aspect = "4:3"
   67     length = None
   68 
   69     f = os.popen("tcprobe -i '%s'" % input, "r")
   70     for l in f:
   71 
   72         # Frame size (guess aspect ratio.)
   73         m = re.search(r"-g (\d+)x(\d+)", l)
   74         if m:
   75             x = float(m.group(1))
   76             y = float(m.group(2))
   77 
   78             if x / y > 1.7:
   79                 aspect = "16:9"
   80             else:
   81                 aspect = "4:3"
   82 
   83         # Explicit aspect ratio.
   84         m = re.search(r"aspect ratio: ([\d:]+)", l)
   85         if m:
   86             aspect = m.group(1)
   87 
   88         # If the framerate is less than 25 fps, enable pulldown.
   89         m = re.search(r"-f ([\d\.]+)", l)
   90         if m:
   91             framerate = float(m.group(1))
   92             print "Detected framerate is:", framerate, "fps"
   93             if framerate < 25.0:
   94                 pulldown = True
   95 
   96         # Try to find the length of the movie.
   97         m = re.search(r"length: (\d+) frames", l)
   98         if m:
   99             length = int(m.group(1))
  100 
  101     f.close()
  102 
  103     # The problem with that previous test is that tcprobe sometimes lies.
  104     # Mpeg video may have pulldown, the repetition of frames. This results
  105     # in a 24fps mpeg claiming to be 30fps. To fix this, we play 60 frames
  106     # of the video through mplayer, and see if it mentions detecting
  107     # telecine.
  108 
  109     f = os.popen("mplayer -vo null -ao null '%s' -frames 60 2>/dev/null", "r")
  110     for l in f:        
  111         if re.search(r"TELECINE", l):
  112             if not pulldown:
  113                 print "TELECINE detected, enabling pulldown."
  114                 pulldown = True
  115             
  116     f.close()
  117 
  118     return pulldown, aspect, length
  119 
  120 # Displays a command on the screen, with all arguments escaped.
  121 def quote_command(args):
  122     l = ""
  123 
  124     for w in ['"%s"' % i.replace("\\", "\\\\").replace('"', '\\"')
  125               for i in args]:
  126 
  127         if len(l + " " + w) > 78:
  128             print l[1:], "\\"
  129             l = " " + w
  130         else:
  131             l = l + " " + w
  132 
  133     print l[1:]
  134 
  135 # Runs the command with the given list of arguments.
  136 def run_command(args):
  137     print "Running %s." % args[0]
  138 
  139     # This is ugly. We have to go and remove the sigpipe handler
  140     # that python puts in for some reason.
  141 
  142     # Grr... Took me a few hours to find this one.
  143 
  144     pid = os.fork()
  145 
  146     if not pid:
  147         signal.signal(signal.SIGPIPE, signal.SIG_DFL)
  148         os.execvp(args[0], args)
  149         os._exit(-1)
  150 
  151     pid, rv = os.waitpid(pid, 0)
  152 
  153     return rv
  154     
  155     
  156 def main(): 
  157 
  158     # Parse the options.
  159     op = optparse.OptionParser(usage="%prog [options] <from> <to> [ -- <transcode options> ]",
  160                                version="%prog 1")
  161 
  162     op.add_option("--full", action="store_const", dest="size", const="full",
  163                   default="full",
  164                   help="encode full size frames (704x480)")
  165     op.add_option("--half", action="store_const", dest="size", const="half",
  166                   help="encode half size frames (352x480)")
  167     op.add_option("--quarter", action="store_const", dest="size", const="quarter",
  168                   help="encode quarter size frames (352x240)")
  169     op.add_option("--border", "--anime", action="store_true", dest="border", default=False,
  170                   help="add a border to the outside of the movie.")
  171 
  172     options, args = op.parse_args()
  173 
  174     if len(args) < 2:
  175         op.error("At least two arguments are required.")
  176 
  177     input = args[0]
  178     output = args[1]
  179     transcode_args = args[2:]
  180 
  181     # The name in tmp.
  182     tmp = "/tmp/encoding"
  183 
  184     # Probe for some things.
  185     pulldown, aspect, count = probe(input)
  186 
  187     # This contains the arguments to transcode that we are
  188     # building up.
  189 
  190     tcargs = [ "transcode",
  191                "-i", input,               # The input file.
  192                "-o", tmp,                 # The root name of the output files.
  193                "-y", "mpeg2enc,mp2enc",   # mpeg2 and mp2.
  194                "-F", "8",                 # Dvd video.
  195                "-w", "8000",              # Dvd bitrate.
  196                "-V",                      # Work in YUV, for a speedup.
  197                "-E", "48000",             # Dvd audio.
  198                ]
  199 
  200     # Ditto, for tcmplex.
  201 
  202     tmargs = [ "tcmplex",
  203                "-o", output,              # The output.
  204                "-m", "d",                 # DVD mode.
  205                "-i", tmp + ".m2v",        # The encoded mpeg video.
  206                "-p", tmp + ".mpa",        # The encoded mpeg audio.
  207                ]
  208                
  209     # Look up the sizes.
  210     xsize, ysize, xborder, yborder = sizes[options.size]
  211 
  212     # If we need a border, subtract it from the x and y sizes.
  213     if options.border:
  214         xsize -= xborder
  215         ysize -= yborder
  216 
  217     print "Encoding to size: %dx%d" % (xsize, ysize)
  218     tcargs += [ "-Z", "%dx%d,fast" % (xsize, ysize) ]
  219 
  220     # Add --pulldown, if needed.
  221     if pulldown:
  222         tcargs += [ "--pulldown" ]
  223 
  224         if options.size == "quarter":
  225             op.error("Can't have pulldown and a quarter-size image. Try --half.")
  226 
  227     # Add aspect ratio, if known.
  228 
  229     # A map from aspect ration to number in transcode.
  230     asrs = { '16:9' : '3', '4:3' : '2' }
  231 
  232     if aspect in asrs:
  233         tcargs += [ "--export_asr", asrs[aspect] ]
  234 
  235     # Add frame count, if known.
  236     if count:
  237         tcargs += [ "-c", "1-%d" % count ]
  238 
  239     # Add border, if necessary.
  240     if options.border:
  241         vborder = -(yborder/2)
  242         hborder = -(xborder/2)
  243 
  244         tcargs += [ "-Y", "%d,%d" % (vborder, hborder) ]
  245         
  246 
  247     # Add additional transcode arguments.
  248     tcargs += transcode_args
  249 
  250     # Show the various commands.
  251     print
  252     print "Transcode Command:"
  253     quote_command(tcargs)
  254     print
  255     print "Tcmplex Command:"
  256     quote_command(tmargs)
  257     print 
  258 
  259     os.nice(10)
  260 
  261     # Run the commands.
  262     ec = run_command(tcargs)
  263 
  264     if not ec:
  265         run_command(tmargs)
  266 
  267 if __name__ == "__main__":
  268     main()
  269