55

I'm doing a project with HTML and Javascript that will run local with local files. I need to select a file by input, get its information and then decide if I'll add to my list and reproduce or not. And if Ii decide to use it I'll have to put it on a queue to use later. Otherwise I'll just discard and select another file.

The problem that I'm facing is that I can't find a way to get the video duration just by selecting it on input.

I've searched a lot and I didn't find any method to get the duration. In this code below I tried to use 'file.duration' but it didn't work, it just returns 'undefined'.

This is my input, normal as you can see.

<div id="input-upload-file" class="box-shadow">
   <span>upload! (ღ˘⌣˘ღ)</span> <!--ignore the text face lol -->
   <input type="file" class="upload" id="fileUp" name="fileUpload" onchange="setFileInfo()">
</div>

And this is the function that I'm using to get all the information.

function setFileInfo(){
  showInfo(); //change div content
  var file = document.getElementById("fileUp").files[0];
  var pid =  1;
  var Pname = file.name;
  Pname = Pname.slice(0, Pname.indexOf(".")); //get filename without extension
  var Ptype = file.type;
  var Psize = bytesToSize(file.size); //turns into KB,MB, etc...
  var Pprior = setPriority(Ptype); //returns 1, 2 or 3 
  var Pdur = file.duration;
  var Pmem = getMemory(Psize); //returns size * (100 || 10 || 1)
  var Pown = 'user';
  /* a lot of stuff throwing this info to the HTML */
  console.log(Pdur);
}

Is there way to do this? If not, what are the alternatives that can help me?

danronmoon
  • 3,814
  • 5
  • 34
  • 56
davis
  • 1,216
  • 5
  • 14
  • 27
  • Are you looking for something like this? http://jsfiddle.net/derickbailey/s4P2v/ This isn't mine, and it's for audio, but I can't imagine it will be very different for video – Robin-Hoodie Mar 26 '15 at 17:43
  • @RobinHellemans This one creates a – davis Mar 26 '15 at 18:31
  • Any idea how to get this work with avi files? – Spy Mar 05 '19 at 09:19

6 Answers6

113

In modern browsers, You can use the URL API's URL.createObjectURL() with an non appended video element to load the content of your file.

var myVideos = [];

window.URL = window.URL || window.webkitURL;

document.getElementById('fileUp').onchange = setFileInfo;

function setFileInfo() {
  var files = this.files;
  myVideos.push(files[0]);
  var video = document.createElement('video');
  video.preload = 'metadata';

  video.onloadedmetadata = function() {
    window.URL.revokeObjectURL(video.src);
    var duration = video.duration;
    myVideos[myVideos.length - 1].duration = duration;
    updateInfos();
  }

  video.src = URL.createObjectURL(files[0]);;
}


function updateInfos() {
  var infos = document.getElementById('infos');
  infos.textContent = "";
  for (var i = 0; i < myVideos.length; i++) {
    infos.textContent += myVideos[i].name + " duration: " + myVideos[i].duration + '\n';
  }
}
<div id="input-upload-file" class="box-shadow">
  <span>upload! (ღ˘⌣˘ღ)</span>
  <input type="file" class="upload" id="fileUp" name="fileUpload">
