MediaRecorder переключает видео треки - PullRequest
2 голосов
/ 21 января 2020

Я использую MediaRecorder API для записи видео в веб-приложениях. В приложении есть возможность переключения между камерой и экраном. Я использую Canvas для увеличения потоковой записи. Логика c включает захват потока с камеры и перенаправление его на элемент видео. Это видео затем отображается на холсте, и поток с холста передается в MediaRecorder. Я заметил, что переключение с экрана на видео (и наоборот) работает нормально, пока пользователь не переключает / минимизирует окно chrome. Для рендеринга холста используется requestAnimationFrame, и он останавливается после того, как вкладка теряет фокус.

Есть ли способ указать chrome не приостанавливать выполнение requestAnimationFrame? Есть ли альтернативный способ переключения потоков без влияния на запись MediaRecorder?

Обновление: После прочтения документации вкладки, воспроизводящие аудио или имеющие активное соединение с веб-сокетом, не регулируются. Это то, что мы не делаем в данный момент. Это может быть обходной путь, но в надежде на какое-либо альтернативное решение от сообщества. (setTimeout или setInterval слишком ограничены и, следовательно, не используют это, плюс это влияет на качество рендеринга)

Обновление 2: Я мог бы решить эту проблему с помощью Worker. Вместо использования основного потока пользовательского интерфейса для requestAnimationFrame рабочий вызывает API, и уведомление отправляется в основной поток через postMessage. После завершения рендеринга потоком пользовательского интерфейса сообщение отправляется обратно в Worker. Существует также вычисление дельта-периода для подавления подавляющих сообщений от работника.

1 Ответ

1 голос
/ 22 января 2020

Есть предложение о добавлении метода .replaceTrack() в MediaRecorder API, но в настоящее время спецификации все еще читаются

Если в любой момент дорожка добавляется или удаляется из набора дорожек потока, UA ДОЛЖЕН немедленно прекратить сбор данных, отбросить все собранные данные [...]

И вот что


Таким образом, мы все еще должны полагаться на уродливые хаки , чтобы сделать это сами ...

Вот один, , который кажется правильно работать только в Firefox, потому что я до сих пор не знаю, по каким причинам , используя MediaSource в качестве микшера.

Это работает следующим образом:

  • захватывайте ваши видео
  • записывать их все с помощью MediaRecorder для каждого видео
  • перехватывать dataavailable этих MediaRecorder и подавать MediaSource с их порциями
  • захватывать поток видеоэлемента который воспроизводит этот MediaSource
  • запись этого смешанного потока
* 103 7 * Однако вся эта установка добавляет значительную задержку (не удивляйтесь, если вам придется подождать несколько секунд, прежде чем станет видно переключение источников), и это безумно тяжело для процессора ...

Помните, что ниже демо будет работать правильно, если страница размыта только в Firefox

{ // 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' );
  
  const type = 'video/webm; codecs="vp8"';
  if( !MediaSource.isTypeSupported( type ) ) {
    throw new Error( 'Not Supported' );
  }
  let stopped = false;
  let current = 0;
  switcher_btn.onclick = (evt) => { current = +!current; };
  
  console.log( 'loading videos, please wait' );
  // see below for 'recordVid'
  const recorders = await Promise.all( urls.map( (url, index) =>  recordVid( url, type ) ) );
  
  const source = new MediaSource();

  // create an offscreen video so it doesn't get paused when hidden
  const mixed_vid = document.createElement( 'video' );
  mixed_vid.autoplay = true;
  mixed_vid.muted = true;
  mixed_vid.src = URL.createObjectURL( source );
  
  await waitForEvent( source, 'sourceopen' );
  
  const buffer = source.addSourceBuffer( type );
  buffer.mode = "sequence";
  
  // init our requestData loop
  appendBuffer();
  mixed_vid.play();
  await waitForEvent( mixed_vid, 'playing' );
  console.clear();
  
  // final recording part below
  const mixed_stream = mixed_vid.captureStream();
  // only for demo, so we can see what happens now
  video_out.srcObject = mixed_stream;
  
  const rec = new MediaRecorder( mixed_stream );
  const chunks = [];

  rec.ondataavailable = (evt) => chunks.push( evt.data );

  rec.onstop = (evt) => {
    stopped = true;
    const final_file = new Blob( chunks );
    recorders.forEach( (rec) => rec.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();
  };

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

  rec.start();
  
  // requestData loop
  async function appendBuffer() {
    if( stopped ) { return; }
    const chunks = await Promise.all( recorders.map( rec => rec.requestData() ) );
    const chunk = chunks[ current ];
    // first iteration is generally empty
    if( !chunk.byteLength ) { setTimeout( appendBuffer, 100 ); return; }
    buffer.appendBuffer( chunk );
    await waitForEvent( buffer, 'update' );
    appendBuffer();
  };
    
}))
.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
** returns an object which exposes two method
** 'requestData()' returns a Promise resolved by the latest available chunk of data
** 'stop()' stops the video element and the recorder
*/
async function recordVid( url, type ) {
  const player = await makeVid( url );
  const stream = videoStream( player.captureStream() );
//  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  const recorder = new MediaRecorder( stream, { mimeType: type } );
  const chunks = [];
  recorder.start( );
  
  return {
    requestData() {
      
      recorder.requestData();
      const data_prom = waitForEvent( recorder, "dataavailable" )
        .then( (evt) => evt.data.arrayBuffer() );
      return data_prom;
      
    },
    stop() { recorder.stop(); player.pause(); }
  };
}
// removes the audio tracks from a MediaStream
function videoStream( mixed ) {
  return new MediaStream( mixed.getVideoTracks() );
}
// 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>

Другой такой взлом - создание локального соединения RT C и запись принимающей стороны.

Хотя хотя на бумаге это должно было сработать, мой Firefox будет просто странно смешивать оба потока в чем-то, что я бы посоветовал избегать читателям epilepti c, а рекордеры Chrome производят однокадровое видео, возможно потому, что размер видео изменить ...

Итак, в данный момент это, похоже, нигде не работает , но здесь дело в том, что браузеры исправляют свои ошибки перед реализацией MediaRecorder.replaceTrack.

{ // 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


// 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 (e) {
    console.error(e);
  }
  
  return {
    stream: await wait_for_stream,
    async replaceTrack( new_track ) {
      const sender = pc1.getSenders().find( ( { track } ) => track.kind == new_track.kind );
      console.log( new_track );
      return sender && sender.replaceTrack( new_track ) ||
        Promise.reject('no such track');
    }
  }  
}

// 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>

Тогда, на данный момент, наилучшим, вероятно, является еще go холст, с помощью Web Audio Timer Я сделал, когда страница размыта, хотя это не будет работать на Firefox.

...