ब्राउझरवरून (जवळजवळ) निरुपयोगी वेबकॅम स्ट्रीमिंग. मीडिया स्ट्रीम आणि वेबसॉकेट्स

या लेखात मला Adobe Flash Player सारख्या तृतीय-पक्ष ब्राउझर प्लगइन न वापरता वेबसॉकेटद्वारे व्हिडिओ प्रवाहित करण्याचे माझे प्रयत्न सामायिक करायचे आहेत. त्यातून काय आले हे शोधण्यासाठी वाचा.

Adobe Flash, पूर्वी Macromedia Flash, वेब ब्राउझरमध्ये चालणारे ॲप्लिकेशन तयार करण्यासाठी एक व्यासपीठ आहे. मीडिया स्ट्रीम एपीआयचा परिचय करण्यापूर्वी, वेबकॅमवरून व्हिडिओ आणि व्हॉइस प्रवाहित करण्यासाठी तसेच ब्राउझरमध्ये विविध प्रकारच्या कॉन्फरन्स आणि चॅट्स तयार करण्यासाठी हे व्यावहारिकरित्या एकमेव व्यासपीठ होते. मीडिया माहिती RTMP (रिअल टाईम मेसेजिंग प्रोटोकॉल) प्रसारित करण्याचा प्रोटोकॉल बर्याच काळापासून बंद होता, ज्याचा अर्थ असा होता: जर तुम्हाला तुमच्या स्ट्रीमिंग सेवेला चालना द्यायची असेल, तर स्वत: Adobe - Adobe Media Server (AMS) कडून सॉफ्टवेअर वापरण्याइतपत दया दाखवा.

2012 मध्ये काही काळानंतर, Adobe ने लोकांसमोर “त्याग केला आणि थुंकला”. तपशील RTMP प्रोटोकॉल, ज्यामध्ये त्रुटी होत्या आणि ते मूलत: अपूर्ण होते. तोपर्यंत, विकसकांनी या प्रोटोकॉलची स्वतःची अंमलबजावणी करण्यास सुरवात केली आणि वोझा सर्व्हर दिसू लागला. 2011 मध्ये, Adobe ने RTMP-संबंधित पेटंटचा बेकायदेशीर वापर केल्याबद्दल Wowza विरुद्ध खटला दाखल केला; 4 वर्षांनंतर, हा संघर्ष सौहार्दपूर्णपणे सोडवला गेला.

Adobe Flash प्लॅटफॉर्म 20 वर्षांहून अधिक जुना आहे, त्या काळात अनेक गंभीर भेद्यता शोधल्या गेल्या आहेत, समर्थन वचन दिले स्ट्रीमिंग सेवेसाठी काही पर्याय सोडून 2020 पर्यंत.

माझ्या प्रकल्पासाठी, मी ताबडतोब ब्राउझरमध्ये फ्लॅशचा वापर पूर्णपणे सोडून देण्याचा निर्णय घेतला. मी वरील मुख्य कारण सूचित केले आहे; फ्लॅश देखील मोबाइल प्लॅटफॉर्मवर अजिबात समर्थित नाही आणि मला विंडोज (वाइन एमुलेटर) वर विकासासाठी Adobe Flash तैनात करायचे नव्हते. म्हणून मी JavaScript मध्ये क्लायंट लिहायला निघालो. हे फक्त एक प्रोटोटाइप असेल, कारण नंतर मला कळले की p2p वर आधारित स्ट्रीमिंग अधिक कार्यक्षमतेने केले जाऊ शकते, फक्त माझ्यासाठी ते पीअर - सर्व्हर - पीअर असेल, परंतु त्याबद्दल आणखी एक वेळ, कारण ते अद्याप तयार नाही.

प्रारंभ करण्यासाठी, आम्हाला वास्तविक वेबसॉकेट सर्व्हरची आवश्यकता आहे. मी मेलोडी गो पॅकेजवर आधारित सर्वात सोपा बनवला:

सर्व्हर कोड

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(), जे पॉपअप दाखवते. कॅमेरा आणि/किंवा मायक्रोफोनमध्ये प्रवेश करण्यासाठी वापरकर्त्याला परवानगी मागणारी विंडो. मी हे लक्षात घेऊ इच्छितो की मी Google Chrome मध्ये सर्व प्रयोग केले आहेत, परंतु मला वाटते की फायरफॉक्समध्ये सर्व काही समान कार्य करेल.

पुढे, getUserMedia() एक वचन देतो, ज्यामध्ये ते मीडियास्ट्रीम ऑब्जेक्ट पास करते - व्हिडिओ-ऑडिओ डेटाचा प्रवाह. आम्ही हा ऑब्जेक्ट व्हिडिओ घटकाच्या 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 तुम्हाला एन्कोड करण्याची आणि प्रवाहाचे तुकडे करण्याची परवानगी देते. नेटवर्कवर कमी बाइट्स पाठवण्यासाठी मी व्हिडिओ स्ट्रीम कॉम्प्रेस करण्यासाठी एन्कोडिंग करतो. त्याचे तुकडे करून, तुम्ही प्रत्येक तुकडा वेबसॉकेटवर पाठवू शकता. कोड:

आम्ही व्हिडिओ प्रवाह एन्कोड करतो, त्यास भागांमध्ये खंडित करतो

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

प्रसारण बाजू तयार आहे! आता व्हिडिओ प्रवाह प्राप्त करण्याचा आणि क्लायंटवर प्रदर्शित करण्याचा प्रयत्न करूया. यासाठी आम्हाला काय हवे आहे? प्रथम, अर्थातच, सॉकेट कनेक्शन. आम्ही वेबसॉकेट ऑब्जेक्टला "श्रोता" संलग्न करतो आणि 'संदेश' इव्हेंटची सदस्यता घेतो. बायनरी डेटाचा एक तुकडा प्राप्त झाल्यानंतर, आमचा सर्व्हर तो सदस्यांना, म्हणजे क्लायंटसाठी प्रसारित करतो. या प्रकरणात, 'मेसेज' इव्हेंटच्या "श्रोता" शी संबंधित कॉलबॅक फंक्शन क्लायंटवर ट्रिगर केले जाते; ऑब्जेक्ट स्वतः फंक्शन युक्तिवादात जातो - 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 и FileReader API.

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 ms मागे जाईल; सर्व्हरवर प्रसारित करण्यापूर्वी व्हिडिओ प्रवाह विभाजित करताना आम्ही हे स्वतः सेट करतो. शिवाय, जेव्हा मी माझ्या लॅपटॉपवर तपासले तेव्हा ट्रान्समिटिंग आणि रिसीव्हिंग बाजूंमधील अंतर हळूहळू जमा होत गेले, हे स्पष्टपणे दिसत होते. मी या गैरसोयीवर मात करण्याचे मार्ग शोधू लागलो, आणि... समोर आले RTCPeerConnection API, जे तुम्हाला स्ट्रीमचे तुकडे करणे यासारख्या युक्त्यांशिवाय व्हिडिओ प्रवाह प्रसारित करण्यास अनुमती देते. जमा होणारा अंतर, माझ्या मते, प्रसारणापूर्वी ब्राउझर प्रत्येक तुकडा वेबम स्वरूपात पुन्हा एन्कोड करतो या वस्तुस्थितीमुळे आहे. मी आणखी काही खोदले नाही, परंतु WebRTC चा अभ्यास सुरू केला. मला वाटते की माझ्या संशोधनाच्या परिणामांबद्दल मी एक स्वतंत्र लेख लिहीन जर मला ते समुदायासाठी मनोरंजक वाटले.

स्त्रोत: www.habr.com

एक टिप्पणी जोडा