diff --git a/README.md b/README.md
index e4e46ec..336ad77 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,10 @@ $ python
>>> import ffmpeg
```
+## [Examples](https://github.com/kkroening/ffmpeg-python/tree/master/examples)
+
+When in doubt, take a look at the [examples](https://github.com/kkroening/ffmpeg-python/tree/master/examples) to see if there's something that's close to whatever you're trying to do.
+
## [API Reference](https://kkroening.github.io/ffmpeg-python/)
API documentation is automatically generated from python docstrings and hosted on github pages: https://kkroening.github.io/ffmpeg-python/
@@ -140,6 +144,13 @@ Pull requests are welcome as well.
+### Special thanks
+
+- [Arne de Laat](https://github.com/153957)
+- [Davide Depau](https://github.com/depau)
+- [Dim](https://github.com/lloti)
+- [Noah Stier](https://github.com/noahstier)
+
## Additional Resources
- [API Reference](https://kkroening.github.io/ffmpeg-python/)
diff --git a/doc/jupyter-screenshot.png b/doc/jupyter-screenshot.png
new file mode 100644
index 0000000..6b16031
Binary files /dev/null and b/doc/jupyter-screenshot.png differ
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..bf7997c
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,63 @@
+# Examples
+
+## [Get video info](https://github.com/kkroening/ffmpeg-python/blob/master/examples/video_info.py#L15)
+
+```python
+probe = ffmpeg.probe(args.in_filename)
+video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
+width = int(video_stream['width'])
+height = int(video_stream['height'])
+```
+
+## [Convert video to numpy array](https://github.com/kkroening/ffmpeg-python/blob/master/examples/ffmpeg-numpy.ipynb)
+
+```python
+out, _ = (
+ ffmpeg
+ .input('in.mp4')
+ .output('pipe:', format='rawvideo', pix_fmt='rgb24')
+ .run(capture_stdout=True)
+)
+video = (
+ np
+ .frombuffer(out, np.uint8)
+ .reshape([-1, height, width, 3])
+)
+```
+
+## [Generate thumbnail for video](https://github.com/kkroening/ffmpeg-python/blob/master/examples/get_video_thumbnail.py#L21)
+```python
+(
+ ffmpeg
+ .input(in_filename, ss=time)
+ .filter_('scale', width, -1)
+ .output(out_filename, vframes=1)
+ .run()
+)
+```
+
+## [Read single video frame as jpeg through pipe](https://github.com/kkroening/ffmpeg-python/blob/master/examples/read_frame_as_jpeg.py#L16)
+```python
+out, _ = (
+ ffmpeg
+ .input(in_filename)
+ .filter_('select', 'gte(n,{})'.format(frame_num))
+ .output('pipe:', vframes=1, format='image2', vcodec='mjpeg')
+ .run(capture_output=True)
+)
+```
+
+## [Convert sound to raw PCM audio](https://github.com/kkroening/ffmpeg-python/blob/master/examples/transcribe.py#L23)
+```python
+out, _ = (ffmpeg
+ .input(in_filename, **input_kwargs)
+ .output('-', format='s16le', acodec='pcm_s16le', ac=1, ar='16k')
+ .overwrite_output()
+ .run(capture_stdout=True)
+)
+```
+
+## [JupyterLab/Notebook widgets](https://github.com/kkroening/ffmpeg-python/blob/master/examples/ffmpeg-numpy.ipynb)
+
+
+
diff --git a/examples/ffmpeg-numpy.ipynb b/examples/ffmpeg-numpy.ipynb
new file mode 100644
index 0000000..c64d9ab
--- /dev/null
+++ b/examples/ffmpeg-numpy.ipynb
@@ -0,0 +1,103 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 116,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from ipywidgets import interact\n",
+ "from matplotlib import pyplot as plt\n",
+ "import ffmpeg\n",
+ "import ipywidgets as widgets\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 117,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "probe = ffmpeg.probe('in.mp4')\n",
+ "video_info = next(stream for stream in probe['streams'] if stream['codec_type'] == 'video')\n",
+ "width = int(video_info['width'])\n",
+ "height = int(video_info['height'])\n",
+ "num_frames = int(video_info['nb_frames'])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 118,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "out, err = (\n",
+ " ffmpeg\n",
+ " .input('in.mp4')\n",
+ " .output('pipe:', format='rawvideo', pix_fmt='rgb24')\n",
+ " .run(capture_stdout=True)\n",
+ ")\n",
+ "video = (\n",
+ " np\n",
+ " .frombuffer(out, np.uint8)\n",
+ " .reshape([-1, height, width, 3])\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 115,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "17d13d7551114fb39a1fad933cf0398a",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "interactive(children=(IntSlider(value=0, description='frame', max=209), Output()), _dom_classes=('widget-inter…"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "@interact(frame=(0, num_frames))\n",
+ "def show_frame(frame=0):\n",
+ " plt.imshow(video[frame,:,:,:])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.4"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/examples/get_video_thumbnail.py b/examples/get_video_thumbnail.py
new file mode 100755
index 0000000..0a006d5
--- /dev/null
+++ b/examples/get_video_thumbnail.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+from __future__ import unicode_literals, print_function
+import argparse
+import ffmpeg
+import sys
+
+
+parser = argparse.ArgumentParser(description='Generate video thumbnail')
+parser.add_argument('in_filename', help='Input filename')
+parser.add_argument('out_filename', help='Output filename')
+parser.add_argument(
+ '--time', type=int, default=0.1, help='Time offset')
+parser.add_argument(
+ '--width', type=int, default=120,
+ help='Width of output thumbnail (height automatically determined by aspect ratio)')
+
+
+def generate_thumbnail(in_filename, out_filename, time, width):
+ try:
+ (
+ ffmpeg
+ .input(in_filename, ss=time)
+ .filter_('scale', width, -1)
+ .output(out_filename, vframes=1)
+ .overwrite_output()
+ .run(capture_stdout=True, capture_stderr=True)
+ )
+ except ffmpeg.Error as e:
+ print(e.stderr.decode(), file=sys.stderr)
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ generate_thumbnail(args.in_filename, args.out_filename, args.time, args.width)
diff --git a/examples/read_frame_as_jpeg.py b/examples/read_frame_as_jpeg.py
new file mode 100755
index 0000000..e7ec52c
--- /dev/null
+++ b/examples/read_frame_as_jpeg.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+from __future__ import unicode_literals
+import argparse
+import ffmpeg
+import sys
+
+
+parser = argparse.ArgumentParser(
+ description='Read individual video frame into memory as jpeg and write to stdout')
+parser.add_argument('in_filename', help='Input filename')
+parser.add_argument('frame_num', help='Frame number')
+
+
+def read_frame_as_jpeg(in_filename, frame_num):
+ out, err = (
+ ffmpeg
+ .input(in_filename)
+ .filter_('select', 'gte(n,{})'.format(frame_num))
+ .output('pipe:', vframes=1, format='image2', vcodec='mjpeg')
+ .run(capture_stdout=True)
+ )
+ return out
+
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ out = read_frame_as_jpeg(args.in_filename, args.frame_num)
+ sys.stdout.buffer.write(out)
diff --git a/examples/requirements.txt b/examples/requirements.txt
index d5f10fc..ca8ae60 100644
--- a/examples/requirements.txt
+++ b/examples/requirements.txt
@@ -1,2 +1,4 @@
ffmpeg-python
+gevent
google-cloud-speech
+tqdm
diff --git a/examples/show_progress.py b/examples/show_progress.py
new file mode 100755
index 0000000..dd0253a
--- /dev/null
+++ b/examples/show_progress.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+from __future__ import unicode_literals, print_function
+from tqdm import tqdm
+import argparse
+import contextlib
+import ffmpeg
+import gevent
+import gevent.monkey; gevent.monkey.patch_all(thread=False)
+import os
+import shutil
+import socket
+import sys
+import tempfile
+import textwrap
+
+
+parser = argparse.ArgumentParser(description=textwrap.dedent('''\
+ Process video and report and show progress bar.
+
+ This is an example of using the ffmpeg `-progress` option with a
+ unix-domain socket to report progress in the form of a progress
+ bar.
+
+ The video processing simply consists of converting the video to
+ sepia colors, but the same pattern can be applied to other use
+ cases.
+'''))
+
+parser.add_argument('in_filename', help='Input filename')
+parser.add_argument('out_filename', help='Output filename')
+
+
+@contextlib.contextmanager
+def _tmpdir_scope():
+ tmpdir = tempfile.mkdtemp()
+ try:
+ yield tmpdir
+ finally:
+ shutil.rmtree(tmpdir)
+
+
+def _do_watch_progress(filename, sock, handler):
+ """Function to run in a separate gevent greenlet to read progress
+ events from a unix-domain socket."""
+ connection, client_address = sock.accept()
+ data = b''
+ try:
+ while True:
+ more_data = connection.recv(16)
+ if not more_data:
+ break
+ data += more_data
+ lines = data.split(b'\n')
+ for line in lines[:-1]:
+ line = line.decode()
+ parts = line.split('=')
+ key = parts[0] if len(parts) > 0 else None
+ value = parts[1] if len(parts) > 1 else None
+ handler(key, value)
+ data = lines[-1]
+ finally:
+ connection.close()
+
+
+@contextlib.contextmanager
+def _watch_progress(handler):
+ """Context manager for creating a unix-domain socket and listen for
+ ffmpeg progress events.
+
+ The socket filename is yielded from the context manager and the
+ socket is closed when the context manager is exited.
+
+ Args:
+ handler: a function to be called when progress events are
+ received; receives a ``key`` argument and ``value``
+ argument. (The example ``show_progress`` below uses tqdm)
+
+ Yields:
+ socket_filename: the name of the socket file.
+ """
+ with _tmpdir_scope() as tmpdir:
+ socket_filename = os.path.join(tmpdir, 'sock')
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ with contextlib.closing(sock):
+ sock.bind(socket_filename)
+ sock.listen(1)
+ child = gevent.spawn(_do_watch_progress, socket_filename, sock, handler)
+ try:
+ yield socket_filename
+ except:
+ gevent.kill(child)
+ raise
+
+
+
+@contextlib.contextmanager
+def show_progress(total_duration):
+ """Create a unix-domain socket to watch progress and render tqdm
+ progress bar."""
+ with tqdm(total=round(total_duration, 2)) as bar:
+ def handler(key, value):
+ if key == 'out_time_ms':
+ time = round(float(value) / 1000000., 2)
+ bar.update(time - bar.n)
+ elif key == 'progress' and value == 'end':
+ bar.update(bar.total - bar.n)
+ with _watch_progress(handler) as socket_filename:
+ yield socket_filename
+
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+ total_duration = float(ffmpeg.probe(args.in_filename)['format']['duration'])
+
+ with show_progress(total_duration) as socket_filename:
+ # See https://ffmpeg.org/ffmpeg-filters.html#Examples-44
+ sepia_values = [.393, .769, .189, 0, .349, .686, .168, 0, .272, .534, .131]
+ try:
+ (ffmpeg
+ .input(args.in_filename)
+ .colorchannelmixer(*sepia_values)
+ .output(args.out_filename)
+ .global_args('-progress', 'unix://{}'.format(socket_filename))
+ .overwrite_output()
+ .run(capture_stdout=True, capture_stderr=True)
+ )
+ except ffmpeg.Error as e:
+ print(e.stderr, file=sys.stderr)
+ sys.exit(1)
+
diff --git a/examples/transcribe.py b/examples/transcribe.py
index fb484df..0b7200c 100755
--- a/examples/transcribe.py
+++ b/examples/transcribe.py
@@ -1,13 +1,11 @@
#!/usr/bin/env python
-from __future__ import unicode_literals
-
+from __future__ import unicode_literals, print_function
from google.cloud import speech
from google.cloud.speech import enums
from google.cloud.speech import types
import argparse
import ffmpeg
import logging
-import subprocess
import sys
@@ -21,21 +19,17 @@ parser.add_argument('in_filename', help='Input filename (`-` for stdin)')
def decode_audio(in_filename, **input_kwargs):
- p = subprocess.Popen(
- (ffmpeg
+ try:
+ out, err = (ffmpeg
.input(in_filename, **input_kwargs)
.output('-', format='s16le', acodec='pcm_s16le', ac=1, ar='16k')
.overwrite_output()
- .compile()
- ),
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE
- )
- out = p.communicate()
- if p.returncode != 0:
- sys.stderr.write(out[1])
+ .run(capture_stdout=True, capture_stderr=True)
+ )
+ except ffmpeg.Error as e:
+ print(e.stderr, file=sys.stderr)
sys.exit(1)
- return out[0]
+ return out
def get_transcripts(audio_data):
diff --git a/examples/video_info.py b/examples/video_info.py
new file mode 100755
index 0000000..df9c992
--- /dev/null
+++ b/examples/video_info.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+from __future__ import unicode_literals, print_function
+import argparse
+import ffmpeg
+import sys
+
+
+parser = argparse.ArgumentParser(description='Get video information')
+parser.add_argument('in_filename', help='Input filename')
+
+
+if __name__ == '__main__':
+ args = parser.parse_args()
+
+ try:
+ probe = ffmpeg.probe(args.in_filename)
+ except ffmpeg.Error as e:
+ print(e.stderr, file=sys.stderr)
+ sys.exit(1)
+
+ video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
+ if video_stream is None:
+ print('No video stream found', file=sys.stderr)
+ sys.exit(1)
+
+ width = int(video_stream['width'])
+ height = int(video_stream['height'])
+ num_frames = int(video_stream['nb_frames'])
+ print('width: {}'.format(width))
+ print('height: {}'.format(height))
+ print('num_frames: {}'.format(num_frames))
diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py
index e450a8b..cbc0676 100644
--- a/ffmpeg/_run.py
+++ b/ffmpeg/_run.py
@@ -1,10 +1,9 @@
from __future__ import unicode_literals
-
from .dag import get_outgoing_edges, topo_sort
from ._utils import basestring
from builtins import str
from functools import reduce
-from past.builtins import basestring
+import collections
import copy
import operator
import subprocess
@@ -126,6 +125,11 @@ def _get_output_args(node, stream_name_map):
args += ['-b:v', str(kwargs.pop('video_bitrate'))]
if 'audio_bitrate' in kwargs:
args += ['-b:a', str(kwargs.pop('audio_bitrate'))]
+ if 'video_size' in kwargs:
+ video_size = kwargs.pop('video_size')
+ if not isinstance(video_size, basestring) and isinstance(video_size, collections.Iterable):
+ video_size = '{}x{}'.format(video_size[0], video_size[1])
+ args += ['-video_size', video_size]
args += _convert_kwargs_to_cmd_line_args(kwargs)
args += [filename]
return args
diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py
index 0854379..49654bc 100644
--- a/ffmpeg/tests/test_ffmpeg.py
+++ b/ffmpeg/tests/test_ffmpeg.py
@@ -237,7 +237,6 @@ def test_filter_asplit():
]
-
def test__output__bitrate():
args = (
ffmpeg
@@ -248,6 +247,17 @@ def test__output__bitrate():
assert args == ['-i', 'in', '-b:v', '1000', '-b:a', '200', 'out']
+@pytest.mark.parametrize('video_size', [(320, 240), '320x240'])
+def test__output__video_size(video_size):
+ args = (
+ ffmpeg
+ .input('in')
+ .output('out', video_size=video_size)
+ .get_args()
+ )
+ assert args == ['-i', 'in', '-video_size', '320x240', 'out']
+
+
def test_filter_normal_arg_escape():
"""Test string escaping of normal filter args (e.g. ``font`` param of ``drawtext`` filter)."""
def _get_drawtext_font_repr(font):