Cloudinary Blog

Amplify Your Jamstack With Video

By Alex Patterson
Amplify Your Jamstack With Cloudinary Video

As defined by Amazon Web Services (AWS), Amplify is a set of products and tools with which mobile and front-end web developers can build and deploy AWS-powered, secure, and scalable full-stack apps. Also, you can efficiently configure their back ends, connect them to your app with just a few lines of code, and deploy static web apps in only three steps. Historically, because of their performance issues, managing images and videos is a daunting challenge for developers. Even though you can easily load media to an S3 bucket with AWS Amplify, transforming, compressing, and responsively delivering them is labor intensive and time consuming.

Deploy AWS app

Enter Cloudinary as an effective cloud-based media-management platform, with which you can efficiently and seamlessly create, manage, and deliver satisfactory experiences to all browsers and devices, regardless of their bandwidth. Uploading media to Cloudinary is effortless, after which it dynamically transforms them on the fly, largely heading off infrastructure- and maintenance-related concerns. Furthermore, Cloudinary offers software development kits (SDKs) for all popular programming languages.

Cloudinary SDK library

Building the Demo App

This post shows you how to build a blog, host it with Amplify, and transform its videos with Cloudinary. The code repository is on GitHub.

Creating a React App

As a prerequisite, set up an AWS account. For reference, see the Amplify documentation and this Amplify tutorial. Follow the steps below:

  1. Add Amplify to your terminal. Type: npm install -g @aws-amplify/cli
  2. Configure the new project. Type: amplify configure
  3. Create a React app as a starting point for the project. Type: npx create-react-app amplify-jamstack-cloudinary-video

    This step takes a while, after which you’ll see output on the Yarn commands you can run from the project directory. For more details, see this writeup on React toolchains.

  4. Go to the project directory and start the React app. Type:

    cd amplify-jamstack-cloudinary-video yarn start

Note
To see the demo running, run the local server with the command npm start.

When the app finishes running, React displays its rotating icon.

Adding AWS Amplify

Tip: If you cannot connect to the correct instance, go to the ~/.aws directory and verify that the credentials you entered match those listed there. For more details, see the AWS documentation on named profiles. Now turn your React app into an AWS Amplify app. First, configure your AWS account with the connections required for your app. Type: amplify init

Note
In case of no response from Amplify, type ctrl+c to ensure that you have exited your local server.

Amplify then displays the following prompts. Respond to each of them by typing Enter to select the default as shown. Below are my responses for your reference.

Copy to clipboard
? Enter a name for the project amplifyjamstackcloud
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider awscloudformation
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use (Use arrow keys)
❯ default

Note
If this is your first time you set up an AWS account profile, I suggest you pick the defaults for your settings. However, if you’re on another AWS-related project that uses the AWS SDK, you might have a separate profile already. Ensure that you specify the correct one.

Afterwards, AWS displays a confirmation:

Adding backend environment dev to AWS Amplify Console app: d2hxdxps86f74m

Go to AWS Amplify and you’ll see your new app amplifyjamstackcloud there, assuming that’s the name you specified.

AWS Amplify

Amplify Jamstack

If you don’t see your app, you might be in the wrong geographical region. To double-check, pull down the drop-down menu in the upper-left corner.

app name

Now that your app is in the cloud, click General in the left navigation and note these details:

  • The app, called d2hxdxps86f74m, is identical to the one in the console.
  • The back end of this project and its location are displayed under Backend environments (see the screenshot below). AWS Amplify leverages AWS CloudFormation under the hood, a technique known as back end as code (BaC).

Dev console

Dev Console

Setting Up the Front End

aws-amplify is the main library that works with Amplify in apps. The @aws-amplify/ui-react package contains React-specific UI components, which you’ll leverage. Install it with this command:

npm install aws-amplify @aws-amplify/ui-react

Next, have React import Amplify and configure it according to the settings created with Amplify's CLI tool, which is located in ./aws-exports. Type the following commands:

import Amplify from "aws-amplify"; import awsExports from "./aws-exports"; Amplify.configure(awsExports);

Your app is now ready to call Amplify and, inherently, the AWS SDK.

Adding a Database Layer

