🐙

Elastic TranscoderでHLS動画に変換しRailsアプリで配信する

2023/04/26に公開

概要

Railsアプリからアップロードしたmp4動画をAmazon Elastic Transcoderを使ってHLS動画に変換し、Railsアプリで配信します。

システム概要

  1. Railsアプリから動画をS3へアップロード
  2. S3に動画がアップロードされたら、LambdaでElastic Transcoderを呼び出す
  3. Elastic TranscoderでHLS動画へ変換
  4. 変換された動画のアップロード先は、同じS3バケットの同じディレクトリパス
  5. 変換が終了したらRailsアプリへ通知を送信
  6. HLS動画をRailsアプリで配信

やらないこと

  • CloudFrontでHLS動画を配信することが多いと思いますが今回はやりません。
  • AWSの料金説明(無料枠などもあるようです)
  • HLS動画の説明

使用したAWSサービス

  • S3
  • Elastic Transcoder
  • Lambda
  • SNS(Simple Notification Service)
  • CloudWatch
  • IAM

S3バケットの作成

  • 動画のアップロード先
  • 変換したHLS動画の保存先
    になります。

バケット名:transcoder(好きな名前で大丈夫ですが一意でないといけません)
リージョン:ap-northeast-1
オブジェクトの所有者:ACL有効

バケットポリシーの設定

