Auto-generate Video Slideshows

Ifeoma Imoh

Build a Personal Image Slideshow

In this article, we will take advantage of a new feature provided by Cloudinary, which is video slideshow generation. We will build an application that uses a webcam to capture images and generate a slideshow video with them.

Here is a link to the demo on CodeSandbox.

Cloudinary Setup

To store and manage media using the Cloudinary platform, we need to create an account. In the account details section on your dashboard, you should see your Credentials, as shown below. In our case, we only need the Cloud name.

Since all our uploads will be done on our client application, we need to create an unsigned upload preset. See here for how to create an unsigned upload preset.

Take note of your preset name because we will need it later. As seen in the image above, I created an unsigned upload preset called testpreset, but you can use any name you like.

Cloudinary provides us with two ways to generate slideshows. In our case, we will be using the delivery URL method, and this requires a CLT template file provided by Cloudinary. This template file will be used to generate our slideshows. We need to download the template file and upload it to our Cloudinary account. See here for how to upload an asset to Cloudinary using the media library.

After a successful upload, you should see the template file appear in the list of your media files. Take note of the template file's publicID as seen above. We'll need it later when generating URLs for our slideshows.

Project Setup

Run this command in your terminal to create a simple React application:

1npx create-react-app my-slideshow-app

Next, run the following command in your terminal to install the dependencies we'll need for this project:

1npm i axios react-webcam

The axios module will be the HTTP client, while react-webcam will be used to access the user’s camera to capture images.

Breaking Down Our Application

The essence of our application is to display a slideshow containing some images. These are the pieces that make up our app, along with their fundamental roles:

  • Camera: This represents our image source.
  • Image Previewer: Used to display and delete images.
  • SlideShow Previewer: Used to display the slideshow, which is just a regular video.

Capturing and Displaying Images

Create a folder called components in your src directory. Create a file called WebCamera.js inside the components folder and add the following to it:

1import Webcam from "react-webcam";
2 import { useRef } from "react";
3
4 const WebCamera = ({ onCapture, loading }) => {
5 const capture = async () => {
6 // get screenshot
7 const image = webCamRef.current.getScreenshot();
8 onCapture(image);
9 };
10 const webCamRef = useRef();
11 const videoConstraints = {
12 width: 500,
13 height: 400,
14 facingMode: "user",
15 };
16 return (
17 <article className="media_box">
18 {/* web cam */}
19 <Webcam
20 audio={false}
21 height={400}
22 ref={webCamRef}
23 screenshotFormat="image/jpeg"
24 width={600}
25 videoConstraints={videoConstraints}
26 />
27 <button
28 disabled={loading}
29 onClick={capture}
30 className={"capture_btn"}
31 ></button>
32 </article>
33 );
34 };
35 export default WebCamera

In the code above, the Webcamera component expects two props. The onCapure prop will be used to return a captured image to the parent component that renders it, and the loading prop will be used to toggle the loading states of some sort internally. The component starts by creating a function called capture accompanied by a reference variable and an object that defines the specifications we want on the video track. Here, we specified that the video should have a resolution of 600 by 400 and that it should be streamed from the front camera.

Its return statement renders the Webcam component, which accepts the reference variable, the video constraints, and the format for the captured image as props.

A notification will be displayed requesting permission to access the front camera. If the user accepts, it binds some methods to the reference variable passed, one of which is the getScreenshot() function used to take a picture and then feed the resulting base64 encoded string representing the image to the onCapture prop. The capture function is triggered by a button that is disabled when loading to prevent the user from taking more pictures.

Next, we need to create a component that will be used to display captured images. In your components folder, create a file named ImagePreviewer.js and add the following to it:

1const ImagePreviewer = ({ url, del }) => {
2 return (
3 <figure className="single_img">
4 <img src={url} alt={`captures`} />
5 <button className="btn_red" onClick={del}>
6 Delete
7 </button>
8 </figure>
9 );
10 };
11 export default ImagePreviewer;

This component renders an image using the URL it accepts as props and a button that deletes the image when clicked. Now that we have our WebCamera and ImagePreviewer components set up, we can use them in our App.js file. Replace the code in your App.js file with the following:

1import { useState } from "react";
2 import WebCamera from "./components/WebCamera";
3 import ImagePreviewer from "./components/ImagePreviewer";
4 import "./App.css";
5 export default function App() {
6 const [loading, setLoading] = useState(false);
7 const [images, setImages] = useState([]);
8 const onCapture = (newImage) =>
9 setImages((prevImages) => [...prevImages, newImage]);
10 const deleteImage = (ind) =>
11 setImages((prevImages) => images.filter((_, index) => index !== ind));
12 return (
13 <main>
14 <section className="main con">
15 <WebCamera onCapture={onCapture} loading={loading} />
16 </section>
17 <div></div>
18 <section className="captured_imags_con con">
19 {images.map((imgURL, index) => (
20 <ImagePreviewer
21 url={imgURL}
22 key={index}
23 del={() => deleteImage(index)}
24 />
25 ))}
26 </section>
27 </main>
28 );
29 }

This Component starts by creating two state variables, one to manage the loading state, and the other is an array to store the captured images. We also have two functions that update and delete images from the array.

It then returns the WebCam component, which receives the required props. We also iterate over the array of images and display them on the screen.

Start your development server and head over to your browser to see how our application currently looks.

1npm start

Requesting for the Slideshow

Our app currently allows us to capture, view, and delete images. Next, we need to combine these images and generate a slideshow. Irrespective of the method chosen to generate slideShows, every Slideshow created using Cloudinary consists of the following components:

  • Template: this is the template file provided by Cloudinary that we uploaded earlier.
  • Manifest transformation: refers to the global Slideshow settings, global slide Settings, and ****individual slide settings(the images or videos you want to include in the slideshow and their settings).
  • Global transformations: the visual enhancements, filters, and effects you wish to apply to the resulting slideshow.

In this post, we will create our slideshows using the Delivery URL syntax, which combines the components mentioned above to give the following syntax.

From the diagram above, we can see the part of the URL where the settings would live, but most importantly, we see where our captured images will live. We cannot directly embed the base64 encoded version of our captured images in this URL. We need to upload them first, get their publicIDs, and embed them in the URL.

Let's create a file that will hold some utility functions. Create a file named helpers.js inside your src folder and add the following to it:

1import axios from "axios";
2
3 const cloudName = "ifeomaimoh";
4 const upload = async (imgFileB64) => {
5 const imageData = new FormData();
6 imageData.append("file", imgFileB64);
7 imageData.append("upload_preset", "testpreset");
8 const res = await axios.post(
9 ` https://api.cloudinary.com/v1_1/${cloudName}/image/upload`,
10 imageData
11 );
12 const imageDetails = res.data;
13 return imageDetails.public_id;
14 };
15
16 const genDeliveryURL = (arrOfAsetIds) => {
17 const templateID = "slideshow_i2o0w9_a1hyoq";
18 const globalSettings = `w_500;h_500;du_10`;
19 const slideSettings = `tdur_1500;transition_s:InvertedPageCurl`;
20 const individualSlides = arrOfAsetIds
21 .map((id) => "(media_i:" + id + ")")
22 .join(";");
23 return `https://res.cloudinary.com/${cloudName}/video/upload/fn_render:${globalSettings};vars_(${slideSettings};slides_(${individualSlides}))/${templateID}.mp4`;
24 };
25 export { upload, genDeliveryURL };

In the code above, we start by importing the axios module, and then we define a variable that holds our Cloud name. The upload function accepts a base64 encoded string representing an image as an input. It uses the input file and the upload preset we created earlier to create the request body. It then makes a request to the API to upload the image. If the request is successful, it returns the public ID of the uploaded image.

The genDeliveryURL function returns a URL for the slideshow. It accepts an array containing the public IDs of images. It creates several variables that match the components of the slideshow. Most of the settings specified are based on preferences.

First, we stored the publicID of our CLT template. For global settings, we specified the dimensions of the video to be 500 by 500, and it should last for a duration that is twice the number of images provided (i.e., two images would take 4s). We specified 1500ms as the duration for each slide transition for the global slide settings.

Finally, we created a string that contains the media assets that would be included in the slideshow. Click here to see a list of the available settings that can be applied to a slideshow.

Create a file called SlideShowPreviewer.js inside your components directory and add the following to it.