I suggest adopting GraphQL, which sends real-time notifications, as the database layer for your app. Feel free to use the REST API instead if you prefer.

To add a database layer, type:

amplify add api Amplify then displays several prompts. Below is my suggestion of the responses at the end of each of the prompts.

Note
To take this demo further later on, respond with multiple objects.

Copy to clipboard
? Please select from one of the below mentioned services: GraphQL
? Provide API name: amplifyjamstackcloud
? Choose the default authorization type for the API API key
? Enter a description for the API key: sample
? After how many days from now the API key should expire (1-365): 365
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)

When prompted "Do you want to edit the schema now? (y/n)," typing y opens your default editor for the schema file. I typed n and ran the command below instead, opening all the project files in VSCode.

code .

The code for the back end you’re creating resides in the amplify directory. The newly created schema, which is in amplify/backend/api/amplifyjamstackcloud/schema.graphql, reads like this:

type Todo @model { id: ID! name: String! description: String }

Replace Todo with Video and rename the file Videos.

type Video @model { id: ID! name: String! description: String }

Next, "push" the schema to the AWS cloud by typing this command: amplify push

Note
This process contains numerous steps, many of which are reflected in the amplify/backend/api/amplifyjamstackcloud/build directory. Any questions, feel free to contact me.

Amplify then prompts you to reply to the questions below. Respond with the default for each of them.

alexs-mbp-2:amplify-jamstack-cloudinary-video ajonp$ amplify push ✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

Category Resource name Operation Provider plugin
Api amplifyjamstackcloud Create awscloudformation
Copy to clipboard
? Are you sure you want to continue? Yes

The following types do not have '@auth' enabled. Consider using @auth with @model
         - Videos
Learn more about @auth here: <https://docs.amplify.aws/cli/graphql-transformer/directives#auth>

GraphQL schema compiled successfully.

Edit your schema at /Users/ajonp/web/amplify-jamstack-cloudinary-video/amplify/backend/api/amplifyjamstackcloud/schema.graphql or place .graphql files in a directory at /Users/ajonp/web/amplify-jamstack-cloudinary-video/amplify/backend/api/amplifyjamstackcloud/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js

? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
⠼ Updating resources in the cloud. This may take a few minutes . . .

After creating and executing CloudFormation in the cloud, the AWS Amplify CLI outputs two credentials and stores them in the src/aws-exports.js file:

Copy to clipboard
GraphQL endpoint: https://<example>.appsync-api.us-east-1.amazonaws.com/graphql
GraphQL API KEY: <example>

Your React app can now connect with GraphQL with the endpoint and API key.

Connecting the Front End to the API

Now create a simple app, largely according to the procedure described in the Amplify tutorial Getting Started. Type this command to start React in your browser:

npm run start

Amplify video

At the bottom of the form are the name and description you added, if any. To see the DynamoDB data and verify that the storage is local, log in to your AWS account and click the table that is displayed for your video name and description.

geographical region

Note
In case of no such display, verify that you’re in the correct geographical region.

Uploading Video

Note
To set up video on demand or streaming, follow the procedure in the tutorial AWS Amplify Video.

Upload the video for this project with Cloudinary's upload widget. Adopt the signed approach to leverage additional AWS Amplify features and better secure your Cloudinary account.

Setting Up Cloudinary

Add an upload preset in Cloudinary by first clicking the gear icon in your console and then clicking the Upload tab near the top to go to https://cloudinary.com/console/<your console>/settings/upload.

Scroll down to Upload presets and click Add upload preset. On the screen that is displayed:

  1. Choose Signed from the pull-down menu under Signing Mode.
  2. Specify a folder name (e.g., example folder) under Folder.

upload preset

Note
Note the name of the upload preset at the top and your cloud name in your Cloudinary dashboard for use later.

Dashboard

Adding the Upload Code

To add the upload code, edit your public/index.html file, as follows:

1. Add Cloudinary's upload widget by adding this script to the header section:

<script src="<https://widget.cloudinary.com/v2.0/global/all.js>" type="text/javascript"></script>

Upload script

2. Add the variable below for the upload widget. Be sure to replace my_cloud_name and my_preset with their values.

