V tomto článku se chci podělit o své pokusy o streamování videa přes websockets bez použití pluginů prohlížeče třetích stran, jako je Adobe Flash Player. Čtěte dále a zjistěte, co z toho vzešlo.
Adobe Flash, dříve Macromedia Flash, je platforma pro vytváření aplikací, které běží ve webovém prohlížeči. Před zavedením Media Stream API to byla prakticky jediná platforma pro streamování videa a hlasu z webové kamery a také pro vytváření různých druhů konferencí a chatů v prohlížeči. Protokol pro přenos mediálních informací RTMP (Real Time Messaging Protocol) byl ve skutečnosti na dlouhou dobu uzavřen, což znamenalo: pokud chcete posílit svou streamovací službu, buďte tak laskaví a použijte software od samotné společnosti Adobe - Adobe Media Server (AMS).
Po nějaké době v roce 2012 to Adobe „vzdalo a vyplivlo to“ veřejnosti.
Platforma Adobe Flash je stará více než 20 let a během této doby bylo objeveno mnoho kritických zranitelností.
Pro svůj projekt jsem se okamžitě rozhodl úplně opustit používání Flashe v prohlížeči. Hlavní důvod jsem uvedl výše; Flash také není vůbec podporován na mobilních platformách a opravdu jsem nechtěl nasadit Adobe Flash pro vývoj na Windows (emulátor vína). Pustil jsem se tedy do psaní klienta v JavaScriptu. Toto bude jen prototyp, protože později jsem se dozvěděl, že streamování lze dělat mnohem efektivněji na základě p2p, jen pro mě to bude peer - server - peers, ale o tom jindy, protože to ještě není připraveno.
Abychom mohli začít, potřebujeme skutečný server websockets. Udělal jsem ten nejjednodušší na základě balíčku melodie go:
Kód serveru
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)
}
Na straně klienta (strana streamování) musíte nejprve získat přístup ke kameře. To se provádí skrz
Získáme přístup (oprávnění) ke kameře/mikrofonu prostřednictvím
Dále getUserMedia() vrátí Promise, kterému předá objekt MediaStream – tok video-audio dat. Tento objekt přiřadíme vlastnosti src prvku video. Kód:
Strana vysílání
<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>
Chcete-li vysílat video stream přes zásuvky, musíte jej někde zakódovat, uložit do vyrovnávací paměti a přenést po částech. Nezpracovaný video stream nelze přenášet prostřednictvím webových zásuvek. Tady nám přichází na pomoc
Video stream zakódujeme, rozdělíme na části
<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>
Nyní přidáme přenos přes webové zásuvky. Kupodivu vám k tomu stačí jen předmět
Přenášíme video stream na server
<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>
Vysílací strana je připravena! Nyní se pokusíme přijmout video stream a zobrazit jej na klientovi. Co k tomu potřebujeme? Za prvé, samozřejmě, připojení zásuvky. K objektu WebSocket připojíme „posluchač“ a přihlásíme se k události „zpráva“. Po přijetí části binárních dat je náš server vysílá předplatitelům, tedy klientům. V tomto případě je na klientovi spuštěna funkce zpětného volání spojená s „posluchačem“ události „zpráva“; samotný objekt je předán do argumentu funkce - část video streamu kódovaného vp8.
Přijímáme video stream
<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>
Dlouho jsem se snažil pochopit, proč není možné okamžitě odeslat přijaté kousky do video elementu k přehrání, ale ukázalo se, že to samozřejmě nejde, musíte skladbu nejprve vložit do speciální vyrovnávací paměti vázané na prvek videa a teprve poté začne přehrávat video stream. K tomu budete potřebovat
MediaSource funguje jako jakýsi prostředník mezi objektem přehrávání médií a zdrojem tohoto mediálního toku. Objekt MediaSource obsahuje připojitelnou vyrovnávací paměť pro zdroj video/audio streamu. Jednou funkcí je, že vyrovnávací paměť může obsahovat pouze data Uint8, takže k vytvoření takové vyrovnávací paměti budete potřebovat FileReader. Podívejte se na kód a bude to jasnější:
Přehrávání video streamu
<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>
Prototyp streamovací služby je připraven. Hlavní nevýhodou je zpoždění přehrávání videa za vysílací stranou o 100 ms, to si nastavujeme sami při rozdělování video streamu před přenosem na server. Navíc, když jsem kontroloval svůj notebook, zpoždění mezi vysílací a přijímací stranou se postupně hromadilo, bylo to jasně vidět. Začal jsem hledat způsoby, jak tuto nevýhodu překonat, a... narazil jsem
Zdroj: www.habr.com