-
-

Welcome to ffmpeg-python’s documentation!

+
+

ffmpeg-python: Python bindings for FFmpeg

-

ffmpeg-python: Python bindings for FFmpeg

-
+
ffmpeg.colorchannelmixer(parent, *args, **kwargs)
-

FIXME

+

Adjust video input frames by re-mixing color channels.

+

FFmpeg colorchannelmixer filter

ffmpeg.concat(*parents, **kwargs)
-

FIXME

-
+
ffmpeg.drawbox(parent, x, y, width, height, color, thickness=None, **kwargs)
-

FIXME

-
- -
-
-ffmpeg.file_input(filename)
-

Input from a file.

-

Corresponds to ffmpeg -i option.

-
-
Args:
-
filename: Input filename
-
-
- -
-
-ffmpeg.file_output(parent, filename)
-

FIXME

-
- -
-
-ffmpeg.fluent(node_classes=set([<class ‘ffmpeg.nodes.Node’>]))
-

Decorator to make function show up as an instance method on specified node classes.

- --- - - - -
Parameters:node_classes – list of node classes
-
- -
-
-ffmpeg.get_args(parent)
-

FIXME

-
+
ffmpeg.hflip(parent)
-

FIXME

-
+
ffmpeg.hue(parent, **kwargs)
-

FIXME

-
- -
-
-ffmpeg.merge_outputs(*parents)
-

FIXME

-
+
ffmpeg.overlay(main_parent, overlay_parent, eof_action=’repeat’, **kwargs)
-

FIXME

-
- -
-
-ffmpeg.overwrite_output(parent)
-

FIXME

-
- -
-
-ffmpeg.run(parent, cmd=’ffmpeg’)
-

FIXME

-
+
ffmpeg.setpts(parent, expr)
-

FIXME

-
+
ffmpeg.trim(parent, **kwargs)
-

FIXME

-
+
ffmpeg.vflip(parent)
-

FIXME

-
+
ffmpeg.zoompan(parent, **kwargs)
-

FIXME

-
+
+ +
+
+ffmpeg.file_input(filename)
+
+ +
+
+ffmpeg.file_output(parent, filename)
+
+ +
+
+ffmpeg.merge_outputs(*parents)
+
+ +
+
+ffmpeg.overwrite_output(parent)
+
+ +
+
+ffmpeg.get_args(parent)
+
+ +
+
+ffmpeg.run(parent, cmd=’ffmpeg’)
+
@@ -179,7 +145,7 @@

Table Of Contents

