O YouTube tem de tudo né, uma coisa até que comum são vídeos de compilação. São vídeos geralmente curtos, abaixo de 10 minutos que tem vários vídeos dentro. Meu amigo Adriano tem dias que tá inspirado e manda um monte de coisa nos status do WhatsApp mesmo. A gente nisso fundou o AcheiPorAi que a gente basicamente encaminhava essas coisas pra lá, o que agilizava muito as coisas por que era bem mais rápido para postar. Eu criei até um Twitter que postava no Telegram usando um bot de RSS, o problema é que foi tudo flopado. A página e o canal no Telegram juntos tem coisa de 20 seguidores atualmente. A intenção nunca foi decolar. A gente mandava as coisas e quem viesse ver era lucro.

O Achei por Ai até onde eu me lembro não tem nada próprio autoral, a gente não tem a preocupação de dizer de quem é os créditos até por que o bot que posta posta o link do tweet e os vídeos são encaminhados então aparece de onde veio. Eu ativei também a assinatura do canal pro povo saber quem que postou. Nível de preguiça aqui é mais de oito mil.

Para as automações inicialmente eu usei IFTTT mas ai eles mudaram o modelo de negócio e eu achei mais conveniente usar o IFTTT como fonte de eventos e o Pipedream como reator a esses eventos, até depois me dar conta que é mais confiável e simples usar o RSSHub pelo melhor bot de RSS do Telegram e está assim até hoje.

Esses dias eu tinha lido um artigo que eu não lembro onde de um cara que fez um esquema em shell script usando ffmpeg para gerar essas compilações baixando vídeos do TikTok do ultimo dia de um subreddit, nisso eu tive a ideia de fazer isso com um grupo do Telegram.

Minha ideia era de fazer um acumulador de file_ids no Pipedream, a parte interativa do bot. Todos os vídeos mandados a um chat são adicionados na lista e carregados em um objeto persistente. Esse objeto persistente, e a autenticação gerenciada, são para mim as killer features do Pipedream. Não é o melhor em latência ou volume mas para coisas simples atende super bem, e de graça. O acumulador mantém uma lista dos arquivos que ainda não foram processados seguidos do seu tamanho em segundos. Por limitação do Telegram eu não posso aceitar arquivos maiores que 20MB, o que até agora não impediu nada.

Para cada mensagem que acontece no chat autorizado o Telegram faz uma request no endpoint do Pipedream, coisa que é necessário habilitar usando o método setWebhook. Se a mensagem não vir do chat autorizado o bot manda uma mensagem no chat autorizado dedurando a tentativa, se vir do chat autorizado e for vídeo ele adiciona na lista, se não for vídeo ele responde que não é vídeo. Todas as mensagens desse agente Pipedream mencionam a mensagem em questão quando respondem.

O Pipedream claramente não vai postar vídeos no Telegram, até por que nenhuma invocação pode levar mais de 30s para acontecer, logo eu fiz o agente que gera e posta os vídeos como uma aplicação separada. Diferente do agente pipedream que é feito em JS Vanilla, o agente video é feito em Go subindo nos ombros do maravilhoso FFmpeg. O agente vídeo necessita do mesmo token do Telegram do agente Pipedream e o endereço por onde o agente pipedream tá na escuta, ambos segredos pois o agente pipedream gera os tokens de acesso do YouTube.

Pelo agente pipedream o agente vídeo descobre qual o chat autorizado, quais file_ids estão na fila, quais as credenciais do youtube e consegue marcar vídeos como processados, que são então deletados da fila.

Por problemas de confiabilidade sabe se lá por que, em um dos ambientes de execução testados (Github Actions), o Pipedream em algumas chamadas específicas retornava status code 502. O agente video notificava algumas coisas para o chat autorizado como que ele começou a rodar e qual o link do vídeo postado, para resolver esse problema eu tive que fazer o agente vídeo conversar diretamente com o Telegram, o que foi bem tranquilo por que o token do Telegram já estava disponível igual.

