こんにちは、広野です。
先日、アプリから Amazon S3 バケットにアップロードした動画ファイルを処理するとともに、アプリから動画を再生できるようストリーミング用データ (HLS) に自動変換するジョブを作成したので紹介します。
つくったもの全体像
本記事では全てを紹介しないので恐縮ですが、以下の AWS Step Functions ステートマシンを組んでいます。
- 大きく分けて、動画ファイルのラベルを検出する処理 と 動画ファイルをアプリ画面での再生用にストリーミング用データにコンバートする処理 に分かれています。
- アプリの利用イメージですが、ユーザがインプットとなる動画ファイルを送ると、処理結果としてインプットした動画をストリーミング再生でき、アウトプットとして動画ファイルから検出されたラベルリストを閲覧できるようになっています。
- インプットの動画ファイルは MP4 または MOV とし、処理後のデータは m3u8 (HLS形式) にコンバートされます。
- ステートマシンには動画ファイルの保存先 Amazon S3 バケット、キーがインプットとして渡されます。以下の記事で紹介しておりますが、Amazon S3 イベント通知から AWS Lambda 関数を経由してステートマシンが呼び出されます。
- 最終的に、並列で実行された 2 つの処理結果を AWS Lambda 関数でとりまとめて、結果に応じた処理を行います。(エラー処理や、結果のメタデータ保存など)
本記事では、赤い枠線内の処理を抜粋して解説します。
どのように動画ファイルコンバート処理をつくったか
動画ファイルのコンバートには、AWS Elemental MediaConvert (以下、MediaConvert) を使用しています。いわゆる動画処理ソフトがやってくれる機能を切り出したようなサービスです。動画をアプリからストリーミングできるようにするために必要でして、私は多用しています。
この MediaConvert にはクセがありまして、コンバート処理を自動化するのに少々トリッキーな作業が必要でした。それを含めこの後紹介していきます。
本記事の内容は執筆時点の情報なので、今後は改善される可能性があります。
動画ファイルコンバート処理の流れ
以下、動画ファイルコンバート処理のみ切り出した AWS Step Functions ステートマシンです。
記事の最下部に AWS CloudFormation テンプレートを用意していますので、詳細な設定はそちらをご確認下さい。ご自身の AWS 環境にスタックを作成し、出来上がった実物と見比べて頂けると理解が早くなると思います。
- MediaConvert に動画ファイルをコンバートさせるには、MediaConvert の「ジョブ」を作成します。このジョブ作成とは MediaConvert にコンバート命令を出すだけで、処理結果は教えてくれません。そのため、結果 (ジョブステータス) を確認する処理を組み込む必要があります。
- ジョブステータス確認はジョブ作成の 10 秒後に行います。ステータスがまだ実行中であれば、再度 10 秒待ち状態に戻ります。結果が成功、失敗いずれかになれば処理は終了です。
- 最後に「何もしない」Pass ステートが入っていますが、ここにはジョブの結果によって実行したいアクションを入れましょう。本記事では説明簡略化のため、何もしないダミー処理を入れています。
- AWS Step Functions には MediaConvert 用のネイティブ API がまだ組み込まれていなかったため、AWS Lambda 関数をわざわざ作成しています。
- 一見無駄な処理フローに見えますが、AWS Lambda 関数が「命令を出して終わり」になっているので課金に優しく、ステートマシンに設定したタイムアウト範囲内である限り処理時間を問わない、スケーリング可能なフローになっています。
以下、AWS Lambda 関数内で使用している API です。
- MediaConvert のジョブ作成 API
- MediaConvert のジョブステータス確認 API
その他必要になった作業
5 点、面倒だったりつまずいたりした点がありましたので、解決のために実施した作業を列挙します。
- MediaConvert ジョブ実行用 IAM ロール作成
- MediaConvert ジョブテンプレート作成
- MediaConvert ジョブのキュー設定
- MediaConvert ジョブ作成 API 呼出時の設定
- MediaConvert ジョブの出力結果加工
1. MediaConvert ジョブ実行用 IAM ロール作成
MediaConvert のコンバートジョブを作成する際に、そのジョブに割り当てる IAM ロールを作成する必要があります。その IAM ロールの ARN を、ジョブ作成時に明示的に API に渡さないとジョブは動きません。
この IAM ロールは強力な権限が必要で、AWS の日本語版ドキュメントには最小権限の原則に則っていないと書かれています。英語版ドキュメントだと権限に制限を入れるようになっていました。
本記事で使用している構成では、以下のドキュメントに書かれている IAM ロールを作成して実行しています。AWS Lambda 関数内に IAM ロール名がベタ書きされているのでご注意下さい。
2. MediaConvert ジョブテンプレート作成
ジョブテンプレートとは、動画ファイルをコンバートするときの事前定義済み共通設定だと思って下さい。ジョブ作成時にジョブテンプレートを指定すれば、ジョブテンプレートに上書きしたいジョブ個別の情報だけ追加で渡すだけで済むので、ジョブ作成時のパラメータ量を大きく削減できます。以下リンクにある API のパラメータ量の多さは見て頂けたらわかると思います。
本記事のケースではインプットの動画ファイル名が毎回変わるので、それだけを API に都度渡す記述にしました。※厳密にはそれだけではないのですが、動的なに変わる部分は、という意味です。
ジョブテンプレートを AWS CloudFormation で作成するのは大変でした。全てゼロから作成するのは無理です。上述の通り、パラメータが膨大にあるからです。しかも動画の専門用語が多く、何をどう設定すればよいのか調べるのに苦労しました。※動画は見られればいいやと思っているぐらいなので、当然設定にこだわりがあるわけもなく、ただただ面倒でした。
AWS CloudFormation テンプレートにパラメータを落とし込むためには、一度 AWS マネジメントコンソール上で仮のジョブテンプレートを作成してしまうのが早道です。一度作成するとその内容を忠実に JSON 形式でエクスポートできるので、その JSON を AWS CloudFormation テンプレートに転記します。AWS 公式ドキュメントにもジョブテンプレートのパラメータについては説明がなく (笑) マネジメントコンソールで作成してエクスポートしてくれと書いてありましたので、まあ、そうなのでしょう。AWS CloudFormation テンプレートが YAML フォーマットの場合はエクスポートした JSON を YAML 形式に書き換える必要があるのでそこもひと手間かかります。
ジョブテンプレートの設定の 1 つに、プリセットがあります。プリセットは、動画の画質に関する設定です。解像度やフレームレートなど、動画ファイルのサイズにも大きく関わってくる内容です。これについては AWS が標準でプリセットのテンプレートを多数用意してくれているので、あえてカスタムプリセットを作成するよりは標準のものから選ぶのが楽で良いと思います。
3. MediaConvert ジョブのキュー設定
MediaConvert には独自のキューを作成することができます。AWS アカウント内で共通の default キューを通常使えばよいとは思いますが、分ける場合には AWS Lambda 関数でジョブ作成するときのパラメータとして明示的に渡す必要があります。前述のジョブテンプレートにキューの設定があるのですが、そこに設定しておくだけでは何故か機能しない (default のキューが使用されてしまう) ので注意が必要です。
4. MediaConvert ジョブ作成 API 呼出時の設定
MediaConvert ジョブ作成を AWS Lambda 関数から実行するとき、Python ですと boto3 の中から MediaConvert オブジェクトを最初に作成します。その際、MediaConvert の API URL を設定しないとエラーになり動作しませんでした。AWS の boto3 ドキュメントでは
client = boto3.client('mediaconvert', region_name='${AWS::Region}', endpoint_url=os.environ['MEDIACONVERTAPIENDPOINT'])
具体的には、endpoint_url= の定義になります。コード内では環境変数を使用してしまっているので os.environ から始まる記述になっていますが、中身は https から始まる URL です。この URL は AWS マネジメントコンソールの MediaConvert の画面から確認できます。
5. MediaConvert ジョブの出力結果加工
MediaConvert ジョブでは、出力先の Amazon S3 バケットやキーのプレフィックス等を指定できますが、ファイル名そのものはオリジナルのファイル名を使用します。それはいいのですが、最終的に出力したデータのキーをジョブ実行結果の中に出力してくれません。
そのため、アプリまで連動した自動処理をつくるためには、ジョブによって生成される動画データの命名ルールを理解した上で、自分で AWS Lambda 関数等の中でバケット名やキー名を抽出、加工する必要があります。
何が言いたいかと言いますと、以下のようなコンバート後ファイル名が欲しいのに、ジョブ結果の中にない、ということです。
- xxxxxxxxx.m3u8
S3 バケット名は元々ジョブテンプレート内に自ら指定するので、まあ無くてもかまわないです。ファイル名に関してはオリジナルの動画種類によって拡張子が違うのと、プレフィックス等を設定するとそれも必要になってきますので、少々困ります。
AWS CloudFormation テンプレート
記事掲載用に、動画ファイルコンバート処理のみ抜粋・加工したテンプレートを用意しました。
AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a MediaConvert resources. The IAM role MediaConvert_Default_Role must be created in your AWS account before creating a job. Please refer https://docs.aws.amazon.com/mediaconvert/latest/ug/creating-the-iam-role-in-mediaconvert-configured.html for details. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SubName: Type: String Description: System sub name of EXAMPLE. (e.g. prod or test) Default: test MaxLength: 10 MinLength: 1 MediaConvertApiEndpointUrl: Type: String Description: The URL of your MediaConvert API Endpoint. (e.g. https://xxxxxxxxx.mediaconvert.xxxxxxxxxx.amazonaws.com) Default: https://xxxxxxxxx.mediaconvert.ap-northeast-1.amazonaws.com MaxLength: 80 MinLength: 1 MediaConvertOutputS3Bucket: Type: String Description: The S3 bucket name for MediaConvert outputs. Default: example-s3-bucket-name Resources: # ------------------------------------------------------------# # Elemental MediaConvert Queue # ------------------------------------------------------------# MediaConvertQueue: Type: AWS::MediaConvert::Queue Properties: Description: !Sub For EXAMPLE-${SubName} Name: !Sub EXAMPLE-${SubName} PricingPlan: ON_DEMAND Status: ACTIVE Tags: Cost: !Sub EXAMPLE-${SubName} # ------------------------------------------------------------# # Elemental MediaConvert Job Template # ------------------------------------------------------------# MediaConvertJobTemplate: Type: AWS::MediaConvert::JobTemplate Properties: Name: !Sub EXAMPLE-${SubName}-General-hls-720p Description: !Sub The transcoding configuration for EXAMPLE-${SubName} Category: EXAMPLE AccelerationSettings: Mode: DISABLED Priority: 0 Queue: !GetAtt MediaConvertQueue.Arn SettingsJson: TimecodeConfig: Source: ZEROBASED OutputGroups: - CustomName: HLS_output Name: Apple HLS output Outputs: - Preset: "System-Avc_16x9_720p_29_97fps_5000kbps" AudioDescriptions: - AudioSourceName: "Audio Selector 1" OutputSettings: HlsSettings: IFrameOnlyManifest: EXCLUDE NameModifier: _output OutputGroupSettings: Type: HLS_GROUP_SETTINGS HlsGroupSettings: TargetDurationCompatibilityMode: SPEC_COMPLIANT SegmentLength: 10 SegmentLengthControl: GOP_MULTIPLE Destination: !Sub s3://${MediaConvertOutputS3Bucket}/jobdata/mediaconvert/ MinSegmentLength: 0 MinFinalSegmentLength: 0 SegmentControl: SEGMENTED_FILES ImageBasedTrickPlay: NONE Inputs: - AudioSelectors: "Audio Selector 1": Offset: 0 DefaultSelection: DEFAULT AudioDurationCorrection: AUTO VideoSelector: ColorSpace: FOLLOW SampleRange: FOLLOW Rotate: DEGREE_0 EmbeddedTimecodeOverride: NONE AlphaBehavior: DISCARD PadVideo: DISABLED FilterEnable: AUTO PsiControl: IGNORE_PSI FilterStrength: 0 DeblockFilter: DISABLED DenoiseFilter: DISABLED InputScanType: AUTO TimecodeSource: ZEROBASED StatusUpdateInterval: SECONDS_60 Tags: Cost: !Sub EXAMPLE-${SubName} DependsOn: - MediaConvertQueue # ------------------------------------------------------------# # State Machine # ------------------------------------------------------------# StateMachineEXAMPLE: Type: AWS::StepFunctions::StateMachine Properties: StateMachineName: !Sub EXAMPLE-${SubName} StateMachineType: STANDARD DefinitionSubstitutions: DSSubName: !Sub ${SubName} DSLambdaStartMediaConvertArn: !GetAtt LambdaStartMediaConvert.Arn DSLambdaCheckMediaConvertArn: !GetAtt LambdaCheckMediaConvert.Arn DefinitionString: |- { "Comment": "State machine to convert media for EXAMPLE-${DSSubName}", "StartAt": "StartMediaConvert", "States": { "StartMediaConvert": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "Payload.$": "$", "FunctionName": "${DSLambdaStartMediaConvertArn}" }, "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException", "Lambda.TooManyRequestsException" ], "IntervalSeconds": 2, "MaxAttempts": 6, "BackoffRate": 2 } ], "Next": "Wait" }, "Wait": { "Type": "Wait", "Seconds": 10, "Next": "GetMediaConvertStatus" }, "GetMediaConvertStatus": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "Payload.$": "$", "FunctionName": "${DSLambdaCheckMediaConvertArn}" }, "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException", "Lambda.TooManyRequestsException" ], "IntervalSeconds": 2, "MaxAttempts": 6, "BackoffRate": 2 } ], "Next": "CheckMediaConvertStatus" }, "CheckMediaConvertStatus": { "Type": "Choice", "Choices": [ { "Or": [ { "Variable": "$.Job.Status", "StringEquals": "SUBMITTED" }, { "Variable": "$.Job.Status", "StringEquals": "PROGRESSING" } ], "Next": "Wait" } ], "Default": "Pass" }, "Pass": { "Type": "Pass", "End": true } }, "TimeoutSeconds": 1800 } LoggingConfiguration: Destinations: - CloudWatchLogsLogGroup: LogGroupArn: !GetAtt LogGroupStateMachineEXAMPLE.Arn IncludeExecutionData: true Level: ERROR RoleArn: !GetAtt StateMachineExecutionRole.Arn TracingConfiguration: Enabled: false Tags: - Key: Cost Value: !Sub EXAMPLE-${SubName} DependsOn: - LogGroupStateMachineEXAMPLE - StateMachineExecutionRole - LambdaStartMediaConvert - LambdaCheckMediaConvert # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaStartMediaConvert: Type: AWS::Lambda::Function Properties: FunctionName: !Sub EXAMPLE-StartMediaConvert-${SubName} Description: !Sub Lambda Function to create a job to convert media for EXAMPLE-${SubName} Runtime: python3.9 Timeout: 3 MemorySize: 128 Environment: Variables: MEDIACONVERTJOBTEMPLATEARN: !GetAtt MediaConvertJobTemplate.Arn MEDIACONVERTQUEUEARN: !GetAtt MediaConvertQueue.Arn MEDIACONVERTAPIENDPOINT: !Ref MediaConvertApiEndpointUrl Role: !GetAtt LambdaMediaConvertInvocationRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub EXAMPLE-${SubName} Code: ZipFile: !Sub | import json import boto3 import os import datetime def datetimeconverter(o): if isinstance(o, datetime.datetime): return str(o) def lambda_handler(event, context): try: print(event) client = boto3.client('mediaconvert', region_name='${AWS::Region}', endpoint_url=os.environ['MEDIACONVERTAPIENDPOINT']) res = client.create_job( JobTemplate=os.environ['MEDIACONVERTJOBTEMPLATEARN'], Queue=os.environ['MEDIACONVERTQUEUEARN'], Settings={ 'Inputs': [ { 'FileInput': 's3://' + event['bucket'] + '/' + event['key'] } ] }, Role='arn:aws:iam::${AWS::AccountId}:role/service-role/MediaConvert_Default_Role', Tags={ 'Costs': 'EXAMPLE-${SubName}' } ) return json.dumps(res, indent=4, default=datetimeconverter) except Exception as e: print(e) return e DependsOn: - LambdaMediaConvertInvocationRole LambdaCheckMediaConvert: Type: AWS::Lambda::Function Properties: FunctionName: !Sub EXAMPLE-CheckMediaConvert-${SubName} Description: !Sub Lambda Function to check the job status to convert media for EXAMPLE-${SubName} Runtime: python3.9 Timeout: 3 MemorySize: 128 Environment: Variables: MEDIACONVERTAPIENDPOINT: !Ref MediaConvertApiEndpointUrl Role: !GetAtt LambdaMediaConvertInvocationRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub EXAMPLE-${SubName} Code: ZipFile: !Sub | import json import boto3 import os import datetime def datetimeconverter(o): if isinstance(o, datetime.datetime): return str(o) def lambda_handler(event, context): try: print(event) client = boto3.client('mediaconvert', region_name='${AWS::Region}', endpoint_url=os.environ['MEDIACONVERTAPIENDPOINT']) res = client.get_job( Id=event['Job']['Id'] ) return json.loads(json.dumps(res, indent=4, default=datetimeconverter)) except Exception as e: print(e) return e DependsOn: - LambdaMediaConvertInvocationRole # ------------------------------------------------------------# # State Machine LogGroup (CloudWatch Logs) # ------------------------------------------------------------# LogGroupStateMachineEXAMPLE: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/vendedlogs/states/EXAMPLE-${SubName} RetentionInDays: 365 Tags: - Key: Cost Value: !Sub EXAMPLE-${SubName} # ------------------------------------------------------------# # Lambda MediaConvert Invocation Role (IAM) # ------------------------------------------------------------# LambdaMediaConvertInvocationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub EXAMPLE-LambdaMediaConvertInvocationRole-${SubName} Description: This role allows Lambda to invoke MediaConvert. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess - arn:aws:iam::aws:policy/AWSElementalMediaConvertFullAccess # ------------------------------------------------------------# # State Machine Execution Role (IAM) # ------------------------------------------------------------# StateMachineExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub EXAMPLE-StateMachineExecutionRole-${SubName} Description: This role allows State Machines to call specified AWS resources. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: states.amazonaws.com Action: - sts:AssumeRole Path: /service-role/ ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaRole - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess
まとめ
いかがでしたでしょうか?
AWS Elemental MediaConvert の処理自動化は手間がかかることがご理解頂けたのではないかと思います。ですが今回 AWS CloudFormation テンプレートに落とし込めたので、今後の自動化処理作成は細かいパラメータの変更だけになり、楽になるはずです。
本記事が皆様のお役に立てれば幸いです。