AWS Elemental MediaConvertでMP4をDASH形式に変換し、CloudFront経由で動画配信する仕組みをTerraformで構築してみた

AWS Elemental MediaConvertでMP4をDASH形式に変換し、CloudFront経由で動画配信する仕組みをTerraformで構築してみた

2025.12.05

はじめに

こんにちは、ゲームソリューション部のsoraです。
今回は、AWS Elemental MediaConvertでMP4をDASH形式に変換し、CloudFront経由で動画配信する仕組みをTerraformで構築してみたことについて書いていきます。

Elemental MediaConvertでのMP4からDASHへの変換やそれをTerraformで構築することをメインに確認するため、このアプリを作ってみました。

アプリについて

作成したアプリは以下2画面しかない簡易的なものです。
動画一覧画面があって、サムネイルの表示・動画情報の表示を行い、動画詳細画面でDASH形式で動画を再生します。

mediaconvert-videostream-app-p1-01
mediaconvert-videostream-app-p1-02

構成

以下のような構成を構築しました。

mediaconvert-videostream-app-p1-06

動画投稿者がS3にMP4形式の動画ファイルを配置すると、S3イベント通知でLambdaを呼び出し、DynamoDBへのメタデータの書き込み、Elemental MediaConvertへのジョブの発行を行います。
変換が終了すると、Elemental MediaConvertは変換後のDASH形式のファイルや生成したサムネイルをS3に配置します。

EventBridgeにて、Elemental MediaConvertのジョブの完了イベントをトリガーにLambdaを呼び出し、DynamoDBの動画ステータスをreadyに更新、マニフェストファイルやサムネイルのパスを登録します。

ユーザーはCloudFront経由で動画一覧画面へアクセスします。
動画一覧画面の動画リストなどの動的な情報は、Lambda経由でDynamoDBから取得して表示します。
動画詳細画面の情報もまた、Lambda経由でDynamoDBから取得して表示します。

ちなみに、検証用の簡易なアプリなため、認証が入っていなかったり、動画ファイルへのアクセスが署名付きURLなしで直アクセスする形になってたりします。
CloudFront Functionは、自身のIPアドレスからのみのアクセスに制限をかけるために使用しています。

DASH形式とは

今回はMP4ではなく、DASH(MPEG-DASH)形式に変換して配信します。
DASH形式では、マニフェスト(manifest.mpd)を読み取り、ネットワーク状況に応じてセグメントを選択して再生します。

項目 MP4をそのまま配信 DASH形式で配信
ファイル構成 1つの動画ファイル マニフェスト + 複数のセグメントファイル
再生開始 全体をダウンロードしてから セグメント単位で即座に再生開始
画質調整 固定 ネットワーク状況に応じて自動調整(ABR)

構築

AWSリソースをTerraformで構築して、LambdaではGoを使用しました。
メインであるElemental MediaConvert周りのみを解説していきます。

AWSリソースの構築

Elemental MediaConvertの構築は以下のように構築します。
キューはジョブを発行する際に指定する必要があるため、事前に作成します。

resource "aws_media_convert_queue" "main" {
  name   = "${var.name_prefix}-queue"
  status = "ACTIVE"
}

DynamoDBのテーブル構成は以下です。(構築後の画像です。)
動画再生に必要なマニフェストファイルのパスやサムネイルのパス、タイトルや作成日、あとは動画が配信可能な状態かを表すステータスなどを持たせています。

mediaconvert-videostream-app-p1-04

LambdaからElemental MediaConvertへのジョブ作成

GoでElemental MediaConvertにDASH形式の動画変換ジョブを発行する実装を抜粋したものは以下です。
Elemental MediaConvertに関して、Terraformで構築するものは少なかったですが、バックエンド側で設定などを書く必要があります。
これは、Elemental MediaConvertのジョブテンプレートを使用すれば、バックエンドはもっと簡略化することが可能です。(そっちの方が良かったと反省しています。)

