זרימת מצלמת אינטרנט (כמעט) חסרת תועלת מדפדפן. Media Stream ו-Websockets

במאמר זה אני רוצה לשתף את הניסיונות שלי להזרים וידאו דרך websockets מבלי להשתמש בתוספים של צד שלישי לדפדפן כגון Adobe Flash Player. המשך לקרוא כדי לגלות מה יצא מזה.

Adobe Flash, לשעבר Macromedia Flash, היא פלטפורמה ליצירת יישומים הפועלים בדפדפן אינטרנט. לפני הצגת ה-API של Media Stream, זה היה למעשה הפלטפורמה היחידה להזרמת וידאו וקול ממצלמת אינטרנט, כמו גם ליצירת סוגים שונים של ועידות וצ'אטים בדפדפן. הפרוטוקול להעברת מידע מדיה RTMP (Real Time Messaging Protocol) למעשה נסגר לתקופה ארוכה, מה שאומר: אם אתם רוצים להגביר את שירות הסטרימינג שלכם, תהיו אדיבים להשתמש בתוכנה של אדובי עצמם - Adobe Media Server (AMS).

לאחר זמן מה בשנת 2012, אדובי "התייאשה וירקה את זה" לציבור. מִפרָט פרוטוקול RTMP, שהכיל שגיאות ובעיקרו לא היה שלם. עד אז, מפתחים החלו לבצע יישומים משלהם של פרוטוקול זה, ושרת Wowza הופיע. בשנת 2011, אדובי הגישה תביעה נגד Wowza בגין שימוש לא חוקי בפטנטים הקשורים ל-RTMP; לאחר 4 שנים, הסכסוך נפתר בהסכמה.

פלטפורמת Adobe Flash היא בת יותר מ-20 שנה, ובמהלכן התגלו נקודות תורפה קריטיות רבות, תמיכה מוּבטָח להסתיים עד 2020, מה שיותיר מעט חלופות לשירות הסטרימינג.

עבור הפרויקט שלי, החלטתי מיד לנטוש לחלוטין את השימוש בפלאש בדפדפן. ציינתי את הסיבה העיקרית לעיל; פלאש גם אינו נתמך כלל בפלטפורמות ניידות, וממש לא רציתי לפרוס את 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)
}

בלקוח (צד הסטרימינג), תחילה עליך לגשת למצלמה. זה נעשה דרך MediaStream API.

אנו מקבלים גישה (הרשאה) למצלמה/מיקרופון דרך API של התקני מדיה. API זה מספק שיטה MediaDevices.getUserMedia(), שמציג קופץ. חלון המבקש מהמשתמש רשות לגשת למצלמה ו/או למיקרופון. אני רוצה לציין שביצעתי את כל הניסויים בגוגל כרום, אבל אני חושב שהכל יעבוד בערך אותו דבר בפיירפוקס.

לאחר מכן, 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>

כדי לשדר זרם וידאו דרך שקעים, עליך לקודד אותו איפשהו, לחצץ אותו ולשדר אותו בחלקים. לא ניתן להעביר את זרם הווידאו הגולמי דרך שקעי אינטרנט. כאן זה בא לעזרתנו MediaRecorder API. API זה מאפשר לך לקודד ולפרק את הזרם לחתיכות. אני עושה קידוד כדי לדחוס את זרם הווידאו כדי לשלוח פחות בתים דרך הרשת. לאחר שפירקת אותו לחתיכות, אתה יכול לשלוח כל חלק ל-websocket. קוד:

אנו מקודדים את זרם הווידאו, מפרקים אותו לחלקים

<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>

עכשיו בואו נוסיף שידור באמצעות שקעי אינטרנט. באופן מפתיע, כל מה שאתה צריך בשביל זה הוא חפץ WebSocket. יש לו רק שתי שיטות שליחה וסגירה. השמות מדברים בעד עצמם. קוד נוסף:

אנו מעבירים את זרם הווידאו לשרת

<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 API и ממשק API של FileReader.

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 אלפיות השנייה; אנחנו מגדירים זאת בעצמנו בעת פיצול זרם הווידאו לפני שידורו לשרת. יתרה מכך, כאשר בדקתי במחשב הנייד שלי, הפער בין הצדדים המשדרים והקולטים הצטבר בהדרגה, זה נראה בבירור. התחלתי לחפש דרכים להתגבר על החיסרון הזה, ו... נתקלתי RTCPeerConnection API, המאפשר לך להעביר זרם וידאו ללא טריקים כמו פיצול הזרם לחתיכות. ההשהיה המצטברת, לדעתי, נובעת מהעובדה שהדפדפן מקודד מחדש כל חלק לפורמט webm לפני השידור. לא חפרתי יותר, אבל התחלתי ללמוד WebRTC. אני חושב שאכתוב מאמר נפרד על תוצאות המחקר שלי אם אני אמצא שזה מעניין את הקהילה.

מקור: www.habr.com

הוספת תגובה