MoviePy in 10 Minutes: Creating a Trailer from “Big Buck Bunny”#

Note

This tutorial aims to be a simple and short introduction for new users wishing to use MoviePy. For a more in-depth exploration of the concepts seen in this tutorial, see The MoviePy User Guide.

In this tutorial, you will learn the basics of how to use the MoviePy library in just 10 minutes. As an example project for this tutorial, we will create the following trailer for the movie “Big Buck Bunny.”.

Prerequisites#

Before we start, make sure you have MoviePy installed. You can install it using pip:

pip install moviepy

Also, we will need to gather a few resources such as the original movie, font files, images, etc. To make it easy, we have prepared a template project you can download directly:

  1. Download the project template and unzip it.

  2. Take a look at the resources inside the folder to familiarize yourself.

  3. Create a Python script file named trailer.py in the project directory.

Now, you are ready to proceed to the next steps.

Step 1: Import MoviePy and Load the Video#

Let’s start by importing the necessary modules and loading the “Big Buck Bunny” video into our Python program:

final_clip.write_videofile("./result.mp4")
# Lets import moviepy, lets also import numpy we will use it a some point
from moviepy import *
import numpy as np


#################
# VIDEO LOADING #
#################
# We load our video
video = VideoFileClip("./resources/bbb.mp4")

As you see, loading a video file is really easy, but MoviePy isn’t limited to video. It can handle images, audio, texts, and even custom animations.

No matter the kind of resources, ultimately any clip will be either a VideoClip for any visual element, and an AudioClip for any audio element.

In this tutorial, we will only see a few of those, but if you want to explore more, you can find an exhaustive list in the user guide about Loading resources as clips.

Step 2: Extract the Best Scenes#

To create our trailer, we will focus on presenting the main characters, so we need to extract parts of the movie. This is a very classic task, so let’s turn our main clip into multiple subclips:

#####################
# SCENES EXTRACTION #
#####################
# We extract the scenes we want to use

# First the characters
intro_clip = video.with_subclip(1, 11)
bird_clip = video.with_subclip(16, 20)
bunny_clip = video.with_subclip(37, 55)
rodents_clip = video.with_subclip(
    "00:03:34.75", "00:03:56"
)  # we can also use string notation with format HH:MM:SS.uS
rambo_clip = video.with_subclip("04:41.5", "04:44.70")

Here, we use the with_subclip method to extract specific scenes from the main video. We provide the start and end times (in seconds or as text with the format HH:MM:SS.µS) for each scene. The extracted clips are stored in their respective variables (intro_clip, bird_clip, etc.).

Step 3: Take a First Look with Preview#

When editing videos, it’s often essential to preview the clips to ensure they meet our vision. This allows you to watch the segment you’re working on and make any necessary adjustments for the perfect result.

To do so using MoviePy, you can utilize the preview() function available for each clip (the complementary audio_preview() is also available for AudioClip).

Note

Note that you will need ffplay installed and accessible to MoviePy for preview to work. You can check if ffplay is available by running the command python3 -c "from moviepy.config import check;check()". If not, please see Installation of additional binaries.

#####################
# SCENES PREVIEWING #
#####################
# Now, lets have a first look at our clips
# Warning: you need ffplay installed for preview to work
# We set a low fps so our machine can render in real time without slowing down
intro_clip.preview(fps=20)
bird_clip.preview(fps=20)
bunny_clip.preview(fps=20)
rodents_clip.preview(fps=20)
rambo_clip.preview(fps=20)

By using the preview, you may have noticed that our clips not only contain video but also audio. This is because when loading a video, you not only load the image but also the audio tracks that are turned into AudioClip and added to your video clip.

Note

When previewing, you may encounter video slowing or video/audio shifting. This is not a bug; it’s due to the fact that your computer cannot render the preview in real-time. In such a case, the best course of action is to set the fps parameter for the preview() at a lower value to make things easier on your machine.

Step 4: Modify a Clip by Cutting Out a Part of It#

After previewing the clips, we notice that the rodents’ scene is a bit long. Let’s modify the clip by removing a specific part. It would be nice to remove parts of the scene that we don’t need. This is also quite a common task in video-editing. To do so, we are going to use the with_cutout method to remove a portion of the clip between 00:06:00 to 00:10:00.

