Criando um aplicativo de streaming de vídeo com Nuxt.js, Node e Express

Publicados: 2022-03-10
Resumo rápido ↬ Neste artigo, construiremos um aplicativo de streaming de vídeo usando Nuxt.js e Node.js. Especificamente, criaremos um aplicativo Node.js do lado do servidor que processará a busca e o streaming de vídeos, a geração de miniaturas para seus vídeos e a veiculação de legendas e legendas.

Os vídeos funcionam com streams. Isso significa que, em vez de enviar o vídeo inteiro de uma vez, um vídeo é enviado como um conjunto de partes menores que compõem o vídeo completo. Isso explica por que os vídeos são armazenados em buffer ao assistir a um vídeo em banda larga lenta, porque ele reproduz apenas os pedaços que recebeu e tenta carregar mais.

Este artigo é para desenvolvedores que desejam aprender uma nova tecnologia criando um projeto real: um aplicativo de streaming de vídeo com Node.js como back-end e Nuxt.js como cliente.

  • Node.js é um tempo de execução usado para criar aplicativos rápidos e escaláveis. Vamos usá-lo para lidar com a busca e transmissão de vídeos, gerar miniaturas para vídeos e veicular legendas e legendas para vídeos.
  • Nuxt.js é um framework Vue.js que nos ajuda a construir facilmente aplicativos Vue.js renderizados pelo servidor. Consumiremos nossa API para os vídeos e este aplicativo terá duas visualizações: uma lista de vídeos disponíveis e uma visualização de player para cada vídeo.

Pré-requisitos

  • Uma compreensão de HTML, CSS, JavaScript, Node/Express e Vue.
  • Um editor de texto (por exemplo, VS Code).
  • Um navegador da web (por exemplo, Chrome, Firefox).
  • FFmpeg instalado em sua estação de trabalho.
  • Node.js. nvm.
  • Você pode obter o código-fonte no GitHub.

Configurando nosso aplicativo

Nesta aplicação, vamos construir as rotas para fazer requisições do frontend:

  • videos para obter uma lista de vídeos e seus dados.
  • uma rota para buscar apenas um vídeo da nossa lista de vídeos.
  • rota de streaming para transmitir os vídeos.
  • captions rota para adicionar legendas aos vídeos que estamos transmitindo.

Depois que nossas rotas forem criadas, montaremos nosso frontend Nuxt , onde criaremos a página Home e a página do player dinâmico. Em seguida, solicitamos que nossa rota de videos preencha a página inicial com os dados do vídeo, outra solicitação para transmitir os vídeos em nossa página do player e, finalmente, uma solicitação para veicular os arquivos de legenda a serem usados ​​pelos vídeos.

Para configurar nossa aplicação, criamos nosso diretório de projeto,

 mkdir streaming-app
Mais depois do salto! Continue lendo abaixo ↓

Configurando nosso servidor

Em nosso diretório streaming-app , criamos uma pasta chamada backend .

 cd streaming-app mkdir backend

Em nossa pasta backend, inicializamos um arquivo package.json para armazenar informações sobre nosso projeto de servidor.

 cd backend npm init -y

precisamos instalar os seguintes pacotes para construir nosso aplicativo.

  • nodemon reinicia automaticamente nosso servidor quando fazemos alterações.
  • express nos dá uma boa interface para lidar com rotas.
  • cors nos permitirá fazer solicitações de origem cruzada, pois nosso cliente e servidor serão executados em portas diferentes.

Em nosso diretório de back-end, criamos uma pasta assets para armazenar nossos vídeos para streaming.

 mkdir assets

Copie um arquivo .mp4 para a pasta de recursos e nomeie-o como video1 . Você pode usar vídeos de amostra curtos .mp4 que podem ser encontrados no Github Repo.

Crie um arquivo app.js e adicione os pacotes necessários para nosso aplicativo.

 const express = require('express'); const fs = require('fs'); const cors = require('cors'); const path = require('path'); const app = express(); app.use(cors())

O módulo fs é usado para ler e escrever em arquivos facilmente em nosso servidor, enquanto o módulo path fornece uma maneira de trabalhar com diretórios e caminhos de arquivo.

Agora criamos uma rota ./video . Quando solicitado, ele enviará um arquivo de vídeo de volta ao cliente.

 // add after 'const app = express();' app.get('/video', (req, res) => { res.sendFile('assets/video1.mp4', { root: __dirname }); });

Esta rota serve o arquivo de vídeo video1.mp4 quando solicitado. Em seguida, ouvimos nosso servidor na porta 3000 .

 // add to end of app.js file app.listen(5000, () => { console.log('Listening on port 5000!') });

