MediaInfoをAWS Lambda (Python)で使って動画ファイルのメタ情報を取得してみた

動画やオーディオファイルの各種情報を取得できるツールMediaInfoをAWS Lambda内で使ってみました。必要となるMediaInfoの実行ファイルの作成から簡単なLambda関数内での動作確認までをまとめてみたいと思います。
2020.02.29

はじめに

清水です。動画やオーディオファイルの各種情報を取得できるツールMediaInfoをAWS Lambda内で使ってみました。必要となるMediaInfoの実行ファイルの作成から簡単なLambda関数内での動作確認までをまとめてみたいと思います。

なお、Lambdaの環境(ランタイム)はPython(Python 3.7)となります。

MediaInfo実行ファイルのコンパイル

まずはLambda関数内で使用するための実行ファイルを、MediaInfoのソースコードファイルからコンパイルして作成します。以下のAWS Lambda 開発者ガイド「AWS Lambda ランタイム」の項目を確認し、Python 3.7ランタイムではオペレーティングシステムはAmazon Linux(Amazon Linux 2ではなく、1のほう)とのことでした。

EC2でAmazon Linuxのインスタンスを起動してコンパイルを行います。上記「AWS Lambda ランタイム」ページに記載されていた以下のAMIを使用しました(リージョンはap-northeast-1となります)。コンパイルすることもありインスタンスタイプはt2.largeを選択します。

  • AMI Name
    • amzn-ami-hvm-2018.03.0.20181129-x86_64-gp2
  • AMI ID
    • ami-00a5245b4816c38e6
  • Description
    • Amazon Linux AMI 2018.03.0.20181129 x86_64 HVM gp2

コンパイル手順はこちらのサイトを参考にしました。

事前に'Development Tools'libcurl-develをyumでインストールします。(前者はgroupinstallします。)'Development Tools'についてはコンパイルに必要な開発ツールですね。後述しますがLambdaでのMediaInfo実行にはcurlオプションが必須になるかと思いますので、libcur-develもインストールしておきます。

$ sudo yum groupinstall 'Development tools'
$ sudo yum install libcurl-devel

続いてMediaInfoのソースファイルをダウンロード、付属のスクリプトでコンパイルを行います。今回、MediaInfoのバージョンは最新版の19.09を使用しました。

ダウンロードしたソースファイルを展開、作成されたディレクトリに移動してコンパイル用のスクリプトCLI_Compile.shを実行します。ここでもオプションで--with-libcurを付与しておきます。

実際に実行したコマンドは下記になります。(CLI_Compile.sh実行についてはログをファイルにも書き出すようにしました。)

$ wget https://mediaarea.net/download/binary/mediainfo/19.09/MediaInfo_CLI_19.09_GNU_FromSource.tar.xz
$ tar xvf MediaInfo_CLI_19.09_GNU_FromSource.tar.xz
$ cd MediaInfo_CLI_GNU_FromSource
$ ./CLI_Compile.sh --with-libcurl 2>&1 | tee CLI_Compile.log

コンパイルには2分ちょっとかかりました。無事にコンパイルできると、以下のメッセージが表示されます。

MediaInfo (CLI) compiled
MediaInfo executable is MediaInfo/Project/GNU/CLI/mediainfo
For installing, cd MediaInfo/Project/GNU/CLI && make install

今回はコンパイルしたマシンにインストールすることはしませんので、make installコマンドは実行しません。ただ後ほどLayer機能を使ってこのMediaInfoの実行ファイルを使用します。binディレクトリを作成して実行ファイルをmediainfoをこのディレクトリにコピー、binディレクトリ配下をまとめて1つのzipファイルにまとめておきます。