Cada vídeo baixado do o Telegram pelo agente vídeo passava por um processo de normalização. O vídeo final é na resolução 1920x1080, enquanto os vídeos de entrada são das mais variadas proporções e resoluções, nisso eu aprendi como mexer um pouco com os filtros do ffmpeg. Inicialmente fui caçar código no stack overflow, o que acabou me trollando por que eu não entendia o esquema e os vídeos saiam meio errados. Nisso eu parei pra entender essa sintaxe de filtros. Cada canal de vídeo ou áudio tem um nome, geralmente a entrada é [v:0]. Passando uma sequencia de filtros você pode dar nome a essa camada. O filtro usado para gerar os vídeos foi o seguinte:

[0:v]scale=1920*2:-1,crop=1920:1080,boxblur=luma_radius=min(h\,w)/20:luma_power=1:chroma_radius=min(cw\,ch)/20:chroma_power=1[bg];
[0:v]scale=-1:1080[ov];
[bg][ov]overlay=(W-w)/2,crop=1920:1080

Geralmente no comando todo o filtro é passado na mesma linha, o que dificulta o entendimento, lendo desse jeito parece bem mais claro.

O primeiro filtro estica o vídeo para largura 1920*2 com a altura só seguindo a proporção, depois corta esse vídeo para 1920x1080 aplicando blur, esse então vira a camada bg.

O segundo filtro estica o vídeo com largura automatica a altura 1080, essa é a overlay que é o vídeo em sí, chamada de ov.

O ultimo filtro coloca a overlay em cima do background centralizado e apara no formato do vídeo final para ter certeza que o tamanho vai ser o mesmo.

FUN FACT: Esse código é mais otimizado, além de ser mais genérico que o do Stack Overflow por que aquele aplicava o blur antes de cortar o vídeo gerando desperdício, então deu pra ter uns 20% de ganho de velocidade pra mais com isso.

Com os vídeos normalizados o processo é tão simples quanto enumerar e juntar os vídeos em um só, esse estágio é o mais rápido por que não requere transcodificação.

Depois que o vídeo é gerado ele é mandado para o Youtube, o id recebido do youtube é usado para gerar o link do vídeo e reportar no chat autorizado. Logo depois os ids de arquivo usados são enviados ao agente pipedream para serem removidos de uma vez da fila.

Sobre os ambientes de execução, o primeiro que eu tentei foi no meu humilde i5 7200U, que até que não sofreu tanto para processar os vídeos. Já fiz ele sofrer bem mais. Depois tentei algo mais offload, Actions tem um ambiente legal para essas coisas mas estava tendo alguns problemas estranhos de erro 502 com o Pipedream então não tava dando pra confiar muito. Ai eu lembrei da minha VPS 0800 f1-micro no Google Cloud rodando NixOS. Demora bastante até para processar um vídeo então configurei um systemd timer para criar o vídeo de madrugada, ela conseguiu lançar o vídeo sem problemas apesar de ter demorado um tempo.

A princípio é para ter vídeo postado todo dia depois das 3 da manhã e o canal de YouTube feito com a integração de Telegram, Pipedream, GCP, NixOS e Golang é esse.

A foto de perfil é um símbolo de aleatoriedade e uma referência ao grupo Ilha da Macacada. Como começou isso eu não faço ideia mas queria algo aleatório.

E isso é tudo pessoal, dá like, se inscreve, compartilha com a galera hehehe.

Se esse canal vingar é lucro.

Foto do bot:

image-20210205155635063

Novidades - 2/3/2021

Depois do post eu continuei mexendo no bot. Agora ele suporta gifs, que são basicamente vídeos mp4 sem a track de audio. Uma side effect disso é o vídeo final não ter track de audio quando o primeiro vídeo origem não tiver track de audio. Para fazer funcionar foi uma gambiarra cabulosa. Eu criei uma track de audio vazia para todos os vídeos, se o vídeo já tem track de audio agora vai ter duas, quando junta os vídeos ele normaliza para uma track de audio e sucesso. Tava dando uns paus na junção dos vídeos bugando as timestamps, dessincronizando audio e corrompendo o vídeo final. A track de audio gerada por padrão é infinita então eu cortei o vídeo e o audio para o número de segundos que o vídeo original tinha arredondado para baixo obtida pelo FFProbe.

