(Hầu như) phát trực tuyến webcam vô dụng từ trình duyệt. Luồng phương tiện và ổ cắm web

Trong bài viết này, tôi muốn chia sẻ những nỗ lực của mình để truyền phát video qua websockets mà không cần sử dụng plugin trình duyệt của bên thứ ba như Adobe Flash Player. Đọc tiếp để tìm hiểu những gì đã xảy ra.

Adobe Flash, trước đây là Macromedia Flash, là một nền tảng để tạo các ứng dụng chạy trên trình duyệt web. Trước khi giới thiệu API Media Stream, đây thực tế là nền tảng duy nhất để truyền phát video và giọng nói từ webcam cũng như để tạo nhiều loại hội nghị và trò chuyện khác nhau trong trình duyệt. Giao thức truyền thông tin đa phương tiện RTMP (Giao thức nhắn tin thời gian thực) thực sự đã bị đóng từ lâu, điều đó có nghĩa là: nếu bạn muốn phát triển dịch vụ phát trực tuyến của mình, hãy vui lòng sử dụng phần mềm từ chính Adobe - Adobe Media Server (AMS).

Sau một thời gian vào năm 2012, Adobe đã “từ bỏ và công khai” với công chúng. sự chỉ rõ Giao thức RTMP có lỗi và về cơ bản là chưa hoàn thiện. Vào thời điểm đó, các nhà phát triển bắt đầu triển khai giao thức này của riêng họ và máy chủ Wowza xuất hiện. Năm 2011, Adobe đã đệ đơn kiện Wowza vì sử dụng trái phép các bằng sáng chế liên quan đến RTMP; sau 4 năm, xung đột đã được giải quyết một cách thân thiện.

Nền tảng Adobe Flash đã hơn 20 năm tuổi, trong thời gian đó nhiều lỗ hổng nghiêm trọng đã được phát hiện, hỗ trợ đã hứa kết thúc vào năm 2020, để lại một số lựa chọn thay thế cho dịch vụ phát trực tuyến.

Đối với dự án của mình, tôi ngay lập tức quyết định từ bỏ hoàn toàn việc sử dụng Flash trong trình duyệt. Tôi đã nêu lý do chính ở trên; Flash cũng không được hỗ trợ trên nền tảng di động và tôi thực sự không muốn triển khai Adobe Flash để phát triển trên Windows (trình giả lập rượu vang). Vì vậy, tôi bắt đầu viết một ứng dụng khách bằng JavaScript. Đây sẽ chỉ là một nguyên mẫu, vì sau này tôi biết rằng việc phát trực tuyến có thể được thực hiện hiệu quả hơn nhiều dựa trên p2p, chỉ đối với tôi nó sẽ là ngang hàng - máy chủ - ngang hàng, nhưng sẽ còn nhiều hơn thế vào lúc khác, vì nó vẫn chưa sẵn sàng.

Để bắt đầu, chúng tôi cần máy chủ websockets thực tế. Tôi đã thực hiện cách đơn giản nhất dựa trên gói giai điệu:

Mã máy chủ

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

Trên máy khách (phía phát trực tuyến), trước tiên bạn cần truy cập vào máy ảnh. Điều này được thực hiện thông qua API MediaStream.

Chúng tôi có quyền truy cập (quyền) vào máy ảnh/micrô thông qua API thiết bị đa phương tiện. API này cung cấp một phương thức MediaDevices.getUserMedia(), hiển thị cửa sổ bật lên. một cửa sổ yêu cầu người dùng cấp quyền truy cập vào máy ảnh và/hoặc micrô. Tôi muốn lưu ý rằng tôi đã thực hiện tất cả các thử nghiệm trong Google Chrome, nhưng tôi nghĩ mọi thứ sẽ hoạt động tương tự trong Firefox.

Tiếp theo, getUserMedia() trả về một Promise, qua đó nó chuyển một đối tượng MediaStream - một luồng dữ liệu âm thanh video. Chúng ta gán đối tượng này cho thuộc tính src của thành phần video. Mã số:

