diff --git a/.gitignore b/.gitignore index 780f20e..d20e29d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ .cache .eggs .tox/ +.vscode/ dist/ ffmpeg/tests/sample_data/out*.mp4 ffmpeg_python.egg-info/ venv* build/ +*.pyc diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b5f2aa4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.linting.pylintEnabled": false, + "python.linting.pep8Enabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/ffmpeg/_ffmpeg.py b/ffmpeg/_ffmpeg.py index 007624b..1e4fbbc 100644 --- a/ffmpeg/_ffmpeg.py +++ b/ffmpeg/_ffmpeg.py @@ -6,6 +6,7 @@ from ._utils import basestring from .nodes import ( filter_operator, GlobalNode, + HeaderNode, InputNode, MergeOutputsNode, OutputNode, @@ -13,7 +14,8 @@ from .nodes import ( ) -def input(filename, **kwargs): +@filter_operator() +def input(*streams_and_filename, **kwargs): """Input file URL (ffmpeg ``-i`` option) Any supplied kwargs are passed to ffmpeg verbatim (e.g. ``t=20``, @@ -23,13 +25,26 @@ def input(filename, **kwargs): Official documentation: `Main options `__ """ - kwargs['filename'] = filename + streams_and_filename = list(streams_and_filename) + if 'filename' not in kwargs: + if not isinstance(streams_and_filename[-1], basestring): + raise ValueError('A filename must be provided') + kwargs['filename'] = streams_and_filename.pop(-1) + streams = streams_and_filename + fmt = kwargs.pop('f', None) if fmt: if 'format' in kwargs: raise ValueError("Can't specify both `format` and `f` kwargs") kwargs['format'] = fmt - return InputNode(input.__name__, kwargs=kwargs).stream() + return InputNode(name=input.__name__, stream=streams, kwargs=kwargs).stream() + + +def header(*args, **kwargs): + """Add extra header command-line argument(s), e.g. ``-re``. + """ + stream = None + return HeaderNode(name=header.__name__, args=args, kwargs=kwargs).stream() @output_operator() @@ -92,4 +107,4 @@ def output(*streams_and_filename, **kwargs): return OutputNode(streams, output.__name__, kwargs=kwargs).stream() -__all__ = ['input', 'merge_outputs', 'output', 'overwrite_output'] +__all__ = ['header', 'input', 'merge_outputs', 'output', 'overwrite_output'] diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index f42d1d7..8457a32 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -12,6 +12,7 @@ from .nodes import ( get_stream_spec_nodes, FilterNode, GlobalNode, + HeaderNode, InputNode, OutputNode, output_operator, @@ -57,8 +58,8 @@ def _format_input_stream_name(stream_name_map, edge, is_final_arg=False): else: suffix = ':{}'.format(edge.upstream_selector) if is_final_arg and isinstance(edge.upstream_node, InputNode): - ## Special case: `-map` args should not have brackets for input - ## nodes. + # Special case: `-map` args should not have brackets for input + # nodes. fmt = '{}{}' else: fmt = '[{}{}]' @@ -72,6 +73,7 @@ def _format_output_stream_name(stream_name_map, edge): def _get_filter_spec(node, outgoing_edge_map, stream_name_map): incoming_edges = node.incoming_edges outgoing_edges = get_outgoing_edges(node, outgoing_edge_map) + inputs = [ _format_input_stream_name(stream_name_map, edge) for edge in incoming_edges ] @@ -110,6 +112,15 @@ def _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map): return ';'.join(filter_specs) +def _get_header_args(node): + kwargs = copy.copy(node.kwargs) + args = [] + for arg in node.args: + args += arg + args += convert_kwargs_to_cmd_line_args(kwargs) + return args + + def _get_global_args(node): return list(node.args) @@ -123,7 +134,6 @@ def _get_output_args(node, stream_name_map): raise ValueError('Output node {} has no mapped streams'.format(node)) for edge in node.incoming_edges: - # edge = node.incoming_edges[0] stream_name = _format_input_stream_name( stream_name_map, edge, is_final_arg=True ) @@ -155,12 +165,15 @@ def get_args(stream_spec, overwrite_output=False): args = [] # TODO: group nodes together, e.g. `-i somefile -r somerate`. sorted_nodes, outgoing_edge_maps = topo_sort(nodes) + header_nodes = [node for node in sorted_nodes if isinstance(node, HeaderNode)] + input_nodes = [node for node in sorted_nodes if isinstance(node, InputNode)] output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode)] global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)] filter_nodes = [node for node in sorted_nodes if isinstance(node, FilterNode)] stream_name_map = {(node, None): str(i) for i, node in enumerate(input_nodes)} filter_arg = _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map) + args += reduce(operator.add, [_get_header_args(node) for node in header_nodes], []) args += reduce(operator.add, [_get_input_args(node) for node in input_nodes]) if filter_arg: args += ['-filter_complex', filter_arg] diff --git a/ffmpeg/nodes.py b/ffmpeg/nodes.py index e8b2838..50dbde1 100644 --- a/ffmpeg/nodes.py +++ b/ffmpeg/nodes.py @@ -238,7 +238,7 @@ class Node(KwargReprNode): class FilterableStream(Stream): def __init__(self, upstream_node, upstream_label, upstream_selector=None): super(FilterableStream, self).__init__( - upstream_node, upstream_label, {InputNode, FilterNode}, upstream_selector + upstream_node, upstream_label, {HeaderNode, InputNode, FilterNode}, upstream_selector ) @@ -246,14 +246,14 @@ class FilterableStream(Stream): class InputNode(Node): """InputNode type""" - def __init__(self, name, args=[], kwargs={}): + def __init__(self, name, stream, args=[], kwargs={}): super(InputNode, self).__init__( - stream_spec=None, + stream_spec=stream, name=name, - incoming_stream_types={}, + incoming_stream_types={FilterableStream}, outgoing_stream_type=FilterableStream, min_inputs=0, - max_inputs=0, + max_inputs=None, args=args, kwargs=kwargs, ) @@ -345,7 +345,23 @@ class MergeOutputsNode(Node): ) +class HeaderNode (Node): + + def __init__(self, name, args=[], kwargs={}): + super(HeaderNode, self).__init__( + stream_spec=None, + name=name, + incoming_stream_types={}, + outgoing_stream_type=FilterableStream, + min_inputs=0, + max_inputs=0, + args=args, + kwargs=kwargs, + ) + # noinspection PyMethodOverriding + + class GlobalNode(Node): def __init__(self, stream, name, args=[], kwargs={}): super(GlobalNode, self).__init__( diff --git a/ffmpeg/tests/test_facebook.py b/ffmpeg/tests/test_facebook.py new file mode 100644 index 0000000..a670912 --- /dev/null +++ b/ffmpeg/tests/test_facebook.py @@ -0,0 +1,54 @@ +from __future__ import unicode_literals + +import os +import random +import re +import subprocess +from builtins import bytes, range, str + +import ffmpeg +import pytest +from ffmpeg import nodes + +try: + import mock # python 2 +except ImportError: + from unittest import mock # python 3 + + +def pip_to_rtmp(url): + """ Capture Facetime camera and screen in macOS and stream it to facebook + """ + facetime_camera_input = ( + ffmpeg + .input('FaceTime:1', format='avfoundation', pix_fmt='uyvy422', framerate=30, s='320x240', probesize='200M') + ) + + audio = facetime_camera_input.audio + + # ffmpeg in MacOS AVFoundation MUST increase the thread_queue_size in order to handle PIP + video = ( + ffmpeg + .header(thread_queue_size='512', vsync='2') + .input('1:1', format='avfoundation', + pix_fmt='uyvy422', framerate=30, probesize='200M') + .overlay(facetime_camera_input) + + ) + output = ( + ffmpeg + .output(video, audio, url, vsync='2', s='1280x720', pix_fmt='yuv420p', video_bitrate='1500000', f='flv', + vcodec='libx264', preset='fast', x264opts='keyint=15', g='30', ac='2', ar='48000', acodec="aac", audio_bitrate="128000") + ) + + return output.overwrite_output().compile() + + +def test_rtmp(): + expected_result = ['ffmpeg', '-thread_queue_size', '512', '-vsync', '2', '-f', 'avfoundation', '-framerate', '30', '-pix_fmt', 'uyvy422', '-probesize', '200M', '-i', '1:1', '-f', 'avfoundation', '-framerate', '30', '-pix_fmt', 'uyvy422', '-probesize', '200M', '-s', '320x240', '-i', 'FaceTime:1', '-filter_complex', '[0][1]overlay=eof_action=repeat[s0]', + '-map', '[s0]', '-map', '1:a', '-f', 'flv', '-b:v', '1500000', '-b:a', '128000', '-ac', '2', '-acodec', 'aac', '-ar', '48000', '-g', '30', '-pix_fmt', 'yuv420p', '-preset', 'fast', '-s', '1280x720', '-vcodec', 'libx264', '-vsync', '2', '-x264opts', 'keyint=15', 'rtmps://live-api-s.facebook.com:443/rtmp/input_your_facebook_stream_key_here', '-y'] + # Your facebook key should look like: 123456789012345?s_bl=1&s_ps=1&s_sml=1&s_sw=0&s_vt=api-s&a=AbCdEfGhiJK12345 + your_facebook_key = 'input_your_facebook_stream_key_here' + facebook_stream_url = 'rtmps://live-api-s.facebook.com:443/rtmp/{}'.format(your_facebook_key) + result = pip_to_rtmp(facebook_stream_url) + assert result == expected_result diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 8dbc271..d9ec9f0 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -152,6 +152,22 @@ def test_global_args(): ] +def test_header_args(): + + out_file = ( + ffmpeg.header(thread_queue_size='512') + .input("input.mp4") + .output("output.mp4") + ) + assert out_file.get_args() == [ + '-thread_queue_size', + '512', + '-i', + 'input.mp4', + 'output.mp4', + ] + + def _get_simple_example(): return ffmpeg.input(TEST_INPUT_FILE1).output(TEST_OUTPUT_FILE1) @@ -740,7 +756,7 @@ def test_pipe(): out_data = p.stdout.read() assert len(out_data) == frame_size * (frame_count - start_frame) - assert out_data == in_data[start_frame * frame_size :] + assert out_data == in_data[start_frame * frame_size:] def test__probe(): diff --git a/requirements.txt b/requirements.txt index f8b347e..1034d9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ alabaster==0.7.12 atomicwrites==1.3.0 attrs==19.1.0 +autopep8==1.4.4 Babel==2.7.0 certifi==2019.3.9 chardet==3.0.4