Turn Your Videos into a Vlog

Ifeoma Imoh

Whether it's for sharing your knowledge or giving the world a chance to live vicariously through your experiences, vlogs have provided a more immersive and engaging medium for sharing content and unleashing the creativity of content providers.

In this article, I will show you how to build a simple vlog using React. The application will have an index view displaying all the videos and another for uploading a new video.

We will take advantage of Cloudinary's comprehensive API and easy-to-use transformation URLs to manage media resources. Not only will we be able to store resources via Cloudinary, but uploaded videos can also be automatically transcoded to all relevant formats suitable for web viewing, optimized for web browsers and mobile devices, and transformed in real-time. This makes for a seamless user experience with minimal coding. You will need a Cloudinary account for this tutorial. Create one here if you don't already have one.

For UI components in our application, we will use antd while axios will be used for uploading our videos to Cloudinary.

Here's a link to the demo on CodeSandbox.

Getting started

Create a new React app using the following command:

1npx create-react-app cloudinary_vlog_demo

Next, add the project dependencies using the following command:

1npm install antd @ant-design/icons cloudinary-react axios react-router-dom@6

We need to import the antd CSS. To do this, open your src/App.css file and the following to it:

1@import '~antd/dist/antd.css';

Next, we need to create some environment variables to hold our Cloudinary details. We will be sending images to Cloudinary via unsigned POST requests for this tutorial. To do this, we need our account cloud name and an unsigned upload preset. Create a new file called .env at the root of the project and add the following to it:

1REACT_APP_CLOUD_NAME = 'INSERT YOUR CLOUD NAME HERE'
2 REACT_APP_UPLOAD_PRESET = 'INSERT YOUR UNSIGNED UPLOAD PRESET KEY HERE'

This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env file using the following command.

1cp .env .env.local

By default, this local file is added to .gitignore and mitigates the security risk of inadvertently exposing secret credentials to the public. You can update your .env.local file with your Cloudinary cloud name and generated upload preset.

Create a new folder named util in the src' directory. This folder will hold all the helper classes we will need in our components. In the utilfolder, create a file calledcloudinaryConfig.js. This file will give access to the environment variables and prevent repeated process.env.calls throughout the project. Add the following to thecloudinaryConfig.js` file:

1export const cloudName = process.env.REACT_APP_CLOUD_NAME;
2 export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;
3 export const uploadTag = "my_cloudinary_vlog";

In addition to accessing the environment variables, we also declare a constant named uploadTag. This will be attached to the videos on upload and used to retrieve the vlog posts from Cloudinary.

Create Helper Class for API Requests

We need to make two API requests for this application: uploading a new video and retrieving the list of uploaded videos. Create a new file named api.js in the util folder and add the following to it:

1import axios from "axios";
2 import {cloudName, uploadPreset, uploadTag} from "./cloudinaryConfig";
3
4 export const getVideos = ({successCallback}) => {
5 axios
6 .get(`https://res.cloudinary.com/${cloudName}/video/list/${uploadTag}.json`)
7 .then((response) => successCallback(response.data.resources));
8 };
9
10 const formatMetadata = metadata => {
11 return Object.entries(metadata)
12 .reduce(
13 (result, [key, value]) => `${result}${result !== '' ? '|' : ''}${key}=${value}`
14 , ''
15 );
16 }
17
18 export const uploadVideo = ({file, metadata, successCallback}) => {
19 const url = `https://api.cloudinary.com/v1_1/${cloudName}/video/upload`;
20 const data = new FormData();
21 data.append("file", file);
22 data.append("upload_preset", uploadPreset);
23 data.append("context", formatMetadata(metadata));
24 data.append("tags", uploadTag);
25 axios
26 .post(url, data, {
27 headers: {
28 "Content-Type": "multipart/form-data",
29 },
30 })
31 .then(response => successCallback(response.data))
32 }

In this file, we export two functions - getVideos and uploadVideo. The getVideos function uses axios to make a GET request to Cloudinary. The resources in the JSON response are passed as a parameter to the success callback function, which allows the component perform some action once the list of resources becomes available. This is made possible by Cloudinary’s Client-side asset lists feature.

To ensure that this feature is available on your Cloudinary account, you must ensure that the Resource list option is enabled. By default, the list delivery type is restricted. To enable it, open the Security settings in your Management console, and clear the Resource list item under Restricted image types. You may want to remove this option only temporarily, as needed. Alternatively, you can bypass this (and any) delivery type restriction using a signed URL.

