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 pedaços 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 recebidos e tenta carregar mais.
Este artigo é para desenvolvedores que desejam aprender uma nova tecnologia construindo 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 streaming de vídeos, gerando miniaturas para vídeos e servindo legendas e legendas ocultas para vídeos.
- Nuxt.js é uma estrutura Vue.js que nos ajuda a construir aplicativos Vue.js renderizados por servidor facilmente. Iremos consumir nossa API para os vídeos e este aplicativo terá duas visualizações: uma lista de vídeos disponíveis e uma visualização do 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
Neste aplicativo, construiremos as rotas para fazer solicitações do front-end:
-
videos
rota para obter uma lista de vídeos e seus dados. - uma rota para buscar apenas um vídeo de nossa lista de vídeos.
-
streaming
rota para transmitir os vídeos. -
legendas
é uma rota para adicionar legendas aos vídeos que estamos transmitindo.
Depois que nossas rotas foram criadas, faremos o scaffold de nosso frontend Nuxt
, onde criaremos a página Home
e a página dinâmica do player
. Em seguida, solicitamos nossa rota de videos
para preencher 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 servir os arquivos de legenda para ser usado pelos vídeos.
Para configurar nosso aplicativo, criamos nosso diretório de projeto,
mkdir streaming-app
Configurando nosso servidor
Em nosso diretório streaming-app
, criamos uma pasta chamada backend
.
app streaming de cd
backend mkdir
Em nossa pasta de back-end, inicializamos um arquivo package.json
para armazenar informações sobre nosso projeto de servidor.
backend de cd
npm init-y
precisamos instalar os seguintes pacotes para construir nosso aplicativo.
-
nodemon
reinicia automaticamente nosso servidor quando fazemos alterações. -
express
nos oferece uma interface agradável para lidar com rotas. -
cors
nos permitirá fazer solicitações de origem cruzada, uma vez que nosso cliente e servidor estarão rodando em portas diferentes.
Em nosso diretório de back-end, criamos uma pasta assets
para armazenar nossos vídeos para streaming.
ativos mkdir
Copie um arquivo .mp4
para a pasta de ativos e nomeie-o video1
. Você pode usar .mp4
vídeos de amostra curtos que podem ser encontrados em Repo do Github .
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 gravar 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.
//adicionar depois de'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
.
//adicionar ao final do arquivo app.js app.listen (5000, ()=> { console.log ('Ouvindo na porta 5000!')
});
Um script é adicionado ao arquivo package.json
para iniciar nosso servidor usando o nodemon.
"scripts": { "start":"nodemon app.js" },
Então, em seu terminal, execute:
npm run start
Se você vir a mensagem Listening on port 3000!
no terminal, o servidor está funcionando corretamente. Navegue para http://localhost: 5000/video em seu navegador e você deverá ver o vídeo sendo reproduzido.
Solicitações a serem tratadas pelo front-end
Abaixo estão as solicitações que faremos ao back-end de nosso front-end que precisamos que o servidor manipule.
-
/videos
Retorna uma matriz de dados de maquete de vídeo que será usada para preencher a lista de vídeos na páginaHome
em nosso frontend. -
/video/: id/data
Retorna metadados para um único vídeo. Usado pela páginaPlayer
em nosso frontend. -
/video/: id
Transmite um vídeo com um determinado ID. Usado pela páginaPlayer
.
Vamos criar as rotas.
Retornar dados de maquete para a lista de vídeos
Para este aplicativo de demonstração, criaremos uma matriz de objetos que conterá os metadados e os enviaremos para o front-end quando solicitado. Em um aplicativo real, você provavelmente estaria lendo os dados de um banco de dados, que seria 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 e jerry", poster:' https://image.tmdb.org/t/p/w500/fev8UFNFFYsD5q7AcYS8LyTzqwl.jpg', duração:'3 minutos', nome:'Tom e Jerry' }, { id:"alma", poster:' https://image.tmdb.org/t/p/w500/kf456ZqeC45XTvo6W9pW5clYKfQ.jpg', duração:'4 minutos', nome:'Soul' }, { id:"fora do fio", poster:' https://image.tmdb.org/t/p/w500/lOSdUkGQmbAl5JQ3QoHqBZUbZhC.jpg', duração:'2 minutos', nome:'Fora da rede' },
];
module.exports=allVideos
Podemos ver acima, 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 solicitações a serem feitas pelo frontend são anexadas com /videos
.
Para fazer isso, vamos criar uma pasta 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 nossa lista de vídeos, então vamos exigir o arquivo mockData.js
em nosso Video. arquivo js
e faça nosso pedido.
const express=require ('express')
const router=express.Router ()
const videos=require ('../mockData')
//obter lista de vídeos
router.get ('/', (req, res)=> { res.json (vídeos)
})
module.exports=router;
A rota /videos
agora está declarada, salve o arquivo e ele deve reiniciar o servidor automaticamente. Depois de iniciado, navegue para http://localhost: 3000/videos e nossa matriz é retornada no formato JSON.
Retornar dados para um único vídeo
Queremos fazer uma solicitação para um determinado vídeo em nossa lista de vídeos. Podemos buscar dados de um vídeo em particular em nosso array usando o id
que fornecemos. Vamos fazer uma solicitação, ainda em nosso arquivo Video.js
.
//faça a solicitação de um vídeo específico
router.get ('/: id/data', (req, res)=> { const id=parseInt (req.params.id, 10) res.json (vídeos [id])
})
O código acima obtém o id
dos parâmetros da rota e o converte em um inteiro. Em seguida, enviamos o objeto que corresponde ao id
do array videos
de volta para o cliente.
Transmitindo os vídeos
Em nosso arquivo app.js
, criamos uma rota /video
que serve um vídeo ao cliente. Queremos que este endpoint envie partes menores do vídeo, em vez de servir um arquivo de vídeo inteiro sob solicitação.
Queremos ser capazes de dinamicamente veicular um dos três vídeos que estão na matriz allVideos
e transmitir os vídeos em blocos, então:
Exclua a rota /video
do app.js
.
Precisamos de três vídeos, então copie os vídeos de exemplo do código-fonte do tutorial no diretório assets/
do seu projeto server
. Certifique-se de que os nomes dos arquivos dos vídeos correspondam ao id
na matriz 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 (partes [0], 10); const end=partes [1] ? parseInt (partes [1], 10) : fileSize-1; const chunksize=(fim-início) + 1; arquivo const=fs.createReadStream (videoPath, {início, fim}); const head={ 'Content-Range': bytes $ {start}-$ {end}/$ {fileSize}
, 'Intervalos de aceitação':'bytes', 'Content-Length': chunksize, 'Content-Type':'video/mp4', }; res.writeHead (206, cabeça); arquivo.pipe (res); } senão { const head={ 'Content-Length': fileSize, 'Content-Type':'video/mp4', }; res.writeHead (200, cabeça); fs.createReadStream (videoPath).pipe (res); }
});
Se navegarmos para http://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á um bom código escrito em nossa rota de stream de vídeo, então vamos examiná-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, 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 do usuário enviará um parâmetro range
na solicitação. Isso permite que o servidor saiba qual pedaço 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, 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, cabeça); fs.createReadStream (caminho).pipe (res);
}
Lidaremos com as solicitações subsequentes, incluindo o intervalo em um bloco if
.
if (videoRange) { const parts=videoRange.replace (/bytes=/,"").split ("-"); const start=parseInt (partes [0], 10); const end=partes [1] ? parseInt (partes [1], 10) : fileSize-1; const chunksize=(fim-início) + 1; arquivo const=fs.createReadStream (videoPath, {início, fim}); const head={ 'Content-Range': bytes $ {start}-$ {end}/$ {fileSize}
, 'Intervalos de aceitação':'bytes', 'Content-Length': chunksize, 'Content-Type':'video/mp4', }; res.writeHead (206, cabeça); arquivo.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 todas as partes 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 sinalizará solicitando que a fonte de E/S faça uma pausa 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 (partes [0], 10); const end=partes [1] ? parseInt (partes [1], 10) : fileSize-1; const chunksize=(fim-início) + 1; const file=fs.createReadStream (videoPath, {start, end});
Podemos ver acima que um ReadStream
é criado e veicula o vídeo trecho por trecho.
const head={ 'Content-Range': bytes $ {start}-$ {end}/$ {fileSize}
, 'Intervalos de aceitação':'bytes', 'Content-Length': chunksize, 'Content-Type':'video/mp4', };
res.writeHead (206, cabeça); arquivo.pipe (res);
O cabeçalho da solicitação contém o Content-Range
, que é a mudança inicial e final para obter a próxima parte do vídeo a ser transmitida para o frontend, o content-length
é o pedaço de vídeo enviado. Também especificamos o tipo de conteúdo que estamos transmitindo, que é mp4
. O writehead de 206 está configurado para responder apenas com fluxos recém-criados.
Criando um arquivo de legenda para nossos vídeos
Esta é a aparência de um arquivo de legenda .vtt
.
WEBVTT 00: 00: 00.200-> 00: 00: 01.000
Criar um tutorial pode ser muito 00: 00: 01,500-> 00: 00: 04,300
divertido de fazer.
Arquivos de legendas ocultas 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 legendas para este tutorial, então você pode ir para a pasta de legendas no diretório ativos
em o repositório e baixe as legendas.
Vamos criar uma nova rota que tratará da solicitação de legenda:
router.get ('/video/: id/caption', (req, res)=> res.sendFile ( assets/captions/$ {req.params.id}.vtt
, {root: __dirname}));
Construindo nosso front-end
Para começar na parte visual de nosso sistema, teríamos que construir nosso andaime de front-end.
Observação : você precisa do vue-cli para criar nosso aplicativo. Se você não o instalou em seu computador, pode executar npm install-g @ vue/cli
para instalá-lo.
Instalação
Na raiz do nosso projeto, vamos criar nossa pasta front-end:
Interface
mkdir
frontend de cd
e nele, inicializamos nosso arquivo package.json
, copiamos e colamos o seguinte nele:
{ "nome":"meu-app", "scripts": { "dev":"nuxt", "build":"nuxt build", "gerar":"não gerar", "start":"nuxt start" }
}
em seguida, instale nuxt
:
npm add nuxt
e execute o seguinte comando para executar o aplicativo Nuxt.js:
npm run dev
Nossa estrutura de arquivo Nuxt
Agora que instalamos o Nuxt, podemos começar a planejar nosso front-end.
Primeiro, precisamos criar uma pasta layouts
na raiz do nosso aplicativo. Essa pasta define o layout do aplicativo, independentemente da página em que navegamos. Coisas como nossa barra de navegação e rodapé são encontradas aqui. Na pasta de frontend, criamos default.vue
para nosso layout padrão quando iniciamos nosso aplicativo de frontend.
layouts mkdir
layouts de cd
toque em default.vue
Em seguida, uma pasta componentes
para criar todos os nossos componentes. Precisaremos de apenas dois componentes, NavBar
e o componente video
. Portanto, em nossa pasta raiz do frontend, nós:
componentes mkdir
componentes de cd
toque em NavBar.vue
toque em Video.vue
Finalmente, uma pasta de páginas onde todas as nossas páginas como home
e sobre
podem ser criadas. As duas páginas de que precisamos neste aplicativo são a página home
exibindo todos os nossos vídeos e informações de vídeo e uma página do player dinâmico que direciona para o vídeo em que clicamos.
páginas mkdir
páginas de cd
toque em index.vue
jogador mkdir
tocador de CD
toque em _name.vue
Nosso diretório de front-end agora se parece com isto:
|-frontend |-componentes |-NavBar.vue |-Video.vue |-layouts |-default.vue |-pages |-index.vue |-player |-_name.vue |-package.json |-yarn.lock
Componente Navbar
Nosso NavBar.vue
se parece com isto:
.navbar { display: flex; cor de fundo: # 161616; justificar-conteúdo: centro; alinhar-itens: centro;
}
h1 { cor: # a33327;
}
A NavBar
tem uma tag h1
que exibe Streaming App , com um pouco de estilo.
Vamos importar a NavBar
para nosso layout default.vue
.
//default.vue
O layout default.vue
agora contém nosso componente NavBar
e a tag
depois de indicar onde qualquer página que criarmos irá ser exibido.
Em nosso index.vue
(que é nossa página inicial), vamos fazer uma solicitação para http://localhost: 5000/videos
para obter todos os vídeos de nosso servidor. Passando os dados como um suporte para nosso componente video.vue
que criaremos mais tarde. Mas, por enquanto, já o importamos.
Componente de vídeo
Abaixo, primeiro declaramos nossa prop. Como os dados de vídeo agora estão disponíveis no componente, usando o v-for
do Vue, iteramos 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.
{{video.name}}
{{video.duration}}
.container { display: flex; justificar-conteúdo: centro; alinhar-itens: centro; margem superior: 2rem;
}
.vid-con { display: flex; direção flexível: coluna; encolhimento flexível: 0; justificar-conteúdo: centro; largura: 50%; largura máxima: 16rem; margem: auto 2em; }
.vid { altura: 15rem; largura: 100%; posição de fundo: centro; tamanho do fundo: capa;
}
.movie-info { fundo: preto; cor branca; largura: 100%;
}
.detalhes { preenchimento: 16px 20px;
}
Notamos também que o NuxtLink
possui uma rota dinâmica, que é o roteamento para o /player/video.id
.
A funcionalidade que desejamos é que quando um usuário clica em qualquer um dos vídeos, a transmissão é iniciada. Para conseguir isso, usamos a natureza dinâmica da rota _name.vue
.
Nele, criamos um reprodutor de vídeo e definimos a fonte para nosso endpoint para transmitir o vídeo, mas acrescentamos dinamicamente qual vídeo reproduzir em nosso endpoint com a ajuda de this. $ route.params.name
que captura qual parâmetro o link recebeu.
.player { display: flex; justificar-conteúdo: centro; alinhar-itens: centro; margem superior: 2em;
}
Quando clicamos em qualquer um dos vídeos, obtemos:
Adicionando nosso arquivo de legenda
Para adicionar nosso arquivo de trilha, certificamo-nos de que todos os arquivos .vtt
na pasta legendas tenham o mesmo nome de nosso id
. Atualize nosso elemento de vídeo com a faixa, solicitando as legendas.
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 criar uma transmissão de vídeo resiliente.
Ao criar aplicativos de streaming como Twitch, Hulu ou Netflix, há uma série de coisas que devem ser levadas em consideração:
- Pipeline de processamento de dados de vídeo
Isso pode ser um desafio técnico, pois servidores de alto desempenho são necessários para servir milhões de vídeos aos usuários. Alta latência ou tempo de inatividade devem ser evitados a todo custo. - Cache
Os mecanismos de cache devem ser usados ao construir este tipo de exemplo de aplicativo Cassandra, Amazon S3, AWS SimpleDB. - Geografia dos usuários
Considerando a geografia de seus usuários 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 front-end 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 é muito 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 a vídeos HTML5 ,” MDN Web Docs
- “ Noções básicas sobre legendas e legendas ocultas ,” Screenfont.ca