2

我正在使用MediaRecorder API在 Web 应用程序中录制视频。该应用程序可以选择在相机和屏幕之间切换。我正在使用 Canvas 来增强流记录。该逻辑涉及从相机捕获流并将其重定向到视频元素。然后在画布上渲染此视频,并将来自画布的流传递到MediaRecorder. 我注意到的是,只要用户不切换/最小化 chrome 窗口,从屏幕切换到视频(反之亦然)就可以正常工作。画布渲染使用requestAnimationFrame并在选项卡失去焦点后冻结。

有没有办法指示 chrome 不要暂停执行requestAnimationFrame?有没有其他方法可以在不影响MediaRecorder录制的情况下切换流?

更新: 通读文档后,播放音频或具有活动 websocket 连接的选项卡不会受到限制。这是我们目前没有做的事情。这可能是一种解决方法,但希望社区提供任何替代解决方案。(setTimeout 或 setInterval 过于节流,因此不使用它,而且它会影响渲染质量)

更新 2: 我可以使用 Worker 解决这个问题。工作人员调用 API 并通过 postMessage 将通知发送到主线程,而不是为 requestAnimationFrame 使用主 UI 线程。UI Thread 完成渲染后,会向 Worker 发送一条消息。还有一个增量周期计算来限制来自工人的压倒性消息。

4

2 回答 2

2

正在提议向 MediaRecorder API 添加一个.replaceTrack()方法,但目前,规范仍为阅读

如果在任何时候,将轨道添加到流的轨道集中或从流的轨道集中删除,UA 必须立即停止收集数据,丢弃它收集的任何数据 [...]

这就是实施的。


所以我们仍然必须依靠黑客来自己制作这个......

最好的可能是创建一个本地RTC连接,并记录接收端。

// creates a mixable stream
async function mixableStream( initial_track ) {
  
  const source_stream = new MediaStream( [] );
  const pc1 = new RTCPeerConnection();
  const pc2 = new RTCPeerConnection();
    pc1.onicecandidate = (evt) => pc2.addIceCandidate( evt.candidate );
    pc2.onicecandidate = (evt) => pc1.addIceCandidate( evt.candidate );

  const wait_for_stream = waitForEvent( pc2, 'track')
    .then( evt => new MediaStream( [ evt.track ] ) );

    pc1.addTrack( initial_track, source_stream );
  
  await waitForEvent( pc1, 'negotiationneeded' );
  try {
    await pc1.setLocalDescription( await pc1.createOffer() );
    await pc2.setRemoteDescription( pc1.localDescription );
    await pc2.setLocalDescription( await pc2.createAnswer() );
    await pc1.setRemoteDescription( pc2.localDescription );
  } catch ( err ) {
    console.error( err );
  }
  
  return {
    stream: await wait_for_stream,
    async replaceTrack( new_track ) {
      const sender = pc1.getSenders().find( ( { track } ) => track.kind == new_track.kind );
      return sender && sender.replaceTrack( new_track ) ||
        Promise.reject( "no such track" );
    }
  }  
}


{ // remap unstable FF version
  const proto = HTMLMediaElement.prototype;
  if( !proto.captureStream ) { proto.captureStream = proto.mozCaptureStream; }
}

waitForEvent( document.getElementById( 'starter' ), 'click' )
  .then( (evt) => evt.target.parentNode.remove() )
  .then( (async() => {

  const urls = [
    "2/22/Volcano_Lava_Sample.webm",
    "/a/a4/BBH_gravitational_lensing_of_gw150914.webm"
  ].map( (suffix) => "https://upload.wikimedia.org/wikipedia/commons/" + suffix );
  
  const switcher_btn = document.getElementById( 'switcher' );
  const stop_btn =     document.getElementById( 'stopper' );
  const video_out =    document.getElementById( 'out' );
  
  let current = 0;
  
  // see below for 'recordVid'
  const video_tracks = await Promise.all( urls.map( (url, index) =>  getVideoTracks( url ) ) );
  
  const mixable_stream = await mixableStream( video_tracks[ current ].track );

  switcher_btn.onclick = async (evt) => {

    current = +!current;
    await mixable_stream.replaceTrack( video_tracks[ current ].track );
    
  };

  // final recording part below

  // only for demo, so we can see what happens now
  video_out.srcObject = mixable_stream.stream;

  const rec = new MediaRecorder( mixable_stream.stream );
  const chunks = [];

  rec.ondataavailable = (evt) => chunks.push( evt.data );
  rec.onerror = console.log;
  rec.onstop = (evt) => {

    const final_file = new Blob( chunks );
    video_tracks.forEach( (track) => track.stop() );
    // only for demo, since we did set its srcObject
    video_out.srcObject = null;
    video_out.src = URL.createObjectURL( final_file );
    switcher_btn.remove();
    stop_btn.remove();

        const anchor = document.createElement( 'a' );
    anchor.download = 'file.webm';
    anchor.textContent = 'download';
        anchor.href = video_out.src;
    document.body.prepend( anchor );
    
  };

  stop_btn.onclick = (evt) => rec.stop();

  rec.start();
      
}))
.catch( console.error )

// some helpers below



// returns a video loaded to given url
function makeVid( url ) {

  const vid = document.createElement('video');
  vid.crossOrigin = true;
  vid.loop = true;
  vid.muted = true;
  vid.src = url;
  return vid.play()
    .then( (_) => vid );
  
}

/* Records videos from given url
** @method stop() ::pauses the linked <video>
** @property track ::the video track
*/
async function getVideoTracks( url ) {
  const player = await makeVid( url );
  const track = player.captureStream().getVideoTracks()[ 0 ];
  
  return {
    track,
    stop() { player.pause(); }
  };
}

// Promisifies EventTarget.addEventListener
function waitForEvent( target, type ) {
  return new Promise( (res) => target.addEventListener( type, res, { once: true } ) );
}
video { max-height: 100vh; max-width: 100vw; vertical-align: top; }
.overlay {
  background: #ded;
  position: fixed;
  z-index: 999;
  height: 100vh;
  width: 100vw;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
<div class="overlay">
  <button id="starter">start demo</button>
</div>
<button id="switcher">switch source</button>
<button id="stopper">stop recording</button> 
<video id="out" muted controls autoplay></video>


否则,您仍然可以使用画布方式,使用我为页面模糊时制作的Web 音频计时器,即使这在 Firefox 中不起作用,因为它们在内部连接到 rAF 以在记录器中推送新帧......

于 2020-01-22T08:01:32.190 回答
1

我遇到了同样的问题,并试图在没有太多复杂性的情况下解决它,例如 Canvas 或 SourceBuffer。

我使用 PeerConnection 为同一页面建立连接。建立连接后,您可以通过 peerconnection.addTrack 使用 rtpSender 并从那里轻松切换。

我刚刚制作了一个库和一个演示,您可以找到: https ://github.com/meething/StreamSwitcher/

于 2020-06-16T10:59:58.753 回答