📝

AWS Elastic Transcoder(2025年11月サービス終了) から Media Convert に移行した記録

2025/02/04に公開

Elastic Transcoder がついに 2025 年 11 月 13 日サービス終了とのことで移行した時の記録を残しておきます。

現時点で実現している機能

動画ファイル形式の変換とキャプチャの書き出しを自動化しています。具体的にはmp4のファイルをHLS形式に変換しつつ、動画内の決まった秒数でキャプチャをとってjpgとして保存するという機能になります。また変換が完了したらメールで連絡を受け取るという補助的な機能も実装しています。

mp4 の動画ファイルを s3 のバケットにアップロード

Lambda 実行

Elastic Transcoder のパイプラインが走る

HLS 形式に変換されたストリーミング用ファイルを S3 バケットに保存

Elastic Transcoder のパイプラインその2も走る

キャプチャとして動画のある秒数の映像が JPG として S3 バケットに保存

パイプラインの結果が SES でメール送信されてきます。変換に成功したか失敗したかがわかります。

あとはウェブアプリケーション側で動画一覧画面のテンプレートを用意しておくと一覧画面についてはメンテナンス不要になるという仕様です。

現状の確認

まずは既存の Transcoder を使っているシステムの設定から確認をしてみます。

Transcoder から S3 にアクセスする IAM を確認

Transcoder のパイプライン詳細画面の「Permissions」を確認するとどのロールが使用されているのかがわかります。Media Convert でも同じように IAM ロールが必要になります。

テスト環境で Media Convert を動かしてみる

Media Convert 以外にも IAM や S3、Lambda を使っているので順番に設定してまずはテスト環境で動かしてみます。

S3 バケットを作成

次に S3 にバケットを作成していきます。今回は

  • mp4(加工前)保存用のバケット
  • HLS ファイル(加工後)保存用のバケット
  • キャプチャ画像用(加工後)のバケット

を作成していきます。

バケット作成

基本設定は 3 つのバケットとも以下の通りでバケット名だけわかりやすい名前をつけてください。

1.S3 コンソールの「バケットを作成」をクリックします。
2.以下のように設定をして「バケットを作成」をクリックします。
 バケットタイプ:汎用にチェック
 バケット名:任意の名前(ここでは m4test、hlstest、jpegtest とします。)
 オブジェクト所有者:ACL 無効にチェックを入れます。
 パブリックアクセスをすべてブロックにチェックを入れます。
 バケットのバージョニング:無効にする
 デフォルトの暗号化:SSE-S3
 バケットキー:有効にする(SSE-S3 の場合は無効でも有効でも動作は変わらない)
※ここにない項目はデフォルトで大丈夫です。必要であれば後でわかりやすいようにタグに説明を書いておくのもおすすめです。

これで動画の受け入れ、吐き出し先が構築できました。

ポリシー作成

最低限のポリシーを JSON で定義します。


{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"s3:GetObject",
				"s3:PutObject",
				"s3:ListBucket"
			],
			"Resource": [
				"arn:aws:s3:::your-input-bucket-name/*",
				"arn:aws:s3:::your-output-bucket-name/*"
			]
		},
		{
			"Effect": "Allow",
			"Action": [
				"cloudwatch:PutMetricData",
				"logs:CreateLogGroup",
				"logs:CreateLogStream",
				"logs:PutLogEvents"
			],
			"Resource": "*"
		},
		{
			"Effect": "Allow",
			"Action": [
				"mediaconvert:CreateJob",
				"mediaconvert:GetJob",
				"mediaconvert:CancelJob"
			],
			"Resource": "*"
		}
	]
}

上記のポリシーをざっくり説明すると

  • S3 から画像を取得する
  • S3 に画像を保存する
  • S3 から画像を読み出す
  • Cloud Watch にログを出力する
  • MediaConbert で実行する

これらの権限が付与されています。

IAM ロールを設定

IAM は権限をどの程度許可するのか?ということを設定していきます。
AWS のサービス間で処理をする際や、プログラムから操作をする際にこれらの権限を設定することでセキュアかつ指定したものだけに操作を許可することができます。

