(تقريبًا) كاميرا ويب عديمة الفائدة تتدفق من المستعرض. دفق الوسائط ومقابس الويب

في هذه المقالة ، أرغب في مشاركة محاولاتي لدفق الفيديو عبر مآخذ الويب دون استخدام المكونات الإضافية لمتصفح الطرف الثالث مثل Adobe Flash Player. ما جاء منه ، واصل القراءة.

Adobe Flash - ماكروميديا ​​فلاش سابقًا ، هو نظام أساسي لإنشاء تطبيقات تعمل في مستعرض ويب. قبل تقديم Media Stream API ، كان عمليا النظام الأساسي الوحيد لدفق الفيديو والصوت من كاميرا الويب ، وكذلك لإنشاء أنواع مختلفة من المؤتمرات والمحادثات في المتصفح. تم إغلاق بروتوكول نقل معلومات الوسائط RTMP (Real Time Messaging Protocol) بالفعل لفترة طويلة ، مما يعني: إذا كنت ترغب في رفع مستوى خدمة البث ، فيرجى استخدام البرنامج من Adobe أنفسهم - Adobe Media Server (AMS).

بعد مرور بعض الوقت في عام 2012 ، "استسلمت Adobe وبصقت" للجمهور تخصيص بروتوكول RTMP ، الذي احتوى على أخطاء وكان غير مكتمل في الأساس. بحلول ذلك الوقت ، بدأ المطورون في إجراء تطبيقاتهم الخاصة لهذا البروتوكول ، لذلك ظهر خادم Wowza. في عام 2011 ، رفعت Adobe دعوى قضائية ضد Wowza للاستخدام غير القانوني لبراءات الاختراع المتعلقة بـ RTMP ، بعد 4 سنوات تم حل النزاع من قبل العالم.

منصة Adobe Flash عمرها أكثر من 20 عامًا ، خلال هذا الوقت تم اكتشاف العديد من نقاط الضعف الحرجة ، والدعم وعد توقف بحلول عام 2020 ، لذلك لا توجد بدائل كثيرة لخدمة البث.

بالنسبة لمشروعي ، قررت على الفور التخلي تمامًا عن استخدام Flash في المتصفح. لقد أشرت إلى السبب الرئيسي أعلاه ، كما أن Flash غير مدعوم على الإطلاق على الأنظمة الأساسية للجوّال ، ولم أرغب حقًا في نشر Adobe Flash للتطوير على Windows (محاكي النبيذ). لذلك ، تعهدت بكتابة عميل في JavaScript. سيكون هذا مجرد نموذج أولي ، لأنني علمت لاحقًا أن البث يمكن أن يتم بشكل أكثر كفاءة استنادًا إلى p2p ، بالنسبة لي فقط سيكون نظير - خادم - أقران ، ولكن أكثر في ذلك في وقت آخر ، لأنه ليس جاهزًا بعد.

للبدء ، نحتاج إلى خادم Websocket الفعلي. لقد صنعت أبسطها بناءً على حزمة 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)
}

على العميل (جانب الإرسال) ، تحتاج أولاً إلى الوصول إلى الكاميرا. يتم ذلك من خلال واجهة برمجة تطبيقات ميدياستريم.

نحصل على وصول (إذن) إلى الكاميرا / الميكروفون من خلال واجهة برمجة تطبيقات أجهزة الوسائط. يوفر API طريقة MediaDevices.getUserMedia ()الذي يظهر البوب. نافذة تطلب من المستخدم إذنًا للوصول إلى الكاميرا و / أو الميكروفون. أود أن أشير إلى أنني أجريت جميع التجارب في Google Chrome ، لكنني أعتقد أن كل شيء سيعمل في Firefox بنفس الطريقة تقريبًا.

بعد ذلك ، تُعيد getUserMedia () وعدًا ، والذي يمرر إليه كائن 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>

لبث دفق فيديو عبر المقابس ، من الضروري ترميزه في مكان ما ، وتخزينه مؤقتًا ، ونقله إلى أجزاء. لا يمكن نقل دفق الفيديو الخام عبر مآخذ الويب. هنا يأتي لمساعدتنا Media Recorder API. تسمح لك واجهة برمجة التطبيقات هذه بترميز الدفق وتقسيمه إلى أجزاء. أقوم بتشفير لضغط دفق الفيديو من أجل دفع عدد أقل من وحدات البايت عبر الشبكة. بعد تقسيمها إلى قطع ، يمكنك إرسال كل قطعة إلى مقبس ويب. شفرة:

نقوم بترميز دفق الفيديو ، ونضربه إلى أجزاء

<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. من المستغرب أن كل ما تحتاجه هو كائن 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 ، اشترك في حدث "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>

لقد حاولت لفترة طويلة أن أفهم سبب استحالة إرسال القطع المستلمة فورًا للتشغيل إلى عنصر الفيديو ، ولكن اتضح أنه بالطبع لا يمكنك القيام بذلك ، يجب عليك أولاً وضع القطعة في مخزن مؤقت خاص مرفق إلى عنصر الفيديو ، وعندها فقط سيبدأ تشغيل دفق الفيديو. هذا سوف يتطلب واجهة برمجة تطبيقات مصدر الوسائط и واجهة برمجة تطبيقات قارئ الملفات.

يعمل 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، والذي يسمح لك بنقل دفق فيديو بدون حيل مثل تقسيم الدفق إلى أجزاء. أعتقد أن التأخير التراكمي يرجع إلى حقيقة أن المتصفح يعيد تشفير كل قطعة في تنسيق webm قبل الإرسال. لم أعد أبدأ في البحث أكثر ، لكنني بدأت في دراسة WebRTC ، أفكر في نتائج بحثي ، سأكتب مقالة منفصلة إذا وجدت أنها مثيرة للاهتمام للمجتمع.

المصدر: www.habr.com

إضافة تعليق