eviry tech & service blog

「株式会社エビリー」の社員ブログです。弊社では、クラウド型動画配信サービス「millvi」、ソーシャル動画データ及び分析サービス「kamui tracker」、YouTube総合メディア「かむなび」を開発・提供しています。https://eviry.com/

AWS Elemental MediaLiveでライブ配信サービスを構築してみる

エンジニアのukiです。
eviryではmillviという動画配信サービスを用いたライブ配信を行っていますが、視聴者数が増える(サーバーへの負荷が上がる)と配信が止まってしまう、という問題がありました。
そのバックアップとして、AWSのMediaLiveからの配信を試すことになりました。
今回はそのMediaLiveを使ってライブ配信をするための設定を、色々と紹介したいと思います。

前提

  1. 少なくとも以下のサービスをAWSコンソールから操作できるアカウントを持っている
    (IAMポリシーで許可されている)
    • MediaLive
    • MediaStore
    • CloudFront
    • IAM
    • CloudWatch
    • MediaPackage
    • S3
    • Resource Group Tagging
  2. OBSなどのLive配信ソフトを使って配信できる

MediaLive & MediaStoreからLive配信する

MediaStoreのリソース作成

基本的には【やってみた】AWS Elemental MediaLiveとAWS Elemental MediaStoreでライブ配信してみたを参考に構築。

補足・注意事項

  1. MediaLiveでは出力先を2ヶ所指定しますので、同様の手順でもう一つのコンテナも作成しておきます。 という記載があるが、コンテナは1つでOK

追加設定

  • Container CORS policy
    • 異なるドメインからMediaStoreリソースを読み込む場合に必要
  [
    {
      "AllowedHeaders": [
        "*"
      ],
      "AllowedMethods": [
        "GET",
        "HEAD"
      ],
      "AllowedOrigins": [
        "*"
      ],
      "MaxAgeSeconds": 3000
    }
  ]

MediaLiveのリソース作成

基本的には【やってみた】AWS Elemental MediaLiveとAWS Elemental MediaStoreでライブ配信してみたを参考に構築。
Inputの作成やIAMロールなど、さらっとしか触れられていない箇所は【やってみた】AWS Elemental MediaLiveとAWS Elemental MediaPackageでライブ配信してみたを参考に。

補足・注意事項

  1. HLSグループ送信先について、MediaStoreコンテナを1つのみ作成した場合は、同じ送信先を設定できないため末尾にそれぞれA/Bなどをつけて一意にする
  2. MediaLiveチャネルのOutput groupsHLS settingsCDN SettingsHls media storeを選択

配信してみる その1

  1. MediaLiveチャネルの「Start」をクリックし、StatusがRunningに変わるのを待つ。
  2. ここまでに設定したMediaLiveのInputのエンドポイントURLの片方をOBSの配信設定に以下のように入力し、配信開始!
    • サーバー : rtmp://${IPアドレス}:1935/live/myStreamrtmp~liveまで
    • ストリームキー : rtmp://${IPアドレス}:1935/live/myStreammyStream
  3. MediaStoreコンテナのItemsのRefreshをポチポチ押してるとliveというフォルダができるので、クリックしてフォルダ内へ。
  4. myStreamA.m3u8 (or myStreamB.m3u8)というファイルを探してクリック
    (ソート順変えられないのが不便)
  5. Object nameのURLをコピーしSafariなどのHLS再生できるブラウザで開いて再生確認!

CloudFrontのリソース作成

基本的にはAWS でライブ配信システムを構築する(4)CloudFront による配信を参考に構築。

補足・注意事項

  1. Origin SettingsRestrict Bucket AccessNoでOK

配信してみる その2

  1. その1の手順4までは同じ
  2. Object nameのURLをコピーし、FQDNの部分をCloudFrontのDomain Nameに置き換え、HLS再生できるブラウザで開いて再生確認!

MediaPackageからLive配信する

MediaPackageのリソース作成

OBSとAWS Elemental MediaLiveでライブ配信をしてみたを参考に構築。