Sobre a questão de hospedagem eu consegui de um jeito bem gambiarresco resolver os erros 502 do Github Actions no Pipedream, que aliás não encapsula mais as chamadas para a API do Telegram. Quando você envia um link, seja você ou o bot, o Telegram acessa o link para buscar o preview desse link o que consequentemente causa uma request na endpoint. Nisso houveram duas situações:

  • A primeira é que quando alguém enviava um vídeo o bot retornava com um link para remover esse vídeo da lista, o que no final das contas não fazia nada
  • A segunda é que o link enviado de intervenção manual para remover os vídeos já processados era chamado automaticamente, então não precisava bem de uma intervenção manual

Isso eu mitiguei com um check no User-Agent, o Telegram fornece TelegramBot (like TwitterBot), logo dá pra checar se é o Telegram que pediu para remover os itens. Como a endpoint para remover items era a mesma (/deleteIds) foi colocado um query param disallowTelegram que diz se o Telegram estiver fazendo a request ele só ignora, logo temos o melhor dos dois mundos.

Coloquei o bot de GitHub no grupo para anunciar quando um commit novo acontece então não preciso avisar os stakeholders (eu e o Adriano).

O bot tá configurado pra rodar toda meia noite localtime (GMT -3) e tá terminando a task em cerca de 10 minutos.

O código pertinente ao processamento de vídeos é o seguinte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import (
	"bytes"
	"fmt"
	"os/exec"
)

type Video struct {
    Filename string
    Length int
}

const ffmpegFilter = `
[0:v]scale=1920*2:-1,crop=1920:1080,boxblur=luma_radius=min(h\,w)/20:luma_power=1:chroma_radius=min(cw\,ch)/20:chroma_power=1[bg];
[0:v]scale=-1:1080[ov];
[bg][ov]overlay=(W-w)/2,crop=1920:1080[composed];
anullsrc=r=44100:cl=stereo[anull]
`

func NewVideoFromAnotherVideo(filename string) (*Video, error) {
    var ret Video
    ret.Filename = GetFilenameFromURL("output.mkv")
    Log("Ingesting video: '%s' as '%s'", filename, ret.Filename)
    sizebuf := bytes.NewBuffer([]byte{})
    cmd := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
    cmd.Stdout = sizebuf
    err := cmd.Run()
    if err != nil {
        return nil, err
    }
    var length float32
    fmt.Fscan(sizebuf, &length)
    ret.Length = int(length)
    err = Command("ffmpeg", "-i", filename,
        "-t", fmt.Sprintf("%d", ret.Length),
        "-c:a", "aac", "-c:v", "h264",
        "-filter_complex", ffmpegFilter,
        "-map", "[composed]",
        "-map", "0:a?",
        "-map", "[anull]",
        ret.Filename,
    )
    AddFileCleanupHook(ret.Filename)
    if err != nil {
        return nil, err
    }
    return &ret, nil
}

func ConcatVideos(videos ...*Video) (*Video, error) {
    var ret Video
    ret.Filename = GetFilenameFromURL("output.mkv")
    ret.Length = 0
    Log("Concating %d videos as '%s'", len(videos), ret.Filename)
    for _, video := range videos {
        ret.Length += video.Length
    }
    files := make([]string, len(videos))
    for i := 0; i < len(videos); i++ {
        files[i] = fmt.Sprintf("file '%s'", videos[i].Filename)
    }
    listFile, err := WriteLines(files...)
    if err != nil {
        return nil, err
    }
    err = Command("ffmpeg", "-f", "concat", "-safe", "0", "-i", listFile, ret.Filename)
    if err != nil {
        return nil, err
    }
    AddFileCleanupHook(ret.Filename)
    return &ret, nil
}

Novamente, o link do canal automático é esse.

Viniccius13 estaria orgulhoso se fosse feito com redstone.