Lambda（Go）でffmpegの出力をS3にアップロード

AWS Lambda で Golang がサポートされて早１年[*](https://aws.amazon.com/jp/about-aws/whats-new/2018/01/aws-lambda-supports-go/)。
未だ Go で作った Lambda関数 で ffmpeg を動かしてみたという記事が見当たらなかったので書きました。

今回は ffmpeg で生成した動画サムネイルを S3 にアップロードするサンプルコードを紹介します。

## サンプルコード
### ディレクトリ構成
作業ディレクトリの構成は以下の通りです。

```plaintext:作業ディレクトリのツリーマップ
.
├── ffmpeg
├── credentials.json
└── main.go
```

#### ffmpeg
ffmpeg は以下の公式サイトより取得できます。
https://www.ffmpeg.org

Lambda の OS は Amazon Linux なので、DLページでは Linux のパッケージを入手します。
今回は Lambda 内でローカルに呼び出すので、Linux Static Builds から適当なパッケージを選びます。
（DLしたパッケージファイルを展開すると色々ファイルが入っていますが、今回利用するのは`ffmpeg`のみです）

#### credentials.json
今回は Lambda や S3 の認証情報を外部ファイルにまとめました。

```json:credentials.json
{
	"accessKeyId": "Your Access Key",
	"secretAccessKey": "Your Secret Key",
	"region": "Your Region (ap-northeast-1等)",
	"bucketName": "S3 Bucket Name",
	"keyPath": "S3 Key Path",
}
```

#### main.go
ffmpeg で生成した動画サムネイルを S3 にアップロードするサンプルコードです。

```golang:main.go
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
)

var c = readCredentialsJson()
var client = getS3Client()

var videoURL string = "Target Video URL"
var thumbName string = "thumbName.png"
var frameSecond string = "0"

func main() {
	lambda.Start(lambda2s3)
}

func lambda2s3() (string, error) {
	f := createThumb()
	putFileToS3(f)

	return fmt.Sprintf("done!"), nil
}

// ffmpegでサムネイルを生成
func createThumb() *os.File {
	// 環境変数を設定
	os.Setenv("PATH", os.Getenv("PATH")+os.Getenv("LAMBDA_TASK_ROOT"))

	// サムネイルを生成し、/tmpフォルダに一時保存
	err := exec.Command("ffmpeg", "-i", videoURL, "-ss", frameSecond, "-frames:v", "1", "/tmp/"+thumbName).Run()
	if err != nil {
		fmt.Println(err)
	}

	// 一時保存したサムネイルを読み出す
	f, err := os.Open("/tmp/" + thumbName)
	if err != nil {
		fmt.Println(err)
	}
	return f
}

// S3にアップロード
func putFileToS3(f *os.File) {
	client.PutObject(&s3.PutObjectInput{
		ACL:         aws.String("public-read"),
		Bucket:      aws.String(c.BucketName),
		Key:         aws.String("/" + c.KeyPath + thumbName),
		ContentType: aws.String("image/png"),
		Body:        aws.ReadSeekCloser(f),
	})
}

// S3クライアントを作成
func getS3Client() *s3.S3 {
	sess := session.Must(session.NewSession(&aws.Config{
		Credentials: credentials.NewStaticCredentials(c.AccessKeyId, c.SecretAccessKey, ""),
		Region:      aws.String(c.Region),
	}))
	client := s3.New(sess)

	return client
}

// 認証情報の構造体
type Credentials struct {
	AccessKeyId     string `json:"accessKeyId"`
	SecretAccessKey string `json:"secretAccessKey"`
	Region          string `json:"region"`
	BucketName      string `json:"bucketName"`
	KeyPath         string `json:"keyPath"`
}

// 認証情報JSONを読み込む
func readCredentialsJson() Credentials {
	jsonString, err := ioutil.ReadFile("credentials.json")
	if err != nil {
		fmt.Println(err)
	}

	c := new(Credentials)
	json.Unmarshal(jsonString, c)

	return Credentials{
		AccessKeyId:     c.AccessKeyId,
		SecretAccessKey: c.SecretAccessKey,
		Region:          c.Region,
		BucketName:      c.BucketName,
		KeyPath:         c.KeyPath,
	}
}
```

### 実行コマンド

念のため、サンプルコードの実行コマンドも掲載しておきます。

```terminal
$ GOOS=linux go build -o main
```

でコンパイルして、

```terminal
$ zip -r myfunc.zip main ffmpeg
```

で圧縮。出来た圧縮ファイル `myfunc.zip` を Lambda にアップロードすれば Lambda関数 の完成です。

## 一部解説
### 環境変数の設定

```golang
// 環境変数を設定
os.Setenv("PATH", os.Getenv("PATH")+os.Getenv("LAMBDA_TASK_ROOT"))
```

同梱している ffmpeg を呼び出すために、作業ディレクトリにPATHを通す必要があります。
今回のサンプルでは os.Setenv() と os.Getenv() で設定しました。

### /tmp ディレクトリの利用

```golang
// サムネイルを生成し、/tmpフォルダに一時保存
err := exec.Command("ffmpeg", "-i", videoURL, "-ss", frameSecond, "-frames:v", "1", "/tmp/" + thumbName).Run()
if err != nil {
    fmt.Println(err)
}
```

Lambda では、`/tmp` という一時作業ディレクトリが提供されています。
一般に、Lambda を S3 と連携させる際によく利用されます。

今回は、ffmpeg で出力したサムネイルの一時保存先として利用しました。

## 参考記事
#### ffmpeg のサムネイル生成コマンド
- [Extract frames from HTTP streamed video without downloading entire file - SrackExchange](https://superuser.com/questions/840127/extract-frames-from-http-streamed-video-without-downloading-entire-file)
- [ffmpeg Documentation](https://ffmpeg.org/ffmpeg.html)

#### Golang での外部コマンドの実行
- [Golangで外部コマンドを実行する方法まとめ - Qiita](https://qiita.com/tanksuzuki/items/9205ff70c57c4c03b5e5)

#### /tmp ディレクトリについて
- [AWS Lambda を利用する上で知っておいたほうがよいこと - ボクココ](https://www.bokukoko.info/entry/2015/09/17/AWS_Lambda_を利用する上で知っておいたほうがよいこと)
- [AWS Lambda の制限 - AWS](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/limits.html)
- [Go による Lambda 関数のビルド » 環境変数の使用 - AWS](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/go-programming-model-env-variables.html)

#### Node.js で ffmpeg を利用した例
- [AWS Lambda(Node.js) + ffmpeg でエンコード - CrossPower](http://xp-cloud.jp/blog/2018/05/18/3345/)
- [Creating video thumbnails with AWS Lambda in your s3 Bucket - concrete5](https://concrete5.co.jp/blog/creating-video-thumbnails-aws-lambda-your-s3-bucket)