補足・注意事項

  1. 上記記事中ではCloudFront のディストリビューションを作成する事も出来ますが、今回は作成しません。とありますが、作成してください
    具体的には、MediaPackageチャンネル作成画面で、CloudFront distribution detailsCreate ~~を選択。
  2. リソース作成ユーザーのIAMポリシーにResource Group Taggingが許可されていないと、CloudFrontを作成しても紐付けされません

配信してみる

  1. その1の手順2までは同じ
  2. MediaPackageチャンネルのEndpointPlayリンクをクリックすると、モーダルウィンドウが表示されるため、プレビューを確認
  3. 同じくEndpointURLをコピーしHLS再生できるブラウザで開いて再生確認!
  4. Show CloudFront URLリンクをクリックするとURLが変わるので、それをコピーしブラウザで再生確認!

S3からLive配信する

リソース作成

AWS でライブ配信システムを構築する(5)を参考に構築。
S3バケットオリジンのCloudFrontも作成しておく。設定はMediaStore構築時と同様でOK

補足・注意事項

  1. 出力先となるS3バケットは予め作成する必要あり
  2. MediaLiveチャネルのManifest and SegmentsModeLIVEを選択
    • VODを選択すると、HLS形式でアーカイブとして保存される

追加設定

  • S3バケット
    • ブロックパブリックアクセス
      • 下記2つのみONにする
        • 新しいアクセスコントロールリスト (ACL) を介して~~
        • 任意のアクセスコントロールリスト (ACL) を介して~~
    • バケットポリシー
  {
      "Version": "2012-10-17",
      "Statement": [
          {
              "Effect": "Allow",
              "Principal": "*",
              "Action": "s3:GetObject",
              "Resource": "arn:aws:s3:::${bucket名}/*"
          }
      ]
  }
  • CORSの設定
  <?xml version="1.0" encoding="UTF-8"?>
  <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
      <AllowedOrigin>*</AllowedOrigin>
      <AllowedMethod>GET</AllowedMethod>
      <MaxAgeSeconds>3000</MaxAgeSeconds>
      <AllowedHeader>*</AllowedHeader>
    </CORSRule>
  </CORSConfiguration>

配信してみる

  1. その1の手順2までは同じ
  2. S3バケットの更新ボタンをポチポチ押してるとMediaLiveチャネルの送信先に指定したパスにm3u8ファイルが生成されるのでクリック
  3. オブジェクト URLをコピーしHLS再生できるブラウザで開いて再生確認!
  4. オブジェクト URLをコピーし、FQDNの部分をCloudFrontのDomain Nameに置き換え、HLS再生できるブラウザで開いて再生確認!

MediaLiveからアーカイブ出力する

リソース作成

AWS でライブ配信システムを構築する(5)を参考に構築。

配信してみる

  1. その1の手順2までは同じ
  2. MediaLiveチャネルの「Start」をクリックし、StatusがIdleに変わるのを待つ。
  3. S3バケットの更新ボタンをポチポチ押してるとMediaLiveチャネルの送信先に指定したパスにセグメントファイル(*.ts)が生成される





と、ここまで読んで、「やること多くて面倒くせぇ」と思ったそこのあなた!
以下のCloudFormationテンプレートを利用すれば、簡単に構築できます!
ただ、MediaPackageはまだCloudFormationで非対応とのことなので、ご了承ください。

MediaLive + MediaStore + CloudFrontを構築するCloudFormationテンプレート

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  #ProjectName
  ProjectName:
    Description: "Name of Project"
    Default: "Sample"
    Type: String
  #StreamKeyA
  StreamKeyA:
    Description: "StreamKeyA of Input"
    Type: String
    Default: "streamkey"
  #StreamKeyB
  StreamKeyB:
    Description: "StreamKeyB of Input"
    Type: String
    Default: "streamkey"
  #BaseFileName
  BaseFileName:
    Description: "Base file name of Destination"
    Type: String
    Default: "dest"
  #DaysSinceCreate
  DaysSinceCreate:
    Description: "Lifecycle policy of MediaStore Container"
    Type: Number
    Default: 1

