Commits

Mikhail Porokhovnichenko committed 4a0e560

bumped

  • Participants
  • Parent commits eb7b3aa

Comments (0)

Files changed (4)

 *.flv
 *.jpg
 *.jpeg
+*.tar.gz
+.env
 .settings
 .project
 .pydevproject
+dist
 
 
 setup.py
+conv.py
 #!/usr/bin/env python
+
+"""
+Video converter utilites wrappers collection
+"""
 # -*- coding: utf-8 -*- 
 
 import subprocess
 import re
 import math
 
+
+__all__ = ['escape_shell_arg', 'require_utility', 'get_video_data', 
+           'make_snapshot', 'inject_metadata']
+
+
 def escape_shell_arg(arg):
     """
     Escape a string to be used as a shell argument
     Adds single quotes around a string and quotes/escapes
     any existing single quotes allowing you to pass a string directly to a
     shell function and having it be treated as a single safe argument.
-    
+
     This function should be used to escape individual arguments to shell
     functions coming from user input. 
 
     """
     return '\'' + arg.replace('\'', '\'' + '\\' + '\'' + '\'') + '\''
 
+
+def check_utility(command):
+    """
+    Checks that `command` utility is exists and can be called.
+
+    This function makes pipe call with `command` with '--help' argument. If
+    this call raises OSError, we guess `command` not installed. 
+    """
+    try:
+        subprocess.check_call([command, '--help'], 
+            stdout = subprocess.PIPE,
+            stderr = subprocess.PIPE,
+        )
+    except OSError, e:
+        raise UtilityNotFound, 'Utility %s not found' % command
+    except subprocess.CalledProcessError, e:
+        pass
+
+
+def require_utility(*requirements):
+    """
+    Decorator that checks all required utilities is available
+    """
+    def wrapper(function):
+        """
+        Wrap function
+        """
+        def handler(*args, **kwargs):
+            """
+            Iterates for requirements and checks it            
+            """
+            for requirement in requirements:
+                check_utility(requirement)
+            return function(*args, **kwargs)
+        return handler
+    return wrapper
+
+
+def popen(command, handler=None, kwargs={}, miss_count=5):
+    """
+    Opens the pipe with external utility.
+
+    `command` - shell command that will be created in subprocess.Popen stream
+    or shell.
+
+    `handler` (not required) - the callable object that handles each line readed
+    from pipe. Handler accepts one required argument: stream line for parsing.
+    Other arguments will be passed as keyword arguments.
+
+    `kwargs` (not required) - dictionary with extra arguments that will
+    passed to handler as non-required input arguments.
+
+    `miss_count` - number of pipe read errors after that pipe will be closed.
+
+    """
+    stream, eof_count = subprocess.Popen(command,
+        shell  = True,
+        stdout = subprocess.PIPE,
+        stderr = subprocess.STDOUT,
+        universal_newlines = True
+    ).stdout, 0
+
+    while True:
+        line = stream.readline()
+
+        # Break infinite loop if EOF exceed
+        if len(line) < 1:
+            if eof_count > miss_count: break
+            else:
+                eof_count += 1;
+                continue
+        else:
+            eof_count = 0
+
+        if callable(handler):
+            handler(line, **kwargs)
+
+
+@require_utility('mplayer')
+def get_video_metadata(filename):
+    """
+    Returns a dictionary with video metadata.
+
+    Retrieves all available meta information about movie such as frame width,
+    frame height, bitrate, video- and audio codecs, container format,
+    duration and so on by `mplayer` command line interface: 
+   
+       $ mplayer -vo null -ao null -frames 0 -identify `filename`
+
+    It parses `mplayer` stdout and puts data into dictionary. If system shell
+    can't execute `mencoder`, the UtilityNotFound exception will
+    be raised.
+
+    """
+    infomap, metadata = (
+        # String marker          # Dictionary key     # Filters sequence
+        ('ID_AUDIO_CODEC',       'audio_codec',       [unicode]),
+        ('ID_AUDIO_FORMAT',      'audio_format',      [unicode]),
+        ('ID_AUDIO_BITRATE',     'audio_bitrate',     [int]),
+        ('ID_AUDIO_RATE',        'audio_rate',        [int]),
+        ('ID_AUDIO_NCH',         'audio_nch',         [int]),
+        ('ID_VIDEO_FORMAT',      'video_format',      [unicode]),
+        ('ID_VIDEO_BITRATE',     'video_bitrate',     [int]),
+        ('ID_VIDEO_ASPECT',      'video_aspect',      [float]),
+        ('ID_VIDEO_WIDTH',       'width',             [int]),
+        ('ID_VIDEO_HEIGHT',      'height',            [int]),         
+        ('ID_VIDEO_FPS',         'frame_rate',        [float]),
+        ('ID_LENGTH',            'duration',          [float, math.ceil, int]),
+        ('ID_CLIP_INFO_VALUE0',  'clip_info_value0',  [unicode]),
+        ('ID_VIDEO_ID',          'video_id',          [int]),
+        ('ID_AUDIO_ID',          'audio_id',          [int]),
+        ('ID_CLIP_INFO_N',       'clip_info_n',       [unicode]),
+        ('ID_FILENAME',          'filename',          [unicode]),
+        ('ID_DEMUXER',           'demuxer',           [unicode]),
+        ('ID_SEEKABLE',          'seekable',          [bool]),
+        ('ID_CHAPTERS',          'chapters',          [int]),
+        ('ID_VIDEO_CODEC',       'video_codec',       [unicode]),
+        ('ID_EXIT',              'exit',              [unicode]),
+    ), {}
+
+    def line_handler(line, metadata, infomap):
+        """
+        Stream line parser
+        """
+        if not line.startswith('ID_'):
+            return
+
+        for token, field, filters  in infomap:
+            if line.startswith(token):
+                key, val = line.strip().split('=', 2)
+                for f in filters:
+                    val = f(val)
+                metadata[field] = val
+
+    # Command for retrieving metadata from video
+    command = ' '.join(['mplayer',
+        '-vo null',
+        '-ao null',
+        '-frames 0',
+        '-identify',
+        escape_shell_arg(filename)]
+    )
+
+    # Parse the string
+    popen(command, line_handler, kwargs={
+        'infomap': infomap,
+        'metadata': metadata,
+    })
+
+    if len(metadata.keys()) < 2:
+        raise WrongVideoFormat, 'Can\'t parse metadata, maybe wrong format'
+    return metadata
+
+
+@require_utility('ffmpeg')
+def make_snapshot(video_filename, snapshot_filename, position): 
+    """
+    Makes a snapshot for `video_filename` at position `position` (in seconds)
+    and names it `snapshot_filename`.
+
+    This function is command line interface to ffmpeg utility. It opens such
+    shell command in python subprocess.Popen pipe:    
+
+     $ ffmpeg -ss `pos` -i `video` -an -vframes 1 -y -f mjpeg `snapshot`
+     
+    where `pos` - is position of video (in seconds from start), `video` - is
+    input video file, `snapshot` - snapshot filename.
+
+    IMPORTANT! The '-ss' argument must follow first '-i' (input file) 
+    argument, in other case ffmpeg will seek given position in whole file
+    and thus it can significantly slow process.
+
+    """
+    command = ' '.join(['ffmpeg',
+        '-ss {position}'.format(position=position),
+        '-i  {video}'.format(video=escape_shell_arg(video_filename)),
+        '-an -vframes 1 -y -f mjpeg',
+        escape_shell_arg(snapshot_filename),
+    ])
+    popen(command)
+
+    # If snapshot is broken (null length), delete it and returns False
+    if not os.path.getsize(snapshot_filename) > 0:
+        os.remove(snapshot_filename)
+        return False
+    return True
+
+
+@require_utility('yamdi')
+def inject_metadata(input_filename, output_filename):
+    command = ' '.join(['yamdi',
+        '-i {input}'.format(input=escape_shell_arg(input_filename)),
+        '-o {output}'.format(output=escape_shell_arg(output_filename)),
+        ])
+    popen(command)
+
+
 class UtilityNotFound(Exception):
     """
     Exception that raises when required utility is not found in $PATH
     """
 
