January 28, 2019     4min read

Serverless Watermark using AWS Lambda Layers & FFmpeg


Get the code for this post!

t04glovern/udemy-video-utils

Lambda Layers are a relatively new way of extending the use cases of your cloud functions. A lot of the time I find myself not able to use Lambda / Serverless patterns purely because of a dependency on a <insert_random_binary_here>. Layers are a way to get around this by letting you mount a common piece of code to the function at runtime that can be leveraged.

The Layer itself is attached to the /opt directory of your lambda. It can be referenced directly; as you'll see later we can call our layer from /opt/bin/ffmpeg. The benefit is that instead of having to bundle up our dependency with our lambda (100mb+ each time we deploy), we can inherit its functionality (deploys remain tiny).

Serverless lambda layers diagram
Serverless lambda layers diagram

Gojko Adzic's fantastic image describing this on https://serverless.pub/lambda-utility-layers/

Video Watermarking Issue

Let's talk through the problem that we're looking to solve with Lambda Layers. As you might know, I put up video on Udemy, which has proven to be a great way to distribute some of my video learnings. Recently however I've started DevOpStar and wanted to consolidate my company branding across all my videos. This meant having to re-edit all my (250+) video files to include a watermark in the bottom left corner of the screen.

Serverless Watermark example
Serverless Watermark example

Lower lefthand corner watermark

Initially I tried to do this manually in my video editing tool but quickly realised after only getting through 3 videos in 20 minutes (transcoding time on a MacBook pro 2015 kills me) that I'd have to change up my strategy. Not to mention it was really involved, required a lot of manual steps and for me to get this right 250 times without small inconsistencies was very unlikely.

FFmpeg Watermark

To start with I worked on getting the FFmpeg pipeline working on my local laptop. It did take a while to get it working; but ultimately the implementation for adding a Watermark (and also merging videos) was pretty straight forward.

You can find the code for these steps in t04glovern/udemy-video-utils.

### all of these are 10 pixel padding

## Top Left
ffmpeg -i input-video.mp4 -i watermark-icon.png \
    -filter_complex "overlay=10:10" \
    output-video.mp4

## Bottom Left
ffmpeg -i input-video.mp4 -i watermark-icon.png \
    -filter_complex "overlay=10:main_h-overlay_h-10" \
    output-video.mp4

## Top Right
ffmpeg -i input-video.mp4 -i watermark-icon.png \
    -filter_complex "overlay=main_w-overlay_w-10:10" \
    output-video.mp4

## Bottom Right
ffmpeg -i input-video.mp4 -i watermark-icon.png \
    -filter_complex "overlay=main_w-overlay_w-10:main_h-overlay_h-10" \
    output-video.mp4

## Center
ffmpeg -i input-video.mp4 -i watermark-icon.png \
    -filter_complex "overlay=main_w/2-overlay_w/2:main_h/2-overlay_h/2" \
    output-video.mp4

Slightly unrelated to this post, but there's also a method for merging two videos (adding an intro video)

ffmpeg -i intro-video.mp4 -i input-video.mp4 \
    -filter_complex "[0:v:0] [0:a:0] [1:v:0] [1:a:0] concat=unsafe=1:n=2:v=1:a=1 [v] [a]" \
    -map "[v]" -map "[a]" output-video.mp4

I then put a bunch of videos inside the input folder and then run the following bash script to add the watermark

#!/bin/bash

cd input
for line in *.mp4;
  do name=`echo $line | cut -d'.' -f1`;
  echo $line;
  echo $name;
  ffmpeg -i $line -i ../media/base/icon.png -filter_complex "overlay=10:main_h-overlay_h-10" "../output/${name}-watermarked.mp4"
done

This method worked well enough, and I was patient enough to let it process 100 or so of my videos over the course of a day. The major drawback was that my computer was basically unusable while I was running this conversion. I knew I had to come up with a better way to offload the processing to AWS.

FFmpeg Lambda Layer

I read a fantastic post by Gojko Adzic and was directed to the newly released FFmpeg implementation for Lambda Layers. Although I could just utilise the one that he had deployed I was eager to understand how it worked myself. I created my own version using his FFmpeg/FFprobe AWS Lambda layer and then heavily influenced my watermark code on his Serverless video thumbnail builder (though I had some issues with his code).

You can find my final implementation at https://github.com/t04glovern/aws-ffmpeg-serverless-convert if you would like to follow along.

The actual Lambda Layer itself is deployed over a CloudFormation template where the resource type is AWS::Lambda::LayerVersion and takes the location of an S3 bucket and S3 key (the source files for the lambda).

  LambdaLayer:
    Type: AWS::Lambda::LayerVersion
    Properties:
      CompatibleRuntimes:
        - nodejs8.10
        - python3.6
        - ruby2.5
        - java8
        - go1.x
      Description: FFMPEG for AWS Lambda
      LayerName: ffmpeg
      LicenseInfo: GPL-2.0-or-later
      Content:
        S3Bucket: DEPLOYMENT_BUCKET_NAME
        S3Key: DEPLOYMENT_KEY