Resources:
  # ---------------------
  # MediaLiveAccessRole
  # ---------------------
  MediaLiveAccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "medialive.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
      Path: /
      Policies:
        - PolicyName: !Sub "${ProjectName}-MediaLiveCustomPolicy"
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:PutObject
                  - s3:GetObject
                  - s3:DeleteObject
                Resource: '*'
              - Effect: Allow
                Action:
                  - mediastore:ListContainers
                  - mediastore:PutObject
                  - mediastore:GetObject
                  - mediastore:DeleteObject
                  - mediastore:DescribeObject
                Resource: '*'
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogStreams
                  - logs:DescribeLogGroups
                Resource: 'arn:aws:logs:*:*:*'
              - Effect: Allow
                Action:
                  - mediaconnect:ManagedDescribeFlow
                  - mediaconnect:ManagedAddOutput
                  - mediaconnect:ManagedRemoveOutput
                Resource: '*'
              - Effect: Allow
                Action:
                  - ec2:describeSubnets
                  - ec2:describeNetworkInterfaces
                  - ec2:createNetworkInterface
                  - ec2:createNetworkInterfacePermission
                  - ec2:deleteNetworkInterface
                  - ec2:deleteNetworkInterfacePermission
                  - ec2:describeSecurityGroups
                Resource: '*'
              - Effect: Allow
                Action:
                  - mediapackage:DescribeChannel
                Resource: '*'
      RoleName: !Sub "${ProjectName}-MediaLiveAccessRole"

  # ------------
  # MediaStoreContainer
  # ------------
  MediaStoreContainer:
    Type: AWS::MediaStore::Container
    Properties:
      ContainerName: !Sub "${ProjectName}-container"
      CorsPolicy:
        - AllowedHeaders:
            - "*"
          AllowedMethods:
            - "GET"
            - "HEAD"
          AllowedOrigins:
            - "*"
          MaxAgeSeconds: 3000
      LifecyclePolicy:
        !Sub '{
          "rules": [
            {
              "definition": {
                "path": [
                  {
                    "prefix": ""
                  }
                ],
                "days_since_create": [
                  {
                    "numeric": [
                      ">",
                      ${DaysSinceCreate}
                    ]
                  }
                ]
              },
              "action": "EXPIRE"
            }
          ]
        }'
      Policy:
        !Sub
          - '{
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Sid": "PublicReadOverHttps",
                  "Effect": "Allow",
                  "Action": ["mediastore:GetObject", "mediastore:DescribeObject"],
                  "Principal": "*",
                  "Resource": "arn:aws:mediastore:${AWS::Region}:${AWS::AccountId}:container/${CONTAINER}/*",
                  "Condition": {
                    "Bool": {
                        "aws:SecureTransport": "true"
                    }
                  }
                }
              ]
            }'
          - CONTAINER: !Sub "${ProjectName}-container"

  # -----------------------------
  # MediaLiveInputSecurityGroup
  # -----------------------------
  MediaLiveInputSecurityGroup:
    Type: AWS::MediaLive::InputSecurityGroup
    Properties:
      WhitelistRules:
        - Cidr: "0.0.0.0/0"

  # ----------------
  # MediaLiveInput
  # ----------------
  MediaLiveInput:
    Type: AWS::MediaLive::Input
    Properties:
      Destinations:
        - StreamName: !Sub "${ProjectName}/${StreamKeyA}"
        - StreamName: !Sub "${ProjectName}/${StreamKeyB}"
      InputSecurityGroups:
        - !Ref MediaLiveInputSecurityGroup
      Name: !Sub "${ProjectName}-input"
      Type: "RTMP_PUSH"

  # ------------------
  # MediaLiveChannel
  # ------------------
  MediaLiveChannel:
    Type: AWS::MediaLive::Channel
    Properties:
      ChannelClass: "STANDARD"
      Destinations:
        - Id: "destination1"
          Settings:
            - Url: !Sub
                - "mediastoressl://${MediaStoreEndpoint}/${ProjectName}/${BaseFileName}A"
                - MediaStoreEndpoint: !Select [ 1, !Split [ "//", !GetAtt MediaStoreContainer.Endpoint ]]
            - Url: !Sub
                - "mediastoressl://${MediaStoreEndpoint}/${ProjectName}/${BaseFileName}B"
                - MediaStoreEndpoint: !Select [ 1, !Split [ "//", !GetAtt MediaStoreContainer.Endpoint ]]
      EncoderSettings:
        AudioDescriptions:
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                InputType: "NORMAL"
                Bitrate: 192000
                CodingMode: "CODING_MODE_2_0"
                RawFormat: "NONE"
                Spec: "MPEG4"
                Profile: "LC"
                RateControlMode: "CBR"
                SampleRate: 48000
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_1"
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                InputType: "NORMAL"
                Bitrate: 192000
                CodingMode: "CODING_MODE_2_0"
                RawFormat: "NONE"
                Spec: "MPEG4"
                Profile: "LC"
                RateControlMode: "CBR"
                SampleRate: 48000
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_2"
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                InputType: "NORMAL"
                Bitrate: 128000
                CodingMode: "CODING_MODE_2_0"
                RawFormat: "NONE"
                Spec: "MPEG4"
                Profile: "LC"
                RateControlMode: "CBR"
                SampleRate: 48000
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_3"
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                InputType: "NORMAL"
                Bitrate: 128000
                CodingMode: "CODING_MODE_2_0"
                RawFormat: "NONE"
                Spec: "MPEG4"
                Profile: "LC"
                RateControlMode: "CBR"
                SampleRate: 48000
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_4"
        OutputGroups:
          - OutputGroupSettings:
              HlsGroupSettings:
                CaptionLanguageSetting: "OMIT"
                HlsCdnSettings:
                  HlsMediaStoreSettings:
                    NumRetries: 10
                    ConnectionRetryInterval: 1
                    RestartDelay: 15
                    FilecacheDuration: 300
                    MediaStoreStorageClass: "TEMPORAL"
                InputLossAction: "EMIT_OUTPUT"
                ManifestCompression: "NONE"
                Destination:
                  DestinationRefId: "destination1"
                IvInManifest: "INCLUDE"
                IvSource: "FOLLOWS_SEGMENT_NUMBER"
                ClientCache: "ENABLED"
                TsFileMode: "SEGMENTED_FILES"
                ManifestDurationFormat: "INTEGER"
                SegmentationMode: "USE_SEGMENT_DURATION"
                RedundantManifest: "DISABLED"
                OutputSelection: "MANIFESTS_AND_SEGMENTS"
                StreamInfResolution: "INCLUDE"
                IFrameOnlyPlaylists: "DISABLED"
                IndexNSegments: 10
                ProgramDateTime: "EXCLUDE"
                ProgramDateTimePeriod: 600
                KeepSegments: 21
                SegmentLength: 6
                TimedMetadataId3Frame: "PRIV"
                TimedMetadataId3Period: 10
                HlsId3SegmentTagging: "DISABLED"
                CodecSpecification: "RFC_4281"
                DirectoryStructure: "SINGLE_DIRECTORY"
                SegmentsPerSubdirectory: 10000
                Mode: "LIVE"
            Name: "MediaStore"
            Outputs:
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_1080p30"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                VideoDescriptionName: "video_1080p30"
                AudioDescriptionNames:
                  - "audio_1"
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_720p30"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                VideoDescriptionName: "video_720p30"
                AudioDescriptionNames:
                  - "audio_2"
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_480p30"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                VideoDescriptionName: "video_480p30"
                AudioDescriptionNames:
                  - "audio_3"
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_240p30"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                VideoDescriptionName: "video_240p30"
                AudioDescriptionNames:
                  - "audio_4"
        TimecodeConfig:
          Source: "EMBEDDED"
        VideoDescriptions:
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 5000000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30
                FramerateDenominator: 1
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_AUTO"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 3
                ParControl: "INITIALIZE_FROM_SOURCE"
                Profile: "HIGH"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                Slices: 1
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 1080
            Name: "video_1080p30"
            RespondToAfd: "NONE"
            Sharpness: 50
            ScalingBehavior: "DEFAULT"
            Width: 1920
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 3000000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30
                FramerateDenominator: 1
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_AUTO"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 3
                ParControl: "INITIALIZE_FROM_SOURCE"
                Profile: "HIGH"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                Slices: 1
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 720
            Name: "video_720p30"
            RespondToAfd: "NONE"
            Sharpness: 100
            ScalingBehavior: "DEFAULT"
            Width: 1280
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 1500000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30
                FramerateDenominator: 1
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_AUTO"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 3
                ParControl: "SPECIFIED"
                ParNumerator: 4
                ParDenominator: 3
                Profile: "MAIN"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                Slices: 1
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 480
            Name: "video_480p30"
            RespondToAfd: "NONE"
            Sharpness: 100
            ScalingBehavior: "STRETCH_TO_OUTPUT"
            Width: 640
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 750000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30
                FramerateDenominator: 1
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_AUTO"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 3
                ParControl: "SPECIFIED"
                ParNumerator: 4
                ParDenominator: 3
                Profile: "MAIN"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                Slices: 1
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 240
            Name: "video_240p30"
            RespondToAfd: "NONE"
            Sharpness: 100
            ScalingBehavior: "STRETCH_TO_OUTPUT"
            Width: 320
      InputAttachments:
        - InputAttachmentName: !Sub "${ProjectName}-channelinput"
          InputId: !Ref MediaLiveInput
          InputSettings:
            DeblockFilter: "DISABLED"
            DenoiseFilter: "DISABLED"
            FilterStrength: 1
            InputFilter: "AUTO"
            SourceEndBehavior: "CONTINUE"
      InputSpecification:
          Codec: "AVC"
          MaximumBitrate: "MAX_20_MBPS"
          Resolution: "HD"
      Name: !Sub "${ProjectName}-channel"
      RoleArn: !GetAtt MediaLiveAccessRole.Arn

  # ---------------------
  # CloudFrontDistribution
  # ---------------------
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          AllowedMethods:
            - "GET"
            - "HEAD"
            - "OPTIONS"
          CachedMethods:
            - "GET"
            - "HEAD"
          ForwardedValues:
            Headers:
              - "Origin"
            QueryString: false
          TargetOriginId: !Sub
            - "MS-${MediaStoreEndpoint}"
            - MediaStoreEndpoint: !Select [ 0, !Split [ ".mediastore.", !Select [ 1, !Split [ "//", !GetAtt MediaStoreContainer.Endpoint ]]]]
          ViewerProtocolPolicy: "allow-all"
        Enabled: true
        HttpVersion: "http2"
        IPV6Enabled: true
        Origins:
          - CustomOriginConfig:
              OriginProtocolPolicy: "https-only"
              OriginSSLProtocols:
                - "TLSv1"
            DomainName: !Select [ 1, !Split [ "//", !GetAtt MediaStoreContainer.Endpoint ]]
            Id: !Sub
              - "MS-${MediaStoreEndpoint}"
              - MediaStoreEndpoint: !Select [ 0, !Split [ ".mediastore.", !Select [ 1, !Split [ "//", !GetAtt MediaStoreContainer.Endpoint ]]]]
        PriceClass: "PriceClass_200"

