How To Convert Video to GIF in Next.js

Ashutosh K Singh

In this media jam, we will discuss how to build a Video to GIF converter in Next.js using Cloudinary. We will use Next.js API routes to upload the video to Cloudinary and then fetch the converted GIF. We will also see how to build a download button to download the GIF and a clear button to delete the video from Cloudinary.

If you want to jump right into the code, check out the GitHub Repo here.

CodeSandbox

Setup

You will create the initial Next.js app using the create-next-app template. Run the following commands in your terminal.

1npx create-next-app video-2-gif
2cd video-2-gif
3npm run dev

The last command, npm run dev, will start the development server on http://localhost:3000/. You can stop the development server by hitting CTRL+C in the terminal.

You will use Cloudinary node.js server-side SDK in the API routes to upload and delete the video from Cloudinary. You will also need to install cloudinary-react to display GIF to the users, react-dropzone for drag & drop operation, and axios to make the requests to the API routes from the frontend.

Run the following command to install the required libraries.

1npm install cloudinary cloudinary-react react-dropzone axios

To store your Cloudinary API Keys securely, create a .env file in your root directory by running the following command.

1touch .env

Head over to your Cloudinary dashboard and copy the API Keys.

Paste the API keys in the .env file

1NEXT_PUBLIC_CLOUDINARY_CLOUD=''
2CLOUDINARY_API_KEY=''
3CLOUDINARY_API_SECRET=''

In this tutorial, we will not discuss the styling of the app. You can copy-paste the CSS used to style the app in the styles/Home.module.css file from here.

How to Upload Videos to Cloudinary

In this section, we will discuss how to upload the videos to Cloudinary using the react-dropzone library and FileReader API.

The first step is to create a dropzone in your app that only accepts videos and then read the selected video using the FileReader API.

Update the pages/index.js file with the following code.

1import React, { useState } from "react";
2import Head from "next/head";
3import { useDropzone } from "react-dropzone";
4import styles from "../styles/Home.module.css";
5import axios from "axios";
6import { Transformation, Image } from "cloudinary-react";
7
8export default function Home() {
9 const [uploadVideo, setUploadVideo] = useState("");
10 const [publicId, setPublicId] = useState("");
11 const [fileName, setFileName] = useState("");
12 const [progress, setProgress] = useState("");
13
14 const { getRootProps, getInputProps } = useDropzone({
15 accept: "video/*",
16 multiple: false,
17 onDrop: (acceptedFiles) => {
18 const file = acceptedFiles[0];
19 setFileName(file.name);
20
21 const reader = new FileReader();
22 reader.readAsDataURL(file);
23 reader.onload = () => {
24 setUploadVideo(reader.result);
25 };
26
27 reader.onerror = () => {
28 console.error("Error has Occured");
29 };
30 },
31 });
32
33 const convert = async () => {
34 setProgress("Converting Video to GIF");
35 console.log(uploadVideo)
36 };
37
38 return (
39 <div className={styles.container}>
40 <Head>
41 <title>Video 2 GIF Converter</title>
42 <link rel="icon" href="/favicon.ico" />
43 </Head>
44
45 <main className={styles.main}>
46 <h1 className={styles.title}>Video 2 GIF Converter</h1>
47
48 <div className={styles.card} {...getRootProps()}>
49 <input {...getInputProps()} />
50 {fileName ? (
51 <div>
52 <p>Selected Video: {fileName}.</p> <p>{progress}</p>
53 </div>
54 ) : (
55 <p>Drag 'n' drop some files here, or click to select files</p>
56 )}
57 </div>
58
59 <div className={styles.grid}>
60 <button className={styles.btn} onClick={convert}>Convert</button>
61 </div>
62 </main>
63 </div>
64 );
65}

In the above code, you start by importing all the packages and then initialize four different states using the useState() hook.

  • uploadVideo - for video data URL.
  • publicId - for the public_id of video, sent after a successful upload.
  • fileName - for storing the original name of the video.
  • progress - for showing users the current progress of requests.

