在 MediaSource HTML5 中播放 MediaRecorder 块——视频冻结

Posted

技术标签:

【中文标题】在 MediaSource HTML5 中播放 MediaRecorder 块——视频冻结【英文标题】:Play MediaRecorder chunks in MediaSource HTML5 -- video frozen 【发布时间】:2016-10-06 15:08:10 【问题描述】:

我有这个简单的代码来获取视频流块并在 MediaSource 中播放它们。我看到视频,但有时它会停止。它可能工作几秒钟或几分钟。但最终它会在某​​个时刻停止。 chrome://media-internals/ 没有显示错误。

这里有什么问题?

    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
var mediaSource = new MediaSource();
var constraints = 
    "audio": true,
    "video": 
        "mandatory": 
            "minWidth": 320, "maxWidth": 320,
            "minHeight": 240, "maxHeight": 240
        , "optional": []
    
;
window.mediaSource = mediaSource;
var sourceBuffer;
var video = document.querySelector('#video');
window.video = video;
video.src = window.URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', function (e) 
    console.log("sourceopen");
    sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"');
    window.sourceBuffer = sourceBuffer;
, false);
mediaSource.addEventListener('error', function (e) 
    console.log("error", e)
, false);
var stack = [];

video.play();
navigator.getUserMedia(constraints, function (stream) 
    console.log("stream", stream);
    mediaRecorder = new MediaRecorder(stream);
    mediaRecorder.ondataavailable = function (e) 
        var reader = new FileReader();
        reader.addEventListener("loadend", function () 
            var arr = new Uint8Array(reader.result);
            sourceBuffer.appendBuffer(arr);
        );
        reader.readAsArrayBuffer(e.data);
    ;
    mediaRecorder.start(100);
, function (e) 
    console.log(e)
);

这是 JSFIDDLE,它将尝试这样做: https://jsfiddle.net/stivyakovenko/fkt89cLu/6/ 我使用 Chrome 作为我的主要目标。

【问题讨论】:

您的示例适用于我的浏览器 UserAgent:“Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/50.0.2661.102 Safari/537.36" 只是时间问题。如果您等待足够长的时间(1-2 分钟,它会被冻结)。我的 chrome 完全一样。 我将它放置了 15 分钟以上,这很好,我还注意到,当我运行你的代码并运行我的代码时,我的代码也可以工作 link,但是当我关闭你的代码时它冻结了 我猜这是巧合。进行足够的尝试,我相信我的也会冻结/ @Steve,我确认打开这个 jsfiddle 会使我的本地主机工作更稳定 :)) 【参考方案1】:

看起来这是 Chrome 中的一个错误...

https://bugs.chromium.org/p/chromium/issues/detail?id=606000

【讨论】:

现在似乎已修复(我是该错误的报告者)。 更改 addSourceBuffer 以使用“opus,vp9”,因为这可能是 chrome 正在使用的(您可以在设置 MediaRecorder 时指定它)。 你还需要检查sourceBuffer.updating,如果是真的你需要等待,然后再将新数据添加到sourceBuffer。 @CpnCrunch 评论 81 被删除 添加 opus,vp9 不适用于 Chrome - 在 Version 80.0.3987.149 (Official Build) (64-bit) 上测试【参考方案2】:

媒体记录器会在 ondataavailable 回调中为您提供整个 webm 文件的一部分。看起来这种东西不适用于 mediaSource。它在我的 chrome 66 中根本无法工作。

这是一种在没有 ffmpeg 的情况下使用 MediaRecorder 进行“视频聊天”或“直播”的方式:

您可以使用 ajax 将该数据部分发送到您的服务器。 服务器可以一次将“整个 webm 文件”返回到您的 chrome 浏览器 长时间响应。一旦服务器从客户端获得一些数据,服务器就可以在该响应中返回更多数据部分。

而且这种工作方法也只适用于 html:

您可以使用 blob 列表来收集来自 ondataavailable 的所有 blob。 然后一次又一次地设置video.src。

这是一个有效的 jsfiddle:

const constraints = video: true;

const video1 = document.querySelector('.real1');
const video2 = document.querySelector('.real2');

var blobList = [];

var gCurrentTime = 0;
function playNew()
	gCurrentTime = video2.currentTime;
	var thisBlob = new Blob(blobList,type:"video/webm");
	var url = URL.createObjectURL(thisBlob);
	video2.src = url;
	video2.currentTime = gCurrentTime;
	video2.play();

video2.onended = playNew;

var isFirst = true;
function handleSuccess(stream) 
  video1.srcObject = stream;
  var mediaRecorder = new MediaRecorder(stream,mimeType:"video/webm");
  mediaRecorder.ondataavailable = function(e)
	blobList.push(e.data);
	if (isFirst)
		playNew();
		isFirst = false;
	
  
  mediaRecorder.start(1000);


function handleError(error) 
  console.error('Reeeejected!', error);

navigator.mediaDevices.getUserMedia(constraints).
  then(handleSuccess).catch(handleError);
