我如何制作(几乎)无用的Javascript网络摄像头流

在本文中,我想分享我尝试通过websocket传输视频而不使用第三方浏览器插件(如Adobe Flash Player)的尝试。 继续读下去。

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

在2012年的一段时间后,Adobe“放弃并放弃了” RTMP协议的规范 ,其中包含错误,但实际上还不完整。 到那时,开发人员开始实现该协议的实现,因此Wowza服务器出现了。 2011年,Adobe在Wowza提起诉讼,指控其非法使用与RTMP相关的专利,四年后,这一冲突被全世界解决。

Adobe Flash平台已经存在20多年了,在这段时间内发现了许多关键漏洞,他们承诺到2020年将停止支持,因此流媒体服务的替代方案并不多。

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


在客户端(广播端),您必须首先访问摄像机。 这是通过MediaStream API完成的。

我们可以通过媒体设备API来访问(分辨率)摄像机/麦克风。 该API提供了MediaDevices.getUserMedia()方法,该方法显示了一个弹出窗口。 询问用户允许访问摄像头和/或麦克风的窗口。 我想指出的是,我在Google Chrome浏览器中进行了所有实验,但我认为,在Firefox中,所有工作原理都大致相同。

接下来,getUserMedia()返回一个Promise,将MediaStream对象传递到该Promise中-视频和音频数据流。 我们将此对象分配给src中的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'); //   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> 


现在,在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对象上挂起一个“侦听器”,订阅“​​消息”事件。 接收到二进制数据后,我们的服务器将其广播给订户,即客户端。 同时,与“消息”事件的“侦听器”相关的回调函数在客户端上触发,对象本身被传递给函数参数-由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元素进行播放,但是事实证明这是不可能的,当然,您必须首先将片段放入附在video元素上的特殊缓冲区中,然后才可以开始播放视频流。 为此,您需要MediaSource APIFileReader 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 ,该API允许您传输视频流而没有诸如将流分成几部分的技巧。 我认为累积的滞后是由于以下事实:在浏览器中,在传输之前,每段代码都已转码为webm格式。 我不再进行深入研究,而是开始研究WebRTC,考虑到我的研究结果,如果发现这个社区很有趣,我将另写文章。

Source: https://habr.com/ru/post/zh-CN461769/


All Articles