DIVX テックブログ

catch-img

VT1インスタンス+S3+CloudFrontを使ってサーバーサイドエフェクト付きライブストリーミング


目次[非表示]

  1. 1.はじめに
  2. 2.AWSにおけるライブストリーミングについて
  3. 3.VT1インスタンスとは?
  4. 4.本記事のゴール
  5. 5.注意・免責事項
  6. 6.インフラ構築
  7. 7.EC2のセッティング
  8. 8.nginx関連
  9. 9.goofys
  10. 10.視聴用ページの用意
  11. 11.動作確認
  12. 12.改善点
  13. 13.DIVXのアピールタイム
  14. 14.おわりに

はじめに

DIVXの井上です。

今回はVT1インスタンス、S3、CloudFrontを使ってリアルタイムで映像素材を合成しつつ動画配信(ライブストリーミング)を行います。

AWSにおけるライブストリーミングについて

AWSでライブストリーミング基盤を構築する際はMediaLive、MediaConvert、MediaStoreといったいわゆるMedia系サービスとCloudFrontを組み合わせて構築するのが一般的です。また、近年ではInteractive Video Serviceといったマネージドなライブストリーミングサービスも提供されているため、単純にライブストリーミングを行うのであればまずそれらの利用を検討すべきでしょう。

上記のサービスで一般的な動画配信で求められる機能(画像による透かしなど)はおおよそ提供されていますが、それらのサービスでは実現できない特殊な要件がある場合には今回紹介するVT1インスタンスのようなEC2インスタンスを利用することになります。

VT1インスタンスとは?

AWSのEC2で提供されているインスタンスタイプの一つです。 リアルタイムの動画トランスコーディングに特化したインスタンスであり、Xilinx Alveo U30メディアアクセラレータカードを搭載しているため、高速に動画のトランスコードを行うことができます。


  Amazon EC2 VT1 Instances - Amazon Web Services Amazon EC2 VT1 instances are designed to provide up to 30% better price per stream compared to the latest GPU-based EC2 instances and up to 60% better price per stream compared to the latest CPU-based EC2 instances. Amazon Web Services, Inc.

本記事のゴール

今回は「サーバーサイドでリアルタイムに雪が降る*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

  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

### S3 & CloudFront

  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

### IAM

  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

### EC2

  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-027100de2229f942b" # AL2
      ImageId: "ami-0c90a32843da57f18" # Ubuntu
      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をインストールします。

  GitHub - arut/nginx-rtmp-module: NGINX-based Media Streaming Server NGINX-based Media Streaming Server. Contribute to arut/nginx-rtmp-module development by creating an account on GitHub. GitHub


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 に記述します。

  Exec wrapper in bash NGINX-based Media Streaming Server. Contribute to arut/nginx-rtmp-module development by creating an account on GitHub. GitHub
#!/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を利用します。

  GitHub - kahing/goofys: a high-performance, POSIX-ish Amazon S3 file system written in Go a high-performance, POSIX-ish Amazon S3 file system written in Go - GitHub - kahing/goofys: a high-performance, POSIX-ish Amazon S3 file system written in Go GitHub

以下のコマンドでインストールを行います。

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='{}'>
        <!-- 下記 src="/hls/test/index.m3u8" の test はストリームキーと同じにします -->
        <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では一緒に働ける仲間を募集しています。
興味があるかたはぜひ採用ページを御覧ください。

  採用情報 | 株式会社divx(ディブエックス) 可能性を広げる転職を。DIVXの採用情報ページです。 株式会社divx(ディブエックス)


*1:申し訳程度のクリスマス要素
*2:エフェクトをかけるリソースが無いような比較的貧弱なマシンが配信元もでエフェクト等の効果を適用したい場合や、何かしらの理由で配信元ではできない効果を適用したい場合を想定してもらえればと思います
*3:2022/12現在
*4:もう少し強めに雪が降る素材を作ればよかった
*5:もちろん一定のルールや使いすぎを防ぐ仕組みはあります

お気軽にご相談ください


ご不明な点はお気軽に
お問い合わせください

サービス資料や
お役立ち資料はこちら

DIVXブログ

テックブログ タグ一覧

採用ブログ タグ一覧

人気記事ランキング

GoTopイメージ