Kunwar Aditya Singh

NodeJs Lambda – use case Transcoding/Transrating Video using S3, Lambda Function and FFMPEG

By Kunwar Aditya Singh

Last updated cal_iconAugust 27, 2021

FFMPEG is a wonderful library to perform various operations on Video and Audio.
In this blog you will see how we can perform Transcoding/Transrating on a video.
You can find a step by step guide for converting a video to it’s lower quality videos, when any video is uploaded to source S3 bucket and upload result to output S3 bucket.

To use FFMPEG in lambda function you need to create a layer of FFMPEG 

You can create a layer either manually or using cloudformation.

Create FFMPEG layer manually

  • In your working directory, download and unpack the latest static release build of FFmpeg for Linux amd64 from https://johnvansickle.com/ffmpeg/ . You can find instructions in the FAQ of the page, for convenience written in the section below.

Run below command:

Wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz

wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz.md5

md5sum -c ffmpeg-release-amd64-static.tar.xz.md5

tar xvf ffmpeg-release-amd64-static.tar.xz
  • The FFmpeg binary is located in the folder “ffmpeg-4.3.2-amd64-static” (because version 4.3.2 is the latest release available at the time of this post)
  • For Lambda layer create a ZIP as follows:
mkdir -p ffmpeg/bin

cp ffmpeg-4.3.2-amd64-static/ffmpeg ffmpeg/bin/

cd ffmpeg

zip -r ../ffmpeg.zip .
  • Upload ffmpeg.zip package to S3 and create a FFMPEG layer in Lambda console by selecting the ffmpeg.zip package from S3 as shown in the below image. For this demo, we only need the “Node.js 10.x” runtime, however you can optionally add other Compatible runtimes from the drop-down list to the Layer configuration.

Create FFMPEG layer using cloudformation

  • Inside your base repository create a Makefile file and write the below code in there.
STACK_NAME ?= ffmpeg-lambda-layer

clean:
rm -rf build

build/layer/bin/ffmpeg:
mkdir -p build/layer/bin
rm -rf build/ffmpeg*
cd build && curl https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz | tar x
mv build/ffmpeg*/ffmpeg build/ffmpeg*/ffprobe build/layer/bin

build/output.yaml: template.yaml build/layer/bin/ffmpeg
aws cloudformation package –template $< –s3-bucket $(DEPLOYMENT_BUCKET) –output-template-file $@

deploy-layer: build/output.yaml
aws cloudformation deploy –template $< –stack-name $(STACK_NAME)
aws cloudformation describe-stacks –stack-name $(STACK_NAME) –query Stacks[].Outputs –output table
  • Inside your base repository create a template.yaml file and write the below code in there.
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: >
  ffmpeg layer
Resources:
  LambdaLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: ffmpeg
      Description: FFMPEG for AWS Lambda
      ContentUri: build/layer
      CompatibleRuntimes:
        – nodejs10.x
        – python3.6
        – ruby2.5
        – java8
        – go1.x
      LicenseInfo: GPL-2.0-or-later
      RetentionPolicy: Retain

Outputs:
  LayerVersion:
    Description: Layer ARN Reference
    Value: !Ref LambdaLayer

Metadata:
  AWS::ServerlessRepo::Application:
    Name: ffmpeg-lambda-layer
    Description: >
      Static build of FFmpeg/FFprobe,
      packaged as a Lambda layer. Bundles FFmpeg 4.1.3
    Author: successive
    SpdxLicenseId: GPL-2.0-or-later
    LicenseUrl: LICENSE.txt
    ReadmeUrl: README-SAR.md
    Labels: [‘layer’, ‘lambda’, ‘ffmpeg’, ‘ffprobe’]
    HomePageUrl: https://github.com/aditya-nx?tab=repositories
    SemanticVersion: 1.0.0
    SourceCodeUrl: https://github.com/aditya-nx?tab=repositories
  • Inside that repo, use command  AWS_PROFILE=<YOUR AWS PROFILE>  make deploy-layer DEPLOYMENT_BUCKET=<YOUR BUCKET NAME> 
  • This will create a ffmpeg layer.

