Skip to content

RESOURCES / BLOG

Convert Video to Boomerang Using NextJS

Instagram’s boomerang-style videos have become increasingly popular over the past few years. Let’s see how we can create the same programmatically using Cloudinary and Next.js

The completed project is available on Codesandbox.

Ensure you have Node.js + NPM installed. The official documentation has a guide on how you can install it on your development environment. It is also ideal that you have a working knowledge of Javascript and React.js. Apart from that, the only other thing we need is Cloudinary API credentials.

Cloudinary is a media management solution that offers a wide array of APIs that allow for upload, optimization, and manipulation of media. It’s free and easy to get started with a developer account.

Sign in to your Cloudinary account and navigate to the Console. You’ll find your credentials on the console page at the top.

Cloudinary Dashboard

Remember the location of your credentials, we will need to use them later on.

We will be using Next.js, a react framework to build our application. The concept can, however, be applied to any Node.js application with a compatible front-end. Let’s start a new Next project.

Open your terminal and run the following in your desired location.


npx create-next-app videos-to-boomerang-using-cloudinary

We’ve just created a new project and named it videos-to-boomerang-using-cloudinary. The name can be whatever you want. If you would like to use advanced features such as Typescript, you should have a look at the official documentation.

Change directory to your new project


cd videos-to-boomerang-using-cloudinary

Open your project in any code editor. I recommend Visual Studio Code as it has amazing support for Javascript and React.

Let’s first install any dependencies we might have and get those out of the way. For this tutorial, we shall be using the Cloudinary SDK to communicate to cloudinary and Formidable to parse Form data. Run the following to install the two.


npm install --save cloudinary formidable

We need a secure way to define our API keys and secrets. Luckily, Next.js has built-in support for environment variables. Read more about it in the documentation.

Create a new file at the root of your project and name it .env.local. Paste the following inside


CLOUD_NAME=YOUR_CLOUD_NAME

API_KEY=YOUR_API_KEY

API_SECRET=YOUR_API_SECRET

Replace YOUR_CLOUD_NAME,YOUR_API_KEY, and YOUR_API_SECRET with the appropriate values from Cloudinary API Credentials. We got these values in the getting-cloudinary-api-credentials section.

Now we need to define functions that will handle the upload to cloudinary. Create a folder at the root of your project and name it lib. Inside the folder, create a file called cloudinary.js


// lib/cloudinary.js

  

// Import the v2 api and rename it to cloudinary

import  {  v2  as  cloudinary  }  from  "cloudinary";

  

// Initialize the SDK with cloud_name, api_key, and api_secret

cloudinary.config({

cloud_name:  process.env.CLOUD_NAME,

api_key:  process.env.API_KEY,

api_secret:  process.env.API_SECRET,

});

Code language: JavaScript (javascript)

We’ve imported the v2 SDK and initialized it using our environment variables as the values for cloud_name, api_key, and api_secret. Note how we rename v2 to cloudinary for code readability.

We want to store all our videos in one folder. Add the following just below the code we added above.


// lib/cloudinary.js

  

// ... other code

  

const  FOLDER_NAME  =  "boomerang-videos/";

Code language: JavaScript (javascript)

And now a function to get all videos in that folder. Add the following below the line we just added


// lib/cloudinary.js

  

export  const  handleGetCloudinaryUploads  =  ()  =>  {

return  new  Promise((resolve,  reject)  =>  {

cloudinary.api.resources(

{

type:  "upload",

prefix:  FOLDER_NAME,

resource_type:  "video",

},

(error,  result)  =>  {

if  (error)  {

return  reject(error);

}

  

return  resolve(result);

}

);

});

};

Code language: JavaScript (javascript)

We’re exporting a function named handleGetCloudinaryUploads. The function calls the api.resources method on the cloudinary SDK. This gets all resources matching the options that we pass. The options include the type of resource, where we specify uploaded resources, and the prefix, which is the folder name where our videos are stored. We also tell it to only get video resources. We then either reject the promise in case of an error or resolve it with the result.

Next, a function to handle the upload. Add the following to the same file.


// lib/cloudinary.js

  

