cis notified web server of a job completion; upload form interface and validation...
[living-lab-site.git] / cis / api / ffmpeg.py
index 35650ee..1974497 100644 (file)
@@ -1,5 +1,6 @@
 #!/usr/bin/env python
 
+
 """
 Classes derived from BaseTranscoder and BaseThumbExtractor for transcoding of
 videos and thumbnail extraction from videos using FFmpeg CLI program.
@@ -8,7 +9,9 @@ videos and thumbnail extraction from videos using FFmpeg CLI program.
 import base
 import cis_exceptions
 import subprocess
+import re
 import os
+import math
 
 class FFmpegTranscoder(base.BaseTranscoder):
     """
@@ -17,7 +20,7 @@ class FFmpegTranscoder(base.BaseTranscoder):
 
     prog_bin = "ffmpeg"
 
-    log_file = 'log/ffmpeg.log'
+    log_file = 'log/FFmpegTranscoder.log'
 
     containers = {
         "avi": "avi",
@@ -41,7 +44,7 @@ class FFmpegTranscoder(base.BaseTranscoder):
             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
+        args = self.prog_bin + ' -y -i "' + self.input_file + '" -f ' + container
         
         # Audio
         if a_codec != None:
@@ -70,10 +73,6 @@ class FFmpegTranscoder(base.BaseTranscoder):
         
         # 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, 
@@ -82,6 +81,7 @@ class FFmpegTranscoder(base.BaseTranscoder):
 
         # WRITE handler for logging.
         log = open(self.log_file, 'w')
+        log.write(args + '\n')
 
         while True:
             line = pipe.readline()
@@ -89,7 +89,110 @@ class FFmpegTranscoder(base.BaseTranscoder):
                 break
             log.write(line)
 
-        if p.poll() > 0:
-            raise cis_exceptions.TranscodingException
+        exit_code = p.wait()
+        if exit_code > 0:
+            raise cis_exceptions.TranscodingException( \
+                    'FFmpeg exited with code ' + str(exit_code) + '.')
 
         log.close()
+
+        return self.output_file
+
+
+class FFmpegThumbExtractor(base.BaseThumbExtractor):
+    """
+    FFmpeg CLI API for video thumbnail extraction.
+    """
+
+    prog_bin = "ffmpeg"
+
+    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):
+        return FFprobeAVInfo.get_video_duration(self.input_file)
+
+
+class FFprobeAVInfo(base.BaseAVInfo):
+    
+    prog_bin = "ffprobe"
+
+    log_file = 'log/FFprobeAVInfo.log'
+
+    @staticmethod
+    def get_video_duration(input_file, formated=False):
+        args = FFprobeAVInfo.prog_bin + ' -show_format "' \
+                + input_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(FFprobeAVInfo.log_file, 'w')
+        log.write(args + '\n')
+
+        # Parse ffprobe's output.
+        while True:
+            line = pipe.readline()
+            if len(line) == 0:
+                break
+            log.write(line)
+            
+            # Search for the line which contains duration information.
+            m = re.match(r"duration=([\d\.]+)", line)
+            if m is not None:
+                seconds = float(m.group(1))
+                if not formated:
+                    return seconds
+                else:
+                    seconds = math.floor(seconds)
+                    minutes = math.floor(seconds / 60)
+                    seconds = seconds % 60
+                    if minutes >= 60:
+                        hours = math.floor(minutes / 60)
+                        minutes = minutes % 60
+                        
+                        return "%02d:%02d:%02d" % (hours, minutes, seconds)
+                    else:
+                        return "%02d:%02d" % (minutes, seconds)
+
+        exit_code = p.wait()
+        if exit_code > 0:
+            raise cis_exceptions.AVInfoException( \
+                    'ffprobe exited with code ' + str(exit_code) + '.')