</div>
<pre id="infos"></pre>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • This one worked well, but when before I return the value it's ok then after I return It's undefined. Any idea of what could I do to solve this? i.e. 24.105 undefined – davis Mar 26 '15 at 19:20
  • yep, store it in a global variable, or even better, store your files in an array, then in the event handler, add to your stored file a duration attribute – Kaiido Mar 26 '15 at 19:23
  • @davis, see edit, you can then remove the files you don't want with `myVideos.splice(the_index_of_the_video)` – Kaiido Mar 26 '15 at 19:38
  • Oh, that helped me a lot, Thank you! Also, why can't I send this info to outside of the function? Even using getters and setters I still having problem to do it. – davis Mar 26 '15 at 20:51
  • It's because you are in the videos's`onmetadata` [scope](http://www.w3schools.com/js/js_scope.asp), which works asynchronously. You'll have to store it in a global variable (like I did with `myVideos = [];`) and only call your `setFileInfo()` once this event has been triggered. or alternatively, you could use a `setTimeout()` loop to check if the property is set. – Kaiido Mar 27 '15 at 02:26
  • I was doing this. I found the error. It doesn't get the first input, only when I select one file (that shows all the data, but not the duration) and then I select a second one, which shows the first one duration. And then it goes showing on the third the second's duration, bla bla bla. Example: first | file: 111 duration: 10 what shows: undefined | second: file: 222 duration: 30 what shows: 10 | – davis Mar 27 '15 at 05:47
  • @ParamSingh obviously this will only work for medias supported by the html video element. Your browser probably doesn't support reading avi files. – Kaiido Jul 14 '17 at 23:50
  • Any idea why uploading a mp4 or mp3 recorded with the `MediaRecorder` gives you a duration of Infinity? The media file is otherwise playable. Also chrome takes its time to show the duration of the video when embedding it into the html. – filip Aug 10 '20 at 06:42
  • @filip https://stackoverflow.com/questions/38443084/how-can-i-add-predefined-length-to-audio-recorded-from-mediarecorder-in-chrome – Kaiido Aug 10 '20 at 06:46
  • Thanks! But why tho? I tried my recorded files, it's not working out of the box.When using other web recorders on the web, and download their file, I get an duration# – filip Aug 10 '20 at 07:00
  • @filip It's explained in the linked answer and its links: Chrome has a bug where they don't set the duration of recorded files. Other browsers don't have this bug. – Kaiido Aug 10 '20 at 07:02
  • Yeah, but I'm using Chrome, and tried other Recording Services on Chrome (other websites) and downloaded their results. With their files it works, I think they just convert their files with the mentioned ffmpeg.js or something like that – filip Aug 10 '20 at 07:13
  • Yes, they did worked-around the bug, they probably didn't even use the MediaRecorder API but some other way to record it. I can't tell from here, my magic-ball is currently busy. – Kaiido Aug 10 '20 at 07:14
  • This is actually a bad idea. Wait till someone uploads a 5GB video—using this method will totally lock up their browser. – ffxsam Jul 30 '21 at 21:18
  • @ffxsam did you actually try it? No it will not lock their browser, the blob URL is a direct pointer to the file on disk, the browser doesn't need to load the full file in memory to load it and seek in it. So it will be able to grab the metadata with a minimal memory footprint, even in files not optimized for the web. – Kaiido Jul 30 '21 at 22:42
  • @Kaiido Of course I tried it. Checking uploaded videos locks up the browser for several seconds (depending on the video size) in Firefox. So if someone wants to truly support multiple browsers, this is a deal-breaker. I'll get in touch with the Mozilla team to see if this is a bug. – ffxsam Aug 01 '21 at 19:50
  • Yes please reach to them. There is no reason that would lock your page. Though before you do, please triple check that it's really that part of the code that does lock the browser. Loading the file itself (even without doing anything else with it) may lock the browser with big files (the os needs to build some metadata and on slow storage devices it can take time) – Kaiido Aug 01 '21 at 23:06
  • @Kaiido Aha! It wasn't actually locking anything up. The loadedmetadata event never fires, because the MOV file apparently doesn't have a proper header or index for it to reference. Still.. Chrome seems to figure it out somehow. So I'll still say this is a Firefox bug. – ffxsam Aug 02 '21 at 13:18
  • It's very probable that they simply don't support that file. What codec is in this mov? Can you try to make a sliced version of the video, of a few minutes only using ffmpeg and keeping the same a/v codecs? – Kaiido Aug 02 '21 at 13:33
26

I needed to validate a single file before continuing to execute more code, here is my method with the help of Kaiido's answer!

onchange event when a user uploads a file:

$("input[type=file]").on("change", function(e) {

    var file = this.files[0]; // Get uploaded file

    validateFile(file) // Validate Duration

    e.target.value = ''; // Clear value to allow new uploads
})

Now validate duration:

function validateFile(file) {

    var video = document.createElement('video');
    video.preload = 'metadata';

    video.onloadedmetadata = function() {

        window.URL.revokeObjectURL(video.src);

        if (video.duration < 1) {

            console.log("Invalid Video! video is less than 1 second");
            return;
        }

        methodToCallIfValid();
    }

    video.src = URL.createObjectURL(file);
}
ricks
  • 3,154
  • 31
  • 51
19

Here is async/await Promise version:

const loadVideo = file => new Promise((resolve, reject) => {
    try {
        let video = document.createElement('video')
        video.preload = 'metadata'

        video.onloadedmetadata = function () {
            resolve(this)
        }

        video.onerror = function () {
            reject("Invalid video. Please select a video file.")
        }

        video.src = window.URL.createObjectURL(file)
    } catch (e) {
        reject(e)
    }
})

Can be used as follows:

const video = await loadVideo(e.currentTarget.files[0])
console.log(video.duration)
Adam
  • 1,470
  • 1
  • 17
  • 13
  • 2
    typescript complains about passing `this` to `resolve` if you specify the return type of `loadVideo` as `Promise`. Throws the error `Argument of type 'GlobalEventHandlers' is not assignable to parameter of type 'HTMLVideoElement | PromiseLike'` To fix this just pass the `video` object itself to `resolve` instead of `this`, like so `resolve(video)` – Ismael Jan 22 '21 at 17:17
  • Simple and easy! – Darwin Marcelo Feb 13 '23 at 12:13
16

It's simple to get video duration from FileReader and it's easy to manage in async/await.

const getVideoDuration = file =>
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const media = new Audio(reader.result);
      media.onloadedmetadata = () => resolve(media.duration);
    };
    reader.readAsDataURL(file);
    reader.onerror = error => reject(error);
  });
