如何将预定义长度添加到从 Chrome 中的 MediaRecorder 录制的音频?

How can I add predefined length to audio recorded from MediaRecorder in Chrome?

我正在用内置的 MediaRecorder 替换 RecordRTC,以便在 Chrome 中录制音频。然后在程序中播放录制的音频,音频为api。我无法让 audio.duration 属性 正常工作。它说

If the video (audio) is streamed and has no predefined length, "Inf" (Infinity) is returned.

使用 RecordRTC,我不得不使用 ffmpeg_asm.js 将音频从 wav 转换为 ogg。我的猜测是 RecordRTC 设置预定义音频长度的过程中的某个地方。有什么方法可以使用 MediaRecorder 设置预定义长度吗?

这是一个chrome bug

FF 确实公开了录制媒体的持续时间,如果您将录制媒体的 currentTime 设置为超过其实际 duration,则 属性 可用在 chrome...

var recorder,
  chunks = [],
  ctx = new AudioContext(),
  aud = document.getElementById('aud');

function exportAudio() {
  var blob = new Blob(chunks);
  aud.src = URL.createObjectURL(new Blob(chunks));

  aud.onloadedmetadata = function() {
    // it should already be available here
    log.textContent = ' duration: ' + aud.duration;
    // handle chrome's bug
    if (aud.duration === Infinity) {
      // set it to bigger than the actual duration
      aud.currentTime = 1e101;
      aud.ontimeupdate = function() {
        this.ontimeupdate = () => {
          return;
        }
        log.textContent += ' after workaround: ' + aud.duration;
        aud.currentTime = 0;
      }
    }
  }
}

function getData() {
  var request = new XMLHttpRequest();
  request.open('GET', 'https://upload.wikimedia.org/wikipedia/commons/4/4b/011229beowulf_grendel.ogg', true);
  request.responseType = 'arraybuffer';
  request.onload = decodeAudio;
  request.send();
}


function decodeAudio(evt) {
  var audioData = this.response;
  ctx.decodeAudioData(audioData, startRecording);
}

function startRecording(buffer) {

  var source = ctx.createBufferSource();
  source.buffer = buffer;
  var dest = ctx.createMediaStreamDestination();
  source.connect(dest);

  recorder = new MediaRecorder(dest.stream);
  recorder.ondataavailable = saveChunks;
  recorder.onstop = exportAudio;
  source.start(0);
  recorder.start();
  log.innerHTML = 'recording...'
  // record only 5 seconds
  setTimeout(function() {
    recorder.stop();
  }, 5000);
}

function saveChunks(evt) {
  if (evt.data.size > 0) {
    chunks.push(evt.data);
  }

}

// we need user-activation
document.getElementById('button').onclick = function(evt){
  getData();
  this.remove();
}
<button id="button">start</button>
<audio id="aud" controls></audio><span id="log"></span>

所以这里的建议是给 bug report 加注星标,这样 Chromium 的团队需要一些时间来修复它,即使这个解决方法可以解决问题...

感谢@Kaiido 识别错误并提供工作修复。

我准备了一个名为 get-blob-duration 的 npm 包,您可以安装它以获得一个很好的 Promise 包装函数来完成肮脏的工作。

用法如下:

// Returns Promise<Number>
getBlobDuration(blob).then(function(duration) {
  console.log(duration + ' seconds');
});

或 ECMAScript 6:

// yada yada async
const duration = await getBlobDuration(blob)
console.log(duration + ' seconds')

Chrome 中的错误,于 2016 年检测到,但今天(2019 年 3 月)仍然存在,是此行为背后的根本原因。在某些情况下 audioElement.duration 将 return Infinity.

Chrome Bug information here and here

以下代码提供了一种解决方法来避免该错误。

用法:创建你的audioElement,并一次性调用这个函数,提供你的audioElement的参考。当 returned promise 解析时,audioElement.duration 属性 应该包含正确的值。 (它还解决了与 videoElements 相同的问题)

/**
 *  calculateMediaDuration() 
 *  Force media element duration calculation. 
 *  Returns a promise, that resolves when duration is calculated
 **/
function calculateMediaDuration(media){
  return new Promise( (resolve,reject)=>{
    media.onloadedmetadata = function(){
      // set the mediaElement.currentTime  to a high value beyond its real duration
      media.currentTime = Number.MAX_SAFE_INTEGER;
      // listen to time position change
      media.ontimeupdate = function(){
        media.ontimeupdate = function(){};
        // setting player currentTime back to 0 can be buggy too, set it first to .1 sec
        media.currentTime = 0.1;
        media.currentTime = 0;
        // media.duration should now have its correct value, return it...
        resolve(media.duration);
      }
    }
  });
}

// USAGE EXAMPLE :  
calculateMediaDuration( yourAudioElement ).then( ()=>{ 
  console.log( yourAudioElement.duration ) 
});

感谢 @colxi 的实际解决方案,我添加了一些验证步骤(因为解决方案工作正常但有问题带有长音频文件)。

我花了大约 4 个小时才让它处理长音频文件,结果证明验证是解决方法

        function fixInfinity(media) {
          return new Promise((resolve, reject) => {
            //Wait for media to load metadata
            media.onloadedmetadata = () => {
              //Changes the current time to update ontimeupdate
              media.currentTime = Number.MAX_SAFE_INTEGER;
              //Check if its infinite NaN or undefined
              if (ifNull(media)) {
                media.ontimeupdate = () => {
                  //If it is not null resolve the promise and send the duration
                  if (!ifNull(media)) {
                    //If it is not null resolve the promise and send the duration
                    resolve(media.duration);
                  }
                  //Check if its infinite NaN or undefined //The second ontime update is a fallback if the first one fails
                  media.ontimeupdate = () => {
                    if (!ifNull(media)) {
                      resolve(media.duration);
                    }
                  };
                };
              } else {
                //If media duration was never infinity return it
                resolve(media.duration);
              }
            };
          });
        }
        //Check if null
        function ifNull(media) {
          if (media.duration === Infinity || media.duration === NaN || media.duration === undefined) {
            return true;
          } else {
            return false;
          }
        }

    //USAGE EXAMPLE
            //Get audio player on html
            const AudioPlayer = document.getElementById('audio');
            const getInfinity = async () => {
              //Await for promise
              await fixInfinity(AudioPlayer).then(val => {
                //Reset audio current time
                AudioPlayer.currentTime = 0;
                //Log duration
                console.log(val)
              })
            }

我封装了 webm-duration-fix 包来解决 webm 长度问题,它可以在 nodejs 和网络浏览器中使用,以支持超过 2GB 的视频文件,而不会占用太多内存。

用法如下:

import fixWebmDuration from 'webm-duration-fix';

const mimeType = 'video/webm\;codecs=vp9';
const blobSlice: BlobPart[] = [];

mediaRecorder = new MediaRecorder(stream, {
  mimeType
});

mediaRecorder.ondataavailable = (event: BlobEvent) => {
  blobSlice.push(event.data);
}

mediaRecorder.onstop = async () => {  
    // fix blob, support fix webm file larger than 2GB
    const fixBlob = await fixWebmDuration(new Blob([...blobSlice], { type: mimeType }));
    // to write locally, it is recommended to use fs.createWriteStream to reduce memory usage
    const fileWriteStream = fs.createWriteStream(inputPath);
    const blobReadstream = fixBlob.stream();
    const blobReader = blobReadstream.getReader();

    while (true) {
      let { done, value } = await blobReader.read();
      if (done) {
        console.log('write done.');
        fileWriteStream.close();
        break;
      }
      fileWriteStream.write(value);
      value = null;
    }
    blobSlice = [];
};