FFmpeg API for thumbnail extraction and transcoding in CIS implemented
authorCalin-Andrei Burloiu <calin.burloiu@gmail.com>
Tue, 22 Nov 2011 14:36:04 +0000 (16:36 +0200)
committerCalin-Andrei Burloiu <calin.burloiu@gmail.com>
Tue, 22 Nov 2011 14:36:04 +0000 (16:36 +0200)
cis/api/avhandling.py [new file with mode: 0644]
cis/api/base.py
cis/api/ffmpeg.py [deleted file]
cis/cis_exceptions.py
cis/cis_util.py [new file with mode: 0644]
cis/cisd.py
cis/config.py

diff --git a/cis/api/avhandling.py b/cis/api/avhandling.py
new file mode 100644 (file)
index 0000000..a64f7f2
--- /dev/null
@@ -0,0 +1,168 @@
+#!/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 re
+import os
+
+class FFmpegTranscoder(base.BaseTranscoder):
+    """
+    FFmpeg CLI API for video transcoding.
+    """
+
+    prog_bin = "ffmpeg"
+
+    log_file = 'log/FFmpegTranscoder.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=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):
+
+        args = self.prog_bin + ' -y -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 + '"'
+            
+        # 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')
+        log.write(args + '\n')
+
+        while True:
+            line = pipe.readline()
+            if len(line) == 0:
+                break
+            log.write(line)
+
+        exit_code = p.wait()
+        if exit_code > 0:
+            raise cis_exceptions.TranscodingException( \
+                    'FFmpeg exited with code ' + str(exit_code) + '.')
+
+        log.close()
+
+
+class FFmpegThumbExtractor(base.BaseThumbExtractor):
+    """
+    FFmpeg CLI API for video thumbnail extraction.
+    """
+
+    prog_bin = "ffmpeg"
+    info_prog_bin = "ffprobe"
+
+    log_file = 'log/FFmpegThumbExtractor.log'
+
+    def extract_thumb(self, seek_pos, resolution="120x90", index=0):
+        output_file = self.get_output_file_name(index)
+
+        args = self.prog_bin + ' -y -i "' + self.input_file \
+                + '" -f rawvideo -vcodec mjpeg' + (' -ss ' + str(seek_pos)) \
+                + " -vframes 1 -an -s " + resolution + ' "' \
+                + output_file + '"'
+
+        # 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')
+        log.write(args + '\n')
+
+        while True:
+            line = pipe.readline()
+            if len(line) == 0:
+                break
+            log.write(line)
+
+        exit_code = p.wait()
+        if exit_code > 0:
+            raise cis_exceptions.ThumbExtractionException( \
+                    'FFmpeg exited with code ' + str(exit_code) + '.')
+
+        # FFmpeg bug: when no key frame is found from seek_pos to the
+        # end of file an empty image file is created.
+        if os.path.getsize(output_file) == 0L:
+            os.unlink(output_file)
+            raise cis_exceptions.ThumbExtractionException( \
+                    'FFmpeg created an empty file.')
+
+    def get_video_duration(self):
+        args = self.info_prog_bin + ' -show_format "' \
+                + self.input_file + '"'
+
+        # READ handler for process's output.
+        p = subprocess.Popen(args, shell=True,
+                stdout=subprocess.PIPE, stderr=open(os.devnull, 'w'))
+        pipe = p.stdout
+
+        # Parse ffprobe's output.
+        while True:
+            line = pipe.readline()
+            if len(line) == 0:
+                break
+            
+            # Search for the line which contains duration information.
+            m = re.match(r"duration=([\d\.]+)", line)
+            if m is not None:
+                return float(m.group(1))
+
+        exit_code = p.wait()
+        if exit_code > 0:
+            raise cis_exceptions.ThumbExtractionException( \
+                    'FFmpeg exited with code ' + str(exit_code) + '.')
+
index 96006f8..8ea2cd3 100644 (file)
@@ -6,6 +6,8 @@ Base classes for the external programs API.
 
 import cis_exceptions
 import re
