У артыкуле я жадаю падзяліцца сваімі спробамі зрабіць стрымінг відэа праз websockets без выкарыстання іншых плагінаў браўзэра тыпу Adobe Flash Player. Што з гэтага атрымалася чытайце далей.
Adobe Flash - раней Macromedia Flash, гэта платформа для стварэння прыкладанняў, якія працуюць у вэб-браўзэры. Да ўкаранення Media Stream API гэта была практычна адзіная платформа для стрыммінгу з вэб-камеры відэа і галасы, а таксама для стварэння рознага роду канферэнцый і чатаў у браўзэры. Пратакол для перадачы медыя-інфармацыі RTMP (Real Time Messaging Protocol), быў фактычна закрытым доўгі час, што азначала: калі хочаш падняць свой стрыммінг-сэрвіс, будзь добры выкарыстоўвай софт ад саміх Adobe – Adobe Media Server (AMS).
Праз некаторы час у 2012 годзе Adobe «здаліся і выплюнулі» на суд публікі.
Adobe Flash платформе ўжо больш за 20 гадоў, за гэты час выявілася мноства крытычных уразлівасцяў, падтрымку
Для свайго праекту я адразу вырашыў цалкам адмовіцца ад выкарыстання Flash у браўзэры. Асноўную прычыну я ўказаў вышэй, таксама Flash зусім не падтрымліваецца на мабільных платформах, ды і разгортваць Adobe Flash для распрацоўкі на windows (эмулятары wine) зусім ужо не хацелася. Таму я ўзяўся пісаць кліент на JavaScript. Гэта будзе ўсяго толькі прататып, бо ў далейшым я даведаўся, што стрымінг можна зрабіць значна больш эфектыўна на аснове p2p, толькі ў мяне гэта будзе peer – server – peers, але пра гэта ў іншы раз, таму што гэта яшчэ не гатова.
Для пачатку працы нам неабходны ўласна websockets-сервер. Я зрабіў найпросты на аснове go-пакета melody:
Код сервернай часткі
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)
}
На кліенце (які транслюе боку) спачатку неабходна атрымаць доступ да камеры. Робіцца гэта праз
Атрымліваем доступ (дазвол) да камеры/мікрафона праз
Далей getUserMedia() вяртае Promise, у якое перадае MediaStream аб'ект - струмень відэа-аўдыё дадзеных. Гэты аб'ект мы прысвойваем у src уласцівасць элемента 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');
// если доступен MediaDevices API, пытаемся получить доступ к камере (можно еще и к микрофону)
// getUserMedia вернет обещание, на которое подписываемся и полученный видеопоток в колбеке направляем в video объект на странице
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
// видео поток привязываем к video тегу, чтобы клиент мог видеть себя и контролировать
video.srcObject = stream;
});
}
</script>
Каб трансляваць відэаструмень праз сокеты, неабходна яго неяк дзесьці кадзіраваць, буферызаваць, і перадаваць часткамі. Сырой відэаструмень не перадаць праз websockets. Тут на дапамогу нам прыходзіць
Кадуем відэаструмень, б'ем яго на часткі
<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>
Цяпер дадамо перадачу па websockets. Як ні дзіўна, для гэтага патрэбен толькі аб'ект
Перадаем відэаструмень на сервер
<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>
Транслюючы бок гатовы! Цяпер давайце паспрабуем прымаць відэаструмень і паказваць яго на кліенце. Што нам спатрэбіцца? Па-першае вядома ж сокет-злучэнне. На аб'ект WebSocket вешаем "слухач" (listener), падпісваемся на падзею 'message'. Атрымаўшы кавалачак бінарных дадзеных наш сервер бродкасціць яго падпісантам, гэта значыць кліентам. На кліенце пры гэтым спрацоўвае callback-функцыя злучаная са «слухачом» падзеі 'message', у аргумент функцыі перадаецца ўласна сам аб'ект — кавалачак відэаструменю, закадаваны vp8.
Прымаем відэаструмень
<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>
Я доўгі час спрабаваў зразумець, чаму нельга атрыманыя кавалачкі адразу ж адправіць на прайграванне элементу video, але аказалася так вядома нельга рабіць, трэба спачатку кавалачак пакласці ў спецыяльны буфер, прывязаны да элемента video, і толькі тады пачне прайграваць відэаструмень. Для гэтага спатрэбіцца
MediaSource выступае нейкім пасярэднікам паміж аб'ектам прайгравання media і крыніцай дадзенага струменя медыя. MediaSource аб'ект утрымоўвае які падлучаецца буфер для крыніцы відэа/аўдыё струменя. Адна асаблівасць складаецца ў тым, што буфер можа ўтрымоўваць толькі дадзеныя тыпу Uint8, таму для стварэння такога буфера запатрабуецца FileReader. Паглядзіце код, і стане больш зразумела:
Прайграваем відэаструмень
<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>
Прататып стрымінг-сэрвісу гатовы. Асноўны мінус у тым, што відэапрайграванне будзе адставаць ад перадаючага боку на 100 мс, гэта мы задалі самі пры разбіцці відэаструменю перад перадачай на сервер. Больш таго, калі я правяраў у сябе на наўтбуку, у мяне паступова збіраўся лаг паміж які перадае і прымаючым бокам, гэта было добра відаць. Я пачаў шукаць спосабы як перамагчы дадзены недахоп, і… натрапіў на
Крыніца: habr.com