Šajā rakstā es vēlos dalīties ar saviem mēģinājumiem straumēt video, izmantojot tīmekļa ligzdas, neizmantojot trešās puses pārlūkprogrammas spraudņus, piemēram, Adobe Flash Player. Lasiet tālāk, lai uzzinātu, kas no tā izriet.
Adobe Flash, agrāk Macromedia Flash, ir platforma lietojumprogrammu izveidei, kas darbojas tīmekļa pārlūkprogrammā. Pirms Media Stream API ieviešanas tā bija praktiski vienīgā platforma video un balss straumēšanai no tīmekļa kameras, kā arī dažādu konferenču un tērzēšanas veidu veidošanai pārlūkprogrammā. Multivides informācijas pārsūtīšanas protokols RTMP (Real Time Messaging Protocol) faktiski bija slēgts uz ilgu laiku, kas nozīmēja: ja vēlaties uzlabot savu straumēšanas pakalpojumu, esiet laipni un izmantojiet pašu Adobe programmatūru - Adobe Media Server (AMS).
Pēc kāda laika 2012. gadā Adobe “padevās un to izspļāva” sabiedrībai.
Adobe Flash platforma ir vairāk nekā 20 gadus veca, un šajā laikā ir atklātas daudzas kritiskas ievainojamības, atbalsts
Savam projektam es nekavējoties nolēmu pilnībā atteikties no Flash izmantošanas pārlūkprogrammā. Iepriekš norādīju galveno iemeslu; Flash arī vispār netiek atbalstīts mobilajās platformās, un es patiešām negribēju izvietot Adobe Flash izstrādei operētājsistēmā Windows (vīna emulators). Tāpēc es nolēmu rakstīt klientu JavaScript. Šis būs tikai prototips, jo vēlāk uzzināju, ka uz p2p straumēšanu var veikt daudz efektīvāk, tikai man tas būs peer - server - peers, bet par to citreiz, jo tas vēl nav gatavs.
Lai sāktu, mums ir nepieciešams faktiskais Websockets serveris. Es izveidoju vienkāršāko, pamatojoties uz melody go pakotni:
Servera kods
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)
}
Klientā (straumēšanas pusē) vispirms ir jāpiekļūst kamerai. Tas tiek darīts cauri
Mēs iegūstam piekļuvi (atļauju) kamerai/mikrofonam caur
Pēc tam getUserMedia() atgriež solījumu, kuram tas nodod MediaStream objektu — video-audio datu straumi. Mēs piešķiram šo objektu video elementa rekvizītam src. Kods:
Apraides puse
<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>
Lai pārraidītu video straumi pa ligzdām, tā kaut kur ir jākodē, buferē un jāpārraida pa daļām. Neapstrādātu video straumi nevar pārsūtīt, izmantojot tīmekļa ligzdas. Šeit tas nāk mums palīgā
Mēs iekodējam video straumi, sadalām to daļās
<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>
Tagad pievienosim pārraidi, izmantojot tīmekļa ligzdas. Pārsteidzoši, viss, kas jums nepieciešams, ir objekts
Mēs pārraidām video straumi uz serveri
<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>
Raidījuma puse ir gatava! Tagad mēģināsim saņemt video straumi un parādīt to klientam. Kas mums tam vajadzīgs? Pirmkārt, protams, kontaktligzdas savienojums. Mēs pievienojam WebSocket objektam "klausītāju" un abonējam notikumu "ziņojums". Saņemot bināro datu daļu, mūsu serveris tos pārraida abonentiem, tas ir, klientiem. Šajā gadījumā klientam tiek aktivizēta atzvanīšanas funkcija, kas saistīta ar ziņojuma notikuma “klausītāju”; pats objekts tiek nodots funkcijas argumentam — vp8 kodētā video straumes daļai.
Mēs pieņemam video straumi
<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>
Ilgi mēģināju saprast, kāpēc saņemtos gabalus nav iespējams uzreiz nosūtīt uz video elementu atskaņošanai, bet izrādījās, ka to nevar izdarīt, protams, vispirms jāieliek gabals speciālā buferī, kas saistīts ar video elementu, un tikai tad tas sāks atskaņot video straumi. Šim nolūkam jums būs nepieciešams
MediaSource darbojas kā sava veida starpnieks starp multivides atskaņošanas objektu un šīs multivides straumes avotu. MediaSource objekts satur pievienojamu buferi video/audio straumes avotam. Viena iezīme ir tāda, ka buferis var saturēt tikai Uint8 datus, tāpēc jums būs nepieciešams FileReader, lai izveidotu šādu buferi. Apskatiet kodu, un tas kļūs skaidrāks:
Tiek atskaņota video straume
<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>
Straumēšanas pakalpojuma prototips ir gatavs. Galvenais trūkums ir tas, ka video atskaņošana atpaliks no raidošās puses par 100 ms; mēs to iestatām paši, sadalot video straumi pirms tās pārsūtīšanas uz serveri. Turklāt, pārbaudot klēpjdatoru, pakāpeniski uzkrājās nobīde starp raidīšanas un uztveršanas pusēm, tas bija skaidri redzams. Es sāku meklēt veidus, kā pārvarēt šo trūkumu, un... sanāca
Avots: www.habr.com