diff --git a/doc/html/objects.inv b/doc/html/objects.inv index 2cc9cb5..70a5c88 100644 Binary files a/doc/html/objects.inv and b/doc/html/objects.inv differ diff --git a/doc/html/searchindex.js b/doc/html/searchindex.js index e65c02f..294eda3 100644 --- a/doc/html/searchindex.js +++ b/doc/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["index"],envversion:52,filenames:["index.rst"],objects:{"":{ffmpeg:[0,0,0,"-"]},ffmpeg:{colorchannelmixer:[0,1,1,""],concat:[0,1,1,""],drawbox:[0,1,1,""],file_input:[0,1,1,""],file_output:[0,1,1,""],fluent:[0,1,1,""],get_args:[0,1,1,""],hflip:[0,1,1,""],hue:[0,1,1,""],merge_outputs:[0,1,1,""],overlay:[0,1,1,""],overwrite_output:[0,1,1,""],run:[0,1,1,""],setpts:[0,1,1,""],trim:[0,1,1,""],vflip:[0,1,1,""],zoompan:[0,1,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:function"},terms:{"class":0,"function":0,arg:0,bind:0,cmd:0,color:0,colorchannelmix:0,concat:0,correspond:0,decor:0,drawbox:0,eof_act:0,expr:0,file:0,file_input:0,file_output:0,filenam:0,fixm:0,fluent:0,from:0,get_arg:0,height:0,hflip:0,hue:0,index:0,input:0,instanc:0,kwarg:0,list:0,main_par:0,make:0,merge_output:0,method:0,modul:0,node:0,node_class:0,none:0,option:0,overlai:0,overlay_par:0,overwrite_output:0,page:0,paramet:0,parent:0,repeat:0,run:0,search:0,set:0,setpt:0,show:0,specifi:0,thick:0,trim:0,vflip:0,width:0,zoompan:0},titles:["Welcome to ffmpeg-python\u2019s documentation!"],titleterms:{document:0,ffmpeg:0,indic:0,python:0,tabl:0,welcom:0}}) \ No newline at end of file +Search.setIndex({docnames:["index"],envversion:52,filenames:["index.rst"],objects:{"":{ffmpeg:[0,0,0,"-"]},ffmpeg:{colorchannelmixer:[0,1,1,""],concat:[0,1,1,""],drawbox:[0,1,1,""],file_input:[0,1,1,""],file_output:[0,1,1,""],get_args:[0,1,1,""],hflip:[0,1,1,""],hue:[0,1,1,""],merge_outputs:[0,1,1,""],overlay:[0,1,1,""],overwrite_output:[0,1,1,""],run:[0,1,1,""],setpts:[0,1,1,""],trim:[0,1,1,""],vflip:[0,1,1,""],zoompan:[0,1,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:function"},terms:{"class":[],"function":[],adjust:0,arg:0,base:[],bind:[],channel:0,cmd:0,color:0,colorchannelmix:0,concat:0,correspond:[],decor:[],drawbox:0,eof_act:0,expr:0,file:[],file_input:0,file_output:0,filenam:0,filter:0,filternod:[],fixm:[],fluent:[],frame:0,from:[],get_arg:0,globalnod:[],height:0,hflip:0,hue:0,index:0,input:0,inputnod:[],instanc:[],kwarg:0,list:[],main_par:0,make:[],merge_output:0,method:[],mix:0,modul:0,name:[],node:[],node_class:[],none:0,object:[],oper:[],option:[],outputnod:[],overlai:0,overlay_par:0,overwrite_output:0,page:0,paramet:[],parent:0,repeat:0,run:0,search:0,set:[],setpt:0,show:[],specifi:[],thick:0,trim:0,vflip:0,video:0,width:0,zoompan:0},titles:["ffmpeg-python: Python bindings for FFmpeg"],titleterms:{bind:0,document:[],ffmpeg:0,indic:0,python:0,tabl:0,welcom:[]}}) \ No newline at end of file diff --git a/ffmpeg/__init__.py b/ffmpeg/__init__.py old mode 100755 new mode 100644 index 241c828..ff5c064 --- a/ffmpeg/__init__.py +++ b/ffmpeg/__init__.py @@ -1,234 +1,5 @@ -#!./venv/bin/python - -import hashlib -import json -import operator as _operator -import subprocess - - -class Node(object): - def __init__(self, parents, name, *args, **kwargs): - parent_hashes = [parent._hash for parent in parents] - assert len(parent_hashes) == len(set(parent_hashes)), 'Same node cannot be included as parent multiple times' - self._parents = parents - self._name = name - self._args = args - self._kwargs = kwargs - self._update_hash() - - @classmethod - def _add_operator(cls, func): - setattr(cls, func.__name__, func) - - def __repr__(self): - formatted_props = ['{}'.format(arg) for arg in self._args] - formatted_props += ['{}={!r}'.format(key, self._kwargs[key]) for key in sorted(self._kwargs)] - return '{}({})'.format(self._name, ','.join(formatted_props)) - - def __eq__(self, other): - return self._hash == other._hash - - def _update_hash(self): - props = {'args': self._args, 'kwargs': self._kwargs} - my_hash = hashlib.md5(json.dumps(props, sort_keys=True)).hexdigest() - parent_hashes = [parent._hash for parent in self._parents] - hashes = parent_hashes + [my_hash] - self._hash = hashlib.md5(','.join(hashes)).hexdigest() - - -class InputNode(Node): - def __init__(self, name, *args, **kwargs): - super(InputNode, self).__init__(parents=[], name=name, *args, **kwargs) - - -class FilterNode(Node): - def _get_filter(self): - params_text = self._name - arg_params = ['{}'.format(arg) for arg in self._args] - kwarg_params = ['{}={}'.format(k, self._kwargs[k]) for k in sorted(self._kwargs)] - params = arg_params + kwarg_params - if params: - params_text += '={}'.format(':'.join(params)) - return params_text - - -class OutputNode(Node): - pass - - -class GlobalNode(Node): - def __init__(self, parent, name, *args, **kwargs): - assert isinstance(parent, OutputNode), 'Global nodes can only be attached after output nodes' - super(GlobalNode, self).__init__([parent], name, *args, **kwargs) - - -def operator(node_classes={Node}): - def decorator(func): - [node_class._add_operator(func) for node_class in node_classes] - return func - return decorator - - -def file_input(filename): - return InputNode(file_input.__name__, filename=filename) - - -@operator() -def setpts(parent, expr): - return FilterNode([parent], setpts.__name__, expr) - - -@operator() -def trim(parent, **kwargs): - return FilterNode([parent], trim.__name__, **kwargs) - - -@operator() -def overlay(main_parent, overlay_parent, eof_action='repeat', **kwargs): - kwargs['eof_action'] = eof_action - return FilterNode([main_parent, overlay_parent], overlay.__name__, **kwargs) - - -@operator() -def hflip(parent): - return FilterNode([parent], hflip.__name__) - - -@operator() -def vflip(parent): - return FilterNode([parent], vflip.__name__) - - -@operator() -def drawbox(parent, x, y, width, height, color, thickness=None, **kwargs): - if thickness: - kwargs['t'] = thickness - return FilterNode([parent], drawbox.__name__, x, y, width, height, color, **kwargs) - - -@operator() -def concat(*parents, **kwargs): - kwargs['n'] = len(parents) - return FilterNode(parents, concat.__name__, **kwargs) - - -@operator() -def zoompan(parent, **kwargs): - return FilterNode([parent], zoompan.__name__, **kwargs) - - -@operator() -def hue(parent, **kwargs): - return FilterNode([parent], hue.__name__, **kwargs) - - -@operator() -def colorchannelmixer(parent, *args, **kwargs): - return FilterNode([parent], colorchannelmixer.__name__, **kwargs) - - -@operator(node_classes={OutputNode, GlobalNode}) -def overwrite_output(parent): - return GlobalNode(parent, overwrite_output.__name__) - - -@operator(node_classes={OutputNode}) -def merge_outputs(*parents): - return OutputNode(parents, merge_outputs.__name__) - - -@operator(node_classes={InputNode, FilterNode}) -def file_output(parent, filename): - return OutputNode([parent], file_output.__name__, filename=filename) - - -def _get_stream_name(name): - return '[{}]'.format(name) - - -def _get_input_args(input_node): - if input_node._name == file_input.__name__: - args = ['-i', input_node._kwargs['filename']] - else: - assert False, 'Unsupported input node: {}'.format(input_node) - return args - - -def _topo_sort(start_node): - marked_nodes = [] - sorted_nodes = [] - child_map = {} - def visit(node, child): - assert node not in marked_nodes, 'Graph is not a DAG' - if child is not None: - if node not in child_map: - child_map[node] = [] - child_map[node].append(child) - if node not in sorted_nodes: - marked_nodes.append(node) - [visit(parent, node) for parent in node._parents] - marked_nodes.remove(node) - sorted_nodes.append(node) - unmarked_nodes = [start_node] - while unmarked_nodes: - visit(unmarked_nodes.pop(), None) - return sorted_nodes, child_map - - -def _get_filter_spec(i, node, stream_name_map): - stream_name = _get_stream_name('v{}'.format(i)) - stream_name_map[node] = stream_name - inputs = [stream_name_map[parent] for parent in node._parents] - filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(), stream_name) - return filter_spec - - -def _get_filter_arg(filter_nodes, stream_name_map): - filter_specs = [_get_filter_spec(i, node, stream_name_map) for i, node in enumerate(filter_nodes)] - return ';'.join(filter_specs) - - -def _get_global_args(node): - if node._name == overwrite_output.__name__: - return ['-y'] - else: - assert False, 'Unsupported global node: {}'.format(node) - - -def _get_output_args(node, stream_name_map): - args = [] - if node._name != merge_outputs.__name__: - stream_name = stream_name_map[node._parents[0]] - if stream_name != '[0]': - args += ['-map', stream_name] - if node._name == file_output.__name__: - args += [node._kwargs['filename']] - else: - assert False, 'Unsupported output node: {}'.format(node) - return args - - -@operator(node_classes={OutputNode, GlobalNode}) -def get_args(parent): - args = [] - # TODO: group nodes together, e.g. `-i somefile -r somerate`. - sorted_nodes, child_map = _topo_sort(parent) - input_nodes = [node for node in sorted_nodes if isinstance(node, InputNode)] - output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode) and not - isinstance(node, GlobalNode)] - global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)] - filter_nodes = [node for node in sorted_nodes if node not in (input_nodes + output_nodes + global_nodes)] - stream_name_map = {node: _get_stream_name(i) for i, node in enumerate(input_nodes)} - filter_arg = _get_filter_arg(filter_nodes, stream_name_map) - args += reduce(_operator.add, [_get_input_args(node) for node in input_nodes]) - if filter_arg: - args += ['-filter_complex', filter_arg] - args += reduce(_operator.add, [_get_output_args(node, stream_name_map) for node in output_nodes]) - args += reduce(_operator.add, [_get_global_args(node) for node in global_nodes], []) - return args - - -@operator(node_classes={OutputNode, GlobalNode}) -def run(parent, cmd='ffmpeg'): - args = [cmd] + parent.get_args() - subprocess.check_call(args) +from . import _filters, _ffmpeg, _run +from ._filters import * +from ._ffmpeg import * +from ._run import * +__all__ = _filters.__all__ + _ffmpeg.__all__ + _run.__all__ diff --git a/ffmpeg/_ffmpeg.py b/ffmpeg/_ffmpeg.py new file mode 100644 index 0000000..fe7c8e4 --- /dev/null +++ b/ffmpeg/_ffmpeg.py @@ -0,0 +1,34 @@ +from .nodes import ( + FilterNode, + GlobalNode, + InputNode, + operator, + OutputNode, +) + + +def file_input(filename): + return InputNode(file_input.__name__, filename=filename) + + +@operator(node_classes={OutputNode, GlobalNode}) +def overwrite_output(parent): + return GlobalNode(parent, overwrite_output.__name__) + + +@operator(node_classes={OutputNode}) +def merge_outputs(*parents): + return OutputNode(parents, merge_outputs.__name__) + + +@operator(node_classes={InputNode, FilterNode}) +def file_output(parent, filename): + return OutputNode([parent], file_output.__name__, filename=filename) + + +__all__ = [ + 'file_input', + 'file_output', + 'merge_outputs', + 'overwrite_output', +] diff --git a/ffmpeg/_filters.py b/ffmpeg/_filters.py new file mode 100644 index 0000000..437ab8b --- /dev/null +++ b/ffmpeg/_filters.py @@ -0,0 +1,79 @@ +from .nodes import ( + FilterNode, + operator, +) + + +@operator() +def setpts(parent, expr): + return FilterNode([parent], setpts.__name__, expr) + + +@operator() +def trim(parent, **kwargs): + return FilterNode([parent], trim.__name__, **kwargs) + + +@operator() +def overlay(main_parent, overlay_parent, eof_action='repeat', **kwargs): + kwargs['eof_action'] = eof_action + return FilterNode([main_parent, overlay_parent], overlay.__name__, **kwargs) + + +@operator() +def hflip(parent): + return FilterNode([parent], hflip.__name__) + + +@operator() +def vflip(parent): + return FilterNode([parent], vflip.__name__) + + +@operator() +def drawbox(parent, x, y, width, height, color, thickness=None, **kwargs): + if thickness: + kwargs['t'] = thickness + return FilterNode([parent], drawbox.__name__, x, y, width, height, color, **kwargs) + + +@operator() +def concat(*parents, **kwargs): + kwargs['n'] = len(parents) + return FilterNode(parents, concat.__name__, **kwargs) + + +@operator() +def zoompan(parent, **kwargs): + return FilterNode([parent], zoompan.__name__, **kwargs) + + +@operator() +def hue(parent, **kwargs): + return FilterNode([parent], hue.__name__, **kwargs) + + +@operator() +def colorchannelmixer(parent, *args, **kwargs): + """Adjust video input frames by re-mixing color channels. + + `FFmpeg colorchannelmixer filter`_ + + .. _FFmpeg colorchannelmixer filter: + https://ffmpeg.org/ffmpeg-filters.html#toc-colorchannelmixer + """ + return FilterNode([parent], colorchannelmixer.__name__, **kwargs) + + +__all__ = [ + 'colorchannelmixer', + 'concat', + 'drawbox', + 'hflip', + 'hue', + 'overlay', + 'setpts', + 'trim', + 'vflip', + 'zoompan', +] diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py new file mode 100644 index 0000000..501f1af --- /dev/null +++ b/ffmpeg/_run.py @@ -0,0 +1,112 @@ +import operator as _operator +import subprocess as _subprocess + +from ._ffmpeg import ( + file_input, + file_output, + merge_outputs, + overwrite_output, +) +from .nodes import ( + GlobalNode, + InputNode, + operator, + OutputNode, +) + +def _get_stream_name(name): + return '[{}]'.format(name) + + +def _get_input_args(input_node): + if input_node._name == file_input.__name__: + args = ['-i', input_node._kwargs['filename']] + else: + assert False, 'Unsupported input node: {}'.format(input_node) + return args + + +def _topo_sort(start_node): + marked_nodes = [] + sorted_nodes = [] + child_map = {} + def visit(node, child): + assert node not in marked_nodes, 'Graph is not a DAG' + if child is not None: + if node not in child_map: + child_map[node] = [] + child_map[node].append(child) + if node not in sorted_nodes: + marked_nodes.append(node) + [visit(parent, node) for parent in node._parents] + marked_nodes.remove(node) + sorted_nodes.append(node) + unmarked_nodes = [start_node] + while unmarked_nodes: + visit(unmarked_nodes.pop(), None) + return sorted_nodes, child_map + + +def _get_filter_spec(i, node, stream_name_map): + stream_name = _get_stream_name('v{}'.format(i)) + stream_name_map[node] = stream_name + inputs = [stream_name_map[parent] for parent in node._parents] + filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(), stream_name) + return filter_spec + + +def _get_filter_arg(filter_nodes, stream_name_map): + filter_specs = [_get_filter_spec(i, node, stream_name_map) for i, node in enumerate(filter_nodes)] + return ';'.join(filter_specs) + + +def _get_global_args(node): + if node._name == overwrite_output.__name__: + return ['-y'] + else: + assert False, 'Unsupported global node: {}'.format(node) + + +def _get_output_args(node, stream_name_map): + args = [] + if node._name != merge_outputs.__name__: + stream_name = stream_name_map[node._parents[0]] + if stream_name != '[0]': + args += ['-map', stream_name] + if node._name == file_output.__name__: + args += [node._kwargs['filename']] + else: + assert False, 'Unsupported output node: {}'.format(node) + return args + + +@operator(node_classes={OutputNode, GlobalNode}) +def get_args(parent): + args = [] + # TODO: group nodes together, e.g. `-i somefile -r somerate`. + sorted_nodes, child_map = _topo_sort(parent) + input_nodes = [node for node in sorted_nodes if isinstance(node, InputNode)] + output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode) and not + isinstance(node, GlobalNode)] + global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)] + filter_nodes = [node for node in sorted_nodes if node not in (input_nodes + output_nodes + global_nodes)] + stream_name_map = {node: _get_stream_name(i) for i, node in enumerate(input_nodes)} + filter_arg = _get_filter_arg(filter_nodes, stream_name_map) + args += reduce(_operator.add, [_get_input_args(node) for node in input_nodes]) + if filter_arg: + args += ['-filter_complex', filter_arg] + args += reduce(_operator.add, [_get_output_args(node, stream_name_map) for node in output_nodes]) + args += reduce(_operator.add, [_get_global_args(node) for node in global_nodes], []) + return args + + +@operator(node_classes={OutputNode, GlobalNode}) +def run(parent, cmd='ffmpeg'): + args = [cmd] + parent.get_args() + _subprocess.check_call(args) + + +__all__ = [ + 'get_args', + 'run', +] diff --git a/ffmpeg/nodes.py b/ffmpeg/nodes.py new file mode 100644 index 0000000..61c9ff0 --- /dev/null +++ b/ffmpeg/nodes.py @@ -0,0 +1,66 @@ +import hashlib +import json + + +class Node(object): + """Node base""" + def __init__(self, parents, name, *args, **kwargs): + parent_hashes = [parent._hash for parent in parents] + assert len(parent_hashes) == len(set(parent_hashes)), 'Same node cannot be included as parent multiple times' + self._parents = parents + self._name = name + self._args = args + self._kwargs = kwargs + self._update_hash() + + def __repr__(self): + formatted_props = ['{}'.format(arg) for arg in self._args] + formatted_props += ['{}={!r}'.format(key, self._kwargs[key]) for key in sorted(self._kwargs)] + return '{}({})'.format(self._name, ','.join(formatted_props)) + + def __eq__(self, other): + return self._hash == other._hash + + def _update_hash(self): + props = {'args': self._args, 'kwargs': self._kwargs} + my_hash = hashlib.md5(json.dumps(props, sort_keys=True)).hexdigest() + parent_hashes = [parent._hash for parent in self._parents] + hashes = parent_hashes + [my_hash] + self._hash = hashlib.md5(','.join(hashes)).hexdigest() + + +class InputNode(Node): + """InputNode type""" + def __init__(self, name, *args, **kwargs): + super(InputNode, self).__init__(parents=[], name=name, *args, **kwargs) + + +class FilterNode(Node): + """FilterNode""" + def _get_filter(self): + params_text = self._name + arg_params = ['{}'.format(arg) for arg in self._args] + kwarg_params = ['{}={}'.format(k, self._kwargs[k]) for k in sorted(self._kwargs)] + params = arg_params + kwarg_params + if params: + params_text += '={}'.format(':'.join(params)) + return params_text + + +class OutputNode(Node): + """OutputNode""" + pass + + +class GlobalNode(Node): + def __init__(self, parent, name, *args, **kwargs): + assert isinstance(parent, OutputNode), 'Global nodes can only be attached after output nodes' + super(GlobalNode, self).__init__([parent], name, *args, **kwargs) + + +def operator(node_classes={Node}, name=None): + def decorator(func): + func_name = name or func.__name__ + [setattr(node_class, func_name, func) for node_class in node_classes] + return func + return decorator diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index da1531d..2a6fc58 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -1,4 +1,3 @@ - import ffmpeg import os import subprocess