const duration = await getVideoDuraion(file);

where file is File object

Live Example

const getVideoDuration = (file) =>
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const media = new Audio(reader.result);
      media.onloadedmetadata = () => resolve(media.duration);
    };
    reader.readAsDataURL(file);
    reader.onerror = (error) => reject(error);
  });

const handleChange = async (e) => {
  const duration = await getVideoDuration(e.target.files[0]);
  document.querySelector("#duration").innerText = `Duration: ${duration}`;
};
<div>
  <input type="file" onchange="handleChange(event)" />
  <p id="duration">Duration: </p>
</div>
Nisharg Shah
  • 16,638
  • 10
  • 62
  • 73
  • 1
    why do you write "For React" ? I don't see what this has to do with React, it's just JS... I encourage you to remove that line from your answer. – mesqueeb Sep 12 '21 at 01:31
  • Yes, I know there is nothing related to javascript but I added a demo of react that is why, but thanks for the question, I will convert demo into pure vanila js – Nisharg Shah Sep 12 '21 at 10:21
  • Browser gets stuck if file size is over 500mb. It's better to use video.duration as mentioned in other posts. – Salman Sep 23 '21 at 16:44
5

This is how I managed to get video duration before pushing it on S3.. I am using this code to upload video files of 4+ GB.

Note - for formats like .avi,.flv, .vob, .mpeg etc, duration will not be found, so handle it with a simple message to the user

  //same method can be used for images/audio too, making some little changes
  getVideoDuration = async (f) => {
    const fileCallbackToPromise = (fileObj) => {
      return Promise.race([
        new Promise((resolve) => {
          if (fileObj instanceof HTMLImageElement) fileObj.onload = resolve;
          else fileObj.onloadedmetadata = resolve;
        }),
        new Promise((_, reject) => {
          setTimeout(reject, 1000);
        }),
      ]);
    };

    const objectUrl = URL.createObjectURL(f);
    // const isVideo = type.startsWith('video/');
    const video = document.createElement("video");
    video.src = objectUrl;
    await fileCallbackToPromise(video);
    return {
      duration: video.duration,
      width: video.videoWidth,
      height: video.videoHeight,
    };
}


//call the function
//removing unwanted code for clarity
const meta = await this.getVideoDuration(file);
//meta.width, meta.height, meta.duration is ready for you to use
mesqueeb
  • 5,277
  • 5
  • 44
  • 77
Milind
  • 4,535
  • 2
  • 26
  • 58
  • Any idea why video duration is missing for the format you mentioned in your note? Actually, the onload and onloadedmetadata event are not even firing for these format (tested with video/avi). – Eturcim Aug 19 '21 at 15:56
  • Yes...its not missing but I didnt wanted to add it as there are many formats of video file like avi, mpeg etc, getting the durarion of some of these video files fails, while rest of them are handled easily, as its more of browser centric. – Milind Aug 21 '21 at 23:47
  • I have asked a similar question here (https://stackoverflow.com/questions/70133389/url-from-file-object?noredirect=1#comment123992069_70133389) and some guys have mentioned that getting the file metadata on the client side (I guess by creating the video element and setting the file blob as source), is a security vulnerability. Is that so? – Flo Ragossnig Nov 29 '21 at 09:11
  • I agree @FloRagossnig, but many times there are video formats who's duration details are not well understood by most of the library, so this is one of the way to get the duration explicitly. – Milind Dec 03 '21 at 11:54
3

I've implemented getting video duration in nano-metadata package https://github.com/kalashnikovisme/nano-metadata.

You can do something like this

import nanoMetadata from 'nano-metadata'

const change = (e) => {
  const file = e.target.files[0]
  
  nanoMetadata.video.duration(file).then((duration) => {
    console.log(duration) // will show you video duration in seconds
  })
}
Pavel Kalashnikov
  • 2,092
  • 1
  • 19
  • 22