From 6bd788a83f947a4a05b7dc0b2fde1c969de402a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 17 Jan 2017 13:03:27 +0100 Subject: [PATCH] Always define pixfmt for timelapse in video filter chain Apparently having a chain AND the pix_fmt parameter produces issues with higher resolutions. Should fix #1317 --- src/octoprint/timelapse.py | 41 ++++--------- tests/timelapse/test_timelapse_renderjob.py | 66 +++++++++++++++++++++ 2 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 tests/timelapse/test_timelapse_renderjob.py diff --git a/src/octoprint/timelapse.py b/src/octoprint/timelapse.py index 5e913bf6..67c9407f 100644 --- a/src/octoprint/timelapse.py +++ b/src/octoprint/timelapse.py @@ -809,17 +809,10 @@ class TimelapseRenderJob(object): @classmethod def _create_ffmpeg_command_string(cls, ffmpeg, fps, bitrate, threads, input, output, hflip=False, vflip=False, - rotate=False, watermark=None): + rotate=False, watermark=None, pixfmt="yuv420p"): """ Create ffmpeg command string based on input parameters. - Examples: - - >>> TimelapseRenderJob._create_ffmpeg_command_string("/path/to/ffmpeg", 25, "10000k", 1, "/path/to/input/files_%d.jpg", "/path/to/output.mpg") - '/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 1 -pix_fmt yuv420p -r 25 -y -b 10000k -f vob "/path/to/output.mpg"' - >>> TimelapseRenderJob._create_ffmpeg_command_string("/path/to/ffmpeg", 25, "10000k", 1, "/path/to/input/files_%d.jpg", "/path/to/output.mpg", hflip=True) - '/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 1 -pix_fmt yuv420p -r 25 -y -b 10000k -f vob -vf \\'[in] hflip [out]\\' "/path/to/output.mpg"' - Arguments: ffmpeg (str): Path to ffmpeg fps (int): Frames per second for output @@ -831,16 +824,19 @@ class TimelapseRenderJob(object): vflip (bool): Perform vertical flip on input material. rotate (bool): Perform 90° CCW rotation on input material. watermark (str): Path to watermark to apply to lower left corner. + pixfmt (str): Pixel format to use for output. Default of yuv420p should usually fit the bill. Returns: (str): Prepared command string to render `input` to `output` using ffmpeg. """ + ### See unit tests in test/timelapse/test_timelapse_renderjob.py + logger = logging.getLogger(__name__) command = [ ffmpeg, '-framerate', str(fps), '-loglevel', 'error', '-i', '"{}"'.format(input), '-vcodec', 'mpeg2video', - '-threads', str(threads), '-pix_fmt', 'yuv420p', '-r', "25", '-y', '-b', str(bitrate), + '-threads', str(threads), '-r', "25", '-y', '-b', str(bitrate), '-f', 'vob'] filter_string = cls._create_filter_string(hflip=hflip, @@ -859,38 +855,25 @@ class TimelapseRenderJob(object): return " ".join(command) @classmethod - def _create_filter_string(cls, hflip=False, vflip=False, rotate=False, watermark=None): + def _create_filter_string(cls, hflip=False, vflip=False, rotate=False, watermark=None, pixfmt="yuv420p"): """ Creates an ffmpeg filter string based on input parameters. - Examples: - - >>> TimelapseRenderJob._create_filter_string() - >>> TimelapseRenderJob._create_filter_string(hflip=True) - '[in] hflip [out]' - >>> TimelapseRenderJob._create_filter_string(vflip=True) - '[in] vflip [out]' - >>> TimelapseRenderJob._create_filter_string(rotate=True) - '[in] transpose=2 [out]' - >>> TimelapseRenderJob._create_filter_string(vflip=True, rotate=True) - '[in] vflip,transpose=2 [out]' - >>> TimelapseRenderJob._create_filter_string(vflip=True, hflip=True, rotate=True) - '[in] hflip,vflip,transpose=2 [out]' - >>> TimelapseRenderJob._create_filter_string(watermark="/path/to/watermark.png") - 'movie=/path/to/watermark.png [wm]; [in][wm] overlay=10:main_h-overlay_h-10 [out]' - >>> TimelapseRenderJob._create_filter_string(hflip=True, watermark="/path/to/watermark.png") - '[in] hflip [postprocessed]; movie=/path/to/watermark.png [wm]; [postprocessed][wm] overlay=10:main_h-overlay_h-10 [out]' - Arguments: hflip (bool): Perform horizontal flip on input material. vflip (bool): Perform vertical flip on input material. rotate (bool): Perform 90° CCW rotation on input material. watermark (str): Path to watermark to apply to lower left corner. + pixfmt (str): Pixel format to use, defaults to "yuv420p" which should usually fit the bill Returns: (str or None): filter string or None if no filters are required """ - filters = [] + + ### See unit tests in test/timelapse/test_timelapse_renderjob.py + + # apply pixel format + filters = ["format={}".format(pixfmt)] # flip video if configured if hflip: diff --git a/tests/timelapse/test_timelapse_renderjob.py b/tests/timelapse/test_timelapse_renderjob.py new file mode 100644 index 00000000..e8663e38 --- /dev/null +++ b/tests/timelapse/test_timelapse_renderjob.py @@ -0,0 +1,66 @@ +# coding=utf-8 +from __future__ import absolute_import + +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2016 The OctoPrint Project - Released under terms of the AGPLv3 License" + +import unittest + +from ddt import ddt, data, unpack + +from octoprint.timelapse import TimelapseRenderJob + +@ddt +class TimelapseRenderJobTest(unittest.TestCase): + + @data( + (("/path/to/ffmpeg", 25, "10000k", 1, "/path/to/input/files_%d.jpg", "/path/to/output.mpg"), + dict(), + '/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 1 -r 25 -y -b 10000k -f vob -vf \'[in] format=yuv420p [out]\' "/path/to/output.mpg"'), + + (("/path/to/ffmpeg", 25, "10000k", 1, "/path/to/input/files_%d.jpg", "/path/to/output.mpg"), + dict(hflip=True), + '/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 1 -r 25 -y -b 10000k -f vob -vf \'[in] format=yuv420p,hflip [out]\' "/path/to/output.mpg"'), + + (("/path/to/ffmpeg", 25, "20000k", 4, "/path/to/input/files_%d.jpg", "/path/to/output.mpg"), + dict(rotate=True, watermark="/path/to/watermark.png"), + '/path/to/ffmpeg -framerate 25 -loglevel error -i "/path/to/input/files_%d.jpg" -vcodec mpeg2video -threads 4 -r 25 -y -b 20000k -f vob -vf \'[in] format=yuv420p,transpose=2 [postprocessed]; movie=/path/to/watermark.png [wm]; [postprocessed][wm] overlay=10:main_h-overlay_h-10 [out]\' "/path/to/output.mpg"') + ) + @unpack + def test_create_ffmpeg_command_string(self, args, kwargs, expected): + actual = TimelapseRenderJob._create_ffmpeg_command_string(*args, **kwargs) + self.assertEquals(actual, expected) + + @data( + (dict(), + '[in] format=yuv420p [out]'), + + (dict(pixfmt="test"), + '[in] format=test [out]'), + + (dict(hflip=True), + '[in] format=yuv420p,hflip [out]'), + + (dict(vflip=True), + '[in] format=yuv420p,vflip [out]'), + + (dict(rotate=True), + '[in] format=yuv420p,transpose=2 [out]'), + + (dict(vflip=True, rotate=True), + '[in] format=yuv420p,vflip,transpose=2 [out]'), + + (dict(vflip=True, hflip=True, rotate=True), + '[in] format=yuv420p,hflip,vflip,transpose=2 [out]'), + + (dict(watermark="/path/to/watermark.png"), + '[in] format=yuv420p [postprocessed]; movie=/path/to/watermark.png [wm]; [postprocessed][wm] overlay=10:main_h-overlay_h-10 [out]'), + + (dict(hflip=True, watermark="/path/to/watermark.png"), + '[in] format=yuv420p,hflip [postprocessed]; movie=/path/to/watermark.png [wm]; [postprocessed][wm] overlay=10:main_h-overlay_h-10 [out]'), + + ) + @unpack + def test_create_filter_string(self, kwargs, expected): + actual = TimelapseRenderJob._create_filter_string(**kwargs) + self.assertEquals(actual, expected)