Outputs:
  # ---------------------
  # InputUrlA
  # ---------------------
  InputUrlA:
    Value: !Select [ 0, !GetAtt MediaLiveInput.Destinations ]

  # ---------------------
  # InputUrlB
  # ---------------------
  InputUrlB:
    Value: !Select [ 1, !GetAtt MediaLiveInput.Destinations ]

  # ---------------------
  # EndpointUrlA
  # ---------------------
  EndpointUrlA:
    Value: !Sub
      - "https://${CloudFrontDomainName}/${ProjectName}/${BaseFileName}A.m3u8"
      - CloudFrontDomainName: !GetAtt CloudFrontDistribution.DomainName

  # ---------------------
  # EndpointUrlB
  # ---------------------
  EndpointUrlB:
    Value: !Sub
      - "https://${CloudFrontDomainName}/${ProjectName}/${BaseFileName}B.m3u8"
      - CloudFrontDomainName: !GetAtt CloudFrontDistribution.DomainName

MediaLive + S3 + CloudFrontを構築するCloudFormationテンプレート

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  #ProjectName
  ProjectName:
    Description: "Name of Project"
    Default: "Sample"
    Type: String
  #StreamKeyA
  StreamKeyA:
    Description: "StreamKeyA of Input"
    Type: String
    Default: "streamkey"
  #StreamKeyB
  StreamKeyB:
    Description: "StreamKeyB of Input"
    Type: String
    Default: "streamkey"
  #BaseFileName
  BaseFileName:
    Description: "Base file name of Destination"
    Type: String
    Default: "dest"
  #BucketName
  BucketName:
    Description: "Bucket name of S3"
    Type: String
    Default: "bucket"

