Lambda Layer版の動画情報取得ツールMediaInfoを使ってみた

動画ファイルの各種メタ情報が取得できるツールMediaInfoでLambda Layer版がダウンロード提供されるようになっていました。実際にLambda Layerに設定、Lambda関数から呼び出して使用してみました。
2022.05.31

はじめに

清水です。動画やオーディオファイルの各種情報を取得できるMediaInfoというツールがあります。WindowsやmacOSのほか、Linux環境でも動作することからAWS Lambda上で実行させることも可能です。Amazon S3に動画ファイルをアップロードしてLambdaで解像度情報を取得、解像度にあったAWS Elemental MediaConvertのジョブテンプレートを用いたジョブをキックする、といった処理が可能になります。

これまでも本DevelopersIOでこの「Lambda関数内でMediaInfo」を動かす、ということはまとめてきたのですが、Amazon Linux上で実行ファイルをコンパイルし(場合によっては関連する共有ライブラリもあわせて)Lambda Layerに設定して動作させる、というものでした。

特にPython3.8ランタイムで動作させる場合には共有ライブラリとの調整などもあり大変だったんですよね。そんな中、MediaInfoのダウンロード画面を確認していたら「AWS Lambda」版があることに気が付きました!Lambda Layerとして使用できるかたちです。本エントリではこのLambda Layer版MediaInfoを使ってみたのでまとめてみたいと思います。

Lambda Layer版MediaInfoを使ってみた

ダウンロードできるLambda Layer版MediaInfoの確認

まずはLambda Layer版MediaInfoのダウンロード先とその中身をを確認しておきましょう。MediaInfoのページからダウンロード先に進むと「AWS Lambda」が見つかりますね。

MediaInfo - Download

Official releasesのリンクをたどると、ダウンロード画面に遷移します。x86_64とarm64のそれぞれで、CLI版とDLL版があります。

MediaInfo - Download MediaInfo for AWS Lambda

それぞれ以下の名称のファイルがダウンロードできました。

  • MediaInfo_CLI_22.03_Lambda_x86_64.zip
  • MediaInfo_DLL_22.03_Lambda_x86_64.zip
  • MediaInfo_CLI_22.03_Lambda_arm64.zip
  • MediaInfo_DLL_22.03_Lambda_arm64.zip

ダウンロードしたzipを解答すると以下の構成で、CLI用の実行ファイルもしくはライブラリ用ファイルが格納されていました。

% ls -R MediaInfo_CLI_22.03_Lambda_x86_64
LICENSE  bin/

MediaInfo_CLI_22.03_Lambda_x86_64/bin:
mediainfo*
% ls -R MediaInfo_DLL_22.03_Lambda_x86_64
LICENSE  lib/

MediaInfo_DLL_22.03_Lambda_x86_64/lib:
libmediainfo.so@       libmediainfo.so.0@     libmediainfo.so.0.0.0*
% ls -R MediaInfo_CLI_22.03_Lambda_arm64
LICENSE  bin/

MediaInfo_CLI_22.03_Lambda_arm64/bin:
mediainfo*
% ls -R MediaInfo_DLL_22.03_Lambda_arm64
LICENSE  lib/

MediaInfo_DLL_22.03_Lambda_arm64/lib:
libmediainfo.so@       libmediainfo.so.0@     libmediainfo.so.0.0.0*

MediaInfoのLambda Layerの作成

Lambda Layer版MediaInfoのダウンロード先や、取得できるファイルなどについて確認ができました。続いて、実際にLambda関数で使用するためのLambda Layerを作成していきます。今回はarm64アーキテクチャのCLI用を使用する想定とします。(MediaInfo_CLI_22.03_Lambda_arm64.zipを使用するぐあいですね。)

AWSマネジメントコンソールのLambdaのページ、Layersの[Create layer]から進みます。Nameは「MediaInfo-arm64-CLI-v22」としました。DescriptionでMediaInfoの詳細バージョン(22.03)も記載しています。ファイルについてはダウンロードしたzipファイルをそのままUplod a .zip fileで指定しました。またアーキテクチャで「arm64」を指定しています。Comatible runtimesとしてはlambda関数で使用する「Python 3.9」をとりあえず選択しておきました。[Create]でLayerを作成します。

MediaInfoを動かすLambda関数の作成

続いてこのMediaInfoを動かすLambda関数本体の作成を進めます。Lambda関数で実行するコードは以前のエントリで試したものを流用することとしました。(本エントリ最後にも実際のコードを記載します。)

またIAMロールについても上記エントリで作成したMediaInfo-on-Lambda-Roleを流用しています。AmazonS3FullAccessAWSLambdaBasicExecutionRoleの権限を持つLambda用のロールとなります、詳細は上記エントリをご確認ください。

AWSマネジメントコンソールのLambdaのページから、[Create function]で関数を作成します。Author from scratchを選択して、関数名、ランタイム、アーキテクチャをそれぞれ指定していきます。関数名は「MediaInfo-Lambda-Layer-Version」、ランタイムは「Python 3.9」としました。アーキテクチャは「arm64」です。(これらは先ほど作成したLambda Layerにあわせています。)

Permissionsでは適切なIAMロールを選択します。[Create function]でLambda関数を作成します。

Lambda関数作成後、遷移したページでまずはPythonのコードを設定します。Code sourceの箇所、デフォルトの内容を本エントリ最後に示すコードでまるっと上書きしてしまいます。上書き後は[Deploy]ボタンを押下しておきましょう。