ロール作成

このロールは Lambda に割り当てられます。Lambda からスクリプトを実行し、

  • s3 からのファイル読み出し、保存
  • cloudwatch へのログ保存
  • sns での実行結果メール送信
  • Media Convert でのジョブ作成、操作

を行うために必要になります。以下のように設定しました。

  1. AWS Management Console で IAM サービスを開きます。
  2. 「ロールの作成」をクリックします。
  3. 信頼されたエンティティタイプで「AWS サービス」を選択します。
  4. ユースケースのプルダウンで「MediaConvert」を指定します。
  5. 許可を追加の画面ではデフォルトで(後で JSON を追加するので許可の追加なしの状態で)「次へ」をクリックします。
  6. 次の画面では後で見た時にわかりやすいようにロール名と説明を書き「ロールを作成」をクリックします。
  7. ロールが作成され、一覧画面に戻るので作成したロールを検索窓に入力して選択し編集画面に移動し、許可タブの許可ポリシーがアタッチされていたら一旦削除します。
  8. 許可タブを開き「許可を追加」をクリックします。「ポリシーをアタッチ」をクリックして前の手順で作成したポリシーを選択し「許可を追加」をクリックします。

これで権限の設定が完了しました。

SNS 設定

SNS は動画変換プロセスが成功したか失敗したかをメールで通知するために設定します。Transcoder では Lambda 上で生成していましたが、これは管理の面から考えると推奨されないようなのでこの機会にあらかじめコンソールから生成するように修正します。

SNS トピックの設定

SNS のコンソール → トピックの作成で以下を設定していきます。

タイプ:スタンダード
名前:test-topic
表示名:test-topic

その他の項目はデフォルトで作成します。

これでトピックの ARN が表示されると思います。
この項目は後の Lambda 実行のコードで使用されます。

SNS サブスクリプションの作成

サブスクリプションでは Media Convert の処理が完了したかどうかを受信するメールアドレスを設定します。

AWS SNS コンソールの左メニュー → サブスクリプションを開き
「サブスクリプションの作成」をクリックし以下の設定をします。
プロトコル:EMAIL
エンドポイント:受信したいメールアドレス
その他の項目はデフォルトで「サブスクリプションの作成」をクリックします。

するとエンドポイントに登録したメールアドレスに確認のメール(from:no-reply@sns.amazonaws.com)が届きます。
そのメールを開き「Confirm subscription」というリンクをクリックします。

Subscription confirmed!

と表示されたら SNS の準備は完了です。

MediaConvert を設定

まず Elastic Transcoder と Media Convert の違いを確認します。
また今回、私の環境では Lambda 上で Python から boto3(AWS sdk for Python)を使用して動的にパイプラインやジョブを生成しています。

Elastic Transcoder と Media Convert の違い

Elastic Transcoder のパイプラインが MediaConvert のキューになります。

そして大きな違いとして S3 のような入力情報・トリガーが
Elastic Transcoder ではパイプラインだったのが
MediaConvert ではジョブになりました。
詳細は以下の参考ページでわかりやすく図解してくれているのでご参照ください。

https://dev.classmethod.jp/articles/migrate-elastic-transcoder-to-mediaconvert/

プリセットを確認

プリセットとは動画や音声をどのように書き出すかという設定をまとめたものとイメージするとわかりやすいかもしれません。

Elastic Transcoder、MediaConvert ともに AWS があらかじめ用意してくれているプリセットがあります。
・Elastic Transcoder コンソール → Preset
・MediaConvert コンソール → 出力プリセットページのタブでシステムプリセットを選択
これで AWS があらかじめ用意してくれているデフォルトのプリセットが確認できます。

Lambda の python コードを生成

この Lambda のコードで動的に MediaConvert のジョブなどを生成していきます。
現状の Lambda で動かしている Transcoder 用のコードを変換します。

elastic-transcoder.py


# coding:utf-8

import boto3

from botocore.client import ClientError
import json

