Video streaming con Nest JS
Elber
Posted on September 20, 2022
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:
$ nest new nestjs-video-streaming
Seleccionamos nuestro gestor de paquetes favoritos, en mi caso npm:
Una vez instaladas todas las dependencias ya tendremos el proyecto iniciado y listo para codear, ahora arrancamos el servidor de desarrollo:
$ 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í:
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í:
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í:
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:
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:
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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
Ejecutamos la petición y listo!
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í.
Posted on September 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.