Streaming de webcam (quase) inútil a partir de um navegador. Fluxo de mídia e Websockets

Neste artigo, quero compartilhar minhas tentativas de transmitir vídeo via websockets sem usar plug-ins de navegador de terceiros, como Adobe Flash Player. Continue lendo para descobrir o que aconteceu.

Adobe Flash, antigo Macromedia Flash, é uma plataforma para criação de aplicativos que rodam em um navegador da web. Antes da introdução da API Media Stream, era praticamente a única plataforma para streaming de vídeo e voz de uma webcam, bem como para criação de diversos tipos de conferências e chats no navegador. O protocolo de transmissão de informações de mídia RTMP (Real Time Messaging Protocol) já estava fechado há muito tempo, o que significava: se você quiser turbinar seu serviço de streaming, tenha a gentileza de usar um software da própria Adobe - Adobe Media Server (AMS).

Depois de algum tempo em 2012, a Adobe “desistiu e cuspiu” para o público. especificação Protocolo RTMP, que continha erros e estava essencialmente incompleto. Naquela época, os desenvolvedores começaram a fazer suas próprias implementações desse protocolo e o servidor Wowza apareceu. Em 2011, a Adobe entrou com uma ação judicial contra Wowza por uso ilegal de patentes relacionadas ao RTMP; após 4 anos, o conflito foi resolvido amigavelmente.

A plataforma Adobe Flash tem mais de 20 anos, período durante o qual muitas vulnerabilidades críticas foram descobertas, o suporte prometido terminará em 2020, deixando poucas alternativas para o serviço de streaming.

Para o meu projeto, decidi imediatamente abandonar completamente o uso do Flash no navegador. Indiquei o motivo principal acima; Flash também não é compatível com plataformas móveis e eu realmente não queria implantar o Adobe Flash para desenvolvimento no Windows (emulador de vinho). Então decidi escrever um cliente em JavaScript. Este será apenas um protótipo, pois mais tarde aprendi que o streaming pode ser feito com muito mais eficiência baseado em p2p, só que para mim será peer - server - peers, mas falaremos mais sobre isso em outra hora, porque ainda não está pronto.

Para começar, precisamos do servidor websockets real. 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 do streaming), primeiro você precisa acessar a câmera. Isto é feito através API MediaStream.

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

Em seguida, getUserMedia() retorna uma Promise, para a qual passa um objeto MediaStream - um fluxo de dados de vídeo-áudio. Atribuímos este objeto à propriedade src do elemento video. 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 através de soquetes, você precisa codificá-lo em algum lugar, armazená-lo em buffer e transmiti-lo em partes. O stream de vídeo bruto não pode ser transmitido via websockets. É aqui que vem em nosso auxílio API MediaRecorder. Esta API permite codificar e dividir o fluxo em pedaços. Faço codificação para compactar o stream de vídeo para enviar menos bytes pela rede. Depois de dividi-lo em pedaços, você pode enviar cada pedaço para um websocket. Código:

Codificamos o stream de vídeo e o 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 vamos adicionar a transmissão via websockets. Surpreendentemente, tudo que você precisa para isso é um objeto WebSocket. Possui apenas dois métodos enviar e fechar. Os nomes falam por si. Código adicionado:

Transmitimos o stream 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 receber um stream de vídeo e exibi-lo no cliente. O que precisamos para isso? Em primeiro lugar, é claro, a conexão do soquete. Anexamos um “ouvinte” ao objeto WebSocket e assinamos o evento ‘message’. Tendo recebido um dado binário, nosso servidor o transmite para assinantes, ou seja, clientes. Neste caso, a função de retorno de chamada associada ao “ouvinte” do evento 'mensagem' é acionada no cliente; o próprio objeto é passado para o argumento da função - um pedaço do fluxo de vídeo codificado por vp8.

Aceitamos transmissão 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>

Por muito tempo tentei entender por que é impossível enviar imediatamente as peças recebidas para o elemento de vídeo para reprodução, mas descobri que isso não pode ser feito, claro, você deve primeiro colocar a peça em um buffer especial vinculado a o elemento de vídeo e só então começará a reproduzir o stream de vídeo. Para isso você vai precisar API MediaSource и API FileReader.

MediaSource atua como uma espécie de intermediário entre o objeto de reprodução de mídia e a origem desse fluxo de mídia. O objeto MediaSource contém um buffer conectável para a origem do fluxo de vídeo/áudio. Um recurso é que o buffer só pode conter dados Uint8, portanto, você precisará de um FileReader para criar esse buffer. Observe o código e ficará mais claro:

Reproduzindo 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 ficará 100 ms atrás do lado de transmissão; nós mesmos definimos isso ao dividir o fluxo de vídeo antes de transmiti-lo ao servidor. Além disso, quando verifiquei no meu laptop, o atraso entre os lados de transmissão e recepção acumulou-se gradualmente, isso era claramente visível. Comecei a procurar maneiras de superar essa desvantagem e... me deparei API RTCPeerConnection, que permite transmitir um stream de vídeo sem truques como dividir o stream em pedaços. O atraso acumulado, creio eu, se deve ao fato de o navegador recodificar cada peça no formato webm antes da transmissão. Não fui mais longe, mas comecei a estudar WebRTC. Acho que escreverei um artigo separado sobre os resultados da minha pesquisa se achar interessante para a comunidade.

Fonte: habr.com

Adicionar um comentário