Copy to clipboard
const uploadWidget = window.cloudinary.createUploadWidget({
    cloudName: 'my_cloud_name', 
    uploadPreset: 'my_preset'}, (error, result) => { 
      if (!error && result && result.event === "success") { 
        console.log('Done! Here is the image info: ', result.info); 
      }
    }
  );

3. Add the <button> tag anywhere in the file for opening the widget. I recommend placing that code above that for the Submit button.

<button style={styles.button} onClick={addVideo}>Create Video</button>

4. Add the function below for opening the widget:

const showWidget = (uploadWidget) => { uploadWidget.open(); }

At this point, clicking the Upload Video button might trigger the error message below. That’s because you must first sign this upload for security. See the next section for the procedure.

error message

Creating a Lambda Datasource for Signing

To obtain a signature from Cloudinary, create an API call with AWS Amplify and a Lambda datasource to enable a return of the request.

First, create a function by running this command:

amplify add function

In response to the top five questions of the nine that are displayed, specify the parameters of the Lambda function (serverless function):

Copy to clipboard
`cloudinarysignature`
`cloudinarysignature`
`NodeJS`
`Hello World`

Respond with No to the bottom four questions. See the screenshot below.

terminal

Next, connect the Lamba datasource to your AppSync API by adding the line below to the amplify/backend/api/amplifyjamstackcloud/schema.graphql file:

Copy to clipboard
type Query {
  cloudinarysignature(msg: String): String @function(name: "cloudinarysignature-${env}")
}

Note
${env} in the above code makes available your development environment for use, especially if you’d like to test your code later in a staging or product environment.

Now load the updates to AWS with this command:

amplify push

AWS then displays the output below, showing the new function to be created and the GraphQL API to be updated.

graphQL api

Type Y in response to all the prompts. The process that follows takes a few minutes.

Tip: Type amplify push --y to skip the above questions in the future. Note that your stack is displayed in both the AWS console and CloudFormation. Below is an example.

Backend

For a lighter version (example below), go to Amplify, select your project, and choose backend > dev.

Backend light

Testing the New Function

After performing a push, Amplify sends you a message with your GraphQL endpoint and API key, which is also a confirmation that Amplify has updated your code, including your GraphQL values, locally. A new query called cloudinarysignature, with which you call your Lambda function through AppSync, is now in your src/graphql/queries.js file.

Add this code, which calls your GraphQL endpoint, to your App.js file:

Copy to clipboard
// eslint-disable-next-line no-unused-vars
async function fetchCloudinarySignature(cb, params) {
  try {
    const cSign = await API.graphql(graphqlOperation(cloudinarysignature, { msg: JSON.stringify(params) }));
    const data = JSON.parse(cSign.data.cloudinarysignature);
    console.log(`Uploading using key ${data.body}`);
    return data.body;
  } catch (err) {
    console.log("error fetching signature");
  }
}

Then update showWidget to call this post before opening our dialog. You could put this anywhere but I thought this would be easy.
const showWidget = () => {
    fetchCloudinarySignature();
    uploadWidget.open();
  }

Lambda then returns the "Hello from Lambda!" response you requested.

{"data":{"cloudinarysignature":"{statusCode=200, body=\\"Hello from Lambda!\\"}"}}

Updating the Function to Call Cloudinary

Note
For details on this topic, see the documentation on Cloudinary's Node.js SDK.

To update the Lambda function to call Cloudinary, follow these steps:

1. Add the Cloudinary SDK by running npm install cloudinary in the directory amplify/backend/function/cloudinarysignature/src. Tip: If you are using VSCode, just type the command and you’ll be taken to that directory.

npm install

2. Add the following code to the amplify/backend/function/cloudinarysignature/src/index.js file:

Copy to clipboard
/* eslint-disable no-unused-vars */
/* eslint-disable no-undef */

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

exports.handler = async (event) => {
  console.log(event);

  const secret = process.env.CLOUDINARY_API_SECRET;
  const response = {
    statusCode: 400,
    body: `Missing CLOUDINARY_API_SECRET`,
  };
  if (!secret) {
    return response;
  }

  const timestamp = Math.round(new Date().getTime() / 1000);
  const signature = await cloudinary.utils.api_sign_request(
    JSON.parse(event.arguments.msg),
    secret
  );

  response.body = signature;
  return JSON.stringify(response);
};

