Merge f839009802a786f4376d0b68bd799d5ecced8c7e into df129c7ba30aaa9ffffb81a48f53aa7253b0b4e6

This commit is contained in:
wkchan 2022-07-29 09:24:18 +09:00 committed by GitHub
commit 4bb00822d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 135 additions and 13 deletions

2
.gitignore vendored
View File

@ -1,8 +1,10 @@
.cache .cache
.eggs .eggs
.tox/ .tox/
.vscode/
dist/ dist/
ffmpeg/tests/sample_data/out*.mp4 ffmpeg/tests/sample_data/out*.mp4
ffmpeg_python.egg-info/ ffmpeg_python.egg-info/
venv* venv*
build/ build/
*.pyc

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"python.linting.pylintEnabled": false,
"python.linting.pep8Enabled": true,
"python.linting.enabled": true
}

View File

@ -6,6 +6,7 @@ from ._utils import basestring
from .nodes import ( from .nodes import (
filter_operator, filter_operator,
GlobalNode, GlobalNode,
HeaderNode,
InputNode, InputNode,
MergeOutputsNode, MergeOutputsNode,
OutputNode, 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) """Input file URL (ffmpeg ``-i`` option)
Any supplied kwargs are passed to ffmpeg verbatim (e.g. ``t=20``, Any supplied kwargs are passed to ffmpeg verbatim (e.g. ``t=20``,
@ -23,13 +25,26 @@ def input(filename, **kwargs):
Official documentation: `Main options <https://ffmpeg.org/ffmpeg.html#Main-options>`__ Official documentation: `Main options <https://ffmpeg.org/ffmpeg.html#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) fmt = kwargs.pop('f', None)
if fmt: if fmt:
if 'format' in kwargs: if 'format' in kwargs:
raise ValueError("Can't specify both `format` and `f` kwargs") raise ValueError("Can't specify both `format` and `f` kwargs")
kwargs['format'] = fmt 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() @output_operator()
@ -92,4 +107,4 @@ def output(*streams_and_filename, **kwargs):
return OutputNode(streams, output.__name__, kwargs=kwargs).stream() return OutputNode(streams, output.__name__, kwargs=kwargs).stream()
__all__ = ['input', 'merge_outputs', 'output', 'overwrite_output'] __all__ = ['header', 'input', 'merge_outputs', 'output', 'overwrite_output']

View File

@ -12,6 +12,7 @@ from .nodes import (
get_stream_spec_nodes, get_stream_spec_nodes,
FilterNode, FilterNode,
GlobalNode, GlobalNode,
HeaderNode,
InputNode, InputNode,
OutputNode, OutputNode,
output_operator, output_operator,
@ -57,8 +58,8 @@ def _format_input_stream_name(stream_name_map, edge, is_final_arg=False):
else: else:
suffix = ':{}'.format(edge.upstream_selector) suffix = ':{}'.format(edge.upstream_selector)
if is_final_arg and isinstance(edge.upstream_node, InputNode): if is_final_arg and isinstance(edge.upstream_node, InputNode):
## Special case: `-map` args should not have brackets for input # Special case: `-map` args should not have brackets for input
## nodes. # nodes.
fmt = '{}{}' fmt = '{}{}'
else: else:
fmt = '[{}{}]' 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): def _get_filter_spec(node, outgoing_edge_map, stream_name_map):
incoming_edges = node.incoming_edges incoming_edges = node.incoming_edges
outgoing_edges = get_outgoing_edges(node, outgoing_edge_map) outgoing_edges = get_outgoing_edges(node, outgoing_edge_map)
inputs = [ inputs = [
_format_input_stream_name(stream_name_map, edge) for edge in incoming_edges _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) 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): def _get_global_args(node):
return list(node.args) 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)) raise ValueError('Output node {} has no mapped streams'.format(node))
for edge in node.incoming_edges: for edge in node.incoming_edges:
# edge = node.incoming_edges[0]
stream_name = _format_input_stream_name( stream_name = _format_input_stream_name(
stream_name_map, edge, is_final_arg=True stream_name_map, edge, is_final_arg=True
) )
@ -155,12 +165,15 @@ def get_args(stream_spec, overwrite_output=False):
args = [] args = []
# TODO: group nodes together, e.g. `-i somefile -r somerate`. # TODO: group nodes together, e.g. `-i somefile -r somerate`.
sorted_nodes, outgoing_edge_maps = topo_sort(nodes) 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)] input_nodes = [node for node in sorted_nodes if isinstance(node, InputNode)]
output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode)] output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode)]
global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)] global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)]
filter_nodes = [node for node in sorted_nodes if isinstance(node, FilterNode)] 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)} 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) 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]) args += reduce(operator.add, [_get_input_args(node) for node in input_nodes])
if filter_arg: if filter_arg:
args += ['-filter_complex', filter_arg] args += ['-filter_complex', filter_arg]

View File

@ -238,7 +238,7 @@ class Node(KwargReprNode):
class FilterableStream(Stream): class FilterableStream(Stream):
def __init__(self, upstream_node, upstream_label, upstream_selector=None): def __init__(self, upstream_node, upstream_label, upstream_selector=None):
super(FilterableStream, self).__init__( 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): class InputNode(Node):
"""InputNode type""" """InputNode type"""
def __init__(self, name, args=[], kwargs={}): def __init__(self, name, stream, args=[], kwargs={}):
super(InputNode, self).__init__( super(InputNode, self).__init__(
stream_spec=None, stream_spec=stream,
name=name, name=name,
incoming_stream_types={}, incoming_stream_types={FilterableStream},
outgoing_stream_type=FilterableStream, outgoing_stream_type=FilterableStream,
min_inputs=0, min_inputs=0,
max_inputs=0, max_inputs=None,
args=args, args=args,
kwargs=kwargs, 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 # noinspection PyMethodOverriding
class GlobalNode(Node): class GlobalNode(Node):
def __init__(self, stream, name, args=[], kwargs={}): def __init__(self, stream, name, args=[], kwargs={}):
super(GlobalNode, self).__init__( super(GlobalNode, self).__init__(

View File

@ -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

View File

@ -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(): def _get_simple_example():
return ffmpeg.input(TEST_INPUT_FILE1).output(TEST_OUTPUT_FILE1) return ffmpeg.input(TEST_INPUT_FILE1).output(TEST_OUTPUT_FILE1)
@ -740,7 +756,7 @@ def test_pipe():
out_data = p.stdout.read() out_data = p.stdout.read()
assert len(out_data) == frame_size * (frame_count - start_frame) 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(): def test__probe():

View File

@ -1,6 +1,7 @@
alabaster==0.7.12 alabaster==0.7.12
atomicwrites==1.3.0 atomicwrites==1.3.0
attrs==19.1.0 attrs==19.1.0
autopep8==1.4.4
Babel==2.7.0 Babel==2.7.0
certifi==2019.3.9 certifi==2019.3.9
chardet==3.0.4 chardet==3.0.4