במאמר זה אני רוצה לשתף את הניסיונות שלי להזרים וידאו דרך websockets מבלי להשתמש בתוספים של צד שלישי לדפדפן כגון Adobe Flash Player. המשך לקרוא כדי לגלות מה יצא מזה.
Adobe Flash, לשעבר Macromedia Flash, היא פלטפורמה ליצירת יישומים הפועלים בדפדפן אינטרנט. לפני הצגת ה-API של Media Stream, זה היה למעשה הפלטפורמה היחידה להזרמת וידאו וקול ממצלמת אינטרנט, כמו גם ליצירת סוגים שונים של ועידות וצ'אטים בדפדפן. הפרוטוקול להעברת מידע מדיה RTMP (Real Time Messaging Protocol) למעשה נסגר לתקופה ארוכה, מה שאומר: אם אתם רוצים להגביר את שירות הסטרימינג שלכם, תהיו אדיבים להשתמש בתוכנה של אדובי עצמם - Adobe Media Server (AMS).
לאחר זמן מה בשנת 2012, אדובי "התייאשה וירקה את זה" לציבור.
פלטפורמת Adobe Flash היא בת יותר מ-20 שנה, ובמהלכן התגלו נקודות תורפה קריטיות רבות, תמיכה
עבור הפרויקט שלי, החלטתי מיד לנטוש לחלוטין את השימוש בפלאש בדפדפן. ציינתי את הסיבה העיקרית לעיל; פלאש גם אינו נתמך כלל בפלטפורמות ניידות, וממש לא רציתי לפרוס את Adobe Flash לפיתוח ב-Windows (אמולטור יין). אז יצאתי לכתוב לקוח ב-JavaScript. זה יהיה רק אב טיפוס, שכן מאוחר יותר למדתי שאפשר לבצע סטרימינג בצורה הרבה יותר יעילה על בסיס p2p, רק בשבילי זה יהיה עמית - שרת - עמיתים, אבל עוד על זה בפעם אחרת, כי זה עדיין לא מוכן.
כדי להתחיל, אנחנו צריכים את שרת ה-websockets האמיתי. הכנתי את הפשוט ביותר על סמך חבילת Melody go:
קוד שרת
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 של אלמנט הווידאו. קוד:
צד השידור
<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>
כדי לשדר זרם וידאו דרך שקעים, עליך לקודד אותו איפשהו, לחצץ אותו ולשדר אותו בחלקים. לא ניתן להעביר את זרם הווידאו הגולמי דרך שקעי אינטרנט. כאן זה בא לעזרתנו
אנו מקודדים את זרם הווידאו, מפרקים אותו לחלקים
<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>
עכשיו בואו נוסיף שידור באמצעות שקעי אינטרנט. באופן מפתיע, כל מה שאתה צריך בשביל זה הוא חפץ
אנו מעבירים את זרם הווידאו לשרת
<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 ונרשמים לאירוע 'הודעה'. לאחר קבלת נתון בינארי, השרת שלנו משדר אותו למנויים, כלומר ללקוחות. במקרה זה, פונקציית ה-callback הקשורה ל"מאזין" של אירוע ה'הודעה' מופעלת בלקוח; האובייקט עצמו מועבר לארגומנט הפונקציה - חלק מזרם הווידאו המקודד על ידי 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>
הרבה זמן ניסיתי להבין למה אי אפשר לשלוח מיד את הקטעים שהתקבלו לאלמנט הווידאו להשמעה, אבל התברר שאי אפשר לעשות את זה כמובן שצריך קודם כל לשים את היצירה במאגר מיוחד המחובר ל רכיב הווידאו, ורק אז הוא יתחיל להפעיל את זרם הווידאו. בשביל זה תצטרך
MediaSource פועלת כמעין מתווך בין אובייקט השמעת המדיה למקור זרם המדיה הזה. האובייקט 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 אלפיות השנייה; אנחנו מגדירים זאת בעצמנו בעת פיצול זרם הווידאו לפני שידורו לשרת. יתרה מכך, כאשר בדקתי במחשב הנייד שלי, הפער בין הצדדים המשדרים והקולטים הצטבר בהדרגה, זה נראה בבירור. התחלתי לחפש דרכים להתגבר על החיסרון הזה, ו... נתקלתי
מקור: www.habr.com