##############################
# CLIPS MODIFICATION CUTTING #
##############################
# Well, looking at the rodent scene it is a bit long isn't?
# Let's see how we modify the clip with one of the many clip manipulation method starting by with_*
# in that case by removing of the clip the part between 00:06:00 to 00:10:00 of the clip, using with_cutout
rodents_clip = rodents_clip.with_cutout(start_time=4, end_time=10)

# Note: You may have noticed that we have reassign rodents_clip, this is because all with_* methods return a modified *copy* of the
# original clip instead of modifying it directly. In MoviePy any function starting by with_* is out-place instead of in-place
# meaning it does not modify the original data, but instead copy it and modify/return the copy

# Lets check the result
rodents_clip.preview(fps=10)

In that particular case, we have used the with_cutout, but this is only one of the many clip manipulation methods starting with with_*. We will see a few others in this tutorial, but we will miss a lot more. If you want an exhaustive list, go see Api Reference.

Note

You may have noticed that we have reassigned the rodents_clip variable instead of just calling a method on it. This is because in MoviePy, any function starting with with_* is out-of-place instead of in-place, meaning it does not modify the original data but instead copies it and modifies/returns the copy. So you need to store the result of the method and, if necessary, reassign the original variable to update your clip.

Step 5: Creating Text/Logo Clips#

In addition to videos, we often need to work with images and texts. MoviePy offers some specialized kinds of VideoClip specifically for that purpose: ImageClip and TextClip.

In our case, we want to create text clips to add text overlays between the video clips. We’ll define the font, text content, font size, and color for each text clip. We also want to create image clips for the “Big Buck Bunny” logo and the “Made with MoviePy” logo and resize them as needed.

############################
# TEXT/LOGO CLIPS CREATION #
############################
# Lets create the texts to put between our clips
font = "./resources/font/font.ttf"
intro_text = TextClip(
    font=font,
    text="The Blender Foundation and\nPeach Project presents",
    font_size=50,
    color="#fff",
    text_align="center",
)
bird_text = TextClip(font=font, text="An unlucky bird", font_size=50, color="#fff")
bunny_text = TextClip(
    font=font, text="A (slightly overweight) bunny", font_size=50, color="#fff"
)
rodents_text = TextClip(
    font=font, text="And three rodent pests", font_size=50, color="#fff"
)
revenge_text = TextClip(
    font=font, text="Revenge is coming...", font_size=50, color="#fff"
)
made_with_text = TextClip(font=font, text="Made with", font_size=50, color="#fff")

# We will also need the big buck bunny logo, so lets load it and resize it
logo_clip = ImageClip("./resources/logo_bbb.png").resized(width=400)
moviepy_clip = ImageClip("./resources/logo_moviepy.png").resized(width=300)

As you can see, ImageClip is quite simple, but TextClip is a rather complicated object. Don’t hesitate to explore the arguments it accepts.

Note

In our example, we have used the resized() method to resize our image clips. This method works just like any with_* method, but because resizing is such a common task, the name has been shortened to resized(). The same is true for cropped() and rotated().

Feel free to experiment with different effects and transitions to achieve the desired trailer effect.

Step 6: Timing the clips#

We have all the clips we need, but if we were to combine all those clips into a single one using composition (we will see that in the next step), all our clips would start at the same time and play on top of each other, which is obviously not what we want. Also, some video clips, like the images and texts, have no endpoint/duration at creation (except if you have provided a duration parameter), which means trying to render them will throw an error as it would result in an infinite video.

To fix that, we need to specify when a clip should start and stop in the final clip. So, let’s start by indicating when each clip must start and end with the appropriate with_* methods.

################
# CLIPS TIMING #
################
# We have all the clips we need, but if we was to turn all thoses clips into a single one with composition (we will see that during next step)
# all our clips would start at the same time and play on top of each other, which is obviously not what we want.
# To fix that, we need to say when a clip should start and stop in the final clip.
# So, lets start by telling when each clip must start and end with appropriate with_* methods
intro_text = intro_text.with_duration(6).with_start(
    3
)  # Intro for 6 seconds, start after 3 seconds
logo_clip = logo_clip.with_start(intro_text.start + 2).with_end(
    intro_text.end
)  # Logo start 2 second after intro text and stop with it
bird_clip = bird_clip.with_start(
    intro_clip.end
)  # Make bird clip start after intro, duration already known
bird_text = bird_text.with_start(bird_clip.start).with_end(
    bird_clip.end
)  # Make text synchro with clip
bunny_clip = bunny_clip.with_start(bird_clip.end)  # Make bunny clip follow bird clip
bunny_text = bunny_text.with_start(bunny_clip.start + 2).with_duration(7)
rodents_clip = rodents_clip.with_start(bunny_clip.end)
rodents_text = rodents_text.with_start(rodents_clip.start).with_duration(4)
rambo_clip = rambo_clip.with_start(rodents_clip.end - 1.5)
revenge_text = revenge_text.with_start(rambo_clip.start + 1.5).with_duration(4)
made_with_text = made_with_text.with_start(rambo_clip.end).with_duration(3)
moviepy_clip = moviepy_clip.with_start(made_with_text.start).with_duration(3)

