GO - Função Lambda para converter audio a partir do S3(AWS) - ffmpeg
Published: 2023-04-17 18:03:43

Atuei em um projeto pessoal, onde necessitei fazer a conversão de um arquivo wav(audio de qualidade maxima, sem compressão) para mp3.

Neste post, vou demonstrar o passo a passo, como fazer de forma "assíncrona", a partir de um upload para um bucket no S3(serviço de armazenamento de arquivos) da AWS.

No desenho abaixo, segue um contexto do que será feito e explicado neste post:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/51dvp7yyndf7yrqh8pag.png)

## Criando Buckets

Em seu console da AWS, vá até o service _S3_, crie 2 novos _buckets_. O primeiro _bucket_, será o destino do upload do arquivo de áudio "bruto", no format .wav. O segundo, será para o arquivo comprimido e convertido para .mp3. No meu caso, usei estes nomes:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eb52spjgdc5xmnhu5w3y.png)

## Criando Lambda

Sim, vamos já proceder na criação da função lambda que será responsável por converter os arquivos de audio bruto, para formado de audio compactado. Para isso, em seu _AWS Console_, na busca procuro por _Lambda_. Clique em _Create Function_

Vamos utilizar a linguagem GO, conforme a tela abaixo:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ytq5qa0w8ngriai323xp.png)

## Permissões de acesso Lambda - S3
Será necessário a criação de uma Police, e atrelá-la a Role criada na criação da lambda.

