AWS

AWS Lambda(Node.js)+FFmpegでS3の動画を編集する

この記事ではAWSのサービスの1つである、AWS Lambda内でFFmpegを使用して、S3内の動画ファイルを加工する方法に関してまとめていきます。

実際に手を動かして実装していただきやすいように、1つ1つ手順をスクショや実際のコードを交えながら説明していきます。

この記事の対象者

この記事では、AWS LambdaFFmpegの使い方はある程度わかった上で説明していきます。
もしあまり良くわかっていない人は、別記事でそれぞれまとめていますので、参考にしてみてください。

AWS Lambdaに関して

AWS Lambdaに関してはこちらの記事でまとめています。

コンソール画面の使い方、関数の作成方法、S3のトリガーの設定、テストの実行方法、ログの見方などの基本的な使い方は、わかっている前提で書いていきますので、もしわからない場合は以下の記事で説明しています。

【AWS入門】Lambdaの概要とサンプルコードで手を動かしながら実践入門この記事ではAWSのサービスの1つである、AWS Lambdaに関してまとめていきます。初心者でも理解しやすいように、1つ1つ手順をスクショを交えて整理しながらサンプル機能を実装していきます。...

FFmpegに関して

FFmpegに関しては、以下の記事で使い方や各OSごとのインストール方法などまとめています。

FFmpegで動画変換!各OSごとのインストール方法と使い方まとめFFmpegは動画を変換、編集などできるフリーソフトでターミナルで動かします。動画の切り取り、トリミング、クロップ、形式変換、速さを変える、画像を合成、などコマンドを使いこなせさえすれば基本的に幅広く可能です。今回はそのFFmpegの使い方を初心者でもわかるように紹介していきます。...
HomebrewでMacにFFmpegをインストールする方法と使い方FFmpegは動画を変換、編集などできるフリーソフトでターミナルで動かします。この記事ではMacOSにHomebrewを使ってFFmpegをインストールする方法とFFmpegの使用方法を紹介します。...
CentOS 6系,7系にFFmpegをインストールする手順FFmpegは動画を変換、編集などできるフリーソフトでターミナルで動かします。今回はFFmpegをCentOS6系、CentOS7系にインストールする方法をまとめていきます。...
【windows】FFmpegをインストールする手順FFmpegは動画を変換、編集などできるフリーソフトでコマンドプロンプトで動かします。この記事ではこのFFmpegをWindowsにインストールする手順を1つずつスクショも交えながら紹介していきます。...

環境

macOS Mojave バージョン10.14.3
Node.js 10.x
画面はMacで説明していきますが、ほぼAWSのコンソール画面内でできるので、Windowsでも問題なくできるかと思います。

FFmpegのダウンロード

FFmpeg公式ページを開きます。

↓「Download」をクリックしましょう。

「Linuxのアイコン」「32-bit and 64-bit for kernel 2.6.32 and above」をクリックしましょう

「Static Builds」の一覧になりますが、特に指定がなければどれでも問題ないかと思います。
左上のものを選択しました。
クリックするとダウンロードが開始します。

↓ダウンロードしたものを解凍すると以下のようになるかと思います。
この中にあるFFmpegの実行ファイルを使用します。

Lambda関数の作成

ここからはLambda関数を作成していきます。
Node.js 10.xで作成していきました。

実際のコードはこちらになります。

今回は、S3に動画ファイルがアップロードされた際に、gifファイルに変換したものをS3の同じ階層にアップロードするというようなものになっています。


console.log('Loading function');

const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });
const fs = require('fs');
const execSync = require('child_process').execSync;
process.env.PATH += ':/var/task/bin';

exports.handler = async (event, context) => {

  console.log('Received event:', JSON.stringify(event,));

  const bucket = event.Records[0].s3.bucket.name;
  const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
  const extension = key.split('/')[key.split('/').length - 1].split('.');
  const filename = key.split('/')[key.split('/').length - 1].split('.')[0];
  const params = {
        Bucket: bucket,
        Key: key,
  };

  const uploaded_data = await s3.getObject(params)
        .promise()
        .catch((err) => {
            console.log(err);
            const message = `Error getting object ${key} from bucket ${bucket}. Make sure they exist and your bucket is in the same region as this function.`;
            console.log(message);
            throw new Error(message);
        });

  fs.writeFileSync('/tmp/' + filename + '.' + extension, uploaded_data.Body);

  execSync('ffmpeg -i /tmp/' + filename + '.' + extension + ' /tmp/' + filename + '.gif -y');

  const fileStream = fs.createReadStream('/tmp/' + filename + '.gif');
  fileStream.on('error', function(error) {
        console.log(error);
        throw new Error(error);
  });

  await s3.putObject({
            Bucket: bucket,
            Key: 'output/' + key.replace(extension, 'gif'),
            Body: fileStream,
            ContentType: uploaded_data.ContentType,
        })
        .promise()
        .catch((err) => {
            console.log(err);
            const message = `Error putting object ${key} from bucket ${bucket}. Make sure they exist and your bucket is in the same region as this function.`;
            console.log(message);
            throw new Error(message);
        });

  return `Success getting and putting object ${key} from bucket ${bucket}.`;
};

コードの部分ごとの説明は後ほど書いていきます。

【AWS入門】Lambdaの概要とサンプルコードで手を動かしながら実践入門この記事ではAWSのサービスの1つである、AWS Lambdaに関してまとめていきます。初心者でも理解しやすいように、1つ1つ手順をスクショを交えて整理しながらサンプル機能を実装していきます。...