Um script é adicionado no arquivo package.json para iniciar nosso servidor usando nodemon.

 "scripts": { "start": "nodemon app.js" },

Em seguida, no seu terminal, execute:

 npm run start

Se você vir a mensagem Listening on port 3000! no terminal, então o servidor está funcionando corretamente. Navegue até https://localhost:5000/video no seu navegador e você deverá ver o vídeo sendo reproduzido.

Solicitações a serem tratadas pelo frontend

Abaixo estão as solicitações que faremos ao backend de nosso frontend que precisamos que o servidor trate.

  • /videos
    Retorna uma matriz de dados de maquete de vídeo que serão usados ​​para preencher a lista de vídeos na página Home em nosso frontend.
  • /video/:id/data
    Retorna metadados para um único vídeo. Usado pela página Player em nosso frontend.
  • /video/:id
    Transmite um vídeo com um determinado ID. Usado pela página Player .

Vamos criar as rotas.

Retornar dados de maquete para lista de vídeos

Para este aplicativo de demonstração, criaremos uma matriz de objetos que conterá os metadados e os enviaremos ao frontend quando solicitado. Em uma aplicação real, você provavelmente estaria lendo os dados de um banco de dados, que seria então usado para gerar um array como este. Para simplificar, não faremos isso neste tutorial.

Em nossa pasta de back-end, crie um arquivo mockdata.js e preencha-o com metadados para nossa lista de vídeos.

 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

Podemos ver de cima, cada objeto contém informações sobre o vídeo. Observe o atributo poster que contém o link para uma imagem de pôster do vídeo.

Vamos criar uma rota de videos já que todas as nossas requisições a serem feitas pelo frontend são prefixadas com /videos .

Para fazer isso, vamos criar uma pasta de routes e adicionar um arquivo Video.js para nossa rota /videos . Neste arquivo, exigiremos express e usaremos o roteador expresso para criar nossa rota.

 const express = require('express') const router = express.Router()

Quando vamos para a rota /videos , queremos obter nossa lista de vídeos, então vamos exigir o arquivo mockData.js em nosso arquivo Video.js e fazer nossa solicitação.

 const express = require('express') const router = express.Router() const videos = require('../mockData') // get list of videos router.get('/', (req,res)=>{ res.json(videos) }) module.exports = router;

A rota /videos agora está declarada, salve o arquivo e ele deverá reiniciar automaticamente o servidor. Uma vez iniciado, navegue até https://localhost:3000/videos e nosso array será retornado no formato JSON.

Dados de retorno para um único vídeo

Queremos poder fazer uma solicitação para um vídeo específico em nossa lista de vídeos. Podemos buscar dados de vídeo específicos em nosso array usando o id que demos a ele. Vamos fazer uma solicitação, ainda em nosso arquivo Video.js .

 // make request for a particular video router.get('/:id/data', (req,res)=> { const id = parseInt(req.params.id, 10) res.json(videos[id]) })

O código acima obtém o id dos parâmetros de rota e o converte em um inteiro. Em seguida, enviamos o objeto que corresponde ao id do array de videos de volta ao cliente.

Transmitindo os vídeos

Em nosso arquivo app.js , criamos uma rota /video que veicula um vídeo para o cliente. Queremos que esse endpoint envie partes menores do vídeo, em vez de veicular um arquivo de vídeo inteiro mediante solicitação.

Queremos poder veicular dinamicamente um dos três vídeos que estão na matriz allVideos e transmitir os vídeos em partes, então:

Exclua a rota /video de app.js .

Precisamos de três vídeos, então copie os vídeos de exemplo do código-fonte do tutorial para o diretório assets/ do seu projeto de server . Verifique se os nomes dos arquivos dos vídeos correspondem ao id na matriz de videos :

De volta ao nosso arquivo Video.js , crie a rota para streaming de vídeos.

 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); } });

Se navegarmos para https://localhost:5000/videos/video/outside-the-wire em nosso navegador, podemos ver o streaming de vídeo.

Como funciona a rota de streaming de vídeo

Há uma boa parte do código escrito em nossa rota de stream de vídeo, então vamos analisá-lo linha por linha.

 const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range;

Primeiro, a partir de nossa solicitação, obtemos o id da rota usando req.params.id e o usamos para gerar o videoPath para o vídeo. Em seguida, lemos o fileSize usando o sistema de arquivos fs que importamos. Para vídeos, o navegador de um usuário enviará um parâmetro de range na solicitação. Isso permite que o servidor saiba qual parte do vídeo enviar de volta ao cliente.

Alguns navegadores enviam um intervalo na solicitação inicial, mas outros não. Para aqueles que não enviam, ou se por qualquer outro motivo o navegador não enviar um intervalo, tratamos disso no bloco else . Este código obtém o tamanho do arquivo e envia os primeiros pedaços do vídeo:

 else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(path).pipe(res); }

Vamos lidar com solicitações subsequentes, incluindo o intervalo em um bloco if .

 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 acima cria um fluxo de leitura usando os valores start e end do intervalo. Defina o Content-Length dos cabeçalhos de resposta para o tamanho do bloco que é calculado a partir dos valores start e end . Também usamos o código HTTP 206, significando que a resposta contém conteúdo parcial. Isso significa que o navegador continuará fazendo solicitações até obter todos os trechos do vídeo.

O que acontece em conexões instáveis

Se o usuário estiver em uma conexão lenta, o fluxo de rede o sinalizará solicitando que a fonte de E/S pause até que o cliente esteja pronto para mais dados. Isso é conhecido como contrapressão . Podemos levar este exemplo um passo adiante e ver como é fácil estender o fluxo. Também podemos adicionar compactação facilmente!

 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 acima que um ReadStream é criado e serve o vídeo pedaço por pedaço.

 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);

O cabeçalho da solicitação contém o Content-Range , que é o início e o fim da alteração para obter o próximo trecho de vídeo a ser transmitido para o frontend, o content-length é o trecho de vídeo enviado. Também especificamos o tipo de conteúdo que estamos transmitindo, que é mp4 . O cabeçote de gravação de 206 está configurado para responder apenas com fluxos recém-criados.

Criando um arquivo de legenda para nossos vídeos

É assim que um arquivo de legenda .vtt se parece.

 WEBVTT 00:00:00.200 --> 00:00:01.000 Creating a tutorial can be very 00:00:01.500 --> 00:00:04.300 fun to do.

Os arquivos de legendas contêm texto para o que é dito em um vídeo. Ele também contém códigos de tempo para quando cada linha de texto deve ser exibida. Queremos que nossos vídeos tenham legendas e não criaremos nosso próprio arquivo de legenda para este tutorial, então você pode ir para a pasta de legendas no diretório de assets no repositório e baixar as legendas.

Vamos criar uma nova rota que irá lidar com a solicitação de legenda:

 router.get('/video/:id/caption', (req, res) => res.sendFile(`assets/captions/${req.params.id}.vtt`, { root: __dirname }));

Construindo nosso frontend

Para começar na parte visual do nosso sistema, teríamos que construir nosso scaffold de front-end.

Nota : Você precisa do vue-cli para criar nosso aplicativo. Se você não o tiver instalado em seu computador, poderá executar npm install -g @vue/cli para instalá-lo.

Instalação

Na raiz do nosso projeto, vamos criar nossa pasta front-end:

 mkdir frontend cd frontend

e nele, inicializamos nosso arquivo package.json , copiamos e colamos o seguinte nele:

 { "name": "my-app", "scripts": { "dev": "nuxt", "build": "nuxt build", "generate": "nuxt generate", "start": "nuxt start" } }

então instale o nuxt :

 npm add nuxt

e execute o seguinte comando para executar o aplicativo Nuxt.js:

 npm run dev

Nossa estrutura de arquivos Nuxt

Agora que temos o Nuxt instalado, podemos começar a fazer o layout do nosso frontend.

Primeiro, precisamos criar uma pasta de layouts na raiz do nosso aplicativo. Essa pasta define o layout do aplicativo, independentemente da página para a qual navegamos. Coisas como nossa barra de navegação e rodapé são encontradas aqui. Na pasta frontend, criamos default.vue para nosso layout padrão quando iniciamos nosso aplicativo frontend.

 mkdir layouts cd layouts touch default.vue

Em seguida, uma pasta de components para criar todos os nossos componentes. Vamos precisar de apenas dois componentes, NavBar e componente de video . Então, em nossa pasta raiz do frontend, nós:

 mkdir components cd components touch NavBar.vue touch Video.vue

Finalmente, uma pasta de páginas onde todas as nossas páginas como home e about podem ser criadas. As duas páginas que precisamos neste aplicativo são a página home que exibe todos os nossos vídeos e informações de vídeo e uma página de player dinâmica que direciona para o vídeo em que clicamos.

 mkdir pages cd pages touch index.vue mkdir player cd player touch _name.vue

Nosso diretório frontend agora se parece com isso:

 |-frontend |-components |-NavBar.vue |-Video.vue |-layouts |-default.vue |-pages |-index.vue |-player |-_name.vue |-package.json |-yarn.lock

Componente da barra de navegação