Note

By default, all clips have a start point at 0. If a clip has no duration but you set the endtime, then the duration will be calculated for you. The reciprocity is also true.

So in our case, we either use duration or endtime, depending on what is more practical for each specific case.

Step 7: Seeing how all clips combine#

Now that all our clips are timed, let’s get a first idea of how our final clip will look. In video editing, the act of assembling multiple videos into a single one is known as composition. So, MoviePy offers a special kind of VideoClip dedicated to the act of combining multiple clips into one, the CompositeVideoClip.

CompositeVideoClip takes an array of clips as input and will play them on top of each other at render time, starting and stopping each clip at its start and end points.

Note

If possible, CompositeVideoClip will extract endpoint and size from the biggest/last ending clip. If a clip in the list has no duration, then you will have to manually set the duration of CompositeVideoClip before rendering.

########################
# CLIPS TIMING PREVIEW #
########################
# Lets make a first compositing of thoses clips into one single clip and do a quick preview to see if everything is synchro

quick_compo = CompositeVideoClip(
    [
        intro_clip,
        intro_text,
        logo_clip,
        bird_clip,
        bird_text,
        bunny_clip,
        bunny_text,
        rodents_clip,
        rodents_text,
        rambo_clip,
        revenge_text,
        made_with_text,
        moviepy_clip,
    ]
)
quick_compo.preview(fps=10)

Step 8: Positioning our clips#

By looking at this first preview, we see that our clips are pretty well timed, but that the positions of our texts and logo are not satisfying.

This is because, for now, we have only specified when our clips should appear, and not the position at which they should appear. By default, all clips are positioned from the top left of the video, at (0, 0).

All our clips do not have the same sizes (the texts and images are smaller than the videos), and the CompositeVideoClip takes the size of the biggest clip (so in our case, the size of the videos), so the texts and images are all in the top left portion of the clip.

To fix this, we simply have to define the position of our clips in the composition with the method with_position.

######################
# CLIPS POSITIONNING #
######################
# Now that we have set the timing of our different clips, we need to make sure they are in the right position
# We will keep things simple, and almost always set center center for every texts
bird_text = bird_text.with_position(("center", "center"))
bunny_text = bunny_text.with_position(("center", "center"))
rodents_text = rodents_text.with_position(("center", "center"))
revenge_text = revenge_text.with_position(("center", "center"))

# For the logos and intro/end, we will use pixel position instead of center
top = intro_clip.h // 2
intro_text = intro_text.with_position(("center", 200))
logo_clip = logo_clip.with_position(("center", top))
made_with_text = made_with_text.with_position(("center", 300))
moviepy_clip = moviepy_clip.with_position(("center", 360))

# Lets take another look to check positions
quick_compo = CompositeVideoClip(
    [
        intro_clip,
        intro_text,
        logo_clip,
        bird_clip,
        bird_text,
        bunny_clip,
        bunny_text,
        rodents_clip,
        rodents_text,
        rambo_clip,
        revenge_text,
        made_with_text,
        moviepy_clip,
    ]
)
quick_compo.preview(fps=10)

Note

The position is a tuple with horizontal and vertical position. You can give them as pixels, as strings (top, left, right, bottom, center), and even as a percentage by providing a float and passing the argument relative=True.

Now, all our clips are in the right place and timed as expected.

Step 9: Adding transitions and effects#

So, our clips are timed and placed, but for now, the result is quite raw. It would be nice to have smoother transitions between the clips. In MoviePy, this is achieved through the use of effects.

Effects play a crucial role in enhancing the visual and auditory appeal of your video clips. Effects are applied to clips to create transitions, transformations, or modifications, resulting in better-looking videos. Whether you want to add smooth transitions between clips, alter visual appearance, or manipulate audio properties, MoviePy comes with many existing effects to help you bring your creative vision to life with ease.