Para isso acessaremos o console do _IAM_, sem seguida vamos ao menu _Polices_ > Add Police. Mudaremos o modo para JSON, e vamos inserir o código a seguir:

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::mgm-wav-files/*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::mgm-mp3-files/*"
        }
    ]
}
```
A política acima dá permissão de leitura no _bucket_ de arquivos _wav_ e escrita no _bucket_ para arquivos _mp3_, convertidos. Vamos salvar essa _Police_.

No console do _IAM_, vamos ao menu _Roles_, vamos selecionar a _Role_ criada para a _lambda_. Em seguida, vamos em _Add Permissions_ > _atacch policies_. Na buscar, buscar pelo nome da política criada e salvar.

## Run time Lambda. O que é isso?

As funções lambda, na linguagem Go(entre outras), rodam em um contexto de ambiente Linux, no caso da distribuição AWS Linux. Nesse contexto, precisamos "carregar" uma lib, que será a responsável por fazer a conversão de áudio. 

Essa lib é o FFMPEG. Que trata de vários formatos de mídias, audio, video.

Para isso, utilizaremos uma feature das funções lambda na AWS, chamada _Layer_, traduzindo: camada

Com uma _layer_, conseguimos rodar algumas _libs_ em tempo de execução de uma ou mais funções lambda. Como?

## Criando Layer FFMPEG

Vamos baixar os binários desta lib em nossa máquina local, e transferir apenas o necessário para um arquivo ZIP, que será inserido em um Lambda Layer.

Para isso, vou utilizar o exemplo abaixo, de ambiente Linux/MacOS:

```bash
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz

wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz.md5

md5sum -c ffmpeg-release-amd64-static.tar.xz.md5

tar xvf ffmpeg-release-amd64-static.tar.xz
```

Após o download e descompactar o arquivo, vamos criar uma nova basta, para copiar o conteudo do binario do ffmpeg, e _zipar_ o conteúdo:

```bash
mkdir -p ffmpeg/bin

cp ffmpeg-4.3.1-amd64-static/ffmpeg ffmpeg/bin/

cd ffmpeg

zip -r ../ffmpeg.zip .
```
Agora, vamos criar uma nova _Layer_ em nosso ambiente de Lambdas na AWS, anexando o arquivo _zip_ criado no passo acima como no exemplo a seguir:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r1senvz49bdnmfu41gg5.png)

## Anexando Layer FFMPEG a nossa função Lambda

Vamos voltar ao menu _Functions_ dentro de _Lambda_ em nosso _Console_. Na aba _code_ vamos descer a tela, e ir até a sessão _Layers_ e clicar em _Add Layer_. Vamos preencher e selecionar o formulário, como na imagem abaixo


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kwdoxuh9mb80yc55eqss.png)

## E (FINALMENTE) vamos ao nosso código!

Para a linguagem Go, não temos um editor online em funções lambda na AWS. Por isso, vamos gerar nosso código localmente, buildá-lo e subir um zip do mesmo via console.

# Início de tudo

```go
package main

func main() {
}
```

Em nosso ambiente local de desenvolvimento em Go, vamos criar uma nova pasta. Nesta pasta, iniciaremos o projeto com o famoso comando "go mod" para criar nosso arquivo de controle de módulos go do Projeto.

Após, iremos criar o arquivo main.go com o seguinte conteúdo:

```go
package main

import "github.com/aws/aws-lambda-go/lambda"

func handler()  {
	
}
func main() {
	lambda.Start(handler)
}
```
Acima, importamos a biblioteca oficial _lambda_, para utilização em _Go_. Criamos uma função chamada _handler_ onde vamos inserir nossa futura estrutura de conversão de áudio, e abaixo a função _main_, apenas executa a função _handler_ em ambiente de execução do _Lambda_.

Agora vamos carregar a biblioteca oficial do s3, para conectar no bucket que irá iniciar a função(essa informação é importante, pois o _start_ traz informações do _bucket_ para a _lambda_). Em seguida, vamos criar um diretório temporário, dentro do ambiente de execução da lambda, para fazer download do arquivo, onde possa ser manipulado. Em seguida realizar o download, utilizando um log para verificar:

```go
package main

import (
	"context"
	"fmt"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"log"
	"os"
)

func handler(_ context.Context, s3Event events.S3Event) {
	record := s3Event.Records[0]
	key := record.S3.Object.Key
	sess, _ := session.NewSession(&aws.Config{Region: &record.AWSRegion})
	downloader := s3manager.NewDownloader(sess)
	file, err := os.Create(fmt.Sprintf("/tmp/%s", key))
	if err != nil {
		panic(err)
	}
	defer file.Close()
	_, err = downloader.Download(file,
		&s3.GetObjectInput{
			Bucket: &record.S3.Bucket.Name,
			Key:    &key,
		})
	if err != nil {
		panic(err)
	}
	log.Printf("Downloaded %s", file.Name())
}

func main() {
	lambda.Start(handler)
}
```

Nossa função handler, recebe 2 parâmetros vindos de Context e events, libs da AWS que trazem informações a respeito do bucket que inicia a lambda.
Neste ponto, é interessante que façamos o primeiro upload do código buildado, para realizar um teste "manual". Primeiro vamos criar o build dessa aplicação com o comando:

```bash
go build -o main
```
Em seguida, vamos empacotar o arquivo em um arquivo zip, para realizar o upload:

```bash
zip main.zip main
```

Agora vamos ao console, realizar o upload:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/24dgddqf8njwtym6tc9z.png)

#Criando Trigger

Antes de testarmos o código, precisamos criar a trigger ou gatilho, que dará inicio a lambda que criamos. 

Dentro da nossa função, no console AWS vamos clicar em _Add Trigger_.

Vamos preencher o formulário, conforme as 3 imagens abaixo:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4vr8znbjbxbunhcbjr2t.png)

Neste ponto, vamos selecionar o bucket dos arquivos brutos:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/scoboptz8534qewp4flx.png)

Em seguida selecionar todos os tipos de entrada de arquivos
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ik65rp1u37e0cofhhcm.png)

Por final, vamos deixar um filtro para somente iniciar a lambda, para arquivos .wav
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hjno1emxo3vgp35tx114.png)

#Primeiro teste

Agora, vamos ao nosso bucket, fazer um upload de arquivo .wav, e verificar o status no CloudWatch, e posteriormente verificar se o arquivo mp3 foi criado no outro bucket.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7cimy31rjowwn4wohwfg.png)

Acima vemos os 2 _buckets_ utilizados em nosso projeto. Vamos acessar o _mgm-wav-files_ para realizar o upload. Após o upload, vamos acessar o CloudWatch, ir ao meno Logs > Logs Groups. Lá haverá um grupo de log, com nome de nossa Lambda. Em nosso casso está assim:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dwrcl03hwa04o0uigqcj.png)

Entrando em nosso log, vamos procurar se há a mensagem "Downloaded NOMEDOARQUIVO", como a seguir:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dyukygoghifugd8y0uh4.png)

Neste caso, tudo certo, vamos continuar!

## Convertando arquivo .wav com FFMPEG

Vamos utilizar a funciona a lib nativa Go _OS_, para utilizar a função exec.Command, que nos possibilita rodar comandos em nível de bash. E a partir dele, usar o ffmpeg para convertar o arquivo .wav para .mp3, gravando-o no diretório temporário criado anteriormente:

```go
outputFile := strings.Replace(file.Name(), filepath.Ext(file.Name()), ".mp3", 1)
	cmd := exec.Command("ffmpeg", "-i", file.Name(), outputFile)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Fatalf("cmd.Run() failed with %s\n", err)
	}
	log.Printf("Execution output:\n%s\n", string(out))
	output, err := os.Open(outputFile)
	if err != nil {
		panic(err)
	}
```

## Gravando arquivo convertido em outro bucket

Agora, vamos acessar o arquivo criado, e fazer o upload para o bucket criado, apenas para receber os arquivos convertidos. O código exemplo, existe na documentação da AWS:

```go
//put mp3 file in converted bucket
	destinationBucket := "mgm-mp3-files"
	_, err = s3.New(sess).PutObject(&s3.PutObjectInput{
		Bucket: &destinationBucket,
		Key:    aws.String(filepath.Base(outputFile)),
		Body:   output,
	})
	log.Printf("Copied %s to %s", outputFile, record.S3.Bucket.Name)
```

## Código Final
```go
package main

import (
	"context"
	"fmt"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

func handler(_ context.Context, s3Event events.S3Event) {
	record := s3Event.Records[0]
	key := record.S3.Object.Key
	sess, _ := session.NewSession(&aws.Config{Region: &record.AWSRegion})

	//Download file to a temporary folder
	downloader := s3manager.NewDownloader(sess)
	file, err := os.Create(fmt.Sprintf("/tmp/%s", key))
	if err != nil {
		panic(err)
	}
	defer file.Close()
	_, err = downloader.Download(file,
		&s3.GetObjectInput{
			Bucket: &record.S3.Bucket.Name,
			Key:    &key,
		})
	if err != nil {
		panic(err)
	}
	log.Printf("Downloaded %s", file.Name())

	//transform wav file to a compress mp3 file
	outputFile := strings.Replace(file.Name(), filepath.Ext(file.Name()), ".mp3", 1)
	cmd := exec.Command("ffmpeg", "-i", file.Name(), outputFile)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Fatalf("cmd.Run() failed with %s\n", err)
	}
	log.Printf("Execution output:\n%s\n", string(out))
	output, err := os.Open(outputFile)
	if err != nil {
		panic(err)
	}

	//put mp3 file in converted bucket
	destinationBucket := "mgm-mp3-files"
	_, err = s3.New(sess).PutObject(&s3.PutObjectInput{
		Bucket: &destinationBucket,
		Key:    aws.String(filepath.Base(outputFile)),
		Body:   output,
	})
	log.Printf("Copied %s to %s", outputFile, record.S3.Bucket.Name)
}

func main() {
	lambda.Start(handler)
}
```

## PLUS - Mensagem assíncrona de fim da atividade

Por final, vamos fazer com que, ao final da execução do lambda, em caso de sucesso uma fila _SQS_ seja abastecida com uma mensagem, para que consumidores(aplicativos, web, etc..) possam ser avisados do final do processo e disponibilização do arquivo _mp3_ convertido.

Vamos agora ao console _AWS_, procurar por _SQS_. O serviço simples de fila da própria Amazon. Vamos criar um tópico, chamado _**mp3-ready**_

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x4o3ueuprhlnv0mfqtn3.png)

Agora, voltando a nossa função lambda, vamos acessar o menu Add Destination.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ze1paxxna2vm7vcltlnu.png)

Nosso formulário deve ser preenchido destga maneira, escolhendo a fila SQS criada(A mensagem de falta de permissão para uso do SQS pelo lambda, será solucionada no próximo passo)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rvth1hf10ekfw4ud46tp.png)

Agora com nosso destino de lambda criado, precisamos dar permissão no IAM para nossa role de execução da lambda, criar mensagem no tópico criado no SQS

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/egj2w5e4t89b1eosoriu.png)

## Permissão lambda > SQS

No console do IAM, clicar no menu _ROLES_. Procurar pela role criada para a lambda. Dentro dessa role, vamos no menu _Add Permissions_ > _Atacch polices_. Na busca, procurar por _SQS_, e escolher _AmazonSQSFullAccess_(sabemos que não é o ideal rs.) e clicar em Add Permissions.

## Repositório
[Repositorio Github](https://github.com/markgerald/go-audioconvert)