この記事ではAWSのサービスの1つである、AWS Lambda内でFFmpegを使用して、S3内の動画ファイルを加工する方法に関してまとめていきます。
実際に手を動かして実装していただきやすいように、1つ1つ手順をスクショや実際のコードを交えながら説明していきます。
この記事の対象者
この記事では、AWS LambdaとFFmpegの使い方はある程度わかった上で説明していきます。
もしあまり良くわかっていない人は、別記事でそれぞれまとめていますので、参考にしてみてください。
AWS Lambdaに関して
AWS Lambdaに関してはこちらの記事でまとめています。
コンソール画面の使い方、関数の作成方法、S3のトリガーの設定、テストの実行方法、ログの見方などの基本的な使い方は、わかっている前提で書いていきますので、もしわからない場合は以下の記事で説明しています。
FFmpegに関して
FFmpegに関しては、以下の記事で使い方や各OSごとのインストール方法などまとめています。
環境
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}.`;
};
コードの部分ごとの説明は後ほど書いていきます。
こちらの記事で扱った内容ではコードはコンソール画面内で編集できましたが、今回は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: mp4filename: 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.jsでFFmpegを使うライブラリはありますが、やりたいことに対して書き方がわからない時があったので、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 LambdaでFFmpegを使用する流れをまとめていきました。
今回のように実行ファイルも含めてアップロードして実行することもできるので、やれることの幅がとても広いと感じました。
今後もAWS Lambdaを他のパターンでも使用していけるようにしていきたいです。