Como fiz streaming de webcam Javascript (quase) inútil

Neste artigo, quero compartilhar minhas tentativas de transmitir vídeo via websockets sem usar plug-ins de navegador de terceiros, como o Adobe Flash Player. O que veio dessa leitura.

O Adobe Flash - anteriormente Macromedia Flash, é uma plataforma para criar aplicativos executados em um navegador da web. Antes da introdução da API Media Stream, era praticamente a única plataforma para transmitir vídeo e voz de uma webcam, além de criar várias conferências e bate-papos em um navegador. O protocolo para transmissão de informações de mídia RTMP (Real Time Messaging Protocol) ficou realmente fechado por um longo tempo, o que significava: se você deseja aumentar seu serviço de streaming, use o software da própria Adobe - Adobe Media Server (AMS).

Depois de algum tempo em 2012, a Adobe “rendeu e cuspiu” a especificação do protocolo RTMP, que continha erros e, de fato, não estava completa. Naquele momento, os desenvolvedores começaram a fazer suas implementações deste protocolo, então o servidor Wowza apareceu. Em 2011, a Adobe entrou com uma ação contra a Wowza pelo uso ilegal de patentes relacionadas ao RTMP, após quatro anos o conflito foi resolvido pelo mundo.

A plataforma Adobe Flash existe há mais de 20 anos. Durante esse período, muitas vulnerabilidades críticas foram descobertas, elas prometeram parar de dar suporte até 2020, portanto, não existem muitas alternativas para o serviço de streaming.

Para o meu projeto, decidi imediatamente abandonar completamente o uso do Flash no navegador. O principal motivo que eu indiquei acima, o Flash não é suportado em todas as plataformas móveis, e eu realmente não queria implantar o Adobe Flash para desenvolvimento no Windows (emulador de vinho). Então comecei a escrever um cliente em JavaScript. Este será apenas um protótipo, como aprendi mais tarde que o streaming pode ser feito de maneira muito mais eficiente com base no p2p, apenas terei pares - servidores - pares, mas mais nesse outro momento, porque ainda não está pronto.

Para começar, precisamos do próprio servidor websockets. Eu fiz o mais simples baseado no pacote melody go:

Código do servidor
package main import ( "errors" "github.com/go-chi/chi" "gopkg.in/olahol/melody.v1" "log" "net/http" "time" ) func main() { r := chi.NewRouter() m := melody.New() m.Config.MaxMessageSize = 204800 r.Get("/", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "public/index.html") }) r.Get("/ws", func(w http.ResponseWriter, r *http.Request) { m.HandleRequest(w, r) }) //    m.HandleMessageBinary(func(s *melody.Session, msg []byte) { m.BroadcastBinary(msg) }) log.Println("Starting server...") http.ListenAndServe(":3000", r) } 


No cliente (lado da transmissão), você deve primeiro acessar a câmera. Isso é feito através da API MediaStream .

Temos acesso (resolução) à câmera / microfone através da API de dispositivos de mídia . Essa API fornece o método MediaDevices.getUserMedia () , que mostra um pop-up. uma janela solicitando ao usuário permissão para acessar a câmera e / ou microfone. Gostaria de observar que conduzi todos os experimentos no Google Chrome, mas acho que no Firefox tudo funcionará aproximadamente da mesma maneira.

Em seguida, getUserMedia () retorna uma promessa, na qual um objeto MediaStream é passado - um fluxo de dados de vídeo e áudio. Atribuímos esse objeto à propriedade do elemento de vídeo no src. Código:

Lado da transmissão
 <style> #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; } </style> </head> <body> <!--    ""     --> <video autoplay id="videoObjectHtml5ApiServer"></video> <script type="application/javascript"> var video = document.getElementById('videoObjectHtml5ApiServer'); //   MediaDevices API,      (    ) // getUserMedia  ,           video    if (navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) { //     video ,        video.srcObject = stream; }); } </script> 


Para transmitir um fluxo de vídeo por soquetes, é necessário codificá-lo de alguma forma, armazenar em buffer e transmiti-lo em partes. O fluxo de vídeo bruto não pode ser transmitido através de websockets. Aí vem a API MediaRecorder para nos ajudar. Essa API permite codificar e dividir um fluxo em pedaços. Eu codifico para compactar o fluxo de vídeo, para que eu possa dirigir menos bytes pela rede. Tendo quebrado em pedaços, é possível enviar cada pedaço para o websocket. Código:

Codificamos o fluxo de vídeo, dividimos em pedaços
 <style> #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; } </style> </head> <body> <!--    ""     --> <video autoplay id="videoObjectHtml5ApiServer"></video> <script type="application/javascript"> var video = document.getElementById('videoObjectHtml5ApiServer'); //   MediaDevices API,      (    ) // getUserMedia  ,           video    if (navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) { //     video ,        video.srcObject = s; var recorderOptions = { mimeType: 'video/webm; codecs=vp8' //      webm  vp8 }, mediaRecorder = new MediaRecorder(s, recorderOptions ); //  MediaRecorder mediaRecorder.ondataavailable = function(e) { if (e.data && e.data.size > 0) { //     e.data } } mediaRecorder.start(100); //      100   }); } </script> 


