GitHub - Repositório do código
Olá! Eu sou o Anderson Viana, Desenvolvedor de Software, e este é meu primeiro artigo. Tenho um pouco mais de um ano de experiência profissional, mas sou muito entusiasta, especialmente quando se trata de entender como diferentes tipos de sistemas funcionam. Um exemplo disso são as plataformas de streaming, como YouTube e Twitch. Por conta desse entusiasmo, resolvi estudar e criar minha própria API de live streaming em NestJS.
Neste artigo, vou compartilhar o que aprendi sobre fluxos e protocolos envolvidos nesse tipo de sistema e mostrar como coloquei tudo em prática no código!
Fluxo do Sistema
Esse diagrama ilustra o fluxo de uma API de live streaming que utiliza os protocolos RTMP e HLS para transmitir conteúdo de vídeo ao vivo de um streamer para um espectador.
1. Streamer (Usuário que faz a transmissão ao vivo)
O streamer usa um software de transmissão, como o OBS Studio, para capturar seu vídeo e áudio ao vivo. Esse software se conecta a um servidor RTMP usando uma stream_key
associada ao nome de usuário do streamer. O fluxo segue assim:
O streamer configura o OBS Studio para enviar o fluxo de vídeo codificado (geralmente em H.264 para vídeo e AAC para áudio) para o servidor RTMP.
2. RTMP (Real-Time Messaging Protocol)
O servidor RTMP recebe o fluxo de vídeo enviado pelo software de transmissão. Esse protocolo serve para transportar o fluxo de vídeo ao vivo de baixa latência até o servidor. Depois de receber o fluxo:
O RTMP repassa esse conteúdo para o FFmpeg para processamento adicional.
3. FFmpeg
O FFmpeg é utilizado para converter o fluxo RTMP em um formato compatível com HLS. Ele realiza tarefas como:
Codificar ou recodificar o vídeo em segmentos menores.
Criar a playlist .m3u8
com os segmentos de vídeo .ts.
Isso permite que o vídeo possa ser servido de maneira eficiente para diversos dispositivos em tempo real, garantindo que o conteúdo esteja dividido em partes facilmente consumíveis.
4. Storage
Após o processamento pelo FFmpeg, os segmentos gerados (arquivos .ts
) e a playlist (output.m3u8) são armazenados em um diretório específico, geralmente baseado no nome de usuário do streamer. Esses arquivos são servidos por meio de um servidor HTTP para serem acessados via o protocolo HLS.
5. HLS (HTTP Live Streaming)
O servidor HLS entrega os arquivos .m3u8
e os segmentos .ts
aos espectadores. Ele permite que o vídeo seja transmitido continuamente para o espectador:
Quando o espectador faz uma requisição GET /stream/{username}
, o servidor HLS responde com o fluxo do streamer correspondente.
O player do espectador baixa a playlist .m3u8, e depois começa a requisitar e reproduzir os segmentos .ts para assistir ao vídeo ao vivo.
6. Espectador (Usuário que assiste à transmissão)
O espectador acessa a transmissão ao vivo enviando uma requisição HTTP para o servidor, solicitando o stream de um usuário específico (/stream/{username})
. O player de vídeo do espectador processa a playlist e os segmentos recebidos, exibindo o conteúdo ao vivo em tempo real.
Resumo do Fluxo
- O streamer transmite o vídeo usando o OBS Studio via RTMP.
- O servidor RTMP recebe o fluxo e o envia para o FFmpeg.
- O FFmpeg converte o fluxo em segmentos e cria uma playlist HLS.
- O servidor HLS disponibiliza o conteúdo para os espectadores.
- Os espectadores acessam a transmissão via HTTP e assistem ao fluxo ao vivo.
- Esse sistema de streaming permite uma transmissão ao vivo eficiente e em tempo real, utilizando uma combinação de RTMP para o transporte inicial do vídeo e HLS para a entrega final aos espectadores.
Vamos para o código!!!
1. Configuração do Servidor RTMP
const config = {
rtmp: {
port: 1935,
chunk_size: 4096,
gop_cache: true,
ping: 25,
ping_timeout: 50,
},
};
const rtmpServer = new NodeMediaServer(config);
Aqui, você configura um servidor RTMP usando o pacote NodeMediaServer. O protocolo RTMP é usado para enviar o fluxo do streamer para o servidor com baixa latência, essencial para transmissões ao vivo. No objeto config, você define:
-
port
: A porta em que o servidor RTMP escutará (porta 1935, padrão para RTMP). -
chunk_size
: Define o tamanho dos chunks de dados. -
gop_cache
: Cache dos grupos de imagens (GOP), permitindo uma transmissão mais eficiente. -
ping
eping_timeout
: Intervalo e timeout do ping para manter a conexão ativa com o servidor.
2. Conversão do Fluxo com FFmpeg
ffmpeg(`rtmp://localhost:1935/live/${streamPath}?sign=${sign}`)
.addOptions([
'-c:v libx264',
'-preset ultrafast',
'-tune zerolatency',
'-maxrate 3000k',
'-bufsize 6000k',
'-pix_fmt yuv420p',
'-g 30',
'-c:a aac',
'-b:a 160k',
'-ac 2',
'-ar 44100',
'-f hls',
'-hls_time 1',
'-hls_flags delete_segments',
'-hls_list_size 2',
])
.output(`./public/media/${streamPath}/output.m3u8`)
.on('start', () => {
console.log('FFmpeg started with command');
})
.on('end', async () => {
console.log('Conversion finished');
});
Aqui, o fluxo RTMP é processado pelo FFmpeg, que o converte para o formato HLS, adequado para streaming adaptativo:
-
-c:v libx264
: Codifica o vídeo em H.264. -
-preset ultrafast
: Usa uma configuração mais rápida (ideal para transmissões em tempo real). -
-tune zerolatency
: Ajusta o codec para uma latência muito baixa. -
-f hls
: Define o formato de saída como HLS. -
-hls_time 1
: Define a duração de cada segmento .ts em 1 segundo. -
-hls_flags delete_segments
: Exclui segmentos antigos, economizando espaço. -
-hls_list_size 2
: Define o número de segmentos a serem mantidos na playlist .m3u8.
O FFmpeg converte o fluxo em uma playlist .m3u8
e segmentos .ts
para serem usados pelo HLS.
3. Servidor HLS e Manipulação de Segmentos
const provider = fsProvider;
const CONTENT_TYPE = {
MANIFEST: 'application/vnd.apple.mpegurl',
SEGMENT: 'video/MP2T',
HTML: 'text/html',
};
const rootPath = '/';
export function serverHLS(
req: Request,
res: Response,
next: NextFunction,
username: string,
param: string,
) {
const uri = param || 'output.m3u8';
const uriRelativeToPath = uri.startsWith(rootPath)
? uri.slice(rootPath.length)
: uri;
const relativePath = path.normalize(uriRelativeToPath);
const filePath = path.join(`public/media/${username}`, relativePath);
req['filePath'] = filePath;
const extension = path.extname(filePath);
const ae = req.headers['accept-encoding'] || '';
req['acceptsCompression'] = ae.includes('gzip');
provider.exists(req, (err: any, exists: boolean) => {
if (err) {
throw new HttpException(
'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
} else if (!exists) {
throw new HttpException('Not Found', HttpStatus.NOT_FOUND);
} else {
switch (extension) {
case '.m3u8':
req.headers['req-username'] = username;
return writeManifest(req, res, next);
case '.ts':
return writeSegment(req, res);
default:
next();
}
}
});
}
Esse trecho configura o endpoint HLS. Quando o espectador faz uma solicitação, essa função:
- Recebe a requisição com o
username
do streamer. - Identifica o arquivo solicitado (
output.m3u8
ou um segmento.ts
). - Verifica se o arquivo existe. Se não, retorna um erro
Not Found
.
Se o arquivo for .m3u8
, chama writeManifest
; se for .ts
, chama writeSegment
.
Função writeManifest
export function writeManifest(req: Request, res: Response, next: NextFunction) {
provider.getManifestStream(req, (err: any, stream: fs.ReadStream) => {
if (err) {
res.status(HttpStatus.INTERNAL_SERVER_ERROR).end();
return next();
}
res.setHeader('Content-Type', CONTENT_TYPE.MANIFEST);
res.status(HttpStatus.OK);
if (req['acceptsCompression']) {
res.setHeader('Content-Encoding', 'gzip');
const gzip = zlib.createGzip();
stream.pipe(gzip).pipe(res);
} else {
stream.pipe(res, { end: true });
}
});
}
A writeManifest
serve a playlist .m3u8
:
- Abre o arquivo
.m3u8
comgetManifestStream
. - Define o
Content-Type
comoapplication/vnd.apple.mpegurl
. - Verifica se a requisição aceita
gzip
e, se sim, comprime o fluxo para otimizar a transferência.
Função writeSegment
export function writeSegment(req: Request, res: Response) {
provider.getSegmentStream(req, (err: any, stream: fs.ReadStream) => {
if (err) {
res.status(HttpStatus.INTERNAL_SERVER_ERROR).end();
return;
}
res.setHeader('Content-Type', CONTENT_TYPE.SEGMENT);
res.status(HttpStatus.OK);
stream.pipe(res);
});
}
A writeSegment
serve os segmentos .ts
:
Abre o arquivo .ts
correspondente e define o Content-Type
como video/MP2T
.
Envia o fluxo .ts
diretamente ao espectador, permitindo que o player reproduza o conteúdo ao vivo.
4. Rota para Entrega de Vídeo ao Vivo via HLS
@Controller('stream')
export class StreamController {
constructor(private readonly usersService: UsersService) {}
@Get(':username/*')
async stream(
@Req() req: Request,
@Res() res: Response,
@Next() next: NextFunction,
@Param('username') username: string,
@Param() params: string[],
) {
const user = await this.usersService.findOne(username);
if (!user) {
throw new HttpException('Not Found', HttpStatus.NOT_FOUND);
}
if (!user.streaming) {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
serverHLS(req, res, next, username, params[0]);
}
}
A função stream
atende às requisições para entregar vídeo ao vivo via protocolo HLS:
-
Busca o usuário:
- Usa
findOne
para verificar se o usuário existe no banco de dados.
- Usa
-
Valida o status da transmissão:
- Retorna
404 Not Found
se o usuário não existe. - Retorna
403 Forbidden
se o usuário não está transmitindo.
- Retorna
-
Serve o conteúdo HLS:
- Chama
serverHLS
para entregar o conteúdo HLS, usando ousername
para localizar os arquivos da transmissão eparams[0]
para determinar o caminho do conteúdo específico (playlist.m3u8
ou segmento.ts
).
- Chama
Top comments (0)