<video class="real1" autoplay controls></video>
<video class="real2" controls></video>

https://jsfiddle.net/4akkadht/1/

仅 html 的解决方案(第二个)会一次又一次地闪烁并有很大的延迟。服务器长推解决方案(第一个)不会闪烁,有五秒延迟。

【讨论】:

【参考方案3】:

根据我使用 MediaRecorder 和 MediaSource 的经验,与视频冻结或返回错误相关的大多数错误可能是由于接收到的块不同步。我相信 webm(可能还​​有其他媒体类型)需要按照时间码的递增顺序接收块。记录、发送和接收块 Async 可能无法保留时间码的这种递增顺序。

所以,在上面分析了我自己使用 MediaRecorder/MediaSource 冻结视频的经验之后,我更改了我的代码以同步发送录制的块,而不是异步发送。

【讨论】:

【参考方案4】:

我也在尝试这样做,但是我根本没有收到任何视频。您的 jsfiddle 在 chrome 或 firefox 上对我不起作用(在 ubuntu 14.04 和 windows 7 上测试)。

经过一番研究(主要是在文件录制后流回文件),我发现该文件没有正确分段以供 MSE 播放。 @Steve:我很想知道您是如何使用 ffmpeg 完成分段的。

作为旁注,我在这里也有一个类似的问题:Display getUserMediaStream live video with media stream extensions (MSE),错误描述来自 chrome://media-internals。

【讨论】:

Vasile,如果 jsfiddle 不起作用,您是否也应该将此作为错误报告给 chrome 开发人员? 好吧,它在 Firefox 中不起作用的事实也让我认为它可能不是一个错误。我会看一下 mse 规范,看看我能想出什么。 @Vasilie,chrome 团队已将其视为错误,请参阅另一个答案。 是我报告了 chrome 错误,我可以确认它在 Firefox 中完美运行。请注意,上面的代码中有一个错误:它没有处理 sourcebuffer 繁忙的情况,因此 appendBuffer 可能会失败。【参考方案5】:

