Në këtë artikull, dua të ndajem përpjekjet e mia për të bërë streaming video përmes websockets pa përdorur plugina të palëve të treta të shfletuesit, si Adobe Flash Player. Çfarë doli nga kjo, lexoni më poshtë.
Adobe Flash — më parë Macromedia Flash, është një platformë për krijimin e aplikacioneve që funksionojnë në shfletuesin e internetit. Para se të përdorej Media Stream API, kjo ishte praktisht platforma e vetme për streaming video dhe audio nga kamera, si dhe për krijimin e llojeve të ndryshme të konferencave dhe bisedave në shfletues. Protokolli për transferimin e informacionit mediatik RTMP (Real Time Messaging Protocol), ishte faktikisht i mbyllur për një kohë të gjatë, çka do të thoshte: nëse dëshiron të krijosh shërbimin tënd të streaming, duhet të përdorësh softin e Adobe — Adobe Media Server (AMS).
Pas një kohë, në 2012, Adobe "u dorëzuan dhe e lanë" publikun të njihte protokollin RTMP, i cili kishte gabime dhe faktikisht nuk ishte i plotë. E në atë kohë zhvilluesit filluan të krijonin implementime të veta të këtij protokolli, kështu që u krijua serveri Wowza. Në 2011, Adobe paditi Wowza për përdorimin e paligjshëm të patentave lidhur me RTMP, dhe pas 4 vitesh, konflikti u zgjodh në paqe.
Platforma Adobe Flash është mbi 20 vjeçare, dhe gjatë kësaj kohe janë zbuluar shumë vulnerabilitete kritike, mbështetja do të përfundojë në vitin 2020, kështu që alternativat për shërbimin e streamingut nuk janë aq të shumta.
Për projektin tim, vendosa menjëherë të heq dorë plotësisht nga përdorimi i Flash në shfletues. Arsyeja kryesore e kam përmendur më lart, gjithashtu Flash nuk mbështetet në platformat mobile, dhe gjithashtu nuk doja të konfigurja Adobe Flash për zhvillim në Windows (në emulatorin Wine). Prandaj, fillova të shkruaj një klient në JavaScript. Kjo do të jetë vetëm një prototip, pasi më vonë mësova se streaming mund të bëhet shumë më efektivisht mbi bazën e p2p, vetëm se në rastin tim do të jetë peer — server — peers, por për këtë njëherë tjetër, sepse akoma nuk është gati.
Për të filluar punën, na nevojitet një server websockets. Unë krijova një të thjeshtë mbi paketën go melody:
Kodi i pjesës server
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)
})
// Bërja e broadcasting të videopolit
m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
m.BroadcastBinary(msg)
})
log.Println("Duke nisur serverin...")
http.ListenAndServe(":3000", r)
}
Në klient (anën që transmeton) fillimisht është e nevojshme të fitohet akses në kamerë. Kjo bëhet përmes .
Marrim lejen për akses në kamerë/mikrofon përmes . Ky API ofron metodën , e cila shfaq një dritare pop-up, që kërkon lejen e përdoruesit për akses në kamerë ose/edhe mikrofon. Dëshiroj të theksoj se të gjitha eksperimentet i kam krejt në Google Chrome, por mendoj se në Firefox do të funksionojë përafërsisht siç duhet.
Më pas getUserMedia() kthen një Promise, në të cilin dërgon një objekt MediaStream — një rrjedhë të dhënash video-audio. Ky objekt e kemi caktuar në pronësinë src të elementit video. Kodi:
Anë e transmetimit
<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>
Për të transmetuar videopolit përmes soketeve, është e nevojshme që ndonjë mënyrë ta kodosh, ta bufferosh, dhe ta transmetosh në pjesë. Një rrjedhë videoje e papërpunuar nuk mund të dërgohet përmes websockets. Këtu vjen në ndihmë . Ky API lejon kodimin dhendarjen e rrjedhës në copa. Kodimi e bëj për të kompresuar rrjedhën video, që të bëj më pak bytes në rrjet. Pasi të ndahet në copa, çdo copë mund të dërgohet në websocket. Kodi:
Kodojmë rrjedhën video, e ndajmë në pjesë
<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>
Tani, le t'i shtojmë dërgimin përmes websockets. Siç është e habitshme, për këtë nevojitet vetëm një objekt . Ka vetëm dy metoda send dhe close. Emrat flasin për veten. Kodi i plotësuar:
Dërgojmë rrjedhën video në 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>
Anë e transmetimit është gati! Tani le të provojmë të pranojmë rrjedhën video dhe ta shfaqim në klient. Çfarë na nevojitet për këtë? Së pari natyrisht lidhja me soket. Në objektin WebSocket vendosim "dëgjuesin" (listener), dhe regjistrohemi për ngjarjen 'message'. Pas marrjes së një blloku të të dhënave binare, serveri ynë bën broadcasting për ndjekësit e tij, pra klientët. Në klient, njëkohësisht aktivizohet funksioni callback lidhur me "dëgjuesin" e ngjarjes 'message', ku argumentohet objekti — blloku i rrjedhës video, i koduar vp8.
Pranojmë rrjedhën video
<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>
Kam kaluar një kohë të gjatë duke u përpjekur të kuptoj pse blloqet e marra nuk mund të dërgohen direkt për t'u luajtur nga elementi video, por duket se kështu nuk mund të ndodhë, duhet së pari ta vendosësh bllokun në një buffer të veçantë të lidhur me elementin video, dhe vetëm atëherë do të fillojë të luajë rrjedhën video. Për këtë do të na nevojitet dhe .
MediaSource vepron si një ndërmjetës midis objektit të riprodhimit të medias dhe burimit të këtij rrjedhje media. Objekti MediaSource përmban një bufer të lidhshëm për burimin e rrjedhës video/audio. Një veçori është se buferi mund të përmbajë vetëm të dhëna të tipit Uint8, prandaj për të krijuar një bufer të tillë do të nevojitet FileReader. Shikoni kodin, dhe do të kuptoni më qartë:
Riprodho rrjedhën video
<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>
Prototipi i shërbimit të transmetimit është gati. Dobësia kryesore është se riprodhimi i videos do të ka një vonesë prej 100 ms nga pala e transmetimit, këtë e kemi vendosur ne vetë duke ndarë rrjedhën video para se ta dërgojmë në server. Më tej, kur e verifikoja në laptopin tim, fillova të përjetoj një vonesë në rritje midis palës që transmeton dhe asaj që pranon, ishte shumë e dukshme. Fillova të kërkoj mënyra për të përballuar këtë disavantazh dhe… përfundova në , i cili lejon dërgimin e rrjedhës video pa mashtresa të tilla si ndarja e rrjedhës në copëza. Vonesa e akumuluar, mendoj, ndodh për shkak se në shfletues para dërgimit ndodh rikodimi i çdo copëze në formatin webm. Nuk vazhdova më tej me hulumtimin, por fillova të studioj WebRTC, dhe mendoj se do të shkruaj një artikull të veçantë për rezultatet e hulumtimeve të mia, nëse e gjykoj të tillë tërheqëse për komunitetin.
Burimi: habr.com