// ========================================
// ビデオ出力の生成(解像度ごとに呼び出す)
// ========================================
func createVideoOutput(width, height int32, bitrate int32, nameModifier string) mctypes.Output {
    return mctypes.Output{
        NameModifier: strPtr(nameModifier),
        ContainerSettings: &mctypes.ContainerSettings{
            Container: mctypes.ContainerTypeMpd,
        },
        VideoDescription: &mctypes.VideoDescription{
            Width:  &width,
            Height: &height,
            CodecSettings: &mctypes.VideoCodecSettings{
                Codec: mctypes.VideoCodecH264,
                H264Settings: &mctypes.H264Settings{
                    Bitrate:            &bitrate,
                    RateControlMode:    mctypes.H264RateControlModeCbr,
                    GopSize:            float64Ptr(2.0),
                    GopSizeUnits:       mctypes.H264GopSizeUnitsSeconds,
                    CodecProfile:       mctypes.H264CodecProfileMain,
                    CodecLevel:         mctypes.H264CodecLevelAuto,
                    InterlaceMode:      mctypes.H264InterlaceModeProgressive,
                    ParControl:         mctypes.H264ParControlSpecified,
                    ParNumerator:       intPtr(1),
                    ParDenominator:     intPtr(1),
                    NumberBFramesBetweenReferenceFrames: intPtr(2),
                },
            },
        },
    }
}

// ========================================
// MediaConvertジョブ設定の構築
// ========================================
func createJobSettings(inputS3, outputS3, thumbnailS3 string) *mctypes.JobSettings {
    return &mctypes.JobSettings{
        // 入力設定
        Inputs: []mctypes.Input{
            {
                FileInput: &inputS3,
                AudioSelectors: map[string]mctypes.AudioSelector{
                    "Audio Selector 1": {
                        DefaultSelection: mctypes.AudioDefaultSelectionDefault,
                    },
                },
                VideoSelector: &mctypes.VideoSelector{},
            },
        },
        OutputGroups: []mctypes.OutputGroup{
            // ----------------------------------------
            // DASH出力グループ(ABR用に複数解像度を出力)
            // ----------------------------------------
            {
                Name: strPtr("DASH ISO"),
                OutputGroupSettings: &mctypes.OutputGroupSettings{
                    Type: mctypes.OutputGroupTypeDashIsoGroupSettings,
                    DashIsoGroupSettings: &mctypes.DashIsoGroupSettings{
                        Destination:      &outputS3,
                        SegmentLength:    intPtr(6),
                        FragmentLength:   intPtr(2),
                        SegmentControl:   mctypes.DashIsoSegmentControlSegmentedFiles,
                        WriteSegmentTimelineInRepresentation: mctypes.DashIsoWriteSegmentTimelineInRepresentationEnabled,
                    },
                },
                Outputs: []mctypes.Output{
                    // 映像: 1080p / 720p / 480p
                    createVideoOutput(1920, 1080, 5000000, "video_1080p"),
                    createVideoOutput(1280, 720, 2500000, "video_720p"),
                    createVideoOutput(854, 480, 1000000, "video_480p"),
                    // 音声: AAC 128kbps
                    {
                        NameModifier: strPtr("audio"),
                        ContainerSettings: &mctypes.ContainerSettings{
                            Container: mctypes.ContainerTypeMpd,
                        },
                        AudioDescriptions: []mctypes.AudioDescription{
                            {
                                AudioSourceName: strPtr("Audio Selector 1"),
                                CodecSettings: &mctypes.AudioCodecSettings{
                                    Codec: mctypes.AudioCodecAac,
                                    AacSettings: &mctypes.AacSettings{
                                        Bitrate:    intPtr(128000),
                                        CodingMode: mctypes.AacCodingModeCodingMode20,
                                        SampleRate: intPtr(48000),
                                    },
                                },
                            },
                        },
                    },
                },
            },
            // ----------------------------------------
            // サムネイル出力グループ(動画から静止画を抽出)
            // ----------------------------------------
            {
                Name: strPtr("Thumbnails"),
                OutputGroupSettings: &mctypes.OutputGroupSettings{
                    Type: mctypes.OutputGroupTypeFileGroupSettings,
                    FileGroupSettings: &mctypes.FileGroupSettings{
                        Destination: &thumbnailS3,
                    },
                },
                Outputs: []mctypes.Output{
                    {
                        NameModifier: strPtr("nail"),
                        ContainerSettings: &mctypes.ContainerSettings{
                            Container: mctypes.ContainerTypeRaw,
                        },
                        VideoDescription: &mctypes.VideoDescription{
                            Width:              intPtr(480),
                            Height:             intPtr(270),
                            ScalingBehavior:    mctypes.ScalingBehaviorDefault,
                            AntiAlias:          mctypes.AntiAliasEnabled,
                            CodecSettings: &mctypes.VideoCodecSettings{
                                Codec: mctypes.VideoCodecFrameCapture,
                                FrameCaptureSettings: &mctypes.FrameCaptureSettings{
                                    FramerateNumerator:   intPtr(1),
                                    FramerateDenominator: intPtr(1),
                                    MaxCaptures:          intPtr(1),
                                    Quality:              intPtr(80),
                                },
                            },
                        },
                    },
                },
            },
        },
    }
}