chrome 中的一个工作示例,但它在 firefox 中冻结

  const main = async(function* main()
  const logging = true;
  let tasks = Promise.resolve(void 0);

  const devices = yield navigator.mediaDevices.enumerateDevices();
  console.table(devices);

  const stream = yield navigator.mediaDevices.getUserMedia(video: true, audio: true);
  if(logging)
    stream.addEventListener("active", (ev)=> console.log(ev.type); );
    stream.addEventListener("inactive", (ev)=> console.log(ev.type); );
    stream.addEventListener("addtrack", (ev)=> console.log(ev.type); );
    stream.addEventListener("removetrack", (ev)=> console.log(ev.type); );
  

  const rec = new MediaRecorder(stream, mimeType: 'video/webm; codecs="opus,vp8"');
  if(logging)
    rec.addEventListener("dataavailable", (ev)=> console.log(ev.type); );
    rec.addEventListener("pause", (ev)=> console.log(ev.type); );
    rec.addEventListener("resume", (ev)=> console.log(ev.type); );
    rec.addEventListener("start", (ev)=> console.log(ev.type); );
    rec.addEventListener("stop", (ev)=> console.log(ev.type); );
    rec.addEventListener("error", (ev)=> console.error(ev.type, ev); );
  

  const ms = new MediaSource();
  if(logging)
    ms.addEventListener('sourceopen', (ev)=> console.log(ev.type); );
    ms.addEventListener('sourceended', (ev)=> console.log(ev.type); );
    ms.addEventListener('sourceclose', (ev)=> console.log(ev.type); );
    ms.sourceBuffers.addEventListener('addsourcebuffer', (ev)=> console.log(ev.type); );
    ms.sourceBuffers.addEventListener('removesourcebuffer', (ev)=> console.log(ev.type); );
  

  const video = document.createElement("video");
  if(logging)
    video.addEventListener('loadstart', (ev)=> console.log(ev.type); );
    video.addEventListener('progress', (ev)=> console.log(ev.type); );
    video.addEventListener('loadedmetadata', (ev)=> console.log(ev.type); );
    video.addEventListener('loadeddata', (ev)=> console.log(ev.type); );
    video.addEventListener('canplay', (ev)=> console.log(ev.type); );
    video.addEventListener('canplaythrough', (ev)=> console.log(ev.type); );
    video.addEventListener('playing', (ev)=> console.log(ev.type); );
    video.addEventListener('waiting', (ev)=> console.log(ev.type); );
    video.addEventListener('seeking', (ev)=> console.log(ev.type); );
    video.addEventListener('seeked', (ev)=> console.log(ev.type); );
    video.addEventListener('ended', (ev)=> console.log(ev.type); );
    video.addEventListener('emptied', (ev)=> console.log(ev.type); );
    video.addEventListener('stalled', (ev)=> console.log(ev.type); );
    video.addEventListener('timeupdate', (ev)=> console.log(ev.type); ); // annoying
    video.addEventListener('durationchange', (ev)=> console.log(ev.type); );
    video.addEventListener('ratechange', (ev)=> console.log(ev.type); );
    video.addEventListener('play', (ev)=> console.log(ev.type); );
    video.addEventListener('pause', (ev)=> console.log(ev.type); );
    video.addEventListener('error', (ev)=> console.warn(ev.type, ev); );
  
  //video.srcObject = ms;
  video.src = URL.createObjectURL(ms);
  video.volume = 0;
  video.controls = true;
  video.autoplay = true;
  document.body.appendChild(video);

  yield new Promise((resolve, reject)=>
    ms.addEventListener('sourceopen', ()=> resolve(), once: true);
  );

  const sb = ms.addSourceBuffer(rec.mimeType);
  if(logging)
    sb.addEventListener('updatestart', (ev)=> console.log(ev.type); ); // annoying
    sb.addEventListener('update', (ev)=> console.log(ev.type); ); // annoying
    sb.addEventListener('updateend', (ev)=> console.log(ev.type); ); // annoying
    sb.addEventListener('error', (ev)=> console.error(ev.type, ev); );
    sb.addEventListener('abort', (ev)=> console.log(ev.type); );
    

  const stop = async(function* stop()
    console.info("stopping");
    if(sb.updating) sb.abort(); 
    if(ms.readyState === "open") ms.endOfStream(); 
    rec.stop();
    stream.getTracks().map((track)=> track.stop(); );
    yield video.pause();
    console.info("end");
  );

  const button = document.createElement("button");
  button.innerHTML = "stop";
  button.addEventListener("click", ()=>
    document.body.removeChild(button);
    tasks = tasks.then(stop);
  , once: true);
  document.body.appendChild(button);

  let i = 0;
  rec.ondataavailable = (data)=>
    tasks = tasks.then(async(function*()
        console.group(""+i);

      try
        if(logging) console.log("dataavailable", "size:", data.size); 

        if(data.size === 0)
          console.warn("empty recorder data");
          throw new Error("empty recorder data");
        

        const buf = yield readAsArrayBuffer(data);

        sb.appendBuffer(buf);
        yield new Promise((resolve, reject)=>
          sb.addEventListener('updateend', ()=> resolve(), once: true);
          sb.addEventListener("error", (err)=> reject(ev), once: true);
        );

                if(logging)
          console.log("timestampOffset", sb.timestampOffset);
          console.log("appendWindowStart", sb.appendWindowStart);
          console.log("appendWindowEnd", sb.appendWindowEnd);
          for(let i=0; i<sb.buffered.length; i++)
            console.log("buffered", i, sb.buffered.start(i), sb.buffered.end(i));
          
          for(let i=0; i<video.seekable.length; i++)
            console.log("seekable", i, video.seekable.start(i), video.seekable.end(i));
          
          console.log("webkitAudioDecodedByteCount", video.webkitAudioDecodedByteCount);
          console.log("webkitVideoDecodedByteCount", video.webkitVideoDecodedByteCount);
          console.log("webkitDecodedFrameCount", video.webkitDecodedFrameCount);
          console.log("webkitDroppedFrameCount", video.webkitDroppedFrameCount);
        

        if (video.buffered.length > 1) 
          console.warn("MSE buffered has a gap!");
          throw new Error("MSE buffered has a gap!");
        
      catch(err)
          console.error(err);
        yield stop();
        console.groupEnd(""+i); i++;
        return Promise.reject(err);
      

      console.groupEnd(""+i);
      i++;
    ));
  ;

  rec.start(1000);
  console.info("start");
);



function sleep(ms)
  return new Promise(resolve =>
    setTimeout((()=>resolve(ms)), ms));



function readAsArrayBuffer(blob) 
  return new Promise((resolve, reject)=>
    const reader = new FileReader();
    reader.addEventListener("loadend", ()=> resolve(reader.result), once: true);
    reader.addEventListener("error", (err)=> reject(err.error), once: true);
    reader.readAsArrayBuffer(blob);
  );



function async(generatorFunc)
  return function (arg) 
    const generator = generatorFunc(arg);
    return next(null);
    function next(arg) 
      const result = generator.next(arg);
      if(result.done) return result.value; 
      else if(result.value instanceof Promise) return result.value.then(next); 
      else return Promise.resolve(result.value); 
    
  


console.clear();
main().catch(console.error);

https://jsfiddle.net/nthyfgvs/

【讨论】:

以上是关于在 MediaSource HTML5 中播放 MediaRecorder 块——视频冻结的主要内容,如果未能解决你的问题,请参考以下文章

如何下载(或以 blob 形式获取)用于流式传输 HTML5 视频的 MediaSource?

MediaSource API:视频无法播放

尝试将 MediaSource 对象附加为 HTML5 视频标签的源时出现“不允许加载本地资源”错误

如何使用 Blob URL、MediaSource 或其他方法播放连接的媒体片段 Blob?

HTML5 MediaSource 适用于某些 mp4 文件,而不适用于其他文件(相同的编解码器)

如何保持实时 MediaSource 视频流同步?