Create Lambda function using above created layer

  • Create function as shown in below image
  • Click on Add a layer highlighted in the image
  • Select Custom layers and choose ffmpeg as custom layer which we have created earlier
  • Add environment variables to the lambda function
  • Create an index.js file in the lambda function and write the following code.
const s3Util = require(‘./s3-util’),
childProcessPromise = require(‘./child-process-promise’),
path = require(‘path’),
os = require(‘os’),
fs = require(‘fs’),
EXTENSION = process.env.EXTENSION,
THUMB_WIDTH = process.env.THUMB_WIDTH,
OUTPUT_BUCKET = process.env.OUTPUT_BUCKET,
MIME_TYPE =  process.env.MIME_TYPE;

exports.handler = async function (eventObject, context) {
const eventRecord = eventObject.Records && eventObject.Records[0],
  inputBucket = eventRecord.s3.bucket.name,
  key = eventRecord.s3.object.key,
  id = context.awsRequestId,
  resultKey = key.replace(/\.[^.]+$/, EXTENSION),
  workdir = os.tmpdir(),
  inputFile = path.join(workdir,  id + path.extname(key));

 
  let uriComponents = key.split(“/”);
  let fileName = uriComponents[uriComponents.length – 1];
  fileName = fileName.split(‘.’).slice(0, -1).join(‘.’);
  fileName = fileName.split(‘-‘).slice(0, -1).join(‘-‘);

await s3Util.downloadFileFromS3(inputBucket, key, inputFile);

//Get the current resolution of the video
const data = await childProcessPromise.spawn(
      ‘/opt/bin/ffprobe’,
      [‘-v’, ‘error’, ‘-select_streams’, ‘v:0’, ‘-show_entries’, ‘stream=width,height’, ‘-of’, ‘json’, inputFile],
      {env: process.env, cwd: workdir}
  );

let result = JSON.parse(data);
const originalHeight = result.streams[0].height;  
const resolutions = getRemainingResolution(originalHeight);

await Promise.all(resolutions.map(res => {
  let outputFile = path.join(workdir, id + res + EXTENSION);
 
  //Commands for different resolutions
  let resolutionCommands = {
      360: [‘-loglevel’, ‘error’, ‘-y’, ‘-i’, inputFile, ‘-preset’, ‘slow’, ‘-codec:a’, ‘aac’, ‘-b:a’, ‘128k’, ‘-codec:v’, ‘libx264’, ‘-pix_fmt’, ‘yuv420p’, ‘-b:v’, ‘750k’, ‘-minrate’, ‘400k’, ‘-maxrate’, ‘1000k’, ‘-bufsize’, ‘1500k’, ‘-vf’, ‘scale=-2:360’, outputFile],
      480: [‘-loglevel’, ‘error’, ‘-y’, ‘-i’, inputFile, ‘-preset’, ‘slow’, ‘-codec:a’, ‘aac’, ‘-b:a’, ‘128k’, ‘-codec:v’, ‘libx264’, ‘-pix_fmt’, ‘yuv420p’, ‘-b:v’, ‘1000k’, ‘-minrate’, ‘500k’, ‘-maxrate’, ‘2000k’, ‘-bufsize’, ‘2000k’, ‘-vf’, ‘scale=854:480’, outputFile],
      720: [‘-loglevel’, ‘error’, ‘-y’, ‘-i’, inputFile, ‘-preset’, ‘slow’, ‘-codec:a’, ‘aac’, ‘-b:a’, ‘128k’, ‘-codec:v’, ‘libx264’, ‘-pix_fmt’, ‘yuv420p’, ‘-b:v’, ‘2500k’, ‘-minrate’, ‘1500k’, ‘-maxrate’, ‘4000k’, ‘-bufsize’, ‘5000k’, ‘-vf’, ‘scale=-2:720’, outputFile],
      1080: [‘-loglevel’, ‘error’, ‘-y’, ‘-i’, inputFile, ‘-preset’, ‘slow’, ‘-codec:a’, ‘aac’, ‘-b:a’, ‘128k’, ‘-codec:v’, ‘libx264’, ‘-pix_fmt’, ‘yuv420p’, ‘-b:v’, ‘4500k’, ‘-minrate’, ‘4500k’, ‘-maxrate’, ‘9000k’, ‘-bufsize’, ‘9000k’, ‘-vf’, ‘scale=-2:1080’, outputFile],
  };
 
     
  return childProcessPromise.spawn(
          ‘/opt/bin/ffmpeg’,
          resolutionCommands[res],
          {env: process.env, cwd: workdir}
      ).then(() => s3Util.uploadFileToS3(OUTPUT_BUCKET, `${fileName}-${res}${EXTENSION}`, outputFile, MIME_TYPE));
 
}));


return true;
};

