FFmpeg をカスタムレイヤーとして Lambda 上で動かしてみた

FFmpegをLambdaで動かしたい時、ありませんか
2020.12.23

こんにちは、大前です。

AWS Media Blog に最近投稿された以下ブログにある、FFmpeg を Lambda レイヤーとして使用する方法が気になったので、実際にやってみました。

Lambda レイヤーってなんぞ?って方は下記ブログを参照ください。


動画を扱うワークフローで必要となる機能は、マネージドサービスとして AWS Elemental MediaLive や AWS Elemental MediaConvert 等が提供されています。

一方で、上記では実現できない要件がある場合などは FFmpeg の使用が必要となってくるケースも考えられます。

FFmpeg を Lambda で動かすブログは既に世に色々出ていますが、個人的に実際に動かした経験がなかったこともあり、ブログとして書き留めて行きたいと思います。


では、早速行きましょう。

やってみた

前準備

前準備として、以下のリソースを準備する必要があります。

  • S3 バケット
    • FFmpeg で扱う動画ファイルのの保管先
    • Lambda レイヤーのパッケージ保管先
  • IAM ロール
    • Lambda 関数用

S3 バケット

今回は動画ファイルのアップロードを契機に Lambda 関数を実行させるため、動画アップロード用の S3 バケットが必要です。

既存のバケットがあればそれを使っても良いですし、新規に作成しても良いです。

FFmpeg で行なった処理の出力結果を別途保管したい場合などはバケットを複数用意してください。


また、Lambda レイヤーとして使用する FFmpeg パッケージを保管する S3 バケットも必要であるため、用意しておきましょう。(直接 zip ファイルをアップロードしても良いですが、FFmpeg はそこそこファイルサイズがあるため、S3 経由でアップロードするのが良いでしょう)

IAM ロール

Lambda 関数にアタッチする IAM ロールを作成しておきます。

S3 への操作が必要となるため、必要な権限をアタッチしてあげます。