We have to deploy our own version of the Layer using the Makefile in the root folder of lambda-layer/. The key parts to take note of are seen below.

You should change the DEPLOYMENT_BUCKET_NAME and DEPLOYMENT_KEY to a bucket you have control over.

BASE_NAME=ffmpeg
DEPLOYMENT_BUCKET_NAME := devopstar
DEPLOYMENT_KEY := resources/aws-ffmpeg-serverless-convert/$(shell echo $(BASE_NAME)-$$RANDOM.zip)
STACK_NAME := $(BASE_NAME)-lambda-layer

clean:
  rm -rf build

build/bin/ffmpeg:
  mkdir -p build/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/bin

build/layer.zip: build/bin/ffmpeg
  cd build && zip -r layer.zip bin

The simplicity of the layer is quite beautiful really; all we do is curl down the binary, move the binary(ies) to the build folder then zip the folder up. Then the zip is pushed to our S3 bucket, and turned into a CloudFormation stack.

To run this deployment, simply execute make deploy from the lambda-layer folder. You should receive and output containing the ARN of your brand new Lambda Layer! In my case it was arn:aws:lambda:us-east-1:XXXXXXXXXXXX:layer:ffmpeg:2.

Watermark Converter Function

Now that we have our ARN, we can deploy our watermark converter lambda function. First up, lets upload a watermark to an S3 bucket that we'll make accessible from our Lambda. Running the following (change for whatever bucket you decide to use) will achieve this.

aws s3 cp assets/icon.png s3://devopstar/resources/aws-ffmpeg-serverless-convert/base/icon.png

Jump into the video-converter folder in the project and again, in the Makefile make sure you have the DEPLOYMENT_BUCKET_NAME and DEPLOYMENT_BUCKET_PREFIX parameters setup the way you need them for your bucket.

Finally, open up the CloudFormation template in video-converter/cloudformation/template.yml and make sure:

  1. LambdaLayerArn - Points to your Lambda Layer ARN from the previous section
  2. S3ResourceBucket - Is setup to be your Bucket Name
  3. S3ResourceKeyIcon - The key to the bucket contents

Here's an example of what mine would look like

Parameters:
  LambdaLayerArn:
    Type: String
    Default: 'arn:aws:lambda:us-east-1:XXXXXXXXXXXX:layer:ffmpeg:1'
  ConversionFileType:
    Type: String
    Default: mp4
  ConversionMimeType:
    Type: String
    Default: video/mp4v-es
  S3ResourceBucket:
    Type: String
    Default: 'devopstar'
  S3ResourceKeyIcon:
    Type: String
    Default: 'resources/aws-ffmpeg-serverless-convert/base/icon.png'

Now you can simply run make deploy again (within video-converter this time). It will go through and create two buckets (yours will definitely be different based on your stack name):

  • uploads-ffmpeg-video-converter
  • results-ffmpeg-video-converter

We can now go ahead and upload our video we want watermarked to the uploads-ffmpeg-video-converter bucket, and expect the processed video to land in the results-ffmpeg-video-converter once completed. Run the following from the project root to test using the demo video (change to your bucket name).

aws s3 cp assets/demo.mp4 s3://uploads-ffmpeg-video-converter/demo.mp4

You can track the conversion process through CloudWatch logging for the lambda function. The process can take up to 5 minutes (based on the video file I included as a demo).

Serverless CloudWatch logs
Serverless CloudWatch logs

Once its complete, download your output video from the results bucket and confirm you have your watermark

Watermark example slide
Watermark example slide

FFmpeg Lambda Code

The code for the convert isn't anything super special. It can be viewed in video-converter/src/index.js. The important lines are near the bottom; we download the ICON file (watermark) from our S3 bucket, then using that we spawn a ffmpeg child process and pass in the command we used from before to convert our video. Then finally, we upload the converted file back to S3 (our results bucket).

s3Util.downloadFileFromS3(RESOURCE_BUCKET, ICON_FILE, iconFile)
    .then(() => {
        console.log('converting', inputBucket, key, 'using', inputFile);
        return s3Util.downloadFileFromS3(inputBucket, key, inputFile)
            .then(() => childProcessPromise.spawn(
                '/opt/bin/ffmpeg',
                ['-loglevel', 'error', '-i', inputFile, '-i', iconFile, '-filter_complex', 'overlay=10:main_h-overlay_h-10', watermarkerFile], {
                    env: process.env,
                    cwd: workdir
                }
            ))
            .then(() => s3Util.uploadFileToS3(OUTPUT_BUCKET, resultKey, watermarkerFile, MIME_TYPE));
    })

Conclusion

I'd be lying if I said I thought this solution was the best way to perform this sort of workload. In fact the chances of it working really well at scale aren't great at all (15 minute deadline on Lambda functions).

However It worked really well for my workload, and gave me a great excuse to learn how Lambda Layers worked.

devopstar

DevOpStar by Nathan Glover | 2020