(几乎)无用的网络摄像头从浏览器流式传输。 媒体流和 Websocket

在本文中,我想分享我在不使用 Adob​​e Flash Player 等第三方浏览器插件的情况下通过 Websocket 流式传输视频的尝试。 请继续阅读以了解结果。

Adobe Flash(以前称为 Macromedia Flash)是一个用于创建在 Web 浏览器中运行的应用程序的平台。 在引入 Media Stream API 之前,它实际上是唯一用于从网络摄像头传输视频和语音以及在浏览器中创建各种会议和聊天的平台。 传输媒体信息的协议RTMP(实时消息协议)实际上已经关闭了很长一段时间,这意味着:如果你想提升你的流媒体服务,请善意地使用Adobe自己的软件——Adobe Media Server (AMS)。

2012 年一段时间后,Adobe 向公众“放弃并吐出了它”。 规格 RTMP 协议包含错误并且本质上不完整。 到那时,开发人员开始自己实现该协议,Wowza 服务器出现了。 2011年,Adobe对Wowza提起诉讼,指控其非法使用RTMP相关专利;4年后,冲突得到友好解决。

Adobe Flash 平台已有 20 多年的历史,在此期间发现了许多严重漏洞,支持 许诺 到 2020 年结束,流媒体服务几乎没有其他选择。

对于我的项目,我立即决定完全放弃在浏览器中使用 Flash。 我在上面指出了主要原因;移动平台上根本不支持Flash,而且我真的不想在Windows(wine模拟器)上部署Adobe Flash进行开发。 所以我开始用 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)
}

在客户端(流媒体端),您首先需要访问摄像头。 这是通过 媒体流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>

要通过套接字广播视频流,您需要在某处对其进行编码、缓冲并分段传输。 原始视频流无法通过 websocket 传输。 这就是我们需要帮助的地方 媒体记录器 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 添加传输。 令人惊讶的是,你所需要的只是一个对象 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 对象并订阅“消息”事件。 收到一段二进制数据后,我们的服务器将其广播给订阅者,即客户端。 在这种情况下,与“消息”事件的“侦听器”关联的回调函数在客户端被触发;对象本身被传递到函数参数中 - 由 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>

很长一段时间我试图理解为什么不可能立即将接收到的片段发送到视频元素进行播放,但事实证明这是无法做到的,当然,你必须首先将片段放入绑定到的特殊缓冲区中video 元素,只有这样它才会开始播放视频流。 为此你需要 媒体源API и 文件阅读器 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>

流媒体服务的原型已经准备就绪。 主要缺点是视频播放会滞后于发送端100ms;我们在将视频流传输到服务器之前分割视频流时自己设置这个。 而且,当我检查我的笔记本电脑时,发送端和接收端之间的延迟逐渐累积,这一点清晰可见。 我开始寻找克服这个缺点的方法,并且......遇到了 RTCPeerConnection API,它允许您传输视频流,而无需使用诸如将流分割成片段之类的技巧。 我认为,累积的延迟是由于浏览器在传输之前将每个片段重新编码为 webm 格式造成的。 我没有进一步挖掘,而是开始研究 WebRTC。如果我发现社区对我的研究结果感兴趣,我想我会写一篇单独的文章。

来源: habr.com

添加评论