Cloudinary Tricks for Video

Avatar of Jason Lengstorf
Jason Lengstorf on

Grow sales with Customer Journey Smarts with MailChimp Mailchimp tracking pixel

Creating video is time consuming. A well-made 5-minute video can take hours to plan, record, and edit — and that’s before we start talking about making that video consistent with all the other videos on your site.

When we took on the Jamstack Explorers project (a video-driven educational resource for web developers), we wanted to find the right balance of quality and shipping: what could we automate in our video production process to reduce the time and number of steps required to create video content without sacrificing quality?

With the help of Cloudinary, we were able to deliver a consistent branding approach in all our video content without adding a bunch of extra editing tasks for folks creating videos. And, as a bonus, if we update our branding in the future, we can update all the video branding across the whole site at once — no video editing required!

What does “video branding” mean?

To make every video on the Explorers site feel like it all fits together, we include a few common pieces in each video:

  1. A title scene
  2. A short intro bumper (video clip) that shows the Jamstack Explorers branding
  3. A short outro bumper that either counts down to the next video or shows a “mission accomplished” if this is the last video in the mission

Skip to the end: here’s how a branded video looks

To show the impact of adding the branding, here’s one of the videos from Jamstack Explorers without any branding:

This video (and this Vue mission from Ben Hong) is legitimately outstanding! However, it starts and ends a little abruptly, and we don’t have a sense of where this video lives.

We worked with Adam Hald to create branded video assets that help give each video a sense of place. Check out the same video with all the Explorers branding applied:

We get the same great content, but now we’ve added a little extra va-va-voom that makes this feel like it’s part of a larger story.

In this article, we’ll walk through how we automatically customize every video using Cloudinary.

How does Cloudinary make this possible?

Cloudinary is a cloud-based asset delivery network that gives us a powerful, URL-based API to manipulate and transform media. It supports all sorts of asset types, but where it really shines is with images and video.

To use Cloudinary, you create a free account, then upload your asset. This asset then becomes available at a Cloudinary URL:

https://res.cloudinary.com/netlify/image/upload/v1605632851/explorers/avatar.jpg
                           ^^^^^^^             ^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
                              |                      |                |
                              V                      V                V
                      cloud (account) name    version (optional)  file name

This URL points to the original image and can be used in <img /> tags and other markup.

The original image size is 97.6kB.

Dynamically adjust file format and quality to reduce file sizes

If we’re using this image on a website and want to improve our site performance, we may decide to reduce the size of this image by using next-generation formats like WebP, AVIF, and so on. These new formats are much smaller, but aren’t supported by all browsers, which would usually mean using a tool to generate multiple versions of this image in different formats, then using a <picture> element or other specialized markup to provide modern options with the JPG fallback for older browsers.

With Cloudinary, all we have to do is add a transformation to the URL:

https://res.cloudinary.com/netlify/image/upload/q_auto,f_auto/v1605632851/explorers/avatar.jpg
                                                ^^^^^^^^^^^^
                                                      |
                                                      V
                                    automatic quality & format transformations

What we see in the browser is visually identical:

The transformed image is 15.4kB.

By setting the file format and quality settings to automatic (f_auto,q_auto), Cloudinary is able to detect which formats are supported by the client and serves the most efficient format at a reasonable quality level. In Chrome, for example, this image transforms from a 97.6kB JPG to a 15.4kB WebP, and all we had to do was add a couple of things to the URL!

We can transform our images in lots of different ways!

We can go further with other transformations, including resizing (w_150 for “resize to 150px wide”) and color effects (e_grayscale for “apply the grayscale effect”):

https://res.cloudinary.com/netlify/image/upload/q_auto,f_auto,w_150,e_grayscale/v1605632851/explorers/avatar.jpg
The same image after adding grayscale effects and resizing.

This is only a tiny taste of what’s possible — make sure to check out the Cloudinary docs for more examples!

There’s a Node SDK to make this a little more human-readable

For more advanced transformations like what we’re going to get into, writing the URLs by hand can get a little hard to read. We ended up using the Cloudinary Node SDK to give us the ability to add comments and explain what each transformation was doing, and that’s been extremely helpful as we maintain and evolve the platform.

To install it, get your Cloudinary API key and secret from your console, then install the SDK using npm:

# create a new directory
mkdir cloudinary-video

# move into the new directory
cd cloudinary-video/

