From 1de29a7d573ccbae30422f994c73f824b05ac681 Mon Sep 17 00:00:00 2001 From: Tom Burrows <22625968+tburrows13@users.noreply.github.com> Date: Mon, 12 Feb 2018 10:52:33 +0000 Subject: [PATCH 1/5] Update (#6) --- .travis.yml | 45 ++-- README.rst | 10 +- appveyor.yml | 161 ++++++++++++ appveyor/run_with_env.cmd | 86 +++++++ docs/advanced_tools/advanced_tools.py | 4 +- docs/examples/quick_recipes.rst | 2 +- docs/gallery.rst | 22 +- docs/getting_started/effects.rst | 2 +- docs/getting_started/efficient_moviepy.rst | 26 ++ docs/install.rst | 2 +- docs/ref/Clip.rst | 2 +- docs/ref/audiofx.rst | 6 +- .../moviepy.audio.fx.all.audio_normalize.rst | 6 + moviepy/Clip.py | 234 ++++++++++-------- moviepy/audio/AudioClip.py | 9 +- moviepy/audio/fx/audio_normalize.py | 9 + moviepy/audio/fx/volumex.py | 8 +- moviepy/audio/io/AudioFileClip.py | 43 ++-- moviepy/audio/io/ffmpeg_audiowriter.py | 28 ++- moviepy/audio/io/readers.py | 5 +- moviepy/config.py | 13 +- moviepy/editor.py | 4 +- moviepy/video/VideoClip.py | 1 + .../video/compositing/CompositeVideoClip.py | 28 ++- moviepy/video/compositing/transitions.py | 20 +- moviepy/video/io/VideoFileClip.py | 28 ++- moviepy/video/io/downloader.py | 18 +- moviepy/video/io/ffmpeg_reader.py | 28 +-- moviepy/video/io/ffmpeg_writer.py | 47 ++-- moviepy/video/io/gif_writers.py | 9 +- moviepy/video/tools/cuts.py | 4 +- moviepy/video/tools/subtitles.py | 3 +- setup.py | 53 +++- tests/download_media.py | 18 +- tests/test_ImageSequenceClip.py | 10 +- tests/test_PR.py | 54 ++-- tests/test_TextClip.py | 12 +- tests/test_VideoFileClip.py | 45 ++-- tests/test_Videos.py | 17 +- tests/test_compositing.py | 39 +-- tests/test_examples.py | 93 +++---- tests/test_fx.py | 217 +++++++++++++--- tests/test_helper.py | 14 +- tests/test_issues.py | 172 +++++++------ tests/test_misc.py | 12 +- tests/test_resourcerelease.py | 53 ++++ tests/test_resourcereleasedemo.py | 75 ++++++ tests/test_tools.py | 12 - 48 files changed, 1299 insertions(+), 510 deletions(-) create mode 100644 appveyor.yml create mode 100644 appveyor/run_with_env.cmd create mode 100644 docs/ref/audiofx/moviepy.audio.fx.all.audio_normalize.rst create mode 100644 moviepy/audio/fx/audio_normalize.py create mode 100644 tests/test_resourcerelease.py create mode 100644 tests/test_resourcereleasedemo.py diff --git a/.travis.yml b/.travis.yml index 83bdbf6de..7976b104e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,26 +1,43 @@ -dist: Trusty +dist: trusty +sudo: required language: python +cache: pip python: - "2.7" - "3.3" - "3.4" - "3.5" - "3.6" -# command to install dependencies + before_install: - sudo add-apt-repository -y ppa:kirillshkrogalev/ffmpeg-next - - sudo apt-get -y update - - sudo apt-get install -y ffmpeg + - sudo apt-get -y -qq update + - sudo apt-get install -y -qq ffmpeg - mkdir media -install: - - if [[ $TRAVIS_PYTHON_VERSION == '3.4' || $TRAVIS_PYTHON_VERSION == '3.5' || $TRAVIS_PYTHON_VERSION == '3.6' ]]; then pip install matplotlib; pip install -U scikit-learn; pip install scipy; pip install opencv-python; fi - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install scipy; pip install opencv-python; fi - - pip install coveralls - - pip install pytest-cov - - python setup.py install -# command to run tests -before_script: - - py.test tests/ --cov -script: py.test tests/ --doctest-modules -v --cov moviepy --cov-report term-missing + + # Ensure PIP is up-to-date to avoid warnings. + - python -m pip install --upgrade pip + # Ensure setuptools is up-to-date to avoid environment_markers bug. + - pip install --upgrade setuptools + # The default py that is installed is too old on some platforms, leading to version conflicts + - pip install --upgrade py pytest + +install: + - echo "No install action required. Implicitly performed by the testing." + +# before_script: + +script: + - python setup.py test --pytest-args "tests/ --doctest-modules -v --cov moviepy --cov-report term-missing" + # Now the *code* is tested, let's check that the setup is compatible with PIP without falling over. + - pip install -e . + - pip install -e .[optional] + - pip install -e .[test] + # Only test doc generation on latest. Doesn't work on some earlier versions (3.3), but doesn't matter. + - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then pip install -e .[doc]; fi + after_success: - coveralls + +matrix: + fast_finish: true diff --git a/README.rst b/README.rst index 4accdb53b..2375f8402 100644 --- a/README.rst +++ b/README.rst @@ -82,9 +82,11 @@ For advanced image processing, you will need one or several of the following pac Once you have installed it, ImageMagick will be automatically detected by MoviePy, (except for windows users and Ubuntu 16.04LTS users). -For Windows users, before installing MoviePy by hand, go into the ``moviepy/config_defaults.py`` file and provide the path to the ImageMagick binary called `convert`. It should look like this :: +For Windows users, before installing MoviePy by hand, go into the ``moviepy/config_defaults.py`` file and provide the path to the ImageMagick binary called ``magick``. It should look like this :: - IMAGEMAGICK_BINARY = "C:\\Program Files\\ImageMagick_VERSION\\convert.exe" + IMAGEMAGICK_BINARY = "C:\\Program Files\\ImageMagick_VERSION\\magick.exe" + +If you are using an older version of ImageMagick, keep in mind the name of the executable is not ``magick.exe`` but ``convert.exe``. In that case, the IMAGEMAGICK_BINARY property should be ``C:\\Program Files\\ImageMagick_VERSION\\convert.exe`` For Ubuntu 16.04LTS users, after installing MoviePy on the terminal, IMAGEMAGICK will not be detected by moviepy. This bug can be fixed. Modify the file in this directory: /etc/ImageMagick-6/policy.xml, comment out the statement . @@ -137,7 +139,7 @@ Maintainers - Zulko_ (owner) -- `@Gloin1313`_ +- `@tburrows13`_ - `@earney`_ - Kay `@kerstin`_ - `@mbeacom`_ @@ -171,7 +173,7 @@ Maintainers .. People .. _Zulko: https://github.com/Zulko -.. _`@Gloin1313`: https://github.com/Gloin1313 +.. _`@tburrows13`: https://github.com/tburrows13 .. _`@earney`: https://github.com/earney .. _`@kerstin`: https://github.com/kerstin .. _`@mbeacom`: https://github.com/mbeacom diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..da3f943ce --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,161 @@ +# This file is used to configure the AppVeyor CI system, for testing on Windows machines. +# +# Code loosely based on https://github.com/ogrisel/python-appveyor-demo +# +# To test with AppVeyor: +# Register on appveyor.com with your GitHub account. +# Create a new appveyor project, using the GitHub details. +# Ideally, configure notifications to post back to GitHub. (Untested) + +environment: + global: + # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the + # /E:ON and /V:ON options are not enabled in the batch script interpreter + # See: http://stackoverflow.com/a/13751649/163740 + CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd" + + matrix: + + # MoviePy supports Python 2.7 and 3.3 onwards. + # Strategy: + # Test the latest known patch in each version + # Test the oldest and the newest 32 bit release. 64-bit otherwise. + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.13" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python33-x64" + PYTHON_VERSION: "3.3.5" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda3-x64 + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python34-x64" + PYTHON_VERSION: "3.4.5" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda3-x64 + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5.3" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda35-x64 + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.2" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda36-x64 + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7.13" + PYTHON_ARCH: "32" + MINICONDA: C:\Miniconda + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python34" + PYTHON_VERSION: "3.6.2" + PYTHON_ARCH: "32" + MINICONDA: C:\Miniconda36 + CONDA_INSTALL: "numpy" + +install: + # If there is a newer build queued for the same PR, cancel this one. + # The AppVeyor 'rollout builds' option is supposed to serve the same + # purpose but it is problematic because it tends to cancel builds pushed + # directly to master instead of just PR builds (or the converse). + # credits: JuliaLang developers. + - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` + https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` + Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` + throw "There are newer queued builds for this pull request, failing early." } + + # Dump some debugging information about the machine. + # - ECHO "Filesystem root:" + # - ps: "ls \"C:/\"" + # + # - ECHO "Installed SDKs:" + # - ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\"" + # + # - ECHO "Installed projects:" + # - ps: "ls \"C:\\projects\"" + # - ps: "ls \"C:\\projects\\moviepy\"" + + # - ECHO "Environment Variables" + # - set + + + # Prepend desired Python to the PATH of this build (this cannot be + # done from inside the powershell script as it would require to restart + # the parent CMD process). + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + + # Prepare Miniconda. + - "ECHO Miniconda is installed in %MINICONDA%, and will be used to install %CONDA_INSTALL%" + + - "set PATH=%MINICONDA%;%MINICONDA%\\Scripts;%PATH%" + - conda config --set always_yes yes --set changeps1 no + - conda update -q conda + + # Avoid warning from conda info. + - conda install -q -n root _license + # Dump the setup for debugging. + - conda info -a + + # PIP finds some packages challenging. Let Miniconda install them. + - conda create --verbose -q -n test-environment python=%PYTHON_VERSION% %CONDA_INSTALL% + - activate test-environment + + # Upgrade to the latest version of pip to avoid it displaying warnings + # about it being out of date. + - pip install --disable-pip-version-check --user --upgrade pip + - pip install --user --upgrade setuptools + + + # Install ImageMagick (which also installs ffmpeg.) + # This installation process is a big fragile, as new releases are issued, but no Conda package exists yet. + - "ECHO Downloading ImageMagick" + # Versions >=7.0 have problems - executables changed names. + # Assume 64-bit. Need to change to x86 for 32-bit. + # The available version at this site changes - each time it needs to be corrected in four places + # in the next few lines. + - curl -fskLO ftp://ftp.fifi.org/pub/ImageMagick/binaries/ImageMagick-6.9.9-5-Q16-x64-static.exe + - "ECHO Installing ImageMagick" + - "ImageMagick-6.9.9-5-Q16-x64-static.exe /verySILENT /SP" + - set IMAGEMAGICK_BINARY=c:\\Program Files\\ImageMagick-6.9.9-Q16\\convert.exe + - set FFMPEG_BINARY=c:\\Program Files\\ImageMagick-6.9.9-Q16\\ffmpeg.exe + + # Check that we have the expected set-up. + - "ECHO We specified %PYTHON_VERSION% win%PYTHON_ARCH%" + - "python --version" + - "python -c \"import struct; print('Architecture is win'+str(struct.calcsize('P') * 8))\"" + +build_script: + + # Build the compiled extension + - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py build" + +test_script: + # Run the project tests + - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py test" + +# TODO: Support the post-test generation of binaries - Pending a version number that is supported (e.g. 0.3.0) +# +# after_test: +# +# # If tests are successful, create binary packages for the project. +# - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py bdist_wheel" +# - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py bdist_wininst" +# - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py bdist_msi" +# - ps: "ls dist" +# +# artifacts: +# # Archive the generated packages in the ci.appveyor.com build report. +# - path: dist\* +# +# on_success: +# - TODO: upload the content of dist/*.whl to a public wheelhouse diff --git a/appveyor/run_with_env.cmd b/appveyor/run_with_env.cmd new file mode 100644 index 000000000..87c8761e1 --- /dev/null +++ b/appveyor/run_with_env.cmd @@ -0,0 +1,86 @@ +:: To build extensions for 64 bit Python 3, we need to configure environment +:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) +:: +:: To build extensions for 64 bit Python 2, we need to configure environment +:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) +:: +:: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific +:: environment configurations. +:: +:: Note: this script needs to be run with the /E:ON and /V:ON flags for the +:: cmd interpreter, at least for (SDK v7.0) +:: +:: More details at: +:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows +:: http://stackoverflow.com/a/13751649/163740 +:: +:: Author: Olivier Grisel +:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ +:: +:: Notes about batch files for Python people: +:: +:: Quotes in values are literally part of the values: +:: SET FOO="bar" +:: FOO is now five characters long: " b a r " +:: If you don't want quotes, don't include them on the right-hand side. +:: +:: The CALL lines at the end of this file look redundant, but if you move them +:: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y +:: case, I don't know why. +@ECHO OFF + +SET COMMAND_TO_RUN=%* +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows +SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf + +:: Extract the major and minor versions, and allow for the minor version to be +:: more than 9. This requires the version number to have two dots in it. +SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1% +IF "%PYTHON_VERSION:~3,1%" == "." ( + SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1% +) ELSE ( + SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2% +) +:: Based on the Python version, determine what SDK version to use, and whether +:: to set the SDK for 64-bit. +IF %MAJOR_PYTHON_VERSION% == 2 ( + SET WINDOWS_SDK_VERSION="v7.0" + SET SET_SDK_64=Y +) ELSE ( + IF %MAJOR_PYTHON_VERSION% == 3 ( + SET WINDOWS_SDK_VERSION="v7.1" + IF %MINOR_PYTHON_VERSION% LEQ 4 ( + SET SET_SDK_64=Y + ) ELSE ( + SET SET_SDK_64=N + IF EXIST "%WIN_WDK%" ( + :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ + REN "%WIN_WDK%" 0wdf + ) + ) + ) ELSE ( + ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" + EXIT 1 + ) +) +IF %PYTHON_ARCH% == 64 ( + IF %SET_SDK_64% == Y ( + ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture + SET DISTUTILS_USE_SDK=1 + SET MSSdk=1 + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 + ) ELSE ( + ECHO Using default MSVC build environment for 64 bit architecture + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 + ) +) ELSE ( + ECHO Using default MSVC build environment for 32 bit architecture + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 +) diff --git a/docs/advanced_tools/advanced_tools.py b/docs/advanced_tools/advanced_tools.py index 11deade6b..5929fe270 100644 --- a/docs/advanced_tools/advanced_tools.py +++ b/docs/advanced_tools/advanced_tools.py @@ -1,3 +1,4 @@ +""" Advanced tools =============== @@ -7,4 +8,5 @@ Subtitles ---------- -Credits \ No newline at end of file +Credits +""" diff --git a/docs/examples/quick_recipes.rst b/docs/examples/quick_recipes.rst index ec92577da..7a0b59752 100644 --- a/docs/examples/quick_recipes.rst +++ b/docs/examples/quick_recipes.rst @@ -11,7 +11,7 @@ Blurring all frames of a video :: - from skimage.filter import gaussian_filter + from skimage.filters import gaussian_filter from moviepy.editor import VideoFileClip def blur(image): diff --git a/docs/gallery.rst b/docs/gallery.rst index db5e355d5..2c5827b62 100644 --- a/docs/gallery.rst +++ b/docs/gallery.rst @@ -19,9 +19,7 @@ This mix of 60 covers of the Cup Song demonstrates the non-linear video editing
- +
The (old) MoviePy reel video. @@ -33,8 +31,7 @@ in the :ref:`examples`. .. raw:: html
-
@@ -129,8 +126,7 @@ This `transcribing piano rolls blog post - @@ -171,8 +167,7 @@ Videogrep is a python script written by Sam Lavigne, that goes through the subti .. raw:: html
-
@@ -200,12 +195,5 @@ This `Videogrep blog post -This `other post `_ uses MoviePy to automatically cut together all the highlights of a soccer game, based on the fact that the crowd cheers louder when something interesting happens. All in under 30 lines of Python: - -.. raw:: html +This `other post `_ uses MoviePy to automatically cut together `all the highlights of a soccer game `_, based on the fact that the crowd cheers louder when something interesting happens. All in under 30 lines of Python: -
- -
diff --git a/docs/getting_started/effects.rst b/docs/getting_started/effects.rst index 63c565421..923c3a15f 100644 --- a/docs/getting_started/effects.rst +++ b/docs/getting_started/effects.rst @@ -47,7 +47,7 @@ but this is not easy to read. To have a clearer syntax you can use ``clip.fx``: .fx( effect_2, args2) .fx( effect_3, args3)) -Much better ! There are already many effects implemented in the modules ``moviepy.video.fx`` and ``moviepy.audio.fx``. The fx methods in these modules are automatically applied to the sound and the mask of the clip if it is relevant, so that you don't have to worry about modifying these. For practicality, when you use ``from moviepy import.editor *``, these two modules are loaded as ``vfx`` and ``afx``, so you may write something like :: +Much better ! There are already many effects implemented in the modules ``moviepy.video.fx`` and ``moviepy.audio.fx``. The fx methods in these modules are automatically applied to the sound and the mask of the clip if it is relevant, so that you don't have to worry about modifying these. For practicality, when you use ``from moviepy.editor import *``, these two modules are loaded as ``vfx`` and ``afx``, so you may write something like :: from moviepy.editor import * clip = (VideoFileClip("myvideo.avi") diff --git a/docs/getting_started/efficient_moviepy.rst b/docs/getting_started/efficient_moviepy.rst index d05151e64..ea328c307 100644 --- a/docs/getting_started/efficient_moviepy.rst +++ b/docs/getting_started/efficient_moviepy.rst @@ -29,12 +29,38 @@ provides all you need to play around and edit your videos but it will take time .. _previewing: +When to close() a clip +~~~~~~~~~~~~~~~~~~~~~~ + +When you create some types of clip instances - e.g. ``VideoFileClip`` or ``AudioFileClip`` - MoviePy creates a subprocess and locks the file. In order to release those resources when you are finished you should call the ``close()`` method. + +This is more important for more complex applications and it particularly important when running on Windows. While Python's garbage collector should eventually clean it the resources for you, clsing them makes them available earlier. + +However, if you close a clip too early, methods on the clip (and any clips derived from it) become unsafe. + +So, the rules of thumb are: + + * Call ``close()`` on any clip that you **construct** once you have finished using it, and have also finished using any clip that was derived from it. + * Also close any clips you create through ``AudioFileClip.coreader()``. + * Even if you close a ``CompositeVideoClip`` instance, you still need to close the clips it was created from. + * Otherwise, if you have a clip that was created by deriving it from from another clip (e.g. by calling ``set_mask()``), then generally you shouldn't close it. Closing the original clip will also close the copy. + +Clips act as `context managers `_. This means you +can use them with a ``with`` statement, and they will automatically be closed at the end of the block, even if there is +an exception. :: + + with AudioFileClip("song.wav") as clip: + raise NotImplementedError("I will work out how process this song later") + # clip.close() is implicitly called, so the lock on song.wav file is immediately released. + + The many ways of previewing a clip ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When you are editing a video or trying to achieve an effect with MoviePy through a trial and error process, generating the video at each trial can be very long. This section presents a few tricks to go faster. + clip.save_frame """"""""""""""""" diff --git a/docs/install.rst b/docs/install.rst index e5654d560..70f6bef4d 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -44,7 +44,7 @@ For advanced image processing you will need one or several of these packages. Fo - `Scikit Image`_ may be needed for some advanced image manipulation. - `OpenCV 2.4.6`_ or more recent (provides the package ``cv2``) or more recent may be needed for some advanced image manipulation. -If you are on linux, these softwares will surely be in your repos. +If you are on linux, these packages will likely be in your repos. .. _`Numpy`: https://www.scipy.org/install.html .. _Decorator: https://pypi.python.org/pypi/decorator diff --git a/docs/ref/Clip.rst b/docs/ref/Clip.rst index 443be07dc..bac02813c 100644 --- a/docs/ref/Clip.rst +++ b/docs/ref/Clip.rst @@ -5,7 +5,7 @@ Clip :class:`Clip` ========================== -.. autoclass:: Clip.Clip +.. autoclass:: moviepy.Clip.Clip :members: :inherited-members: :show-inheritance: diff --git a/docs/ref/audiofx.rst b/docs/ref/audiofx.rst index 80f036cdc..a9a214493 100644 --- a/docs/ref/audiofx.rst +++ b/docs/ref/audiofx.rst @@ -19,7 +19,8 @@ You can either import a single function like this: :: Or import everything: :: import moviepy.audio.fx.all as afx - newaudio = (audioclip.afx( vfx.volumex, 0.5) + newaudio = (audioclip.afx( vfx.normalize) + .afx( vfx.volumex, 0.5) .afx( vfx.audio_fadein, 1.0) .afx( vfx.audio_fadeout, 1.0)) @@ -41,4 +42,5 @@ the module ``audio.fx`` is loaded as ``afx`` and you can use ``afx.volumex``, et audio_fadein audio_fadeout audio_loop - volumex \ No newline at end of file + audio_normalize + volumex diff --git a/docs/ref/audiofx/moviepy.audio.fx.all.audio_normalize.rst b/docs/ref/audiofx/moviepy.audio.fx.all.audio_normalize.rst new file mode 100644 index 000000000..a5cc3c771 --- /dev/null +++ b/docs/ref/audiofx/moviepy.audio.fx.all.audio_normalize.rst @@ -0,0 +1,6 @@ +moviepy.audio.fx.all.audio_normalize +================================== + +.. currentmodule:: moviepy.audio.fx.all + +.. autofunction:: audio_normalize diff --git a/moviepy/Clip.py b/moviepy/Clip.py index efdfb22c6..015ecf513 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -18,31 +18,31 @@ class Clip: """ - + Base class of all clips (VideoClips and AudioClips). - - + + Attributes ----------- - + start: When the clip is included in a composition, time of the - composition at which the clip starts playing (in seconds). - + composition at which the clip starts playing (in seconds). + end: When the clip is included in a composition, time of the composition at which the clip starts playing (in seconds). - + duration: Duration of the clip (in seconds). Some clips are infinite, in this case their duration will be ``None``. - + """ - - # prefix for all tmeporary video and audio files. + + # prefix for all temporary video and audio files. # You can overwrite it with # >>> Clip._TEMP_FILES_PREFIX = "temp_" - + _TEMP_FILES_PREFIX = 'TEMP_MPY_' def __init__(self): @@ -50,7 +50,7 @@ def __init__(self): self.start = 0 self.end = None self.duration = None - + self.memoize = False self.memoized_t = None self.memoize_frame = None @@ -60,14 +60,14 @@ def __init__(self): def copy(self): """ Shallow copy of the clip. - Returns a shwallow copy of the clip whose mask and audio will + Returns a shallow copy of the clip whose mask and audio will be shallow copies of the clip's mask and audio if they exist. - + This method is intensively used to produce new clips every time there is an outplace transformation of the clip (clip.resize, clip.subclip, etc.) """ - + newclip = copy(self) if hasattr(self, 'audio'): newclip.audio = copy(self.audio) @@ -75,14 +75,14 @@ def copy(self): newclip.mask = copy(self.mask) return newclip - + @convert_to_seconds(['t']) def get_frame(self, t): """ Gets a numpy array representing the RGB picture of the clip at time t or (mono or stereo) value for a sound clip """ - # Coming soon: smart error handling for debugging at this point + # Coming soon: smart error handling for debugging at this point if self.memoize: if t == self.memoized_t: return self.memoized_frame @@ -99,48 +99,48 @@ def fl(self, fun, apply_to=None, keep_duration=True): Returns a new Clip whose frames are a transformation (through function ``fun``) of the frames of the current clip. - + Parameters ----------- - + fun A function with signature (gf,t -> frame) where ``gf`` will represent the current clip's ``get_frame`` method, i.e. ``gf`` is a function (t->image). Parameter `t` is a time in seconds, `frame` is a picture (=Numpy array) which will be returned by the transformed clip (see examples below). - + apply_to Can be either ``'mask'``, or ``'audio'``, or ``['mask','audio']``. Specifies if the filter ``fl`` should also be applied to the audio or the mask of the clip, if any. - + keep_duration Set to True if the transformation does not change the ``duration`` of the clip. - + Examples -------- - + In the following ``newclip`` a 100 pixels-high clip whose video content scrolls from the top to the bottom of the frames of ``clip``. - + >>> fl = lambda gf,t : gf(t)[int(t):int(t)+50, :] >>> newclip = clip.fl(fl, apply_to='mask') - + """ if apply_to is None: apply_to = [] #mf = copy(self.make_frame) newclip = self.set_make_frame(lambda t: fun(self.get_frame, t)) - + if not keep_duration: newclip.duration = None newclip.end = None - + if isinstance(apply_to, str): apply_to = [apply_to] @@ -150,76 +150,76 @@ def fl(self, fun, apply_to=None, keep_duration=True): if a is not None: new_a = a.fl(fun, keep_duration=keep_duration) setattr(newclip, attr, new_a) - + return newclip - - + + def fl_time(self, t_func, apply_to=None, keep_duration=False): """ Returns a Clip instance playing the content of the current clip but with a modified timeline, time ``t`` being replaced by another time `t_func(t)`. - + Parameters ----------- - + t_func: A function ``t-> new_t`` - + apply_to: Can be either 'mask', or 'audio', or ['mask','audio']. Specifies if the filter ``fl`` should also be applied to the audio or the mask of the clip, if any. - + keep_duration: ``False`` (default) if the transformation modifies the ``duration`` of the clip. - + Examples -------- - + >>> # plays the clip (and its mask and sound) twice faster - >>> newclip = clip.fl_time(lambda: 2*t, apply_to=['mask','audio']) + >>> newclip = clip.fl_time(lambda: 2*t, apply_to=['mask', 'audio']) >>> >>> # plays the clip starting at t=3, and backwards: >>> newclip = clip.fl_time(lambda: 3-t) - + """ if apply_to is None: apply_to = [] - + return self.fl(lambda gf, t: gf(t_func(t)), apply_to, keep_duration=keep_duration) - - - + + + def fx(self, func, *args, **kwargs): """ - + Returns the result of ``func(self, *args, **kwargs)``. for instance - + >>> newclip = clip.fx(resize, 0.2, method='bilinear') - + is equivalent to - + >>> newclip = resize(clip, 0.2, method='bilinear') - + The motivation of fx is to keep the name of the effect near its parameters, when the effects are chained: - + >>> from moviepy.video.fx import volumex, resize, mirrorx >>> clip.fx( volumex, 0.5).fx( resize, 0.3).fx( mirrorx ) >>> # Is equivalent, but clearer than >>> resize( volumex( mirrorx( clip ), 0.5), 0.3) - + """ - + return func(self, *args, **kwargs) - - - + + + @apply_to_mask @apply_to_audio @convert_to_seconds(['t']) @@ -230,27 +230,27 @@ def set_start(self, t, change_end=True): to ``t``, which can be expressed in seconds (15.35), in (min, sec), in (hour, min, sec), or as a string: '01:03:05.35'. - + If ``change_end=True`` and the clip has a ``duration`` attribute, the ``end`` atrribute of the clip will be updated to ``start+duration``. - + If ``change_end=False`` and the clip has a ``end`` attribute, - the ``duration`` attribute of the clip will be updated to + the ``duration`` attribute of the clip will be updated to ``end-start`` - + These changes are also applied to the ``audio`` and ``mask`` clips of the current clip, if they exist. """ - + self.start = t if (self.duration is not None) and change_end: self.end = t + self.duration elif (self.end is not None): self.duration = self.end - self.start - - - + + + @apply_to_mask @apply_to_audio @convert_to_seconds(['t']) @@ -264,6 +264,7 @@ def set_end(self, t): of the returned clip. """ self.end = t + if self.end is None: return if self.start is None: if self.duration is not None: self.start = max(0, t - newclip.duration) @@ -271,7 +272,7 @@ def set_end(self, t): self.duration = self.end - self.start - + @apply_to_mask @apply_to_audio @convert_to_seconds(['t']) @@ -288,12 +289,13 @@ def set_duration(self, t, change_end=True): of the clip. """ self.duration = t + if change_end: self.end = None if (t is None) else (self.start + t) else: - if duration is None: + if self.duration is None: raise Exception("Cannot change clip start when new" - "duration is None") + "duration is None") self.start = self.end - t @@ -308,53 +310,53 @@ def set_make_frame(self, make_frame): @outplace def set_fps(self, fps): """ Returns a copy of the clip with a new default fps for functions like - write_videofile, iterframe, etc. """ + write_videofile, iterframe, etc. """ self.fps = fps @outplace def set_ismask(self, ismask): - """ Says wheter the clip is a mask or not (ismask is a boolean)""" + """ Says wheter the clip is a mask or not (ismask is a boolean)""" self.ismask = ismask @outplace def set_memoize(self, memoize): - """ Sets wheter the clip should keep the last frame read in memory """ - self.memoize = memoize - + """ Sets wheter the clip should keep the last frame read in memory """ + self.memoize = memoize + @convert_to_seconds(['t']) def is_playing(self, t): """ - + If t is a time, returns true if t is between the start and the end of the clip. t can be expressed in seconds (15.35), in (min, sec), in (hour, min, sec), or as a string: '01:03:05.35'. If t is a numpy array, returns False if none of the t is in theclip, else returns a vector [b_1, b_2, b_3...] where b_i - is true iff tti is in the clip. + is true iff tti is in the clip. """ - + if isinstance(t, np.ndarray): # is the whole list of t outside the clip ? tmin, tmax = t.min(), t.max() - + if (self.end is not None) and (tmin >= self.end) : return False - + if tmax < self.start: return False - + # If we arrive here, a part of t falls in the clip result = 1 * (t >= self.start) if (self.end is not None): result *= (t <= self.end) return result - + else: - + return( (t >= self.start) and ((self.end is None) or (t < self.end) ) ) - + @convert_to_seconds(['t_start', 't_end']) @@ -370,13 +372,13 @@ def subclip(self, t_start=0, t_end=None): of the clip (potentially infinite). If ``t_end`` is a negative value, it is reset to ``clip.duration + t_end. ``. For instance: :: - + >>> # cut the last two seconds of the clip: >>> newclip = clip.subclip(0,-2) - + If ``t_end`` is provided or if the clip has a duration attribute, the duration of the returned clip is set automatically. - + The ``mask`` and ``audio`` of the resulting subclip will be subclips of ``mask`` and ``audio`` the original clip, if they exist. @@ -387,7 +389,6 @@ def subclip(self, t_start=0, t_end=None): t_start = self.duration + t_start #remeber t_start is negative if (self.duration is not None) and (t_start>self.duration): - raise ValueError("t_start (%.02f) "%t_start + "should be smaller than the clip's "+ "duration (%.02f)."%self.duration) @@ -395,28 +396,28 @@ def subclip(self, t_start=0, t_end=None): newclip = self.fl_time(lambda t: t + t_start, apply_to=[]) if (t_end is None) and (self.duration is not None): - + t_end = self.duration - + elif (t_end is not None) and (t_end<0): - + if self.duration is None: - + print ("Error: subclip with negative times (here %s)"%(str((t_start, t_end))) +" can only be extracted from clips with a ``duration``") - + else: - + t_end = self.duration + t_end - + if (t_end is not None): - + newclip.duration = t_end - t_start newclip.end = newclip.start + newclip.duration - + return newclip - + @apply_to_mask @apply_to_audio @convert_to_seconds(['ta', 'tb']) @@ -429,20 +430,20 @@ def cutout(self, ta, tb): If the original clip has a ``duration`` attribute set, the duration of the returned clip is automatically computed as `` duration - (tb - ta)``. - + The resulting clip's ``audio`` and ``mask`` will also be cutout if they exist. """ - + fl = lambda t: t + (t >= ta)*(tb - ta) newclip = self.fl_time(fl) - + if self.duration is not None: - + return newclip.set_duration(self.duration - (tb - ta)) - + else: - + return newclip @requires_duration @@ -450,22 +451,22 @@ def cutout(self, ta, tb): def iter_frames(self, fps=None, with_times = False, progress_bar=False, dtype=None): """ Iterates over all the frames of the clip. - + Returns each frame of the clip as a HxWxN np.array, where N=1 for mask clips and N=3 for RGB clips. - + This function is not really meant for video editing. It provides an easy way to do frame-by-frame treatment of a video, for fields like science, computer vision... - + The ``fps`` (frames per second) parameter is optional if the clip already has a ``fps`` attribute. - Use dtype="uint8" when using the pictures to write video, images... - + Use dtype="uint8" when using the pictures to write video, images... + Examples --------- - + >>> # prints the maximum of red that is contained >>> # on the first line of each frame of the clip. >>> from moviepy.editor import VideoFileClip @@ -483,9 +484,32 @@ def generator(): yield t, frame else: yield frame - + if progress_bar: nframes = int(self.duration*fps)+1 return tqdm(generator(), total=nframes) return generator() + + def close(self): + """ + Release any resources that are in use. + """ + + # Implementation note for subclasses: + # + # * Memory-based resources can be left to the garbage-collector. + # * However, any open files should be closed, and subprocesses should be terminated. + # * Be wary that shallow copies are frequently used. Closing a Clip may affect its copies. + # * Therefore, should NOT be called by __del__(). + + pass + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + diff --git a/moviepy/audio/AudioClip.py b/moviepy/audio/AudioClip.py index aa18adb36..08a008b16 100644 --- a/moviepy/audio/AudioClip.py +++ b/moviepy/audio/AudioClip.py @@ -67,15 +67,14 @@ def iter_chunks(self, chunksize=None, chunk_duration=None, fps=None, totalsize = int(fps*self.duration) - if (totalsize % chunksize == 0): - nchunks = totalsize // chunksize - else: - nchunks = totalsize // chunksize + 1 + nchunks = totalsize // chunksize + 1 - pospos = list(range(0, totalsize, chunksize))+[totalsize] + pospos = np.linspace(0, totalsize, nchunks + 1, endpoint=True, dtype=int) def generator(): for i in range(nchunks): + size = pospos[i+1] - pospos[i] + assert(size <= chunksize) tt = (1.0/fps)*np.arange(pospos[i],pospos[i+1]) yield self.to_soundarray(tt, nbytes= nbytes, quantize=quantize, fps=fps, buffersize=chunksize) diff --git a/moviepy/audio/fx/audio_normalize.py b/moviepy/audio/fx/audio_normalize.py new file mode 100644 index 000000000..127e9ea64 --- /dev/null +++ b/moviepy/audio/fx/audio_normalize.py @@ -0,0 +1,9 @@ +from moviepy.decorators import audio_video_fx + +@audio_video_fx +def audio_normalize(clip): + """ Return an audio (or video) clip whose volume is normalized + to 0db.""" + + mv = clip.max_volume() + return clip.volumex(1 / mv) diff --git a/moviepy/audio/fx/volumex.py b/moviepy/audio/fx/volumex.py index 75d1bf2a0..400da4046 100644 --- a/moviepy/audio/fx/volumex.py +++ b/moviepy/audio/fx/volumex.py @@ -1,5 +1,6 @@ from moviepy.decorators import audio_video_fx + @audio_video_fx def volumex(clip, factor): """ Returns a clip with audio volume multiplied by the @@ -7,13 +8,14 @@ def volumex(clip, factor): This effect is loaded as a clip method when you use moviepy.editor, so you can just write ``clip.volumex(2)`` - + Examples --------- >>> newclip = volumex(clip, 2.0) # doubles audio volume >>> newclip = clip.fx( volumex, 0.5) # half audio, use with fx >>> newclip = clip.volumex(2) # only if you used "moviepy.editor" - """ + """ return clip.fl(lambda gf, t: factor * gf(t), - keep_duration = True) + keep_duration=True) + diff --git a/moviepy/audio/io/AudioFileClip.py b/moviepy/audio/io/AudioFileClip.py index 5c9042475..8b86b8920 100644 --- a/moviepy/audio/io/AudioFileClip.py +++ b/moviepy/audio/io/AudioFileClip.py @@ -44,12 +44,27 @@ class AudioFileClip(AudioClip): buffersize See Parameters. + Lifetime + -------- + + Note that this creates subprocesses and locks files. If you construct one of these instances, you must call + close() afterwards, or the subresources will not be cleaned up until the process ends. + + If copies are made, and close() is called on one, it may cause methods on the other copies to fail. + + However, coreaders must be closed separately. + Examples ---------- >>> snd = AudioFileClip("song.wav") + >>> snd.close() >>> snd = AudioFileClip("song.mp3", fps = 44100, bitrate=3000) - >>> snd = AudioFileClip(mySoundArray,fps=44100) # from a numeric array + >>> second_reader = snd.coreader() + >>> second_reader.close() + >>> snd.close() + >>> with AudioFileClip(mySoundArray,fps=44100) as snd: # from a numeric array + >>> pass # Close is implicitly performed by context manager. """ @@ -59,28 +74,26 @@ def __init__(self, filename, buffersize=200000, nbytes=2, fps=44100): AudioClip.__init__(self) self.filename = filename - reader = FFMPEG_AudioReader(filename,fps=fps,nbytes=nbytes, + self.reader = FFMPEG_AudioReader(filename,fps=fps,nbytes=nbytes, buffersize=buffersize) - - self.reader = reader self.fps = fps - self.duration = reader.duration - self.end = reader.duration + self.duration = self.reader.duration + self.end = self.reader.duration - self.make_frame = lambda t: reader.get_frame(t) - self.nchannels = reader.nchannels + self.make_frame = lambda t: self.reader.get_frame(t) + self.nchannels = self.reader.nchannels def coreader(self): """ Returns a copy of the AudioFileClip, i.e. a new entrance point to the audio file. Use copy when you have different clips watching the audio file at different times. """ - return AudioFileClip(self.filename,self.buffersize) + return AudioFileClip(self.filename, self.buffersize) + - def __del__(self): - """ Close/delete the internal reader. """ - try: - del self.reader - except AttributeError: - pass + def close(self): + """ Close the internal reader. """ + if self.reader: + self.reader.close_proc() + self.reader = None diff --git a/moviepy/audio/io/ffmpeg_audiowriter.py b/moviepy/audio/io/ffmpeg_audiowriter.py index a13a92130..7ec56be6a 100644 --- a/moviepy/audio/io/ffmpeg_audiowriter.py +++ b/moviepy/audio/io/ffmpeg_audiowriter.py @@ -88,7 +88,7 @@ def write_frames(self,frames_array): ffmpeg_error = self.proc.stderr.read() error = (str(err)+ ("\n\nMoviePy error: FFMPEG encountered " "the following error while writing file %s:"%self.filename - + "\n\n"+ffmpeg_error)) + + "\n\n" + str(ffmpeg_error))) if b"Unknown encoder" in ffmpeg_error: @@ -125,11 +125,27 @@ def write_frames(self,frames_array): def close(self): - self.proc.stdin.close() - if self.proc.stderr is not None: - self.proc.stderr.close() - self.proc.wait() - del self.proc + if self.proc: + self.proc.stdin.close() + self.proc.stdin = None + if self.proc.stderr is not None: + self.proc.stderr.close() + self.proc.stdee = None + # If this causes deadlocks, consider terminating instead. + self.proc.wait() + self.proc = None + + def __del__(self): + # If the garbage collector comes, make sure the subprocess is terminated. + self.close() + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() diff --git a/moviepy/audio/io/readers.py b/moviepy/audio/io/readers.py index 8e4935ad0..6d85a6897 100644 --- a/moviepy/audio/io/readers.py +++ b/moviepy/audio/io/readers.py @@ -148,7 +148,7 @@ def close_proc(self): for std in [ self.proc.stdout, self.proc.stderr]: std.close() - del self.proc + self.proc = None def get_frame(self, tt): @@ -185,6 +185,8 @@ def get_frame(self, tt): try: result = np.zeros((len(tt),self.nchannels)) indices = frames - self.buffer_startframe + if len(self.buffer) < self.buffersize // 2: + indices = indices - (self.buffersize // 2 - len(self.buffer) + 1) result[in_time] = self.buffer[indices] return result @@ -245,4 +247,5 @@ def buffer_around(self,framenumber): def __del__(self): + # If the garbage collector comes, make sure the subprocess is terminated. self.close_proc() diff --git a/moviepy/config.py b/moviepy/config.py index e102253f8..5a8451607 100644 --- a/moviepy/config.py +++ b/moviepy/config.py @@ -45,10 +45,9 @@ def try_cmd(cmd): else: success, err = try_cmd([FFMPEG_BINARY]) if not success: - raise IOError(err.message + - "The path specified for the ffmpeg binary might be wrong") - - + raise IOError( + str(err) + + " - The path specified for the ffmpeg binary might be wrong") if IMAGEMAGICK_BINARY=='auto-detect': if os.name == 'nt': @@ -65,8 +64,10 @@ def try_cmd(cmd): else: success, err = try_cmd([IMAGEMAGICK_BINARY]) if not success: - raise IOError(err.message + - "The path specified for the ImageMagick binary might be wrong") + raise IOError( + "%s - The path specified for the ImageMagick binary might be wrong: %s" % + (err, IMAGEMAGICK_BINARY) + ) diff --git a/moviepy/editor.py b/moviepy/editor.py index 0bf2feeb8..f8a914082 100644 --- a/moviepy/editor.py +++ b/moviepy/editor.py @@ -53,6 +53,7 @@ for method in [ "afx.audio_fadein", "afx.audio_fadeout", + "afx.audio_normalize", "afx.volumex", "transfx.crossfadein", "transfx.crossfadeout", @@ -75,6 +76,7 @@ for method in ["afx.audio_fadein", "afx.audio_fadeout", "afx.audio_loop", + "afx.audio_normalize", "afx.volumex" ]: @@ -111,4 +113,4 @@ def preview(self, *args, **kwargs): """ NOT AVAILABLE : clip.preview requires Pygame installed.""" raise ImportError("clip.preview requires Pygame installed") -AudioClip.preview = preview \ No newline at end of file +AudioClip.preview = preview diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 28caf7d19..6ad8de976 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -250,6 +250,7 @@ def write_videofile(self, filename, fps=None, codec=None, >>> from moviepy.editor import VideoFileClip >>> clip = VideoFileClip("myvideo.mp4").subclip(100,120) >>> clip.write_videofile("my_new_video.mp4") + >>> clip.close() """ name, ext = os.path.splitext(os.path.basename(filename)) diff --git a/moviepy/video/compositing/CompositeVideoClip.py b/moviepy/video/compositing/CompositeVideoClip.py index 30172b782..50fe69212 100644 --- a/moviepy/video/compositing/CompositeVideoClip.py +++ b/moviepy/video/compositing/CompositeVideoClip.py @@ -40,8 +40,7 @@ class CompositeVideoClip(VideoClip): have the same size as the final clip. If it has no transparency, the final clip will have no mask. - If all clips with a fps attribute have the same fps, it becomes the fps of - the result. + The clip with the highest FPS will be the FPS of the composite clip. """ @@ -60,10 +59,11 @@ def __init__(self, clips, size=None, bg_color=None, use_bgclip=False, if bg_color is None: bg_color = 0.0 if ismask else (0, 0, 0) - - fps_list = list(set([c.fps for c in clips if hasattr(c,'fps')])) - if len(fps_list)==1: - self.fps= fps_list[0] + fpss = [c.fps for c in clips if hasattr(c, 'fps') and c.fps is not None] + if len(fpss) == 0: + self.fps = None + else: + self.fps = max(fpss) VideoClip.__init__(self) @@ -75,9 +75,11 @@ def __init__(self, clips, size=None, bg_color=None, use_bgclip=False, if use_bgclip: self.bg = clips[0] self.clips = clips[1:] + self.created_bg = False else: self.clips = clips self.bg = ColorClip(size, col=self.bg_color) + self.created_bg = True @@ -95,7 +97,7 @@ def __init__(self, clips, size=None, bg_color=None, use_bgclip=False, # compute mask if necessary if transparent: maskclips = [(c.mask if (c.mask is not None) else - c.add_mask().mask).set_pos(c.pos) + c.add_mask().mask).set_pos(c.pos).set_end(c.end).set_start(c.start, change_end=False) for c in self.clips] self.mask = CompositeVideoClip(maskclips,self.size, ismask=True, @@ -117,6 +119,18 @@ def playing_clips(self, t=0): actually playing at the given time `t`. """ return [c for c in self.clips if c.is_playing(t)] + def close(self): + if self.created_bg and self.bg: + # Only close the background clip if it was locally created. + # Otherwise, it remains the job of whoever created it. + self.bg.close() + self.bg = None + if hasattr(self, "audio") and self.audio: + self.audio.close() + self.audio = None + + + def clips_array(array, rows_widths=None, cols_widths=None, diff --git a/moviepy/video/compositing/transitions.py b/moviepy/video/compositing/transitions.py index 0c7336339..a6837d7f5 100644 --- a/moviepy/video/compositing/transitions.py +++ b/moviepy/video/compositing/transitions.py @@ -43,7 +43,7 @@ def slide_in(clip, duration, side): Parameters =========== - + clip A video clip. @@ -53,10 +53,10 @@ def slide_in(clip, duration, side): side Side of the screen where the clip comes from. One of 'top' | 'bottom' | 'left' | 'right' - + Examples ========= - + >>> from moviepy.editor import * >>> clips = [... make a list of clips] >>> slided_clips = [clip.fx( transfx.slide_in, 1, 'left') @@ -69,7 +69,7 @@ def slide_in(clip, duration, side): 'right' : lambda t: (max(0,w*(1-t/duration)),'center'), 'top' : lambda t: ('center',min(0,h*(t/duration-1))), 'bottom': lambda t: ('center',max(0,h*(1-t/duration)))} - + return clip.set_pos( pos_dict[side] ) @@ -83,7 +83,7 @@ def slide_out(clip, duration, side): Parameters =========== - + clip A video clip. @@ -93,10 +93,10 @@ def slide_out(clip, duration, side): side Side of the screen where the clip goes. One of 'top' | 'bottom' | 'left' | 'right' - + Examples ========= - + >>> from moviepy.editor import * >>> clips = [... make a list of clips] >>> slided_clips = [clip.fx( transfx.slide_out, 1, 'bottom') @@ -106,12 +106,12 @@ def slide_out(clip, duration, side): """ w,h = clip.size - t_s = clip.duration - duration # start time of the effect. + ts = clip.duration - duration # start time of the effect. pos_dict = {'left' : lambda t: (min(0,w*(1-(t-ts)/duration)),'center'), 'right' : lambda t: (max(0,w*((t-ts)/duration-1)),'center'), 'top' : lambda t: ('center',min(0,h*(1-(t-ts)/duration))), 'bottom': lambda t: ('center',max(0,h*((t-ts)/duration-1))) } - + return clip.set_pos( pos_dict[side] ) @@ -119,7 +119,7 @@ def slide_out(clip, duration, side): def make_loopable(clip, cross_duration): """ Makes the clip fade in progressively at its own end, this way it can be looped indefinitely. ``cross`` is the duration in seconds - of the fade-in. """ + of the fade-in. """ d = clip.duration clip2 = clip.fx(crossfadein, cross_duration).\ set_start(d - cross_duration) diff --git a/moviepy/video/io/VideoFileClip.py b/moviepy/video/io/VideoFileClip.py index a44ae2a19..a5e300c40 100644 --- a/moviepy/video/io/VideoFileClip.py +++ b/moviepy/video/io/VideoFileClip.py @@ -12,7 +12,9 @@ class VideoFileClip(VideoClip): A video clip originating from a movie file. For instance: :: >>> clip = VideoFileClip("myHolidays.mp4") - >>> clip2 = VideoFileClip("myMaskVideo.avi") + >>> clip.close() + >>> with VideoFileClip("myMaskVideo.avi") as clip2: + >>> pass # Implicit close called by contex manager. Parameters @@ -61,6 +63,14 @@ class VideoFileClip(VideoClip): Read docs for Clip() and VideoClip() for other, more generic, attributes. + + Lifetime + -------- + + Note that this creates subprocesses and locks files. If you construct one of these instances, you must call + close() afterwards, or the subresources will not be cleaned up until the process ends. + + If copies are made, and close() is called on one, it may cause methods on the other copies to fail. """ @@ -74,7 +84,6 @@ def __init__(self, filename, has_mask=False, # Make a reader pix_fmt= "rgba" if has_mask else "rgb24" - self.reader = None # need this just in case FFMPEG has issues (__del__ complains) self.reader = FFMPEG_VideoReader(filename, pix_fmt=pix_fmt, target_resolution=target_resolution, resize_algo=resize_algorithm, @@ -110,14 +119,15 @@ def __init__(self, filename, has_mask=False, fps = audio_fps, nbytes = audio_nbytes) - def __del__(self): - """ Close/delete the internal reader. """ - try: - del self.reader - except AttributeError: - pass + def close(self): + """ Close the internal reader. """ + if self.reader: + self.reader.close() + self.reader = None try: - del self.audio + if self.audio: + self.audio.close() + self.audio = None except AttributeError: pass diff --git a/moviepy/video/io/downloader.py b/moviepy/video/io/downloader.py index cd3df06f4..5b579de02 100644 --- a/moviepy/video/io/downloader.py +++ b/moviepy/video/io/downloader.py @@ -4,10 +4,7 @@ import os -try: # Py2 and Py3 compatibility - from urllib import urlretrieve -except: - from urllib.request import urlretrieve +import requests from moviepy.tools import subprocess_call @@ -22,13 +19,16 @@ def download_webfile(url, filename, overwrite=False): return if '.' in url: - urlretrieve(url, filename) + r = requests.get(url, stream=True) + with open(filename, 'wb') as fd: + for chunk in r.iter_content(chunk_size=128): + fd.write(chunk) + else: try: subprocess_call(['youtube-dl', url, '-o', filename]) except OSError as e: - raise OSError(e.message + '\n A possible reason is that youtube-dl' + raise OSError( + e.message + '\n A possible reason is that youtube-dl' ' is not installed on your computer. Install it with ' - ' "pip install youtube-dl"') - - + ' "pip install youtube_dl"') diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index 641be0b8f..0a26a88aa 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -28,6 +28,7 @@ def __init__(self, filename, print_infos=False, bufsize = None, fps_source='tbr'): self.filename = filename + self.proc = None infos = ffmpeg_parse_infos(filename, print_infos, check_duration, fps_source) self.fps = infos['video_fps'] @@ -103,9 +104,6 @@ def initialize(self, starttime=0): self.proc = sp.Popen(cmd, **popen_params) - - - def skip_frames(self, n=1): """Reads and throws away n frames """ w, h = self.size @@ -155,7 +153,7 @@ def get_frame(self, t): Note for coders: getting an arbitrary frame in the video with ffmpeg can be painfully slow if some decoding has to be done. - This function tries to avoid fectching arbitrary frames + This function tries to avoid fetching arbitrary frames whenever possible, by moving between adjacent frames. """ @@ -168,10 +166,16 @@ def get_frame(self, t): pos = int(self.fps*t + 0.00001)+1 + # Initialize proc if it is not open + if not self.proc: + self.initialize(t) + self.pos = pos + self.lastread = self.read_frame() + if pos == self.pos: return self.lastread else: - if(pos < self.pos) or (pos > self.pos+100): + if (pos < self.pos) or (pos > self.pos + 100): self.initialize(t) self.pos = pos else: @@ -181,20 +185,16 @@ def get_frame(self, t): return result def close(self): - if hasattr(self,'proc'): + if self.proc: self.proc.terminate() self.proc.stdout.close() self.proc.stderr.close() self.proc.wait() - del self.proc - - def __del__(self): - self.close() - if hasattr(self,'lastread'): + self.proc = None + if hasattr(self, 'lastread'): del self.lastread - def ffmpeg_read_image(filename, with_mask=True): """ Read an image file (PNG, BMP, JPEG...). @@ -262,12 +262,12 @@ def ffmpeg_parse_infos(filename, print_infos=False, check_duration=True, if print_infos: # print the whole info text returned by FFMPEG - print( infos ) + print(infos) lines = infos.splitlines() if "No such file or directory" in lines[-1]: - raise IOError(("MoviePy error: the file %s could not be found !\n" + raise IOError(("MoviePy error: the file %s could not be found!\n" "Please check that you entered the correct " "path.")%filename) diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index 54bcc8e99..f4a2fa6f0 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -122,7 +122,7 @@ def __init__(self, filename, size, fps, codec="libx264", audiofile=None, # This was added so that no extra unwanted window opens on windows # when the child process is created if os.name == "nt": - popen_params["creationflags"] = 0x08000000 + popen_params["creationflags"] = 0x08000000 # CREATE_NO_WINDOW self.proc = sp.Popen(cmd, **popen_params) @@ -138,7 +138,7 @@ def write_frame(self, img_array): _, ffmpeg_error = self.proc.communicate() error = (str(err) + ("\n\nMoviePy error: FFMPEG encountered " "the following error while writing file %s:" - "\n\n %s" % (self.filename, ffmpeg_error))) + "\n\n %s" % (self.filename, str(ffmpeg_error)))) if b"Unknown encoder" in ffmpeg_error: @@ -178,12 +178,21 @@ def write_frame(self, img_array): raise IOError(error) def close(self): - self.proc.stdin.close() - if self.proc.stderr is not None: - self.proc.stderr.close() - self.proc.wait() + if self.proc: + self.proc.stdin.close() + if self.proc.stderr is not None: + self.proc.stderr.close() + self.proc.wait() - del self.proc + self.proc = None + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() def ffmpeg_write_video(clip, filename, fps, codec="libx264", bitrate=None, preset="medium", withmask=False, write_logfile=False, @@ -198,24 +207,22 @@ def ffmpeg_write_video(clip, filename, fps, codec="libx264", bitrate=None, logfile = None verbose_print(verbose, "[MoviePy] Writing video %s\n"%filename) - writer = FFMPEG_VideoWriter(filename, clip.size, fps, codec = codec, + with FFMPEG_VideoWriter(filename, clip.size, fps, codec = codec, preset=preset, bitrate=bitrate, logfile=logfile, audiofile=audiofile, threads=threads, - ffmpeg_params=ffmpeg_params) - - nframes = int(clip.duration*fps) + ffmpeg_params=ffmpeg_params) as writer: - for t,frame in clip.iter_frames(progress_bar=progress_bar, with_times=True, - fps=fps, dtype="uint8"): - if withmask: - mask = (255*clip.mask.get_frame(t)) - if mask.dtype != "uint8": - mask = mask.astype("uint8") - frame = np.dstack([frame,mask]) + nframes = int(clip.duration*fps) - writer.write_frame(frame) + for t,frame in clip.iter_frames(progress_bar=progress_bar, with_times=True, + fps=fps, dtype="uint8"): + if withmask: + mask = (255*clip.mask.get_frame(t)) + if mask.dtype != "uint8": + mask = mask.astype("uint8") + frame = np.dstack([frame,mask]) - writer.close() + writer.write_frame(frame) if write_logfile: logfile.close() diff --git a/moviepy/video/io/gif_writers.py b/moviepy/video/io/gif_writers.py index b46e5c58f..b5f060710 100644 --- a/moviepy/video/io/gif_writers.py +++ b/moviepy/video/io/gif_writers.py @@ -278,8 +278,13 @@ def write_gif_with_image_io(clip, filename, fps=None, opt=0, loop=0, fps = clip.fps quantizer = 0 if opt!= 0 else 'nq' - writer = imageio.save(filename, duration=1.0/fps, - quantizer=quantizer, palettesize=colors) + writer = imageio.save( + filename, + duration=1.0/fps, + quantizer=quantizer, + palettesize=colors, + loop=loop + ) verbose_print(verbose, "\n[MoviePy] Building file %s with imageio\n"%filename) diff --git a/moviepy/video/tools/cuts.py b/moviepy/video/tools/cuts.py index fbf5d05d9..5c3d449a5 100644 --- a/moviepy/video/tools/cuts.py +++ b/moviepy/video/tools/cuts.py @@ -274,7 +274,7 @@ def write_gifs(self, clip, gif_dir): @use_clip_fps_by_default def detect_scenes(clip=None, luminosities=None, thr=10, - progress_bar=False, fps=None): + progress_bar=True, fps=None): """ Detects scenes of a clip based on luminosity changes. Note that for large clip this may take some time @@ -320,7 +320,7 @@ def detect_scenes(clip=None, luminosities=None, thr=10, if luminosities is None: luminosities = [f.sum() for f in clip.iter_frames( - fps=fps, dtype='uint32', progress_bar=1)] + fps=fps, dtype='uint32', progress_bar=progress_bar)] luminosities = np.array(luminosities, dtype=float) if clip is not None: diff --git a/moviepy/video/tools/subtitles.py b/moviepy/video/tools/subtitles.py index ebd373901..427e2fe86 100644 --- a/moviepy/video/tools/subtitles.py +++ b/moviepy/video/tools/subtitles.py @@ -25,8 +25,7 @@ class SubtitlesClip(VideoClip): >>> from moviepy.video.tools.subtitles import SubtitlesClip >>> from moviepy.video.io.VideoFileClip import VideoFileClip - >>> generator = lambda txt: TextClip(txt, font='Georgia-Regular', - fontsize=24, color='white') + >>> generator = lambda txt: TextClip(txt, font='Georgia-Regular', fontsize=24, color='white') >>> sub = SubtitlesClip("subtitles.srt", generator) >>> myvideo = VideoFileClip("myvideo.avi") >>> final = CompositeVideoClip([clip, subtitles]) diff --git a/setup.py b/setup.py index 15f18cc73..a00c5bbba 100644 --- a/setup.py +++ b/setup.py @@ -21,12 +21,12 @@ class PyTest(TestCommand): """Handle test execution from setup.""" - user_options = [('pytest-args=', 'a', "Arguments to pass into py.test")] + user_options = [('pytest-args=', 'a', "Arguments to pass into pytest")] def initialize_options(self): """Initialize the PyTest options.""" TestCommand.initialize_options(self) - self.pytest_args = [] + self.pytest_args = "" def finalize_options(self): """Finalize the PyTest options.""" @@ -42,7 +42,7 @@ def run_tests(self): raise ImportError('Running tests requires additional dependencies.' '\nPlease run (pip install moviepy[test])') - errno = pytest.main(self.pytest_args) + errno = pytest.main(self.pytest_args.split(" ")) sys.exit(errno) @@ -57,15 +57,45 @@ def run_tests(self): cmdclass['build_docs'] = BuildDoc +__version__ = None # Explicitly set version to quieten static code checkers. exec(open('moviepy/version.py').read()) # loads __version__ # Define the requirements for specific execution needs. -requires = ['decorator==4.0.11', 'imageio==2.1.2', 'tqdm==4.11.2', 'numpy'] -optional_reqs = ['scikit-image==0.13.0', 'scipy==0.19.0', 'matplotlib==2.0.0'] -documentation_reqs = ['pygame==1.9.3', 'numpydoc>=0.6.0', - 'sphinx_rtd_theme>=0.1.10b0', 'Sphinx>=1.5.2'] + optional_reqs -test_reqs = ['pytest>=2.8.0', 'nose', 'sklearn', 'pytest-cov', 'coveralls'] \ - + optional_reqs +requires = [ + 'decorator>=4.0.2,<5.0', + 'imageio>=2.1.2,<3.0', + 'tqdm>=4.11.2,<5.0', + 'numpy', + ] + +optional_reqs = [ + "opencv-python>=3.0,<4.0; python_version!='2.7'", + "scikit-image>=0.13.0,<1.0; python_version>='3.4'", + "scikit-learn; python_version>='3.4'", + "scipy>=0.19.0,<1.0; python_version!='3.3'", + "matplotlib>=2.0.0,<3.0; python_version>='3.4'", + "youtube_dl" + ] + +doc_reqs = [ + "pygame>=1.9.3,<2.0; python_version!='3.3'", + 'numpydoc>=0.6.0,<1.0', + 'sphinx_rtd_theme>=0.1.10b0,<1.0', + 'Sphinx>=1.5.2,<2.0', + ] + +test_reqs = [ + 'coveralls>=1.1,<2.0', + 'pytest-cov>=2.5.1,<3.0', + 'pytest>=3.0.0,<4.0', + 'requests>=2.8.1,<3.0' + ] + +extra_reqs = { + "optional": optional_reqs, + "doc": doc_reqs, + "test": test_reqs + } # Load the README. with open('README.rst', 'r', 'utf-8') as f: @@ -109,8 +139,5 @@ def run_tests(self): 'release': ('setup.py', __version__)}}, tests_require=test_reqs, install_requires=requires, - extras_require={ - 'optional': optional_reqs, - 'docs': documentation_reqs, - 'test': test_reqs} + extras_require=extra_reqs, ) diff --git a/tests/download_media.py b/tests/download_media.py index a57428479..89ef7cc19 100644 --- a/tests/download_media.py +++ b/tests/download_media.py @@ -8,9 +8,9 @@ def download_url(url, filename): """Download a file.""" if not os.path.exists(filename): - print('\nDownloading {}\n'.format(filename)) - download_webfile(url, filename) - print('Downloading complete...\n') + print('Downloading {} ...'.format(filename)) + download_webfile(url, filename) + print('Downloading complete.') def download_youtube_video(youtube_id, filename): """Download a video from youtube.""" @@ -35,8 +35,14 @@ def download(): # Loop through download url strings, build out path, and download the asset. for url in urls: _, tail = os.path.split(url) - download_url('{}/{}'.format(github_prefix, url), output.format(tail)) + download_url( + url='{}/{}'.format(github_prefix, url), + filename=output.format(tail)) # Download remaining asset. - download_url('https://data.vision.ee.ethz.ch/cvl/video2gif/kAKZeIzs0Ag.mp4', - 'media/video_with_failing_audio.mp4') + download_url( + url='https://data.vision.ee.ethz.ch/cvl/video2gif/kAKZeIzs0Ag.mp4', + filename='media/video_with_failing_audio.mp4') + +if __name__ == "__main__": + download() \ No newline at end of file diff --git a/tests/test_ImageSequenceClip.py b/tests/test_ImageSequenceClip.py index c188049cd..37911be35 100644 --- a/tests/test_ImageSequenceClip.py +++ b/tests/test_ImageSequenceClip.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Image sequencing clip tests meant to be run with pytest.""" +import os import sys import pytest @@ -7,6 +8,7 @@ sys.path.append("tests") import download_media +from test_helper import TMP_DIR def test_download_media(capsys): with capsys.disabled(): @@ -22,9 +24,9 @@ def test_1(): durations.append(i) images.append("media/python_logo_upside_down.png") - clip = ImageSequenceClip(images, durations=durations) - assert clip.duration == sum(durations) - clip.write_videofile("/tmp/ImageSequenceClip1.mp4", fps=30) + with ImageSequenceClip(images, durations=durations) as clip: + assert clip.duration == sum(durations) + clip.write_videofile(os.path.join(TMP_DIR, "ImageSequenceClip1.mp4"), fps=30) def test_2(): images=[] @@ -37,7 +39,7 @@ def test_2(): #images are not the same size.. with pytest.raises(Exception, message='Expecting Exception'): - ImageSequenceClip(images, durations=durations) + ImageSequenceClip(images, durations=durations).close() if __name__ == '__main__': diff --git a/tests/test_PR.py b/tests/test_PR.py index 0c4e2849d..3fda53da1 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -8,9 +8,13 @@ from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.tools.interpolators import Trajectory from moviepy.video.VideoClip import ColorClip, ImageClip, TextClip +from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip + sys.path.append("tests") -from test_helper import TMP_DIR, TRAVIS +from test_helper import TMP_DIR, TRAVIS, FONT + + def test_download_media(capsys): """Test downloading.""" @@ -34,11 +38,11 @@ def test_PR_339(): return # In caption mode. - TextClip(txt='foo', color='white', font="Liberation-Mono", size=(640, 480), - method='caption', align='center', fontsize=25) + TextClip(txt='foo', color='white', font=FONT, size=(640, 480), + method='caption', align='center', fontsize=25).close() # In label mode. - TextClip(txt='foo', font="Liberation-Mono", method='label') + TextClip(txt='foo', font=FONT, method='label').close() def test_PR_373(): result = Trajectory.load_list("media/traj.txt") @@ -65,16 +69,16 @@ def test_PR_424(): warnings.simplefilter('always') # Alert us of deprecation warnings. # Recommended use - ColorClip([1000, 600], color=(60, 60, 60), duration=10) + ColorClip([1000, 600], color=(60, 60, 60), duration=10).close() with pytest.warns(DeprecationWarning): # Uses `col` so should work the same as above, but give warning. - ColorClip([1000, 600], col=(60, 60, 60), duration=10) + ColorClip([1000, 600], col=(60, 60, 60), duration=10).close() # Catch all warnings as record. with pytest.warns(None) as record: # Should give 2 warnings and use `color`, not `col` - ColorClip([1000, 600], color=(60, 60, 60), duration=10, col=(2,2,2)) + ColorClip([1000, 600], color=(60, 60, 60), duration=10, col=(2,2,2)).close() message1 = 'The `ColorClip` parameter `col` has been deprecated. ' + \ 'Please use `color` instead.' @@ -90,26 +94,40 @@ def test_PR_458(): clip = ColorClip([1000, 600], color=(60, 60, 60), duration=10) clip.write_videofile(os.path.join(TMP_DIR, "test.mp4"), progress_bar=False, fps=30) + clip.close() def test_PR_515(): # Won't actually work until video is in download_media - clip = VideoFileClip("media/fire2.mp4", fps_source='tbr') - assert clip.fps == 90000 - clip = VideoFileClip("media/fire2.mp4", fps_source='fps') - assert clip.fps == 10.51 + with VideoFileClip("media/fire2.mp4", fps_source='tbr') as clip: + assert clip.fps == 90000 + with VideoFileClip("media/fire2.mp4", fps_source='fps') as clip: + assert clip.fps == 10.51 def test_PR_528(): - clip = ImageClip("media/vacation_2017.jpg") - new_clip = scroll(clip, w=1000, x_speed=50) - new_clip = new_clip.set_duration(20) - new_clip.fps = 24 - new_clip.write_videofile(os.path.join(TMP_DIR, "pano.mp4")) + with ImageClip("media/vacation_2017.jpg") as clip: + new_clip = scroll(clip, w=1000, x_speed=50) + new_clip = new_clip.set_duration(20) + new_clip.fps = 24 + new_clip.write_videofile(os.path.join(TMP_DIR, "pano.mp4")) def test_PR_529(): - video_clip = VideoFileClip("media/fire2.mp4") - assert video_clip.rotation == 180 + with VideoFileClip("media/fire2.mp4") as video_clip: + assert video_clip.rotation == 180 + + +def test_PR_610(): + """ + Test that the max fps of the video clips is used for the composite video clip + """ + clip1 = ColorClip((640, 480), color=(255, 0, 0)).set_duration(1) + clip2 = ColorClip((640, 480), color=(0, 255, 0)).set_duration(1) + clip1.fps = 24 + clip2.fps = 25 + composite = CompositeVideoClip([clip1, clip2]) + + assert composite.fps == 25 if __name__ == '__main__': diff --git a/tests/test_TextClip.py b/tests/test_TextClip.py index b07605c7b..ffd14b462 100644 --- a/tests/test_TextClip.py +++ b/tests/test_TextClip.py @@ -3,9 +3,9 @@ import pytest from moviepy.video.fx.blink import blink from moviepy.video.VideoClip import TextClip -from test_helper import TMP_DIR, TRAVIS sys.path.append("tests") +from test_helper import TMP_DIR, TRAVIS def test_duration(): #TextClip returns the following error under Travis (issue with Imagemagick) @@ -15,12 +15,14 @@ def test_duration(): return clip = TextClip('hello world', size=(1280,720), color='white') - clip.set_duration(5) + clip = clip.set_duration(5) # Changed due to #598. assert clip.duration == 5 + clip.close() clip2 = clip.fx(blink, d_on=1, d_off=1) - clip2.set_duration(5) + clip2 = clip2.set_duration(5) assert clip2.duration == 5 + clip2.close() # Moved from tests.py. Maybe we can remove these? def test_if_textclip_crashes_in_caption_mode(): @@ -28,13 +30,13 @@ def test_if_textclip_crashes_in_caption_mode(): return TextClip(txt='foo', color='white', size=(640, 480), method='caption', - align='center', fontsize=25) + align='center', fontsize=25).close() def test_if_textclip_crashes_in_label_mode(): if TRAVIS: return - TextClip(txt='foo', method='label') + TextClip(txt='foo', method='label').close() if __name__ == '__main__': pytest.main() diff --git a/tests/test_VideoFileClip.py b/tests/test_VideoFileClip.py index a8ec83e2a..584e5235e 100644 --- a/tests/test_VideoFileClip.py +++ b/tests/test_VideoFileClip.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- """Video file clip tests meant to be run with pytest.""" import os +import sys import pytest from moviepy.video.compositing.CompositeVideoClip import clips_array from moviepy.video.VideoClip import ColorClip from moviepy.video.io.VideoFileClip import VideoFileClip +sys.path.append("tests") +from test_helper import TMP_DIR def test_setup(): """Test VideoFileClip setup.""" @@ -15,39 +18,43 @@ def test_setup(): blue = ColorClip((1024,800), color=(0,0,255)) red.fps = green.fps = blue.fps = 30 - video = clips_array([[red, green, blue]]).set_duration(5) - video.write_videofile("/tmp/test.mp4") + with clips_array([[red, green, blue]]).set_duration(5) as video: + video.write_videofile(os.path.join(TMP_DIR, "test.mp4")) - assert os.path.exists("/tmp/test.mp4") + assert os.path.exists(os.path.join(TMP_DIR, "test.mp4")) - clip = VideoFileClip("/tmp/test.mp4") - assert clip.duration == 5 - assert clip.fps == 30 - assert clip.size == [1024*3, 800] + with VideoFileClip(os.path.join(TMP_DIR, "test.mp4")) as clip: + assert clip.duration == 5 + assert clip.fps == 30 + assert clip.size == [1024*3, 800] + + red.close() + green.close() + blue.close() def test_ffmpeg_resizing(): """Test FFmpeg resizing, to include downscaling.""" video_file = 'media/big_buck_bunny_432_433.webm' target_resolution = (128, 128) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[0:2] == target_resolution + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[0:2] == target_resolution target_resolution = (128, None) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[0] == target_resolution[0] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[0] == target_resolution[0] target_resolution = (None, 128) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[1] == target_resolution[1] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[1] == target_resolution[1] # Test upscaling target_resolution = (None, 2048) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[1] == target_resolution[1] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[1] == target_resolution[1] if __name__ == '__main__': diff --git a/tests/test_Videos.py b/tests/test_Videos.py index bd001a602..7506c8ee4 100644 --- a/tests/test_Videos.py +++ b/tests/test_Videos.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Video tests meant to be run with pytest.""" +import os import sys import pytest @@ -10,22 +11,22 @@ import download_media sys.path.append("tests") - +from test_helper import TMP_DIR def test_download_media(capsys): with capsys.disabled(): download_media.download() def test_afterimage(): - ai = ImageClip("media/afterimage.png") - masked_clip = mask_color(ai, color=[0,255,1]) # for green + with ImageClip("media/afterimage.png") as ai: + masked_clip = mask_color(ai, color=[0,255,1]) # for green - some_background_clip = ColorClip((800,600), color=(255,255,255)) + with ColorClip((800,600), color=(255,255,255)) as some_background_clip: - final_clip = CompositeVideoClip([some_background_clip, masked_clip], - use_bgclip=True) - final_clip.duration = 5 - final_clip.write_videofile("/tmp/afterimage.mp4", fps=30) + with CompositeVideoClip([some_background_clip, masked_clip], + use_bgclip=True) as final_clip: + final_clip.duration = 5 + final_clip.write_videofile(os.path.join(TMP_DIR, "afterimage.mp4"), fps=30) if __name__ == '__main__': pytest.main() diff --git a/tests/test_compositing.py b/tests/test_compositing.py index 4b2db7a41..fbb47970b 100644 --- a/tests/test_compositing.py +++ b/tests/test_compositing.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- """Compositing tests for use with pytest.""" +from os.path import join +import sys import pytest from moviepy.editor import * +sys.path.append("tests") +from test_helper import TMP_DIR def test_clips_array(): red = ColorClip((1024,800), color=(255,0,0)) @@ -12,25 +16,30 @@ def test_clips_array(): with pytest.raises(ValueError, message="Expecting ValueError (duration not set)"): - video.resize(width=480).write_videofile("/tmp/test_clips_array.mp4") + video.resize(width=480).write_videofile(join(TMP_DIR, "test_clips_array.mp4")) + video.close() + red.close() + green.close() + blue.close() def test_clips_array_duration(): - red = ColorClip((1024,800), color=(255,0,0)) - green = ColorClip((1024,800), color=(0,255,0)) - blue = ColorClip((1024,800), color=(0,0,255)) - - video = clips_array([[red, green, blue]]).set_duration(5) + for i in range(20): + red = ColorClip((1024,800), color=(255,0,0)) + green = ColorClip((1024,800), color=(0,255,0)) + blue = ColorClip((1024,800), color=(0,0,255)) - with pytest.raises(AttributeError, - message="Expecting ValueError (fps not set)"): - video.write_videofile("/tmp/test_clips_array.mp4") + with clips_array([[red, green, blue]]).set_duration(5) as video: + with pytest.raises(AttributeError, + message="Expecting ValueError (fps not set)"): + video.write_videofile(join(TMP_DIR, "test_clips_array.mp4")) - #this one should work correctly - red.fps=green.fps=blue.fps=30 - video = clips_array([[red, green, blue]]).set_duration(5) - video.write_videofile("/tmp/test_clips_array.mp4") + #this one should work correctly + red.fps = green.fps = blue.fps = 30 + with clips_array([[red, green, blue]]).set_duration(5) as video: + video.write_videofile(join(TMP_DIR, "test_clips_array.mp4")) -if __name__ == '__main__': - pytest.main() + red.close() + green.close() + blue.close() diff --git a/tests/test_examples.py b/tests/test_examples.py index 87ee3cf6b..cbafec1fa 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,45 +1,50 @@ # -*- coding: utf-8 -*- -"""Example tests for use with pytest.""" -import os -import sys - -import pytest - -import download_media -from test_helper import PYTHON_VERSION, TMP_DIR, TRAVIS - -sys.path.append("tests") - - -def test_download_media(capsys): - with capsys.disabled(): - download_media.download() - -def test_matplotlib(): - #for now, python 3.5 installs a version of matplotlib that complains - #about $DISPLAY variable, so lets just ignore for now. - if PYTHON_VERSION in ('2.7', '3.3') or (PYTHON_VERSION == '3.5' and TRAVIS): - return - - import matplotlib.pyplot as plt - import numpy as np - from moviepy.video.io.bindings import mplfig_to_npimage - from moviepy.video.VideoClip import VideoClip - - x = np.linspace(-2, 2, 200) - - duration = 2 - - fig, ax = plt.subplots() - - def make_frame(t): - ax.clear() - ax.plot(x, np.sinc(x**2) + np.sin(x + 2*np.pi/duration * t), lw=3) - ax.set_ylim(-1.5, 2.5) - return mplfig_to_npimage(fig) - - animation = VideoClip(make_frame, duration=duration) - animation.write_gif(os.path.join(TMP_DIR, 'matplotlib.gif'), fps=20) - -if __name__ == '__main__': - pytest.main() +"""Example tests for use with pytest. + +TODO: + * Resolve matplotlib errors during automated testing. +""" +# import os +# import sys +# +# import pytest +# +# import download_media +# from test_helper import PYTHON_VERSION, TMP_DIR, TRAVIS +# +# sys.path.append("tests") +# +# +# def test_download_media(capsys): +# with capsys.disabled(): +# download_media.download() +# +# def test_matplotlib(): +# #for now, python 3.5 installs a version of matplotlib that complains +# #about $DISPLAY variable, so lets just ignore for now. +# if PYTHON_VERSION in ('2.7', '3.3') or (PYTHON_VERSION == '3.5' and TRAVIS): +# return +# +# import matplotlib +# import numpy as np +# from moviepy.video.io.bindings import mplfig_to_npimage +# from moviepy.video.VideoClip import VideoClip +# +# x = np.linspace(-2, 2, 200) +# +# duration = 2 +# +# matplotlib.use('Agg') +# fig, ax = matplotlib.plt.subplots() +# +# def make_frame(t): +# ax.clear() +# ax.plot(x, np.sinc(x**2) + np.sin(x + 2*np.pi/duration * t), lw=3) +# ax.set_ylim(-1.5, 2.5) +# return mplfig_to_npimage(fig) +# +# animation = VideoClip(make_frame, duration=duration) +# animation.write_gif(os.path.join(TMP_DIR, 'matplotlib.gif'), fps=20) +# +# if __name__ == '__main__': +# pytest.main() diff --git a/tests/test_fx.py b/tests/test_fx.py index 7e400148a..41c1214c8 100644 --- a/tests/test_fx.py +++ b/tests/test_fx.py @@ -8,12 +8,27 @@ from moviepy.video.fx.crop import crop from moviepy.video.fx.fadein import fadein from moviepy.video.fx.fadeout import fadeout +from moviepy.video.fx.invert_colors import invert_colors +from moviepy.video.fx.loop import loop +from moviepy.video.fx.lum_contrast import lum_contrast +from moviepy.video.fx.make_loopable import make_loopable +from moviepy.video.fx.margin import margin +from moviepy.video.fx.mirror_x import mirror_x +from moviepy.video.fx.mirror_y import mirror_y +from moviepy.video.fx.resize import resize +from moviepy.video.fx.rotate import rotate +from moviepy.video.fx.speedx import speedx +from moviepy.video.fx.time_mirror import time_mirror +from moviepy.video.fx.time_symmetrize import time_symmetrize +from moviepy.audio.fx.audio_normalize import audio_normalize +from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.video.io.VideoFileClip import VideoFileClip +sys.path.append("tests") + import download_media from test_helper import TMP_DIR -sys.path.append("tests") def test_download_media(capsys): @@ -21,51 +36,195 @@ def test_download_media(capsys): download_media.download() def test_blackwhite(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") - clip1 = blackwhite(clip) - clip1.write_videofile(os.path.join(TMP_DIR,"blackwhite1.webm")) + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: + clip1 = blackwhite(clip) + clip1.write_videofile(os.path.join(TMP_DIR,"blackwhite1.webm")) # This currently fails with a with_mask error! # def test_blink(): -# clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,10) -# clip1 = blink(clip, 1, 1) -# clip1.write_videofile(os.path.join(TMP_DIR,"blink1.webm")) +# with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,10) as clip: +# clip1 = blink(clip, 1, 1) +# clip1.write_videofile(os.path.join(TMP_DIR,"blink1.webm")) def test_colorx(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") - clip1 = colorx(clip, 2) - clip1.write_videofile(os.path.join(TMP_DIR,"colorx1.webm")) + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: + clip1 = colorx(clip, 2) + clip1.write_videofile(os.path.join(TMP_DIR,"colorx1.webm")) def test_crop(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: - clip1=crop(clip) #ie, no cropping (just tests all default values) - clip1.write_videofile(os.path.join(TMP_DIR, "crop1.webm")) + clip1=crop(clip) #ie, no cropping (just tests all default values) + clip1.write_videofile(os.path.join(TMP_DIR, "crop1.webm")) - clip2=crop(clip, x1=50, y1=60, x2=460, y2=275) - clip2.write_videofile(os.path.join(TMP_DIR, "crop2.webm")) + clip2=crop(clip, x1=50, y1=60, x2=460, y2=275) + clip2.write_videofile(os.path.join(TMP_DIR, "crop2.webm")) - clip3=crop(clip, y1=30) #remove part above y=30 - clip3.write_videofile(os.path.join(TMP_DIR, "crop3.webm")) + clip3=crop(clip, y1=30) #remove part above y=30 + clip3.write_videofile(os.path.join(TMP_DIR, "crop3.webm")) - clip4=crop(clip, x1=10, width=200) # crop a rect that has width=200 - clip4.write_videofile(os.path.join(TMP_DIR, "crop4.webm")) + clip4=crop(clip, x1=10, width=200) # crop a rect that has width=200 + clip4.write_videofile(os.path.join(TMP_DIR, "crop4.webm")) - clip5=crop(clip, x_center=300, y_center=400, width=50, height=150) - clip5.write_videofile(os.path.join(TMP_DIR, "crop5.webm")) + clip5=crop(clip, x_center=300, y_center=400, width=50, height=150) + clip5.write_videofile(os.path.join(TMP_DIR, "crop5.webm")) - clip6=crop(clip, x_center=300, width=400, y1=100, y2=600) - clip6.write_videofile(os.path.join(TMP_DIR, "crop6.webm")) + clip6=crop(clip, x_center=300, width=400, y1=100, y2=600) + clip6.write_videofile(os.path.join(TMP_DIR, "crop6.webm")) def test_fadein(): - clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) - clip1 = fadein(clip, 1) - clip1.write_videofile(os.path.join(TMP_DIR,"fadein1.webm")) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) as clip: + clip1 = fadein(clip, 1) + clip1.write_videofile(os.path.join(TMP_DIR,"fadein1.webm")) def test_fadeout(): - clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) - clip1 = fadeout(clip, 1) - clip1.write_videofile(os.path.join(TMP_DIR,"fadeout1.webm")) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) as clip: + clip1 = fadeout(clip, 1) + clip1.write_videofile(os.path.join(TMP_DIR,"fadeout1.webm")) + +def test_invert_colors(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + clip1 = invert_colors(clip) + clip1.write_videofile(os.path.join(TMP_DIR, "invert_colors1.webm")) + +def test_loop(): + #these do not work.. what am I doing wrong?? + return + + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + clip1 = clip.loop() #infinite looping + clip1.write_videofile(os.path.join(TMP_DIR, "loop1.webm")) + + clip2 = clip.loop(duration=10) #loop for 10 seconds + clip2.write_videofile(os.path.join(TMP_DIR, "loop2.webm")) + + clip3 = clip.loop(n=3) #loop 3 times + clip3.write_videofile(os.path.join(TMP_DIR, "loop3.webm")) + +def test_lum_contrast(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + clip1 = lum_contrast(clip) + clip1.write_videofile(os.path.join(TMP_DIR, "lum_contrast1.webm")) + + #what are the correct value ranges for function arguments lum, + #contrast and contrast_thr? Maybe we should check for these in + #lum_contrast. + +def test_make_loopable(): + clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,10) + clip1 = make_loopable(clip, 1) + clip1.write_videofile(os.path.join(TMP_DIR, "make_loopable1.webm")) + +def test_margin(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + clip1 = margin(clip) #does the default values change anything? + clip1.write_videofile(os.path.join(TMP_DIR, "margin1.webm")) + + clip2 = margin(clip, mar=100) # all margins are 100px + clip2.write_videofile(os.path.join(TMP_DIR, "margin2.webm")) + + clip3 = margin(clip, mar=100, color=(255,0,0)) #red margin + clip3.write_videofile(os.path.join(TMP_DIR, "margin3.webm")) + +def test_mask_and(): + pass + +def test_mask_color(): + pass + +def test_mask_or(): + pass + +def test_mirror_x(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + clip1 = mirror_x(clip) + clip1.write_videofile(os.path.join(TMP_DIR, "mirror_x1.webm")) + +def test_mirror_y(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + clip1 = mirror_y(clip) + clip1.write_videofile(os.path.join(TMP_DIR, "mirror_y1.webm")) + +def test_painting(): + pass + +def test_resize(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + + clip1=clip.resize( (460,720) ) # New resolution: (460,720) + assert clip1.size == (460,720) + clip1.write_videofile(os.path.join(TMP_DIR, "resize1.webm")) + + clip2=clip.resize(0.6) # width and heigth multiplied by 0.6 + assert clip2.size == (clip.size[0]*0.6, clip.size[1]*0.6) + clip2.write_videofile(os.path.join(TMP_DIR, "resize2.webm")) + + clip3=clip.resize(width=800) # height computed automatically. + assert clip3.w == 800 + #assert clip3.h == ?? + clip3.write_videofile(os.path.join(TMP_DIR, "resize3.webm")) + + #I get a general stream error when playing this video. + #clip4=clip.resize(lambda t : 1+0.02*t) # slow swelling of the clip + #clip4.write_videofile(os.path.join(TMP_DIR, "resize4.webm")) + +def test_rotate(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + + clip1=rotate(clip, 90) # rotate 90 degrees + assert clip1.size == (clip.size[1], clip.size[0]) + clip1.write_videofile(os.path.join(TMP_DIR, "rotate1.webm")) + + clip2=rotate(clip, 180) # rotate 90 degrees + assert clip2.size == tuple(clip.size) + clip2.write_videofile(os.path.join(TMP_DIR, "rotate2.webm")) + + clip3=rotate(clip, 270) # rotate 90 degrees + assert clip3.size == (clip.size[1], clip.size[0]) + clip3.write_videofile(os.path.join(TMP_DIR, "rotate3.webm")) + + clip4=rotate(clip, 360) # rotate 90 degrees + assert clip4.size == tuple(clip.size) + clip4.write_videofile(os.path.join(TMP_DIR, "rotate4.webm")) + +def test_scroll(): + pass + +def test_speedx(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + + clip1=speedx(clip, factor=0.5) # 1/2 speed + assert clip1.duration == 2 + clip1.write_videofile(os.path.join(TMP_DIR, "speedx1.webm")) + + clip2=speedx(clip, final_duration=2) # 1/2 speed + assert clip2.duration == 2 + clip2.write_videofile(os.path.join(TMP_DIR, "speedx2.webm")) + + clip2=speedx(clip, final_duration=3) # 1/2 speed + assert clip2.duration == 3 + clip2.write_videofile(os.path.join(TMP_DIR, "speedx3.webm")) + +def test_supersample(): + pass + +def test_time_mirror(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + + clip1=time_mirror(clip) + assert clip1.duration == clip.duration + clip1.write_videofile(os.path.join(TMP_DIR, "time_mirror1.webm")) + +def test_time_symmetrize(): + clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + + clip1=time_symmetrize(clip) + clip1.write_videofile(os.path.join(TMP_DIR, "time_symmetrize1.webm")) + +def test_normalize(): + clip = AudioFileClip('media/crunching.mp3') + clip = audio_normalize(clip) + assert clip.max_volume() == 1 if __name__ == '__main__': diff --git a/tests/test_helper.py b/tests/test_helper.py index 7913b44e9..9a17b495b 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -2,7 +2,17 @@ """Define general test helper attributes and utilities.""" import os import sys +import tempfile -TRAVIS=os.getenv("TRAVIS_PYTHON_VERSION") is not None +TRAVIS = os.getenv("TRAVIS_PYTHON_VERSION") is not None PYTHON_VERSION = "%s.%s" % (sys.version_info.major, sys.version_info.minor) -TMP_DIR="/tmp" +TMP_DIR = tempfile.gettempdir() # because tempfile.tempdir is sometimes None + +# Arbitrary font used in caption testing. +if sys.platform in ("win32", "cygwin"): + FONT = "Arial" + # Even if Windows users install the Liberation fonts, it is called LiberationMono on Windows, so + # it doesn't help. +else: + FONT = "Liberation-Mono" # This is available in the fonts-liberation package on Linux. + diff --git a/tests/test_issues.py b/tests/test_issues.py index a362b4229..ebfb91e02 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- """Issue tests meant to be run with pytest.""" import os -#import sys +import sys import pytest from moviepy.editor import * -#sys.path.append("tests") +sys.path.append("tests") import download_media from test_helper import PYTHON_VERSION, TMP_DIR, TRAVIS @@ -18,9 +18,9 @@ def test_download_media(capsys): download_media.download() def test_issue_145(): - video = ColorClip((800, 600), color=(255,0,0)).set_duration(5) - with pytest.raises(Exception, message='Expecting Exception'): - concatenate_videoclips([video], method='composite') + with ColorClip((800, 600), color=(255,0,0)).set_duration(5) as video: + with pytest.raises(Exception, message='Expecting Exception'): + concatenate_videoclips([video], method='composite') def test_issue_190(): #from PIL import Image @@ -28,7 +28,7 @@ def test_issue_190(): #from imageio import imread #image = imread(os.path.join(TMP_DIR, "issue_190.png")) - + #clip = ImageSequenceClip([image, image], fps=1) #clip.write_videofile(os.path.join(TMP_DIR, "issue_190.mp4")) pass @@ -40,6 +40,9 @@ def test_issue_285(): ImageClip('media/python_logo.png', duration=10) merged_clip = concatenate_videoclips([clip_1, clip_2, clip_3]) assert merged_clip.duration == 30 + clip_1.close() + clip_2.close() + clip_3.close() def test_issue_334(): last_move = None @@ -126,78 +129,79 @@ def size(t): return (nsw, nsh) return (last_move1[3], last_move1[3] * 1.33) - avatar = VideoFileClip("media/big_buck_bunny_432_433.webm", has_mask=True) - avatar.audio=None - maskclip = ImageClip("media/afterimage.png", ismask=True, transparent=True) - avatar.set_mask(maskclip) #must set maskclip here.. + with VideoFileClip("media/big_buck_bunny_432_433.webm", has_mask=True) as avatar: + avatar.audio=None + maskclip = ImageClip("media/afterimage.png", ismask=True, transparent=True) + avatar.set_mask(maskclip) #must set maskclip here.. - avatar = concatenate_videoclips([avatar]*11) + concatenated = concatenate_videoclips([avatar]*11) - tt = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,11) - # TODO: Setting mask here does not work: .set_mask(maskclip).resize(size)]) - final = CompositeVideoClip([tt, avatar.set_position(posi).resize(size)]) - final.duration = tt.duration - final.write_videofile(os.path.join(TMP_DIR, 'issue_334.mp4'), fps=24) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,11) as tt: + # TODO: Setting mask here does not work: .set_mask(maskclip).resize(size)]) + final = CompositeVideoClip([tt, concatenated.set_position(posi).resize(size)]) + final.duration = tt.duration + final.write_videofile(os.path.join(TMP_DIR, 'issue_334.mp4'), fps=24) def test_issue_354(): - clip = ImageClip("media/python_logo.png") + with ImageClip("media/python_logo.png") as clip: - clip.duration = 10 - crosstime = 1 + clip.duration = 10 + crosstime = 1 - # TODO: Should this be removed? - # caption = editor.TextClip("test text", font="Liberation-Sans-Bold", - # color='white', stroke_color='gray', - # stroke_width=2, method='caption', - # size=(1280, 720), fontsize=60, - # align='South-East') - #caption.duration = clip.duration + # TODO: Should this be removed? + # caption = editor.TextClip("test text", font="Liberation-Sans-Bold", + # color='white', stroke_color='gray', + # stroke_width=2, method='caption', + # size=(1280, 720), fontsize=60, + # align='South-East') + #caption.duration = clip.duration - fadecaption = clip.crossfadein(crosstime).crossfadeout(crosstime) - CompositeVideoClip([clip, fadecaption]) + fadecaption = clip.crossfadein(crosstime).crossfadeout(crosstime) + CompositeVideoClip([clip, fadecaption]).close() def test_issue_359(): - video = ColorClip((800, 600), color=(255,0,0)).set_duration(5) - video.fps=30 - video.write_gif(filename=os.path.join(TMP_DIR, "issue_359.gif"), - tempfiles=True) - -def test_issue_368(): - # Matplotlib only supported in python >= 3.4 and Travis/3.5 fails. - if PYTHON_VERSION in ('2.7', '3.3') or (PYTHON_VERSION == '3.5' and TRAVIS): - return - - import numpy as np - import matplotlib.pyplot as plt - from sklearn import svm - from sklearn.datasets import make_moons - from moviepy.video.io.bindings import mplfig_to_npimage - - X, Y = make_moons(50, noise=0.1, random_state=2) # semi-random data - - fig, ax = plt.subplots(1, figsize=(4, 4), facecolor=(1,1,1)) - fig.subplots_adjust(left=0, right=1, bottom=0) - xx, yy = np.meshgrid(np.linspace(-2,3,500), np.linspace(-1,2,500)) - - def make_frame(t): - ax.clear() - ax.axis('off') - ax.set_title("SVC classification", fontsize=16) - - classifier = svm.SVC(gamma=2, C=1) - # the varying weights make the points appear one after the other - weights = np.minimum(1, np.maximum(0, t**2+10-np.arange(50))) - classifier.fit(X, Y, sample_weight=weights) - Z = classifier.decision_function(np.c_[xx.ravel(), yy.ravel()]) - Z = Z.reshape(xx.shape) - ax.contourf(xx, yy, Z, cmap=plt.cm.bone, alpha=0.8, - vmin=-2.5, vmax=2.5, levels=np.linspace(-2,2,20)) - ax.scatter(X[:,0], X[:,1], c=Y, s=50*weights, cmap=plt.cm.bone) - - return mplfig_to_npimage(fig) - - animation = VideoClip(make_frame, duration=2) - animation.write_gif(os.path.join(TMP_DIR, "svm.gif"), fps=20) + with ColorClip((800, 600), color=(255,0,0)).set_duration(5) as video: + video.fps=30 + video.write_gif(filename=os.path.join(TMP_DIR, "issue_359.gif"), + tempfiles=True) + +# TODO: Debug matplotlib failures following successful travis builds. +# def test_issue_368(): +# # Matplotlib only supported in python >= 3.4 and Travis/3.5 fails. +# if PYTHON_VERSION in ('2.7', '3.3') or (PYTHON_VERSION == '3.5' and TRAVIS): +# return +# +# import numpy as np +# import matplotlib.pyplot as plt +# from sklearn import svm +# from sklearn.datasets import make_moons +# from moviepy.video.io.bindings import mplfig_to_npimage +# +# X, Y = make_moons(50, noise=0.1, random_state=2) # semi-random data +# +# fig, ax = plt.subplots(1, figsize=(4, 4), facecolor=(1,1,1)) +# fig.subplots_adjust(left=0, right=1, bottom=0) +# xx, yy = np.meshgrid(np.linspace(-2,3,500), np.linspace(-1,2,500)) +# +# def make_frame(t): +# ax.clear() +# ax.axis('off') +# ax.set_title("SVC classification", fontsize=16) +# +# classifier = svm.SVC(gamma=2, C=1) +# # the varying weights make the points appear one after the other +# weights = np.minimum(1, np.maximum(0, t**2+10-np.arange(50))) +# classifier.fit(X, Y, sample_weight=weights) +# Z = classifier.decision_function(np.c_[xx.ravel(), yy.ravel()]) +# Z = Z.reshape(xx.shape) +# ax.contourf(xx, yy, Z, cmap=plt.cm.bone, alpha=0.8, +# vmin=-2.5, vmax=2.5, levels=np.linspace(-2,2,20)) +# ax.scatter(X[:,0], X[:,1], c=Y, s=50*weights, cmap=plt.cm.bone) +# +# return mplfig_to_npimage(fig) +# +# animation = VideoClip(make_frame, duration=2) +# animation.write_gif(os.path.join(TMP_DIR, "svm.gif"), fps=20) def test_issue_407(): red = ColorClip((800, 600), color=(255,0,0)).set_duration(5) @@ -252,7 +256,7 @@ def test_issue_470(): subclip = audio_clip.subclip(t_start=6, t_end=9) with pytest.raises(IOError, message="Expecting IOError"): - subclip.write_audiofile('/tmp/issue_470.wav', write_logfile=True) + subclip.write_audiofile(os.path.join(TMP_DIR, 'issue_470.wav'), write_logfile=True) #but this one should work.. subclip = audio_clip.subclip(t_start=6, t_end=8) @@ -265,6 +269,34 @@ def test_audio_reader(): subclip.write_audiofile(os.path.join(TMP_DIR, 'issue_246.wav'), write_logfile=True) +def test_issue_547(): + red = ColorClip((640, 480), color=(255,0,0)).set_duration(1) + green = ColorClip((640, 480), color=(0,255,0)).set_duration(2) + blue = ColorClip((640, 480), color=(0,0,255)).set_duration(3) + + video=concatenate_videoclips([red, green, blue], method="compose") + assert video.duration == 6 + assert video.mask.duration == 6 + + video=concatenate_videoclips([red, green, blue]) + assert video.duration == 6 + +def test_issue_636(): + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,11) as video: + with video.subclip(0,1) as subclip: + pass + +def test_issue_655(): + video_file = 'media/fire2.mp4' + for subclip in [(0,2),(1,2),(2,3)]: + with VideoFileClip(video_file) as v: + with v.subclip(1,2) as s: + pass + next(v.subclip(*subclip).iter_frames()) + assert True + + if __name__ == '__main__': pytest.main() + diff --git a/tests/test_misc.py b/tests/test_misc.py index a375900ad..7d79e4042 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -10,7 +10,7 @@ from moviepy.video.io.VideoFileClip import VideoFileClip import download_media -from test_helper import TMP_DIR, TRAVIS +from test_helper import TMP_DIR, TRAVIS, FONT sys.path.append("tests") @@ -20,8 +20,8 @@ def test_download_media(capsys): download_media.download() def test_cuts1(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm").resize(0.2) - cuts.find_video_period(clip) == pytest.approx(0.966666666667, 0.0001) + with VideoFileClip("media/big_buck_bunny_432_433.webm").resize(0.2) as clip: + cuts.find_video_period(clip) == pytest.approx(0.966666666667, 0.0001) def test_subtitles(): red = ColorClip((800, 600), color=(255,0,0)).set_duration(10) @@ -35,14 +35,14 @@ def test_subtitles(): if TRAVIS: return - generator = lambda txt: TextClip(txt, font='Liberation-Mono', + generator = lambda txt: TextClip(txt, font=FONT, size=(800,600), fontsize=24, method='caption', align='South', color='white') subtitles = SubtitlesClip("media/subtitles1.srt", generator) - final = CompositeVideoClip([myvideo, subtitles]) - final.to_videofile(os.path.join(TMP_DIR, "subtitles1.mp4"), fps=30) + with CompositeVideoClip([myvideo, subtitles]) as final: + final.to_videofile(os.path.join(TMP_DIR, "subtitles1.mp4"), fps=30) data = [([0.0, 4.0], 'Red!'), ([5.0, 9.0], 'More Red!'), ([10.0, 14.0], 'Green!'), ([15.0, 19.0], 'More Green!'), diff --git a/tests/test_resourcerelease.py b/tests/test_resourcerelease.py new file mode 100644 index 000000000..98c9eb0cd --- /dev/null +++ b/tests/test_resourcerelease.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +""" + Tool tests meant to be run with pytest. + + Testing whether issue #596 has been repaired. + + Note: Platform dependent test. Will only fail on Windows > NT. """ + +from os import remove +from os.path import join +import subprocess as sp +import time +# from tempfile import NamedTemporaryFile +from test_helper import TMP_DIR + +from moviepy.video.compositing.CompositeVideoClip import clips_array +from moviepy.video.VideoClip import ColorClip +from moviepy.video.io.VideoFileClip import VideoFileClip + + +def test_release_of_file_via_close(): + # Create a random video file. + red = ColorClip((1024, 800), color=(255, 0, 0)) + green = ColorClip((1024, 800), color=(0, 255, 0)) + blue = ColorClip((1024, 800), color=(0, 0, 255)) + + red.fps = green.fps = blue.fps = 30 + + # Repeat this so we can see no conflicts. + for i in range(5): + # Get the name of a temporary file we can use. + local_video_filename = join(TMP_DIR, "test_release_of_file_via_close_%s.mp4" % int(time.time())) + + with clips_array([[red, green, blue]]) as ca: + video = ca.set_duration(1) + + video.write_videofile(local_video_filename) + + # Open it up with VideoFileClip. + with VideoFileClip(local_video_filename) as clip: + # Normally a client would do processing here. + pass + + # Now remove the temporary file. + # This would fail on Windows if the file is still locked. + + # This should succeed without exceptions. + remove(local_video_filename) + + red.close() + green.close() + blue.close() diff --git a/tests/test_resourcereleasedemo.py b/tests/test_resourcereleasedemo.py new file mode 100644 index 000000000..e190a913a --- /dev/null +++ b/tests/test_resourcereleasedemo.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +""" + Tool tests meant to be run with pytest. + + Demonstrates issue #596 exists. + + Note: Platform dependent test. Will only fail on Windows > NT. """ + +from os import remove +from os.path import join +import subprocess as sp +import time +# from tempfile import NamedTemporaryFile +from test_helper import TMP_DIR + +from moviepy.video.compositing.CompositeVideoClip import clips_array +from moviepy.video.VideoClip import ColorClip +from moviepy.video.io.VideoFileClip import VideoFileClip +# import pytest + +def test_failure_to_release_file(): + + """ This isn't really a test, because it is expected to fail. + It demonstrates that there *is* a problem with not releasing resources when running on + Windows. + + The real issue was that, as of movepy 0.2.3.2, there was no way around it. + + See test_resourcerelease.py to see how the close() methods provide a solution. + """ + + # Get the name of a temporary file we can use. + local_video_filename = join(TMP_DIR, "test_release_of_file_%s.mp4" % int(time.time())) + + # Repeat this so we can see that the problems escalate: + for i in range(5): + + # Create a random video file. + red = ColorClip((1024,800), color=(255,0,0)) + green = ColorClip((1024,800), color=(0,255,0)) + blue = ColorClip((1024,800), color=(0,0,255)) + + red.fps = green.fps = blue.fps = 30 + video = clips_array([[red, green, blue]]).set_duration(1) + + try: + video.write_videofile(local_video_filename) + + # Open it up with VideoFileClip. + clip = VideoFileClip(local_video_filename) + + # Normally a client would do processing here. + + # All finished, so delete the clipS. + del clip + del video + + except IOError: + print("On Windows, this succeeds the first few times around the loop, but eventually fails.") + print("Need to shut down the process now. No more tests in this file.") + return + + + try: + # Now remove the temporary file. + # This will fail on Windows if the file is still locked. + + # In particular, this raises an exception with PermissionError. + # In there was no way to avoid it. + + remove(local_video_filename) + print("You are not running Windows, because that worked.") + except OSError: # More specifically, PermissionError in Python 3. + print("Yes, on Windows this fails.") diff --git a/tests/test_tools.py b/tests/test_tools.py index b1510bb6c..2c276b4a4 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -69,17 +69,5 @@ def test_5(): file = sys.stdout.read() assert file == b"" -def test_6(): - """Test subprocess_call for operation. - - The process sleep should run for a given time in seconds. - This checks that the process has deallocated from the stack on - completion of the called process. - - """ - process = tools.subprocess_call(["sleep" , '1']) - time.sleep(1) - assert process is None - if __name__ == '__main__': pytest.main() From 006f4c86bceb3c87d5f961e374e302176ed798c3 Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Mon, 12 Feb 2018 11:20:54 +0000 Subject: [PATCH 2/5] More PEP8 compliances --- moviepy/video/VideoClip.py | 84 +++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 6ad8de976..0744bef90 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -262,11 +262,11 @@ def write_videofile(self, filename, fps=None, codec=None, codec = extensions_dict[ext]['codec'][0] except KeyError: raise ValueError("MoviePy couldn't find the codec associated " - "with the filename. Provide the 'codec' parameter in " - "write_videofile.") + "with the filename. Provide the 'codec' " + "parameter in write_videofile.") if audio_codec is None: - if (ext in ['ogv', 'webm']): + if ext in ['ogv', 'webm']: audio_codec = 'libvorbis' else: audio_codec = 'libmp3lame' @@ -297,8 +297,9 @@ def write_videofile(self, filename, fps=None, codec=None, raise ValueError( "The audio_codec you chose is unknown by MoviePy. " - "You should report this. In the meantime, you can specify a " - "temp_audiofile with the right extension in write_videofile.") + "You should report this. In the meantime, you can " + "specify a temp_audiofile with the right extension " + "in write_videofile.") audiofile = (name + Clip._TEMP_FILES_PREFIX + "wvf_snd.%s" % audio_ext) @@ -319,7 +320,7 @@ def write_videofile(self, filename, fps=None, codec=None, bitrate=bitrate, preset=preset, write_logfile=write_logfile, - audiofile = audiofile, + audiofile=audiofile, verbose=verbose, threads=threads, ffmpeg_params=ffmpeg_params, progress_bar=progress_bar) @@ -354,7 +355,7 @@ def write_images_sequence(self, nameformat, fps=None, verbose=True, will save the clip's mask (if any) as an alpha canal (PNGs only). verbose - Boolean indicating whether to print infomation. + Boolean indicating whether to print information. progress_bar Boolean indicating whether to show the progress bar. @@ -443,13 +444,13 @@ def write_gif(self, filename, fps=None, program='imageio', write_gif_with_image_io(self, filename, fps=fps, opt=opt, loop=loop, verbose=verbose, colors=colors) elif tempfiles: - #convert imageio opt variable to something that can be used with - #ImageMagick - opt1=opt + # convert imageio opt variable to something that can be used with + # ImageMagick + opt1 = opt if opt1 == 'nq': - opt1='optimizeplus' + opt1 ='optimizeplus' else: - opt1='OptimizeTransparency' + opt1 ='OptimizeTransparency' write_gif_with_tempfiles(self, filename, fps=fps, program=program, opt=opt1, fuzz=fuzz, verbose=verbose, @@ -636,7 +637,7 @@ def on_color(self, size=None, color=(0, 0, 0), pos=None, bg_color=color) if (isinstance(self, ImageClip) and (not hasattr(pos, "__call__")) - and ((self.mask is None) or isinstance(self.mask, ImageClip))): + and ((self.mask is None) or isinstance(self.mask, ImageClip))): new_result = result.to_ImageClip() if result.mask is not None: new_result.mask = result.mask.to_ImageClip() @@ -682,7 +683,6 @@ def set_opacity(self, op): """ self.mask = self.mask.fl_image(lambda pic: op * pic) - @apply_to_mask @outplace def set_position(self, pos, relative=False): @@ -715,7 +715,7 @@ def set_position(self, pos, relative=False): else: self.pos = lambda t: pos - #-------------------------------------------------------------- + # -------------------------------------------------------------- # CONVERSIONS TO OTHER TYPES @convert_to_seconds(['t']) @@ -730,7 +730,6 @@ def to_ImageClip(self, t=0, with_mask=True): newclip.mask = self.mask.to_ImageClip(t) return newclip - def to_mask(self, canal=0): """Return a mask a video clip made from the clip.""" if self.ismask: @@ -751,7 +750,7 @@ def to_RGB(self): else: return self - #---------------------------------------------------------------- + # ---------------------------------------------------------------- # Audio @outplace @@ -797,10 +796,11 @@ def __init__(self, data, data_to_frame, fps, ismask=False, has_constant_size=True): self.data = data self.data_to_frame = data_to_frame - self.fps=fps - make_frame = lambda t: self.data_to_frame( self.data[int(self.fps*t)]) + self.fps = fps + make_frame = lambda t: self.data_to_frame(self.data[int(self.fps*t)]) VideoClip.__init__(self, make_frame, ismask=ismask, - duration=1.0*len(data)/fps, has_constant_size=has_constant_size) + duration=1.0*len(data)/fps, + has_constant_size=has_constant_size) class UpdatedVideoClip(VideoClip): @@ -838,12 +838,14 @@ class UpdatedVideoClip(VideoClip): def __init__(self, world, ismask=False, duration=None): self.world = world + def make_frame(t): while self.world.clip_t < t: world.update() return world.to_frame() - VideoClip.__init__(self, make_frame= make_frame, - ismask=ismask, duration=duration) + + VideoClip.__init__(self, make_frame=make_frame, + ismask=ismask, duration=duration) """--------------------------------------------------------------------- @@ -895,11 +897,11 @@ def __init__(self, img, ismask=False, transparent=True, VideoClip.__init__(self, ismask=ismask, duration=duration) if PY3: - if isinstance(img, str): - img = imread(img) + if isinstance(img, str): + img = imread(img) else: - if isinstance(img, (str, unicode)): - img = imread(img) + if isinstance(img, (str, unicode)): + img = imread(img) if len(img.shape) == 3: # img is (now) a RGB(a) numpy array @@ -985,12 +987,12 @@ def fl_time(self, time_func, apply_to=None, # replaced by the more explicite write_videofile, write_gif, etc. VideoClip.set_pos = deprecated_version_of(VideoClip.set_position, - 'set_pos') + 'set_pos') VideoClip.to_videofile = deprecated_version_of(VideoClip.write_videofile, 'to_videofile') VideoClip.to_gif = deprecated_version_of(VideoClip.write_gif, 'to_gif') VideoClip.to_images_sequence = deprecated_version_of(VideoClip.write_images_sequence, - 'to_images_sequence') + 'to_images_sequence') class ColorClip(ImageClip): @@ -1119,10 +1121,10 @@ def __init__(self, txt=None, filename=None, size=None, color='black', size = ('' if size[0] is None else str(size[0]), '' if size[1] is None else str(size[1])) - cmd = ( [get_setting("IMAGEMAGICK_BINARY"), + cmd = ([get_setting("IMAGEMAGICK_BINARY"), "-background", bg_color, - "-fill", color, - "-font", font]) + "-fill", color, + "-font", font]) if fontsize is not None: cmd += ["-pointsize", "%d" % fontsize] @@ -1146,18 +1148,18 @@ def __init__(self, txt=None, filename=None, size=None, color='black', "-type", "truecolormatte", "PNG32:%s" % tempfilename] if print_cmd: - print( " ".join(cmd) ) + print(" ".join(cmd)) try: - subprocess_call(cmd, verbose=False ) - except (IOError,OSError) as err: - error = ("MoviePy Error: creation of %s failed because " - "of the following error:\n\n%s.\n\n."%(filename, str(err)) - + ("This error can be due to the fact that " - "ImageMagick is not installed on your computer, or " - "(for Windows users) that you didn't specify the " - "path to the ImageMagick binary in file conf.py, or." - "that the path you specified is incorrect" )) + subprocess_call(cmd, verbose=False) + except (IOError, OSError) as err: + error = ("MoviePy Error: creation of %s failed because of the " + "following error:\n\n%s.\n\n." % (filename, str(err)) + + ("This error can be due to the fact that ImageMagick " + "is not installed on your computer, or (for Windows " + "users) that you didn't specify the path to the " + "ImageMagick binary in file conf.py, or that the path " + "you specified is incorrect")) raise IOError(error) ImageClip.__init__(self, tempfilename, transparent=transparent) From 501232a52c78e1f80f511d347e8c7f50a3634714 Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Mon, 12 Feb 2018 12:07:04 +0000 Subject: [PATCH 3/5] PEP8 --- moviepy/video/io/VideoFileClip.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/moviepy/video/io/VideoFileClip.py b/moviepy/video/io/VideoFileClip.py index a5e300c40..015963eb0 100644 --- a/moviepy/video/io/VideoFileClip.py +++ b/moviepy/video/io/VideoFileClip.py @@ -5,6 +5,7 @@ from moviepy.Clip import Clip from moviepy.video.io.ffmpeg_reader import FFMPEG_VideoReader + class VideoFileClip(VideoClip): """ @@ -83,7 +84,7 @@ def __init__(self, filename, has_mask=False, VideoClip.__init__(self) # Make a reader - pix_fmt= "rgba" if has_mask else "rgb24" + pix_fmt = "rgba" if has_mask else "rgb24" self.reader = FFMPEG_VideoReader(filename, pix_fmt=pix_fmt, target_resolution=target_resolution, resize_algo=resize_algorithm, @@ -102,9 +103,9 @@ def __init__(self, filename, has_mask=False, if has_mask: self.make_frame = lambda t: self.reader.get_frame(t)[:,:,:3] - mask_mf = lambda t: self.reader.get_frame(t)[:,:,3]/255.0 - self.mask = (VideoClip(ismask = True, make_frame = mask_mf) - .set_duration(self.duration)) + mask_mf = lambda t: self.reader.get_frame(t)[:,:,3]/255.0 + self.mask = (VideoClip(ismask = True, make_frame=mask_mf) + .set_duration(self.duration)) self.mask.fps = self.fps else: @@ -115,9 +116,9 @@ def __init__(self, filename, has_mask=False, if audio and self.reader.infos['audio_found']: self.audio = AudioFileClip(filename, - buffersize= audio_buffersize, - fps = audio_fps, - nbytes = audio_nbytes) + buffersize=audio_buffersize, + fps=audio_fps, + nbytes=audio_nbytes) def close(self): """ Close the internal reader. """ From 8676acc2c2ba051ebc5e71e2a78be2301150696d Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Mon, 12 Feb 2018 12:14:19 +0000 Subject: [PATCH 4/5] PEP8 2 --- moviepy/Clip.py | 48 ++++++++++++++----------------- moviepy/video/io/VideoFileClip.py | 6 ++-- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index 015ecf513..4e72f5d70 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -7,14 +7,15 @@ from copy import copy import numpy as np -from moviepy.decorators import ( apply_to_mask, - apply_to_audio, - requires_duration, - outplace, - convert_to_seconds, - use_clip_fps_by_default) +from moviepy.decorators import (apply_to_mask, + apply_to_audio, + requires_duration, + outplace, + convert_to_seconds, + use_clip_fps_by_default) from tqdm import tqdm + class Clip: """ @@ -53,9 +54,7 @@ def __init__(self): self.memoize = False self.memoized_t = None - self.memoize_frame = None - - + self.memoize_frame = None def copy(self): """ Shallow copy of the clip. @@ -148,13 +147,11 @@ def fl(self, fun, apply_to=None, keep_duration=True): if hasattr(newclip, attr): a = getattr(newclip, attr) if a is not None: - new_a = a.fl(fun, keep_duration=keep_duration) + new_a = a.fl(fun, keep_duration=keep_duration) setattr(newclip, attr, new_a) return newclip - - def fl_time(self, t_func, apply_to=None, keep_duration=False): """ Returns a Clip instance playing the content of the current clip @@ -190,9 +187,7 @@ def fl_time(self, t_func, apply_to=None, keep_duration=False): apply_to = [] return self.fl(lambda gf, t: gf(t_func(t)), apply_to, - keep_duration=keep_duration) - - + keep_duration=keep_duration) def fx(self, func, *args, **kwargs): """ @@ -246,7 +241,7 @@ def set_start(self, t, change_end=True): self.start = t if (self.duration is not None) and change_end: self.end = t + self.duration - elif (self.end is not None): + elif self.end is not None: self.duration = self.end - self.start @@ -348,7 +343,7 @@ def is_playing(self, t): # If we arrive here, a part of t falls in the clip result = 1 * (t >= self.start) - if (self.end is not None): + if self.end is not None: result *= (t <= self.end) return result @@ -384,14 +379,15 @@ def subclip(self, t_start=0, t_end=None): they exist. """ - if t_start < 0: #make this more python like a negative value - #means to move backward from the end of the clip - t_start = self.duration + t_start #remeber t_start is negative + if t_start < 0: + # Make this more Python-like, a negative value means to move + # backward from the end of the clip + t_start = self.duration + t_start # Remember t_start is negative - if (self.duration is not None) and (t_start>self.duration): - raise ValueError("t_start (%.02f) "%t_start + + if (self.duration is not None) and (t_start > self.duration): + raise ValueError("t_start (%.02f) "% t_start + "should be smaller than the clip's "+ - "duration (%.02f)."%self.duration) + "duration (%.02f)."% self.duration) newclip = self.fl_time(lambda t: t + t_start, apply_to=[]) @@ -403,14 +399,14 @@ def subclip(self, t_start=0, t_end=None): if self.duration is None: - print ("Error: subclip with negative times (here %s)"%(str((t_start, t_end))) - +" can only be extracted from clips with a ``duration``") + print("Error: subclip with negative times (here %s)" % (str((t_start, t_end))) + + " can only be extracted from clips with a ``duration``") else: t_end = self.duration + t_end - if (t_end is not None): + if t_end is not None: newclip.duration = t_end - t_start newclip.end = newclip.start + newclip.duration diff --git a/moviepy/video/io/VideoFileClip.py b/moviepy/video/io/VideoFileClip.py index 015963eb0..cd5534e1d 100644 --- a/moviepy/video/io/VideoFileClip.py +++ b/moviepy/video/io/VideoFileClip.py @@ -15,7 +15,7 @@ class VideoFileClip(VideoClip): >>> clip = VideoFileClip("myHolidays.mp4") >>> clip.close() >>> with VideoFileClip("myMaskVideo.avi") as clip2: - >>> pass # Implicit close called by contex manager. + >>> pass # Implicit close called by context manager. Parameters @@ -76,7 +76,7 @@ class VideoFileClip(VideoClip): """ def __init__(self, filename, has_mask=False, - audio=True, audio_buffersize = 200000, + audio=True, audio_buffersize=200000, target_resolution=None, resize_algorithm='bicubic', audio_fps=44100, audio_nbytes=2, verbose=False, fps_source='tbr'): @@ -104,7 +104,7 @@ def __init__(self, filename, has_mask=False, self.make_frame = lambda t: self.reader.get_frame(t)[:,:,:3] mask_mf = lambda t: self.reader.get_frame(t)[:,:,3]/255.0 - self.mask = (VideoClip(ismask = True, make_frame=mask_mf) + self.mask = (VideoClip(ismask=True, make_frame=mask_mf) .set_duration(self.duration)) self.mask.fps = self.fps From 8e9516c3cf022f15fe1644e03ba6586d7f9db4a5 Mon Sep 17 00:00:00 2001 From: Tom Burrows Date: Mon, 12 Feb 2018 16:43:01 +0000 Subject: [PATCH 5/5] Removed support for Python 3.3 --- .travis.yml | 1 - appveyor.yml | 8 +------- setup.py | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7976b104e..8afc1929b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ language: python cache: pip python: - "2.7" - - "3.3" - "3.4" - "3.5" - "3.6" diff --git a/appveyor.yml b/appveyor.yml index da3f943ce..d662ecef1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,7 +16,7 @@ environment: matrix: - # MoviePy supports Python 2.7 and 3.3 onwards. + # MoviePy supports Python 2.7 and 3.4 onwards. # Strategy: # Test the latest known patch in each version # Test the oldest and the newest 32 bit release. 64-bit otherwise. @@ -26,12 +26,6 @@ environment: PYTHON_ARCH: "64" MINICONDA: C:\Miniconda CONDA_INSTALL: "numpy" - - - PYTHON: "C:\\Python33-x64" - PYTHON_VERSION: "3.3.5" - PYTHON_ARCH: "64" - MINICONDA: C:\Miniconda3-x64 - CONDA_INSTALL: "numpy" - PYTHON: "C:\\Python34-x64" PYTHON_VERSION: "3.4.5" diff --git a/setup.py b/setup.py index a00c5bbba..b8770b6e2 100644 --- a/setup.py +++ b/setup.py @@ -117,7 +117,6 @@ def run_tests(self): 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6',