Repository: Zulko/moviepy
Pull request #1076 (2020-02-09 05:06:20)

Implement basic data model
/This is a very raw proof of concept that try to explore a more pythonic API for basic clip manipulation using few of the "dunder" methods (aka python's data model). 

```
In [2]: v = VideoFileClip('/home/tin/Videos/Screencast 2019-12-10 09:56:22.mp4')                                                                                                              

In [3]: v[-2.3:]         #   last 2.3 seconds                                                                                                                                     
Out[3]: <moviepy.video.io.VideoFileClip.VideoFileClip at 0x7fdce65ac1d0>

In [4]: _.duration                                                                                                                                                                            
Out[4]: 2.3000000000000007

In [5]: v[:1, -1:]      # first second + last second.  same than v[:1] +  v[-1:]                                                                                                                                                                           
Out[5]: <moviepy.video.VideoClip.VideoClip at 0x7fdce65a2c90>

In [6]: _.duration                                                                                                                                                                            
Out[6]: 2.0
```
Extending slicing syntax is supported with `speed` is supported. See docstring of `__getattr__`. 
 
The idea is to simplify the most common tasks, not to be cryptic but practical. 

other methods implemented are

- `__add__` to concatenate (ie `clip1 + clip2 == concatenate_videoclips([clip1, clip2])`)
- `__mul__`  for repetition (ie, `clip * 3`) 
- `__and__`  for masking 
- `__matmul__`  for rotation ( `clip @ 270`) 
- `__or__`  for simple horizontal union (clip1 | clip2)
- `__truediv__`  for simple vertical union (clip1 / clip2)


coveralls: 
[![Coverage Status](https://coveralls.io/builds/28612250/badge)](https://coveralls.io/builds/28612250)

Coverage decreased (-0.6%) to 64.395% when pulling **4f2fceea2dd2332f58009ae3762591eeea340fd7 on mgaitan:data_model** into **f670d1ab0c1621b39910d506772ee36de064eb5e on Zulko:master**.

mgaitan: Depends on #1077 
tburrows13: I like this a lot, and I think it should definitely be merged at some point, possibly into an upcoming v2.0 (I've got some other plans as well).

I'd be in favour of `__neg__` to reverse a clip, `__mul__` to repeat it. `__and__` for masks seems fun as well, and would just call `myclip.set_mask(maskclip)`.
mgaitan: excellent @tburrows13 . I'll continue working on this on my spare time. Meanwhile, in order to simplify its review, would be great to have #1077 merged before. I'll be glad if you could check that


tburrows13: I'd like to merge this one, but I think you need to merge master into it first to resolve the conflicts.

@Zulko @mgaitan do you think that we should encourage the use of this syntax (in particular `clip1 + clip2` for concatenation) in things like 'getting started' examples, or just keep it as an optional extra?

Also I think that the concatenation here should possibly use method='compose' instead of 'chain' as it is more intuitive?
Zulko: I'm not against this commit because I like experimentations and that looks very fun, but from a programming and maintenance standpoint overloading operators for objects as complex as videoclips could cause confusion. It will make code harder to read for people who don't know what these operators mean, and I expect that many people will be asking for support on these.  For instance should  "+" mean concatenation or overlay? Why use ``-clip`` for time mirroring instead of ``clip[::-1]``? What happens when you just add two clips with different sizes? I like the subclipping with ``__getitems__``though, I think it is long due.
mgaitan: I'll update this branch as soon as possible so we could discuss its scope again. 

Of course, I understand it's a bit sensitive, and need to be a well document and limited subset of features: the ones that are used more frequently and feels "similar" to behaviours already present in python structures. Concatenation, repetition, slicing, length, masking, etc are common operations supported in builtin types and popular libraries like numpy, with a common syntax. 

For instance, the slicing support I've implemented is `clip[start:stop:speed]`, so 
`clip[::-1]` is actually a time_mirror, like reverse any sequence. 

My goal is be pragmatic and pythonic, and I would like as much feedback as possible. 
mgaitan: The PR is ready to review. I've documented the most important method `__getattr__` but not sure where to put that text (or a similar one) in the documentation. 


mgaitan: Sorry for the forced push, but the rebasing was a nightmare 
tburrows13: I'm ok with these features, however I'm more than happy to defer to @Zulko if he would rather veto some of them. Once we've got confirmation on which bits to keep, I'll go through and write some documentation and add it to the changelog.

I don't think that we should use the add/mul/and features in the source code (outside of testing of course). They seem like useful tools for quick editing, but (the add in particular) seems like a bit of a black box that the user has no control over which doesn't seem suitable for eg the fx functions.
Ethkuil: >  It will make code harder to read for people who don't know what these operators mean, and I expect that many people will be asking for support on these. For instance should "+" mean concatenation or overlay? 

We can naturally treat a "clip" like a "str", and almost nobody will be confused by why `'1'+'1'` equals to `'11'` but not `'2'`.
Ethkuil: Currently it's 2023 and so excuse me, any progress? Or anything needs help?
mgaitan: @Ethkuil hi, I still consider this approach would be useful for several use cases but I felt that @Zulko wasn't totally convinced on my approach and then nobody clarified which "magic methods" would be acceptable or not. 

I'll be happy to resume and finish this work if there is consensus on what would be considered too much 

cc / @Ethkuil @tburrows13 
mgaitan: @Ethkuil @tburrows13 @keikoro this is ready to review, 

Patch:
diff --git a/docs/getting_started/audioclips.rst b/docs/getting_started/audioclips.rst
index 4727149..a7d9796 100644
--- a/docs/getting_started/audioclips.rst
+++ b/docs/getting_started/audioclips.rst
@@ -5,7 +5,7 @@ Audio in MoviePy
 
 This section shows how to use MoviePy to create and edit audio clips.
 
-Note that when you cut, mix or concatenate video clips in MoviePy the audio is automatically handled and you need to worry about it. This section is of interest if you just want to edit audiofiles or you want custom audio clips for your videos.
+Note that when you cut, mix or concatenate video clips in MoviePy the audio is automatically handled and you don't need to worry about it. This section is of interest if you just want to edit audiofiles or you want custom audio clips for your videos.
 
 What audioclips are made of
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/getting_started/compositing.rst b/docs/getting_started/compositing.rst
index 96eff73..1afb256 100644
--- a/docs/getting_started/compositing.rst
+++ b/docs/getting_started/compositing.rst
@@ -18,7 +18,7 @@ Before starting, note that video clips generally carry an audio track and a mask
 Stacking and concatenating clips
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Two simple ways of putting clips together is to concatenate them (to play them one after the other in a single long clip) or to stack them (to them side by side in a single larger clip).
+Two simple ways of putting clips together is to concatenate them (to play them one after the other in a single long clip) or to stack them (to put them side by side in a single larger clip).
 
 Concatenation is done with the function ``concatenate_videoclips``: ::
 
diff --git a/docs/getting_started/effects.rst b/docs/getting_started/effects.rst
index 5436584..7688115 100644
--- a/docs/getting_started/effects.rst
+++ b/docs/getting_started/effects.rst
@@ -7,7 +7,7 @@ There are several categories of clip modifications in MoviePy:
 
 - The very common methods to change the attributes of a clip: ``clip.with_duration``, ``clip.with_audio``, ``clip.with_mask``, ``clip.with_start`` etc.
 - The already-implemented effects. Core effects like ``clip.subclip(t1, t2)`` (keep only the cut between t1 and t2), which are very important, are implemented as class methods. More advanced and less common effects like ``loop`` (makes the clip play in a loop) or ``time_mirror`` (makes the clip play backwards) are placed in the special modules ``moviepy.video.fx`` and ``moviepy.audio.fx`` and are applied with the ``clip.fx`` method, for instance ``clip.fx(time_mirror)`` (makes the clip play backwards), ``clip.fx(black_white)`` (turns the clip black and white), etc.
-- The effects that you can create yourself. using 
+- The effects that you can create yourself using  ``clip.fl``.
 
 All these effects have in common that they are **not inplace**: they do NOT modify the original clip, instead they create a new clip that is a version of the former with the changes applied. For instance: ::
 
diff --git a/docs/getting_started/quick_presentation.rst b/docs/getting_started/quick_presentation.rst
index 3009ffd..71c6f3b 100644
--- a/docs/getting_started/quick_presentation.rst
+++ b/docs/getting_started/quick_presentation.rst
@@ -42,13 +42,14 @@ In a typical MoviePy script, you load video or audio files, modify them, put the
     from moviepy import *
 
     # Load myHolidays.mp4 and select the subclip 00:00:50 - 00:00:60
-    clip = VideoFileClip("myHolidays.mp4").subclip(50,60)
+    clip = VideoFileClip("myHolidays.mp4")
+    clip = clip.subclip(50, 60)    # or just clip[50:60]
 
     # Reduce the audio volume (volume x 0.8)
     clip = clip.multiply_volume(0.8)
 
     # Generate a text clip. You can customize the font, color, etc.
-    txt_clip = TextClip("My Holidays 2013",fontsize=70,color='white')
+    txt_clip = TextClip("My Holidays 2013", fontsize=70, color="white")
 
     # Say that you want it to appear 10s at the center of the screen
     txt_clip = txt_clip.with_position('center').with_duration(10)
diff --git a/moviepy/Clip.py b/moviepy/Clip.py
index 73ebdfa..71c13f6 100644
--- a/moviepy/Clip.py
+++ b/moviepy/Clip.py
@@ -1,8 +1,10 @@
 """Implements the central object of MoviePy, the Clip, and all the methods that
 are common to the two subclasses of Clip, VideoClip and AudioClip.
 """
-
 import copy as _copy
+from functools import reduce
+from numbers import Real
+from operator import add
 
 import numpy as np
 import proglog
@@ -388,6 +390,9 @@ class Clip:
         The ``mask`` and ``audio`` of the resulting subclip will be subclips of
         ``mask`` and ``audio`` the original clip, if they exist.
 
+        It's equivalent to slice the clip as a sequence, like
+        ``clip[t_start:t_end]``.
+
         Parameters
         ----------
 
@@ -562,10 +567,85 @@ class Clip:
 
         return True
 
-    # Support the Context Manager protocol, to ensure that resources are cleaned up.
-
     def __enter__(self):
+        """
+        Support the Context Manager protocol,
+        to ensure that resources are cleaned up.
+        """
         return self
 
     def __exit__(self, exc_type, exc_value, traceback):
         self.close()
+
+    def __getitem__(self, key):
+        """
+        Support extended slice and index operations over
+        a clip object.
+
+        Simple slicing is implemented via `subclip`.
+        So, ``clip[t_start:t_end]`` is equivalent to
+        ``clip.subclip(t_start, t_end)``. If ``t_start`` is not
+        given, default to ``0``, if ``t_end`` is not given,
+        default to ``self.duration``.
+
+        The slice object optionally support a third argument as
+        a ``speed`` coefficient (that could be negative),
+        ``clip[t_start:t_end:speed]``.
+
+        For example ``clip[::-1]`` returns a reversed (a time_mirror fx)
+        the video and ``clip[:5:2]`` returns the segment from 0 to 5s
+        accelerated to 2x (ie. resulted duration would be 2.5s)
+
+        In addition, a tuple of slices is supported, resulting in the concatenation
+        of each segment. For example ``clip[(:1, 2:)]`` return a clip
+        with the segment from 1 to 2s removed.
+
+        If ``key`` is not a slice or tuple, we assume it's a time
+        value (expressed in any format supported by `cvsec`)
+        and return the frame at that time, passing the key
+        to ``get_frame``.
+        """
+        apply_to = ["mask", "audio"]
+        if isinstance(key, slice):
+            # support for [start:end:speed] slicing. If speed is negative
+            # a time mirror is applied.
+            clip = self.subclip(key.start or 0, key.stop or self.duration)
+
+            if key.step:
+                # change speed of the subclip
+                factor = abs(key.step)
+                if factor != 1:
+                    # change speed
+                    clip = clip.time_transform(
+                        lambda t: factor * t, apply_to=apply_to, keep_duration=True
+                    )
+                    clip = clip.with_duration(1.0 * clip.duration / factor)
+                if key.step < 0:
+                    # time mirror
+                    clip = clip.time_transform(
+                        lambda t: clip.duration - t - 1,
+                        keep_duration=True,
+                        apply_to=apply_to,
+                    )
+            return clip
+        elif isinstance(key, tuple):
+            # get a concatenation of subclips
+            return reduce(add, (self[k] for k in key))
+        else:
+            return self.get_frame(key)
+
+    def __del__(self):
+        self.close()
+
+    def __add__(self, other):
+        # concatenate. implemented in specialized classes
+        return NotImplemented
+
+    def __mul__(self, n):
+        # loop n times where N is a real
+        if not isinstance(n, Real):
+            return NotImplemented
+
+        from moviepy.video.fx.loop import loop
+
+        return loop(self, n)
diff --git a/moviepy/audio/AudioClip.py b/moviepy/audio/AudioClip.py
index e36cebd..5811088 100644
--- a/moviepy/audio/AudioClip.py
+++ b/moviepy/audio/AudioClip.py
@@ -259,6 +259,11 @@ class AudioClip(Clip):
             logger=logger,
         )
 
+    def __add__(self, other):
+        if isinstance(other, AudioClip):
+            return concatenate_audioclips([self, other])
+        return super(AudioClip, self).__add__(other)
+
 
 class AudioArrayClip(AudioClip):
     """
diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py
index b6a3631..caa2682 100644
--- a/moviepy/video/VideoClip.py
+++ b/moviepy/video/VideoClip.py
@@ -8,6 +8,7 @@ import copy as _copy
 import os
 import subprocess as sp
 import tempfile
+from numbers import Real
 
 import numpy as np
 import proglog
@@ -909,6 +910,46 @@ class VideoClip(Clip):
         """
         self.audio = self.audio.fx(fun, *args, **kwargs)
 
+    def __add__(self, other):
+        if isinstance(other, VideoClip):
+            from moviepy.video.compositing.concatenate import concatenate_videoclips
+
+            method = "chain" if self.size == other.size else "compose"
+            return concatenate_videoclips([self, other], method=method)
+        return super(VideoClip, self).__add__(other)
+
+    def __or__(self, other):
+        """
+        Implement the or (self | other) to produce a video with self and other
+        placed side by side horizontally.
+        """
+        if isinstance(other, VideoClip):
+            from moviepy.video.compositing.CompositeVideoClip import clips_array
+
+            return clips_array([[self, other]])
+        return super(VideoClip, self).__or__(other)
+
+    def __truediv__(self, other):
+        """
+        Implement division (self / other) to produce a video with self
+        placed on top of other.
+        """
+        if isinstance(other, VideoClip):
+            from moviepy.video.compositing.CompositeVideoClip import clips_array
+
+            return clips_array([[self], [other]])
+        return super(VideoClip, self).__or__(other)
+
+    def __matmul__(self, n):
+        if not isinstance(n, Real):
+            return NotImplemented
+        from moviepy.video.fx.rotate import rotate
+
+        return rotate(self, n)
+
+    def __and__(self, mask):
+        return self.with_mask(mask)
+
 
 class DataVideoClip(VideoClip):
     """
@@ -918,7 +959,7 @@ class DataVideoClip(VideoClip):
     Parameters
     ----------
     data
-      A liste of datasets, each dataset being used for one frame of the clip
+      A list of datasets, each dataset being used for one frame of the clip
 
     data_to_frame
       A function d -> video frame, where d is one element of the list `data`
diff --git a/moviepy/video/fx/freeze.py b/moviepy/video/fx/freeze.py
index 7a052e4..db39fcd 100644
--- a/moviepy/video/fx/freeze.py
+++ b/moviepy/video/fx/freeze.py
@@ -23,7 +23,7 @@ def freeze(clip, t=0, freeze_duration=None, total_duration=None, padding_end=0):
             )
         freeze_duration = total_duration - clip.duration
 
-    before = [clip.subclip(0, t)] if (t != 0) else []
+    before = [clip[:t]] if (t != 0) else []
     freeze = [clip.to_ImageClip(t).with_duration(freeze_duration)]
-    after = [clip.subclip(t)] if (t != clip.duration) else []
+    after = [clip[t:]] if (t != clip.duration) else []
     return concatenate_videoclips(before + freeze + after)
diff --git a/moviepy/video/fx/time_mirror.py b/moviepy/video/fx/time_mirror.py
index f5cc38c..93e1d52 100644
--- a/moviepy/video/fx/time_mirror.py
+++ b/moviepy/video/fx/time_mirror.py
@@ -10,4 +10,4 @@ def time_mirror(clip):
     The clip must have its ``duration`` attribute set.
     The same effect is applied to the clip's audio and mask if any.
     """
-    return clip.time_transform(lambda t: clip.duration - t - 1, keep_duration=True)
+    return clip[::-1]
diff --git a/moviepy/video/fx/time_symmetrize.py b/moviepy/video/fx/time_symmetrize.py
index 688ecc4..65cfdf5 100644
--- a/moviepy/video/fx/time_symmetrize.py
+++ b/moviepy/video/fx/time_symmetrize.py
@@ -1,10 +1,7 @@
-from moviepy.decorators import apply_to_mask, requires_duration
-from moviepy.video.compositing.concatenate import concatenate_videoclips
-from moviepy.video.fx.time_mirror import time_mirror
+from moviepy.decorators import requires_duration
 
 
 @requires_duration
-@apply_to_mask
 def time_symmetrize(clip):
     """
     Returns a clip that plays the current clip once forwards and
@@ -13,4 +10,4 @@ def time_symmetrize(clip):
     This effect is automatically applied to the clip's mask and audio
     if they exist.
     """
-    return concatenate_videoclips([clip, clip.fx(time_mirror)])
+    return clip + clip[::-1]
diff --git a/setup.py b/setup.py
index ceaa966..751ee83 100644
--- a/setup.py
+++ b/setup.py
@@ -1,9 +1,8 @@
 #!/usr/bin/env python
 """MoviePy setup script."""
 
-import os
 import sys
-from codecs import open
+from pathlib import Path
 
 
 try:
@@ -66,9 +65,7 @@ if "build_docs" in sys.argv:
 
     cmdclass["build_docs"] = BuildDoc
 
-__version__ = None
-with open(os.path.join("moviepy", "version.py"), "r", "utf-8") as f:
-    __version__ = f.read().split(" ")[2].strip("\n").strip('"')
+__version__ = Path("moviepy/version.py").read_text().strip().split('"')[1][:-1]
 
 
 # Define the requirements for specific execution needs.
@@ -122,8 +119,7 @@ extra_reqs = {
 }
 
 # Load the README.
-with open("README.rst", "r", "utf-8") as file:
-    readme = file.read()
+readme = Path("README.rst").read_text()
 
 setup(
     name="moviepy",
@@ -143,10 +139,11 @@ setup(
         "License :: OSI Approved :: MIT License",
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
         "Topic :: Multimedia",
         "Topic :: Multimedia :: Sound/Audio",
         "Topic :: Multimedia :: Sound/Audio :: Analysis",


Test patch:
diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml
index 2f7c0a3..30f9d0f 100644
--- a/.github/workflows/test_suite.yml
+++ b/.github/workflows/test_suite.yml
@@ -15,24 +15,12 @@ jobs:
     runs-on: macos-latest
     strategy:
       matrix:
-        python-version: [3.7]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
     steps:
     - uses: actions/checkout@v3
     - uses: s-weigand/setup-conda@v1
       with:
         activate-conda: true
-    # TODO: fix adding the different versions  
-    # Pick the installation of Python Framework build from conda, because they do not support versioning
-    # https://anaconda.org/anaconda/python.app/files
-    #- name: Install conda python version 3.6
-    #  if: matrix.python-version == '3.6'
-    #  run: conda install http://repo.continuum.io/pkgs/main/osx-64/python.app-2-py36_10.tar.bz2
-    #- name: Install conda python version 3.7
-    #  if: matrix.python-version == '3.7'
-    #  run: conda install http://repo.continuum.io/pkgs/main/osx-64/python.app-2-py37_10.tar.bz2
-    #- name: Install conda python version 3.8
-    #  if: matrix.python-version == '3.8'
-    #  run: conda install http://repo.continuum.io/pkgs/main/osx-64/python.app-2-py38_10.tar.bz2
     - name: Install pythonw
       run: conda install python.app
     - name: Python Version Info
@@ -65,7 +53,7 @@ jobs:
     runs-on: windows-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
       fail-fast: false
     steps:
       - uses: actions/checkout@v3
@@ -130,7 +118,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
       fail-fast: false
     steps:
       - uses: actions/checkout@v3
diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py
index 4e2d1ab..151049b 100644
--- a/tests/test_VideoClip.py
+++ b/tests/test_VideoClip.py
@@ -441,5 +441,72 @@ def test_afterimage(util):
     final_clip.write_videofile(filename, fps=30, logger=None)
 
 
+def test_add():
+    clip = VideoFileClip("media/fire2.mp4")
+    new_clip = clip[0:1] + clip[2:3.2]
+    assert new_clip.duration == 2.2
+    assert np.array_equal(new_clip[1.1], clip[2.1])
+
+
+def test_slice_tuples():
+    clip = VideoFileClip("media/fire2.mp4")
+    new_clip = clip[0:1, 2:3.2]
+    assert new_clip.duration == 2.2
+    assert np.array_equal(new_clip[1.1], clip[2.1])
+
+
+def test_slice_mirror():
+    clip = VideoFileClip("media/fire2.mp4")
+    new_clip = clip[::-1]
+    assert new_clip.duration == clip.duration
+    assert np.array_equal(new_clip[0], clip[clip.duration])
+
+
+def test_slice_speed():
+    clip = BitmapClip([["A"], ["B"], ["C"], ["D"]], fps=1)
+    clip1 = clip[::0.5]  # 1/2x speed
+    target1 = BitmapClip(
+        [["A"], ["A"], ["B"], ["B"], ["C"], ["C"], ["D"], ["D"]], fps=1
+    )
+    assert clip1 == target1
+
+
+def test_mul():
+    clip = VideoFileClip("media/fire2.mp4")
+    new_clip = clip[0:1] * 2.5
+    assert new_clip.duration == 2.5
+    assert np.array_equal(new_clip[1.1], clip[0.1])
+
+
+def test_and():
+    clip = VideoFileClip("media/fire2.mp4")
+    maskclip = ImageClip("media/afterimage.png", is_mask=True, transparent=True)
+    clip_with_mask = clip & maskclip
+    assert clip_with_mask.mask is maskclip
+
+
+def test_or(util):
+    clip1 = BitmapClip([["R"]], fps=1)
+    clip2 = BitmapClip([["G"]], fps=1)
+    target = BitmapClip([["RG"]], fps=1)
+    result = clip1 | clip2
+    assert result == target
+
+
+def test_truediv(util):
+    clip1 = BitmapClip([["R"]], fps=1)
+    clip2 = BitmapClip([["G"]], fps=1)
+    target = BitmapClip([["R", "G"]], fps=1)
+    result = clip1 / clip2
+    assert result == target
+
+
+def test_matmul(util):
+    clip1 = BitmapClip([["RG"]], fps=1)
+    target = BitmapClip([["R", "G"]], fps=1)
+    result = clip1 @ 270
+    assert result == target
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/tests/test_compositing.py b/tests/test_compositing.py
index 5824034..365b1ad 100644
--- a/tests/test_compositing.py
+++ b/tests/test_compositing.py
@@ -10,7 +10,6 @@ from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip, cli
 from moviepy.video.compositing.concatenate import concatenate_videoclips
 from moviepy.video.compositing.transitions import slide_in, slide_out
 from moviepy.video.fx.resize import resize
-from moviepy.video.io.VideoFileClip import VideoFileClip
 from moviepy.video.VideoClip import BitmapClip, ColorClip
 
 
@@ -93,22 +92,44 @@ def test_concatenate_floating_point(util):
     concat.write_videofile(os.path.join(util.TMP_DIR, "concat.mp4"), preset="ultrafast")
 
 
+# def test_blit_with_opacity():
+#     # bitmap.mp4 has one second R, one second G, one second B
+#     clip1 = VideoFileClip("media/bitmap.mp4")
+#     # overlay same clip, shifted by 1 second, at half opacity
+#     clip2 = (
+#         VideoFileClip("media/bitmap.mp4")
+#         .subclip(1, 2)
+#         .with_start(0)
+#         .with_end(2)
+#         .with_opacity(0.5)
+#     )
+#     composite = CompositeVideoClip([clip1, clip2])
+#     bt = ClipPixelTest(composite)
+
+#     bt.expect_color_at(0.5, (0x7F, 0x7F, 0x00))
+#     bt.expect_color_at(1.5, (0x00, 0x7F, 0x7F))
+#     bt.expect_color_at(2.5, (0x00, 0x00, 0xFF))
+
+
 def test_blit_with_opacity():
-    # bitmap.mp4 has one second R, one second G, one second B
-    clip1 = VideoFileClip("media/bitmap.mp4")
-    # overlay same clip, shifted by 1 second, at half opacity
-    clip2 = (
-        VideoFileClip("media/bitmap.mp4")
-        .subclip(1, 2)
-        .with_start(0)
-        .with_end(2)
-        .with_opacity(0.5)
+    # has one second R, one second G, one second B
+    size = (2, 2)
+    clip1 = (
+        ColorClip(size, color=(255, 0, 0), duration=1)
+        + ColorClip(size, color=(0, 255, 0), duration=1)
+        + ColorClip(size, color=(0, 0, 255), duration=1)
     )
+
+    # overlay green at half opacity during first 2 sec
+    clip2 = ColorClip(size, color=(0, 255, 0), duration=2).with_opacity(0.5)
     composite = CompositeVideoClip([clip1, clip2])
     bt = ClipPixelTest(composite)
 
+    # red + 50% green
     bt.expect_color_at(0.5, (0x7F, 0x7F, 0x00))
-    bt.expect_color_at(1.5, (0x00, 0x7F, 0x7F))
+    # green + 50% green
+    bt.expect_color_at(1.5, (0x00, 0xFF, 0x00))
+    # blue is after 2s, so keep untouched
     bt.expect_color_at(2.5, (0x00, 0x00, 0xFF))
 
 
diff --git a/tests/test_issues.py b/tests/test_issues.py
index 449dc41..5a3dbaa 100644
--- a/tests/test_issues.py
+++ b/tests/test_issues.py
@@ -233,7 +233,7 @@ def test_issue_334(util):
     avatar.audio = None
     maskclip = ImageClip("media/afterimage.png", is_mask=True, transparent=True)
     avatar.with_mask(maskclip)  # must set maskclip here..
-    concatenated = concatenate_videoclips([avatar] * 3)
+    concatenated = avatar * 3
 
     tt = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0, 3)
     # TODO: Setting mask here does not work: