Creación de una aplicación de transmisión de video con Nuxt.js, Node y Express

- Requisitos previos
- Configurando nuestra aplicación
- Configurando nuestro servidor
- Solicitudes que serán manejadas por el frontend
- Devolver datos de maqueta para la lista de videos
- Devolver datos para un solo video
- Transmitiendo los vídeos
- Cómo funciona la ruta del vídeo en streaming
- ¿Qué sucede con las conexiones inestables?
- Creando un archivo de subtítulos para nuestros videos
- Construyendo nuestra interfaz
- Instalación
- Nuestra estructura de archivos Nuxt
- Componente de barra de navegación
- Componente de vídeo
- Agregar nuestro archivo de subtítulos
- Qué tener en cuenta al crear una transmisión de vídeo resiliente.
- Conclusión
En este artículo, crearemos una aplicación de transmisión de video utilizando Nuxt.js y Node.js. Específicamente, crearemos una aplicación Node.js del lado del servidor que se encargará de buscar y transmitir videos, generar miniaturas para sus videos y publicar subtítulos.
Los vídeos funcionan con transmisiones. Esto significa que, en lugar de enviar el vídeo completo de una vez, se envía un vídeo como un conjunto de fragmentos más pequeños que conforman el vídeo completo. Esto explica por qué los videos se almacenan en el buffer cuando se mira un video en una banda ancha lenta porque solo reproduce los fragmentos que ha recibido e intenta cargar más.
Este artículo está dirigido a desarrolladores que estén dispuestos a aprender una nueva tecnología mediante la creación de un proyecto real: una aplicación de transmisión de video con Node.js como backend y Nuxt.js como cliente.
- Node.js es un tiempo de ejecución que se utiliza para crear aplicaciones rápidas y escalables. Lo usaremos para gestionar la búsqueda y transmisión de videos, generar miniaturas para videos y publicar subtítulos para videos.
- Nuxt.js es un marco de Vue.js que nos ayuda a crear fácilmente aplicaciones Vue.js renderizadas en servidor. Consumiremos nuestra API para los videos y esta aplicación tendrá dos vistas: una lista de videos disponibles y una vista de reproductor para cada video.
Requisitos previos
- Comprensión de HTML, CSS, JavaScript, Node/Express y Vue.
- Un editor de texto (por ejemplo, VS Code).
- Un navegador web (por ejemplo, Chrome, Firefox).
- FFmpeg instalado en su estación de trabajo.
- Nodo.js. nvm .
- Puedes obtener el código fuente en GitHub .
Configurando nuestra aplicación
En esta aplicación, construiremos las rutas para realizar solicitudes desde el frontend:
videos
ruta para obtener una lista de videos y sus datos.- una ruta para buscar solo un video de nuestra lista de videos.
streaming
ruta para transmitir los videos.captions
ruta para agregar subtítulos a los videos que estamos transmitiendo.
Una vez creadas nuestras rutas, crearemos una estructura estructural de nuestra Nuxt
interfaz, donde crearemos la página Home
dinámica player
. Luego solicitamos nuestra videos
ruta para llenar la página de inicio con los datos del video, otra solicitud para transmitir los videos en nuestra player
página y, finalmente, una solicitud para entregar los archivos de subtítulos que usarán los videos.
Para configurar nuestra aplicación, creamos nuestro directorio de proyecto,
mkdir streaming-app
Configurando nuestro servidor
En nuestro streaming-app
directorio, creamos una carpeta llamada backend
.
cd streaming-appmkdir backend
En nuestra carpeta backend, inicializamos un package.json
archivo para almacenar información sobre nuestro proyecto de servidor.
cd backendnpm init -y
Necesitamos instalar los siguientes paquetes para construir nuestra aplicación.
nodemon
reinicia automáticamente nuestro servidor cuando realizamos cambios.express
nos brinda una buena interfaz para manejar rutas.cors
nos permitirá realizar solicitudes de origen cruzado ya que nuestro cliente y servidor se ejecutarán en diferentes puertos.
En nuestro directorio backend, creamos una carpeta assets
para guardar nuestros videos para transmisión.
mkdir assets
Copie un .mp4
archivo en la carpeta de activos y asígnele el nombre video1
. Puede utilizar .mp4
vídeos breves de muestra que se pueden encontrar en Github Repo .
Crea un app.js
archivo y agrega los paquetes necesarios para nuestra aplicación.
const express = require('express');const fs = require('fs');const cors = require('cors');const path = require('path');const app = express();app.use(cors())
El fs
módulo se utiliza para leer y escribir archivos fácilmente en nuestro servidor, mientras que el path
módulo proporciona una forma de trabajar con directorios y rutas de archivos.
Ahora creamos una ./video
ruta. Cuando se solicite, enviará un archivo de video al cliente.
// add after 'const app = express();'app.get('/video', (req, res) = { res.sendFile('assets/video1.mp4', { root: __dirname });});
Esta ruta sirve el video1.mp4
archivo de vídeo cuando se solicita. Luego escuchamos a nuestro servidor en el puerto 3000
.
// add to end of app.js fileapp.listen(5000, () = { console.log('Listening on port 5000!')});
Se agrega un script en el package.json
archivo para iniciar nuestro servidor usando nodemon.
"scripts": { "start": "nodemon app.js" },
Luego en tu terminal ejecuta:
npm run start
Si ve el mensaje Listening on port 3000!
en la terminal, entonces el servidor está funcionando correctamente. Navegue a https://localhost:5000/video en su navegador y debería ver el video reproduciéndose.
Solicitudes que serán manejadas por el frontend
A continuación se muestran las solicitudes que haremos al backend desde nuestro frontend y que necesitamos que el servidor maneje.
/videos
Devuelve una serie de datos de maquetas de vídeo que se utilizarán para completar la lista de vídeos en laHome
página de nuestra interfaz./video/:id/data
Devuelve metadatos para un solo vídeo. Utilizado por laPlayer
página en nuestro frontend./video/:id
Transmite un vídeo con una identificación determinada. Utilizado por laPlayer
página.
Creemos las rutas.
Devolver datos de maqueta para la lista de videos
Para esta aplicación de demostración, crearemos una matriz de objetos que contendrán los metadatos y los enviaremos a la interfaz cuando se solicite. En una aplicación real, probablemente estarías leyendo los datos de una base de datos, que luego se usarían para generar una matriz como esta. En aras de la simplicidad, no haremos eso en este tutorial.
En nuestra carpeta backend, cree un archivo mockdata.js
y rellénelo con metadatos para nuestra lista de videos.
const allVideos = [ { id: "tom and jerry", poster: 'https://image.tmdb.org/t/p/w500/fev8UFNFFYsD5q7AcYS8LyTzqwl.jpg', duration: '3 mins', name: 'Tom Jerry' }, { id: "soul", poster: 'https://image.tmdb.org/t/p/w500/kf456ZqeC45XTvo6W9pW5clYKfQ.jpg', duration: '4 mins', name: 'Soul' }, { id: "outside the wire", poster: 'https://image.tmdb.org/t/p/w500/lOSdUkGQmbAl5JQ3QoHqBZUbZhC.jpg', duration: '2 mins', name: 'Outside the wire' },];module.exports = allVideos
Como podemos ver desde arriba, cada objeto contiene información sobre el video. Observe el poster
atributo que contiene el enlace a una imagen de póster del vídeo.
Creemos una videos
ruta ya que todas nuestras solicitudes que debe realizar la interfaz van precedidas de /videos
.
Para hacer esto, creemos una routes
carpeta y agreguemos un Video.js
archivo para nuestra /videos
ruta. En este archivo, necesitaremos express
y usaremos el enrutador express para crear nuestra ruta.
const express = require('express')const router = express.Router()
Cuando vamos a la /videos
ruta, queremos obtener nuestra lista de videos, así que solicitemos el mockData.js
archivo en nuestro Video.js
archivo y hagamos nuestra solicitud.
const express = require('express')const router = express.Router()const videos = require('../mockData')// get list of videosrouter.get('/', (req,res)={ res.json(videos)})module.exports = router;
La /videos
ruta ahora está declarada, guarde el archivo y debería reiniciar automáticamente el servidor. Una vez que haya comenzado, navegue hasta https://localhost:3000/videos y nuestra matriz se devolverá en formato JSON.
Devolver datos para un solo video
Queremos poder realizar una solicitud para un video en particular en nuestra lista de videos. Podemos recuperar datos de video en particular en nuestra matriz usando el que id
le proporcionamos. Hagamos una solicitud, todavía en nuestro Video.js
expediente.
// make request for a particular videorouter.get('/:id/data', (req,res)= { const id = parseInt(req.params.id, 10) res.json(videos[id])})
El código anterior obtiene los id
parámetros de ruta y los convierte a un número entero. Luego enviamos el objeto que coincide con el id
de la videos
matriz al cliente.
Transmitiendo los vídeos
En nuestro app.js
archivo, creamos una /video
ruta que entrega un video al cliente. Queremos que este punto final envíe fragmentos más pequeños del video, en lugar de entregar un archivo de video completo a pedido.
Queremos poder servir dinámicamente uno de los tres videos que están en la allVideos
matriz y transmitir los videos en fragmentos, por lo tanto:
Eliminar la /video
ruta de app.js
.
Necesitamos tres videos, así que copie los videos de ejemplo del código fuente del tutorial en el assets/
directorio de su server
proyecto. Asegúrese de que los nombres de archivo de los videos correspondan a los id
de la videos
matriz:
De vuelta en nuestro Video.js
archivo, crea la ruta para la transmisión de videos.
router.get('/video/:id', (req, res) = { const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range; if (videoRange) { const parts = videoRange.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end}); const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); } else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(videoPath).pipe(res); }});
Si navegamos a https://localhost:5000/videos/video/outside-the-wire en nuestro navegador, podremos ver la transmisión de video.
Cómo funciona la ruta del vídeo en streaming
Hay bastante código escrito en nuestra ruta de transmisión de video, así que veámoslo línea por línea.
const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range;
Primero, de nuestra solicitud, obtenemos el id
uso de la ruta req.params.id
y lo usamos para generar videoPath
el video. Luego leemos fileSize
usando el sistema de archivos fs
que importamos. Para videos, el navegador de un usuario enviará un range
parámetro en la solicitud. Esto le permite al servidor saber qué parte del video enviar al cliente.
Algunos navegadores envían un rango en la solicitud inicial, pero otros no. Para aquellos que no lo hacen, o si por cualquier otro motivo el navegador no envía un rango, lo manejamos en el else
bloque. Este código obtiene el tamaño del archivo y envía los primeros fragmentos del vídeo:
else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(path).pipe(res);}
Manejaremos solicitudes posteriores, incluido el rango en un if
bloque.
if (videoRange) { const parts = videoRange.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end}); const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); }
Este código anterior crea una secuencia de lectura utilizando los valores start
y end
del rango. Establezca los Content-Length
encabezados de respuesta en el tamaño del fragmento que se calcula a partir de los valores start
y end
. También usamos el código HTTP 206 , lo que significa que la respuesta contiene contenido parcial. Esto significa que el navegador seguirá realizando solicitudes hasta que haya recuperado todos los fragmentos del vídeo.
¿Qué sucede con las conexiones inestables?
Si el usuario tiene una conexión lenta, la transmisión de red lo indicará solicitando que la fuente de E/S se detenga hasta que el cliente esté listo para recibir más datos. Esto se conoce como contrapresión . Podemos llevar este ejemplo un paso más allá y ver lo fácil que es extender la transmisión. ¡También podemos agregar compresión fácilmente!
const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end});
Podemos ver arriba que ReadStream
se crea a y sirve el video fragmento por fragmento.
const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', };res.writeHead(206, head); file.pipe(res);
El encabezado de la solicitud contiene Content-Range
, que es el cambio de inicio y final para que el siguiente fragmento de video se transmita al frontend, es content-length
el fragmento de video enviado. También especificamos el tipo de contenido que estamos transmitiendo, que es mp4
. El cabezal de escritura de 206 está configurado para responder solo con transmisiones recién creadas.
Creando un archivo de subtítulos para nuestros videos
Así es como .vtt
se ve un archivo de título.
WEBVTT00:00:00.200 -- 00:00:01.000Creating a tutorial can be very00:00:01.500 -- 00:00:04.300fun to do.
Los archivos de subtítulos contienen texto de lo que se dice en un vídeo. También contiene códigos de tiempo para saber cuándo debe mostrarse cada línea de texto. Queremos que nuestros videos tengan subtítulos y no crearemos nuestro propio archivo de subtítulos para este tutorial, por lo que puede dirigirse a la carpeta de subtítulos en el assets
directorio del repositorio y descargarlos.
Creemos una nueva ruta que manejará la solicitud de título:
router.get('/video/:id/caption', (req, res) = res.sendFile(`assets/captions/${req.params.id}.vtt`, { root: __dirname }));
Construyendo nuestra interfaz
Para comenzar con la parte visual de nuestro sistema, tendríamos que construir nuestra plataforma frontal.
Nota : necesita vue-cli para crear nuestra aplicación. Si no lo tienes instalado en tu computadora, puedes ejecutarlo npm install -g @vue/cli
para instalarlo.
Instalación
En la raíz de nuestro proyecto, creemos nuestra carpeta de front-end:
mkdir frontendcd frontend
y en él inicializamos nuestro package.json
archivo, copiamos y pegamos lo siguiente en él:
{ "name": "my-app", "scripts": { "dev": "nuxt", "build": "nuxt build", "generate": "nuxt generate", "start": "nuxt start" }}
luego instale nuxt
:
npm add nuxt
y ejecute el siguiente comando para ejecutar la aplicación Nuxt.js:
npm run dev
Nuestra estructura de archivos Nuxt
Ahora que tenemos Nuxt instalado, podemos comenzar a diseñar nuestra interfaz.
Primero, necesitamos crear una layouts
carpeta en la raíz de nuestra aplicación. Esta carpeta define el diseño de la aplicación, sin importar la página a la que naveguemos. Cosas como nuestra barra de navegación y pie de página se encuentran aquí. En la carpeta frontend, creamos default.vue
nuestro diseño predeterminado cuando iniciamos nuestra aplicación frontend.
mkdir layoutscd layoutstouch default.vue
Luego una components
carpeta para crear todos nuestros componentes. Necesitaremos solo dos componentes NavBar
y video
componente. Entonces, en nuestra carpeta raíz de frontend nosotros:
mkdir componentscd componentstouch NavBar.vuetouch Video.vue
Finalmente, una carpeta de páginas donde todas nuestras páginas gustan home
y about
se pueden crear. Las dos páginas que necesitamos en esta aplicación son la home
página que muestra todos nuestros videos e información de video y una página de reproductor dinámico que dirige al video en el que hacemos clic.
mkdir pagescd pagestouch index.vuemkdir playercd playertouch _name.vue
Nuestro directorio frontend ahora se ve así:
|-frontend |-components |-NavBar.vue |-Video.vue |-layouts |-default.vue |-pages |-index.vue |-player |-_name.vue |-package.json |-yarn.lock
Nuestro NavBar.vue
se ve así:
template div h1Streaming App/h1 /div/templatestyle scoped.navbar { display: flex; background-color: #161616; justify-content: center; align-items: center;}h1{ color:#a33327;}/style
Tiene NavBar
una h1
etiqueta que muestra Streaming App , con un pequeño estilo.
Importemos el NavBar
a nuestro default.vue
diseño.
// default.vuetemplate div NavBar / nuxt / /div/templatescriptimport NavBar from "@/components/NavBar.vue"export default { components: { NavBar, }}/script
El default.vue
diseño ahora contiene nuestro NavBar
componente y la nuxt /
etiqueta que aparece después indica dónde se mostrará cualquier página que creemos.
En nuestra index.vue
(que es nuestra página de inicio), hagamos una solicitud para https://localhost:5000/videos
obtener todos los videos de nuestro servidor. Pasar los datos como accesorio a nuestro video.vue
componente que crearemos más adelante. Pero por ahora ya lo hemos importado.
templatediv Video :videoList="videos"//div/templatescriptimport Video from "@/components/Video.vue"export default { components: { Video },head: { title: "Home" }, data() { return { videos: [] } }, async fetch() { this.videos = await fetch( 'https://localhost:5000/videos' ).then(res = res.json()) }}/script
Componente de vídeo
A continuación, primero declaramos nuestro accesorio. Dado que los datos de video ahora están disponibles en el componente, usando Vue v-for
iteramos sobre todos los datos recibidos y para cada uno, mostramos la información. Podemos usar la v-for
directiva para recorrer los datos y mostrarlos como una lista. También se han agregado algunos estilos básicos.
templatediv div div v-for="(video, id) in videoList" :key="id" NuxtLink :to="`/player/${video.id}`" div :style="{ backgroundImage: `url(${video.poster})` }" /div div div h2{{video.name}}/h2 p{{video.duration}}/p /div /div /NuxtLink /div /div/div/templatescriptexport default { props:['videoList'],}/scriptstyle scoped.container { display: flex; justify-content: center; align-items: center; margin-top: 2rem;}.vid-con { display: flex; flex-direction: column; flex-shrink: 0; justify-content: center; width: 50%; max-width: 16rem; margin: auto 2em; }.vid { height: 15rem; width: 100%; background-position: center; background-size: cover;}.movie-info { background: black; color: white; width: 100%;}.details { padding: 16px 20px;}/style
También notamos que NuxtLink
tiene una ruta dinámica, es decir, enrutamiento al /player/video.id
.
La funcionalidad que queremos es que cuando un usuario haga clic en cualquiera de los videos, comience a transmitirse. Para conseguirlo aprovechamos el carácter dinámico del _name.vue
recorrido.
En él, creamos un reproductor de video y configuramos la fuente en nuestro punto final para transmitir el video, pero agregamos dinámicamente qué video reproducir a nuestro punto final con la ayuda de this.$route.params.name
ese parámetro que captura el enlace recibido.
template div video controls muted autoPlay source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4" /video /div/templatescriptexport default { data() { return { vidName: '' } },mounted(){ this.vidName = this.$route.params.name}}/scriptstyle scoped.player { display: flex; justify-content: center; align-items: center; margin-top: 2em;}/style
Cuando hacemos clic en cualquiera de los vídeos obtenemos:
Agregar nuestro archivo de subtítulos
Para agregar nuestro archivo de pista, nos aseguramos de que todos los .vtt
archivos en la carpeta de subtítulos tengan el mismo nombre que nuestro id
. Actualice nuestro elemento de video con la pista, solicitando los subtítulos.
template div video controls muted autoPlay crossOrigin="anonymous" source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4" track label="English" kind="captions" srcLang="en" :src="`https://localhost:5000/videos/video/${vidName}/caption`" default /video /div/template
Hemos agregado crossOrigin="anonymous"
al elemento de video; de lo contrario, la solicitud de subtítulos fallará. Ahora actualice y verá que los subtítulos se agregaron correctamente.
Qué tener en cuenta al crear una transmisión de vídeo resiliente.
Al crear aplicaciones de streaming como Twitch, Hulu o Netflix, se tienen en cuenta una serie de cosas:
- Canal de procesamiento de datos de vídeo
Esto puede ser un desafío técnico, ya que se necesitan servidores de alto rendimiento para ofrecer millones de vídeos a los usuarios. Se debe evitar a toda costa una alta latencia o tiempo de inactividad. - Almacenamiento en caché
Se deben utilizar mecanismos de almacenamiento en caché al crear este tipo de aplicación, por ejemplo Cassandra, Amazon S3, AWS SimpleDB. - Geografía de los usuarios
Se debe considerar la geografía de sus usuarios para la distribución.
Conclusión
En este tutorial, hemos visto cómo crear un servidor en Node.js que transmite videos, genera subtítulos para esos videos y proporciona metadatos de los videos. También hemos visto cómo usar Nuxt.js en el frontend para consumir los puntos finales y los datos generados por el servidor.
A diferencia de otros marcos, crear una aplicación con Nuxt.js y Express.js es bastante fácil y rápido. Lo bueno de Nuxt.js es la forma en que administra tus rutas y te permite estructurar mejor tus aplicaciones.
- Puede obtener más información sobre Nuxt.js aquí .
- Puedes obtener el código fuente en Github .
Recursos
- “ Agregar leyendas y subtítulos a videos HTML5 ”, MDN Web Docs
- “ Comprensión de los títulos y subtítulos ”, Screenfont.ca
(ks, vf, yk, il)Explora más en
- vista
- Nuxt.js
- Nodo.js
- Tutoriales
- javascript
Deja un comentario