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

8

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?

Weedy answered 18/7, 2016 at 17:54 Comment(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?Hewes
@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?Bergquist
C
56

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.

Cicero answered 11/10, 2016 at 5:52 Comment(6)
There is a bug: crbug.com/642012. I suggest we "star" it so the developers can prioritize accordingly.Cw
@miguelao, yep I filed this one which has been merged to the one you mentioned.Cicero
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.Corrupt
#67936572 pls help on this. Thanks.Unfold
Genius solution....thank you! This fix seems to be the easiest way to get it to work.Bargain
What if I only have an AudioBuffer or an AudioBufferSourceNode or a MediaElementAudioSourceNode ? I don't have an HTML audio element, and still get the bug.Judenberg
C
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')
Corrigan answered 15/2, 2018 at 3:10 Comment(0)
T
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 ) 
});
Tinkle answered 11/3, 2019 at 0:10 Comment(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!Delorsedelos
M
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 = [];
};
Matildematin answered 21/3, 2022 at 8:6 Comment(1)
The library worked for WebM of a few MB also. Very useful. Thanks!Klemm
F
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)


Fideliafidelio answered 27/10, 2023 at 7:15 Comment(0)
W
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)
              })
            }
Waft answered 19/3, 2020 at 23:24 Comment(0)
K
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);
      };
Khalsa answered 24/1, 2023 at 6:59 Comment(0)
L
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.

Leuctra answered 19/4 at 13:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.