export  const  handleCloudinaryUpload  =  (path)  =>  {

// Create and return a new Promise

return  new  Promise((resolve,  reject)  =>  {

// Use the SDK to upload media

cloudinary.uploader.upload(

path,

{

// Folder to store video in

folder:  FOLDER_NAME,

// Type of resource

resource_type:  "auto",

// Formats we want to allow

allowed_formats:  ["mp4"],

// Transformations to run on the video

transformation:  [

// Set the video to be trimmed at 2 seconds

{ end_offset:  "2.0"  },

// Set the boomerang effect

{ effect:  "boomerang"  },

// Set the boomerang effect to be looped 3 times

{ effect:  "loop:3"  },

],

},

(error,  result)  =>  {

if  (error)  {

// Reject the promise with an error if any

return  reject(error);

}

  

// Resolve the promise with a successful result

return  resolve(result);

}

);

});

};

Code language: JavaScript (javascript)

Here we’re exporting a function named handleCloudinaryUpload which takes in a path to the video file we want to upload. We then call the uploader.upload method on the SDK. I will only focus on the transformation option that we pass. I will leave a link to the documentation for reference. In the transformation array, we need to pass all the transformations that we want to apply to the video before it’s stored on cloudinary servers. For the first transformation, we set an end offset of 2 seconds. This means that the video will be trimmed at 2 seconds and the boomerang applied to those 2 seconds. You can leave this transformation out, but long videos don’t make for very good boomerangs. The second and third transformations just set the boomerang effect and loop effect respectively. Please note that we also set the video to loop 3 times. This is also flexible and you can change to however many you want. Read more about all the options in the official documentation. See also.

The final function for this file will handle deleting uploaded media. Add the following below the function we just created above.


// lib/cloudinary.js

export  const  handleCloudinaryDelete  =  async  (ids)  =>  {

return  new  Promise((resolve,  reject)  =>  {

cloudinary.api.delete_resources(

ids,

{

resource_type:  "video",

},

(error,  result)  =>  {

if  (error)  {

return  reject(error);

}

  

return  resolve(result);

}

);

});

};

Code language: JavaScript (javascript)

This is self-explanatory. The function takes in an array of public ids for the media we want to delete. We then call the api.delete_resources method on the SDK and pass to it our ids and specify the resource type to be video.

Moving on, we need API routes, that will receive requests from our front end.

One of the best features of Next.js and the reason why many people prefer it over plain React is support for Server Side Rendering and API routes. I highly recommend you read about Next API routes here if you’re not familiar.

Create a new file called videos.js under the pages/api/ folder. This will allow us to make requests to the api/videos endpoint. Paste the following code inside


// pages/api/videos.js

  

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

import  {  IncomingForm,  Fields,  Files  }  from  "formidable";

import  {

handleCloudinaryDelete,

handleCloudinaryUpload,

handleGetCloudinaryUploads,

}  from  "../../lib/cloudinary";

  

// Custom config for our API route

export  const  config  =  {

api:  {

bodyParser:  false,

},

};

  

export  default  async  function  handler(req,  res)  {

switch  (req.method)  {

case  "GET":  {

try  {

const  result  =  await  handleGetRequest();

  

return  res.status(200).json({ message:  "Success",  result  });

}  catch  (error)  {

return  res.status(400).json({ message:  "Error",  error  });

}

}

  

case  "POST":  {

try  {

const  result  =  await  handlePostRequest(req);

  

return  res.status(200).json({ message:  "Success",  result  });

}  catch  (error)  {

return  res.status(400).json({ message:  "Error",  error  });

}

}

  

case  "DELETE":  {

try  {

const  {  id  }  =  req.query;

  

if  (!id)  {

throw  "id param is required";

}

  

const  result  =  await  handleDeleteRequest(id);

  

return  res.status(200).json({ message:  "Success",  result  });

}  catch  (error)  {

return  res.status(400).json({ message:  "Error",  error  });

}

}

  

default:  {

return  res.status(405).json({ message:  "Method not allowed"  });

}

}

}

  

const  handleGetRequest  =  ()  =>  handleGetCloudinaryUploads();

  

const  handlePostRequest  =  async  (req)  =>  {

// Get the form data using the parseForm function

const  data  =  await  parseForm(req);

  

const  uploadResult  =  await  handleCloudinaryUpload(data.files.video.path);

  

return  uploadResult;

};

  

