U ovom članku želim podijeliti svoje pokušaje streamanja videa putem websocketa bez korištenja dodataka za pretraživače treće strane kao što je Adobe Flash Player. Čitajte dalje da saznate šta je iz toga proizašlo.
Adobe Flash, ranije Macromedia Flash, je platforma za kreiranje aplikacija koje se pokreću u web pretraživaču. Prije uvođenja Media Stream API-ja, to je bila praktički jedina platforma za streaming videa i glasa sa web kamere, kao i za kreiranje raznih vrsta konferencija i ćaskanja u pretraživaču. Protokol za prijenos medijskih informacija RTMP (Real Time Messaging Protocol) je zapravo dugo bio zatvoren, što je značilo: ako želite da poboljšate svoj streaming servis, budite ljubazni da koristite softver od samog Adobe-a - Adobe Media Server (AMS).
Nakon nekog vremena 2012. godine, Adobe je “odustao i ispljunuo ga” javnosti.
Adobe Flash platforma je stara više od 20 godina, tokom kojih su otkrivene mnoge kritične ranjivosti, podrška
Za svoj projekat, odmah sam odlučio da potpuno odustanem od upotrebe Flash-a u pretraživaču. Naveo sam glavni razlog gore; Flash također uopće nije podržan na mobilnim platformama i zaista nisam želio implementirati Adobe Flash za razvoj na Windows (wine emulator). Tako sam krenuo da napišem klijenta u JavaScript-u. Ovo će biti samo prototip, pošto sam kasnije saznao da se streaming može mnogo efikasnije raditi na bazi p2p-a, samo za mene će to biti peer - server - peers, ali o tome drugi put, jer još nije spreman.
Za početak, potreban nam je pravi websockets server. Napravio sam najjednostavniji na osnovu melody go paketa:
Serverski kod
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 klijentu (strani strimovanja), prvo morate pristupiti kameri. Ovo se radi kroz
Dobijamo pristup (dozvolu) kameri/mikrofonu putem
Zatim, getUserMedia() vraća Promise, kojem prosljeđuje MediaStream objekat - tok video-audio podataka. Ovaj objekat dodjeljujemo svojstvu src video elementa. kod:
Strana emitovanja
<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>
Da biste emitovali video stream preko utičnica, morate ga negdje kodirati, baferovati i prenijeti u dijelovima. Sirovi video stream se ne može prenijeti putem websocketa. Tu nam dolazi u pomoć
Kodiramo video stream, razbijamo ga na dijelove
<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>
Sada dodajmo prijenos preko websocketa. Iznenađujuće, sve što vam treba za ovo je objekat
Prenosimo 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>
Strana za emitovanje je spremna! Pokušajmo sada primiti video stream i prikazati ga na klijentu. Šta nam treba za ovo? Prvo, naravno, priključak utičnice. Prilažemo “slušatelja” na WebSocket objekt i pretplaćujemo se na događaj 'poruka'. Nakon što primi dio binarnog podatka, naš server ga emituje pretplatnicima, odnosno klijentima. U ovom slučaju, funkcija povratnog poziva povezana sa "slušateljem" događaja "poruka" se pokreće na klijentu; sam objekt se prosljeđuje u argument funkcije - dio video toka koji je kodiran pomoću vp8.
Prihvatamo 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>
Dugo sam pokušavao da shvatim zašto je nemoguće odmah poslati primljene komade u video element na reprodukciju, ali se pokazalo da se to ne može učiniti, naravno, prvo morate staviti komad u poseban međuspremnik vezan za video element, i tek tada će početi reproducirati video stream. Za ovo će vam trebati
MediaSource djeluje kao neka vrsta posrednika između objekta za reprodukciju medija i izvora ovog medijskog toka. Objekt MediaSource sadrži bafer koji se može priključiti za izvor video/audio toka. Jedna od karakteristika je da bafer može zadržati samo podatke Uint8, tako da će vam trebati FileReader da kreirate takav bafer. Pogledajte kod i bit će vam jasnije:
Reprodukcija video toka
<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>
Prototip streaming servisa je spreman. Glavni nedostatak je što će reprodukcija videa zaostajati za stranom odašiljanja za 100 ms; to sami postavljamo kada dijelimo video stream prije nego što ga prenesemo na server. Štaviše, kada sam provjerio na svom laptopu, zaostajanje između odašiljačke i prijemne strane se postepeno akumuliralo, to je bilo jasno vidljivo. Počeo sam da tražim načine da prevaziđem ovaj nedostatak i... naišao sam
izvor: www.habr.com