// ========================================
// MediaConvertクライアントの初期化とジョブ発行
// ========================================
// AWS設定の読み込み
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
    return fmt.Errorf("failed to load AWS config: %w", err)
}

// MediaConvertエンドポイントを取得(アカウント固有のカスタムエンドポイントが必要)
mcClient := mediaconvert.NewFromConfig(cfg)
endpointsOutput, err := mcClient.DescribeEndpoints(ctx, &mediaconvert.DescribeEndpointsInput{})
if err != nil {
    return fmt.Errorf("failed to describe MediaConvert endpoints: %w", err)
}
if len(endpointsOutput.Endpoints) == 0 {
    return fmt.Errorf("no MediaConvert endpoints found")
}

// カスタムエンドポイントでクライアントを再作成
mcClient = mediaconvert.NewFromConfig(cfg, func(o *mediaconvert.Options) {
    o.EndpointResolver = mediaconvert.EndpointResolverFromURL(*endpointsOutput.Endpoints[0].Url)
})

// S3パスの構築
inputS3 := fmt.Sprintf("s3://%s/%s", rawBucket, s3Key)
outputS3 := fmt.Sprintf("s3://%s/%s/manifest", convertedBucket, outputPrefix)
thumbnailS3 := fmt.Sprintf("s3://%s/%s/thumb", convertedBucket, outputPrefix)

// ジョブ設定を作成
jobSettings := createJobSettings(inputS3, outputS3, thumbnailS3)

// MediaConvertジョブを発行
_, err = mcClient.CreateJob(ctx, &mediaconvert.CreateJobInput{
    Queue:    &mediaConvertQueueArn,
    Role:     &mediaConvertRoleArn,
    Settings: jobSettings,
    UserMetadata: map[string]string{
        "video_id": videoID,
    },
})

動作確認

ブラウザ上からアクセスしてみると、以下の動画一覧画面が表示されます。
Elemental MediaConvertで生成したサムネイルも表示されています。
mediaconvert-videostream-app-p1-01

動画をクリックすると、動画詳細画面が表示され、動画を再生することができました。
この動画はDASH形式でありファイルがセグメント分けされているため、動画全てのロードが終わらなくても、取得できたセグメントを順に再生していくことが可能です。
mediaconvert-videostream-app-p1-02

動画変換後ファイルの確認

動画変換後にファイルが配置されるS3バケットを見てみると、動画ごとのディレクトリが作成されて、以下のようなファイルが配置されていました。

mediaconvert-videostream-app-p1-05

  • manifest.mpd:マニフェストファイル
  • manifestaudioinit.mp4:初期化セグメント
  • manifestvideo_xxxxpinit.mp4:映像トラックのコーデック情報やメタデータを含む初期化ファイル
  • manifestaudio_000000001.mp4など:音声セグメント
  • manifestvideo_xxxxp_000000001.mp4など:映像セグメント
  • thumbnail.0000000.jpg:サムネイル画像

最後に

今回は、AWS Elemental MediaConvertでMP4をDASH形式に変換し、CloudFront経由で動画配信する仕組みをTerraformで構築してみたことを記事にしました。
どなたかの参考になると幸いです。

この記事をシェアする

FacebookHatena blogX