const  handleDeleteRequest  =  async  (id)  =>  handleCloudinaryDelete([id]);

  

/**

*

* @param  {*}  req

* @returns  {Promise<{ fields:Fields; files:Files; }>}

*/

const  parseForm  =  (req)  =>  {

return  new  Promise((resolve,  reject)  =>  {

const  form  =  new  IncomingForm({ keepExtensions:  true, multiples:  true  });

  

form.parse(req,  (error,  fields,  files)  =>  {

if  (error)  {

return  reject(error);

}

  

return  resolve({  fields,  files  });

});

});

};

Code language: JavaScript (javascript)

There’s quite a lot going on here. We first import the functions we created in the previous section, as well as a few things from the formidable package. Just after that, we export some custom configurations for our API route. Since we will be expecting multipart/form-data, we don’t need to use the default body-parser. Read about custom configs for API routes here.

We then have our route handler. We use a switch statement to assign different handlers for the GET, POST, and DELETE HTTP requests. For each, we either return a 200 OK status or a 400 BAD REQUEST status. For other methods that we don’t support we return a 405 status code. The handleGetRequest and handleDeleteRequest functions are pretty straight forward. For the handlePostRequest, we pass the incoming request to a parseForm function that will use formidable to parse the form data. Once we get the video file that was sent via the form data we pass its path to the handleCloudinaryUpload function and return the upload result. You might want to have a look at formidable docs to better understand everything happening in the parseForm function.

With this in place, all that’s remaining is our front end.

Open index.js under the pages folder and replace its content with the following.


// pages/index.js

  

import  Head  from  "next/head";

import  {  useCallback,  useEffect,  useState  }  from  "react";

  

