Firefox 的 MediaRecorder 接口仅每两秒提供一次新的视频数据

Posted

技术标签:

【中文标题】Firefox 的 MediaRecorder 接口仅每两秒提供一次新的视频数据【英文标题】:Firefox's MediaRecorder interface only providing new video data every two seconds 【发布时间】:2014-08-31 20:32:13 【问题描述】:

我正在尝试将通过 WebRTC 的 getUserMedia() 方法获得的视频流发送到服务器以进行额外处理。延迟很重要,因为我希望检测视频流的变化并立即更新客户端。对于这种特殊用途,仅 Firefox 的解决方案是可以接受的,因此我正在研究MediaRecorder 接口。

我整理了一个简单的测试用例,如下所示。没有错误,并且按预期每 500 毫秒调用一次 ondataavailable 回调。但是,对于四分之三的调用,提供的数据大小为零。这向我表明,数据被分组为大约两秒的块(可能是由于所使用的视频编码的限制)。

是否可以让 MediaRecorder 以更精细的粒度提供数据?如果不是,那么以低延迟将视频数据从 userMedia 流获取到服务器的最佳方法是什么?一个特定于 Chrome 或 Firefox 的界面会很好,但同时在这两种情况下都可以使用的界面会更好。

<html>
  <body>
    <h1>MediaRecorder Test</h1>
    <video id="video"  style="border: 1px solid black"></video>
  </body>
</html>

<script>
 // The variable that holds the video stream
 var mediastream = null;

 // Start video capture (and provide a way to stop it)
 navigator.mozGetUserMedia (  video: true, audio: false ,
   function(stream_arg) 
     mediastream = stream_arg;
     var vendorURL = window.URL || window.webkitURL;
     video.src = vendorURL.createObjectURL(mediastream);
     video.play();
     recordStream();
   ,
   function(err)  console.log("Error starting video stream: " + err); 
 );

 // Record the stream
 var recorder = null;
 function recordStream() 
   recorder = new MediaRecorder(mediastream);
   recorder.ondataavailable = function(ev) 
     console.log("Got: "+ev.data.size);
   ;
   recorder.start(500);
 
</script>

【问题讨论】:

我应该在原始问题中提到,我们当前的方法是使用画布捕获图像并使用 XHR 将这些图像发送到服务器(如下面 CuriousGuy 的回答中详细描述的)。使用 MediaRecorder 的目的是减少传输的数据量,从而使我们能够支持更高的帧速率。我也在考虑在 javascript 中做某种视频编码,但是这个问题主要是为了看看我们是否可以利用浏览器现有的编码。 【参考方案1】:

您传递给 MediaRecorder 的 500 毫秒间隔是建议性的。使用的编解码器可能需要更大的数据块/时间才能使用。它可能会尽可能快地为您提供数据。

如果您需要低延迟,MediaRecorder 是不适合这项工作的工具。常规 WebRTC 调用将使用优化延迟而不是质量的编解码器设置。我听说有人在服务器端录制 WebRTC,但我不知道有什么开源可以做到这一点。

也许在未来的某一天,MediaRecorder API 将允许我们选择编解码器参数,这不会成为问题。

【讨论】:

谢谢;这证实了我的想法。运行 WebRTC 端点是可能的,但据我所见,很难将各个部分分开——尤其是路由部分,它不适合我们的应用程序。从好的方面来说,我们发现捕获和发送单个帧(如 Curious 的回答和我的 cmets 中所述)足以满足我们的需要。 @RobHague 我可能会建议通过二进制 websocket 发送您的数据,而不是通过 base64 编码的方式发送数据。 BinaryJS 提供了一种很好的方法来获取可以写入的常规 Node.js 样式流。我已经将它与其他一些 NPM 模块一起使用,以这种方式多路复用多个流,取得了良好的成功和常规二进制数据的效率。 我们目前使用 Blob 和 FormData 以二进制形式发送数据(JPEG 编码)。但是,如果我们提高帧速率,我们可能会切换到 websocket 以减少开销。【参考方案2】:

您可以使用另一种方法:每 N 毫秒将视频发送到(可选隐藏)画布,该画布允许获取图像的 base64 表示。因此,您将获得一个 base64 帧数组。现在你有两个选择:

以 base64 格式将每个帧发送到服务器。由于 base64 是常规字符串,这是最简单的方法; 将每个base64帧转换为Blob并通过FormData发送到服务器。就我而言,以这种方式上传速度是第一种方式的两倍。

您可以在下面看到我的示例(执行第二个选项)。这个例子很大,但是每个部分都很重要。

index.html:

<!DOCTYPE html>
<html>
<head>
<script src="record-test.js"></script>
</head>
<body>

    <video id="video"></video>
    <canvas id="canvas" style="display:none;"></canvas>
    <input type="button" id="stopRecordBtn" value="Stop recording">