Resources:
  # ---------------------
  # MediaLiveAccessRole
  # ---------------------
  MediaLiveAccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "medialive.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
      Path: /
      Policies:
        - PolicyName: !Sub "${ProjectName}-MediaLiveCustomPolicy"
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:PutObject
                  - s3:GetObject
                  - s3:DeleteObject
                Resource: '*'
              - Effect: Allow
                Action:
                  - mediastore:ListContainers
                  - mediastore:PutObject
                  - mediastore:GetObject
                  - mediastore:DeleteObject
                  - mediastore:DescribeObject
                Resource: '*'
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogStreams
                  - logs:DescribeLogGroups
                Resource: 'arn:aws:logs:*:*:*'
              - Effect: Allow
                Action:
                  - mediaconnect:ManagedDescribeFlow
                  - mediaconnect:ManagedAddOutput
                  - mediaconnect:ManagedRemoveOutput
                Resource: '*'
              - Effect: Allow
                Action:
                  - ec2:describeSubnets
                  - ec2:describeNetworkInterfaces
                  - ec2:createNetworkInterface
                  - ec2:createNetworkInterfacePermission
                  - ec2:deleteNetworkInterface
                  - ec2:deleteNetworkInterfacePermission
                  - ec2:describeSecurityGroups
                Resource: '*'
              - Effect: Allow
                Action:
                  - mediapackage:DescribeChannel
                Resource: '*'
      RoleName: !Sub "${ProjectName}-MediaLiveAccessRole"

  # ---------------------
  # S3Bucket
  # ---------------------
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${BucketName}"
      CorsConfiguration:
        CorsRules:
          - AllowedHeaders:
              - "*"
            AllowedMethods:
              - "GET"
            AllowedOrigins:
              - "*"
            MaxAge: 3000
      ObjectLockEnabled: false
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: false
        IgnorePublicAcls: true
        RestrictPublicBuckets: false

  # ---------------------
  # S3BucketPolicy
  # ---------------------
  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement:
          - Action: "s3:GetObject"
            Effect: Allow
            Resource: !Sub "arn:aws:s3:::${S3Bucket}/*"
            Principal: "*"

  # -----------------------------
  # MediaLiveInputSecurityGroup
  # -----------------------------
  MediaLiveInputSecurityGroup:
    Type: AWS::MediaLive::InputSecurityGroup
    Properties:
      WhitelistRules:
        - Cidr: "0.0.0.0/0"

  # ----------------
  # MediaLiveInput
  # ----------------
  MediaLiveInput:
    Type: AWS::MediaLive::Input
    Properties:
      Destinations:
        - StreamName: !Sub "${ProjectName}/${StreamKeyA}"
        - StreamName: !Sub "${ProjectName}/${StreamKeyB}"
      InputSecurityGroups:
        - !Ref MediaLiveInputSecurityGroup
      Name: !Sub "${ProjectName}-input"
      Type: "RTMP_PUSH"

  # ------------------
  # MediaLiveChannel
  # ------------------
  MediaLiveChannel:
    Type: AWS::MediaLive::Channel
    Properties:
      ChannelClass: "STANDARD"
      Destinations:
        - Id: "destination1"
          Settings:
            - Url: !Sub "s3ssl://${BucketName}/${ProjectName}/${BaseFileName}A"
            - Url: !Sub "s3ssl://${BucketName}/${ProjectName}/${BaseFileName}B"
      EncoderSettings:
        AudioDescriptions:
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                InputType: "NORMAL"
                Bitrate: 192000
                CodingMode: "CODING_MODE_2_0"
                RawFormat: "NONE"
                Spec: "MPEG4"
                Profile: "LC"
                RateControlMode: "CBR"
                SampleRate: 48000
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_1"
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                InputType: "NORMAL"
                Bitrate: 192000
                CodingMode: "CODING_MODE_2_0"
                RawFormat: "NONE"
                Spec: "MPEG4"
                Profile: "LC"
                RateControlMode: "CBR"
                SampleRate: 48000
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_2"
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                InputType: "NORMAL"
                Bitrate: 128000
                CodingMode: "CODING_MODE_2_0"
                RawFormat: "NONE"
                Spec: "MPEG4"
                Profile: "LC"
                RateControlMode: "CBR"
                SampleRate: 48000
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_3"
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                InputType: "NORMAL"
                Bitrate: 128000
                CodingMode: "CODING_MODE_2_0"
                RawFormat: "NONE"
                Spec: "MPEG4"
                Profile: "LC"
                RateControlMode: "CBR"
                SampleRate: 48000
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_4"
        OutputGroups:
          - OutputGroupSettings:
              HlsGroupSettings:
                CaptionLanguageSetting: "OMIT"
                HlsCdnSettings:
                  HlsBasicPutSettings:
                    NumRetries: 10
                    ConnectionRetryInterval: 1
                    RestartDelay: 15
                    FilecacheDuration: 300
                    MediaStoreStorageClass: "TEMPORAL"
                InputLossAction: "EMIT_OUTPUT"
                ManifestCompression: "NONE"
                Destination:
                  DestinationRefId: "destination1"
                IvInManifest: "INCLUDE"
                IvSource: "FOLLOWS_SEGMENT_NUMBER"
                ClientCache: "ENABLED"
                TsFileMode: "SEGMENTED_FILES"
                ManifestDurationFormat: "INTEGER"
                SegmentationMode: "USE_SEGMENT_DURATION"
                RedundantManifest: "DISABLED"
                OutputSelection: "MANIFESTS_AND_SEGMENTS"
                StreamInfResolution: "INCLUDE"
                IFrameOnlyPlaylists: "DISABLED"
                IndexNSegments: 10
                ProgramDateTime: "EXCLUDE"
                ProgramDateTimePeriod: 600
                KeepSegments: 21
                SegmentLength: 6
                TimedMetadataId3Frame: "PRIV"
                TimedMetadataId3Period: 10
                HlsId3SegmentTagging: "DISABLED"
                CodecSpecification: "RFC_4281"
                DirectoryStructure: "SINGLE_DIRECTORY"
                SegmentsPerSubdirectory: 10000
                Mode: "LIVE"
            Name: "S3"
            Outputs:
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_1080p30"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                VideoDescriptionName: "video_1080p30"
                AudioDescriptionNames:
                  - "audio_1"
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_720p30"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                VideoDescriptionName: "video_720p30"
                AudioDescriptionNames:
                  - "audio_2"
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_480p30"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                VideoDescriptionName: "video_480p30"
                AudioDescriptionNames:
                  - "audio_3"
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_240p30"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                VideoDescriptionName: "video_240p30"
                AudioDescriptionNames:
                  - "audio_4"
        TimecodeConfig:
          Source: "EMBEDDED"
        VideoDescriptions:
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 5000000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30
                FramerateDenominator: 1
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_AUTO"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 3
                ParControl: "INITIALIZE_FROM_SOURCE"
                Profile: "HIGH"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                Slices: 1
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 1080
            Name: "video_1080p30"
            RespondToAfd: "NONE"
            Sharpness: 50
            ScalingBehavior: "DEFAULT"
            Width: 1920
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 3000000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30
                FramerateDenominator: 1
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_AUTO"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 3
                ParControl: "INITIALIZE_FROM_SOURCE"
                Profile: "HIGH"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                Slices: 1
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 720
            Name: "video_720p30"
            RespondToAfd: "NONE"
            Sharpness: 100
            ScalingBehavior: "DEFAULT"
            Width: 1280
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 1500000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30
                FramerateDenominator: 1
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_AUTO"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 3
                ParControl: "SPECIFIED"
                ParNumerator: 4
                ParDenominator: 3
                Profile: "MAIN"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                Slices: 1
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 480
            Name: "video_480p30"
            RespondToAfd: "NONE"
            Sharpness: 100
            ScalingBehavior: "STRETCH_TO_OUTPUT"
            Width: 640
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 750000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30
                FramerateDenominator: 1
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_AUTO"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 3
                ParControl: "SPECIFIED"
                ParNumerator: 4
                ParDenominator: 3
                Profile: "MAIN"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                Slices: 1
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 240
            Name: "video_240p30"
            RespondToAfd: "NONE"
            Sharpness: 100
            ScalingBehavior: "STRETCH_TO_OUTPUT"
            Width: 320
      InputAttachments:
        - InputAttachmentName: !Sub "${ProjectName}-channelinput"
          InputId: !Ref MediaLiveInput
          InputSettings:
            DeblockFilter: "DISABLED"
            DenoiseFilter: "DISABLED"
            FilterStrength: 1
            InputFilter: "AUTO"
            SourceEndBehavior: "CONTINUE"
      InputSpecification:
          Codec: "AVC"
          MaximumBitrate: "MAX_20_MBPS"
          Resolution: "HD"
      Name: !Sub "${ProjectName}-channel"
      RoleArn: !GetAtt MediaLiveAccessRole.Arn

  # ---------------------
  # CloudFrontDistribution
  # ---------------------
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          AllowedMethods:
            - "GET"
            - "HEAD"
            - "OPTIONS"
          CachedMethods:
            - "GET"
            - "HEAD"
          ForwardedValues:
            Headers:
              - "Origin"
            QueryString: false
          TargetOriginId: !Sub "S3-${BucketName}"
          ViewerProtocolPolicy: "allow-all"
        Enabled: true
        HttpVersion: "http2"
        IPV6Enabled: true
        Origins:
          - DomainName: !Sub "${BucketName}.s3.amazonaws.com"
            Id: !Sub "S3-${BucketName}"
            S3OriginConfig:
              OriginAccessIdentity: ""
        PriceClass: "PriceClass_200"

Outputs:
  # ---------------------
  # InputUrlA
  # ---------------------
  InputUrlA:
    Value: !Select [ 0, !GetAtt MediaLiveInput.Destinations ]

  # ---------------------
  # InputUrlB
  # ---------------------
  InputUrlB:
    Value: !Select [ 1, !GetAtt MediaLiveInput.Destinations ]

  # ---------------------
  # EndpointUrlA
  # ---------------------
  EndpointUrlA:
    Value: !Sub
      - "https://${CloudFrontDomainName}/${ProjectName}/${BaseFileName}A.m3u8"
      - CloudFrontDomainName: !GetAtt CloudFrontDistribution.DomainName

  # ---------------------
  # EndpointUrlB
  # ---------------------
  EndpointUrlB:
    Value: !Sub
      - "https://${CloudFrontDomainName}/${ProjectName}/${BaseFileName}B.m3u8"
      - CloudFrontDomainName: !GetAtt CloudFrontDistribution.DomainName