Build a Video Streaming API with NestJS and Cloudinary

In this step-by-step tutorial, learn how to upload, generate thumbnails, and stream videos using NestJS and Cloudinary. Enhance your web applications by efficiently handling video content with this comprehensive guide on integrating NestJS with Cloudinary's powerful video management capabilities.

· 5 min read
NestJS and Cloudinary logos
Video Streaming API with NestJS and Cloudinary

In this tutorial, we will build a video uploading and streaming application using NestJS and Cloudinary. We will separate our code into a service and a controller, and use Multer for handling file uploads.

Prerequisites

  • Basic knowledge of TypeScript and NestJS
  • Node.js installed on your machine
  • A Cloudinary account. Sign up for a free account here.

Setting up the NestJS project

  1. Install the Nest CLI globally by running
npm install -g @nestjs/cli
  1. Create a new NestJS project by running
nest new video-uploading-app
  1. Change to the project directory
cd video-uploading-app

Navigate to the project folder and install the necessary packages:

npm i @nestjs/platform-express multer cloudinary @nestjs/config @types/multer

Setting up environment variables and ConfigModule
Create a .env file in the root of your project (same level as package.json) and add your Cloudinary credentials:

CLOUDINARY_CLOUD_NAME=<your-cloudinary-cloud-name>
CLOUDINARY_API_KEY=<your-cloudinary-api-key>
CLOUDINARY_API_SECRET=<your-cloudinary-api-secret>

Create a cloudinary.provider.ts file to provide the service with the Cloudinary environment variables.

import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { v2 as cloudinary } from 'cloudinary';

export const CloudinaryProvider: Provider = {
  provide: 'CLOUDINARY',
  useFactory: (configService: ConfigService) => {
    cloudinary.config({
      cloud_name: configService.get('CLOUDINARY_CLOUD_NAME'),
      api_key: configService.get('CLOUDINARY_API_KEY'),
      api_secret: configService.get('CLOUDINARY_API_SECRET'),
    });
    return cloudinary;
  },
  inject: [ConfigService],
};

Creating the videos module, controller, and service
Generate the videos module, controller, and service using the Nest CLI:

nest g mo cloudinary
nest g co cloudinary
nest g s cloudinary

Open the project in your favorite code editor.

Navigate a cloudinary.module.ts file in the src folder and configure the Cloudinary module:
update the module by importing our ConfigModule which contains  

import { Module } from '@nestjs/common';
import { CloudinaryController } from './cloudinary.controller';
import { MulterModule } from '@nestjs/platform-express';
import * as multer from 'multer';
import { CloudinaryService } from './cloudinary.service';
import { CloudinaryProvider } from 'src/cloudinary/cloudinary.provider';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    MulterModule.register({
      storage: multer.memoryStorage(),
    }),
  ],
  providers: [CloudinaryService, CloudinaryProvider],
  exports: [CloudinaryService, CloudinaryProvider],
  controllers: [CloudinaryController],
})
export class CloudinaryModule {}

we used Multer with memory storage, which stores the files in memory as Buffer objects instead of writing them to disk.
Using memory storage can be helpful in situations where you don't want to store the uploaded files on the server, but instead, want to process them and send them to another service like Cloudinary.

But, Keep in mind that using memory storage might cause your application to run out of memory when uploading very large files or a large number of small files quickly expressjs.com. If you decide to use memory storage, ensure that you have proper memory management and limits in place to prevent potential issues.

Implementing the videos controller
In the src/cloudinary/cloudinary.controller.ts file, update the controller code:

import {
  Controller, Post, UseInterceptors, UploadedFile, Get, Param, Res,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Response } from 'express';
import { CloudinaryService } from './cloudinary.service';