import urllib.request, urllib.parse, urllib.error


REGION_NAME = 'example-region-1'
TRANSCODER_ROLE_NAME = 'lambda_auto_transcoder_role_sample'
PIPELINE_NAME = 'test-hls-v4'
OUT_BUCKET_NAME = 'test-hls-output'
THUMBNAIL_BUCKET_NAME = 'test-images'
COMPLETE_TOPIC_NAME = 'transcoder'

print('Loading function')

s3 = boto3.resource('s3')
iam = boto3.resource('iam')
sns = boto3.resource('sns', REGION_NAME)
transcoder = boto3.client('elastictranscoder', REGION_NAME)

def lambda_handler(event, context):

    complete_topic_arn = sns.create_topic(Name=COMPLETE_TOPIC_NAME).arn
    transcoder_role_arn = iam.Role(TRANSCODER_ROLE_NAME).arn
    # Get the object from the event
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'])
    print(("bucket={}, key={}".format(bucket, key)))
    try:
        obj = s3.Object(bucket, key)
    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)))
        # Publish a message
        sns.Topic(complete_topic_arn).publish(
            Subject="Error!",
            Message="Failed to get object from S3. bucket={}, key={}, {}".format(bucket, key, e),
        )
        raise e
    # Delete inactive pipelines
    pipeline_ids = [pipeline['Id'] for pipeline in transcoder.list_pipelines()['Pipelines'] if pipeline['Name'] == PIPELINE_NAME]
    for pipeline_id in pipeline_ids:
        try:
            response = transcoder.delete_pipeline(Id=pipeline_id)
            print(("Delete a transcoder pipeline. pipeline_id={}".format(pipeline_id)))
            print(("response={}".format(response)))
        except Exception as e:
            # Raise nothing
            print(("Failed to delete a transcoder pipeline. pipeline_id={}".format(pipeline_id)))
            print(e)
    # Create a pipeline
    try:
        response = transcoder.create_pipeline(
            Name=PIPELINE_NAME,
            InputBucket=bucket,
            ContentConfig={'Bucket': OUT_BUCKET_NAME},
            Role=transcoder_role_arn,
            ThumbnailConfig={
                'Bucket': THUMBNAIL_BUCKET_NAME,
                'Permissions': [
                    {
                        'GranteeType':'Group',
                        'Grantee': 'AllUsers',
                        'Access': [
                            'Read',
                        ]
                    },
                ],
            },
            Notifications={
                'Progressing': '',
                'Completed': complete_topic_arn,
                'Warning': '',
                'Error': ''
            },
        )
        pipeline_id = response['Pipeline']['Id']
        print(("Create a transcoder pipeline. pipeline_id={}".format(pipeline_id)))
        print(("response={}".format(response)))
    except Exception as e:
        print("Failed to create a transcoder pipeline.")
        print(e)
        # Publish a message
        sns.Topic(complete_topic_arn).publish(
            Subject="Error!",
            Message="Failed to create a transcoder pipeline. bucket={}, key={}, {}".format(bucket, key, e),
        )
        raise e
    # Create a job
    try:
        job = transcoder.create_job(
            PipelineId=pipeline_id,
            Input={
                'Key': key,
                'FrameRate': 'auto',
                'Resolution': 'auto',
                'AspectRatio': 'auto',
                'Interlaced': 'auto',
                'Container': 'auto',
            },
            Outputs=[
                {
                    'Key': 'hls/{}_d/{}'.format('.'.join(key.split('.')[:-1]),'.'.join(key.split('.')[:-1])) + '_Video',
                    'ThumbnailPattern': 'hls/{}_d/{}'.format('.'.join(key.split('.')[:-1]),'.'.join(key.split('.')[:-1])) + '{count}_thumbnail',
                    'PresetId': 'xxxxxxxxxxxxxx-xxxxxx',  # System preset: Original
                    'SegmentDuration': '10',
                },
                {
                    'Key': 'hls/{}_d/{}'.format('.'.join(key.split('.')[:-1]),'.'.join(key.split('.')[:-1])) + '_Audio',
                    'PresetId': '1351620000001-200060',  # System preset: HLS 1M
                    'SegmentDuration': '10',
                },
            ],
            Playlists=[
                {
                    'Name': 'hls/{}_d/{}'.format('.'.join(key.split('.')[:-1]),'.'.join(key.split('.')[:-1])),
                    'Format': 'HLSv4',
                    'OutputKeys': [
                        'hls/{}_d/{}'.format('.'.join(key.split('.')[:-1]),'.'.join(key.split('.')[:-1])) + '_Video',
                        'hls/{}_d/{}'.format('.'.join(key.split('.')[:-1]),'.'.join(key.split('.')[:-1])) + '_Audio',
                    ],
                }
            ],
        )
        job_id = job['Job']['Id']
        print(("Create a transcoder job. job_id={}".format(job_id)))
        print(("job={}".format(job)))
    except Exception as e:
        print(("Failed to create a transcoder job. pipeline_id={}".format(pipeline_id)))
        print(e)
        # Publish a message
        sns.Topic(complete_topic_arn).publish(
            Subject="Error!",
            Message="Failed to create transcoder job. pipeline_id={}, {}".format(pipeline_id, e),
        )
        raise e
    return "Success"