# initialize a new Node project
npm init -y

# install the Cloudinary Node SDK
npm install cloudinary

Next, create a new file called index.js and initialize the SDK with your cloud_name and API credentials:

const cloudinary = require('cloudinary').v2;

// TODO replace these values with your own Cloudinary credentials
cloudinary.config({
  cloud_name: 'your_cloud_name',
  api_key: 'your_api_key',
  api_secret: 'your_api_secret',
});

Don’t commit your API credentials to GitHub or share them anywhere. Use environment variables to keep them safe! If you’re unfamiliar with environment variables, Colby Fayock has written a great introduction to using environment variables.

Next, we can create the same transformation as before using slightly more human-readable configuration settings:

cloudinary.uploader
  // the first argument should be the public ID (including folders!) of the
  // image we want to transform
  .explicit('explorers/avatar', {
    // these two properties match the beginning of the URL:
    // https://res.cloudinary.com/netlify/image/upload/...
    //                                    ^^^^^^^^^^^^
    resource_type: 'image',
    type: 'upload',

    // "eager" means we want to run these transformations ahead of time to avoid
    // a slow first load time
    eager: [
      {
        fetch_format: 'auto',
        quality: 'auto',
        width: 150,
        effect: 'grayscale',
      },
    ],

    // allow this transformed image to be cached to avoid re-running the same
    // transformations over and over again
    overwrite: false,
  })
  .then((result) => {
    console.log(result);
  });

Let’s run this code by typing node index.js in our terminal. The output will look something like this:

{
  asset_id: 'fca4abba96ffdf70ef89498aa340ae4e',
  public_id: 'explorers/avatar',
  version: 1605632851,
  version_id: 'b8a923931af20404e89d03852ff1bff1',
  signature: 'e7201c9ab36cb5b6a0545cee4f5f8ee27fb7f99f',
  width: 300,
  height: 300,
  format: 'jpg',
  resource_type: 'image',
  created_at: '2020-11-17T17:07:31Z',
  bytes: 97633,
  type: 'upload',
  url: 'http://res.cloudinary.com/netlify/image/upload/v1605632851/explorers/avatar.jpg',
  secure_url: 'https://res.cloudinary.com/netlify/image/upload/v1605632851/explorers/avatar.jpg',
  access_mode: 'public',
  eager: [
    {
      transformation: 'e_grayscale,f_auto,q_auto,w_150',
      width: 150,
      height: 150,
      bytes: 6192,
      format: 'jpg',
      url: 'http://res.cloudinary.com/netlify/image/upload/e_grayscale,f_auto,q_auto,w_150/v1605632851/explorers/avatar.jpg',
      secure_url: 'https://res.cloudinary.com/netlify/image/upload/e_grayscale,f_auto,q_auto,w_150/v1605632851/explorers/avatar.jpg'
    }
  ]
}

Under the eager property, our transformations are shown along with the full URL to view the transformed image.

While the Node SDK is probably overkill for a straightforward transformation like this one, it becomes really handy when we start looking at the complex transformations required to add video branding.

Transforming videos with Cloudinary

To transform our videos in Jamstack Explorers, we follow the same approach: each video is uploaded to Cloudinary, and then we modify the URLs to resize, adjust quality, and insert the title card and bumpers.

There are a few major categories of transformation that we’ll be tackling to add the branding:

  1. Overlays
  2. Transitions
  3. Text overlays
  4. Splicing

Let’s look at each of these categories and see if we can’t reimplement the Jamstack Explorers branding on Ben’s video! Let’s get set up by setting up index.js to transform our base video:

cloudinary.uploader
  .explicit('explorers/bumper', {
    // these two properties match the beginning of the URL:
    // https://res.cloudinary.com/netlify/image/upload/...
    //                                    ^^^^^^^^^^^^
    resource_type: 'video',
   type: 'upload',

    // "eager" means we want to run these transformations ahead of time to avoid
    // a slow first load time
    eager: [
      {
        fetch_format: 'auto',
        quality: 'auto',
        height: 360,
        width: 640,
        crop: 'fill', // avoid letterboxing if videos are different sizes
      },
    ],

    // allow this transformed image to be cached to avoid re-running the same
    // transformations over and over again
    overwrite: false,
  })
  .then((result) => {
    console.log(result);
  });

You may have noticed that we’re using a video called “bumper” instead of Ben’s original video. This is due to the way Cloudinary orders videos as we add them together. We’ll add Ben’s video in the next section!