@Controller('videos')
export class CloudinaryController {
  constructor(private readonly cloudinaryService: CloudinaryService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  async uploadVideo(@UploadedFile() file: Express.Multer.File) {
    return this.cloudinaryService.uploadVideo(file);
  }

  @Get(':publicId/thumbnail')
  async getThumbnail(
    @Param('publicId') publicId: string,
    @Res() res: Response,
  ) {
    const thumbnailUrl = await this.cloudinaryService.getThumbnail(publicId);
    res.status(200).json({ thumbnailUrl });
  }

  @Get(':publicId/stream')
  async streamVideo(@Param('publicId') publicId: string, @Res() res: Response) {
    const videoUrl = await this.cloudinaryService.streamVideo(publicId);
    res.status(200).json({ videoUrl });
  }
}
  1. POST /videos/upload: This route is responsible for uploading a video to Cloudinary. It uses the FileInterceptor from @nestjs/platform-express to handle the file upload. The interceptor extracts the file from the request and passes it to the uploadVideo method of the CloudinaryService. The method processes the file and uploads it to Cloudinary.
  2. GET /videos/:publicId/thumbnail: This route is responsible for fetching a video thumbnail from Cloudinary. It takes the publicId of the video as a parameter and calls the getThumbnail method of the CloudinaryService. The method generates a thumbnail URL and sends it back as a JSON response.
  3. GET /videos/:publicId/stream: This route is responsible for streaming a video from Cloudinary. It takes the publicId of the video as a parameter and calls the streamVideo method of the CloudinaryService. The method generates a streaming URL and sends it back as a JSON response.

Implementing the videos service
In the src/videos/videos.service.ts file, update the service code:

import { Injectable } from '@nestjs/common';
import { v2 as cloudinary } from 'cloudinary';
import { Express } from 'express';

@Injectable()
export class CloudinaryService {
  async uploadVideo(file: Express.Multer.File) {
    return new Promise((resolve, reject) => {
      const uploadStream = cloudinary.uploader.upload_stream(
        {
          resource_type: 'video',
        },
        (error, result) => {
          if (error) {
            reject(error);
          } else {
            resolve(result);
          }
        },
      );
      uploadStream.end(file.buffer);
    });
  }

  async getThumbnail(publicId: string) {
    const thumbnailUrl = cloudinary.url(publicId, {
      resource_type: 'video',
      transformation: [
        {
          width: 300,
          height: 300,
          crop: 'fill',
        },
      ],
      format: 'png',
    });
    return thumbnailUrl;
  }

  async streamVideo(publicId: string) {
    const videoUrl = cloudinary.url(publicId, {
      resource_type: 'video',
      format: 'mp4',
      flags: 'streaming_attachment',
    });
    return videoUrl;
  }
}

In the CloudinaryService class, three methods are defined to interact with Cloudinary: uploading videos, generating video thumbnails, and streaming videos.

  1. uploadVideo(file: Express.Multer.File): This method takes a Multer file object as input and uploads it to Cloudinary as a video. It returns a Promise that resolves with the upload result. The method uses the cloudinary.uploader.upload_stream function to create an upload stream, then writes the file buffer to the stream. If the upload is successful, the Promise resolves with the result; otherwise, it rejects with an error.
  2. getThumbnail(publicId: string): This method takes the publicId of a video and generates a thumbnail URL using Cloudinary's url function. The thumbnail is resized to 300x300 pixels and returned in PNG format.
  3. streamVideo(publicId: string): This method takes the publicId of a video and generates a streaming URL using Cloudinary's url function. The video is streamed in MP4 format with the streaming_attachment flag set.

Testing the API

To test the API, start the NestJS server:

npm run start:dev

Now, use Postman to test the following endpoints:

  • POST http://localhost:3000/videos/upload: Upload a video file using the "file" form field.
upload endpoint request & response with Public ID
  • GET http://localhost:3000/videos/:publicId/thumbnail: Get a thumbnail for a video with the specified public ID.
thumbnail endpoint request & response with the thumbnail Url
  • GET http://localhost:3000/videos/:publicId/stream: Stream a video with the specified public ID.
Stream endpoint request & response with video Url

That's it! You have successfully created a video streaming API with NestJS, including video upload, transcoding, thumbnail generation, and streaming features.

You can check the project source code on Github.