はじめに
DIVXの井上です。
今回はVT1インスタンス、S3、CloudFrontを使ってリアルタイムで映像素材を合成しつつ動画配信(ライブストリーミング)を行います。
AWSにおけるライブストリーミングについて
AWSでライブストリーミング基盤を構築する際はMediaLive、MediaConvert、MediaStoreといったいわゆるMedia系サービスとCloudFrontを組み合わせて構築するのが一般的です。また、近年ではInteractive Video Serviceといったマネージドなライブストリーミングサービスも提供されているため、単純にライブストリーミングを行うのであればまずそれらの利用を検討すべきでしょう。
上記のサービスで一般的な動画配信で求められる機能(画像による透かしなど)はおおよそ提供されていますが、それらのサービスでは実現できない特殊な要件がある場合には今回紹介するVT1インスタンスのようなEC2インスタンスを利用することになります。
VT1インスタンスとは?
AWSのEC2で提供されているインスタンスタイプの一つです。 リアルタイムの動画トランスコーディングに特化したインスタンスであり、Xilinx Alveo U30メディアアクセラレータカードを搭載しているため、高速に動画のトランスコードを行うことができます。
本記事のゴール
今回は「サーバーサイドでリアルタイムに雪が降る*1映像素材を合成してライブストリーミングを行う」ことを目指していきます。*2 構築するインフラの構成は以下の図の通りです。
注意・免責事項
- この記事を参考にして生じた損害などの責任は負いかねます
- aws-cliやsshなどの基本的なコマンドは使える前提で進めていきます
- VT1インスタンスは比較的高額なインスタンスです。利用しない時は停止することを推奨します
- 今回使用するvt1.3xlargeのオンデマンド料金は0.81824USD/hourなので、24時間30日起動させっぱなしの場合の料金はおおよそ8万円超になります*3
- 本記事は「VT1インスタンスでこんなことできるよ」といった紹介が目的であるため、本来必要なチューニングやセキュリティ対応などは一部省略しています
- チューニングではエンコード時のビットレートやサイズ調整、セキュリティ対応ではRTMPS対応や配信元IP制限など
- 本記事で使用しているXilinx Video SDKのバージョンは1.5です。またXilinxから提供されているUbuntuベースのSDKインストール済みのAMIを使ってEC2を作成しています
- Xilinx Video SDKの最新バージョンは2.0です。そちらを利用したい方やAmazon Linux 2ベースのイメージを使いたい方はインスタンスを別途作成してSDKや各種ミドルウェアの設定などを行ってください。
インフラ構築
最初に先ほど示したインフラ構成に必要なAWSリソースをCloudFormationで作成します。
適当なディレクトリにinfra.ymlというファイルを作り、以下の内容を書き込みます。 (EC2で使用するSSHキーのみ各自所有のものを記入してください)
AWSTemplateFormatVersion: "2010-09-09"
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 192.168.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-vpc"
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-igw"
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: ap-northeast-1a
CidrBlock: 192.168.10.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} Public Subnet (AZ1)"
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} Public Routes"
DefaultPublicRoute:
Type: AWS::EC2::Route
DependsOn: InternetGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet
S3Bucket:
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Sub "${AWS::StackName}-s3-bucket"
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: false
IgnorePublicAcls: true
RestrictPublicBuckets: false
WebsiteConfiguration:
IndexDocument: index.html
S3BucketPolicy:
Type: "AWS::S3::BucketPolicy"
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Statement:
- Effect: Allow
Principal: "*"
Action:
- s3:GetObject
Resource:
- !Sub 'arn:aws:s3:::${S3Bucket}/*'
Condition:
StringEquals:
aws:UserAgent: Amazon CloudFront
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: !Sub "${S3Bucket}.s3-website-${AWS::Region}.amazonaws.com"
Id: CustomOrigin
CustomOriginConfig:
HTTPPort: 80
OriginProtocolPolicy: http-only
Enabled: true
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: CustomOrigin
ForwardedValues:
QueryString: false
DefaultTTL: 1
MaxTTL: 1
MinTTL: 1
ViewerProtocolPolicy: redirect-to-https
EncoderIamRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: !Sub "${AWS::StackName}-encoder-instance-role"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "ec2.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
EncoderIamPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub "${AWS::StackName}-encoder-instance-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "s3:*"
Resource:
- "*"
Roles:
- !Ref EncoderIamRole
IAMInstanceProfile:
Type: "AWS::IAM::InstanceProfile"
Properties:
Path: "/"
InstanceProfileName: !Sub "${AWS::StackName}-encoder-instance-profile"
Roles:
- !Ref EncoderIamRole
EC2SecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: "allow ssh, http, rtmp, rtmps"
GroupName: !Sub "${AWS::StackName}-encoder-sg"
VpcId: !Ref VPC
SecurityGroupIngress:
- CidrIp: "0.0.0.0/0"
FromPort: 80
IpProtocol: "tcp"
ToPort: 80
- CidrIp: "0.0.0.0/0"
FromPort: 1935
IpProtocol: "tcp"
ToPort: 1935
- CidrIp: "0.0.0.0/0"
FromPort: 22
IpProtocol: "tcp"
ToPort: 22
SecurityGroupEgress:
- CidrIp: "0.0.0.0/0"
IpProtocol: "-1"
Encoder:
Type: "AWS::EC2::Instance"
Properties:
ImageId: "ami-0c90a32843da57f18"
InstanceType: "vt1.3xlarge"
KeyName: "encoder"
AvailabilityZone: !Sub "${AWS::Region}a"
Tenancy: "default"
SubnetId: !Ref PublicSubnet
EbsOptimized: true
IamInstanceProfile: !Ref IAMInstanceProfile
SecurityGroupIds:
- !Ref EC2SecurityGroup
SourceDestCheck: true
Tags:
- Key: "Name"
Value: !Sub "${AWS::StackName}-encoder-instance"
UserData: !Base64 |
#!/bin/bash
sudo yum install -y git
EncoderElasticIP:
Type: "AWS::EC2::EIP"
Properties:
Domain: vpc
EncoderElasticIPAssociate:
Type: AWS::EC2::EIPAssociation
Properties:
AllocationId: !GetAtt EncoderElasticIP.AllocationId
InstanceId: !Ref Encoder
ymlファイルを作成したら以下のコマンドでCloudFormationでスタックの構築を行います。
本記事ではadv-streamingという名前でスタックを作成しますがスタック名からAWSリソース名を設定しているため、他の名前にした場合はこれ以降に adv-streaming と記載されている部分はそのスタック名に読み替えてください。
(例:divx-liveという名前でスタックを作った場合、「adv-streaming-s3-bucket」は「divx-live-s3-bucket」となります。)
aws cloudformation create-stack --stack-name adv-streaming --template-body file://./infra/infra.yml --capabilities CAPABILITY_NAMED_IAM
EC2のセッティング
まずはsshやAWSコンソールからインスタンスに接続して、apt updateと更新パッチを当てていきます。
sudo apt update -y
# パッチの適用
wget https://raw.githubusercontent.com/Xilinx/video-sdk/v1.5/patches/u30_1.5_patch.sh
chmod 755 u30_1.5_patch.sh
./u30_1.5_patch.sh
nginx関連
配信ストリームを受け取るため、nginx-rtmp-moduleを組み込んだ上でnginxをインストールします。
wget https://nginx.org/download/nginx-1.22.1.tar.gz
tar zxvf nginx-1.22.1.tar.gz
git clone https://github.com/sergey-dryabzhinsky/nginx-rtmp-module.git
cd nginx-1.22.1
./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --add-module=../nginx-rtmp-module/
make
sudo make install
# nginx version: nginx/1.22.1 が出れば成功
nginx -V
配信ストリームを受け取った際にはffmpegによるエンコードを行い、/etc/nginx/html/hls/以下にライブストリーミングに必要な動画ファイルとプレイリストファイルが作成されるようにしたいため、その設定を行います。
sudo cp /etc/nginx/conf/nginx.conf /etc/nginx/conf/nginx.conf.orig
sudo rm /etc/nginx/conf/nginx.conf
sudo vim /etc/nginx/conf/nginx.conf
/etc/nginx/conf/nginx.confには以下の内容を書き込みます。
(通常であればrootでnginxを動かすのは危険ですが、本記事では設定の簡略化のためにrootで動かします。本番に導入する際は別途実行ユーザーを作ることを強く推奨します。)
user root;
worker_processes 1;
events {
worker_connections 1024;
}
rtmp {
server {
listen 1935;
chunk_size 8192;
application src {
live on;
exec /etc/nginx/conf/exec_wrapper.sh $name;
exec_kill_signal term;
access_log logs/src_access.log;
}
application hls {
live on;
hls on;
hls_path /etc/nginx/html/hls/;
hls_nested on;
hls_fragment 5s;
record off;
hls_playlist_length 12h;
access_log logs/hls_access.log;
}
}
}
本来であれば上記のconfファイルにある exec ディレクティブに直接ffmpegのコマンドを書けば良い感じに処理してくれるのですが、Xilinxから提供されているSDKインストール済みのAMIの仕様としてffmpegを使うには毎回コンソールを開く度にセットアップコマンドを実行しなければならないため、execディレクティブには別途スクリプトを呼び出すようにしています。
なので、次はそのスクリプトをこちらを参考にしつつ
/etc/nginx/conf/exec_wrapper.sh に記述します。
#!/bin/bash
LOG_OUT=/home/ubuntu/wrapper_log
LOG_ERR=/home/ubuntu/wrapper_log
exec 1>>$LOG_OUT
exec 2>>$LOG_ERR
on_die ()
{
# kill all children
pkill -KILL -P $$
}
source /opt/xilinx/xcdr/setup.sh
trap 'on_die' TERM
/opt/xilinx/ffmpeg/bin/ffmpeg
-i rtmp://localhost/src/$1
-stream_loop -1 -fflags +genpts -i /home/ubuntu/snow_loop.mp4
-filter_complex "[1:0]colorkey=black:0.01:1[colorkey];[0:0][colorkey]overlay=x=0:y=0"
-c:v mpsoc_vcu_h264 -c:a aac -b:v 1920k -b:a 64k -f flv rtmp://localhost:1935/hls/$1 &
wait
記述が終わったら sudo nginx -t で文法のチェックを行いましょう。
なお、このタイミングで合成に使用する雪が降ってる映像素材をscpコマンドなどを使用して/home/ubuntu/snow_loop.mp4 にアップロードしておきます。
(本物の動画素材ははてブでは貼り付けられないのでそれっぽいgifを貼っておきます。)
自動起動設定も行います。
sudo vim /lib/systemd/system/nginx.serviceで以下の内容を書き込みます。
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/etc/nginx/logs/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
保存したら以下のコマンドを実行して自動起動を有効にします。
sudo systemctl daemon-reload
sudo systemctl enable nginx.service
sudo systemctl start nginx.service
goofys
S3とEC2間の動画関連のファイルの同期にgoofysを利用します。
以下のコマンドでインストールを行います。
sudo yum install -y golang-go fuse
sudo wget https://github.com/kahing/goofys/releases/download/v0.24.0/goofys -P /usr/local/bin
sudo chown ec2-user:ec2-user /usr/local/bin/goofys
chmod 755 /usr/local/bin/goofys
goofys --version
# goofys version 0.24.0 が出れば成功
S3のマウントポイントとなるディレクトリを作成し、実際にgoofysでS3とEC2間のファイル同期を行えるか確認します。 ここからの作業はルートユーザーに切り替えてから行います。
sudo su -
mkdir /mnt/video
chown ec2-user:ec2-user /mnt/video/
/usr/local/bin/goofys --use-content-type adv-streaming-s3-bucket /mnt/video/
mkdir /mnt/video/hls
# 数秒待ってS3側にファイルができていれば成功
# aws-cliがインストールされていれば aws s3 ls adv-streaming-s3-bucket でも確認可能
goofysについても起動時に自動マウントを行うようにします。 /etc/fstabの末尾行に以下を書き込んでください。
/usr/local/bin/goofys#adv-streaming-s3-bucket /mnt/video fuse _netdev,allow_other,--file-mode=0666,--dir-mode=0777 0 0
nginxで受け取った動画ストリームがffmpegによって最終的にHLSに変換されたものが/etc/nginx/html/hlsに格納されるため、最後にS3と同期されるディレクトリにシンボリックリンクを作成します。
ln -s /mnt/video/hls /etc/nginx/html/hls
視聴用ページの用意
/mnt/video/index.htmlを作成します。 /mnt/video/以下はS3に同期されるようになっているため、作成するとS3にもアップロードされているかと思います。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<link href="//vjs.zencdn.net/7.10.2/video-js.min.css" rel="stylesheet">
<script src="//vjs.zencdn.net/7.10.2/video.min.js"></script>
<style>
html {
background-color: black;
}
.video-contents {
display: flex;
height: 95vh;
margin-top: 2.5vh;
justify-content: center;
}
.video-content {
min-height: 95vh;
display: flex;
flex-direction: column;
}
.video-content video {
width: 100%;
}
</style>
</head>
<body>
<div class="video-contents">
<div class="video-content">
<video id="my-player" class="video-js" controls preload="auto" data-setup='{}'>
<source src="/hls/test/index.m3u8" type="application/x-mpegURL" />
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a
web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">
supports HTML5 video
</a>
</p>
</video>
</div>
</div>
<script>
var player = videojs('my-player', options, function onPlayerReady() {
this.play();
});
</script>
</body>
</html>
動作確認
利用する配信機器からrtmp://(EC2のパブリックIPv4 DNS):1935/src/test に対してストリームの送信を行います。
(OBSなど、配信サーバーとストリームキーの入力欄が分かれている場合は rtmp://(EC2のパブリックIPv4 DNS):1935/src までを配信ソフト側のサーバー欄に、testはストリームキー欄に入力します。詳しくは使用する配信ソフトの説明をご覧ください。)
作成したCloudFrontのディストリビューションドメイン名(https://xxxxxxxxxxxxx.cloudfront.net)にアクセスします。 プレイヤーの再生画面を押すと配信元の映像に雪が降る映像素材が合成された映像が映るかと思います。
iPhoneで近所の公園を撮影した映像に素材が合成されて配信される様子*4リアルタイムで合成しています
後片付け
S3のバケット内にあるオブジェクトを全て削除してから aws cloudformation delete-stack --stack-name adv-streaming を実行、またはCloudFormationのWebコンソールのページにて削除します。
(先に述べた通り、vt1.3xlargeを起動させっぱなしにすると約$0.82/時かかるので少なくともインスタンスは停止しましょう。)
改善点
- これでもリアルタイムで配信はできるもののそこそこな頻度でカクついたりするため、ffmpegコマンドのパラメータや配信元機材の設定は念入りに調査した方が良いでしょう
- もう少し本格的にVT1インスタンスを使って(例えば公式のチュートリアルで示されているような複数サイズの動画の同時エンコードも追加で行うなど)何かしらのサービスを作る場合はnginxサーバーとエンコードサーバーを分ける方が良いでしょう
DIVXのアピールタイム
今回VT1インスタンスという比較的高価なインスタンスを利用しました。
DIVXでは社員であれば誰でも自由に使えるAWS環境がある5ので、気兼ねなく色々試せたように思います。
このような環境があるということは初心者であれ熟練者であれ、どのような立場の人にとってもありがたいことではあると思います。
おわりに
DIVXでは一緒に働ける仲間を募集しています。
興味があるかたはぜひ採用ページを御覧ください。
*1:申し訳程度のクリスマス要素
*2:エフェクトをかけるリソースが無いような比較的貧弱なマシンが配信元もでエフェクト等の効果を適用したい場合や、何かしらの理由で配信元ではできない効果を適用したい場合を想定してもらえればと思います
*3:2022/12現在
*4:もう少し強めに雪が降る素材を作ればよかった
*5:もちろん一定のルールや使いすぎを防ぐ仕組みはあります