(Почти) безполезен стрийминг от уеб камера от браузър. Медиен поток и уебсокети

В тази статия искам да споделя моите опити за поточно предаване на видео през websockets, без да използвам приставки за браузъри на трети страни, като Adobe Flash Player. Прочетете, за да разберете какво се получи от това.

Adobe Flash, преди Macromedia Flash, е платформа за създаване на приложения, които се изпълняват в уеб браузър. Преди въвеждането на Media Stream API, това беше практически единствената платформа за поточно видео и глас от уеб камера, както и за създаване на различни видове конференции и чатове в браузъра. Протоколът за предаване на медийна информация RTMP (Real Time Messaging Protocol) всъщност беше затворен за дълго време, което означаваше: ако искате да подобрите услугата си за стрийминг, бъдете достатъчно добри да използвате софтуер от самите Adobe - Adobe Media Server (AMS).

След известно време през 2012 г. Adobe „се отказа и го изплю“ пред обществеността. спецификация RTMP протокол, който съдържаше грешки и беше по същество непълен. По това време разработчиците започнаха да правят свои собствени реализации на този протокол и се появи сървърът Wowza. През 2011 г. Adobe заведе дело срещу Wowza за незаконно използване на патенти, свързани с RTMP; след 4 години конфликтът беше решен по приятелски начин.

Платформата Adobe Flash е на повече от 20 години, през което време са открити много критични уязвимости, поддръжка обещано да приключи до 2020 г., оставяйки няколко алтернативи за стрийминг услугата.

За моя проект веднага реших напълно да се откажа от използването на Flash в браузъра. Посочих основната причина по-горе; Flash също не се поддържа изобщо на мобилни платформи и наистина не исках да внедрявам Adobe Flash за разработка на Windows (емулатор на вино). Така че реших да напиша клиент на JavaScript. Това ще бъде само прототип, тъй като по-късно научих, че стриймингът може да се направи много по-ефективно на базата на p2p, само че за мен ще бъде peer - server - peers, но повече за това друг път, защото все още не е готово.

За да започнем, се нуждаем от действителния уебсокетс сървър. Направих най-простия въз основа на пакета melody go:

Код на сървъра

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

От страна на клиента (от страната на стрийминг) първо трябва да получите достъп до камерата. Това става чрез API на MediaStream.

Получаваме достъп (разрешение) до камерата/микрофона чрез API за медийни устройства. Този API предоставя метод MediaDevices.getUserMedia(), което показва изскачащ прозорец. прозорец, питащ потребителя за разрешение за достъп до камерата и/или микрофона. Бих искал да отбележа, че проведох всички експерименти в Google Chrome, но мисля, че всичко ще работи по същия начин във Firefox.

След това getUserMedia() връща Promise, към който предава обект MediaStream - поток от видео-аудио данни. Присвояваме този обект на свойството src на видео елемента. Код:

Страна на излъчване

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

За да излъчвате видео поток през сокети, трябва да го кодирате някъде, да го буферирате и да го предавате на части. Необработеният видеопоток не може да се предава през websockets. Тук ни идва на помощ API на MediaRecorder. Този API ви позволява да кодирате и разделяте потока на части. Правя кодиране, за да компресирам видео потока, за да изпратя по-малко байтове по мрежата. След като го разбиете на парчета, можете да изпратите всяко парче към уеб гнездо. Код:

Ние кодираме видео потока, разделяме го на части

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

Сега нека добавим предаване през websockets. Изненадващо, всичко, от което се нуждаете за това, е предмет WebSocket. Има само два метода за изпращане и затваряне. Имената говорят сами за себе си. Добавен код:

Ние предаваме видео потока към сървъра

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

Страната на излъчване е готова! Сега нека се опитаме да получим видео поток и да го покажем на клиента. Какво ни трябва за това? Първо, разбира се, връзката на гнездото. Прикрепяме „слушател“ към обекта WebSocket и се абонираме за събитието „съобщение“. След като получи част от двоични данни, нашият сървър го излъчва на абонати, тоест клиенти. В този случай функцията за обратно извикване, свързана с „слушателя“ на събитието „съобщение“, се задейства на клиента, самият обект се предава в аргумента на функцията – част от видеопотока, кодиран от vp8.

Приемаме видео поток

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

Дълго време се опитвах да разбера защо е невъзможно незабавно да изпратя получените парчета към видео елемента за възпроизвеждане, но се оказа, че това не може да стане, разбира се, първо трябва да поставите парчето в специален буфер, свързан с видео елемента и едва тогава ще започне да възпроизвежда видеопотока. За това ще ви трябва API на MediaSource и API на FileReader.

MediaSource действа като вид посредник между обекта за възпроизвеждане на мултимедия и източника на този медиен поток. Обектът MediaSource съдържа сменяем буфер за източника на видео/аудио потока. Една особеност е, че буферът може да съдържа само Uint8 данни, така че ще ви трябва FileReader, за да създадете такъв буфер. Вижте кода и ще стане по-ясно:

Възпроизвеждане на видео потока

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

Прототипът на стрийминг услугата е готов. Основният недостатък е, че възпроизвеждането на видео ще изостане от страната на предаване със 100 ms; ние задаваме това сами, когато разделяме видеопотока, преди да го предадем към сървъра. Освен това, когато проверих на моя лаптоп, забавянето между предавателната и приемащата страна постепенно се натрупваше, това беше ясно видимо. Започнах да търся начини да преодолея този недостатък и... попаднах API за RTCPeerConnection, което ви позволява да предавате видео поток без трикове като разделяне на потока на части. Мисля, че натрупването на забавяне се дължи на факта, че браузърът прекодира всяко парче във формат webm преди предаване. Не рових повече, но започнах да изучавам WebRTC. Мисля, че ще напиша отделна статия за резултатите от моето изследване, ако го намеря интересно за общността.

Източник: www.habr.com

Купете надежден хостинг за сайтове с DDoS защита, VPS VDS сървъри 🔥 Купете надежден уеб хостинг със защита от DDoS атаки, VPS VDS сървъри | ProHoster