Video Streaming the Jamstack way

Eugene Musebe

BACKGROUND

The popularity and use of video as a service (VAAS) platforms like Netflix, Tiktok, Snapchat, Instagram Live, and others, have risen in recent years. In this case, marketing agencies, and departments, are making extensive use of these platforms to reach their target audiences via paid advertisements.

In this MediaJam, we'll build a Next.js video streaming service that uses Cloudinary's video and image APIs, and Airtable as a database, as illustrated in the CodeSandbox below.

N/B To Test video uploads, CodeSandbox and Vercel supports videos not more than 5 megabytes. Therefore, uploading larger files will result in error code 413.

You can find a complete version of the application on Codesandbox.

PREREQUISITES

To get started, a basic understanding of JavaScript and React is required.

Ensure that you have created Cloudinary and Airtable accounts since they are the core platforms that this project will rely on.

Environment Setup

We will use Next.js, an open-source React framework, to build the application.

Navigate to your project's directory, and run:

1npx create-next-app jamflix

After the installation is complete, follow the instructions on your terminal to start the development server.

Cloudinary setup

After you've created a Cloudinary account, you'll need to:

  • Click the settings icon on your Cloudinary dashboard's navigation bar.
  • Scroll down to the upload presets section, and click on the 'Add upload preset' link.
  • Give your preset a name, and make the signing mode 'Unsigned'.
  • On the Folder form, name the folder where you want your application images to be stored. In our case, we named the folder 'Jamflix'.
  • Click on the Save button, and the setup will be complete.

Airtable

We'll use Airtable, an easy-to-use relational database, to store the uploaded and transformed images and videos. On the platform, create a base (database) with the following structure:

Server setup

To get the backend up and running, we'll need to add the Airtable and Cloudinary packages to the project, which we'll do with the following command:

1npm I cloudinary airtable fs formidable swr

Create a .env.local file in your root project structure. Then, go to your Cloudinary and Airtable dashboards, and fill in the values for each of the following keys:

1AIRTABLE_API_KEY=
2AIRTABLE_BASE_ID=
3AIRTABLE_TABLE_NAME=
4CLOUDINARY_CLOUD_NAME=
5CLOUDINARY_API_KEY=
6CLOUDINARY_API_SECRET=
7CLOUDINARY_UPLOAD_PRESET=

Then, create a utils folder in the root project structure of your application. Add cloudinary.js and airtable.js files in it to interact with the images stored in Cloudinary and Airtable from the application.

Cloudinary's image and video upload API takes the same structure, with an exception for the parameters passed in the functions to manipulate the different media sources.

To begin uploading and retrieving images from Cloudinary, create a Cloudinary access configuration in the utils folder of the Cloudinary file:

1import cloudinary from 'cloudinary';
2const fs = require('fs');
3cloudinary.config({
4 cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
5 api_key: process.env.CLOUDINARY_API_KEY,
6 api_secret: process.env.CLOUDINARY_API_SECRET,
7});

Then, create functions to upload the video and its thumbnails, as highlighted below:

1export async function postVideos(file) {
2 const video = await cloudinary.v2.uploader.upload(file, {
3 resource_type: 'video',
4 upload_preset: process.env.CLOUDINARY_UPLOAD_PRESET,
5 });
6 fs.unlinkSync(file)
7 return video;
8}
1export async function postThumbnail(file) {
2 const thumbnail = await cloudinary.v2.uploader.upload(file, {
3 upload_preset: process.env.CLOUDINARY_UPLOAD_PRESET,
4 });
5 fs.unlinkSync(file)
6
7 return thumbnail;
8}

To save storage space, once the thumbnail and video are uploaded to Cloudinary, we use the fs.unlinkSync function to remove them from the application.

The next step is to save the generated callback of url data and tags to Airtable once the thumbnail and video have been uploaded. Create a configuration and function in the airtable.js file to perform this operation, as shown below:

1import Airtable from 'airtable';
2
3Airtable.configure({
4 apiKey: process.env.AIRTABLE_API_KEY,
5});
6
7const base = Airtable.base(process.env.AIRTABLE_BASE_ID);
8const table = base(process.env.AIRTABLE_TABLE_NAME);

To post the generated records to Airtable, create the following function:

1export async function postVideo(obj) {
2 const { Imgid, Name, Tag, Videolink, Thumbnail } = obj;
3
4 const record = await table.create({
5 Imgid,
6 Name,
7 Tag,
8 Videolink,
9 Thumbnail,
10 });
11
12 return record;
13}

In order to get all the uploaded records, one can use the function:

1export async function getAllVideos() {
2 const records = await table.select({}).firstPage();
3 const formattedRecords = records
4 .map((record) => ({
5 id: record.id,
6 ...record.fields,
7 }))
8 .filter((record) => !!record.Imgid);
9 return formattedRecords;
10}

Out of the box, Next.js allows you to write server-side code rendering. To accomplish this, go to the page's directory and create an API folder. Create a cloudinary.js file inside it and paste the following code:

1const { getAllVideos, postVideo } = require('../../utils/airtable');
2
3async function handler(req, res) {
4 if (req.method === 'GET') {
5 try {
6 const formattedRecords = await getAllVideos();
7 res.status(200).json(formattedRecords);
8 } catch (error) {
9 res.status(500).json({ message: 'Failed fetching videos.' });
10 }
11 }
12}

Above, we exported the Airtable utility functions we created earlier, and then created a GET method to pull all of the stored records in Airtable.

Then create a POST method that will enable the records created to be stored to Airtable :

1else if (req.method === 'POST') {
2 try {
3
4 const record = await postVideo(req.body);
5 res.status(200).json(record);
6 } catch (error) {
7 res.status(500).json({ message: 'Failed posting video to airtable.' });
8 }
9 }

To finish the backend API, we'll need to create API routes in the pages folder that will allow us to upload videos and their thumbnails to Cloudinary, and then save the generated callbacks data in Airtable.

In the pages folder, create a video.js file, since we'll be using Formidable to upload the files (Media). We'll need to import it first, as well as the Cloudinary utilities.

1import { postVideos } from '../../utils/cloudinary';
2import formidable from 'formidable';

Then, write the POST method handler that follows:

1async function handler(req, res) {
2 if (req.method === 'POST') {
3 try {
4 const form = new formidable.IncomingForm();
5 form.keepExtensions = true;
6 form.parse(req, async (err, fields, files) => {
7 const video = await postVideos(files['video'].path);
8 res.status(200).json(video);
9 });
10 } catch (error) {
11 res.status(500).json({ message: error });
12 }
13 }
14}

For thumbnail uploads, create a thumbnail.js file in the API folder and emulate the content in the video.js file. In this instance, you will replace the postVideo imports and functions to postThumbnail since videos and thumbnails have different presets applied to them as displayed below:

1import {postThumbnail } from '../../utils/cloudinary';

Post Thumbnail & Video

To begin uploading video trailers and thumbnails to Cloudinary and store the records created in Airtable, create a Components folder on the project's root project structure. Also, create a UploadForm.js file.

In the UploadForm.js file, create a functional component and import the useState function:

1const [file, setFile] = useState('');

This will be used to hold the image file path before initiating the post request to the thumbnail API we created before.

Add the following code block to the return statement, which will be used to consume the state variables declared above in the upload form.

1<label>Cover photo</label>
2<input
3 type='file'
4 className='custom-file-input'
5 id='customFile'
6 required
7 onChange={(e) => {
8 setFile(e.target.files[0]);
9 }}
10/>

To append the file stored in the state upon performing an upload, we will create a formData variable, and attach the selected file to it as shown below:

1const formData = new FormData();
2 formData.append('cover_photo', file);

In order to upload videos to the video API, add a useState variable that will hold the video to the state, and set its values to:

1const [file_2, setFile_2] = useState('');

Then, emulate the thumbnail upload form code by changing the 'onChange' function to setFile_2(e.target.files[0]).

When a POST request is made after selecting a thumbnail and its videos, we will store the returned callback data in Airtable by making the following post request:

1axios.post('/api/airtable', {
2 Imgid:res_2.data.asset_id,
3 Name: name,
4 Tag: tag,
5 Videolink: res_2.data.url,
6 Thumbnail: res.data.url,
7 })
8 .then(function (response) {
9 console.log(response);
10 window.location.reload(false);
11 });

The movie name, category, video link URL, and thumbnail URL will all be sent to Airtable as a single movie object.

Display Video

Create a functional Movies.js file in the pages folder and import useState, useEffect, and useSWR at the top to display the uploaded videos.

To fetch data from Airtable, we'll use the Swr hook to cache previously fetched data, allowing the application to provide a better user experience by only fetching new data on page reload.

1const { data, error } = useSWR('/api/airtable');

Once the cached data is fetched, we will pass it down to the component by fetching it, using the useEffect hook on page reload.

1useEffect(async () => {
2 await setMovies(data);
3 }, [data]);

We'll use Next.js' lifecycle method 'getStaticProps' to render the data to the users. We'll be able to generate all of the videos and thumbnails during the build process, allowing us to serve static data to the end-users.

The following codeblock will be used to accomplish this:

1export async function getStaticProps() {
2 const response = await fetch('/api/airtable');
3 const data = await response.json();
4 return { props: { movies: data } };
5}

Map through the received data and display the returned parameters as shown below to display the fetched data:

1movies.map((movie, id) => {
2 return (
3 <div
4 key={id}
5 className='Item'
6 onClick={() => {
7 setVideoUrl(movie.Videolink);
8 }}
9 style={{
10 backgroundImage: `url(` + movie.Thumbnail + `)`,
11 margin: 5,
12 }}
13 >
14 <div className='overlay'>
15 <div className='title'>{movie.Name}</div>
16 <div className='rating'>{movie.Tag}</div>
17 </div>
18 </div>
19 );
20 })}

Conclusion

With the above MVP, more features such as automatic subtitles, video markers to help users navigate to a specific video section, and much more, can be added using the Cloudinary video API.

Visit the following website for further information:

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.