+
 class CodecNotFound(Exception):
     """
     Exception that raises when trying convert video within unknown codec
     """
 
+
 class WrongCommandLine(Exception):
     """
     Exception that raises when trying convert video within unknown codec
     """
 
+
+class WrongVideoFormat(Exception):
+    """
+    Exception that raises when can't retrieve video metadata
+    """
+
+
 class ConvertResult(object):
     """
     Result object
     # Converted movie snapshots
     snapshots = {}
 
-class Converter(object):
+
+class BaseConverter(object):
     """
-    The mencoder API wrapper class
+    Base converter class
     """
-    # Default output movie frame width
-    width  = None
+    width = 640
+    height = 480
 
-    # Default output movie frame height
-    height = None
-
-    # Default output movie sample rate
     sample_rate = 22050
-
-    # Default output movie containter
     output_format = 'lavf'
-
-    # Default output movie video codec
     video_codec = 'lavc'
-
-    # Default video codec options
     video_opts = None
-
-    # Default video codec options prefix
     video_opts_prefix = ''   
-
-    # Default output movie audio codec
     audio_codec = 'lavc'
-
-    # Default audio codec options
     audio_opts = None
-
-    # Default audio codec options prefix
     audio_opts_prefix = ''
 
-    # Path to `mencoder` binary
-    MENCODER_CMD = 'mencoder'
-
-    # Path to `mplayer` binary
-    MPLAYER_CMD = 'mplayer'
-
-    # Path to `yamdi` binary
-    YAMDI_CMD = 'yamdi'
-
-    # Path to `ffmpeg` binary
-    FFMPEG_CMD = 'ffmpeg'
-
-    # Path to `ffmpeg2theora`
-    FFMPEGTOTHEORA_CMD = 'ffmpeg2theora'
-
     def __init__(self, *args, **kwargs):
         """
         The class constructor
         """
-        self.MENCODER_CMD = kwargs.get('mencoder') or self.MENCODER_CMD 
-        self.MPLAYER_CMD  = kwargs.get('mplayer')  or self.MPLAYER_CMD
-        self.YAMDI_CMD    = kwargs.get('yamdi')    or self.YAMDI_CMD
-        self.FFMPEG_CMD   = kwargs.get('ffmpeg')   or self.FFMPEG_CMD
-        self.FFMPEGTOTHEORA_CMD = kwargs.get('ffmpeg2theora') or self.FFMPEGTOTHEORA_CMD
-
         self.width  = kwargs.get('width') or self.width
         self.height = kwargs.get('height') or self.height 
 
-        self.test_env()
+    def convert(self, input_file, output_file, **kwargs):
+        """
+        Start the converting process 
+        """
+        result = ConvertResult()
+ 
+        result.movie_original = input_file
+        result.movie_info = self.get_movie_info(input_file)
+        result.converted = self.encode(input_file, output_file, **kwargs)
 
-    def __repr__(self):
+        if result.converted:
+            result.movie_converted = output_file            
+            self.make_snapshot(result)
+            self.inject_metadata(output_file)
+        return result
+
+    def encode(self, input_file, output_file, **kwargs):
         """
