(เกือบ) การสตรีมเว็บแคมที่ไร้ประโยชน์จากเบราว์เซอร์ สื่อสตรีมและเว็บซ็อกเก็ต

ในบทความนี้ ฉันต้องการแบ่งปันความพยายามของฉันในการสตรีมวิดีโอผ่านเว็บซ็อกเก็ตโดยไม่ต้องใช้ปลั๊กอินของเบราว์เซอร์บุคคลที่สาม เช่น Adobe Flash Player อ่านต่อไปเพื่อดูว่าเกิดอะไรขึ้น

Adobe Flash เดิมชื่อ Macromedia 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 สำหรับฉันเท่านั้นที่มันจะเป็นเพียร์ - เซิร์ฟเวอร์ - เพียร์ แต่จะมีมากกว่านั้นในครั้งต่อไปเพราะมันยังไม่พร้อม

ในการเริ่มต้น เราจำเป็นต้องมีเซิร์ฟเวอร์เว็บซ็อกเก็ตจริง ฉันสร้างสิ่งที่ง่ายที่สุดโดยใช้แพ็คเกจ 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.

เราเข้าถึง (การอนุญาต) กล้อง/ไมโครโฟนผ่าน API อุปกรณ์สื่อ. API นี้มีวิธีการ MediaDevices.getUserMedia()ซึ่งแสดงป๊อปอัป หน้าต่างขออนุญาตจากผู้ใช้ในการเข้าถึงกล้องและ/หรือไมโครโฟน ฉันต้องการทราบว่าฉันทำการทดลองทั้งหมดใน Google Chrome แต่ฉันคิดว่าทุกอย่างจะทำงานเหมือนกันใน Firefox

ถัดไป 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 และสมัครรับเหตุการณ์ 'ข้อความ' หลังจากได้รับข้อมูลไบนารี่แล้ว เซิร์ฟเวอร์ของเราจะถ่ายทอดข้อมูลดังกล่าวไปยังสมาชิก ซึ่งก็คือ ลูกค้า ในกรณีนี้ ฟังก์ชันการโทรกลับที่เกี่ยวข้องกับ "listener" ของเหตุการณ์ 'ข้อความ' จะถูกทริกเกอร์บนไคลเอ็นต์ โดยตัววัตถุเองจะถูกส่งผ่านไปยังอาร์กิวเมนต์ของฟังก์ชัน - ชิ้นส่วนของสตรีมวิดีโอที่เข้ารหัสโดย 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>

เป็นเวลานานที่ฉันพยายามเข้าใจว่าทำไมจึงเป็นไปไม่ได้ที่จะส่งชิ้นส่วนที่ได้รับไปยังองค์ประกอบวิดีโอเพื่อเล่นทันที แต่กลับกลายเป็นว่าไม่สามารถทำได้แน่นอนคุณต้องใส่ชิ้นส่วนนั้นในบัฟเฟอร์พิเศษที่ผูกไว้กับ องค์ประกอบวิดีโอ จากนั้นจึงจะเริ่มเล่นสตรีมวิดีโอ สำหรับสิ่งนี้คุณจะต้องมี มีเดียซอร์ส API и FileReader API.

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 ฉันคิดว่าฉันจะเขียนบทความแยกต่างหากเกี่ยวกับผลการวิจัยของฉันหากพบว่าชุมชนสนใจ

ที่มา: will.com

เพิ่มความคิดเห็น