</body>
</html>

record-test.js:

(function() 
    'use strict';

    //you can play with these settings
    var FRAME_INTERVAL_MS = 500;  //take snapshot each 500 ms
    var FRAME_WIDTH = 320;    //width and
    var FRAME_HEIGHT = 240;   //height of resulting frame

    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    window.URL = window.URL || window.webkitURL;

    var video, canvas, ctx;
    var mediaStream;
    var videoRecordItv;
    var base64Frames = [];

    var init = function() 
        video = document.getElementById('video');

        canvas = document.getElementById('canvas'); //use canvas to capture a frame and convert it to base64 data
        canvas.width = FRAME_WIDTH;
        canvas.height = FRAME_HEIGHT;
        ctx = canvas.getContext('2d');

        var stopBtn = document.getElementById('stopRecordBtn');
        stopBtn.addEventListener('click', stopRecording);

        navigator.getUserMedia(video: true, onGotStream, function(e) console.log(e););
    

    var onGotStream = function(stream) 
        mediaStream = stream;
        video.src = URL.createObjectURL(mediaStream);
        video.play();

        videoRecordItv = setInterval(function()   //capture a frame each FRAME_INTERVAL_MS milliseconds
            var frame = getBase64FrameFromVideo();
            base64Frames.push(frame);
        , FRAME_INTERVAL_MS);
    

    var getBase64FrameFromVideo = function() 
        ctx.drawImage(video, 0, 0, FRAME_WIDTH, FRAME_HEIGHT);
        //a canvas snapshot looks like data:image/jpeg;base64,ACTUAL_DATA_HERE
        //we need to cut out first 22 characters:
        var base64PrefixLength = 'data:image/jpeg;base64,'.length;
        return canvas.toDataURL('image/jpeg').slice(base64PrefixLength);
    

    var stopRecording = function() 
        mediaStream && mediaStream.stop && mediaStream.stop();
        mediaStream = null;
        clearInterval(videoRecordItv);  //stop capturing video

        uploadFramesToServer();
    

    var uploadFramesToServer = function() 
        var sid = Math.random(); //generate unique id
        var curFrameIdx = 0;  //current frame index
        (function postFrame() 
            console.log('post frame #' + curFrameIdx);

            var base64Frame = base64Frames[curFrameIdx];
            var blobFrame = base64ToBlob(base64Frame, 'image/jpeg');
            var formData = new FormData;
            formData.append('frame', blobFrame, 'upload.jpg');
            formData.append('sid', sid);
            var xhr = new XMLHttpRequest();
            //post a single frame to /postFrame url with multipart/form-data enctype
            //on the server you get "sid" param and "frame" file as you would post a file with regular html form
            xhr.open('POST', '/postFrame', true);
            xhr.onload = function(e) 
                console.log(this.response);
                if (base64Frames[++curFrameIdx]) 
                    postFrame(); //post next frame
                 else 
                    //DONE!
                    console.log('finish post frames');
                
            ;
            xhr.send(formData);
        )();
    

    var base64ToBlob = function(base64Data, contentType, sliceSize) 
        contentType = contentType || '';
        sliceSize = sliceSize || 512;

        var byteCharacters = atob(base64Data);
        var byteArrays = [];

        for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) 
            var slice = byteCharacters.slice(offset, offset + sliceSize);

            var byteNumbers = new Array(slice.length);
            for (var i = 0; i < slice.length; i++) 
                byteNumbers[i] = slice.charCodeAt(i);
            

            var byteArray = new Uint8Array(byteNumbers);

            byteArrays.push(byteArray);
        

        return new Blob(byteArrays, type: contentType);
    

    document.addEventListener('DOMContentLoaded', init);
)();

在服务器端,您仍然需要执行一些操作,例如,使用 FFmpeg 从这些帧创建视频。

这种方法适用于 Chrome 和 Firefox。

希望这会有所帮助。对不起我的英语,祝你好运!

【讨论】:

感谢您的广泛回复。正如我应该在最初的问题中提到的那样,这基本上就是我们现在正在做的事情。但是,当以视频帧速率完成时,这会占用大量带宽。使用 MediaRecorder 或类似工具的好处是能够在给定带宽下支持更高的帧速率。

以上是关于Firefox 的 MediaRecorder 接口仅每两秒提供一次新的视频数据的主要内容,如果未能解决你的问题,请参考以下文章

锚链接内的按钮在 Firefox 中有效,但在 Internet Explorer 中无效?

android MediaRecorder录制音频

Android 10 源码MediaRecorder 录像流程:MediaRecorder 配置

使用MediaRecorder录制音频

Android-MediaRecorder-音频录制-警告-W/MediaRecorder(13811): mediarecorder went away with unhandled events(

MediaRecorder.stop() 停止失败:-1007