こちらの記事で扱った内容ではコードはコンソール画面内で編集できましたが、今回はFFmpegの実行ファイルもアップロードしないといけないので、「コードエントリタイプ」「.zipファイルダウンロード」で行います。

なので、先程のNode.jsのコードは別で編集しておきます。

アップロードの手順

実行ファイルを含めたアップロードの手順を説明していきます。

↓適当な場所に以下のような構成のフォルダを作成します。
先程ダウンロードしたFFmpegの実行ファイルはbinフォルダ以下に設置します。
index.jsの内容は先程のコードになっています。

↓その階層のファイルを全て選択した状態で圧縮をします。

「アーカイブ.zip」が作成されました。
この作成されたzipファイルをアップロードします。

よくあるミスとして、これらのファイルを束ねている上の階層のフォルダを圧縮してしまうとエラーになってしまうので注意です。

その他の設定

S3にトリガーを設定し、S3へのアクセス権限のあるロールを設定しましょう。
タイムアウトの時間はデフォルトでは3秒ですが、おそらく足りないので適宜十分な時間を設定しておきましょう。

コードの説明

さて、先程のコードを1つずつ説明していきます。

関数が呼び出されたらexports.handlerの中身が呼ばれるので上から順番に見ていきます。

アップロードされたファイル情報を整理

今回はS3へのファイルのアップロードをトリガーとしているので、eventにイベントの情報が格納されています。

ここからファイルがアップロードされた、バケット名キー名(ファイルパスとファイル名を組み合わせたもの)を取得し、それをもとにファイルの拡張子ファイル名を整理しています。


const bucket = event.Records[0].s3.bucket.name;
const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
const extension = key.split('/')[key.split('/').length - 1].split('.');
const filename = key.split('/')[key.split('/').length - 1].split('.')[0];
const params = {
  Bucket: bucket,
  Key: key,
};

例えば、s3-sampleバケットにmoviesフォルダ以下にsample.mp4をアップロードした場合には、

  • bucket: 's3-sample'
  • key: 'movies/sample.mp4'
  • extension: mp4
  • filename: sample

となるようにしています。
拡張子やファイル名は、あとでFFmpegのコマンドで使うために整理しています。

S3のファイルのダウンロード

取得したバケット名キー名をパラメータに渡すことでそのファイルをダウンロードします。


const uploaded_data = await s3.getObject(params)
  .promise()
  .catch((err) => {
    console.log(err);
    const message = `Error getting object ${key} from bucket ${bucket}. Make sure they exist and your bucket is in the same region as this function.`;
    console.log(message);
    throw new Error(message);
  });

ダウンロードしたファイルをtmp以下に一時的に保存

この後の流れとして、ダウンロードしたファイルをFFmpegのコマンドで変換しますが、その際にuploaded_dataのままでは実行しづらいので、tmpフォルダ以下に設置します。

Node.js公式のモジュールのfsを使用していきます。
fsを使用できるようにこちらで予め読み込んでいます。


const fs = require('fs');

以下のようにして、tmp以下にダウンロードされたファイル名と同じ名前で一時的に保存します。


fs.writeFileSync('/tmp/' + filename + '.' + extension, uploaded_data.Body);

FFmpegのコマンドを実行

実際にFFmpegのコマンドを実行していきます。

以下のようにすることで、ターミナルで叩くのと同様に実行できるようにexecSyncを使えるようにしていきます。


const execSync = require('child_process').execSync;

次に、アップロードしたFFmpegの実行ファイルを実際に使えるようにパスを通す必要があります。
以下がそのためのコードです。


process.env.PATH += ':/var/task/bin';

上記を記述することで、execSync関数でコマンドを実行することが出来ます。
以下のようにしました。


execSync('ffmpeg -i /tmp/' + filename + '.' + extension + ' /tmp/' + filename + '.gif -y');

ターミナルで実行するのと同様のコマンドになるように引数に渡します。
今回は先程tmp以下に保存したファイルを、gifに変換したものを同様にtmp以下に出力するようにしています。

サンプルなので簡単な例にしていますが、この部分を置き換えれば様々な変換にも対応できるかと思います。

Node.jsFFmpegを使うライブラリはありますが、やりたいことに対して書き方がわからない時があったので、execSyncで行うことにしています。

出力ファイルをS3にアップロード

FFmpegコマンドで出力されたgifファイルをS3にアップロードできるように、tmp以下に出力したものを取り出せるようにしていきます。

先ほどと同様にfsを使っています。

const fileStream = fs.createReadStream('/tmp/' + filename + '.gif');
fileStream.on('error', function(error) {
  console.log(error);
  throw new Error(error);
});

これを利用して以下のようにして、S3にアップロードをしています。


await s3.putObject({
    Bucket: bucket,
    Key: 'output/' + key.replace(extension, 'gif'),
    Body: fileStream,
    ContentType: uploaded_data.ContentType,
  })
  .promise()
  .catch((err) => {
    console.log(err);
    const message = `Error putting object ${key} from bucket ${bucket}. Make sure they exist and your bucket is in the same region as this function.`;
    console.log(message);
    throw new Error(message);
  });

アップロード先はKeyで指定することができます。
変換した結果は、outputフォルダ以下にアップロードするようにしています。

このような流れで実装しました。

まとめ

今回は、AWS LambdaFFmpegを使用する流れをまとめていきました。

今回のように実行ファイルも含めてアップロードして実行することもできるので、やれることの幅がとても広いと感じました。
今後もAWS Lambdaを他のパターンでも使用していけるようにしていきたいです。