Bên phát sóng

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

Để phát luồng video qua ổ cắm, bạn cần mã hóa luồng video đó ở đâu đó, lưu vào bộ đệm và truyền theo từng phần. Luồng video thô không thể được truyền qua ổ cắm web. Đây là nơi chúng tôi hỗ trợ API MediaRecorder. API này cho phép bạn mã hóa và chia luồng thành nhiều phần. Tôi mã hóa để nén luồng video nhằm gửi ít byte hơn qua mạng. Sau khi chia nó thành nhiều phần, bạn có thể gửi từng phần tới một websocket. Mã số:

Chúng tôi mã hóa luồng video, chia nó thành nhiều phần

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

Bây giờ hãy thêm đường truyền qua websockets. Đáng ngạc nhiên là tất cả những gì bạn cần cho việc này là một đồ vật WebSocket. Nó chỉ có hai phương thức gửi và đóng. Những cái tên nói lên chính họ. Đã thêm mã:

Chúng tôi truyền luồng video đến máy chủ

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

Phía phát sóng đã sẵn sàng! Bây giờ hãy thử nhận một luồng video và hiển thị nó trên máy khách. Chúng ta cần gì cho việc này? Đầu tiên, tất nhiên là kết nối ổ cắm. Chúng tôi đính kèm một “người nghe” vào đối tượng WebSocket và đăng ký sự kiện 'tin nhắn'. Sau khi nhận được một phần dữ liệu nhị phân, máy chủ của chúng tôi sẽ phát nó tới những người đăng ký, tức là khách hàng. Trong trường hợp này, chức năng gọi lại được liên kết với “người nghe” của sự kiện 'tin nhắn' được kích hoạt trên máy khách; chính đối tượng đó được chuyển vào đối số của hàm - một phần của luồng video được mã hóa bởi vp8.

Chúng tôi chấp nhận luồng video

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

Trong một thời gian dài, tôi đã cố gắng hiểu tại sao không thể gửi ngay đoạn đã nhận đến phần tử video để phát lại, nhưng hóa ra điều này không thể thực hiện được, tất nhiên, trước tiên bạn phải đặt đoạn đó vào một bộ đệm đặc biệt được ràng buộc với phần tử video và chỉ khi đó nó mới bắt đầu phát luồng video. Đối với điều này bạn sẽ cần API MediaSource и API FileReader.

MediaSource hoạt động như một loại trung gian giữa đối tượng phát lại phương tiện và nguồn của luồng phương tiện này. Đối tượng MediaSource chứa bộ đệm có thể cắm được cho nguồn của luồng video/âm thanh. Một tính năng là bộ đệm chỉ có thể chứa dữ liệu Uint8, vì vậy bạn sẽ cần FileReader để tạo bộ đệm như vậy. Nhìn vào mã và nó sẽ trở nên rõ ràng hơn:

Phát luồng video

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

Nguyên mẫu của dịch vụ phát trực tuyến đã sẵn sàng. Nhược điểm chính là việc phát lại video sẽ chậm hơn phía truyền 100 ms; chúng tôi tự thiết lập điều này khi tách luồng video trước khi truyền nó đến máy chủ. Hơn nữa, khi tôi kiểm tra trên laptop, độ trễ giữa bên truyền và bên nhận dần dần tích tụ, điều này có thể thấy rõ. Tôi bắt đầu tìm cách khắc phục nhược điểm này và… tình cờ gặp được API kết nối RTCPeer, cho phép bạn truyền một luồng video mà không cần thủ thuật như chia luồng thành nhiều phần. Tôi nghĩ độ trễ tích lũy là do trình duyệt mã hóa lại từng phần thành định dạng webm trước khi truyền. Tôi không đào sâu thêm nữa mà bắt đầu nghiên cứu WebRTC. Tôi nghĩ mình sẽ viết một bài riêng về kết quả nghiên cứu của mình nếu thấy cộng đồng thấy thú vị.

Nguồn: www.habr.com

Thêm một lời nhận xét