Bu yazıda mən Adobe Flash Player kimi üçüncü tərəf brauzer plaginlərindən istifadə etmədən veb-sockets vasitəsilə video yayımlamaq cəhdlərimi bölüşmək istəyirəm. Bunun nədən gəldiyini öyrənmək üçün oxuyun.
Adobe Flash, əvvəllər Macromedia Flash, veb brauzerdə işləyən proqramlar yaratmaq üçün platformadır. Media Stream API-nin tətbiqindən əvvəl o, veb-kameradan video və səs axını, həmçinin brauzerdə müxtəlif növ konfranslar və söhbətlər yaratmaq üçün praktiki olaraq yeganə platforma idi. Media məlumatlarının ötürülməsi protokolu RTMP (Real Time Messaging Protocol) əslində uzun müddət bağlandı, bu o demək idi: axın xidmətinizi artırmaq istəyirsinizsə, Adobe-nin proqramlarından istifadə etmək üçün kifayət qədər xeyirxah olun - Adobe Media Server (AMS).
2012-ci ildə bir müddət sonra Adobe ictimaiyyətə "təslim oldu və tükürdü".
Adobe Flash platformasının 20 ildən çox yaşı var və bu müddət ərzində bir çox kritik boşluqlar aşkar edilib, dəstək
Layihəm üçün dərhal brauzerdə Flash istifadəsindən tamamilə imtina etmək qərarına gəldim. Yuxarıda əsas səbəbi qeyd etdim; Flash da mobil platformalarda ümumiyyətlə dəstəklənmir və mən həqiqətən Adobe Flash-ı Windows-da (şərab emulyatoru) inkişaf etdirmək üçün yerləşdirmək istəmirdim. Beləliklə, mən JavaScript-də müştəri yazmağa başladım. Bu, sadəcə bir prototip olacaq, çünki sonradan öyrəndim ki, axın p2p əsasında daha səmərəli həyata keçirilə bilər, yalnız mənim üçün bu, həmyaşıd - server - həmyaşıdlar olacaq, lakin başqa vaxt daha çox olacaq, çünki hələ hazır deyil.
Başlamaq üçün bizə faktiki websockets server lazımdır. Melody go paketi əsasında ən sadəini hazırladım:
Server kodu
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)
}
Müştəridə (axın tərəfində) əvvəlcə kameraya daxil olmalısınız. Bu vasitəsilə həyata keçirilir
Biz vasitəsilə kameraya/mikrofona giriş (icazə) əldə edirik
Sonra, getUserMedia() MediaStream obyektini - video-audio məlumat axınını ötürdüyü Promise qaytarır. Bu obyekti video elementin src xassəsinə təyin edirik. Kod:
Yayım tərəfi
<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>
Video axınını rozetkalar üzərindən yayımlamaq üçün onu haradasa kodlaşdırmaq, bufer etmək və hissə-hissə ötürmək lazımdır. Xam video axını veb yuvalar vasitəsilə ötürülə bilməz. Bu, bizim köməyimizə çatır
Video axını kodlayırıq, onu parçalara ayırırıq
<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>
İndi veb-soketlər vasitəsilə ötürülmə əlavə edək. Təəccüblüdür ki, bunun üçün sizə lazım olan hər şey bir obyektdir
Video axınını serverə ötürürük
<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>
Yayım tərəfi hazırdır! İndi video axını qəbul etməyə və onu müştəridə göstərməyə çalışaq. Bunun üçün bizə nə lazımdır? Birincisi, əlbəttə ki, rozetka bağlantısı. Biz WebSocket obyektinə “dinləyici” əlavə edirik və “mesaj” hadisəsinə abunə oluruq. İkili məlumatların bir hissəsini aldıqdan sonra serverimiz onu abunəçilərə, yəni müştərilərə yayımlayır. Bu halda, "mesaj" hadisəsinin "dinləyicisi" ilə əlaqəli geri çağırış funksiyası müştəridə işə salınır; obyektin özü funksiya arqumentinə - vp8 ilə kodlanmış video axınının bir hissəsinə ötürülür.
Video axını qəbul edirik
<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>
Uzun müddətdir ki, qəbul edilmiş parçaları oxutmaq üçün dərhal video elementə göndərməyin niyə qeyri-mümkün olduğunu anlamağa çalışdım, amma məlum oldu ki, bunu etmək mümkün deyil, əlbəttə ki, əvvəlcə parçanı xüsusi bir buferə qoymalısınız. video elementi və yalnız bundan sonra video axını oynatmağa başlayacaq. Bunun üçün sizə lazım olacaq
MediaSource media oxutma obyekti ilə bu media axınının mənbəyi arasında bir növ vasitəçi kimi çıxış edir. MediaSource obyekti video/audio axınının mənbəyi üçün qoşula bilən buferi ehtiva edir. Xüsusiyyətlərdən biri budur ki, bufer yalnız Uint8 məlumatlarını saxlaya bilər, ona görə də belə bir bufer yaratmaq üçün sizə FileReader lazımdır. Koda baxın və daha aydın olacaq:
Video axını oynayır
<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>
Striminq xidmətinin prototipi hazırdır. Əsas çatışmazlıq odur ki, videonun oxudulması ötürücü tərəfdən 100 ms geri qalacaq; biz bunu video axınını serverə ötürməzdən əvvəl bölərkən özümüz təyin edirik. Üstəlik, dizüstü kompüterimi yoxlayanda, ötürücü və qəbuledici tərəflər arasındakı gecikmə tədricən yığıldı, bu aydın görünürdü. Bu dezavantajı aradan qaldırmağın yollarını axtarmağa başladım və... qarşıma çıxdı
Mənbə: www.habr.com