Nosso NavBar.vue fica assim:

 <template> <div class="navbar"> <h1>Streaming App</h1> </div> </template> <style scoped> .navbar { display: flex; background-color: #161616; justify-content: center; align-items: center; } h1{ color:#a33327; } </style>

A NavBar tem uma tag h1 que exibe o Streaming App , com um pouco de estilo.

Vamos importar a NavBar para nosso layout default.vue .

 // default.vue <template> <div> <NavBar /> <nuxt /> </div> </template> <script> import NavBar from "@/components/NavBar.vue" export default { components: { NavBar, } } </script>

O layout default.vue agora contém nosso componente NavBar e a <nuxt /> depois que indica onde qualquer página que criarmos será exibida.

Em nosso index.vue (que é nossa página inicial), vamos fazer uma solicitação para https://localhost:5000/videos para obter todos os vídeos do nosso servidor. Passando os dados como prop para o nosso componente video.vue que vamos criar mais tarde. Mas, por enquanto, já importamos.

 <template> <div> <Video :videoList="videos"/> </div> </template> <script> import 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

Abaixo, primeiro declaramos nosso prop. Como os dados de vídeo agora estão disponíveis no componente, usando o v-for do Vue, iteramos em todos os dados recebidos e, para cada um, exibimos as informações. Podemos usar a diretiva v-for para percorrer os dados e exibi-los como uma lista. Alguns estilos básicos também foram adicionados.

 <template> <div> <div class="container"> <div v-for="(video, id) in videoList" :key="id" class="vid-con" > <NuxtLink :to="`/player/${video.id}`"> <div : class="vid" ></div> <div class="movie-info"> <div class="details"> <h2>{{video.name}}</h2> <p>{{video.duration}}</p> </div> </div> </NuxtLink> </div> </div> </div> </template> <script> export default { props:['videoList'], } </script> <style 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>

Também notamos que o NuxtLink possui uma rota dinâmica, que é o roteamento para o /player/video.id .

A funcionalidade que queremos é quando um usuário clica em qualquer um dos vídeos, ele começa a transmitir. Para isso, utilizamos a natureza dinâmica da rota _name.vue .

Nele, criamos um player de vídeo e definimos a origem para nosso endpoint para transmitir o vídeo, mas anexamos dinamicamente qual vídeo reproduzir em nosso endpoint com a ajuda de this.$route.params.name que captura qual parâmetro o link recebeu .

 <template> <div class="player"> <video controls muted autoPlay> <source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4"> </video> </div> </template> <script> export default { data() { return { vidName: '' } }, mounted(){ this.vidName = this.$route.params.name } } </script> <style scoped> .player { display: flex; justify-content: center; align-items: center; margin-top: 2em; } </style>

Quando clicamos em qualquer um dos vídeos, obtemos:

Resultado final do aplicativo de streaming de vídeo Nuxt
O streaming de vídeo é iniciado quando o usuário clica na miniatura. (Visualização grande)

Adicionando nosso arquivo de legenda

Para adicionar nosso arquivo de trilha, nos certificamos de que todos os arquivos .vtt na pasta de legendas tenham o mesmo nome do nosso id . Atualize nosso elemento de vídeo com a faixa, solicitando as legendas.

 <template> <div class="player"> <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>

Adicionamos crossOrigin="anonymous" ao elemento de vídeo; caso contrário, a solicitação de legendas falhará. Agora atualize e você verá que as legendas foram adicionadas com sucesso.

O que ter em mente ao construir um streaming de vídeo resiliente.

Ao criar aplicativos de streaming como Twitch, Hulu ou Netflix, há várias coisas que são levadas em consideração:

  • Pipeline de processamento de dados de vídeo
    Isso pode ser um desafio técnico, pois são necessários servidores de alto desempenho para fornecer milhões de vídeos aos usuários. Alta latência ou tempo de inatividade devem ser evitados a todo custo.
  • Cache
    Mecanismos de cache devem ser usados ​​ao construir este tipo de aplicação exemplo Cassandra, Amazon S3, AWS SimpleDB.
  • Geografia dos usuários
    Considerar a geografia de seus usuários deve ser pensado para distribuição.

Conclusão

Neste tutorial, vimos como criar um servidor em Node.js que transmite vídeos, gera legendas para esses vídeos e fornece metadados dos vídeos. Também vimos como usar o Nuxt.js no frontend para consumir os endpoints e os dados gerados pelo servidor.

Ao contrário de outros frameworks, construir um aplicativo com Nuxt.js e Express.js é bastante fácil e rápido. A parte legal do Nuxt.js é a maneira como ele gerencia suas rotas e faz com que você estruture melhor seus aplicativos.

  • Você pode obter mais informações sobre Nuxt.js aqui.
  • Você pode obter o código-fonte no Github.

Recursos

  • “Adicionando legendas e legendas ao vídeo HTML5”, MDN Web Docs
  • “Compreendendo legendas e legendas,” Screenfont.ca