Clicking the Upload Video button now triggers the error message that your Cloudinary key is missing: {"data":{"cloudinarysignature":"{statusCode=400, body=Missing CLOUDINARY_API_SECRET}"}}

3. Get the Cloudinary key from your Cloudinary dashboard, go to the AWS Console, and click Lambda > Functions.

4. Click the cloudinarysignature-dev function on the list. Scroll down to the Environment variables section and click Edit.

Environment variables

5. In the Edit environment variables screen (see below), fill in the CLOUDINARY_API_SECRET field with your API key and click Save.

Note
Keep your API key confidential and do not put it in your code repository.

Edit Environment variables

API Key

As a test, go to your React app at http://localhost:3000/ and click Upload Video to ensure that React returns your secret key.

Updating App.js for Secret

Now the call is ready for use with your creatUploadWidget function request. Below is the complete content of the App.js file.

Copy to clipboard
/* src/App.js */
import React, { useEffect, useState } from "react";
import Amplify, { API, graphqlOperation } from "aws-amplify";
import { createVideo } from "./graphql/mutations";
import { listVideos, cloudinarysignature } from "./graphql/queries";

import awsExports from "./aws-exports";
Amplify.configure(awsExports);

const initialState = {
  name: "",
  description: "",
  cloudinary: null,
};

// eslint-disable-next-line no-unused-vars
async function fetchCloudinarySignature(cb, params) {
  try {
    const cSign = await API.graphql(
      graphqlOperation(cloudinarysignature, { msg: JSON.stringify(params) })
    );
    const data = JSON.parse(cSign.data.cloudinarysignature);
    console.log(`Uploading using key ${data.body}`);
    return data.body;
  } catch (err) {
    console.log("error fetching signature");
  }
}

const App = () => {
  const [formState, setFormState] = useState(initialState);
  const [videos, setVideos] = useState([]);

  useEffect(() => {
    fetchVideos();
  }, []);

  function setInput(key, value) {
    setFormState({ ...formState, [key]: value });
  }

  const uploadWidget = window.cloudinary.createUploadWidget(
    {
      cloudName: "ajonp",
      uploadPreset: "dxf42z9k",
    },
    (error, result) => {
      if (!error && result && result.event === "success") {
        console.log("Done! Here is the video info: ", result.info);
        setInput("cloudinary", JSON.stringify(result.info));
      }
      if (error) {
        console.log(error);
      }
    }
  );
  const showWidget = () => {
    uploadWidget.open();
  };

  async function fetchVideos() {
    try {
      const videoData = await API.graphql(graphqlOperation(listVideos));
      const videos = videoData.data.listVideos.items;
      videos.map((video) => {
        video.cloudinary = JSON.parse(video.cloudinary);
      });
      setVideos(videos);
    } catch (err) {
      console.log("error fetching videos");
    }
  }

  async function addVideo() {
    try {
      if (!formState.name || !formState.description) return;
      const video = { ...formState };
      setVideos([...videos, video]);
      setFormState(initialState);
      await API.graphql(graphqlOperation(createVideo, { input: video }));
    } catch (err) {
      console.log("error creating video:", err);
    }
  }

  return (
    <div style={styles.container}>
      <h2>Amplify Videos</h2>
      <button
        style={styles.uploadButton}
        className="cloudinary-button"
        onClick={showWidget}
      >
        Upload Video
      </button>
      <input
        onChange={(event) => setInput("name", event.target.value)}
        style={styles.input}
        value={formState.name}
        placeholder="Name"
        required
      />
      <input
        onChange={(event) => setInput("description", event.target.value)}
        style={styles.input}
        value={formState.description}
        placeholder="Description"
      />

      <button style={styles.button} onClick={addVideo}>
        Add Video to List
      </button>
      {videos.map((video, index) => (
        <div key={video.id ? video.id : index} style={styles.video}>
          <p style={styles.videoName}>{video.name}</p>
          <p style={styles.videoDescription}>{video.description}</p>
          <div style={styles.vids}>
            <video controls muted width="320" height="240">
              <source
                src={video.cloudinary.secure_url}
                type="video/mp4"
              ></source>
            </video>
          </div>
        </div>
      ))}
    </div>
  );
};