-        Text represent
         """
-        return 'Converter into {video} codec'.format(video=self.video_codec)
+        if hasattr(self, 'parse_converter_output'):
+            self.parse_converter_output(input_file, output_file, **kwargs)
 
-    def test_env(self):
+        command = self.get_encode_command(input_file, output_file)
+        popen(command)
+
+        return True
+
+    def snapshot_filename(self, result, second):
         """
-        Check the environment:
-         
-         - tries to call required utilites and raise exception if test fails;
-         - checks that given codec is available or raises
-          
+        Returns snapshot filename
         """
-        commands = [
-            self.MENCODER_CMD, 
-            self.MPLAYER_CMD, 
-            self.YAMDI_CMD,
-            self.FFMPEG_CMD,
-            self.FFMPEGTOTHEORA_CMD,
-        ]
+        return '{video_file}_{second}.jpeg'.format(
+            video_file = result.movie_converted,
+            second     = second,
+        )
 
-        for cmd in commands:
-            try:
-                subprocess.check_call([cmd, '--help'], 
-                    stdout = subprocess.PIPE,
-                    stderr = subprocess.PIPE,
-                )
+    def make_snapshot(self, result, num=10): 
+        """
+        Make snapshots
+        """
+        # Gets movie duration in seconds
+        duration = result.movie_info['duration'] 
 
-            except OSError, e:
-                raise UtilityNotFound, 'Utility %s not found' % cmd
+        # Gets step (in integer seconds) between two snaphots
+        step = float(duration / num)
+        step = math.floor(step)
 
-            except subprocess.CalledProcessError, e:
-                pass
+        # Gets list of target positions (in seconds)
+        seconds = xrange(1, duration - 1, step)
+        seconds = list(seconds)
+        seconds.append(duration)
+        seconds.append(math.floor(float(duration) / 2) )
 
-        # @TODO Check codec
+        for position in seconds:
+            position = int(position)
 
+            thumb_filename = self.snapshot_filename(result, position)
+
+            if thumb_filename is None:
+                continue
+
+            if make_snapshot(result.movie_converted, thumb_filename, position):
+                result.snapshots[position] = thumb_filename
+
+    def get_movie_info(self, input_file):
+        """
+        Retrieve all metadata such as frame width, height, rate, audio and
+        video codecs, duration and so on.
+        """
+        return get_video_metadata(input_file)
+
+    def inject_metadata(self, output_file):
+        """
+        Inject metadata into converted movie
+        """
+        inject_metadata(output_file, output_file + '.yamdi')       
+        os.remove(output_file)
+        os.rename('%s.yamdi' % output_file, output_file)
+
+    def process_handler(self, old, new):
+        ''
+        print '%s\t->\t%s' % (old, new)
+
+
+class MencoderConverter(BaseConverter):
+    """
+    MEncoder converter base class
+    """
     def get_encode_command(self, input_file, output_file):
         """
         Return `mencoder` console command to convert movie
 
             return ('-vf '+','.join(bits)) if bits else ''
 
