はじめに
今回はWEBアプリ作成にあたり動画をアップロードし、閲覧できる機能を実装したいため
MediaConvertを使用して**HLS(HTTP Live Streaming)**で配信する環境をAWSで構築しました!
HLS VOD配信
AWSのハンズオンをほとんどを採用しているんですが笑
今回実装にあたりつまずいた点(ハンズオンで指示してるのに・・・)
や、少しカスタマイズした点があるのでその手順をアウトプットします。
カスタマイズしたのは処理した動画の出力先をバケットのトップではなく
動画がアップロードされた階層に出力するという点です。
Lambdaへアップロードするファイルの記述を変更してます^^
またハンズオンの画面は古いものがあり迷った点がありました(^ー^;
今回は最新の画面で対応してます!!
なぜHLSなのか
私も最初は大きすぎる動画ファイルを一定のサイズにするものかという認識でしたが、
それでは充分ではありませんでした。
通常の動画ファイルは一旦全てダウンロードした後に再生される仕組みです。
作成しているWEBアプリはスマホでの利用も想定しおり、
使用環境は様々でそれぞれの契約プランにも差がある。
もし通常の動画ファイルだと通信量が多くなり不便になることが容易に
考えられるため、少しづつダウンロードしながら再生する
ということと、
動画の解像度サイズを選べる
というHLSの形式が活きてきます!
なるほど!
ということでやっていきます^^
アーキテクチャー
以下のような構成。
ハンズオンと同じです!
出力先のS3は最初のS3と同じバケットです。
IAMロール作成
MediaConvertが正常に動作するように前もって用意されているポリシーを用いてMediaConvert用ロールを作成します。
サービスからIAMを選択。
左側のメニュー一覧からロールを選択。
そしてロールの作成を選択。
以下の画面に遷移し、AWSサービスを選択し、
下の図のようにMediaConvertを選択し、次のステップへ。
AmazonAPIGatewayInvokeFullAccessと
AmazonS3FullAccessのポリシーが付与されていること確認し、次にステップへ。
タグの作成は任意、次へ。
ロール名を入力し、ロールの作成で作成する。
作成後、念のため検索して作成されてるか確認する。
S3のバケット作成
サービスからS3を選択し、任意の名前のバケットを作成する。
特別な設定はここでしてません。
今回は「mediaconvert-movie」としました。
そして今回ハンズオンからカスタマイズした点は、処理した動画をトップに作成するのではなく、
動画があった階層にアウトプットフォルダを作成させる点です。
なのでmovie
というフォルダを作成し、コンソールから動画をアップロードしました。
下図のようにフォルダを作成してそこへ動画をアップロードしました。
MediaConvertでジョブを作成する
下図のようにジョブテンプレート
を選択。
テンプレートをインポート
を選択し、設定用jsonファイルを選択する。
これはハンズオン用のファイルにaws-mediaconvert-job-sample.json
というファイルが
あるのでそれを選択。
以下のような設定のJSONファイルです。
コード自体は以下。
テンプレートJSONコード
ハンズオンの資料と同じです。
{
"Queue": "",
"UserMetadata": {},
"Role": "",
"Settings": {
"OutputGroups": [
{
"Name": "Apple HLS",
"Outputs": [
{
"ContainerSettings": {
"Container": "M3U8",
"M3u8Settings": {
"AudioFramesPerPes": 4,
"PcrControl": "PCR_EVERY_PES_PACKET",
"PmtPid": 480,
"PrivateMetadataPid": 503,
"ProgramNumber": 1,
"PatInterval": 0,
"PmtInterval": 0,
"Scte35Source": "NONE",
"NielsenId3": "NONE",
"TimedMetadata": "NONE",
"VideoPid": 481,
"AudioPids": [
482,
483,
484,
485,
486,
487,
488,
489,
490,
491,
492
]
}
},
"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": 60,
"Slices": 1,
"GopBReference": "DISABLED",
"SlowPal": "DISABLED",
"SpatialAdaptiveQuantization": "ENABLED",
"TemporalAdaptiveQuantization": "ENABLED",
"FlickerAdaptiveQuantization": "DISABLED",
"EntropyEncoding": "CABAC",
"Bitrate": 15000000,
"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",
"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"
}
],
"OutputSettings": {
"HlsSettings": {
"AudioGroupId": "program_audio",
"AudioOnlyContainer": "AUTOMATIC",
"IFrameOnlyManifest": "EXCLUDE"
}
},
"NameModifier": "s1"
},
{
"ContainerSettings": {
"Container": "M3U8",
"M3u8Settings": {
"AudioFramesPerPes": 4,
"PcrControl": "PCR_EVERY_PES_PACKET",
"PmtPid": 480,
"PrivateMetadataPid": 503,
"ProgramNumber": 1,
"PatInterval": 0,
"PmtInterval": 0,
"Scte35Source": "NONE",
"NielsenId3": "NONE",
"TimedMetadata": "NONE",
"TimedMetadataPid": 502,
"VideoPid": 481,
"AudioPids": [
482,
483,
484,
485,
486,
487,
488,
489,
490,
491,
492
]
}
},
"VideoDescription": {
"Width": 640,
"ScalingBehavior": "DEFAULT",
"Height": 360,
"TimecodeInsertion": "DISABLED",
"AntiAlias": "ENABLED",
"Sharpness": 50,
"CodecSettings": {
"Codec": "H_264",
"H264Settings": {
"InterlaceMode": "PROGRESSIVE",
"NumberReferenceFrames": 3,
"Syntax": "DEFAULT",
"Softness": 0,
"GopClosedCadence": 1,
"GopSize": 60,
"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",
"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"
}
],
"OutputSettings": {
"HlsSettings": {
"AudioGroupId": "program_audio",
"AudioOnlyContainer": "AUTOMATIC",
"IFrameOnlyManifest": "EXCLUDE"
}
},
"NameModifier": "s2"
},
{
"ContainerSettings": {
"Container": "M3U8",
"M3u8Settings": {
"AudioFramesPerPes": 4,
"PcrControl": "PCR_EVERY_PES_PACKET",
"PmtPid": 480,
"PrivateMetadataPid": 503,
"ProgramNumber": 1,
"PatInterval": 0,
"PmtInterval": 0,
"Scte35Source": "NONE",
"NielsenId3": "NONE",
"TimedMetadata": "NONE",
"TimedMetadataPid": 502,
"VideoPid": 481,
"AudioPids": [
482,
483,
484,
485,
486,
487,
488,
489,
490,
491,
492
]
}
},
"VideoDescription": {
"Width": 480,
"ScalingBehavior": "DEFAULT",
"Height": 270,
"TimecodeInsertion": "DISABLED",
"AntiAlias": "ENABLED",
"Sharpness": 50,
"CodecSettings": {
"Codec": "H_264",
"H264Settings": {
"InterlaceMode": "PROGRESSIVE",
"NumberReferenceFrames": 3,
"Syntax": "DEFAULT",
"Softness": 0,
"GopClosedCadence": 1,
"GopSize": 60,
"Slices": 1,
"GopBReference": "DISABLED",
"SlowPal": "DISABLED",
"SpatialAdaptiveQuantization": "ENABLED",
"TemporalAdaptiveQuantization": "ENABLED",
"FlickerAdaptiveQuantization": "DISABLED",
"EntropyEncoding": "CABAC",
"Bitrate": 800000,
"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",
"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"
}
],
"OutputSettings": {
"HlsSettings": {
"AudioGroupId": "program_audio",
"AudioOnlyContainer": "AUTOMATIC",
"IFrameOnlyManifest": "EXCLUDE"
}
},
"NameModifier": "s3"
}
],
"OutputGroupSettings": {
"Type": "HLS_GROUP_SETTINGS",
"HlsGroupSettings": {
"ManifestDurationFormat": "INTEGER",
"SegmentLength": 10,
"TimedMetadataId3Period": 10,
"CaptionLanguageSetting": "OMIT",
"Destination": "",
"TimedMetadataId3Frame": "PRIV",
"CodecSpecification": "RFC_4281",
"OutputSelection": "MANIFESTS_AND_SEGMENTS",
"ProgramDateTimePeriod": 600,
"MinSegmentLength": 0,
"MinFinalSegmentLength": 0,
"DirectoryStructure": "SINGLE_DIRECTORY",
"ProgramDateTime": "EXCLUDE",
"SegmentControl": "SEGMENTED_FILES",
"ManifestCompression": "NONE",
"ClientCache": "ENABLED",
"StreamInfResolution": "INCLUDE"
}
}
}
],
"AdAvailOffset": 0,
"Inputs": [
{
"AudioSelectors": {
"Audio Selector 1": {
"Offset": 0,
"DefaultSelection": "DEFAULT",
"ProgramSelection": 1
}
},
"VideoSelector": {
"ColorSpace": "FOLLOW",
"Rotate": "DEGREE_0"
},
"FilterEnable": "AUTO",
"PsiControl": "USE_PSI",
"FilterStrength": 0,
"DeblockFilter": "DISABLED",
"DenoiseFilter": "DISABLED",
"TimecodeSource": "EMBEDDED",
"FileInput": ""
}
]
},
"AccelerationSettings": {
"Mode": "DISABLED"
},
"StatusUpdateInterval": "SECONDS_60",
"Priority": 0
}
コードをアップロードしたら、名前をconvert-templateとし、作成する。
作成したconvert-templateをチェックし、ジョブの作成へすすむ。
入力ファイルURLにS3アップロード画像のS3URL
を入力します。
次に左側にある出力グループ
のApple HLSを選択。
S3URL
自体は以下のようにS3のオブジェクトからコピーします。
ここで出力先を選択します。
参照
からバケット、ファルダを選択することができます。
以下のようにAWSの統合
を選択し、
既存のサービスロールを使用
を選択、
MC-role
を選択。(さっき作成したロール)
画面下の作成
をクリックして画像の処理をする。
ジョブのステータスがCOMPLETE
となれば処理完了。
以下のようにS3の指定したフォルダへファイルが追加されていること確認(拡張子が.ts .m3u8)
CloudFrontの設定
先ほど処理したS3のコンテンツをCloudFront経由で配信するための設定です。
サービスからCloudFrontを選択し、作成する。
オリジンドメイン
でS3のバケットを選択する。
S3バケットアクセス
で「はい」を選択。
新しいOAIを作成
を選択し、自動で名前が生成される。
バケットポリシー
は「はい、更新」を選択。
以下の図のようになる。
ここではS3 へのアクセスを「CloudFront からのみ」許可する設定をしてます。直接S3へは拒否となる。
キャッシュポリシー
をCachingOptimised
を選択。
オリジンリクエストポリシー
をCORS-S3Origin
を選択。
以上の設定でディストリビューションの作成をする。
S3のCORS設定
S3のコンソールへアクセスする。
該当のバケット
を選択。
アクセスの許可
を選択。
Cross-Origin Resource Sharing
の編集をクリックして以下を入力する。
[
{
"AllowedMethods": [
"GET"
],
"AllowedOrigins": [
"*"
]
}
]
下の図のように入力し、変更の保存をする。
視聴の確認
動画のURLは「https://{CloudFrontのディストリビューションドメイン名}/{動画ファイル名}.m3u8」です。
CloudFrontのディストリビューションドメイン名
の確認方法は以下。
以下のように、「https://{CloudFrontのディストリビューションドメイン名}/{ファルダあれば/動画ファイル名}.m3u8」を入力しApply
を押すと動画が開始される。
下のほうにレートを選択できるのでそれぞれの解像度で見れることが確認できる!
ここまで構築できたら、あとはLambdaを設定して、動画がアップロードされたら、
自動でMediaConvertのジョブが自動で動くように設定します。
Lambdaの設定
Lambda用のロールを作成
コンソールからIAMを選択。
まずは専用のポリシーを自作します。
左の一覧からポリシー
を選択。
ポリシー
の作成を選択。
JSON
タブを選択し、以下のコードを入力する。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"mediaconvert:CreateJob",
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:PutLogEvents",
"iam:PassRole"
],
"Resource": "*"
}
]
}
MediaConvertへのアクセスやCloudWatchへのログ書き込みを許可してます。
ポリシー名は「LambdaCustomPolicy」でポリシー作成します。
Lambda用のロール作成
さきほどのポリシーでロール作成します。
ロールの作成
を選択し、
一般的なユースケースからLambda
を選択。
ポリシー選択はさきぼどの「LambdaCustomPolicy」
を検索しアタッチする。
ロール名を「MC-Lambda-role」
としました。
名称は任意です。
関数作成
マネージメントコンソールよLambdaを選択。
関数作成
をクリック。
以下のように
任意の関数名を入力。
ランタイム
はPython3.8
を選択。
既存のロールから先ほど作成したLambda用のロールを選択。
トリガーとしてS3を選択します。
以下のように
バケット
を選択し、
サフィックス
に「.mp4」を入力。
再帰呼び出しの注意事項に同意し追加
をクリック。
指定したS3バケットにmp4の拡張子のファイルがアップロードされたら実行するというトリガーです。
次に実行するソースをアップロードします。
コードタブを選択。
アップロード元
から「.zipファイル」を選択しコードをアップロードします。
アップロードするコードは以下です。ほとんどハンズオンと同じです。
・job.json
・lambda_function.py
の二つでzip化してまとめてアップロードします。
※注意点は他の不要なファイルが入ってるとうまく動作しません!
{
"Inputs": [
{
"FileInput": ""
}
],
"OutputGroups": [
{
"OutputGroupSettings": {
"HlsGroupSettings": {
"Destination": ""
}
}
}
]
}
ハンズオンと違う点は以下のコードです。
ディレクトリーの情報を取得し、
出力するときの同じ階層となるように加工してます。
print()
はデバック用です。
import json
import urllib.parse
import boto3
import os
import pathlib
# import pprint
s3 = boto3.client('s3')
def lambda_handler(event, context):
account = context.invoked_function_arn.split(':')[4]
region = event['Records'][0]['awsRegion']
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
#環境変数の値を代入
template_name = os.environ['template_name']
role_name = os.environ['role_name']
inputFile = "s3://" + bucket + "/" + key
print('inputFile=' + inputFile)
pfile = pathlib.Path(inputFile)
pdir = os.path.dirname(key) #ディレクトリ名を取得
print('pdir=' + pdir) #確認
# outputKey = "s3://" + bucket + "/output/"
outputKey = "s3://" + bucket + "/" + pdir + "/output/"
print('outputKey=' + outputKey) #問題ないか確認
mediaconvert = boto3.client(
'mediaconvert',
region_name=region,
endpoint_url=os.environ['endpoint_url']
)
with open("job.json", "r") as jsonfile:
job_object = json.load(jsonfile)
#jsonファイルへインプット元のファイルと出力先を代入してMCへ渡す
job_object["OutputGroups"][0]["OutputGroupSettings"]["HlsGroupSettings"]["Destination"] = outputKey
job_object["Inputs"][0]["FileInput"] = inputFile
response = mediaconvert.create_job(
JobTemplate='arn:aws:mediaconvert:%s:%s:jobTemplates/%s' % (region, account, template_name),
Queue='arn:aws:mediaconvert:%s:%s:queues/Default' % (region, account),
Role='arn:aws:iam::%s:role/%s' % (account, role_name),
Settings=job_object
)
# pprint.pprint(event['Records'])
# pprint.pprint(event['Records'][0]
# pprint.pprint(event['Records'][0]['s3'])
# pprint.pprint(event['Records'][0]['s3']['object'])
# pprint.pprint(event['Records'][0]['s3']['object']['key'])
# print(event['Records'])
# print(os.environ)
# print(context.invoked_function_arn.split(':'))
ここで注意点
アップロードしただけだと下のように上位のフォルダがあり、うまく動作しません。
私は初めてのLambdaだったのでここがよくわからずつまりました(ToT)
トップに
・job.json
・lambda_function.py
をもってくるようにしましょう!(あたりまえ)
環境変数の設定
設定
ダブを選択。
環境変数
を選択。
編集
をクリックして、環境変数を追加します。
追加する内容は以下。
endpoint_url:
MediaConvert の API endpoint
「https://xxxxxx.mediaconvert.ap-northeast-1.amazonaws.com」
role_name:
MediaConvert用のrole
※ここでの注意点ですが、MediaConvert用のロールです!
私は間違ってLambdaのロールを入力し詰まりました・・・
Lambdaの編集だから・・・と。
template_name:
MediaConvertで作成したtemplate名「convert-template」
をキー
、値
として入力してください。
これで設定完了です!
S3に動画ファイル(.mp4)をアップロードすると
数秒後にoutput
フォルダが作成されます!
ファルダの中に複数ファイルが作成されでます!
CloudFront経由で見れることを確認します。
私はこれをCORSでWEBアプリに反映されました^^
うまく動作しないとき
ログを確認します。
Lambdaのモニタリング
タブをクリック。
CloudWatchのログを表示
をクリックするとログが確認できます。
ここでも問題なければMediaConvertのジョブを確認すれば
何らしか失敗した原因が表示されてます!