Ebben a cikkben szeretném megosztani azokat a próbálkozásaimat, amelyekkel a websocketeken keresztül próbáltam streamelni videókat harmadik féltől származó böngészőbővítmények, például az Adobe Flash Player használata nélkül. Olvasson tovább, hogy megtudja, mi lett belőle.
Az Adobe Flash, korábbi nevén Macromedia Flash egy webböngészőben futó alkalmazások létrehozására szolgáló platform. A Media Stream API bevezetése előtt gyakorlatilag ez volt az egyetlen platform a webkamerás videó és hang streamelésére, valamint különféle konferenciák és chatek böngészőben történő létrehozására. A médiainformációk továbbítására szolgáló RTMP (Real Time Messaging Protocol) protokoll hosszú időre le volt zárva, ami azt jelentette: ha szeretné fellendíteni a streaming szolgáltatását, legyen szíves magától az Adobe szoftverétől - Adobe Media Server (AMS) - használni.
2012-ben egy idő után az Adobe „feladta és kiköpte” a nyilvánosság elé.
Az Adobe Flash platform több mint 20 éves, ez idő alatt számos kritikus biztonsági rést fedeztek fel, támogatás
A projektemnél azonnal úgy döntöttem, hogy teljesen elhagyom a Flash használatát a böngészőben. A fő okot fentebb jeleztem; a Flash szintén egyáltalán nem támogatott mobil platformokon, és nagyon nem akartam telepíteni az Adobe Flash-t Windows-on (wine emulator) való fejlesztéshez. Ezért elhatároztam, hogy írok egy klienst JavaScriptben. Ez csak egy prototípus lesz, mert később megtudtam, hogy a streamelést sokkal hatékonyabban lehet csinálni p2p alapján, csak nekem peer - server - peer lesz, de erről majd máskor, mert még nincs kész.
A kezdéshez szükségünk van a tényleges websockets szerverre. A melody go csomag alapján elkészítettem a legegyszerűbbet:
Szerver kódja
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)
}
Az ügyfélen (streaming oldalon) először el kell érnie a kamerát. Ez keresztül történik
Ezen keresztül kapunk hozzáférést (engedélyt) a kamerához/mikrofonhoz
Ezután a getUserMedia() Promise-t ad vissza, amelynek átad egy MediaStream objektumot – egy video-audio adatfolyamot. Ezt az objektumot hozzárendeljük a videoelem src tulajdonságához. Kód:
Műsorszórási oldal
<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>
Ahhoz, hogy videofolyamot socketeken keresztül sugározzon, valahol kódolnia kell, pufferelnie kell, és részenként továbbítania kell. A nyers videofolyam nem továbbítható websocket-en keresztül. Itt jön a segítségünkre
A videofolyamot kódoljuk, részekre bontjuk
<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>
Most adjuk hozzá a websocketeken keresztüli átvitelt. Meglepő módon ehhez csak egy tárgy kell
A video streamet továbbítjuk a szervernek
<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>
Elkészült a közvetítési oldal! Most próbáljunk meg egy videofolyamot fogadni és megjeleníteni az ügyfélen. Mi kell ehhez? Először is természetesen az aljzatcsatlakozás. A WebSocket objektumhoz egy „hallgatót” csatolunk, és feliratkozunk az „üzenet” eseményre. Miután megkapta a bináris adatot, szerverünk továbbítja az előfizetőknek, azaz az ügyfeleknek. Ebben az esetben az „üzenet” esemény „hallgatójához” társított visszahívási funkció aktiválódik a kliensen, maga az objektum pedig átkerül a függvény argumentumába – a vp8 által kódolt videofolyam egy darabjába.
Videó streamet elfogadunk
<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>
Sokáig próbáltam megérteni, hogy miért nem lehet a beérkezett darabokat azonnal visszaküldeni a videóelemre, de kiderült, hogy ezt nem lehet megtenni, természetesen először egy speciális pufferbe kell helyezni a darabot. a videó elemet, és csak ezután kezdi el lejátszani a videofolyamot. Ehhez szüksége lesz
A MediaSource egyfajta közvetítőként működik a médialejátszó objektum és a médiafolyam forrása között. A MediaSource objektum egy csatlakoztatható puffert tartalmaz a videó/audio adatfolyam forrásához. Az egyik jellemző, hogy a puffer csak Uint8 adatokat tud tárolni, így egy ilyen puffer létrehozásához FileReaderre lesz szüksége. Nézd meg a kódot, és világosabb lesz:
A videofolyam lejátszása
<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>
Elkészült a streaming szolgáltatás prototípusa. Legfőbb hátránya, hogy a videó lejátszás 100 ms-al elmarad az átviteli oldaltól, ezt mi magunk állítjuk be a videofolyam felosztásánál, mielőtt továbbítanánk a szerverre. Sőt, amikor megnéztem a laptopomon, fokozatosan felgyülemlett a késés az adó és a vevő oldal között, ez jól látható volt. Elkezdtem keresni a módokat, hogy leküzdjem ezt a hátrányt, és... rájöttem
Forrás: will.com