Combine two videos with a custom transition using Cloudinary

To add our bumpers, we need to add a second transformation “layer” to the eager array that adds a second video as an overlay.

To do this, we use the overlay transformation and set it to video:publicID, where publicID is the Cloudinary public ID of the asset with any slashes (/) transformed to colons (:).

We also need to tell Cloudinary how to transition between the two videos, which we do using a special kind of video called a luma matte that lets us mask one video with the black area of the video, and a second video with the white area. This results in a stylized cross-fade.

Here’s what the luma matte looks like on its own:

The video and the transition both have their own transformations, which means that we need to treat them as different “layers” in the Cloudinary transform. This means splitting them into separate objects, then adding additional objects to “apply” each layer, which allows us to call that section done and continue adding more transformations to the main video.

To tell Cloudinary this this is a luma matte and not another video, we set the effect type to transition.

Make the following changes in index.js to put all of this in place:

const videoBaseTransformations = {
  fetch_format: 'auto',
  quality: 'auto',
  height: 360,
  width: 600,
  crop: 'fill',
}

cloudinary.uploader
  .explicit('explorers/bumper', {
    // these two properties match the beginning of the URL:
    // <https://res.cloudinary.com/netlify/image/upload/>...
    //
    resource_type: 'video',
    type: 'upload',

    // "eager" means we want to run these transformations ahead of time to avoid
    // a slow first load time
    eager: [
      videoBaseTransformations,
      {
        overlay: 'video:explorers:LCA-07-lifecycle-hooks',
        ...videoBaseTransformations,
      },
      {
        overlay: 'video:explorers:transition',
        effect: 'transition',
      },
      { flags: 'layer_apply' }, // <= apply the transformation
      { flags: 'layer_apply' }, // <= apply the actual video
    ],

    // allow this transformed image to be cached to avoid re-running the same
    // transformations over and over again
    overwrite: false,
  })
  .then((result) => {
    console.log(result);
  });

We need the same format, quality, and sizing transformations on all videos, so we pulled those out into a variable called videoBaseTransformations, then added a second object to contain the overlay.

If we run this with node index.js, the video we get back looks like this:

Not bad! This already looks like it’s part of the Jamstack Explorers site, and that transition adds a nice flow from the common bumper into the custom video.

Adding the outro bumper works exactly the same: we need to add another overlay for the ending bumper and a transition. We won’t show this code in the tutorial, but you can see it in the source code if you’re interested.

Add a title card to a video using text overlays

To add a title card, there are two distinct steps:

  1. Extract a short video clip to serve as the title card background
  2. Add a text overlay with the video’s title

The next two sections walk through each step individually so we can see the distinction between the two.

Extract a short video clip to use as the title card background

When Adam Hald created the Explorers video assets, he included a beautiful intro video that opens on a starry sky that’s perfect for a title card. Using Cloudinary, we can grab a few seconds of that starry sky and splice it into every video as a title card!

In index.js, add the following transformation blocks:

cloudinary.uploader
  .explicit('explorers/bumper', {
    // these two properties match the beginning of the URL:
    // https://res.cloudinary.com/netlify/image/upload/...
    //
    resource_type: 'video',
    type: 'upload',

    // "eager" means we want to run these transformations ahead of time to avoid
    // a slow first load time
    eager: [
      videoBaseTransformations,
      {
        overlay: 'video:explorers:LCA-07-lifecycle-hooks',
        ...videoBaseTransformations,
      },
      {
        overlay: 'video:explorers:transition',
        effect: 'transition',
      },
      { flags: 'layer_apply' }, // <= apply the transformation
      { flags: 'layer_apply' }, // <= apply the actual video

      // add the outro bumper and a transition
      {
        overlay: 'video:explorers:countdown',
        ...videoBaseTransformations,
      },
      {
        overlay: 'video:explorers:transition',
        effect: 'transition',
      },
      { flags: 'layer_apply' },
      { flags: 'layer_apply' },

      // splice a title card at the beginning of the video
      {
        overlay: 'video:explorers:intro',
        flags: 'splice', // splice this into the video
        ...videoBaseTransformations,
      },
      {
        audio_codec: 'none', // remove the audio
        end_offset: 3, // shorten to 3 seconds
        effect: 'accelerate:-25', // slow down 25% (to ~4 seconds)
      },
      {
        flags: 'layer_apply',
        start_offset: 0, // put this at the beginning of the video
      },
    ],

    // allow this transformed image to be cached to avoid re-running the same
    // transformations over and over again
    overwrite: false,
  })
  .then((result) => {
    console.log(result);
  });

