Automating Video Transcoding and Transrating with S3, Lambda, and FFMPEG

Autor Name
Suresh Suresh
Read Timer

Calender

September 18, 2024

Category
Product Engineering
Automating Video Transcoding and Transrating with S3, Lambda, and FFMPEG

Video transcoding and transrating are critical processes for optimizing video content across various devices and platforms. With AWS, we can develop a seamless, serverless solution for automating these processes using S3, Lambda functions, and the FFMPEG library. FFMPEG is a powerful library for performing various video and audio operations. This blog will guide you through setting up an automated workflow that converts videos into multiple lower-resolution formats when uploaded to an S3 bucket and then storing the output in an S3 destination bucket. We will explore manual and CloudFormation-based methods for creating an FFMPEG layer and demonstrate how to integrate it with a Lambda function for efficient video processing.Let’s begin!

To leverage FFMPEG within a Lambda function, you must create an FFMPEG layer manually or via CloudFormation.

Wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xzwget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz.md5md5sum -c ffmpeg-release-amd64-static.tar.xz.md5tar xvf ffmpeg-release-amd64-static.tar.xz

mkdir -p ffmpeg/bincp ffmpeg-4.3.2-amd64-static/ffmpeg ffmpeg/bin/cd ffmpegzip -r ../ffmpeg.zip .

STACK_NAME ?= ffmpeg-lambda-layerclean:  rm -rf buildbuild/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/binbuild/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

AWSTemplateFormatVersion: 2010-09-09Transform: AWS::Serverless-2016-10-31Description: >  ffmpeg layerResources:  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: RetainOutputs:  LayerVersion:    Description: Layer ARN Reference    Value: !Ref LambdaLayerMetadata:  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

Successive Digital Playbooks for Future-Ready Businesses
Receive curated insights on enterprise modernization, engineering velocity, industry intelligence, and data-driven decision-making - delivered straight to your inbox.

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 videoconst 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 videoconst getRemainingResolution = (currentResolution) => {const resolutions = [1080, 720, 480, 360, 240];const index = resolutions.findIndex((value) => value <= currentResolution);return resolutions.slice(index + 1, resolutions.length);};

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};

‘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};

Add S3 event trigger to the lambda function using the Add Trigger button on the Lambda Function Page:

We are using PUT since we want this event to trigger our Lambda when new files are uploaded to our source bucket. You can add Prefix & Suffix if you need a particular type of file. 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.ConclusionBy using AWS S3, Lambda, and FFMPEG, you can automate the process of transcoding and transrating videos to multiple resolutions with ease. This scalable and serverless approach minimizes infrastructure management while ensuring efficient video processing. Whether you choose to implement the FFMPEG layer manually or through CloudFormation, the integration with Lambda allows for a flexible and powerful workflow. This solution is ideal for use cases requiring video optimization across different platforms or devices, offering both performance and cost benefits.

Related Blogs

Honoring our achievements in AI strategy and innovation, recognized by industry leaders for driving impactful transformation and setting new standards in consulting.

successive Advantage

We design and engineer AI-enabled solutions that elevate customer experience and help enterprises accelerate growth through scalable, technology-driven innovation.