MediaLiveで出力したアーカイブファイルをMP4に自動変換する

#はじめに
MediaLiveでライブ配信を行う際、アーカイブを残しておくことがあるかと思います。
2020年1月現在では、MediaLiveのアーカイブ出力はtsファイル形式のみ対応しています。

tsファイルはVLCなどの動画プレイヤーを使わないと再生することができないため、
利用しやすいmp4ファイルに変換する必要があるという機会に何度か遭遇しました。

そのときに構築した自動でmp4に変換する仕組みを簡単に記載しておきます。

##構成図

MediaLive -> S3 -> Lambda -> MediaConvert -> S3
<img width="781" alt="Qiita用の構成図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277934/6a150fb9-2eb2-b0e8-a3e8-569fa6dc484a.png">

##MediaLive
Output Groupで「Archive」を選択します。

##S3
tsファイルを置くフォルダと
mp4ファイルを置くフォルダを用意します。

##IAM
IAMロールを2つ作成します。

###MediaConvert用 IAMロール

Policyはロール作成時に自動でアタッチされます。

- AmazonS3FullAccess
- AmazonAPIGatewayInvokeFullAccess

###Lambda用 IAMロール

AWSデフォルトのPolicyを使用する場合はこのPolicyがあれば動作します。
（本番環境で構築する際は権限を絞りましょう）

- AWSElementalMediaConvertFullAccess

##MediaConvert

mp4に変換させるジョブテンプレートを作成します。
（参考までに、JSONを貼っておきます）

```
{
  "Queue": "arn:aws:mediaconvert:ap-northeast-1:<account-id>:queues/<キューの名前>",
  "Name": "<ジョブテンプレートの名前>",
  "Settings": {
    "OutputGroups": [
      {
        "Name": "File Group",
        "Outputs": [
          {
            "ContainerSettings": {
              "Container": "MP4",
              "Mp4Settings": {
                "CslgAtom": "INCLUDE",
                "CttsVersion": 0,
                "FreeSpaceBox": "EXCLUDE",
                "MoovPlacement": "PROGRESSIVE_DOWNLOAD"
              }
            },
            "VideoDescription": {
              "Width": 1280,
              "ScalingBehavior": "DEFAULT",
              "Height": 720,
              "TimecodeInsertion": "DISABLED",
              "AntiAlias": "ENABLED",
              "Sharpness": 50,
              "CodecSettings": {
                "Codec": "H_264",
                "H264Settings": {
                  "InterlaceMode": "PROGRESSIVE",
                  "NumberReferenceFrames": 3,
                  "Syntax": "DEFAULT",
                  "Softness": 0,
                  "GopClosedCadence": 1,
                  "GopSize": 90,
                  "Slices": 1,
                  "GopBReference": "DISABLED",
                  "SlowPal": "DISABLED",
                  "SpatialAdaptiveQuantization": "ENABLED",
                  "TemporalAdaptiveQuantization": "ENABLED",
                  "FlickerAdaptiveQuantization": "DISABLED",
                  "EntropyEncoding": "CABAC",
                  "Bitrate": 1000000,
                  "FramerateControl": "INITIALIZE_FROM_SOURCE",
                  "RateControlMode": "CBR",
                  "CodecProfile": "MAIN",
                  "Telecine": "NONE",
                  "MinIInterval": 0,
                  "AdaptiveQuantization": "HIGH",
                  "CodecLevel": "AUTO",
                  "FieldEncoding": "PAFF",
                  "SceneChangeDetect": "ENABLED",
                  "QualityTuningLevel": "SINGLE_PASS",
                  "FramerateConversionAlgorithm": "DUPLICATE_DROP",
                  "UnregisteredSeiTimecode": "DISABLED",
                  "GopSizeUnits": "FRAMES",
                  "ParControl": "INITIALIZE_FROM_SOURCE",
                  "NumberBFramesBetweenReferenceFrames": 2,
                  "RepeatPps": "DISABLED",
                  "DynamicSubGop": "STATIC"
                }
              },
              "AfdSignaling": "NONE",
              "DropFrameTimecode": "ENABLED",
              "RespondToAfd": "NONE",
              "ColorMetadata": "INSERT"
            },
            "AudioDescriptions": [
              {
                "AudioTypeControl": "FOLLOW_INPUT",
                "AudioSourceName": "Audio Selector 1",
                "CodecSettings": {
                  "Codec": "AAC",
                  "AacSettings": {
                    "AudioDescriptionBroadcasterMix": "NORMAL",
                    "Bitrate": 96000,
                    "RateControlMode": "CBR",
                    "CodecProfile": "LC",
                    "CodingMode": "CODING_MODE_2_0",
                    "RawFormat": "NONE",
                    "SampleRate": 48000,
                    "Specification": "MPEG4"
                  }
                },
                "LanguageCodeControl": "FOLLOW_INPUT"
              }
            ],
            "Extension": ".mp4"
          }
        ],
        "OutputGroupSettings": {
          "Type": "FILE_GROUP_SETTINGS",
          "FileGroupSettings": {
            "Destination": "s3://<任意のS3バケット>/"
          }
        }
      }
    ],
    "AdAvailOffset": 0,
    "Inputs": [
      {
        "AudioSelectors": {
          "Audio Selector 1": {
            "Offset": 0,
            "DefaultSelection": "DEFAULT",
            "ProgramSelection": 1
          }
        },
        "VideoSelector": {
          "ColorSpace": "FOLLOW",
          "Rotate": "DEGREE_0",
          "AlphaBehavior": "DISCARD"
        },
        "FilterEnable": "AUTO",
        "PsiControl": "USE_PSI",
        "FilterStrength": 0,
        "DeblockFilter": "DISABLED",
        "DenoiseFilter": "DISABLED",
        "TimecodeSource": "EMBEDDED"
      }
    ]
  },
  "AccelerationSettings": {
    "Mode": "DISABLED"
  },
  "StatusUpdateInterval": "SECONDS_60",
  "Priority": 0
}
```

