From 5541e2a07cdedb9a8d42de1e871cb04dc186043c Mon Sep 17 00:00:00 2001 From: Mathias Koehler Date: Thu, 13 Oct 2011 00:03:32 +0200 Subject: [PATCH] initial commit first commit of ffmpegwrapper. --- ffmpegwrapper/__init__.py | 3 + ffmpegwrapper/codec.py | 24 +++++ ffmpegwrapper/ffmpeg.py | 48 ++++++++++ ffmpegwrapper/filter.py | 195 ++++++++++++++++++++++++++++++++++++++ ffmpegwrapper/options.py | 69 ++++++++++++++ setup.py | 0 test.py | 69 ++++++++++++++ 7 files changed, 408 insertions(+) create mode 100644 ffmpegwrapper/__init__.py create mode 100644 ffmpegwrapper/codec.py create mode 100644 ffmpegwrapper/ffmpeg.py create mode 100644 ffmpegwrapper/filter.py create mode 100644 ffmpegwrapper/options.py create mode 100644 setup.py create mode 100644 test.py diff --git a/ffmpegwrapper/__init__.py b/ffmpegwrapper/__init__.py new file mode 100644 index 0000000..8bf1324 --- /dev/null +++ b/ffmpegwrapper/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf8 -*- + +from ffmpegwrapper.ffmpeg import FFmpeg, Input, Output diff --git a/ffmpegwrapper/codec.py b/ffmpegwrapper/codec.py new file mode 100644 index 0000000..391037c --- /dev/null +++ b/ffmpegwrapper/codec.py @@ -0,0 +1,24 @@ +# -*- coding: utf8 -*- + +from itertools import chain + +from .options import CombinedOptions + + +class Codec(CombinedOptions): + + def __init__(self, name, *args): + self.name = name + CombinedOptions.__init__(self, *args) + + +class VideoCodec(Codec): + + def __iter__(self): + return chain(['-vcodec', self.name], Codec.__iter__(self)) + + +class AudioCodec(Codec): + + def __iter__(self): + return chain(['-acodec', self.name], Codec.__iter__(self)) diff --git a/ffmpegwrapper/ffmpeg.py b/ffmpegwrapper/ffmpeg.py new file mode 100644 index 0000000..fc54cb7 --- /dev/null +++ b/ffmpegwrapper/ffmpeg.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" + FFmpeg + + Your entrypoint for every Task you want to do with FFmpeg + + :copyright: (c) 2011 by Mathias Koehler. + :license: BSD, see LICENSE for more details. +""" + +from subprocess import Popen, PIPE +from itertools import chain + +from .options import CombinedOptions, Options + + +class Input(CombinedOptions): + + def __init__(self, file, *args): + self.file = file + CombinedOptions.__init__(self, *args) + + def __iter__(self): + return chain(CombinedOptions.__iter__(self), ['-i', self.file]) + + +class Output(CombinedOptions): + + def __init__(self, file, *args): + self.file = file + CombinedOptions.__init__(self, *args) + + def __iter__(self): + return chain(CombinedOptions.__iter__(self), [self.file]) + + +class FFmpeg(CombinedOptions): + + def __init__(self, binary="ffmpeg", *args): + self.binary = binary + CombinedOptions.__init__(self, *args) + + def run(self): + return Popen(self, executable=self.binary, stdin=PIPE, + stdout=PIPE, stderr=PIPE) + + def add_option(self, key, value): + self._list.insert(0, Options({key: value})) diff --git a/ffmpegwrapper/filter.py b/ffmpegwrapper/filter.py new file mode 100644 index 0000000..6a01052 --- /dev/null +++ b/ffmpegwrapper/filter.py @@ -0,0 +1,195 @@ +# -*- coding: utf8 -*- + +from itertools import chain, islice + +from .options import CombinedOptions + + +class CombinedFilter(CombinedOptions): + + def __str__(self): + return ",".join(CombinedFilter.__iter__(self)) + + def __iter__(self): + i = CombinedOptions.__iter__ + keys = islice(i(self), 0, None, 2) + values = islice(i(self), 1, None, 2) + + for key, value in zip(keys, values): + if value: + yield "=".join([key, str(value)]) + else: + yield key + +class VideoFilter(CombinedFilter): + + def blackframe(self, amount, threshold): + filter = self._format_parameter(amount, threshold) + self.add_option('blackframe', filter) + return self + + def copy(self): + self.add_option('copy', None) + return self + + def crop(self, out_w, out_h=None, x=None, y=None): + filter = self._format_parameter(out_w, out_h, x, y) + self.add_option('crop', filter) + return self + + def cropdetect(self, limit=None, round=None, reset=None): + filter = self._format_parameter(limit, round, reset) + self.add_option('cropdetect', filter) + return self + + def drawbox(self, x, y, width, height, color): + filter = self._format_parameter(x, y, width, height, color) + self.add_option('drawbox', filter) + return self + + def drawtext(self, **kwargs): + filter = self._format_keyword_parameter(**kwargs) + self.add_option('drawtext', filter) + return self + + def fade(self, type, start, number): + filter = self._format_parameter(type, start, number) + self.add_option('fade', filter) + return self + + def fieldorder(self, type): + if str(type) not in ['0', '1', 'bff', 'tff']: + raise ValueError('Invalid Option for fieldorder. ' + 'Read FFmpeg manual!') + self.add_option('fieldorder', type) + return self + + def fifo(self): + self.add_option('fifo', None) + return self + + def format(self, *args): + filter = self._format_parameter(*args) + self.add_option('format', filter) + return self + + def freior(self, name, **kwargs): + filter = self._format_keyword_parameter(name, **kwargs) + self.add_option('frei0r', filter) + return self + + def gradfun(self, strength='', radius=''): + filter = self._format_parameter(strength, radius) + self.add_option('gradfun', filter) + return self + + def hflip(self): + self.add_option('hflip', None) + return self + + def hqdn3d(self, luma_sp='', chroma_sp='', luma_tmp='', chroma_tmp=''): + filter = self._format_parameter( + luma_sp, chroma_sp, luma_tmp, chroma_tmp) + self.add_option('hqdn3d', filter) + return self + + def mp(self, **kwargs): + filter = self._format_keyword_parameter(**kwargs) + self.add_option('mp', filter) + return self + + def negate(self): + self.add_option('negate', 1) + return self + + def noformat(self, *args): + filter = self._format_parameter(*args) + self.add_option('noformat', filter) + return self + + def null(self): + self.add_option('null', None) + return self + + def overlay(self, x, y): + filter = self._format_parameter(x, y) + self.add_option('overlay', filter) + return self + + def pad(self, width, height, x, y, color): + filter = self._format_parameter(width, height, x, y, color) + self.add_option('pad', filter) + return self + + def scale(self, width=-1, height=-1): + filter = self._format_parameter(width, height) + self.add_option('scale', filter) + return self + + def select(self, expression): + self.add_option('select', expression) + return self + + def setdar(self, aspect): + self.add_option('setdar', aspect) + return self + + def setpts(self, expression): + self.add_option('setpts', expression) + return self + + def setsar(self, aspect): + self.add_option('setsar', aspect) + return self + + def settb(self, expression): + self.add_option('settb', expression) + return self + + def showinfo(self, **kwargs): + filter = self._format_keyword_parameter(**kwargs) + self.add_option('showinfo', filter) + return self + + def slicify(self, height=16): + self.add_option("slicify", height) + return self + + def transpose(self, type): + if str(type) not in ['0', '1','2', '3']: + raise ValueError('Invalid Option for transpose. ' + 'Read FFmpeg manual') + self.add_option('transpose', type) + return self + + def vflip(self): + self.add_option('vflip', None) + return self + + def yadif(self, mode=0, parity=-1): + filter = self._format_parameter(mode, parity) + self.add_option('yadif', filter) + return self + + def _format_parameter(self, *args): + parameter = filter(lambda x: x is not None, args) + return ':'.join(map(str, parameter)) + + def _format_keyword_parameter(self, **kwargs): + parameter_list = [] + for key, value in kwargs: + try: + parameter_list.append("=".join([key, value])) + except TypeError: + values = ":".join(value) + parameter_list.append("=".join([key, values])) + return ":".join(parameter_list) + + def __iter__(self): + return chain(['-vf', CombinedFilter.__str__(self)]) + + +class AudioFilter(CombinedFilter): + + def __iter__(self): + return chain(['-af', CombinedFilter.__str__(self)]) diff --git a/ffmpegwrapper/options.py b/ffmpegwrapper/options.py new file mode 100644 index 0000000..cfdbf07 --- /dev/null +++ b/ffmpegwrapper/options.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +from itertools import chain + + +class Options(dict): + + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + + def __getitem__(self, key): + if key in self: + return dict.__getitem__(self, key) + + def __iter__(self): + return self.iteritems() + + def iteritems(self): + for option, value in self.items(): + yield option + if value: + yield value + + def __repr__(self): + return "<{cls} {opts}>".format(opts=list(self), + cls=self.__class__.__name__) + + +class CombinedOptions(object): + + def __init__(self, *args): + self._list = list(args) + + def __add__(self, other): + self.append(other) + + def append(self, item): + self._list.append(item) + + def insert(self, item): + self._list.insert(item) + + def pop(self): + return self._list.pop() + + def remove(self, item): + self._list.remove() + + def count(self, items): + return self._list.count() + + def index(self, item): + return self._list.index(item) + + def add_option(self, key, value): + self._list.append(Options({key: value})) + + @property + def option_containers(self): + return self._list + + def __iter__(self): + for item in self._list: + for option in item: + yield option + + def __repr__(self): + return "<{cls} {opts}>".format(opts=list(self), + cls=self.__class__.__name__) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e69de29 diff --git a/test.py b/test.py new file mode 100644 index 0000000..b30eacc --- /dev/null +++ b/test.py @@ -0,0 +1,69 @@ +# -*- coding: utf8 -*- + +import unittest + +from ffmpegwrapper import FFmpeg, Input, Output +from ffmpegwrapper.codec import VideoCodec, AudioCodec +from ffmpegwrapper.filter import VideoFilter +from ffmpegwrapper.options import Options + + +class FFmpegTestCase(unittest.TestCase): + + def test_input_interface(self): + input = Input('/old') + self.assertEqual(list(input), ['-i', '/old']) + self.assertEqual(input.file, '/old') + + option = Options({'-vf': 'x11grab'}) + input.append(option) + self.assertEqual(list(input), ['-vf', 'x11grab', '-i', '/old']) + self.assertEqual(input.pop(), option) + + input.add_option('-vf', 'x11grab') + self.assertEqual(input.option_containers, [option]) + + def test_output_interface(self): + output = Output('/new') + self.assertEqual(list(output), ['/new']) + self.assertEqual(output.file, '/new') + + option = Options({'-vcodec': 'libx264'}) + output.append(option) + self.assertEqual(list(output), ['-vcodec', 'libx264', '/new']) + self.assertEqual(output.pop(), option) + + output.add_option('-vcodec', 'libx264') + self.assertEqual(output.option_containers, [option]) + + def test_codec_interface(self): + codec = VideoCodec('libx264') + self.assertEqual(list(codec), ['-vcodec', 'libx264']) + + codec = AudioCodec('ac3') + self.assertEqual(list(codec), ['-acodec', 'ac3']) + + def test_filter_interface(self): + filter = VideoFilter() + filter.blackframe(1, 2).crop(792) + self.assertEqual(list(filter), ['-vf', + 'blackframe=1:2,crop=792']) + + output = Output('/new', filter) + self.assertEqual(list(output), ['-vf', + 'blackframe=1:2,crop=792', '/new']) + + def test_ffmpeg_interface(self): + input = Input('/old') + output = Output('/new') + + ffmpeg = FFmpeg('ffmpeg', input, output) + self.assertEqual(list(ffmpeg), ['-i', '/old', '/new']) + + +class VideoFilter(unittest.TestCase): + pass + + +if __name__ == '__main__': + unittest.main()