$ mkdir bin
$ cp -p MediaInfo_CLI_GNU_FromSource/MediaInfo/Project/GNU/CLI/mediainfo ./bin/
$ zip mediainfo_v19.09.zip ./bin/*

zipファイルを展開すると以下のような構成になっています。

$ unzip mediainfo_v19.09.zip
Archive:  mediainfo_v19.09.zip
  inflating: bin/mediainfo
$ ls -R
.:
bin  mediainfo_v19.09.zip

./bin:
mediainfo

Lambda関数の作成

コンパイルしたMediaInfo実行ファイルをLambda関数内で使えるように設定していきます。今回動作確認として、S3バケットへのファイルアップロード(オブジェクトの新規作成)でLambda関数をキック、アップロードされたファイルからMediaInfoで情報を取り出し、解像度情報についてログ(CloudWatch Logs)に書き出す、というコードを準備しました。コード自体は次章にまとめるとして、本章ではまずLambdaで使うIAMロール、MediaInfoのLayer、そしてLambda関数自体の設定についてまとめます。

Lambda関数で利用するIAMロールの作成

まずはLambda関数にアタッチするIAMロールを作成します。権限はAWS manged policyのAWSLambdaBasicExecutionRoleの他、S3バケットにアップロードされたファイルへのアクセス権も付与しておきます。(今回は大雑把にAmazonS3FullAccessのAWS managed policyを使用しました。実際の環境にあわせて適切な権限を選択しましょう。)

AWSマネジメントコンソールのIAM、Roleの画面の[Create role]から進めます。ユースケースとしてLambdaを選択しましょう。

ポリシー選択画面ではAWS managed policyのAmazonS3FullAccessAWSLambdaBasicExecutionRoleを選択します。今回はRole名をMediaInfo-on-Lambda-Roleとしました。[Create role]でIAMロールを作成します。

MediaInfoのLayerの作成

続いてLambda関数で使用する、MediaInfoのLayerを作成します。MediaInfoの実行ファイルをソースコードファイルとまとめてzipにしLambda関数に設定することもできますが、zipファイル(主にMediaInfo実行ファイル)の容量が大きくなり、マネジメントコンソール上でのソースコードの確認や編集ができなくなります。またLayerを作成しておけば、複数のLambda関数からMediaInfo実行ファイルを扱える、というメリットもあります。

AWSマネジメントコンソールのLambdaのページ、Layersの[Create layer]から進みます。

NameはそのままMediaInfoとし、DescriptionでMediaInfoの詳細バージョン(19.09)とコンパイル環境(Amazon Linux)を記載しておきました。今回はUpload a .zip fileを選択し、先ほどEC2で作成したzipファイルmediainfo_v19.09.zipをいちどローカル環境にコピー後してアップロードしました。Comatible runtimesとしてはPython 3.7を選択し、[Create]をクリックします。

Layerが作成できました。(作っては消しての試行錯誤の結果、version 6で作成されています。初回作成時であればversion 1となるはずです。)

Lambda関数の設定

IAMロールとLayerが作成できたら、続いてLambda関数本体の作成を進めていきます。AWSマネジメントコンソールのLambdaのページから、[Create function]で関数を作成します。Author from scratchを選択して、関数名、ランタイムを入力、IAMロールを選択します。関数名はMediaInfo-on-Lambdaとしましました。ランタイムはPython 3.7です。IAMロールは先ほど作成したMediaInfo-on-Lambda-Roleを選択します。

Lambda関数作成後、遷移したページでまずはPythonのコードを設定します。Function codeの箇所、デフォルトの内容を次章に示すコードでまるっと上書きしてしまいます。

続いてBasic settingsの項目を編集します。デフォルトではメモリが128MB、タイムアウトは3秒ですが、メモリを256MB、そしてタイムアウトを1分と設定しました。特にタイムアウトについては設定必須かと思います。デフォルトの3秒の場合、S3からのファイル取得中に3秒以上経過し実際にタイムアウトが発生してしまうことがありました。

そしてLayer機能についての設定です。DesignerのLayers部分をクリック、Layersの項目の[Add a layer]で進みます。

先ほど作成したLayerMediaInfoを選択、バージョンもLayerにあったものを選択して[Add]で追加します。

Layerが追加されました。ここまで設定したら右上の[Save]ボタンでLambda関数の内容を保存しておきましょう。

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

S3にファイルアップロード(オブジェクトの新規作成)があったら、Lambdaを実行するようにします。Lambdaのマネジメントコンソールから行う方法もありますが、今回はS3のマネジメントコンソールから設定しました。

対象のS3バケット、Properties項目からAdvanced settingsのEventsを設定します。Add notificationをクリック、現れた設定項目でNameを「Execute-MediaInfo-on-Lambda」と入力、EventはAll object create eventsで全てのオブジェクト作成イベント時に実行されるようにしました。Send toでLambda Functionを選択、Lambdaの部分でLambda関数MediaInfo-on-Lambdaを選んで[Save]します。

なお、設定後にLambdaのマネジメントコンソール、Designerの箇所を確認すると、以下のようにtriggerが登録されていることがわかります。

Lambda関数内でMediaInfoを実行

以上設定ができたら、実際にLambda関数をキックしてMediaInfoを実行してみます。Lambdaをトリガするよ設定したS3バケットに動画ファイル(ファイル名IMG_8947.MOV)をアップロードしました。CloudWach LogsでLambda関数実行のログを確認してみます。

ファイルサイズ、横縦の解像度情報が取得できていますね!

Lambda関数で実行するコード

今回Lambda関数に設定したPythonのコードが下記になります。(なお筆者はあまりコードを書かなかったり、だいぶ久しぶりに書いたコードだったりしますので拙い内容であることはご了承ください。参考に挙げているサイトをはじめいろいろなページ、既出のコードを参考にしました。)

今回、Lambda関数内でMediaInfoを実行します。通常MediaInfoを実行する場合は、MediaInfo実行ファイルがあるシステム上に、解析対象の動画ファイルがあるかと思います。(手元のMacにある動画ファイルをMac上のMediaInfoで解析する、など。)Lambdaで同様に動画ファイルをいちどローカルストレージに保存して、ということもできるかと思いますが、/tmpディレクトリに保存してと考えるとファイルサイズの上限が512MBとなってしまいます。そのためMediaInfoには動画ファイルのURLを渡して、動画ファイルのローカルへのダウンロードなしに解析を行います。このMediaInfoでURLサポート機能を追加するため、コンパイル時に--with-libcurlオプションを付与していました。このMediaInfoの引数にURLを指定しての実行が、43、44行目になります。

MediaInfoの引数として渡すURLですが、対象ファイルがS3上のオブジェクトであり、Lambdaからアクセスするために署名付きURLとしています。(変数signed_url)処理内容としては14-20行目の関数get_signed_urlで行っています。

MediaInfoの実行結果はJSON形式で出力し、このデータ内からファイルサイズ、Width、Heightを別途用意した変数video_infoに格納、70行目の少し整形をしてログに書き出す、ということをしています。

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

まとめ

Lambda関数内で動画解析ツールのMediaInfoを実行してみました。本エントリではLambda内でファイルサイズ、Width、Heightの情報を抜き出してログに出力する、というところまででしたが、Lambda内で動画ファイル情報が解析できるといろいろと用途が広がると思います。例えば以下のようなS3バケットにファイルをアップロードして、LambdaでMediaConvertのジョブをキックするような処理です。動画の解像度情報がなどLambda関数内でわかれば、それにあわせたMediaConvertのJob Templateを選択してジョブを作成する、ということができますね。これで入力元動画にあわせたABR(Adaptive bitrate)での動画出力、なども可能です。

またそのほか動画ファイルのメタ情報(長さなど)を例えばDynamoDBなどに保存しておき、アプリケーションと連携させる、なども考えられると思います。MediaInfo自体とてもパワフルなツールだと思いますが、これがLambda関数内で使えるのは嬉しいですね!

参考