export  default  function  Home()  {

const  [videos,  setVideos]  =  useState([]);

  

const  [file,  setFile]  =  useState(null);

  

const  [loading,  setLoading]  =  useState(false);

  

const  getVideos  =  useCallback(async  ()  =>  {

try  {

const  response  =  await  fetch(`/api/videos`,  {

method:  "GET",

});

  

const  data  =  await  response.json();

  

if  (!response.ok)  {

throw  data;

}

  

setVideos(data.result.resources);

}  catch  (error)  {

// TODO: Show error message to the user

console.error(error);

}  finally  {

// setLoading(false);

}

},  []);

  

useEffect(()  =>  {

getVideos();

},  [getVideos]);

  

const  handleFormSubmit  =  async  (e)  =>  {

e.preventDefault();

  

setLoading(true);

try  {

// Get the form data

const  formData  =  new  FormData(e.target);

  

// Post the form data to the /api/videos endpoint

const  response  =  await  fetch("/api/videos",  {

method:  "POST",

body:  formData,

});

  

const  data  =  await  response.json();

  

if  (!response.ok)  {

throw  data;

}

  

e.target[0].value  =  "";

setFile(null);

getVideos();

}  catch  (error)  {

// TODO: Show error message to the user

console.error(error);

}  finally  {

setLoading(false);

}

};

  

const  handleDownloadResource  =  async  (resourceUrl,  assetId,  format)  =>  {

try  {

setLoading(true);

const  response  =  await  fetch(resourceUrl,  {});

  

if  (response.status  >=  200  &&  response.status  <  300)  {

const  blob  =  await  response.blob();

  

const  fileUrl  =  URL.createObjectURL(blob);

  

const  a  =  document.createElement("a");

a.href  =  fileUrl;

a.download  =  `${assetId}.${format}`;

document.body.appendChild(a);

a.click();

a.remove();

return;

}

  

throw  await  response.json();

}  catch  (error)  {

// TODO: Show error message to the user

console.error(error);

}  finally  {

setLoading(false);

}

};

  

const  handleDeleteResource  =  async  (id)  =>  {

try  {

setLoading(true);

const  response  =  await  fetch(`/api/videos/?id=${id}`,  {

method:  "DELETE",

});

  

const  data  =  await  response.json();

  

if  (response.status  >=  200  &&  response.status  <  300)  {

return  getVideos();

}

  

throw  data;

}  catch  (error)  {

// TODO: Show error message to the user

console.error(error);

}  finally  {

setLoading(false);

}

};

  

return  (

<div>

<Head>

<title>Videos to Boomerangs Using Cloudinary</title>

<meta

name="description"

content="Videos to Boomerangs Using Cloudinary"

/>

<link rel="icon"  href="/favicon.ico"  />

</Head>

  

<main>

<div className="header">

<h1>Videos to Boomerangs Using Cloudinary</h1>

</div>

<hr />

<form onSubmit={handleFormSubmit}>

<label htmlFor="video">

<b>Select video for upload</b>

</label>

<input

type="file"

name="video"

id="video"

multiple={false}

accept=".mp4"

required

disabled={loading}

onChange={(e)  =>  {

setFile(e.target.files[0]);

}}

/>

<button type="submit"  disabled={loading  ||  !file}>

Upload Video

</button>

</form>

<hr />

{loading  &&  (

<div className="loading">

<p>Please be patient as the action is performed...</p>

<hr />

</div>

)}

<div className="videos-wrapper">

{videos.map((video,  index)  =>  (

<div className="video-wrapper"  key={`video-${index}`}>

<video

src={video.secure_url}

loop

preload="none"

controls

poster={video.secure_url.replace(".mp4",  ".gif")}

></video>

<div className="video-info">

<a href={video.secure_url}>Link to video</a>

<div className="actions">

<button

disabled={loading}

onClick={()  =>  {

handleDownloadResource(

video.secure_url,

video.asset_id,

video.format

);

}}

>

Download

</button>

<button

disabled={loading}

onClick={()  =>  {

handleDeleteResource(video.public_id);

}}

>

Delete

</button>

</div>

</div>

</div>

))}

</div>

</main>

<style jsx>{`

main {

}

main .header {

min-height: 100px;

display: flex;

flex-flow: column;

align-items: center;

justify-content: center;

}

main form {

display: flex;

flex-flow: column;

align-items: center;

justify-content: center;

min-height: 200px;

background-color: #ebebeb;

}

main form button {

min-width: 300px;

min-height: 50px;

margin-top: 20px;

}

main div.videos-wrapper {

width: 100%;

display: flex;

flex-flow: row wrap;

gap: 20px;

}

main div.videos-wrapper div.video-wrapper {

flex: 0 0 calc((100% / 3) - 20px);

box-shadow: 0 1px 5px rgba(0, 0, 0, 0.15);

border-radius: 5px;

background-color: #fafafa;

}

main div.videos-wrapper div.video-wrapper video {

width: 100%;

height: 300px;

border-radius: 5px 5px 0 0;

}

main div.videos-wrapper div.video-wrapper div.video-info {

height: 100px;

padding: 10px;

}

main

div.videos-wrapper

div.video-wrapper

div.video-info

div.actions

button {

margin-right: 10px;

}

`}</style>

</div>

);

}

Code language: JavaScript (javascript)

You are free to rename index.js to index.jsx for better code autocompletion. Most of the code here is just plain old React, Javascript, HTML, and CSS. We have a few useState hooks at the top. These should be familiar to anyone who has done basic React. We also have a useCallback hook and a useEffect hook. The former creates a memoized callback function that won’t trigger unwanted re-renders and will be memorized for use across multiple re-renders. Read about it here. The latter is used to trigger side effects. In our case, we want to get all videos when the component is rendered. Read about it here. Careful with the useEffect hook, you can end up having an infinite render loop. This is why we used useCallback to memorize the function we want to run.

Additionally, we have methods that handle form submission, resource download, and deletion; handleFormSubmit, handleDownloadResource and handleDeleteResource respectively. handleFormSubmit makes a POST request to the /api/videos endpoint with our form data. handleDeleteResource makes a DELETE request to the /api/videos endpoint with the public id of the video we want to delete. The memoized function getVideos makes a GET request to the /api/videos endpoint to get all uploaded videos. The rest is just some simple HTML and CSS. Have a look at CSS-in-js support for Next.js.

That has been how to apply the boomerang effect to short videos using Cloudinary and Next.js. There’s a couple of optimizations you could do for a better result. For example, you could add logic that allows the user to only select videos of a certain length so that the videos are not too long. You can find the full code on my Github

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free