U ovom članku želim podijeliti svoje pokušaje strujanja videa putem web-utičnica bez korištenja dodataka za preglednike trećih strana kao što je Adobe Flash Player. Čitajte dalje kako biste saznali što je iz toga proizašlo.
Adobe Flash, bivši Macromedia Flash, platforma je za izradu aplikacija koje se izvode u web pregledniku. Prije uvođenja Media Stream API-ja, to je bila praktički jedina platforma za streaming videa i glasa s web kamere, kao i za kreiranje raznih vrsta konferencija i chatova u pregledniku. Protokol za prijenos medijskih informacija RTMP (Real Time Messaging Protocol) zapravo je dugo bio zatvoren, što je značilo: ako želite poboljšati svoju uslugu streaminga, budite ljubazni i koristite Adobeov softver - Adobe Media Server (AMS).
Nakon nekog vremena 2012. Adobe je "odustao i ispljunuo" u javnost.
Adobe Flash platforma stara je više od 20 godina, a tijekom tog vremena otkrivene su mnoge kritične ranjivosti, podrška
Za svoj sam projekt odmah odlučio potpuno napustiti korištenje Flasha u pregledniku. Gore sam naveo glavni razlog; Flash također uopće nije podržan na mobilnim platformama i stvarno nisam želio implementirati Adobe Flash za razvoj na Windows (wine emulator). Stoga sam krenuo pisati klijenta u JavaScriptu. Ovo će biti samo prototip, budući da sam kasnije saznao da se streaming može raditi puno učinkovitije na temelju p2p-a, samo što će za mene biti peer - server - peers, ali o tome drugi put, jer još nije gotovo.
Za početak trebamo stvarni websockets poslužitelj. Napravio sam najjednostavniji na temelju paketa melody go:
Kod poslužitelja
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 strani klijenta (streaming), najprije morate pristupiti kameri. Ovo se radi putem
Dobivamo pristup (dopuštenje) kameri/mikrofonu putem
Zatim, getUserMedia() vraća Promise, kojem prosljeđuje MediaStream objekt - tok video-audio podataka. Ovaj objekt dodjeljujemo svojstvu src video elementa. Kodirati:
Strana emitiranja
<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>
Za emitiranje video streama preko soketa, trebate ga negdje kodirati, spremiti u međuspremnik i prenijeti u dijelovima. Neobrađeni video stream ne može se prenositi putem websocketa. Ovdje 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>
Dodajmo sada prijenos putem websocketa. Začudo, sve što vam za ovo treba je predmet
Video stream prenosimo na poslužitelj
<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 emitiranja je spremna! Pokušajmo sada primiti video stream i prikazati ga na klijentu. Što nam treba za ovo? Prvo, naravno, priključak utičnice. WebSocket objektu prilažemo "slušača" i pretplaćujemo se na događaj "message". Nakon što primi dio binarnog podatka, naš poslužitelj ga emitira pretplatnicima, odnosno klijentima. U ovom slučaju, funkcija povratnog poziva povezana sa "slušačem" događaja 'poruke' pokreće se na klijentu; sam objekt se prosljeđuje u argument funkcije - dio video streama kodiran vp8.
Prihvaćamo 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 shvatiti zašto je nemoguće odmah poslati primljene dijelove u video element za reprodukciju, ali pokazalo se da to nije moguće učiniti, naravno, prvo morate staviti komad u poseban međuspremnik vezan za video element, i tek tada će započeti reprodukciju video streama. Za ovo će vam trebati
MediaSource djeluje kao neka vrsta posrednika između objekta reprodukcije medija i izvora ovog medija. Objekt MediaSource sadrži međuspremnik koji se može priključiti za izvor video/audio streama. Jedna značajka je da međuspremnik može držati samo Uint8 podatke, pa će vam trebati FileReader za stvaranje takvog međuspremnika. Pogledajte kod i bit će vam jasnije:
Reprodukcija video streama
<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 usluge je spreman. Glavni nedostatak je što će video reprodukcija kasniti za odašiljačkom stranom za 100 ms; to sami postavljamo prilikom dijeljenja video streama prije prijenosa na poslužitelj. Štoviše, kad sam provjerio na svom prijenosnom računalu, zaostatak između odašiljačke i prijemne strane postupno se nakupljao, to je bilo jasno vidljivo. Počeo sam tražiti načine da prevladam ovaj nedostatak i... naišao sam
Izvor: www.habr.com