From 42cb85273f6ef35a40c183f1184313bbc06afa05 Mon Sep 17 00:00:00 2001 From: Calin-Andrei Burloiu Date: Thu, 17 Nov 2011 14:34:51 +0200 Subject: [PATCH] CIS FFmpeg API implemented --- .gitignore | 1 + cis/api/base.py | 125 ++++++++++++++++++++++++++++++++++++++---- cis/api/ffmpeg.py | 97 ++++++++++++++++++++++++++++---- cis/cis_exceptions.py | 3 + cis/cisd.py | 8 +-- 5 files changed, 205 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index b7df4ee..110f02a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ data/thumbs/* data/torrents/* data/user_pictures/* data/media/* +cis/log/* diff --git a/cis/api/base.py b/cis/api/base.py index 3e5f508..96006f8 100644 --- a/cis/api/base.py +++ b/cis/api/base.py @@ -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 x + v_dar: video display aspect ratio as x 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 x.') + + 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 :.') + + 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] diff --git a/cis/api/ffmpeg.py b/cis/api/ffmpeg.py index 13a711e..35650ee 100644 --- a/cis/api/ffmpeg.py +++ b/cis/api/ffmpeg.py @@ -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() diff --git a/cis/cis_exceptions.py b/cis/cis_exceptions.py index 7bf1266..89cbb09 100644 --- a/cis/cis_exceptions.py +++ b/cis/cis_exceptions.py @@ -10,3 +10,6 @@ class NotImplementedException(Exception): def __str__(self): return repr(self.value) + +class TranscodingException(Exception): + pass diff --git a/cis/cisd.py b/cis/cisd.py index 09181f3..aa50b33 100755 --- a/cis/cisd.py +++ b/cis/cisd.py @@ -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") -- 2.20.1