Repository: Zulko/moviepy

Issue:
CompositeVideoClip - Different rules for clip appereance
I have stumbled upon this problem while coding a feature involving clips being sorted in a different manner than the one intended by the CompositeVideoClip.

What i mean by that is that usually while working with any video (outside of Python). You imagine the videos in layers where You would like to have different layers.

At this point every Video is in its own layer based on the index on the list [clip0, transition_clip, clip1], would make it that clip1 is cutting the transition_clip.

I haven't tested any workarounds. but I suspect that this might be solved by using Two CompositeVideoClips with two lists of clips with different .start_time() parameters.

That would mean using:
```
transition_clips = CompositeVideoClip([transition_clip, transition_clip, transition_clip])
legit_clips = CompositeVideoClip([clip0, clip1, clip2])

final_composition = CompositeVideoClip([legit_clips, transition_clips])
```
Which might not be computationally efficient at all as there are 3x nested clips.

Please note that the code shown above is not fully operational. I have ommited .set_start()

Which leads me to thing that this might be solved simply by sorting the lists somehow.


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

Coverage increased (+0.03%) to 66.174% when pulling **09a365021014b84c50597fa695119fbe1a11b706 on tburrows13:add-set_layer** into **90e42c108dcbefaf3e538d525800db9345fee714 on Zulko:master**.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4010da4..65103a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Optional `encoding` parameter in `SubtitlesClip` [#1043]
 - Added new `ffmpeg_stabilize_video()` function in `ffmpeg_tools`
 - Optional `temp_audiofile_path` parameter in `VideoClip.write_videofile()` to specify where the temporary audiofile should be created [#1144]
+- `VideoClip.set_layer()` to specify the layer of the clip for use when creating a `CompositeVideoClip` [#1176]
 - `ffmpeg_parse_infos` additionally returns `"video_bitrate"` and `"audio_bitrate"` values [#930]
 - Access to the source video's bitrate in a `VideoFileClip` or `AudioFileClip` through `videoclip.reader.bitrate` and `audioclip.reader.bitrate` [#930]
 
diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py
index f182eb2..428a8b1 100644
--- a/moviepy/video/VideoClip.py
+++ b/moviepy/video/VideoClip.py
@@ -80,6 +80,11 @@ class VideoClip(Clip):
     relative_pos
       See variable ``pos``.
 
+    layer
+      Indicates which clip is rendered on top when two clips overlap in
+      a CompositeVideoClip. The highest number is rendered on top.
+      Default is 0.
+
     """
 
     def __init__(
@@ -90,6 +95,7 @@ class VideoClip(Clip):
         self.audio = None
         self.pos = lambda t: (0, 0)
         self.relative_pos = False
+        self.layer = 0
         if make_frame:
             self.make_frame = make_frame
             self.size = self.get_frame(0).shape[:2][::-1]
@@ -778,6 +784,15 @@ class VideoClip(Clip):
         else:
             self.pos = lambda t: pos
 
+    @apply_to_mask
+    @outplace
+    def set_layer(self, layer):
+        """Set the clip's layer in compositions. Clips with a greater ``layer``
+        attribute will be displayed on top of others.
+
+        Note: Only has effect when the clip is used in a CompositeVideoClip."""
+        self.layer = layer
+
     # --------------------------------------------------------------
     # CONVERSIONS TO OTHER TYPES
 
@@ -1325,7 +1340,7 @@ class BitmapClip(VideoClip):
             "B": (0, 0, 255),
             "O": (0, 0, 0),  # "O" represents black
             "W": (255, 255, 255),
-            "A": (89, 225, 62),  # "A", "C", "D" and "E" represent arbitrary colors
+            "A": (89, 225, 62),  # "A", "C", "D", "E", "F" represent arbitrary colors
             "C": (113, 157, 108),
             "D": (215, 182, 143),
             "E": (57, 26, 252),
@@ -1348,6 +1363,7 @@ class BitmapClip(VideoClip):
                 "C": (113, 157, 108),
                 "D": (215, 182, 143),
                 "E": (57, 26, 252),
+                "F": (225, 135, 33),
             }
 
         frame_list = []
diff --git a/moviepy/video/compositing/CompositeVideoClip.py b/moviepy/video/compositing/CompositeVideoClip.py
index 6349bef..fc97498 100644
--- a/moviepy/video/compositing/CompositeVideoClip.py
+++ b/moviepy/video/compositing/CompositeVideoClip.py
@@ -20,8 +20,14 @@ class CompositeVideoClip(VideoClip):
       The size (height x width) of the final clip.
 
     clips
-      A list of videoclips. Each clip of the list will
-      be displayed below the clips appearing after it in the list.
+      A list of videoclips.
+
+      Clips with a higher ``layer`` attribute will be dislayed
+      on top of other clips in a lower layer.
+      If two or more clips share the same ``layer``,
+      then the one appearing latest in ``clips`` will be displayed
+      on top (i.e. it has the higher layer).
+
       For each clip:
        
       - The attribute ``pos`` determines where the clip is placed.
@@ -78,6 +84,9 @@ class CompositeVideoClip(VideoClip):
             self.bg = ColorClip(size, color=self.bg_color, ismask=ismask)
             self.created_bg = True
 
+        # order self.clips by layer
+        self.clips = sorted(self.clips, key=lambda clip: clip.layer)
+
         # compute duration
         ends = [c.end for c in self.clips]
         if None not in ends:
@@ -97,6 +106,7 @@ class CompositeVideoClip(VideoClip):
                 .set_position(c.pos)
                 .set_end(c.end)
                 .set_start(c.start, change_end=False)
+                .set_layer(c.layer)
                 for c in self.clips
             ]
 


Test patch:
diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py
index f8733a1..9ef5051 100644
--- a/tests/test_VideoClip.py
+++ b/tests/test_VideoClip.py
@@ -6,6 +6,7 @@ from numpy import pi, sin, array
 from moviepy.audio.AudioClip import AudioClip
 from moviepy.audio.io.AudioFileClip import AudioFileClip
 from moviepy.utils import close_all_clips
+from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
 from moviepy.video.fx.speedx import speedx
 from moviepy.video.io.VideoFileClip import VideoFileClip
 from moviepy.video.VideoClip import ColorClip, BitmapClip
@@ -191,6 +192,35 @@ def test_setopacity():
     close_all_clips(locals())
 
 
+def test_set_layer():
+    bottom_clip = BitmapClip([["ABC"], ["BCA"], ["CAB"]]).set_fps(1).set_layer(1)
+    top_clip = BitmapClip([["DEF"], ["EFD"]]).set_fps(1).set_layer(2)
+
+    composite_clip = CompositeVideoClip([bottom_clip, top_clip])
+    reversed_composite_clip = CompositeVideoClip([top_clip, bottom_clip])
+
+    # Make sure that the order of clips makes no difference to the composite clip
+    assert composite_clip.subclip(0, 2) == reversed_composite_clip.subclip(0, 2)
+
+    # Make sure that only the 'top' clip is kept
+    assert top_clip.subclip(0, 2) == composite_clip.subclip(0, 2)
+
+    # Make sure that it works even when there is only one clip playing at that time
+    target_clip = BitmapClip([["DEF"], ["EFD"], ["CAB"]]).set_fps(1)
+    assert composite_clip == target_clip
+
+
+def test_compositing_with_same_layers():
+    bottom_clip = BitmapClip([["ABC"], ["BCA"]]).set_fps(1)
+    top_clip = BitmapClip([["DEF"], ["EFD"]]).set_fps(1)
+
+    composite_clip = CompositeVideoClip([bottom_clip, top_clip])
+    reversed_composite_clip = CompositeVideoClip([top_clip, bottom_clip])
+
+    assert composite_clip == top_clip
+    assert reversed_composite_clip == bottom_clip
+
+
 def test_toimageclip():
     clip = VideoFileClip("media/big_buck_bunny_432_433.webm").subclip(0.2, 0.6)
     clip = clip.to_ImageClip(t=0.1, duration=0.4)