1import { useRef, useState, useEffect } from "react";
2 import axios from "axios";
3 const SlideShowPreviewer = ({ url }) => {
4 const ref = useRef();
5 const [retry, SetRetry] = useState(1);
6 const [canView, setCanView] = useState(false);
7 const [URL, setURL] = useState("");
8 let loadingText = (
9 <p>
10 Requesting slideshow please wait...{retry}{" "}
11 {retry > 1 ? "retries" : "retry"} so far
12 </p>
13 );
14 async function tryToGetSlideShow(URL) {
15 return new Promise(async (res, rej) => {
16 try {
17 const response = await axios(URL);
18 console.log({ response });
19 setURL(URL);
20 setCanView(true);
21 } catch (error) {
22 SetRetry(retry + 1);
23 }
24 });
25 }
26 useEffect(() => setCanView(false), [url]);
27 useEffect(() => {
28 if (!canView) ref.current = setTimeout(() => tryToGetSlideShow(url), 7000);
29 return () => clearTimeout(ref.current);
30 }, [retry, canView, url]);
31 return (
32 <article className="slide_box">
33 {canView || URL ? (
34 <>
35 <video src={URL} autoPlay muted loop controls />
36 {!canView && loadingText}
37 </>
38 ) : (
39 loadingText
40 )}
41 </article>
42 );
43 };
44 export default SlideShowPreviewer;

As you can see, the slideshows are generated asynchronously. This means that even if we generate the delivery URL for the slideshows and make a request for it, it won't be available immediately. The generation of the slideshow may take some time depending on the number of media assets, the transformations used, etc.

In the code above, the component expects the delivery URL as props. We defined some state variables to track if the slideshow can be viewed or not and the number of unsuccessful attempts to get the slideshow. In the effect hook, the component repeatedly requests the slideshow every 7 seconds. If the request is successful, it toggles the state to indicate that the slideshow is ready and feeds the delivery URL to an HTML video element, but if it fails, it retries.

Let's import and use the helper functions and SlideShowPreviewer component in our App component. Update your App.js file with the following:

1import { useState } from "react";
2 import WebCamera from "./components/WebCamera";
3 import ImagePreviewer from "./components/ImagePreviewer";
4 import SlideShowPreviewer from "./components/SlideShowPreviewer";
5 import { upload, genDeliveryURL } from "./helpers";
6 import "./App.css";
7 export default function App() {
8 const [loading, setLoading] = useState(false);
9 const [deliveryURL, setDeliveryURL] = useState("");
10 const [images, setImages] = useState([]);
11 const onCapture = (newImage) =>
12 setImages((prevImages) => [...prevImages, newImage]);
13 const deleteImage = (ind) =>
14 setImages((prevImages) => images.filter((_, index) => index !== ind));
15 const uploadMultipleImages = async (images) => {
16 let arrOfImageIds = [];
17 for (const image of images) {
18 arrOfImageIds.push(await upload(image));
19 }
20 return arrOfImageIds;
21 };
22 const buildSlideShow = async () => {
23 try {
24 setLoading(true);
25 const uploadedImgsPublicIds = await uploadMultipleImages(images);
26 const deliveryURL = genDeliveryURL(uploadedImgsPublicIds);
27 setDeliveryURL(deliveryURL);
28 setImages([]);
29 } catch (error) {
30 console.log(error);
31 } finally {
32 setLoading(false);
33 }
34 };
35 return (
36 <main>
37 <section className="main con">
38 <WebCamera onCapture={onCapture} loading={loading} />
39 {deliveryURL && <SlideShowPreviewer url={deliveryURL} />}
40 </section>
41 {images.length >= 2 && (
42 <div>
43 <button onClick={buildSlideShow} className="create_slide_btn">
44 {loading ? "processing" : "generate slideshow"}
45 </button>
46 </div>
47 )}
48 <section className="captured_imags_con con">
49 {images.map((imgURL, index) => (
50 <ImagePreviewer
51 url={imgURL}
52 key={index}
53 del={() => deleteImage(index)}
54 />
55 ))}
56 </section>
57 </main>
58 );
59 }

In the return statement for this component, we added a button that will only be displayed when we have captured at least three images. This button triggers a function called buildSlideShow. This function first attempts to upload the captured images. Then it generates the URL for the slideshow using the publicIDs returned from the uploadMultipleImages function and stores it in the state. We also passed the URL to the SlideShowPreviewer as props. If we run our app now, we should see the resulting slideshow as expected.

You can find the complete project here on Github.

Conclusion

Today, there are several use-cases for slideshows, and with solutions like Cloudinary, we can easily combine assets to create auto-generated slideshows that match the needs of our projects.

Resources you may find useful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.