##Lambda
S3にtsファイルが作成されたら発火するLambda関数を作成します。

###S3トリガー
- イベントタイプ：ObjectCreated
- サフィックス：.ts
- プレフィックス：<任意のS3バケット>

###関数
- ランタイム：Python 3.8

```lambda_function.py
from __future__ import print_function

import json
import urllib.parse
import boto3
import os

print('Loading function')
mediaconvert =  boto3.client('mediaconvert', region_name='ap-northeast-1', endpoint_url='https://xxxxxxxx.mediaconvert.ap-northeast-1.amazonaws.com')
s3 = boto3.client('s3')

def lambda_handler(event, context):
    # Get the object from the event and show its content type
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    inputFile = "s3://" + bucket + "/" + key
    outputKey = "s3://<任意のS3バケット>/"
     
    try:
        # Load job.json from disk and store as Python object: job_object
        with open("job.json", "r") as jsonfile:
            job_object = json.load(jsonfile)
             
        # Input/Output Setting
        job_object["Inputs"][0]["FileInput"] = inputFile
        job_object["OutputGroups"][0]["OutputGroupSettings"]["FileGroupSettings"]["Destination"] = outputKey

        # Exec MediaConvert's job
        response = mediaconvert.create_job(
          JobTemplate='arn:aws:mediaconvert:ap-northeast-1:<account-id>:jobTemplates/<ジョブテンプレートの名前>',
          Queue='arn:aws:mediaconvert:ap-northeast-1:<account-id>:queues/<キューの名前>',
          Role='arn:aws:iam::<account-id>:role/<ロールの名前>',
          Settings=job_object
        )
    except Exception as e:
        print(e)
        raise e
```


```job.json
{
  "OutputGroups": [
    {
      "Name": "File Group",
      "OutputGroupSettings": {
        "Type": "FILE_GROUP_SETTINGS",
        "FileGroupSettings": {
          "Destination": ""
          }
        }
      }
  ],
    "Inputs": [
      {
        "FileInput": ""
      }
    ]
}
```