DEV Community

Cover image for Criando uma API de Live Streaming com NestJS
Anderson Viana
Anderson Viana

Posted on

Criando uma API de Live Streaming com NestJS

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

fluxo

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

  1. O streamer transmite o vídeo usando o OBS Studio via RTMP.
  2. O servidor RTMP recebe o fluxo e o envia para o FFmpeg.
  3. O FFmpeg converte o fluxo em segmentos e cria uma playlist HLS.
  4. O servidor HLS disponibiliza o conteúdo para os espectadores.
  5. Os espectadores acessam a transmissão via HTTP e assistem ao fluxo ao vivo.
  6. 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);
Enter fullscreen mode Exit fullscreen mode

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 e ping_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');
});
Enter fullscreen mode Exit fullscreen mode

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();
      }
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Esse trecho configura o endpoint HLS. Quando o espectador faz uma solicitação, essa função:

  1. Recebe a requisição com o username do streamer.
  2. Identifica o arquivo solicitado (output.m3u8 ou um segmento .ts).
  3. 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 });
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

A writeManifest serve a playlist .m3u8:

  1. Abre o arquivo .m3u8 com getManifestStream.
  2. Define o Content-Type como application/vnd.apple.mpegurl.
  3. 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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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]);
  }
}
Enter fullscreen mode Exit fullscreen mode

A função stream atende às requisições para entregar vídeo ao vivo via protocolo HLS:

  1. Busca o usuário:

    • Usa findOne para verificar se o usuário existe no banco de dados.
  2. 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.
  3. Serve o conteúdo HLS:

    • Chama serverHLS para entregar o conteúdo HLS, usando o username para localizar os arquivos da transmissão e params[0] para determinar o caminho do conteúdo específico (playlist .m3u8 ou segmento .ts).

Top comments (0)