You can find these effects under the namespace vfx for video effects and afx for audio effects.

Note

You can use audio effects on both audio and video clips because when applying audio effects to a video clip, the effect will actually be applied to the video clip’s embedded audio clip instead.

Using an effect is very simple. You just have to call the method with_effects on your clip and pass an array of effect objects to apply.

In our case, we will add simple fade-in/out and cross-fade-in/out transitions between our clips, as well as slow down the rambo_clip.

################################
# CLIPS TRANSITION AND EFFECTS #
################################
# Now that our clip are timed and positionned, lets add some transition to make it more natural
# To do so we use the with_effects method and the video effects in vfx
# We call with_effects on our clip and pass him an array of effect objects to apply
# We'll keep it simple, nothing fancy just cross fading
intro_text = intro_text.with_effects([vfx.CrossFadeIn(1), vfx.CrossFadeOut(1)])
logo_clip = logo_clip.with_effects([vfx.CrossFadeIn(1), vfx.CrossFadeOut(1)])
bird_text = bird_text.with_effects([vfx.CrossFadeIn(0.5), vfx.CrossFadeOut(0.5)])
bunny_text = bunny_text.with_effects([vfx.CrossFadeIn(0.5), vfx.CrossFadeOut(0.5)])
rodents_text = rodents_text.with_effects([vfx.CrossFadeIn(0.5), vfx.CrossFadeOut(0.5)])

# Also add cross fading on video clips and video clips audio
# See how video effects are under vfx and audio ones under afx
intro_clip = intro_clip.with_effects(
    [vfx.FadeIn(1), vfx.FadeOut(1), afx.AudioFadeIn(1), afx.AudioFadeOut(1)]
)
bird_clip = bird_clip.with_effects(
    [vfx.FadeIn(1), vfx.FadeOut(1), afx.AudioFadeIn(1), afx.AudioFadeOut(1)]
)
bunny_clip = bunny_clip.with_effects(
    [vfx.FadeIn(1), vfx.FadeOut(1), afx.AudioFadeIn(1), afx.AudioFadeOut(1)]
)
rodents_clip = rodents_clip.with_effects(
    [vfx.FadeIn(1), vfx.CrossFadeOut(1.5), afx.AudioFadeIn(1), afx.AudioFadeOut(1.5)]
)  # Just fade in, rambo clip will do the cross fade
rambo_clip = rambo_clip.with_effects(
    [vfx.CrossFadeIn(1.5), vfx.FadeOut(1), afx.AudioFadeIn(1.5), afx.AudioFadeOut(1)]
)
rambo_clip = rambo_clip.with_effects(
    [vfx.CrossFadeIn(1.5), vfx.FadeOut(1), afx.AudioFadeIn(1.5), afx.AudioFadeOut(1)]
)

# Effects are not only for transition, they can also change a clip timing or apparence
# To show that, lets also modify the Rambo-like part of our clip to be in slow motion
# PS : We do it for effect, but this is one of the few effects that have a direct shortcut, with_multiply_speed
# the others are with_multiply_volume, resized, croped and rotated
rambo_clip = rambo_clip.with_effects([vfx.MultiplySpeed(0.5)])

# Because we modified timing of rambo_clip with our MultiplySpeed effect, we must re-assign the following clips timing
made_with_text = made_with_text.with_start(rambo_clip.end).with_duration(3)
moviepy_clip = moviepy_clip.with_start(made_with_text.start).with_duration(3)

# Let's have a last look at the result to make sure everything is working as expected
quick_comp = CompositeVideoClip(
    [
        intro_clip,
        intro_text,
        logo_clip,
        bird_clip,
        bird_text,
        bunny_clip,
        bunny_text,
        rodents_clip,
        rodents_text,
        rambo_clip,
        revenge_text,
        made_with_text,
        moviepy_clip,
    ]
)
quick_comp.preview(fps=10)

Well, this looks a lot nicer! For this tutorial, we want to keep things simple, so we mostly used transitions. However, you can find many different effects and even create your own. For a more in-depth presentation, see moviepy.video.fx, moviepy.audio.fx, and Creating your own effects.

Note

Looking at the result, you may notice that crossfading makes clips go from transparent to opaque, and reciprocally, and wonder how it works.