そのまま画面を下にスクロールすると、Layerの設定箇所が現れます。[Add a layer]ボタンから進みAdd layer画面でLayer sourceとして「Custom layers」を選択し、先ほど作成したLayerとバージョンを選びます。

続いてConfigurationタブの「General configuration」の設定を編集します。デフォルトではメモリが128MB、タイムアウトは3秒ですが、メモリを256MB、そしてタイムアウトを1分と設定しました。これは以前のエントリの設定内容を踏襲するかたちです。Lambdaで実行するコードの特性から、特にタイムアウトについては設定必須かと思います。デフォルトの3秒の場合、S3からのファイル取得中に3秒以上経過し実際にタイムアウトが発生してしまうことがありました。

S3からLambda関数をトリガする設定

Lambda関数の設定の最後に、S3バケットへのオブジェクトアップロード時にこの関数をトリガする設定を行います。今回はLambdaのマネジメントコンソールから行いました。(S3のマネジメントコンソールから行う方法もありますね。)ConfigurationタブのTriggers、[Add trigger]ボタンからAdd trigger画面に進みます。トリガするリソースはS3バケットとし、使用するS3バケットを選択します。Event typeで「All object create events」を選択、動画ファイルを前提とするのでSuffixで「.mp4」を指定しました。

Lambda関数内でMediaInfoが実行されることの確認

以上のLambda LayerならびにLambda関数、そしてLambda関数のトリガの設定が完了したら、実際に動画ファイルを指定したS3バケットにアップロードしてみます。今回使用したLambda関数ではMediaInfoを実行して得られた情報のうち、ファイルサイズと解像度の情報をログとしてCloudWatch Logsに出力します。このログの中身を確認してみましょう。

問題なくLambda Layer版のMediaInfoが実行され、動画ファイルの情報が取得できていました!

まとめ

動画ファイルの情報取得ツールであるMediaInfoのLambda Layer版を実際に使用してみました。これまでLambda関数でMediaInfoを使用する場合には、LambdaのランタイムにあわせたLinux環境上でMediaInfoをコンパイルする、また必要に応じて関連する共通ライブラリを抽出する、という手間が発生していました。Lambda Layerに設定してしまえば一度きりの作業にはなりますが、コンパイルそして特に共通ライブラリをまとめる作業はなかなかに骨が折れます。MediaInfo提供元より公式のLambda Layerがダウンロード、利用できるようになっていることは非常に喜ばしいことかと思います。

Lambda関数で実行したコード

本エントリで使用したLambda関数のコードを以下にまとめておきます。以前のエントリ「MediaInfoをAWS Lambda (Python)で使って動画ファイルのメタ情報を取得してみた | DevelopersIO」で使用したものと同一のものです。コードの処理内容などもリンク先のエントリをご確認ください。

import json
import logging
import os
import subprocess
import urllib.parse

import boto3

SIGNED_URL_EXPIRATION = 300

logger = logging.getLogger('boto3')
logger.setLevel(logging.INFO)

def get_signed_url(expires_in, bucket, obj):
    s3_cli = boto3.client("s3")
    presigned_url = s3_cli.generate_presigned_url(
        'get_object', 
        Params={'Bucket': bucket, 'Key': obj}, 
        ExpiresIn=expires_in)
    return presigned_url

s3 = boto3.client('s3')

logger.info("Loading function")

def lambda_handler(event, context):

    logger.info(json.dumps(event))

    for s3_record in event['Records']:
        try: 
            logger.info("Working on new s3_record...")

            key = urllib.parse.unquote_plus(s3_record['s3']['object']['key'], 
                                            encoding='utf-8')
            bucket = s3_record['s3']['bucket']['name']
            logger.info("Bucket: {} \t Key: {}".format(bucket, key))

            signed_url = get_signed_url(SIGNED_URL_EXPIRATION, bucket, key)
            logger.info("Signed URL: {}".format(signed_url))

            # MediaInfoを実行
            json_output = subprocess.check_output(
                ["mediainfo", "--full", "--output=JSON", signed_url])
            logger.info("MediaInfo Output: {}".format(json_output))

            # MediaInfoの実行結果からFileSize, Width, Heightを取り出す
            json_data = json.loads(json_output)
            tracks=json_data['media']['track']
            video_info = {}
            for track in tracks:
                if track['@type'] == "General":
                    if 'GeneralFileSize' not in video_info:
                        video_info['GeneralFileSize'] = track.get('FileSize')
                    else:
                        logger.info('MediaInfo num of General > 1')
                elif track['@type'] == "Video":
                    if 'VideoHeight' not in video_info:
                        video_info['VideoHeight'] = track.get('Height')
                    else:
                        logger.info('MediaInfo number of Video > 1')
                    if 'VideoWidth' not in video_info:
                        video_info['VideoWidth'] = track.get('Width')
                    else:
                        logger.info('MediaInfo number of Video Width > 1')
            logger.info("video_info: {}".format(video_info))

            # ファイル名とFileSize, Width, Heightをログ出力
            input_file_name = key.rsplit('/', 1)[-1]
            logger.info("\n{} Infomation =>  FileSize: {} Byte, Width: {} pixel, Height: {} pixel".format(
                input_file_name, video_info['GeneralFileSize'], video_info['VideoWidth'], video_info['VideoHeight']))

        except Exception as e:
            print(e)
            print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
            raise e