media-convert.py


import boto3
import json
import urllib.parse
import os

REGION_NAME = 'example-region-1'  # リージョンを指定
MEDIA_CONVERT_ENDPOINT = 'https://abcd1234.mediaconvert.example-region-1.amazonaws.com'  # MediaConvert のエンドポイント
ROLE_ARN = 'arn:aws:iam::123456789012:role/YourMediaConvertRole'  # MediaConvert 用 IAM ロールの ARN
OUTPUT_BUCKET = 'test-hls-output'  # 出力先の S3 バケット名
SNS_TOPIC_ARN = 'arn:aws:sns:example-region-1:123456789012:YourSNSTopic'  # SNS トピックの ARN

print('Loading function')

def lambda_handler(event, context):
    # MediaConvert クライアントの初期化
    client = boto3.client('mediaconvert', region_name=REGION_NAME, endpoint_url=MEDIA_CONVERT_ENDPOINT)
    s3 = boto3.client('s3')
    sns = boto3.client('sns', region_name=REGION_NAME)

    # S3 イベントからバケット名とキーを取得
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'])
    filename = os.path.basename(key)
    basename = os.path.splitext(filename)[0]

    print(f'bucket={bucket}, key={key}')

    try:
        # 入力ファイルの存在確認
        s3.head_object(Bucket=bucket, Key=key)
    except Exception as e:
        print(e)
        sns.publish(
            TopicArn=SNS_TOPIC_ARN,
            Subject="Error!",
            Message=f"Failed to get object from S3. bucket={bucket}, key={key}, error={e}"
        )
        raise e

    # MediaConvert のジョブ設定
    job_settings = {
        'Role': ROLE_ARN,
        'Settings': {
            'Inputs': [
                {
                    'FileInput': f's3://{bucket}/{key}',
                    'AudioSelectors': {
                        'Audio Selector 1': {
                            'DefaultSelection': 'DEFAULT'
                        }
                    }
                }
            ],
            'OutputGroups': [
                {
                    'Name': 'File Group',
                    'OutputGroupSettings': {
                        'Type': 'HLS_GROUP_SETTINGS',
                        'HlsGroupSettings': {
                            'Destination': f's3://{OUTPUT_BUCKET}/hls/{basename}/',
                            'SegmentLength': 10,
                            'DirectoryStructure': 'SINGLE_DIRECTORY',
                            'ManifestDurationFormat': 'INTEGER',
                            'OutputSelection': 'MANIFESTS_AND_SEGMENTS',
                            'ProgramDateTime': 'EXCLUDE',
                            'CodecSpecification': 'RFC_4281',
                            'DestinationSettings': {
                                'S3Settings': {
                                    'AccessControl': {
                                        'CannedAcl': 'PUBLIC_READ'
                                    }
                                }
                            }
                        }
                    },
                    'Outputs': [
                        {
                            'NameModifier': '_Video',  # 出力ファイル名のサフィックスを指定
                            'ContainerSettings': {
                                'Container': 'M3U8'
                            },
                            'VideoDescription': {
                                'CodecSettings': {
                                    'Codec': 'H_264',
                                    'H264Settings': {
                                        'Bitrate': 5000000,
                                        'RateControlMode': 'CBR',
                                        'CodecProfile': 'MAIN',
                                        'CodecLevel': 'LEVEL_4_1',
                                        'GopSize': 90,
                                        'GopSizeUnits': 'FRAMES',
                                        'NumberBFramesBetweenReferenceFrames': 2
                                    }
                                }
                            },
                            'AudioDescriptions': [
                                {
                                    'AudioSourceName': 'Audio Selector 1',
                                    'CodecSettings': {
                                        'Codec': 'AAC',
                                        'AacSettings': {
                                            'Bitrate': 64000,
                                            'RateControlMode': 'CBR',
                                            'CodecProfile': 'LC',
                                            'CodingMode': 'CODING_MODE_2_0',
                                            'SampleRate': 48000,
                                            'Specification': 'MPEG4'
                                        }
                                    }
                                }
                            ]
                        }
                    ]
                }
            ]
        },
        'UserMetadata': {
            'JobCreatedBy': 'Lambda MediaConvert Function'
        }
    }

    try:
        # MediaConvert ジョブの作成
        response = client.create_job(**job_settings)
        job_id = response['Job']['Id']
        print(f'Created a MediaConvert job. job_id={job_id}')
    except Exception as e:
        print('Failed to create a MediaConvert job.')
        print(e)
        sns.publish(
            TopicArn=SNS_TOPIC_ARN,
            Subject='Error!',
            Message=f'Failed to create MediaConvert job. Error: {e}'
        )
        raise e

    return 'Success'