"バケット名"のところは作ったバケット名を入れてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::バケット名/*"
        }
    ]
}

IAMの作成

LambdaがS3にアクセスするために作成します。

ロールの作成

ロール名:Elastic_Transcoder_Default_Role(好きな名前でいいです)
許可ポリシー:AmazonS3FullAccess
信頼されたエンティティ:

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "Service": "elastictranscoder.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

ポリシーの作成

ポリシー名:lambda_elastictranscoder_execution_policy(好きな名前で良い)
ポリシーの編集:

下記の「バケット名」と「XXXXXXXXXXX(アカウント ID)」はご自分の環境で変更して下さい

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "elastictranscoder:Read*",
                "elastictranscoder:*Job"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl",
                "s3:ListBucket",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::バケット名",
                "arn:aws:s3:::バケット名/*"
            ]
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": [
                "elastictranscoder:*Preset",
                "elastictranscoder:List*"
            ],
            "Resource": [
                "arn:aws:elastictranscoder:*:XXXXXXXXXXX:pipeline/*",
                "arn:aws:elastictranscoder:*:*:*",
                "arn:aws:elastictranscoder:*:XXXXXXXXXXX:preset/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "sns:Publish"
            ],
            "Resource": "arn:aws:sns:ap-northeast-1:XXXXXXXXXXX:ElasticTranscoderNotification"
        }
    ]
}

Elastic Transcoder 作成

"Create New Pipeline"で作っていきます。

NoficationsのところはAmazon SNSで作成していないと選択肢が出てこないと思いますので、後で設定すれば大丈夫です。

Amazon SNS

トピックの作成。
トピック名:ElasticTranscoderNotification(好きな名前でいいです。)

ここで作ったものをElastic TranscoderのNoficationsの設定で選択します。

アクセスポリシー

"XXXXXXXXXX" はアカウント IDになります。環境に合わせて変更して下さい。

{
  "Version": "2008-10-17",
  "Id": "__default_policy_ID",
  "Statement": [
    {
      "Sid": "__default_statement_ID",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": [
        "SNS:GetTopicAttributes",
        "SNS:SetTopicAttributes",
        "SNS:AddPermission",
        "SNS:RemovePermission",
        "SNS:DeleteTopic",
        "SNS:Subscribe",
        "SNS:ListSubscriptionsByTopic",
        "SNS:Publish"
      ],
      "Resource": "arn:aws:sns:ap-northeast-1:XXXXXXXXXX:ElasticTranscoderNotification",
      "Condition": {
        "StringEquals": {
          "AWS:SourceOwner": "XXXXXXXXXX"
        }
      }
    }
  ]
}

Lambdaの作成

用途ごとに3つの関数を作成しました。
Nodejs 16を使用。

  • VideoTranscodingInAWS → Elastic Transcoderを呼び出す。
  • VideoEncodedEvent → 動画変換が終わったらRailsアプリへ通知を送る。
  • S3ObjectGrantControl → S3に変換後の動画のアクセス権限を変更する

CloudFrontで配信する場合は必要ないと思いますが、今回はS3のデータをそのまま配信する形で作りましたので用意した関数になります。
※もっと良い方法があるかも知れないんですが、権限を変更しないと配信出来なかった。

VideoTranscodingInAWS

  • 一から作成
  • 関数名:VideoTranscodingInAWS
  • ランタイム:Node.js 16.x
  • アーキテクチャ:x86_64
  • デフォルトの実行ロールの変更:既存のロールを使用する(lambda_elastictranscoder_execution)

下記がコードになります。

  • 「パイプラインID」は、ご自分のElastic TranscoderのパイプラインIDを入れてください。
  • HLS動画のプレイリストはお好みのものをお使いください。下記コードを使ってもらうと600kと1Mになります。
  • 要らないコードは適宜削除してください。
  • async使うとうまくいかなかったので使っていません。
index.js
'use strict';

const AWS = require('aws-sdk');
const s3 = new AWS.S3({
    apiVersion: '2012-09-25'
});

const transcoder = new AWS.ElasticTranscoder({
    apiVersion: '2012-09-25',
    region: 'ap-northeast-1'
});

// return dirname without extensionn
function dirname(path) {
    const filename = path.split('.')[0];
    return decodeURIComponent(filename);
}

exports.handler = function(event, context) {
    console.log('Executing Elastic Transcoder Orchestrator');
    const bucket = event.Records[0].s3.bucket.name;    //transcoder
    const key = event.Records[0].s3.object.key;    //ディレクトリパス/example.mp4
    
    if (key.startsWith('uploads/tmp')) {
        console.log('Ignoring temporary file:', key);
        return;
    }
    
    const dkey = dirname(key);    //ディレクトリパス/example
    const pipelineId = 'パイプラインIDを入れてください';
    const srcKey =  decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); //the object may have spaces
    
    const params = {
        PipelineId: pipelineId,
        Input: {
            Key: srcKey,
            FrameRate: 'auto',
            Resolution: 'auto',
            AspectRatio: 'auto',
            Interlaced: 'auto',
            Container: 'auto'
        },
        Outputs: [
          {
            Key: dkey + '/600k/s',
            PresetId: '1351620000001-200040', // hls 600k
            SegmentDuration: '10'
          }
          ,{
            Key: dkey + '/1M/s',
            PresetId: '1351620000001-200030', // hls 1M
            SegmentDuration: '10'
          }
        ],
        Playlists: [
          {
            Name: dkey,
            Format: 'HLSv3',
            OutputKeys: [
                dkey + '/600k/s', 
                dkey + '/1M/s', 
            ]
          }
        ]
    };
    
    console.log('Starting Job');
    transcoder.createJob(params, function(err, data){
        if (err){
            console.log(err);
        } else {
            console.log(data);
        }
        context.succeed('Job well done');
    });



};

試しに動画をS3にアップロードしてみて、同じディレクトリにHLS形式の動画がアップロードされればOKです。
CloudWatchでログを確認できます。

S3に動画がアップロードされたらこの関数が動くようにトリガーを設定します。

トリガーを追加

  • サービス:S3を選択
  • バケット:作成したバケットを選択
  • イベントタイプ:すべてのオブジェクト作成イベント(デフォルト)
  • サフィックス - オプション: .mp4

VideoEncodedEvent

  • 一から作成
  • 関数名:VideoEncodedEvent
  • ランタイム:Node.js 16.x
  • アーキテクチャ:x86_64
  • デフォルトの実行ロールの変更:既存のロールを使用する(lambda_elastictranscoder_execution)

※RailsアプリのAPIがhttpの場合で書いてます。httpsの時はhttpの箇所が複数あるのでhttpsにコードを変更してください。
※Elastic Transcoderのデータを加工しているコードなので、適宜変更してください。

index.js
const AWS = require('aws-sdk');
const s3 = new AWS.S3({
    apiVersion: '2012-09-25'
});
const http = require('http');

exports.handler = function (event, context) {
  // RailsアプリのエンドポイントURLを指定する
  const url = "http://example/api/v1/APIのコントローラー名/";
  console.log("Event", event.Records[0].Sns.Message);
  
  const message = event.Records[0].Sns.Message;
  const messageObj = JSON.parse(message);
  const outputs = messageObj['outputs'][0];
  console.log('outputs', outputs);

  // リクエストを送信する
  const options = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
  };
  
  console.log('options は、', JSON.stringify(options));
  
  const req = http.request(url, options, function (res) {
    // let body = "";
    res.setEncoding("utf-8");
    res.on("data", function (chunk) {
      console.log("Response: " + chunk);
      context.succeed();
    });
    res.on("error", function (e) {
      console.log("Got error: " + e.message);
      context.done(null, "FAILURE");
    });
  });
  
  console.log('リクエスト送信');
  
  req.on("error", (error) => {
    console.error(error);
  });
  
  req.write(JSON.stringify(outputs));
  req.end();
};

この関数が動くためのトリガーをAmazon SNSを設定しました。

S3ObjectGrantControl

S3の動画データを配信するために権限変更する関数です。
CloudFrontなどを使う場合は必要ないです。

  • 一から作成
  • 関数名:S3ObjectGrantControl
  • ランタイム:Node.js 16.x
  • アーキテクチャ:x86_64
  • デフォルトの実行ロールの変更:既存のロールを使用する(lambda_elastictranscoder_execution)

※注意!同じバケットを指定しているので putObjectAcl メソッドを使ってください。
putObjectメソッドを使うと「再帰呼び出し」になってしまって関数が動き続けて課金額が半端ないことになります。

index.js
'use strict';

const AWS = require('aws-sdk');
const s3 = new AWS.S3({
    apiVersion: '2012-09-25'
});

// return dirname without extensionn
function dirname(path) {
    const filename = path.split('.')[0];
    return decodeURIComponent(filename);
}

exports.handler = function(event, context) {
    console.log('m3u8の権限変更');
    const bucket = event.Records[0].s3.bucket.name;
    const key = event.Records[0].s3.object.key;
    const dkey = dirname(key);
    
    // オブジェクトをパブリックにする
      const publicObjects = [
         {
          Bucket: bucket,
          Key: key,
          ACL: 'public-read',
        }
      ];
      
      for (const publicObject of publicObjects) {
        s3.putObjectAcl(publicObject, function(err, data) {
          if (err) {
            console.log(err);
          } else {
            console.log(data);
            console.log('Object uploaded successfully');
          }
        });
      }

};

この関数が動くためのトリガーを設定します。

トリガーを追加①

  • サービス:S3を選択
  • バケット:作成したバケットを選択
  • イベントタイプ:すべてのオブジェクト作成イベント(デフォルト)
  • サフィックス - オプション: .m3u8

トリガーを追加②

  • サービス:S3を選択
  • バケット:作成したバケットを選択
  • イベントタイプ:すべてのオブジェクト作成イベント(デフォルト)
  • サフィックス - オプション: .ts

参考記事(とても助かりました)

https://tech.medpeer.co.jp/entry/2018/01/18/174542
https://mat0401.info/blog/brief-hlsjs1/

Discussion