The uploadVideo function takes a file, some collected metadata on the video, and a callback function to be executed when the request is handled properly. Using the formatMetadata function, the metadata object is converted to the expected contextual metadata format for file uploads to Cloudinary. Essentially, an = character separates a key from its value while a | character separates key-value pairs. The file, upload preset, context metadata, and upload tag are passed in the formData request to Cloudinary. The provided success callback is used to trigger further action in the component once a successful response is received.

Create VideoUpload Component

In the src folder, create a folder called components. In the src/components folder, create a file called VideoUpload.js and add the following code to it:

1import {useState} from "react";
2 import {Button, Card, Col, Form, Input, message, Upload} from "antd";
3 import {uploadVideo} from "../util/api";
4 import {UploadOutlined} from "@ant-design/icons";
5 import {Navigate} from "react-router-dom";
6
7 const VideoUpload = () => {
8 const [isUploading, setIsUploading] = useState(false);
9 const [uploadedFile, setUploadedFile] = useState(null);
10 const [shouldRedirect, setShouldRedirect] = useState(false);
11
12 const formItemLayout = {
13 labelCol: {
14 sm: {span: 4},
15 },
16 wrapperCol: {
17 sm: {span: 18},
18 },
19 };
20
21 const onFinish = (values) => {
22 if (uploadedFile === null) {
23 message.error('You need to upload a video first');
24 } else {
25 setIsUploading(true);
26 uploadVideo({
27 file: uploadedFile,
28 metadata: values,
29 successCallback: () => {
30 setIsUploading(false);
31 setShouldRedirect(true);
32 }
33 });
34 }
35 };
36
37 const onFailedSubmission = (errorInfo) => {
38 console.log("Failed:", errorInfo);
39 };
40
41 const props = {
42 name: "file",
43 onRemove: () => {
44 setUploadedFile(null);
45 },
46 beforeUpload: (file) => {
47 setUploadedFile(file);
48 return false;
49 },
50 showUploadList: false,
51 maxCount: 1,
52 };
53
54 return (shouldRedirect ? <Navigate to='/'/> :
55 <Card style={{margin: "auto", width: "50%"}}>
56 <Form
57 {...formItemLayout}
58 onFinish={onFinish}
59 onFinishFailed={onFailedSubmission}
60 autoComplete="off"
61 >
62 <Form.Item
63 label="Title"
64 name="title"
65 rules={[{
66 required: true,
67 message: "Please provide a title for the video",
68 }]}
69 >
70 <Input/>
71 </Form.Item>
72 <Form.Item
73 name="description"
74 label="Description"
75 rules={[{
76 required: true,
77 message: 'Please provide a brief summary of the video'
78 }]}
79 >
80 <Input.TextArea showCount maxLength={1000}/>
81 </Form.Item>
82 <Col span={8} offset={9} style={{marginBottom: '10px'}}>
83 <Upload {...props}>
84 <Button icon={<UploadOutlined/>} loading={isUploading}>
85 Click to Upload Video
86 </Button>
87 </Upload>
88 </Col>
89 <Form.Item wrapperCol={{offset: 5, span: 16}}>
90 <Button
91 type="primary"
92 htmlType="submit"
93 loading={isUploading}
94 >
95 Submit
96 </Button>
97 </Form.Item>
98 </Form>
99 </Card>
100 );
101 };
102
103 export default VideoUpload;

This component renders the form that will be used to upload videos. In addition to the video file, we also make provision for the user to provide the title of the post and a brief description. For the sake of this tutorial, the post title and description are required.

The onFinish function is called when the user clicks the Submit button after filling the required fields. The function checks to ensure that a file has been uploaded before making a POST request using the uploadVideo we declared earlier. The function passed as a callback on success sets the value shouldRedirect to true. This, in turn, triggers the rendering of the Navigate component provided by React Router, which redirects the user to the index page.

By returning false in the beforeUpload key of props, we disable the default behavior of the Upload component, which is to upload the selected file to a provided URL. We also restrict the maximum number of files to 1.

Create VideoGrid Component

In the src/components folder, create a file called VideoGrid.js and add the following to it:

1import React, {useEffect, useState} from "react";
2 import {getVideos} from "../util/api";
3 import {Card, Col, Row} from "antd";
4 import {Video} from 'cloudinary-react';
5 import {cloudName} from "../util/cloudinaryConfig";
6
7 const VideoGrid = () => {
8
9 const [videos, setVideos] = useState([]);
10
11 useEffect(() => {
12 getVideos({
13 successCallback: setVideos
14 })
15 }, []);
16
17 return <>
18 <h1>Cloudinary Powered Vlog</h1>
19 <Row
20 gutter={[16, 16]}
21 justify='center'
22 align='middle'
23 >
24 {videos.map(video => {
25 const {title, description} = video.context.custom;
26 const {public_id: publicId} = video;
27 return <Col key={publicId}>
28 <Card style={{width: '600px'}} cover={
29 <Video
30 cloudName={cloudName}
31 publicId={publicId}
32 controls={true}
33 width='480'/>
34 }>
35 <Card.Meta title={title} description={description}/>
36 </Card>
37 </Col>
38 })}
39 </Row>
40 </>;
41
42 };
43
44 export default VideoGrid;

On render, we use an effect to retrieve the list of videos using the getVideos function we declared earlier. The returned resources are saved to the videos state variable. Each video is rendered using the Video component provided by cloudinary-react with the title and description rendered underneath.

Creating the Menu

In the src/components folder, create a file named Menu.js and add the following to it:

1import {Menu as AntDMenu} from 'antd';
2 import {useState} from "react";
3 import {Link} from "react-router-dom";
4 import {HomeOutlined, UploadOutlined} from "@ant-design/icons";
5
6 const Menu = () => {
7 const [currentlySelected, setCurrentlySelected] = useState('home');
8
9 const handleMenuSelection = e => {
10 setCurrentlySelected(e.key);
11 }
12
13 return (
14 <AntDMenu
15 mode='horizontal'
16 onClick={handleMenuSelection}
17 selectedKeys={[currentlySelected]}
18 >
19 <AntDMenu.Item key='home' icon={<HomeOutlined/>}>
20 <Link to='/'>Home</Link>
21 </AntDMenu.Item>
22 <AntDMenu.Item key='upload' icon={<UploadOutlined/>}>
23 <Link to='/upload'>Upload Video</Link>
24 </AntDMenu.Item>
25 </AntDMenu>
26 );
27
28 };
29
30 export default Menu;

Using the AntD Menu component, we provide a means of navigation between the home page (/) and the video upload page (/upload).

Routing Implementation

Now that we have all our components in place let’s implement the routing functionality to allow us navigate between components. In the src/util folder, create a file called routes.js and add the following to it:

1import VideoGrid from "../components/VideoGrid";
2 import VideoUpload from "../components/VideoUpload";
3
4 export default [
5 {
6 path: '/',
7 element: <VideoGrid/>
8 },
9 {
10 path: '/upload',
11 element: <VideoUpload/>
12 }
13 ];

Here we define Route Objects, which will be used by the useRoutes hook to render the <Route> component.

Next, update src/App.js to match the following code:

1import './App.css';
2 import {useRoutes} from "react-router-dom";
3 import routes from "./util/routes";
4 import Menu from "./components/Menu";
5 import {Col, Row} from "antd";
6
7 function App() {
8
9 const router = useRoutes(routes);
10
11 return <div style={{margin: "1%"}}>
12 <Menu/>
13 <div style={{textAlign: 'center'}}>
14 <Row
15 justify='center'
16 align='middle'
17 style={{textAlign: 'center'}}
18 >
19 <Col style={{width: '100%', margin: '2%'}}>
20 {router}
21 </Col>
22 </Row>
23 </div>
24 </div>
25 }
26
27 export default App;

Using the earlier declared routes object and the useRoutes hook, we render a <Route> component.

Finally, wrap the <App> component in a BrowserRouter. To do this, update src/index.js to match the following:

1import React from 'react';
2 import ReactDOM from 'react-dom';
3 import './index.css';
4 import App from './App';
5 import {BrowserRouter} from "react-router-dom";
6
7 ReactDOM.render(
8 <React.StrictMode>
9 <BrowserRouter>
10 <App/>
11 </BrowserRouter>
12 </React.StrictMode>,
13 document.getElementById('root')
14 );

With this in place, our application is complete! Run your application using this command:

1npm start

By default, the application will be available at http://localhost:3000/. The final result will look like the gif shown below.

Find the complete project here on GitHub.

Conclusion

In this article, we looked at how Cloudinary can be used to manage the media content for a vlog. One of the major advantages of using Cloudinary for delivering video content is that uploaded videos can be automatically transcoded to all relevant formats suitable for web viewing, optimized for web browsers and mobile devices, and transformed in real-time to fit your graphic design.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

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