Agora adicione a transferência nos websockets. Surpreendentemente, isso requer apenas um objeto WebSocket . Possui apenas dois métodos de envio e fechamento. Os nomes falam por si. Código estendido:

Nós transmitimos o fluxo de vídeo para o servidor
 <style> #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; } </style> </head> <body> <!--    ""     --> <video autoplay id="videoObjectHtml5ApiServer"></video> <script type="application/javascript"> var video = document.getElementById('videoObjectHtml5ApiServer'); //   MediaDevices API,      (    ) // getUserMedia  ,           video    if (navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) { //     video ,        video.srcObject = s; var recorderOptions = { mimeType: 'video/webm; codecs=vp8' //      webm  vp8 }, mediaRecorder = new MediaRecorder(s, recorderOptions ), //  MediaRecorder socket = new WebSocket('ws://127.0.0.1:3000/ws'); mediaRecorder.ondataavailable = function(e) { if (e.data && e.data.size > 0) { //     e.data socket.send(e.data); } } mediaRecorder.start(100); //      100   }).catch(function (err) { console.log(err); }); } </script> 


O lado da transmissão está pronto! Agora vamos tentar pegar um fluxo de vídeo e mostrá-lo no cliente. Do que precisamos para isso? Antes de tudo, é claro, uma conexão de soquete. Desligamos um "ouvinte" no objeto WebSocket, assinamos o evento 'message'. Depois de receber um dado binário, nosso servidor o transmite aos assinantes, ou seja, clientes. Ao mesmo tempo, a função de retorno de chamada conectada ao “ouvinte” do evento 'message' é acionada no cliente, o próprio objeto é passado para o argumento da função - uma parte do fluxo de vídeo codificado pelo vp8.

Aceitamos um fluxo de vídeo
 <style> #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; } </style> </head> <body> <!--    ""     --> <video autoplay id="videoObjectHtml5ApiServer"></video> <script type="application/javascript"> var video = document.getElementById('videoObjectHtml5ApiServer'), socket = new WebSocket('ws://127.0.0.1:3000/ws'), arrayOfBlobs = []; socket.addEventListener('message', function (event) { // ""     arrayOfBlobs.push(event.data); //     readChunk(); }); </script> 


Durante muito tempo, tentei entender por que é impossível enviar imediatamente as peças recebidas para o elemento de vídeo para reprodução, mas acabou sendo impossível, é claro, você deve primeiro colocar a peça em um buffer especial anexado ao elemento de vídeo e só então começará a reproduzir o fluxo de vídeo. Para fazer isso, você precisa da API MediaSource e da API FileReader .

O MediaSource atua como um tipo de intermediário entre o objeto de reprodução de mídia e a fonte desse fluxo de mídia. O objeto MediaSource contém um buffer conectável para a fonte de fluxo de vídeo / áudio. Uma característica é que o buffer pode conter apenas dados Uint8, portanto, o FileReader é necessário para criar esse buffer. Dê uma olhada no código e ele ficará mais claro:

Reproduzir o fluxo de vídeo
 <style> #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; } </style> </head> <body> <!--    ""     --> <video autoplay id="videoObjectHtml5ApiServer"></video> <script type="application/javascript"> var video = document.getElementById('videoObjectHtml5ApiServer'), socket = new WebSocket('ws://127.0.0.1:3000/ws'), mediaSource = new MediaSource(), //  MediaSource vid2url = URL.createObjectURL(mediaSource), //   URL      arrayOfBlobs = [], sourceBuffer = null; // ,  - socket.addEventListener('message', function (event) { // ""     arrayOfBlobs.push(event.data); //     readChunk(); }); //   MediaSource   ,      // /  //   ,   ,        //     ,       mediaSource.addEventListener('sourceopen', function() { var mediaSource = this; sourceBuffer = mediaSource.addSourceBuffer("video/webm; codecs=\"vp8\""); }); function readChunk() { var reader = new FileReader(); reader.onload = function(e) { //   FileReader  ,      //  ""   Uint8Array ( Blob)   ,  //  ,       / sourceBuffer.appendBuffer(new Uint8Array(e.target.result)); reader.onload = null; } reader.readAsArrayBuffer(arrayOfBlobs.shift()); } </script> 


O protótipo do serviço de streaming está pronto. A principal desvantagem é que a reprodução do vídeo estará 100 ms atrás do lado da transmissão. Definimos isso ao dividir o fluxo de vídeo antes de enviá-lo ao servidor. Além disso, quando chequei no meu laptop, gradualmente acumulei um atraso entre os lados de transmissão e recebimento, isso era claramente visível. Comecei a procurar maneiras de superar essa falha e ... deparei-me com a API RTCPeerConnection , que permite transferir um fluxo de vídeo sem truques como dividir um fluxo em pedaços. O atraso acumulado, eu acho, é devido ao fato de que no navegador, antes da transferência, cada peça é transcodificada para o formato webm. Não continuei pesquisando, mas comecei a estudar o WebRTC. Penso nos resultados de minha pesquisa. Escreverei um artigo separado se achar esta comunidade interessante.

Source: https://habr.com/ru/post/pt461769/


All Articles