From a95dd065bbaaf656f69903e9a3eed83c326c1437 Mon Sep 17 00:00:00 2001 From: Calin-Andrei Burloiu Date: Tue, 22 Nov 2011 16:36:04 +0200 Subject: [PATCH] FFmpeg API for thumbnail extraction and transcoding in CIS implemented --- cis/api/avhandling.py | 168 ++++++++++++++++++++++++++++++++++++++++++ cis/api/base.py | 89 ++++++++++++++++++++-- cis/api/ffmpeg.py | 95 ------------------------ cis/cis_exceptions.py | 3 + cis/cis_util.py | 15 ++++ cis/cisd.py | 9 ++- cis/config.py | 6 +- 7 files changed, 279 insertions(+), 106 deletions(-) create mode 100644 cis/api/avhandling.py delete mode 100644 cis/api/ffmpeg.py create mode 100644 cis/cis_util.py diff --git a/cis/api/avhandling.py b/cis/api/avhandling.py new file mode 100644 index 0000000..a64f7f2 --- /dev/null +++ b/cis/api/avhandling.py @@ -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) + '.') + diff --git a/cis/api/base.py b/cis/api/base.py index 96006f8..8ea2cd3 100644 --- a/cis/api/base.py +++ b/cis/api/base.py @@ -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 index 35650ee..0000000 --- a/cis/api/ffmpeg.py +++ /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() diff --git a/cis/cis_exceptions.py b/cis/cis_exceptions.py index 89cbb09..33c314f 100644 --- a/cis/cis_exceptions.py +++ b/cis/cis_exceptions.py @@ -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 index 0000000..619b95a --- /dev/null +++ b/cis/cis_util.py @@ -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 diff --git a/cis/cisd.py b/cis/cisd.py index aa50b33..cb22887 100755 --- a/cis/cisd.py +++ b/cis/cisd.py @@ -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) diff --git a/cis/config.py b/cis/config.py index d21fb56..7b01397 100644 --- a/cis/config.py +++ b/cis/config.py @@ -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 -- 2.20.1