今回は example-bucket(バケット名は仮) からファイルを取得し、FFmpeg の処理結果についても同じバケット配下に格納するため、以下ポリシーをアタッチしました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::example-bucket/*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::example-bucket/*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": [
                "logs:PutLogEvents",
                "logs:CreateLogStream",
                "logs:CreateLogGroup"
            ],
            "Resource": "*"
        }
    ]
}


これにて前準備は以上です。Lambda 等の設定に移りましょう。


Lambda レイヤー作成

まず、FFmpeg を Lambda レイヤーとして登録していきます。

ローカル環境で以下コマンドを実行し、FFmpeg が含まれた zip パッケージを作成します。

$ wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
$ tar xvf ffmpeg-release-amd64-static.tar.xz
$ mkdir -p ffmpeg/bin
$ cp ffmpeg-4.3.1-amd64-static/ffmpeg ffmpeg/bin/
$ cd ffmpeg
$ zip -r ../ffmpeg.zip .

上記にて作成された zip ファイルを、Lambda レイヤーのパッケージ保管用 S3 バケットにアップロードしておきます。


続いて、AWS マネジメントコンソールから Lambda → レイヤー → レイヤーの作成 を選択し、新規レイヤーを作成します。

S3 のリンク URL には、先ほどアップロードした zip ファイルの URL を記載してください。

ランタイムは Python3.8 としました。


作成をクリックし、レイヤーが作成されたら OK です。

Lambda 関数作成

最後に、Lambda 関数を作成します。

ランタイムは Python3.8、IAM ロールには事前に作成しておいたものをアタッチして作成します。

トリガーの設定

Lambda 関数を作成したら、トリガーの設定をします。

「トリガーを追加」ボタンより以下画面を開き、各項目を設定します。

今回は、対象 S3 バケットの upload/ 配下に .mp4 ファイルがアップロードされた場合に Lambda 関数が起動されるようにしました。

プレフィックス等は必要に応じて改修してください。

  • トリガーリソース ... S3
  • バケット ... 事前に用意した動画アップロード用 S3 バケット
  • イベントタイプ ... すべてのオブジェクト作成イベント
  • プレフィックス ... upload/
  • サフィックス ... .mp4

レイヤーの追加

続いて、先ほど作成した FFmpeg カスタムレイヤーを追加します。

"カスタムレイヤー" を選択し、先ほど作成したカスタムレイヤーを選択します。

「追加」ボタンをクリックし、レイヤーが追加されていれば OK です。

ソースコードの作成

最後に、Lambda 関数のソースコードとして以下を作成しました。

import json
import os
import subprocess
import shlex
import boto3

S3_DESTINATION_BUCKET = "example-bucket"
SIGNED_URL_TIMEOUT = 60

def lambda_handler(event, context):

    s3_source_bucket = event['Records'][0]['s3']['bucket']['name']
    s3_source_key = event['Records'][0]['s3']['object']['key']

    s3_source_basename = os.path.splitext(os.path.basename(s3_source_key))[0]
    s3_destination_filename = "thumbnail/" + s3_source_basename + "_thumbnail.jpg"

    s3_client = boto3.client('s3')
    s3_source_signed_url = s3_client.generate_presigned_url('get_object',
        Params={'Bucket': s3_source_bucket, 'Key': s3_source_key},
        ExpiresIn=SIGNED_URL_TIMEOUT)

    ffmpeg_cmd = "/opt/bin/ffmpeg -i \"" + s3_source_signed_url + "\" -f image2 -ss 1 -vframes 1 -s 1280x720 -"
    command1 = shlex.split(ffmpeg_cmd)
    p1 = subprocess.run(command1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    resp = s3_client.put_object(Body=p1.stdout, Bucket=S3_DESTINATION_BUCKET, Key=s3_destination_filename)

    return {
        'statusCode': 200,
        'body': json.dumps('Processing complete successfully')
    }


上述した AWS 公式ブログのソースをほぼ使っている形ではありますが、中身を軽く説明したいと思います。


トリガーイベントからファイルがアップロードされたバケット名とオブジェクト名を取得します。

    s3_source_bucket = event['Records'][0]['s3']['bucket']['name']
    s3_source_key = event['Records'][0]['s3']['object']['key']


FFmpeg の処理結果を出力する際のファイル名(S3 バケットのキー含む)を設定します。

今回は、ファイルをアップロードしたバケットの thumbnail フォルダ配下に結果が出力される様にしています。

    s3_source_basename = os.path.splitext(os.path.basename(s3_source_key))[0]
    s3_destination_filename = "thumbnail/" + s3_source_basename + "_thumbnail.jpg"


アップロードされたファイルへの署名付き URL を生成します。

S3 の署名付き URL が何者なのかについては以下ブログなどを参照ください。

SIGNED_URL_TIMEOUT = 60
(...)
    s3_client = boto3.client('s3')
    s3_source_signed_url = s3_client.generate_presigned_url('get_object',
        Params={'Bucket': s3_source_bucket, 'Key': s3_source_key},
        ExpiresIn=SIGNED_URL_TIMEOUT)


FFmpeg のコマンドを実行し、結果を指定した S3 バケットに出力します。

ffmpeg_cmn = 〜 部分を実行したいコマンドに変更することで、好きな処理をさせることが可能です。

今回はアップロードされた動画の 1秒時点でのフレームを 720p のサムネイル画像として出力するコマンドを実行しています。そのため、問題なく処理が実行された場合は対象の S3 バケットに画像ファイルが生成されるはずです。

また、コマンドの実行を subprocess.run で行なっています。こちらについては私もあまり詳しくないのですが、ブログがありましたので以下を参照ください。

    ffmpeg_cmd = "/opt/bin/ffmpeg -i \"" + s3_source_signed_url + "\" -f image2 -ss 1 -vframes 1 -s 1280x720 -"
    command1 = shlex.split(ffmpeg_cmd)
    p1 = subprocess.run(command1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    resp = s3_client.put_object(Body=p1.stdout, Bucket=S3_DESTINATION_BUCKET, Key=s3_destination_filename)

(備考)タイムアウト、メモリの設定

FFmpeg で実行する処理によっては、Lambda 関数のデフォルト設定ではタイムアウトしてしまう可能性があります。

実行する処理に応じて、適切なタイムアウトやメモリを設定する様にしましょう。(今回はメモリ 1024MB、タイムアウト 1分としました)

動かしてみる

ここまでで各種リソースの設定は以上です。

それでは、実際に作成した仕組みを動かしてみましょう。

アップロード先として用意した S3 バケットに mp4 ファイルをアップロードします。

アップロード後少し待つと、Lambda 関数内で指定した出力先に FFmpeg の実行結果が出力されていることが確認できました!(今回だとサムネイル画像ファイル)

無事、Lambda で FFmpeg を動かすことができました!

おわりに

FFmpeg をカスタムレイヤーとして Lambda 関数上で動かしてみました。

FFmpeg が必要となった場合には実現方法の 1つとして参考にしていただければと思います。


以上、AWS 事業本部の大前でした。

参考