Как добавить заранее заданную длину к аудио, записанному из MediaRecorder в Chrome?

Я нахожусь в процессе замены RecordRTC на встроенный MediaRecorder для записи звука в Chrome. Записанный звук затем воспроизводится в программе с аудио API. У меня проблемы с получением свойства audio.duration. Это говорит

Если видео (аудио) передается в потоковом режиме и не имеет заданной длины, возвращается "Inf" (бесконечность).

С RecordRTC мне пришлось использовать ffmpeg_asm.js для преобразования аудио из wav в ogg. Я предполагаю, что где-то в процессе RecordRTC устанавливает предопределенную длину звука. Есть ли способ установить предопределенную длину с помощью MediaRecorder?

7 ответов

Решение

Это ошибка Chrome.

FF показывает продолжительность записанного носителя, и если вы установите currentTime записанных носителей более чем фактическим duration, то свойство доступно в хром...

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

function exportAudio() {
  var blob = new Blob(chunks, {
    type: 'audio/ogg'
  });
  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);
  }

}



getData();
<audio id="aud" controls></audio><span id="log"></span>

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

Спасибо @Kaiido за выявление ошибки и предложение исправления.

Я подготовил пакет npm с именем get-blob-duration, который вы можете установить, чтобы получить красивую упакованную в 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 года), является основной причиной такого поведения. По определенным сценариям audioElement.duration вернусь Infinity,

Информация об Chrome Bug здесь и здесь

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

Использование: Создайте свой audioElementи вызовите эту функцию один раз, предоставив ссылку на ваш audioElement, Когда вернулся 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 , чтобы решить проблему длины веб-страницы, которую можно использовать в nodejs и веб-браузерах для поддержки видеофайлов размером более 2 ГБ с небольшим использованием памяти.

Использование выглядит следующим образом:

      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 = [];
};

Принятый ответ - это нормально, однако, если вы используете проигрыватель Kendo Media или другие сторонние видеоплееры, которые зависят от функции ontimeupdate, это решение для вас.

      const seedDuration = (player) => {

    player.onloadedmetadata = () => {
         // handle chrome's bug
         if (player.duration === Infinity) {
             player.currentTime = 1e101;

             // Save the default behaviour of ontimeupdate
             const onTimeUpdateHandler = player.ontimeupdate;

             player.ontimeupdate = function () {
                 // bring back the default behaviour
                 this.ontimeupdate = onTimeUpdateHandler;
                 player.currentTime = 0;
                        
             }

          }
     }
}

//For kendo media player
//const player = document.getElementsByClassName('k-mediaplayer-media')[0];

const player = document.getElementById("videoPlayer");
player.src = YOUR_VIDEO_SOURCE;
seedDuration(player);

// To make sure that this block of code runs after all the executions are done
setTimeout(()=>{
   player.play();

   // For kendo media player
   $("#YOUR_MEDIA_PLAYER_ID").data("kendoMediaPlayer").play();
},0)


// Если вы хотите полностью изменить видеофайл, вы можете использовать этот пакет «webmFixDuration». Другие методы применяются на уровне отображения только для тега видео. С помощью этого метода изменяется весь видеофайл.

пример webmFixDuration на гитхабе

       mediaRecorder.onstop = async () => {
        const duration = Date.now() - startTime;
        const buggyBlob = new Blob(mediaParts, { type: 'video/webm' });
    
        const fixedBlob = await webmFixDuration(buggyBlob, duration);
        displayResult(fixedBlob);
      };
Другие вопросы по тегам