動画ファイルをストリーミング用データに自動変換するジョブをつくる [AWS Elemental MediaConvert 使用]

こんにちは、広野です。

先日、アプリから 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 点、面倒だったりつまずいたりした点がありましたので、解決のために実施した作業を列挙します。

  1. MediaConvert ジョブ実行用 IAM ロール作成
  2. MediaConvert ジョブテンプレート作成
  3. MediaConvert ジョブのキュー設定
  4. MediaConvert ジョブ作成 API 呼出時の設定
  5. MediaConvert ジョブの出力結果加工

1. MediaConvert ジョブ実行用 IAM ロール作成

MediaConvert のコンバートジョブを作成する際に、そのジョブに割り当てる IAM ロールを作成する必要があります。その IAM ロールの ARN を、ジョブ作成時に明示的に API に渡さないとジョブは動きません。

この IAM ロールは強力な権限が必要で、AWS の日本語版ドキュメントには最小権限の原則に則っていないと書かれています。英語版ドキュメントだと権限に制限を入れるようになっていました。

本記事で使用している構成では、以下のドキュメントに書かれている IAM ロールを作成して実行しています。AWS Lambda 関数内に IAM ロール名がベタ書きされているのでご注意下さい。

2. MediaConvert ジョブテンプレート作成

ジョブテンプレートとは、動画ファイルをコンバートするときの事前定義済み共通設定だと思って下さい。ジョブ作成時にジョブテンプレートを指定すれば、ジョブテンプレートに上書きしたいジョブ個別の情報だけ追加で渡すだけで済むので、ジョブ作成時のパラメータ量を大きく削減できます。以下リンクにある API のパラメータ量の多さは見て頂けたらわかると思います。

create_job - Boto3 1.34.80 documentation

本記事のケースではインプットの動画ファイル名が毎回変わるので、それだけを 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 テンプレートに落とし込めたので、今後の自動化処理作成は細かいパラメータの変更だけになり、楽になるはずです。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWSは好きですが、それよりもAWSすげー!って気持ちの方が強いです。
取得資格:AWS 認定は12資格、ITサービスマネージャ、ITIL v3 Expert 等
2020 - 2023 Japan AWS Top Engineer 受賞
2022 - 2023 Japan AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSアプリケーション開発クラウドソリューション
シェアする
タイトルとURLをコピーしました