const styles = {
  vids: {
    maxWidth: "800px",
  },
  container: {
    width: 400,
    margin: "0 auto",
    display: "flex",
    flexDirection: "column",
    justifyContent: "center",
    padding: 20,
  },
  video: { marginBottom: 15 },
  input: {
    border: "none",
    backgroundColor: "#ddd",
    marginBottom: 10,
    padding: 8,
    fontSize: 18,
  },
  videoName: { fontSize: 20, fontWeight: "bold" },
  videoDescription: { marginBottom: 0 },
  button: {
    backgroundColor: "black",
    color: "white",
    outline: "none",
    fontSize: 18,
    padding: "12px 0px",
  },
  uploadButton: { margin: "22px" },
};

export default App;

Be sure to update your GraphQL by adding Cloudinary’s JSON:

Copy to clipboard
type Video @model {
  id: ID!
  name: String!
  description: String
  cloudinary: AWSJSON
}
type Query {
  cloudinarysignature(msg: String): String @function(name: "cloudinarysignature-${env}")
}

Publishing to Amplify Web Hosting

Follow these three simple steps:

1. Build your app. Type:

npm run build

2. Add the hosting capability. Type:

amplify add hosting

Respond to the two prompts that are displayed as follows:

Prompt

3. Push the content to the web. Type: amplify publish Current environment

Deployed app

That URL at the bottom, https://dev.d2hxdxps86f74m.amplifyapp.com/, is your new app. Feel free to rename it with a custom domain.

Accessing the Demo and Amplify Tutorial

Here’s a demo of the app. Also, the tutorial Getting Started With AWS Amplify is a handy reference.

Enhancing the App

A few suggestions:

  • Add the Cloudinary JavaScript loader to spotlight the actual video.
  • Add an authentication process to restrict the publishing privilege to the authorized people only.
  • Instead of uploading videos with the Cloudinary upload widget, upload to S3 and then update with the Cloudinary SDK from a Lambda trigger.
  • For a real-time feel whenever changes occur, update the list through a subscription model.

Recent Blog Posts

Our $2B Valuation

By
Blackstone Growth Invests in Cloudinary

When we started our journey in 2012, we were looking to improve our lives as developers by making it easier for us to handle the arduous tasks of handling images and videos in our code. That initial line of developer code has evolved into a full suite of media experience solutions driven by a mission that gradually revealed itself over the course of the past 10 years: help companies unleash the full potential of their media to create the most engaging visual experiences.

Read more
Direct-to-Consumer E-Commerce Requires Compelling Visual Experiences

When brands like you adopt a direct–to-consumer (DTC) e-commerce approach with no involvement of retailers or marketplaces, you gain direct and timely insight into evolving shopping behaviors. Accordingly, you can accommodate shoppers’ preferences by continually adjusting your product offering and interspersing the shopping journey with moments of excitement and intrigue. Opportunities abound for you to cultivate engaging customer relationships.

Read more
Automatically Translating Videos for an International Audience

No matter your business focus—public service, B2B integration, recruitment—multimedia, in particular video, is remarkably effective in communicating with the audience. Before, making video accessible to diverse viewers involved tasks galore, such as eliciting the service of production studios to manually dub, transcribe, and add subtitles. Those operations were costly and slow, especially for globally destined content.

Read more
Cloudinary Helps Minted Manage Its Image-Generation Pipeline at Scale

Shoppers return time and again to Minted’s global online community of independent artists and designers because they know they can count on unique, statement-making products of the highest quality there. Concurrently, the visual imagery on Minted.com must do justice to the designs into which the creators have poured their hearts and souls. For Minted’s VP of Engineering David Lien, “Because we are a premium brand, we need to ensure that every single one of our product images matches the selected configuration exactly. For example, if you pick an 18x24 art print on blue canvas, we will show that exact combination on the hero images in the PDF.”

Read more
Highlights on ImageCon 2021 and a Preview of ImageCon 2022

New year, same trend! Visual media will continue to play a monumental role in driving online conversions. To keep up with visual-experience trends and best practices, Cloudinary holds an annual conference called ImageCon, a one-of-a-kind event that helps attendees create the most engaging visual experiences possible.

Read more