調整が必要な部分

media-convert.pyでいくつか調整が必要な部分がありますので設定していきます。

Media Convert のエンドポイント

リージョンごとにエンドポイントがあります。以下のページから確認できます。

https://docs.aws.amazon.com/ja_jp/general/latest/gr/mediaconvert.html

値を確認したら MEDIA_CONVERT_ENDPOINT の値に設定します。
確認した値を用いてLambdaのコードに当てはめます。東京リージョンでしたら以下のようになります。


MEDIA_CONVERT_ENDPOINT = 'https://mediaconvert.ap-northeast-1.amazonaws.com'

IAM ロールの設定

MediaConvert が使用する IAM のロールを設定します。ROLE_ARN に前述の「ロール作成」で作成したロールの ARN を AWS コンソールで確認してROLE_ARNの値として設定します。


ROLE_ARN = 'arn:aws:iam::123456789012:role/YourMediaConvertRole'

SNS の変数設定

SNS トピックの設定をしたことでコンソールから ARN の値が確認できると思います。その値を用いて以下のように Lambda のコードに当てはめます。


SNS_TOPIC_ARN = 'arn:aws:sns:ap-northeast-1:xxxxxxxxxxxxx:test-topic'

S3 バケット名を置き換える

あとは HLS 出力用のバケット名としてOUTPUT_BUCKETの値を置き換えます。

Lambda の設定

Lambda コンソールから設定を行います。イベントのきっかけとなる S3 バケットを紐付け、 mp4 ファイルがアップロードされたタイミングで python コードが実行され、HLS ファイルが S3 バケットに吐き出される仕組みです。

  1. Lambda コンソールにログインします。
  2. ナビゲーションメニューから「ダッシュボード」→「関数の作成」をクリックします。
  3. 以下のように設定を進めます。
    一から作成にチェック
    関数名を入力します。
    ランタイムは今回 python の最新版を指定
    アーキテクチャは x86_64 を設定
    デフォルトの実行ロールの変更から「IAMロールを作設定」で作成したロールを選択して
    Lambda コンソール画面の上部にある
    「トリガーを追加」
    プルダウンで「s3」を選択
    バケット名を指定、ここでは「mp4(加工前)保存用のバケット名」を指定
    イベントタイプでは「マルチパートアップロードの完了」を選択
    他はデフォルト設定のままで最後に再起呼び出しの承認にチェックを入れて「追加」をクリックします。
  4. タイムアウト(実行時間)を調整
    今回の処理は少し時間がかかるものになるのでタイムアウト(実行時間)を調整します。
    Lambda コンソール設定タブ → 一般設定の「編集」をクリックしタイムアウトを「30 秒」にします。