We won’t get into details, but know that in MoviePy, you can declare some sections of a video clip to be transparent by using masks. Masks are nothing more than special kinds of video clips that are made of values ranging from 0 for a transparent pixel to 1 for a fully opaque one.

For more info, see Mask clips.

Step 10: Modifying the appearance of a clip using filters#

Finally, to make it more epic, we will apply a custom filter to our Rambo clip to make the image sepia. MoviePy does not come with a sepia effect out of the box, and creating a full custom effect is beyond the scope of this tutorial. However, we will see how we can apply a simple filter to our clip using the image_transform method.

To understand how filters work, you first need to understand that in MoviePy, a clip frame is nothing more than a numpy ndarray of shape HxWx3. This means we can modify how a frame looks like by applying simple math operations. Doing that on all the frames allows us to apply a filter to our clip!

The “apply to all frames” part is done by the image_transform method. This method takes a callback function as an argument, and at render time, it will trigger the callback for each frame of the clip, passing the current frame.

Warning

This is a bit of an advanced usage, and the example involves matrix multiplication. If this is too much for you, you can simply ignore it until you really need to make custom filters, then go look for a more detailed explanation on how to do filtering (Modify a clip apparence and timing using filters) and create custom effects (Creating your own effects) in the user guide.

What you need to remember is just that we can apply filters on images. Here we do it mathematically, but you could very well use a library such as Pillow (provided it can understand numpy images) to do the maths for you!

###############
# CLIP FILTER #
###############
# Lets finish by modifying our rambo clip to make it sepia


# We will start by defining a function that turn a numpy image into sepia
# It takes the image as numpy array in entry and return the modified image as output
def sepia_fitler(frame: np.ndarray):
    # Sepia filter transformation matrix
    # Sepia transform works by applying to each pixel of the image the following rules
    # res_R = (R * .393) + (G *.769) + (B * .189)
    # res_G = (R * .349) + (G *.686) + (B * .168)
    # res_B = (R * .272) + (G *.534) + (B * .131)
    #
    # With numpy we can do that very efficiently by multiplying the image matrix by a transformation matrix
    sepia_matrix = np.array(
        [[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]]
    )

    # Convert the image to float32 format for matrix multiplication
    frame = frame.astype(np.float32)

    # Apply the sepia transformation
    # .T is needed because multiplying matrix of shape (n,m) * (m,k) result in a matrix of shape (n,k)
    # what we want is (n,m), so we must transpose matrix (m,k) to (k,m)
    sepia_image = np.dot(frame, sepia_matrix.T)

    # Because final result can be > 255, we limit the result to range [0, 255]
    sepia_image = np.clip(sepia_image, 0, 255)

    # Convert the image back to uint8 format, because we need integer not float
    sepia_image = sepia_image.astype(np.uint8)

    return sepia_image


# Now, we simply apply the filter to our clip by calling image_transform, which will call our filter on every frame
rambo_clip = rambo_clip.image_transform(sepia_fitler)

# Let's see how our filter look
rambo_clip.preview(fps=10)

Step 11: Rendering the final clip to a file#

So, our final clip is ready, and we have made all the cutting and modifications we want. We are now ready to save the final result into a file. In video editing, this operation is known as rendering.

Again, we will keep things simple and just do video rendering without much tweaking. In most cases, MoviePy and FFMPEG automatically find the best settings. Take a look at the write_videofile doc for more info.

##################
# CLIP RENDERING #
##################
# Everything is good and ready, we can finally render our clip into a file
final_clip = CompositeVideoClip(
    [
        intro_clip,
        intro_text,
        logo_clip,
        bird_clip,
        bird_text,
        bunny_clip,
        bunny_text,
        rodents_clip,
        rodents_text,
        rambo_clip,
        revenge_text,
        made_with_text,
        moviepy_clip,
    ]
)
final_clip.write_videofile("./result.mp4")

Conclusion#

Congratulations! You have successfully created a trailer for the movie “Big Buck Bunny” using the MoviePy library. This tutorial covered the basics of MoviePy, including loading videos, trimming scenes, adding effects and transitions, overlaying text, and even a little bit of filtering.

If you want to dig deeper into MoviePy, we encourage you to try and experiment with this base example by using different effects, transitions, and audio elements to make your trailer truly captivating. We also encourage you to go and read the The MoviePy User Guide, as well as looking directly at the Api Reference.