-        cmd = ' '.join([self.MENCODER_CMD,
+        cmd = ' '.join(['mencoder',
             input_filename(),    # Input filename
             output_filename(),   # Output filename (-o argument)
             container(),         # Format container (-of argument)
         print cmd
         return cmd
 
-    def get_movie_info(self, input_file):
+    def parse_converter_output(self, input_file, output_file, **kwargs):
         """
-        Retrieve all metadata such as frame width, height, rate, audio and
-        video codecs, duration and so on.
+        Track mencoder converting progress
         """
-        info_map, meta_data = (
-            ('ID_AUDIO_CODEC',       'audio_codec',       [unicode]),
-            ('ID_AUDIO_FORMAT',      'audio_format',      [unicode]),
-            ('ID_AUDIO_BITRATE',     'audio_bitrate',     [int]),
-            ('ID_AUDIO_RATE',        'audio_rate',        [int]),
-            ('ID_AUDIO_NCH',         'audio_nch',         [int]),
-            ('ID_VIDEO_FORMAT',      'video_format',      [unicode]),
-            ('ID_VIDEO_BITRATE',     'video_bitrate',     [int]),
-            ('ID_VIDEO_ASPECT',      'video_aspect',      [float]),
-            ('ID_VIDEO_WIDTH',       'width',             [int]),
-            ('ID_VIDEO_HEIGHT',      'height',            [int]),         
-            ('ID_VIDEO_FPS',         'frame_rate',        [float]),
-            ('ID_LENGTH',            'duration',          [float, math.ceil, int]),
-            ('ID_CLIP_INFO_VALUE0',  'clip_info_value0',  [unicode]),
-            ('ID_VIDEO_ID',          'video_id',          [int]),
-            ('ID_AUDIO_ID',          'audio_id',          [int]),
-            ('ID_CLIP_INFO_N',       'clip_info_n',       [unicode]),
-            ('ID_FILENAME',          'filename',          [unicode]),
-            ('ID_DEMUXER',           'demuxer',           [unicode]),
-            ('ID_SEEKABLE',          'seekable',          [bool]),
-            ('ID_CHAPTERS',          'chapters',          [int]),
-            ('ID_VIDEO_CODEC',       'video_codec',       [unicode]),
-            ('ID_EXIT',              'exit',              [unicode]),
-        ), {}
+        command = self.get_encode_command(input_file, output_file)
+        percent = {'percent':0}
 
-        # mplayer -vo null -ao null -frames 0 -identify %s' % file
-        stream = subprocess.Popen(
-            ' '.join([self.MPLAYER_CMD,
-            '-vo null',
-            '-ao null',
-            '-frames 0',
-            '-identify',
-            '"{file}"'.format(file=input_file),
-            ]),
-            shell  = True,
-            stdout = subprocess.PIPE,
-            stderr = subprocess.PIPE,
-            universal_newlines = True
-        ).stdout   
-
-        while True:
-            line = stream.readline()
-
-            if not line:
-                break
-
-            if not line.startswith('ID_'):
-                continue
-
-            # Meta info
-            for token, field, filters  in info_map:
-                if line.startswith(token):
-                    key, val = line.strip().split('=', 2)
-
-                    for f in filters:
-                        val = f(val)
-                    meta_data[field] = val
-                # if
-            # for
-        # while
-        return meta_data
-
-    def snapshot_filename(self, result, second):
-        """
-        Returns snapshot filename
-        """
-        return '{video_file}_{second}.jpeg'.format(
-            video_file = result.movie_converted,
-            second     = second,
-        )
-
-    def make_snapshot(self, result, num=10): 
-        """
-        Make snapshots
-        """
-        # ffmpeg -i out.flv -an -ss 5 -r 1 -vframes 1 -s 320x240 -y -f mjpeg test.jpg
-        duration = result.movie_info['duration'] 
-        width    = result.movie_info['width']
-        height   = result.movie_info['height']
-
-        seconds = list(xrange(1, duration, int(math.floor(float(duration / num)))))
-        seconds.append(duration)
-        seconds.append( math.ceil(float(duration) /2) )
-
-        for s in seconds:
-            s = int(s)
-
-            snapshot_file = '{video_file}_{second}.jpeg'.format(
-                video_file = result.movie_converted,
-                second = s,
-            )
-
-            thumb_filename = self.snapshot_filename(result, s)
-
-            if thumb_filename is None:
-                continue
-
-            stream = subprocess.Popen(' '.join(
-                [
-                    self.FFMPEG_CMD,
-                    '-i {file}'.format(file=escape_shell_arg(result.movie_converted)),
-                    '-an',
-                    '-ss {second}'.format(second=s),
-                    '-vframes 1',
-                    '-y',
-                    '-f mjpeg',
-                    '-s {width}x{height}'.format(width=width,height=height),
-                    escape_shell_arg(thumb_filename),
-                ]),
-                shell = True,
-                stdout = subprocess.PIPE,
-                stderr = subprocess.PIPE,
-                universal_newlines = True             
-            )
-
-            while True:
-                line = stream.stdout.read()
-                if not line:
-                    break
-                print line
-
-            # Avoid empty snapshots
-            if os.path.getsize(snapshot_file) > 0:
-                result.snapshots[s] = snapshot_file
-            else:
-                os.remove(snapshot_file)
-
-    # mplayer out.flv -ss 00:00:17 -frames 1 -ao null -vo png 
-    def convert(self, input_file, output_file, **kwargs):
-        """
-        Convertion entry point 
-        """
-        result = ConvertResult()
-
-        # 
-        result.movie_original = input_file
-        result.movie_info = self.get_movie_info(input_file)
-        result.converted = self.encode(input_file, output_file, **kwargs)
-
-        if result.converted:
-            result.movie_converted = output_file            
-
-            self.make_snapshot(result)
-            self.inject_metadata(output_file)
-
-        return result
-
-    def inject_metadata(self, output_file):
-        """
-        Inject metadata into converted movie
-        """
-        stream  = subprocess.Popen(
-            ' '.join([self.YAMDI_CMD,
-            '-i %s' % escape_shell_arg(output_file),
-            '-o %s' % escape_shell_arg(output_file + '.yamdi'),
-            ]),
-            shell  = True,
-            stdout = subprocess.PIPE,
-            stderr = subprocess.PIPE,
-            universal_newlines = True
-        )
-        while True:
-            line = stream.stdout.readline()
-            if not line:
-                 break
-            print line
-        
-        os.remove(output_file)
-        os.rename('%s.yamdi' % output_file, output_file)
-            
-    def encode(self, input_file, output_file, **kwargs):
-        """
-        Movie encoding with state tracking
-        """
-        re_progress = re.compile('^Pos:.*?\(\s*?(\d+)%\)', re.U)
-        command = self.get_encode_command(input_file, output_file)
-
-        stdout, percent, eof_count = subprocess.Popen(command,
-            shell  = True, 
-            stdout = subprocess.PIPE, 
-            stderr = subprocess.STDOUT,
-            universal_newlines = True
-        ).stdout, 0, 0
-
-        while True:
-            line = stdout.readline()[:-1]
-
-            # Break infinite loop if EOF exceed
-            if len(line) < 1:
-                if eof_count > 5:
-                    break
-                else:
-                    eof_count += 1
-                    continue
-            else:
-                eof_count = 0
-
+        def line_handler(line, percent, process_handler):
             if 'Error parsing option on the command line' in line:
                 raise WrongCommandLine, 'Parse error in command:\n%s\n' % command
 
             # Retrieve current progress position
-            pos_match = re_progress.match(line)
-
+            pos_match = re.compile('^Pos:.*?\(\s*?(\d+)%\)', re.U).match(line)
+            
             if pos_match:
                 curr = pos_match.group(1)
+            
+                if percent['percent'] != curr:
+                    process_handler(int(percent['percent']), int(curr))
+                    percent['percent'] = curr
 
-                if percent != curr:
-                    self.process_handler(int(percent), int(curr))
-                    percent = curr
+        popen(command, line_handler, kwargs={
+            'percent': percent,
+            'process_handler': self.process_handler,
+        })
 
-        # End of loop
         return True
 
-    def process_handler(self, old_percent, new_percent):
+
+class FFMpegConverter(BaseConverter):
+    """
+    FFMpeg converter base class
+    """
+    def parse_converter_output(self, input_file, output_file, **kwargs):
         """
-        Convertion hook. Call on every percent change
+        Track ffmpeg converting progress
         """
-        print "Change state: %s -> %s" % (old_percent, new_percent)
+        command = self.get_encode_command(input_file, output_file)
+        percent = {'percent': 0, 'duration': 0}
 
-class H263Converter(Converter):
+        def line_handler(line, re_duration, re_progress, percent):
+            """
+            Handles the ffmpeg stream lines
+            """
+            def time2sec(match):
+                """
+                Converts time pisition into seconds
+                """
+                g = match.group
+                return 3600 * int(g(1)) + 60 * int(g(2)) + int(g(3)) 
+
+            # Retrieve current progress position
+            duration_match = re_duration.match(line)
+            progress_match = re_progress.match(line)
+
+            if duration_match: 
+                duration = time2sec(duration_match) 
+ 
+            if progress_match:
+                curr = 0
+
+                if duration:
+                    curr = int(math.floor(time2sec(progress_match)*100/duration))
+
+                if percent['percent'] != curr:
+                    self.process_handler(int(percent['percent']), int(curr))
+                    percent['percent'] = curr
+        # End of loop
+        popen('', line_hander, {
+            're_duration' : re.compile('^Duration: (d{2}):(d{2}):(d{2}).(d{2})$', re.U),
+            're_progress' : re.compile('time=(d{2}):(d{2}):(d{2})\.(\d{2})', re.U),
+            'percent'     : percent,
+        })  
+
+
+class H263Converter(MencoderConverter):
     """
     The LAVC codec
     """
     audio_opts  = 'abr:br=64'
     audio_opts_prefix = 'lameopts'
 
-class H264Converter(Converter):
+class H264Converter(MencoderConverter):
     """
     The X264 (Open h264 implementation) codec
     """    
-    # mencoder 1.mpg -o 2.flv -of lavf -ovc x264 -oac mp3lame -srate 44100
-    video_codec = 'x264'
+    video_codec = 'x264'    
     video_opts = 'vcodec=x264:vbitrate=288:mbd=2:mv0:trell:v4mv:cbp:last_pred=3:predia=2:dia=2:vmax_b_frames=0:vb_strategy=1:precmp=2:cmp=2:subcmp=2:preme=2:qns=2'
     video_opts = ''
     video_opts_prefix = 'x264encopts'
 from distutils.core import setup
 import os
 
-version='0.4.3'
+version='0.4.4'
 package = 'video_converter'
 
 setup(