CIS FFmpeg API implemented
authorCalin-Andrei Burloiu <calin.burloiu@gmail.com>
Thu, 17 Nov 2011 12:34:51 +0000 (14:34 +0200)
committerCalin-Andrei Burloiu <calin.burloiu@gmail.com>
Thu, 17 Nov 2011 12:34:51 +0000 (14:34 +0200)
.gitignore
cis/api/base.py
cis/api/ffmpeg.py
cis/cis_exceptions.py
cis/cisd.py

index b7df4ee..110f02a 100644 (file)
@@ -33,3 +33,4 @@ data/thumbs/*
 data/torrents/*
 data/user_pictures/*
 data/media/*
+cis/log/*
index 3e5f508..96006f8 100644 (file)
@@ -5,14 +5,18 @@ Base classes for the external programs API.
 """
 
 import cis_exceptions
+import re
 
 class BaseTranscoder:
     """
-    Abstractization of the API class for the transcoder program. 
+    Abstraction of the API class for the transcoder program. 
     """
 
     prog_bin = None
     input_file = None
+    output_file = None
+    dest_path = ''
+    name = None
 
     # Recommended formats.
     containers = {
@@ -23,6 +27,16 @@ class BaseTranscoder:
         "webm": None,
         "mpegts": None
     }
+    # File extensions by container. First value is for audio files and the
+    # second one is for (audio-)video files.
+    extensions = {
+        "avi": ["avi", "avi"],
+        "flv": ["flv", "flv"],
+        "mp4": ["mp4", "mp4"],
+        "ogg": ["oga", "ogv"],
+        "webm": ["webm", "webm"],
+        "mpegts": ["mts", "mts"]
+    }
     a_codecs = {
         "mp3": None,
         "vorbis": None
@@ -33,35 +47,122 @@ class BaseTranscoder:
         "vp8": None
     }
 
-    def __init__(self, input_file, prog_bin=None):
+    def __init__(self, input_file, name=None, prog_bin=None):
         self.input_file = input_file
-        self.prog_bin = prog_bin
+        if prog_bin is not None:
+            self.prog_bin = prog_bin
+        
+        if name is None:
+            if input_file.find('/') is not -1:
+                name = input_file[(input_file.rindex('/')+1):]
+            else:
+                name = input_file
+            if name.find('.') is not -1:
+                name = name[:name.rindex('.')]
+
+        self.name = name
 
-    def transcode(self, container, a_codec, v_codec,
+    def transcode(self, container, a_codec=None, v_codec=None,
             a_bitrate=None, a_samplingrate=None, a_channels=None,
-            v_bitrate=None, v_fraterate=None, v_resolution=None, v_dar=None):
+            v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
         """
         Transcodes the input file to an audio-video file.
 
         container: possible values are listed in containers member as keys
         a_codec: possible values are listed in a_codecs member as keys
         v_codec: possible values are listed in v_codecs member as keys
+        a_bitrate: (numeric) audio bit rate
+        a_samplingrate: (numeric) audio sampling rate in Hz
+        a_channels: (numeric) number of audio channels
+        v_bitrate: (numeric) video bit rate
+        v_framerate: (numeric) number of frames per second for a video
+        v_resolution: (string) video image size as <width>x<height>
+        v_dar: video display aspect ratio as <den>x<num> or float
         """
 
-        pass
+        # Check parameters.
+        if a_codec is None and v_codec is None:
+            raise ValueError('No audio or video codec specified.')
 
-    def transcode_audio(self, container, a_codec,
-            a_bitrate=None, a_samplingrate=None, a_channels=None):
-        pass
+        if a_codec is not None and type(a_codec) is not str:
+            raise TypeError('Audio codec must be string.')
+
+        if v_codec is not None and type(v_codec) is not str:
+            raise TypeError('Video codec must be string.')
+
+        if a_samplingrate is not None and type(a_samplingrate) is not int:
+            raise TypeError('Audio sampling rate must be an integer.')
+
+        if a_channels is not None and type(a_channels) is not int:
+            raise TypeError('Audio channels parameter must be an integer.')
 
-    def transcode_video(self, container, v_codec,
-            v_bitrate=None, v_fraterate=None, v_resolution=None, v_dar=None):
+        if v_framerate is not None and type(v_framerate) is not int:
+            raise TypeError('Video frate rate must be an integer.')
+
+        if v_resolution is not None \
+                and re.match('[\d]+x[\d]+', v_resolution) is None:
+            raise ValueError('Video resolution must be a string like <width>x<height>.')
+
+        if v_dar is not None and (type(v_dar) is not float \
+                and re.match('[\d]+:[\d]+', v_dar) is None):
+            raise ValueError('Video display aspect ratio must be a float or a string like <den>:<num>.')
+
+        self.output_file = self.dest_path + self.name
+        if v_resolution is not None:
+            self.output_file += '_'
+            self.output_file += v_resolution[(v_resolution.rindex('x')+1):]
+            self.output_file += 'p'
+        ext = self.tr_extension(container, (v_codec is not None))
+        if ext is not None:
+            self.output_file += '.' + ext
+
+        self._transcode(self.tr_container(container),
+                self.tr_a_codec(a_codec), self.tr_v_codec(v_codec),
+                a_bitrate, a_samplingrate, a_channels,
+                v_bitrate, v_framerate, v_resolution, v_dar)
+
+    def _transcode(self, container, a_codec=None, v_codec=None,
+            a_bitrate=None, a_samplingrate=None, a_channels=None,
+            v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
+        """
+        Called by transcode; must be overridden by a child class which
+        effectively transcodes the input file.
+        """
         pass
 
     def tr_container(self, name):
         """ Translates container API name into external program identifier."""
 
-        if not self.containers.has_key(name) or self.containers[name] == None:
+        if not self.containers.has_key(name) or self.containers[name] is None:
             raise cis_exceptions.NotImplementedException("Container " + name)
 
         return self.containers[name]
+
+    def tr_extension(self, name, video=True):
+        """ Translates container API name into file extension."""
+
+        if video is True:
+            i = 1
+        else:
+            i = 0
+
+        if not self.extensions.has_key(name) or self.extensions[name] is None:
+            return None
+
+        return self.extensions[name][i]
+
+    def tr_a_codec(self, name):
+        """ Translates audio codec API name into external program identifier."""
+
+        if not self.a_codecs.has_key(name) or self.a_codecs[name] is None:
+            raise cis_exceptions.NotImplementedException("Audio Codec " + name)
+
+        return self.a_codecs[name]
+
+    def tr_v_codec(self, name):
+        """ Translates video codec API name into external program identifier."""
+
+        if not self.v_codecs.has_key(name) or self.v_codecs[name] is None:
+            raise cis_exceptions.NotImplementedException("Video Codec " + name)
+
+        return self.v_codecs[name]
index 13a711e..35650ee 100644 (file)
@@ -1,20 +1,95 @@
 #!/usr/bin/env python
 
+"""
+Classes derived from BaseTranscoder and BaseThumbExtractor for transcoding of
+videos and thumbnail extraction from videos using FFmpeg CLI program.
+"""
+
 import base
+import cis_exceptions
+import subprocess
+import os
 
 class FFmpegTranscoder(base.BaseTranscoder):
-    prog_bin = 'ffmpeg'
-    input_file = None
+    """
+    FFmpeg CLI API for video transcoding.
+    """
+
+    prog_bin = "ffmpeg"
+
+    log_file = 'log/ffmpeg.log'
+
+    containers = {
+        "avi": "avi",
+        "flv": "flv",
+        "mp4": "mp4",
+        "ogg": "ogg",
+        "webm": "webm",
+        "mpegts": "mpegts"
+    }
+    a_codecs = {
+        "mp3": "libmp3lame",
+        "vorbis": "libvorbis"
+    }
+    v_codecs = {
+        "h264": "libx264",
+        "theora": "libtheora",
+        "vp8": "libvpx"
+    }
 
-    def transcode(self, container, a_codec, v_codec,
+    def _transcode(self, container, a_codec=None, v_codec=None,
             a_bitrate=None, a_samplingrate=None, a_channels=None,
-            v_bitrate=None, v_fraterate=None, v_resolution=None, v_dar=None):
-        pass
+            v_bitrate=None, v_framerate=None, v_resolution=None, v_dar=None):
+
+        args = self.prog_bin + ' -i "' + self.input_file + '" -f ' + container
+        
+        # Audio
+        if a_codec != None:
+            args += ' -acodec ' + a_codec
+            if a_bitrate != None:
+                args += ' -ab ' + str(a_bitrate)
+            if a_samplingrate != None:
+                args += ' -ar ' + str(a_samplingrate)
+            if a_channels != None:
+                args += ' -ac ' + str(a_channels)
+        
+        # Video
+        if v_codec != None:
+            args += ' -vcodec ' + v_codec
+            # Video codec specific options.
+            if v_codec == 'libx264':
+                args += ' -vpre normal'
+            if v_bitrate != None:
+                args += ' -b ' + str(v_bitrate)
+            if v_framerate != None:
+                args += ' -r ' + str(v_framerate)
+            if v_resolution != None:
+                args += ' -s ' + v_resolution
+            if v_dar != None:
+                args += ' -aspect ' + v_dar
+        
+        # Output file.
+        args += ' "' + self.output_file + '"'
+        try:
+            os.unlink(self.output_file)
+        except OSError:
+            pass
+            
+        # READ handler for process's output.
+        p = subprocess.Popen(args, shell=True, 
+                stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        pipe = p.stdout
+
+        # WRITE handler for logging.
+        log = open(self.log_file, 'w')
+
+        while True:
+            line = pipe.readline()
+            if len(line) == 0:
+                break
+            log.write(line)
 
-    def transcode_audio(self, container, a_codec,
-            a_bitrate=None, a_samplingrate=None, a_channels=None):
-        pass
+        if p.poll() > 0:
+            raise cis_exceptions.TranscodingException
 
-    def transcode_video(self, container, v_codec,
-            v_bitrate=None, v_fraterate=None, v_resolution=None, v_dar=None):
-        pass
+        log.close()
index 7bf1266..89cbb09 100644 (file)
@@ -10,3 +10,6 @@ class NotImplementedException(Exception):
 
     def __str__(self):
         return repr(self.value)
+
+class TranscodingException(Exception):
+    pass
index 09181f3..aa50b33 100755 (executable)
@@ -6,9 +6,5 @@ import cis_exceptions
 
 
 if __name__ == '__main__':
-    transcoder = config.TRANSCODER_CLASS("file")
-#    transcoder.transcode()
-    try:
-        print transcoder.tr_container("avi")
-    except cis_exceptions.NotImplementedException as e:
-        sys.stderr.write(e.value)
+    transcoder = config.TRANSCODER_CLASS("../data/media/test.ogv")
+    transcoder.transcode('ogg', "vorbis", "theora", a_bitrate="192k", a_samplingrate=44100, a_channels=2, v_bitrate="700k", v_framerate=25, v_resolution="800x600", v_dar="16:9")