MediaConvertなどを使ってHLS再生する仕組みを作ってみた

はじめに

こんにちは、中村です。

AWS Elemental MediaConvertではファイルベースで簡単に動画変換をすることができます。中でもHLSは特殊な設備が不要になっており、Webサーバーでも配信ができます。 今回は動画変換から配信までをAWS上で構築してみました。これは少しの修正とCloudformationのスタック作成作業のみで完成します。

作成してみた

コード

全てのコードはgithubに置いてあります。ローカルにPullして利用してください。
cm-nakamura-yuki/play-movie-using-aws

構成

今回のシステムはこのような仕組みになっています。

Lambda

Lambdaを作成するにあたり、S3にソースコードをアップロードします。コード用のバケット名を決定し作成しましょう。 cfn/step1.ymlの45行目cfn/step2.ymlの44行目にあるYOUR_CODE_BUCKET_NAMEを作成したバケット名にします。

start-movie-convert/index.tsの2行目にあるYOUR_MEDIA_CONVERT_ENDPOINTをMediaConvertのアカウントページに記載されているエンドポイントに、8行目のYOUR_AWS_ACCOUNT_IDを使用するAWSアカウントのアカウントIDに修正します。 lambda/put-dynamo/index.ts, lambda/start-movie-convert/index.tsもコンパイルし、zip化します。 zipファイル名は、cfn/step1.ymlの46行目, cfn/step2.ymlの45行目と同じにしてください。

$ zip -r NAME.zip index.ts

Cloudformationのスタックを作成

cfn/step1.yml, cfn/step2.yml, cfn/step3.ymlの順番でスタックを作成していきます。step3のYOUR_AWS_ACCOUNT_IDをスタックを作成するアカウントIDに修正してください。step3ではCloudfrontの作成も行うので時間がかかります。

HTMLのアップロード

index.htmlの14行目にあるYOUR_COGNITO_ARNにstep3で出力されているCognitoのARNに修正してください。
client配下のhtmlファイルをアップロードします。アップロード先は、dist-から始まるバケット(変換された動画が格納されるバケット)です。

MediaConvert

MediaConvertにてmp4tohlsというジョブテンプレートを作成します。S3に動画がPutされたタイミングでこのテンプレートを使ってJobを作成します。

{
  "Category": "HLS",
  "Name": "mp4tohls",
  "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,
                  "FramerateDenominator": 1,
                  "GopClosedCadence": 1,
                  "GopSize": 90,
                  "Slices": 1,
                  "GopBReference": "DISABLED",
                  "SlowPal": "DISABLED",
                  "SpatialAdaptiveQuantization": "ENABLED",
                  "TemporalAdaptiveQuantization": "ENABLED",
                  "FlickerAdaptiveQuantization": "DISABLED",
                  "EntropyEncoding": "CABAC",
                  "Bitrate": 5000000,
                  "FramerateControl": "SPECIFIED",
                  "RateControlMode": "CBR",
                  "CodecProfile": "MAIN",
                  "Telecine": "NONE",
                  "FramerateNumerator": 30,
                  "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",
                "IFrameOnlyManifest": "EXCLUDE"
              }
            },
            "NameModifier": "_hls"
          }
        ],
        "OutputGroupSettings": {
          "Type": "HLS_GROUP_SETTINGS",
          "HlsGroupSettings": {
            "ManifestDurationFormat": "INTEGER",
            "SegmentLength": 10,
            "TimedMetadataId3Period": 10,
            "CaptionLanguageSetting": "OMIT",
            "Destination": "s3://dist-movie-source-645332194441/",
            "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"
        },
        "FilterEnable": "AUTO",
        "PsiControl": "USE_PSI",
        "FilterStrength": 0,
        "DeblockFilter": "DISABLED",
        "DenoiseFilter": "DISABLED",
        "TimecodeSource": "EMBEDDED"
      }
    ]
  }
}

テスト

ここまで完成したらsrc-から始まるS3バケットに動画をアップロードしてみます。S3のPutを検知しLambdaがジョブを作成します。

ジョブが完了すると、dist-から始まるバケットへ変換された動画が作成されます。またdist-から始まるバケットに_hls.m3u8のファイルがPutされたのをトリガーにDynamoDBに書き込みます。

ブラウザから確認してみる

Cloudfrontのドメインにアクセスします。index.htmlはDynamoDBに格納されているデータを取得しリスト表示にします。

リンクをクリックするとview.htmlに遷移します。hls再生のためにhls.jsを利用しています。hls.jsは簡単に実装できサポート領域も広いです。(と思っています。)

hls.jsのサポート範囲

  • Chrome for Android 34+
  • Chrome for Desktop 34+
  • Firefox for Android 41+
  • Firefox for Desktop 42+
  • IE11+ for Windows 8.1+
  • Edge for Windows 10+
  • Opera for Desktop
  • Vivaldi for Desktop
  • Safari for Mac 8+ (beta)

iOSのSafariは、残念ながら対象外(hls.jsを使わずにHLSが見れる)のため別の実装方法をしています。詳しくはclient/view.htmlを参照ください。

Playerが表示され動画もロードされているのを確認できました。

まとめ

初めてMediaConvertを使ってみました。
Elastic Transcoderもありますが、標準的な変換はこちらで十分かなと思います。

弊社では、「Amazon Connect」のキャンペーンを開催しています。

また音声を中心とした各種ソリューションの開発支援も行なっております。