Using the splice flag, we tell Cloudinary to add this video directly without a transition.

In the next set of transformations, we add three transformations we haven’t seen before:

  1. We set audio_codec to none to remove sound from this segment of video.
  2. We set end_offset to 3, which means we’ll get only the first 3 seconds of the video.
  3. We add the accelerate effect with a value of -25, which slows the video down by 25%.

Running node index.js will now give us a video that starts with just under 4 seconds of silent, starry skies:

Add text overlays to videos using Cloudinary

Our last step is to add a text overlay to show the video title!

Text overlays use the same overlay property as other overlays, but we pass an object with settings for the font. Cloudinary supports a wide variety of fonts — I haven’t been able to find a definitive list, but it seems to be a large number of Google Fonts — and if you’ve purchased a license to use a custom font, you can upload a custom font to Cloudinary for use in text overlays as well.

cloudinary.uploader
  .explicit('explorers/bumper', {
    // these two properties match the beginning of the URL:
    // <https://res.cloudinary.com/netlify/image/upload/>...
    //
    resource_type: 'video',
    type: 'upload',

    // "eager" means we want to run these transformations ahead of time to avoid
    // a slow first load time
    eager: [
      videoBaseTransformations,
      {
        overlay: 'video:explorers:LCA-07-lifecycle-hooks',
        ...videoBaseTransformations,
      },
      {
        overlay: 'video:explorers:transition',
        effect: 'transition',
      },
      { flags: 'layer_apply' }, // <= apply the transformation
      { flags: 'layer_apply' }, // <= apply the actual video

      // add the outro bumper and a transition
      {
        overlay: 'video:explorers:countdown',
        ...videoBaseTransformations,
      },
      {
        overlay: 'video:explorers:transition',
          effect: 'transition',
        },
        { flags: 'layer_apply' },
        { flags: 'layer_apply' },

        // splice a title card at the beginning of the video
        {
          overlay: 'video:explorers:intro',
          flags: 'splice', // splice this into the video
          ...videoBaseTransformations,
        },
        {
          audio_codec: 'none', // remove the audio
          end_offset: 3, // shorten to 3 seconds
          effect: 'accelerate:-25', // slow down 25% (to ~4 seconds)
        },
        {
        overlay: {
          font_family: 'roboto', // lots of Google Fonts are supported
          font_size: 40,
          text_align: 'center',
          text: 'Lifecycle Hooks', // this can be any text you want
        },
        width: 500,
        crop: 'fit',
        color: 'white',
      },
      { flags: 'layer_apply' },
      {
        flags: 'layer_apply',
        start_offset: 0, // put this at the beginning of the video
      },
    ],

    // allow this transformed image to be cached to avoid re-running the same
    // transformations over and over again
    overwrite: false,
  })
  .then((result) => {
    console.log(result);
  });

In addition to setting the font size and alignment, we also apply a width of 500px (which will be centered by default) to keep our title text from smashing into the side of the title card, and set the crop value to fit, which will wrap longer titles. Setting the color to white makes our text visible against the dark, starry background.

Run node index.js to generate the URL and we’ll see our fully branded video, including a title card and bumpers!

Build your video branding once; use it everywhere

Creating bumpers, transitions, and title cards is a lot of work. Creating high-quality video content is also a lot of work. If we had to manually edit every Jamstack Explorers video to insert these title cards and bumpers, it’s extremely unlikely that we would have actually done it.

We knew that the only realistic way for us to keep the videos consistently branded was to reduce the friction of adding the branding, and Cloudinary let us automate it entirely. This means that we can stay consistent without any manual steps!

As an added bonus, it also means that if we update our title cards or bumpers in the future, we can update all the branding for all the videos by changing the code in one place. This is a huge relief for us, because we know that Explorers is going to continue to grow and evolve over time.

What to do next

Now that you know how to use Cloudinary to add custom branding, here are some additional resources to help you keep learning.

What else can you automate using Cloudinary? How much time could you save by automating the repetitive parts of your video editing workflow? I am exactly the kind of nerd who loves to talk about this stuff, so send me your ideas on Twitter!