Video streaming con Nest JS
Tags: spanish, typescript, nestjs, node
Published: 2022-09-20 02:46:36


Hace poco, en un proyecto en el que me encontraba trabajando existía un modulo de vídeos, el cual consistía en simplemente en subir el vídeo a un servicio de terceros (Youtube, Twitch, etc...) y luego guardar la URL generada por este servicio en la base de datos. Cuando el cliente solicitaba este vídeo la API simplemente retornaba la URL que apuntaba al respectivo servicio donde se subió el vídeo. Todo esto funcionaba bien hasta que se decidió almacenar los vídeos dentro del servidor donde estaba alojada la aplicación y servirlos desde ahí, es decir, tenia que crear un servicio de streaming para servir los vídeos como lo haría Youtube o Twitch. En este ejemplo lo haremos desde un proyecto NestsJS desde cero, Let's go!!!



## Iniciación del proyecto


Primero que nada, crearemos el proyecto Nest desde cero, como lo explica la [documentación](https://docs.nestjs.com/first-steps):

```bash
$ nest new nestjs-video-streaming
```


Seleccionamos nuestro gestor de paquetes favoritos, en mi caso npm: 


![gestor de paquetes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/si4oyketovr2v1yxsctx.png)

Una vez instaladas todas las dependencias ya tendremos el proyecto iniciado y listo para codear, ahora arrancamos el servidor de desarrollo:

```bash
$ cd nestjs-video-streaming
$ npm run start:dev
```

Por defecto, la aplicación abrirá en el puerto 3000, pero puedes cambiarlo si así deseas, yo lo dejaré en ese mismo puerto.

## Estructura del proyecto

Para este caso, utilizaré los archivos app.controller.ts y app.service.ts que se generan al iniciar el proyecto, pero tú podrás adaptarlo a tu proyecto como mejor te convenga.

Luego, crearé una carpeta en la raíz del proyecto llamada videos y agregaré 3 vídeos de prueba. Debería quedar algo más o menos así:



![project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kwnzeztx7bkw6oose88k.png)


## Ruta para obtener todos los vídeos

En app.controller.ts crearemos la ruta para listar todos los vídeos disponibles en la API, quedando así:

```typescript
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('all-videos')
  getAllVideos(): Array<{
    name: string;
    url: string;
  }> {
    return this.appService.getAllVideos();
  }
}

```

Y nuestro servicio quedaría así:

```typescript
import { Injectable } from '@nestjs/common';

import { readdirSync } from 'fs';

@Injectable()
export class AppService {
  public getAllVideos(): Array<{
    name: string;
    url: string;
  }> {
    const baseUrl: string = 'http://localhost:3000';

    const allFiles: Array<string> = readdirSync('videos/');

    const finalResponse: Array<{
      name: string;
      url: string;
    }> = [];

    for (const file of allFiles) {
      finalResponse.push({ name: `${file}`, url: `${baseUrl}/video/${file}` });
    }

    return finalResponse;
  }
}

```

Con esto ya podemos listar todos los vídeos que están disponibles en la API con su respectiva URL, probemos con postman para ver la respuesta:



![postman test1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hwbatg55h44e21yn3x7u.png)

Un detalle bastante importante es que en la respuesta, la url de todos los vídeos apuntan al servidor local más el nombre del vídeo, esto es así para que podamos buscar el vídeo por su nombre y servirlo, en caso de recibir un nombre de vídeo que no exista lanzaremos un BadRequestException. la constante baseUrl la puedes cambiar para que apunte a tu servidor de producción, lo recomendable seria tenerlo en una variable de entorno:

```typescript
const baseUrl: string = 'http://localhost:3000';
```

Ahora bien, nos faltaría los mas importante, la ruta de streaming de vídeo, creamos la ruta en AppController:

```typescript
import { Controller, Get, Param, Response, Req } from '@nestjs/common';
import { Response as IResponse, Request } from 'express';

import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('all-videos')
  getAllVideos(): Array<{
    name: string;
    url: string;
  }> {
    return this.appService.getAllVideos();
  }

  @Get('video/:name')
  streamVideo(
    @Param('name') name: string,
    @Response() response: IResponse,
    @Req() request: Request,
  ) {
    this.appService.streamVideo(name, response, request);
  }
}


```

Y ahora lo más importante, el servicio: 

```typescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { Response, Request } from 'express';

import { createReadStream, readdirSync, statSync } from 'fs';

@Injectable()
export class AppService {
  public getAllVideos(): Array<{
    name: string;
    url: string;
  }> {
    const baseUrl: string = 'http://localhost:3000';

    const allFiles: Array<string> = readdirSync('videos/');

    const finalResponse: Array<{
      name: string;
      url: string;
    }> = [];

    for (const file of allFiles) {
      finalResponse.push({ name: `${file}`, url: `${baseUrl}/video/${file}` });
    }

    return finalResponse;
  }

  public streamVideo(name: string, response: Response, request: Request) {
    const isValidVideo = this.isValidVideo(name);

    if (!isValidVideo) {
      throw new NotFoundException('El video no existe');
    }

    const { range } = request.headers;

    if (!range) {
      throw new NotFoundException('range not found');
    }

    const path = `videos/${name}`;
    const videoSize = statSync(path).size;
    const chunksize = 1 * 1e6;
    const start = Number(range.replace(/\D/g, ''));
    const end = Math.min(start + chunksize, videoSize - 1);
    const contentLength = end - start + 1;

    const headers = {
      'Content-Range': `bytes ${start}-${end}/${videoSize}`,
      'Accept-Ranges': 'bytes',
      'Content-Length': contentLength,
      'Content-Type': 'video/mp4',
    };

    response.writeHead(206, headers);

    const stream = createReadStream(path, { start, end });

    stream.pipe(response);
  }

  private isValidVideo(name: string) {
    const allFiles: Array<string> = readdirSync('videos/');

    return allFiles.includes(name);
  }
}


```

Muy bien, ahora explicaré detalladamente el método streamVideo y isValidVideo. En primer lugar, el método isValidVideo se encarga de decir si el vídeo existe dentro de la aplicación retornando un booleano, y en el método streamVideo lanzamos la excepción si no existe el vídeo.

```typescript
const isValidVideo = this.isValidVideo(name);

if (!isValidVideo) {
  throw new NotFoundException('El video no existe');
}
```


Tenemos que asegurarnos de que existe el parámetro range en el header de la petición. De lo contrario, no sabremos que parte del vídeo debemos mandar. Las declaraciones if se encargan de esto, devolviendo un error 404 alertando al cliente de que necesita una cabecera de rango:

```typescript
const { range } = request.headers;

if (!range) {
  throw new NotFoundException('range not found');
}
```

También necesitamos proporcionar la ruta y el tamaño del vídeo:

```typescript
const path = `videos/${name}`;
const videoSize = statSync(path).size;
```
El chunk size vendria siendo la cantidad de datos que enviará el servidor por partes, en este caso, se enviará de 1mb en 1mb: 

```typescript
const chunksize = 1 * 1e6;
```

Luego, se define el inicio desde donde se enviaran los datos del video, el final además del tamaño del video:

```typescript
const start = Number(range.replace(/\D/g, ''));
const end = Math.min(start + chunksize, videoSize - 1);
const contentLength = end - start + 1;
```

A continuación definimos los encabezados de respuestas: 

```typescript 
const headers = {
  'Content-Range': `bytes ${start}-${end}/${videoSize}`,
  'Accept-Ranges': 'bytes',
  'Content-Length': contentLength,
  'Content-Type': 'video/mp4',
};
```

Y por ultimo, creamos el objeto stream y madamos la respuesta: 

```typescript 
response.writeHead(206, headers);

const stream = createReadStream(path, { start, end });

stream.pipe(response);
```

Con esto tendríamos terminado el servicio de streaming, ahora para probarlo con postman tenemos que setear el rango en el encabezado de la petición, de la siguiente manera:



![postman2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/shya2wtn9ld8uvn6gepr.png)

Ejecutamos la petición y listo! 


![postman3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pdt0tm7hkooigiz3mcvf.png)

En el postman aparece un reproductor con nuestro video!

En este tutorial, hemos aprendido a construir nuestro propio servidor de streaming de vídeo utilizando Nestjs. Siguiendo los pasos de este tutorial, puedes construir tu propio servidor de streaming de vídeo que puedes integrar en tu propia aplicación. Espero que hayas disfrutado de este artículo!.

El repositorio lo puedes conseguir [aquí.](https://github.com/elbernv/nestjs-video-streaming)