Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement basic data model #1076

Merged
merged 13 commits into from
May 26, 2023
7 changes: 4 additions & 3 deletions docs/getting_started/quick_presentation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ In a typical MoviePy script, you load video or audio files, modify them, put the
from moviepy.editor 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.volumex(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.set_pos('center').set_duration(10)
txt_clip = txt_clip.set_pos("center").set_duration(10)

# Overlay the text clip on the first video clip
video = CompositeVideoClip([clip, txt_clip])
Expand Down
122 changes: 111 additions & 11 deletions moviepy/Clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"""

from copy import copy
from functools import reduce
from operator import add
from numbers import Real

import numpy as np
import proglog
Expand All @@ -20,7 +23,6 @@


class Clip:

"""

Base class of all clips (VideoClips and AudioClips).
Expand Down Expand Up @@ -69,8 +71,10 @@ def copy(self):
there is an outplace transformation of the clip (clip.resize,
clip.subclip, etc.)
"""
return copy(self)

newclip = copy(self)
def __copy__(self):
newclip = copy(super(Clip, self))
if hasattr(self, "audio"):
newclip.audio = copy(self.audio)
if hasattr(self, "mask"):
Expand Down Expand Up @@ -179,10 +183,10 @@ def fl_time(self, t_func, apply_to=None, keep_duration=False):
--------

>>> # 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 t: 2*t, apply_to=['mask', 'audio'])
tburrows13 marked this conversation as resolved.
Show resolved Hide resolved
>>>
>>> # plays the clip starting at t=3, and backwards:
>>> newclip = clip.fl_time(lambda: 3-t)
>>> newclip = clip.fl_time(lambda t: 3-t)

"""
if apply_to is None:
Expand Down Expand Up @@ -213,7 +217,6 @@ def fx(self, func, *args, **kwargs):
>>> resize( volumex( mirrorx( clip ), 0.5), 0.3)

"""

return func(self, *args, **kwargs)

@apply_to_mask
Expand All @@ -226,7 +229,6 @@ 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``.
Expand Down Expand Up @@ -317,13 +319,13 @@ def set_memoize(self, 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'.
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.
the clip, else returns a vector [b_1, b_2, b_3...] where b_i
is true if tti is in the clip.
"""

if isinstance(t, np.ndarray):
Expand Down Expand Up @@ -355,8 +357,13 @@ def subclip(self, t_start=0, t_end=None):
between times ``t_start`` and ``t_end``, which can be expressed
in seconds (15.35), in (min, sec), in (hour, min, sec), or as a
string: '01:03:05.35'.

It's equivalent to slice the clip as a sequence, like
``clip[t_start:t_end]``

If ``t_end`` is not provided, it is assumed to be the duration
of the clip (potentially infinite).

If ``t_end`` is a negative value, it is reset to
``clip.duration + t_end. ``. For instance: ::

Expand Down Expand Up @@ -490,10 +497,103 @@ def close(self):
# * Therefore, should NOT be called by __del__().
pass

# Support the Context Manager protocol, to ensure that resources are cleaned up.
# helper private methods
def __unsupported(self, other, operator):
self_type = type(self).__name__
other_type = type(other).__name__
message = "unsupported operand type(s) for {}: '{}' and '{}'"
raise TypeError(message.format(operator, self_type, other_type))
mgaitan marked this conversation as resolved.
Show resolved Hide resolved

@staticmethod
def __apply_to(clip):
apply_to = []
if getattr(clip, "mask", None):
apply_to.append("mask")
if getattr(clip, "audio", None):
apply_to.append("audio")
return apply_to

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 :meth:`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 :func:`cvsec`)
and return the frame at that time, passing the key
to :meth:`get_frame`.
"""
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
apply_to = self.__apply_to(clip)
factor = abs(key.step)
if factor != 1:
# change speed
clip = clip.fl_time(
lambda t: factor * t, apply_to=apply_to, keep_duration=True
)
clip = clip.set_duration(1.0 * clip.duration / factor)
if key.step < 0:
# time mirror
clip = clip.fl_time(
lambda t: clip.duration - t,
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
self.__unsupported(other, "+")

def __mul__(self, n):
# loop n times
if not isinstance(n, Real):
self.__unsupported(n, "*")

apply_to = self.__apply_to(self)
clip = self.fl_time(
lambda t: t % self.duration, apply_to=apply_to, keep_duration=True
)
return clip.set_duration(clip.duration * n)
mgaitan marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions moviepy/audio/AudioClip.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ def write_audiofile(
logger=logger,
)

def __add__(self, other):
if isinstance(other, AudioClip):
return concatenate_audioclips([self, other])
return super(AudioClip, self).__add__(other)


# The to_audiofile method is replaced by the more explicit write_audiofile.
AudioClip.to_audiofile = deprecated_version_of(
Expand Down
3 changes: 0 additions & 3 deletions moviepy/audio/io/AudioFileClip.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,3 @@ def close(self):
if self.reader:
self.reader.close_proc()
self.reader = None

def __del__(self):
self.close()
15 changes: 12 additions & 3 deletions moviepy/video/VideoClip.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,17 @@ def afx(self, fun, *a, **k):
"""
self.audio = self.audio.fx(fun, *a, **k)

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 __and__(self, mask):
return self.set_mask(mask)


class DataVideoClip(VideoClip):
"""
Expand Down Expand Up @@ -911,9 +922,7 @@ def make_frame(t):
world.update()
return world.to_frame()

VideoClip.__init__(
self, make_frame=make_frame, ismask=ismask, duration=duration
)
super().__init__(make_frame=make_frame, ismask=ismask, duration=duration)


"""---------------------------------------------------------------------
Expand Down
1 change: 0 additions & 1 deletion moviepy/video/compositing/concatenate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from moviepy.audio.AudioClip import CompositeAudioClip
from moviepy.tools import deprecated_version_of
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy.video.compositing.on_color import on_color
from moviepy.video.VideoClip import ColorClip, VideoClip


Expand Down
5 changes: 2 additions & 3 deletions moviepy/video/fx/freeze.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from moviepy.decorators import requires_duration
from moviepy.video.compositing.concatenate import concatenate_videoclips
from moviepy.video.VideoClip import ImageClip


@requires_duration
Expand All @@ -21,7 +20,7 @@ def freeze(clip, t=0, freeze_duration=None, total_duration=None, padding_end=0):
if freeze_duration is None:
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).set_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)
11 changes: 1 addition & 10 deletions moviepy/video/fx/speedx.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from moviepy.decorators import apply_to_audio, apply_to_mask


def speedx(clip, factor=None, final_duration=None):
"""
Returns a clip playing the current clip but at a speed multiplied
Expand All @@ -12,10 +9,4 @@ def speedx(clip, factor=None, final_duration=None):

if final_duration:
factor = 1.0 * clip.duration / final_duration

newclip = clip.fl_time(lambda t: factor * t, apply_to=["mask", "audio"])

if clip.duration is not None:
newclip = newclip.set_duration(1.0 * clip.duration / factor)

return newclip
return clip[::factor]
6 changes: 2 additions & 4 deletions moviepy/video/fx/time_mirror.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from moviepy.decorators import apply_to_audio, apply_to_mask, requires_duration
from moviepy.decorators import requires_duration


@requires_duration
@apply_to_mask
@apply_to_audio
def time_mirror(self):
"""
Returns a clip that plays the current clip backwards.
The clip must have its ``duration`` attribute set.
The same effect is applied to the clip's audio and mask if any.
"""
return self.fl_time(lambda t: self.duration - t, keep_duration=True)
return self[::-1]
8 changes: 2 additions & 6 deletions moviepy/video/fx/time_symmetrize.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from moviepy.decorators import apply_to_audio, apply_to_mask, requires_duration
from moviepy.video.compositing.concatenate import concatenate_videoclips

from .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
Expand All @@ -14,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]
32 changes: 24 additions & 8 deletions tests/test_VideoClip.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import os
import sys

import pytest
from numpy import pi, sin
import numpy as np

from moviepy.audio.AudioClip import AudioClip
from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.utils import close_all_clips
from moviepy.video.fx.speedx import speedx
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.VideoClip import ColorClip, VideoClip

from moviepy.editor import VideoFileClip, ImageClip, ColorClip, AudioClip, AudioFileClip
from .test_helper import TMP_DIR


Expand Down Expand Up @@ -117,7 +112,7 @@ def test_oncolor():

def test_setaudio():
clip = ColorClip(size=(100, 60), color=(255, 0, 0), duration=0.5)
make_frame_440 = lambda t: [sin(440 * 2 * pi * t)]
make_frame_440 = lambda t: [np.sin(440 * 2 * np.pi * t)]
audio = AudioClip(make_frame_440, duration=0.5)
audio.fps = 44100
clip = clip.set_audio(audio)
Expand Down Expand Up @@ -163,5 +158,26 @@ def test_withoutaudio():
close_all_clips(locals())


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_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", ismask=True, transparent=True)
clip_with_mask = clip & maskclip
assert clip_with_mask.mask is maskclip


if __name__ == "__main__":
pytest.main()
Loading