You then define the useDropzone() hook with configuration to accept only video files one at a time. In the onDrop() parameter of useDropzone() hook, you first set the fileName state by passing the name of the file to setFileName() method and then using FileReader API's readAsDataURL() and onload() method, you store the data URL of the video to uploadVideo state. You can read more about FileReader API here.

1onDrop: (acceptedFiles) => {
2 const file = acceptedFiles[0];
3 setFileName(file.name);
4
5 const reader = new FileReader();
6 reader.readAsDataURL(file);
7 reader.onload = () => {
8 setUploadVideo(reader.result);
9 };
10
11 reader.onerror = () => {
12 console.error("Error has Occured");
13 };
14}

You also create a function named convert, which is triggered when the Convert button is clicked. Currently, it logs the uploadVideo state or the video data URL to the console, as seen below.

Now that you have converted the video to a data URL, the next step is to send the data URL to the API route to be uploaded to Cloudinary. For this, you will create a file named uploadVideo.js in the pages/api folder. Run the following command to create it.

1touch pages/api/uploadVideo.js

Add the following code to uploadVideo.js.

1// pages/api/uploadVideo.js
2var cloudinary = require("cloudinary").v2;
3
4cloudinary.config({
5 cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD,
6 api_key: process.env.CLOUDINARY_API_KEY,
7 api_secret: process.env.CLOUDINARY_API_SECRET,
8});
9
10export const config = {
11 api: {
12 bodyParser: {
13 sizeLimit: "10mb",
14 },
15 },
16};
17
18export default async (req, res) => {
19 const videoDataUrl = req.body.videoDataUrl;
20
21 let publicId = "";
22
23 await cloudinary.uploader.upload(
24 videoDataUrl,
25 { resource_type: "video", video_codec: "auto" },
26 function (error, result) {
27 if (result) {
28 publicId = result.public_id;
29 res.status(200).json(publicId);
30 }
31 if (error) {
32 console.error(error);
33 res.status(400).json(error);
34 }
35 }
36 );
37};

In the above code, you create an instance of cloudinary and pass the API keys stored in the .env file. You also limit the incoming request to ten MB, i.e., you can only upload videos less than ten MB. You can change this parameter according to your needs.

1export const config = {
2 api: {
3 bodyParser: {
4 sizeLimit: "10mb",
5 },
6 },
7};

You export an async function that extracts the data URL of the video from the request body and then use Cloudinary's upload API to upload the video to your Cloudinary account.

In the parameters of the uploader.upload() method, you specify the resource_type of the upload to video and the video_codec, to auto, which optimizes and reduces the file size. The public_id of the video is sent as a response to the frontend if the video is uploaded successfully.

The next step is to update the convert function in index.js to make the POST request to /uploadVideo route with the video's data URL in the request body. If a successful response is received, you change the progress state to "Converted to GIF Successfully".

1const convert = async () => {
2 setProgress("Converting Video to GIF");
3
4 const res = await axios.post("/api/uploadVideo", {
5 videoDataUrl: uploadVideo,
6 });
7 const data = await res.data;
8
9 setPublicId(data);
10 if (res.statusText == "OK") {
11 setProgress("Converted to GIF Successfully");
12 }
13};

The public_id of the video is stored in the publicId state using the setPublicId() method. Using the publicId state, you show users a preview of the GIF made from the video. In the index.js file, add the following code after the div with className={styles.grid}.

1{
2 publicId && (
3 <Image
4 format="gif"
5 cloudName={process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD}
6 publicId={publicId + ".gif"}
7 resourceType="video"
8 width="600"
9 height="auto"
10 >
11 <Transformation flags="lossy" />
12 <Transformation quality="auto:low" />
13 <Transformation effect="loop" />
14 </Image>
15 )
16}

You use the Image component from cloudinary-react to display the GIF. To convert a video to GIF, you need to add a .gif extension to its public_id when fetching the GIF or while displaying it as shown in the above code. You also need to pass resourceType="video" in the Image component.

Here is the entire video to GIF conversion in action.

How To Download the GIF

You have successfully converted the video to GIF, but what if the user wants to download the GIF on their system. In this section, you will create the download button to download the GIF.