再起ループ検出について

Lambda では再起処理を自動で検出し処理を停止してくれる機能がデフォルトで有効になっています。(Lambda コンソール設定タブの再起検出設定)通常は 16 回処理が呼び出されたら停止するようになっているとのことでした。もし再起処理が必要な場合はこの機能をオフにする必要がありますが、無限ループになった際は請求も比例して増えることに注意してください。

エラーについて

いくつか遭遇したエラーについてもメモしておきます。

タイムアウト

Lambda ではデフォルトの実行時間が 3 秒になっており、今回のような少し時間のかかる処理は全て実行される前にシャットダウンされる場合があります。その際は以下のようなエラー(cloudwatchlog)が出ます。

END RequestId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
REPORT RequestId: xxxxxxx	Duration: 3000.00 ms	Billed Duration: 3000 ms	Memory Size: 128 MB	Max Memory Used: 84 MB	Status: timeout

処理が最後まで実行されない場合は前述のタイムアウト調整の設定を変えてみてください。

media convert の設定

必須項目の minSegmentLength という項目の設定が抜けておりエラーになりました。


An error occurred (BadRequestException) when calling the CreateJob operation: /outputGroups/0/outputGroupSettings/hlsGroupSettings: minSegmentLength is a required property

EventBridge の設定

media convert の実行結果を SNS で通知する際に EventBridge が使用できます。EventBridge はコンソールから設定していきます。

  1. AWS マネジメントコンソールで Amazon EventBridge コンソール を開きます。
  2. ナビゲーションペインで「ルール」を選択し、「ルールを作成」をクリックします。
  3. 「名前」に任意のルール名(例: MediaConvertJobCompleteRule)を入力します。
  4. 「イベントバス」はデフォルトのままにします。
  5. ルールタイプは「イベントパターンを持つルール」を選択し「次へ」をクリックします。
  6. 次の画面で以下の設定をします。書いていない項目はデフォルトで大丈夫です。

【イベントソース】セクション
イベントソース: 「AWS イベントまたは EventBridge パートナーイベント」

【イベントパターン】セクション
イベントソース:  AWS のサービス
AWS のサービス: 「MediaConvert」
イベントタイプ: 「MediaConvert Job State Change」
イベントタイプの仕様1: 特定の状態
特定の状態: COMPLETE

を設定して「次へ」をクリックします。

7.次の画面で以下の設定をします。
ターゲットタイプ: AWS のサービス
ターゲットを選択: SNS トピック
ターゲットの場所: このアカウントのターゲット
トピック: 前述の設定したトピックを選択

設定が終わったら「次へ」をクリックします。

8.必要であればタグを設定して「次へ」→ 確認して「ルールの作成」で完了です。

所感

IAMに関しては移行前より絞れたのでよかった&改めて勉強になりました。少し前であれば以下のようなツールも用意してくださっていたのですが、今回はスクリプトを編集する形で対応しました。

https://github.com/aws-samples/amazon-vod-preset-convert

今回の仕組みは動画一覧ページのメンテナンスがなくせるので重宝しています。Media Convertがあってよかった。

参考ページ

https://dev.classmethod.jp/articles/migrate-elastic-transcoder-to-mediaconvert/

https://d1.awsstatic.com/awselemental/howto/How-To-Converting-Amazon-Elastic-Transcoder-Presets-to-AWS-Elemental-MediaConvert.pdf

Discussion