35

I am in the process of replacing RecordRTC with the built in MediaRecorder for recording audio in Chrome. The recorded audio is then played in the program with audio api. I am having trouble getting the audio.duration property to work. It says

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

With RecordRTC, I had to use ffmpeg_asm.js to convert the audio from wav to ogg. My guess is somewhere in the process RecordRTC sets the predefined audio length. Is there any way to set the predefined length using MediaRecorder?

2
  • What do you mean predefined length? Can you just have a timer that is started when the recording starts and then stop it at the appropiate time?
    – Sean C
    Commented Aug 29, 2016 at 2:20
  • @Tom Chen when I inspect my recorded audio files after a recording (using command-line '$ ffmpeg -i test.webm' I see definition is set as N/A. Did you find a way to set the length? Commented Aug 30, 2016 at 9:47

8 Answers 8

56
+50

This is a chrome bug.

FF does expose the duration of the recorded media, and if you do set the currentTimeof the recorded media to more than its actual duration, then the property is available in chrome...

function exportAudio(blob) {
  const aud = document.getElementById("aud");
  aud.src = URL.createObjectURL(blob);
  aud.addEventListener("loadedmetadata", () => {
    // It should have been already available here
    console.log("duration:", aud.duration);
    // Handle chrome's bug
    if (aud.duration === Infinity) {
      // Set it to bigger than the actual duration
      aud.currentTime = 1e101;
      aud.addEventListener("timeupdate", () => {
        console.log("after workaround:", aud.duration);
        aud.currentTime = 0;
      }, { once: true });
    }
  });
}

// We need user-activation
document.getElementById("button").onclick = async ({ target }) => {
  target.remove();
  const resp = await fetch("https://upload.wikimedia.org/wikipedia/commons/4/4b/011229beowulf_grendel.ogg");
  const audioData = await resp.arrayBuffer();
  const ctx = new AudioContext();
  const audioBuf = await ctx.decodeAudioData(audioData);
  const source = ctx.createBufferSource();
  source.buffer = audioBuf;
  const dest = ctx.createMediaStreamDestination();
  source.connect(dest);

  const recorder = new MediaRecorder(dest.stream);
  const chunks = [];
  recorder.ondataavailable = ({data}) => chunks.push(data);
  recorder.onstop = () => exportAudio(new Blob(chunks));
  source.start(0);
  recorder.start();
  console.log("Recording...");
  // Record only 5 seconds
  setTimeout(function() {
    recorder.stop();
  }, 5000);

}
<button id="button">start</button>
<audio id="aud" controls></audio>

So the advice here would be to star the bug report so that chromium's team takes some time to fix it, even if this workaround can do the trick...


2024 update

Since this answer has been posted it seems unlikely the MediaRecorder API will ever fix this.
Hopefully in the near future the WebCodecs API will provide a way to do this, with enough browser support, but for now to fix the initial issue you'd need to repack the generated media yourself after the whole media has been recorded. (This means you need to do this where you keep all the chunks, this might be on your server). You may take a look at this answer for one such library (that I didn't test myself) which seems to work in both browsers and node.

6
  • 1
    There is a bug: crbug.com/642012. I suggest we "star" it so the developers can prioritize accordingly.
    – miguelao
    Commented Oct 21, 2016 at 2:01
  • @miguelao, yep I filed this one which has been merged to the one you mentioned.
    – Kaiido
    Commented Oct 21, 2016 at 2:04
  • 1
    The chrome bug mentioned here has been marked as WontFix by the chromium team, so it looks looks like we need to rely on a library like ts-ebml if we want to edit the actual file.
    – user10898116
    Commented Feb 28, 2019 at 23:49
  • stackoverflow.com/questions/67936072/… pls help on this. Thanks.
    – sy523
    Commented Jun 12, 2021 at 1:58
  • Genius solution....thank you! This fix seems to be the easiest way to get it to work.
    – Sam
    Commented Aug 27, 2022 at 2:12
8

Thanks to @Kaiido for identifying bug and offering the working fix.

I prepared an npm package called get-blob-duration that you can install to get a nice Promise-wrapped function to do the dirty work.

Usage is as follows:

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

Or ECMAScript 6:

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

A bug in Chrome, detected in 2016, but still open today (March 2019), is the root cause behind this behavior. Under certain scenarios audioElement.duration will return Infinity.

Chrome Bug information here and here

The following code provides a workaround to avoid the bug.

Usage : Create your audioElement, and call this function a single time, providing a reference of your audioElement. When the returned promise resolves, the audioElement.duration property should contain the right value. ( It also fixes the same problem with 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 ) 
});
1
  • 1
    After hours of messing about with all sorts of fixes, this was the only one that worked without further problems. Thank you so much! Commented Jul 10, 2021 at 23:52
4

I wrapped the webm-duration-fix package to solve the webm length problem, which can be used in nodejs and web browsers to support video files over 2GB with not too much memory usage.

Usage is as follows:

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 = [];
};
1
  • The library worked for WebM of a few MB also. Very useful. Thanks! Commented Jun 4, 2022 at 15:07
3

The accepted answer is fine,however, if you use Kendo Media player or other third-party video players that are dependent on ontimeupdate function this solution is for you

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)


2

Thanks @colxi for the actual solution, I've added some validation steps (As the solution was working fine but had problems with long audio files).

It took me like 4 hours to get it to work with long audio files turns out validation was the fix

        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)
              })
            }
0
1

//If you want to modify the video file completely, you can use this package "webmFixDuration", Other methods are applied at the display level only on the video tag With this method, the complete video file is modified

webmFixDuration github example

 mediaRecorder.onstop = async () => {
        const duration = Date.now() - startTime;
        const buggyBlob = new Blob(mediaParts, { type: 'video/webm' });
    
        const fixedBlob = await webmFixDuration(buggyBlob, duration);
        displayResult(fixedBlob);
      };
0
<audio controls *ngIf="url && url!==''" (loadedmetadata)="onAudioLoaded($event)" (ended)="onAudioEnded()">
<source [src]="url| safeUrl">

and my ts file would be like this.

  onAudioLoaded(event: Event) {
  const audioElement = event.target as HTMLAudioElement;
  if (audioElement instanceof HTMLAudioElement) {
   if (audioElement.duration === Infinity) {
     audioElement.currentTime = 1e101;
     audioElement.addEventListener(
      "timeupdate",
      () => {
        audioElement.currentTime = 0;
      },
      { once: true }
    );
  }
}

}

this workaround solved my issue.

Not the answer you're looking for? Browse other questions tagged or ask your own question.