First, create another button named Download in the div with className={styles.grid}.

1<div className={styles.grid}>
2 <button className={styles.btn} onClick={convert}>Convert</button>
3 <button className={styles.btn} onClick={download}>Download</button>
4</div>

Now, add the following code after the convert function to create the download function triggered when the Download button is clicked.

1const download = () => {
2 setProgress("Downloading GIF");
3
4 axios({
5 method: "get",
6 url: `https://res.cloudinary.com/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD}/video/upload/fl_lossy/q_auto:low/${publicId}.gif`,
7 responseType: "blob",
8 })
9 .then((response) => {
10 var link = document.createElement("a");
11 link.href = window.URL.createObjectURL(response.data);
12 link.download = fileName + ".gif";
13
14 document.body.appendChild(link);
15
16 link.click();
17 setProgress("GIF Downloaded");
18
19 setTimeout(function () {
20 window.URL.revokeObjectURL(link);
21 }, 200);
22 })
23 .catch((error) => {
24 console.error(error);
25 })
26};

In the above code, you start by changing the progress state to "Downloading GIF" and then make a POST request with axios to fetch the GIF from Cloudinary as a Blob object.

A blob is a file-like object of immutable, raw data, which can be read as text or binary data. This blob is passed to the URL.createObjectURL() method to create a DOMString containing a URL representing the GIF.

This URL is passed to the link element, which is nothing but an Anchor or an a element. When this link element is clicked, the resource or the GIF is downloaded on your system. After downloading the file, the URL is released using setTimeout function and URL.revokeObjectURL() method. You can read more about the URL.createObjectURL method here.

Here is the download button in action.

How To Delete the Video

You also need to delete the video from your Cloudinary account to not run out of storage. In this section, you will create a button named Clear which will delete the video from your Cloudinary account and reset the states.

Create a button named Clear after Convert and Download buttons.

1<div className={styles.grid}>
2 <button className={styles.btn} onClick={convert}>Convert</button>
3 <button className={styles.btn} onClick={download}>Download</button>
4 <button className={styles.btn} onClick={clear}>Clear</button>
5</div>

Now, create a function named clear which makes a POST request to deleteVideo API route with the public_id of the video to be deleted in the request body. This function also clears the four states to their initial value, i.e., empty strings and logs the response from the API route in the console.

1const clear = async () => {
2 await setFileName("");
3 await setProgress("");
4 await setUploadVideo("");
5 const res = await axios.post("/api/deleteVideo", {
6 publicId,
7 });
8 const data = await res.data;
9 await console.log(data);
10 await setPublicId("");
11};

Create the API route /deleteVideo by creating a file named deleteVideo.js in pages/api folder. Run the following command to create it.

1touch pages/api/deleteVideo.js

Add the following code deleteVideo.js.

1// pages/api/deleteVideo.js
2var cloudinary = require("cloudinary").v2;
3
4cloudinary.config({
5 cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD,
6 api_key: process.env.CLOUDINARY_API_KEY,
7 api_secret: process.env.CLOUDINARY_API_SECRET,
8});
9
10export default async (req, res) => {
11 const publicId = req.body.publicId;
12
13 await cloudinary.uploader
14 .destroy(publicId, { resource_type: "video" }, function (error, result) {
15 if (result) {
16 res.status(200).json("Video Deleted");
17 }
18 })
19 .catch((e) => {
20 console.error(e);
21 })
22};

The above code is similar to the uploadVideo.js file, but here you extract the public_id of the video from the request body and use the uploader.destroy() function to delete the video. You can read more about this method here.

After the successful response from Cloudinary, you send a message "Video Deleted" to the frontend.

Here is the Clear button deleting the video and resetting the state.

Conclusion

In this media jam, we saw how to build a Video to GIF converter using Next.js and Cloudinary. We also saw how to create a download button to download the GIF and a clear button to delete the video from Cloudinary.

Here are some additional resources that can be helpful:

Happy coding!

Ashutosh K Singh

JavaScript Developer

I'm a JavaScript Developer & Technical Writer. I develop awesome stuff with JavaScript and love to write about them.