//Get all the lower resolutions of video from the current resolution of the video
const getRemainingResolution = (currentResolution) => {
const resolutions = [1080, 720, 480, 360, 240];
const index = resolutions.findIndex((value) => value <= currentResolution);
return resolutions.slice(index + 1, resolutions.length);
};
  • Create a s3-utill.js file in the lambda function and write the following code.
const aws = require(‘aws-sdk’),
  fs = require(‘fs’),
  s3 = new aws.S3(),
 
  downloadFileFromS3 = function (bucket, fileKey, filePath) {
      ‘use strict’;
      return new Promise(function (resolve, reject) {
          const file = fs.createWriteStream(filePath),
              stream = s3.getObject({
                  Bucket: bucket,
                  Key: fileKey
              }).createReadStream();
          stream.on(‘error’, reject);
          file.on(‘error’, reject);
          file.on(‘finish’, function () {
              resolve(filePath);
          });
          stream.pipe(file);
      });
  }, uploadFileToS3 = function (bucket, fileKey, filePath, contentType) {
      ‘use strict’;
      return s3.upload({
          Bucket: bucket,
          Key: fileKey,
          Body: fs.createReadStream(filePath),
          ACL: ‘private’,
          ContentType: contentType
      }).promise();
  };

module.exports = {
  downloadFileFromS3: downloadFileFromS3,
  uploadFileToS3: uploadFileToS3
};
  • Create a child-process-promise.js file in the lambda function and write the following code.
‘use strict’;
const childProcess = require(‘child_process’),
  spawnPromise = function (command, argsarray, envOptions) {
      return new Promise((resolve, reject) => {
          const childProc = childProcess.spawn(command, argsarray, envOptions || {env: process.env, cwd: process.cwd()}),
              resultBuffers = [];
          childProc.stdout.on(‘data’, buffer => {
              console.log(buffer.toString());
              resultBuffers.push(buffer);
          });
          childProc.stderr.on(‘data’, buffer => console.error(buffer.toString()));
          childProc.on(‘exit’, (code, signal) => {
              if (code || signal) {
                  reject(`${command} failed with ${code || signal}`);
              } else {
                  resolve(Buffer.concat(resultBuffers).toString().trim());
              }
          });
      });
  };
 
module.exports = {
  spawn: spawnPromise
};

Finally, Add a S3 event trigger to the lambda function using Add Trigger button on lambda function page 

  • We are using PUT since we want this event to trigger our Lambda when any new files are uploaded to our source bucket. You can add Prefix & Suffix if you need any particular type of files. Check on Enable Trigger.

You are done now. When you upload any video to the source S3 bucket, all the possible lower versions of video will be created and uploaded to the output S3 bucket.

Also Read About : Choosing between Node.js and Python for 2023

Get In Touch

How Can We Help ?

We make your product happen. Our dynamic, robust and scalable solutions help you drive value at the greatest speed in the market

We specialize in full-stack software & web app development with a key focus on JavaScript, Kubernetes and Microservices
Your path to drive 360° value starts from here
Enhance your market & geographic reach by partnering with NodeXperts