+import cis_util
+import random
 
 class BaseTranscoder:
     """
@@ -53,12 +55,7 @@ class BaseTranscoder:
             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('.')]
+            name = cis_util.get_name(input_file)
 
         self.name = name
 
@@ -166,3 +163,83 @@ class BaseTranscoder:
             raise cis_exceptions.NotImplementedException("Video Codec " + name)
 
         return self.v_codecs[name]
+
+
+class BaseThumbExtractor:
+    """
+    Abstraction of the API class for the thumbnail extraction program. 
+
+    Thumbnail extracted are in JPEG format.
+    """
+
+    prog_bin = None
+    input_file = None
+    dest_path = ''
+    name = None
+    
+    def __init__(self, input_file, name=None, prog_bin=None):
+        self.input_file = input_file
+        if prog_bin is not None:
+            self.prog_bin = prog_bin
+        
+        if name is None:
+            name = cis_util.get_name(input_file)
+
+        self.name = name
+
+    def extract_thumb(self, seek_pos, resolution="120x90", index=0):
+        """
+        Extracts a thumbnail from the video from a specified position
+        expressed in seconds (int/float).
+
+        index: an index appended to the image name in order to avoid
+        overwriting.
+        """
+        pass
+
+    def extract_random_thumb(self, resolution="120x90", index=0):
+        """
+        Extracts a thumbnail from the video from a random position.
+        """
+        duration = self.get_video_duration()
+        seek_pos = random.random() * duration
+        self.extract_thumb(seek_pos, resolution, index)
+
+    def extract_summary_thumbs(self, count, resolution="120x90"):
+        """
+        Extracts a series of thumbnails from a video by taking several
+        snapshots.
+
+        The snapshots are taken from equally spaced positions such that
+        `count` thumbs are extracted.
+        """
+        duration = self.get_video_duration()
+        interval = duration / (count + 1)
+        
+        n_thumbs_extracted = 0
+        seek_pos = interval
+        for index in range (0, count):
+            thumb_extracted = True
+            try:
+                self.extract_thumb(seek_pos, resolution, index)
+            except cis_exceptions.ThumbExtractionException as e:
+                thumb_extracted = False
+
+            if thumb_extracted:
+                n_thumbs_extracted += 1
+
+            seek_pos += interval
+
+        return n_thumbs_extracted
+
+    def get_video_duration(self):
+        """
+        Returns the number of seconds of a video (int/float).
+        """
+        pass
+
+    def get_output_file_name(self, index):
+        """ Returns the name required as output file name based on index. """
+        output_file_name = self.dest_path + self.name \
+                + '_t' + ("%02d" % index) + '.jpg'
+        return output_file_name
diff --git a/cis/api/ffmpeg.py b/cis/api/ffmpeg.py
deleted file mode 100644 (file)
index 35650ee..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/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):
-    """
-    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=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):
-
-        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)
-
-        if p.poll() > 0:
-            raise cis_exceptions.TranscodingException
-
-        log.close()
index 89cbb09..33c314f 100644 (file)
@@ -13,3 +13,6 @@ class NotImplementedException(Exception):
 
 class TranscodingException(Exception):
     pass
+
+class ThumbExtractionException(Exception):
+    pass
diff --git a/cis/cis_util.py b/cis/cis_util.py
new file mode 100644 (file)
index 0000000..619b95a
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+
+"""
+Useful functions for CIS.
+"""
+
+def get_name(file_name):
+    if file_name.find('/') is not -1:
+        name = file_name[(file_name.rindex('/')+1):]
+    else:
+        name = file_name
+    if name.find('.') is not -1:
+        name = name[:name.rindex('.')]
+
+    return name
index aa50b33..cb22887 100755 (executable)
@@ -6,5 +6,10 @@ import cis_exceptions
 
 
 if __name__ == '__main__':
-    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")
+#    transcoder = config.TRANSCODER_CLASS(sys.argv[1])
+#    transcoder.transcode('webm', "vorbis", "vp8", a_bitrate="128k", a_samplingrate=22050, a_channels=2, v_bitrate="256k", v_framerate=15, v_resolution="320x240", v_dar="4:3")
+    
+    thumb_extractor = config.THUMB_EXTRACTOR_CLASS(sys.argv[1])
+    #print thumb_extractor.get_video_duration()
+    #thumb_extractor.extract_random_thumb()
+    print thumb_extractor.extract_summary_thumbs(5)
index d21fb56..7b01397 100644 (file)
@@ -1,11 +1,11 @@
 #!/usr/bin/env python
 
 # Make here all necessary imports required for API classes.
-from api import ffmpeg
+from api import avhandling
 
 # External programs API classes.
-TRANSCODER_CLASS = ffmpeg.FFmpegTranscoder
-THUMB_EXTRACTER_CLASS = None # TODO
+TRANSCODER_CLASS = avhandling.FFmpegTranscoder
+THUMB_EXTRACTOR_CLASS = avhandling.FFmpegThumbExtractor
 BT_CLIENT_CLASS = None # TODO
 FILE_TRANSFERER_CLASS = None # TODO