Artiklis tahan jagada oma katseid voogedastada videot websocketide kaudu ilma, et kasutaksin veebilehitsejate kolmandate osapoolte lisa, nagu Adobe Flash Player. Mida ma selle käigus saavutasin, loe edasi.
Adobe Flash — endine Macromedia Flash, on platvorm veebibrauseris töötavate rakenduste loomiseks. Enne Media Stream API kasutuselevõttu oli see praktiliselt ainus platvorm voogedastamiseks veebikaamerast video ja heli, samuti erinevate konverentside ja vestluste loomiseks brauseris. Meediaülekande protokoll RTMP (Real Time Messaging Protocol) oli pikka aega praktiliselt suletud, mis tähendas, et kui soovid luua oma voogedastusteenust, pead kasutama Adobe enda tarkvara — Adobe Media Serverit (AMS).
Möödunud mõni aeg pärast 2012. aastat, kui Adobe "andis alla ja viskas" avalikkuse ette RTMP-protokolli, mis sisaldas vigu ja oli sisuliselt puudulik. Sel ajal hakkasid arendajad tegema oma versioone sellest protokollist, nii et tekkis server Wowza. 2011. aastal esitas Adobe Wowza vastu kohtuasja RTMP-ga seotud patentide ebaseadusliku kasutamise eest, mis lahendati neli aastat hiljem sõbralikult.
Adobe Flash platvorm on juba üle 20 aasta, selle aja jooksul on leitud palju kriitilisi turvaauke, mille toetamine peaks lõppema 2020. aastaks, seega ei jää voogedastusteenusele palju alternatiive.
Oma projekti jaoks otsustasin kohe täielikult loobuda Flashi kasutamisest brauseris. Peamine põhjus on eelnevalt mainitud, samuti ei toetata Flashi mobiilplatvormidel ning Adobe Flashi arendamine Windowsis (wine-emulaatoris) tundus liiga vaevanõudev. Seega hakkasin kirjutama klienti JavaScriptis. See saab olema vaid prototüüp, kuna hiljem sain teada, et voogedastust saab palju tõhusamalt teha p2p-põhiselt, kuid minu puhul on see peer — server — peers, aga sellest räägime teinekord, kuna see ei ole veel valmis.
Alustamiseks vajame tegelikult websockets-serverit. Loodud on väga lihtne server go-paketi melody alusel:
Serveri koodi osa
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)
})
// Edastame video voogu
m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
m.BroadcastBinary(msg)
})
log.Println("Starting server...")
http.ListenAndServe(":3000", r)
}
Klientide (ülekandepoole) puhul tuleb kõigepealt saada ligipääs kaamerale. Selleks kasutatakse .
Saame ligipääsu (luba) kaamerale/mikrofonile läbi . See API pakub meetodit , mis kuvab hüpikakna, küsides kasutajalt, kas ta annab ligipääsu kaamerale ja/või mikrofonile. Soovin märkida, et kõik katsetused tegin Google Chrome'is, kuid arvan, et Firefoxis töötab see umbes sama.
Seejärel tagastab getUserMedia() Promise'i, milles edastatakse MediaStream objekt — video- ja audiovoolu. Selle objekti määrame video elemendi src omadusse. Kood:
Ülekandepool
<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>
Video tõukamiseks socketite kaudu tuleb see kuidagi kodeerida, puhverdada ja osade kaupa edastada. Rôhme video voogu ei saa websockets'i kaudu edastada. Siin tuleb meile appi . See API võimaldab voogu kodeerida ja jagada tükkideks. Kodeerin selle video voolu tihendamiseks, et vähem andmeid üle võrgu edastada. Jagades tükkideks, saab iga tüki saata websockets'i kaudu. Kood:
Kodeerime video voogu, lõhname selle osadeks
<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>
Nüüd lisame edastamise websockets'i kaudu. Ootamatult vajame selleks vaid objekti . Sellel on vaid kaks meetodit: send ja close. Nende nimed räägivad enda eest. Täiendatud kood:
Edastame video voogu serverile
<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>
Ülekandepool on valmis! Nüüd proovime vastu võtta video voogu ja kuvada seda kliendil. Mida me selleks vajame? Esiteks loomulikult socket-ühendust. WebSocket objekti peale lisame 'listener'i', registreerime end 'message' sündmuse jaoks. Saades osa binaarsetest andmetest, meie server edastab selle oma tellijatele, st klientidele. Kliendi poolel aktiveerub callback-funktsioon, mis on seotud 'listener'i 'message' sündmusega, funktsiooni argumendiks on tegelikult sama objekt — osa video voost, kodeeritud vp8.
Võtame vastu video voogu
<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>
Ma olen pika aega püüdnud mõista, miks ei saa saadud tükke kohe videoelemendile esitamiseks saata, kuid selgus, et seda ei saa kindlasti teha, tuleb esmalt tükk panna spetsiaalsesse vahemällu, mis on seotud videoelemendiga, ja alles siis hakkab videovoog mängima. Selleks on vajalik ja .
MediaSource on teatud vahendaja meedia esitamise objekti ja antud meedia voogude allika vahel. MediaSource objekt sisaldab ühendatud vahemälu video/helivooge. Üks omadus on see, et vahemälu võib sisaldada ainult Uint8 tüüpi andmeid, seega vajate sellise vahemälu loomiseks FileReaderit. Vaadake koodi ja see muutub selgemaks:
Mängitav videovoog
<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>
Streaming-teenuse prototüüp on valmis. Peamine puudus on see, et videomängimine jääb saatja poolele 100 ms võrra maha, mille me ise määrasime videovoogude jagamisel serverisse edastamiseks. Veelgi enam, kui ma testisin oma sülearvutil, kogunes mul järk-järgult viivitus saatja ja vastuvõtja vahel, mis oli hästi näha. Hakkasin otsima viise, kuidas seda puudust ületada ja... sattusin , mis võimaldab edastada videovoogu ilma voogude tükkideks jagamise vahepealsete trikkideta. Kogunev viivitus on, ma arvan, tingitud sellest, et brauseris toimub iga tükki edastamise loomisel ümberkodeerimine webm formaati. Ma ei hakanud enam sügavamale kaevama, vaid alustasin WebRTC uurimist, oma uurimistulemuste kohta mõtlen, et kirjutan eraldi artikli, kui pean seda kogukonna jaoks huvitavaks.
Allikas: habr.com
