jsHtml5VideoRecorder – Record video stream in html5 with automatic fps

Another day, another tutorial.
Today, we will see how we can record live video from webcam in html5.

You can see many plugins on the web, but what they don’t tell you is that, the speed of your record will depend on your cpu power and your webcam model.
So, in fast modern machines with high quality webcal, your fps will be different than olders one.

To solve this problem, We will calculate the appropriate fps record, and delete frames we don’t need, if it’s the case.
To record html5 video, you’ll need the Whammy library. Whammy is fast webm encoder, you can download it here: https://github.com/antimatter15/whammy.

Also, actually, only Chrome is able to proceed to live html5 video record.
Everything is ok? So, let’s go!

First, we must check that navigator.getUserMedia is supported by our browser.

if (!navigator.getUserMedia) {
    navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
}
window.URL  = window.URL || window.webkitURL;
this.url    = window.URL;        
        
window.onload = this.onLoad();

Now, we can initiate the navigator.getUserMedia api, with audio config to true, and a stream function.

navigator.getUserMedia({ 
    video: true
}, this.startUserMedia.bind(this), function(e) {
    console.log('No live video stream: ' + e);
    alert("Webcam not enabled or no live video stream");
});

For recording a video, we first have to save the stream in a variable and load it in a video tag for live stream

this.mediaStream = stream;

//We will see this method later        
this.resetTags();

Let’s develop the resetTags method. It’s only a method that allows us to create (video and canvas) tags if needed

//Create video and canvas tag if not exists
this.createTag('video', this.videoTagId);
this.createTag('canvas', this.canvasTagId);

Why? If you don’t have html5 video or canvas tag into your document, this object can create them for you.
Here is the createTag method

var myTag   = document.getElementById(tagId);
       
if (myTag === null) {
            
    myTag = document.createElement(tag);
            
    if (tag === 'canvas') {
        myTag.width             = this.width;
        myTag.height            = this.height;
        myTag.id                = tagId;
        myTag.style.position    = 'absolute';
        myTag.style.visibility  = 'hidden';
        this.ctx                = myTag.getContext('2d');
        this.canvasTag          = this.ctx.canvas;
            
    } else if (tag === 'video') {    
        myTag.setAttribute('autoplay','true');
        myTag.width             = this.width;
        myTag.height            = this.height;
        myTag.id                = tagId;
        if (this.mediaStream !== '') {
            myTag.src = window.URL.createObjectURL(this.mediaStream);
        }
        this.videoTag   = myTag;
    }
            
    document.getElementById(this.videoTagIdHost).appendChild(myTag);    
}

Ok, right, that’s huge work, let’s have fun now by starting the record

//Remove result video tag and recreate it to empty cache
var videoElement = document.getElementById(this.resultTagId);   
if (videoElement) {
    videoElement.remove();
}
        
this.resetTags();
       
if (this.hideWebcamWhileRecording) {
    //Hide video stream while recording for performance
    this.showHideStream('hide');
}

this.hasStopped = false;
		
this.startTime   = Date.now();

this.frames      = []; // clear existing frames;
console.log('Recording video...');

this.rafId = requestAnimationFrame(this.drawVideoFrame_.bind(this));

return true; 

Notice, we have here a drawVideoFrame method. This method save each frame from canvas in an array, like this:

this.ctx.drawImage(this.videoTag, 0, 0, this.videoTag.width, this.videoTag.height);
var url = this.canvasTag.toDataURL('image/webp', 1);
this.frames.push(url);

this.rafId   = requestAnimationFrame(this.drawVideoFrame_.bind(this));

We don’t forget the showhide method, that will simplify our actions

if (status === 'show') {
    this.videoTag.style.visibility  = 'visible';
    this.videoTag.style.display     = 'block';            
} else if (status === 'hide') {
    this.videoTag.style.visibility  = 'hidden';
    this.videoTag.style.display     = 'none';           
}

So, what happens whe we decide to stop the video?
Naturally we calculate the real recorded time, that will be divided by the number of frames recorded, so we have the real video fps.
If the record was equal to max record time specified, we estimate the number of frames that will be captured.
If we have much frames than expected, we delete the extra frames, and send the other frames to Whammy for encoding.
And, we can stream, download and save the video on the server.

this.endTime = Date.now();   
        
this.hasStopped = true; 
        
cancelAnimationFrame(this.rafId);

//Recorded time
var recordedTime = (this.endTime - this.startTime)/1000;

console.log('Captured frames: ' + this.frames.length + ' => ' + recordedTime + 's video');

//We consider that the normal gap between Max recording time and real user recording time is 1s
//When the gap is greater than 1s, we auto-check the fps to synchronize the medias
var recordingGap        = this.maxRecordTime - recordedTime;

//Detect fps
var fps                 = (this.frames.length / recordedTime).toFixed(1); 
var encodingRatio       = fps; 

//Expected frames is the number of frames expected depending on the max record time
var expectedFrames      = (recordingGap <= 1) ? encodingRatio * this.maxRecordTime : encodingRatio * recordedTime ;
var unexpectedFrames    = this.frames.length - expectedFrames; 

//If there are more frames than expected, remove unexpected frames
if (unexpectedFrames > 0) {
    var i=0;
    for (i = 0; i <= unexpectedFrames; i++) { 
       this.frames.pop();
    }
} else {
    encodingRatio   = ((this.frames.length * fps) / expectedFrames).toFixed(3);
}                

console.log('Stop Recording video!'); 
console.log('My FPS => '+ fps);
console.log(encodingRatio);             

var webmBlob = this.whammy.fromImageArray(this.frames, encodingRatio);
console.log(webmBlob);
        
if (method === 'save') {
    this.save(webmBlob, false);
            
} else if (method === 'download') {
    this.download(webmBlob, false);
            
} else if (method === 'stream') {
    this.stream(webmBlob);

} else if (method === 'saveAndDownload') {
    this.save(webmBlob, false);
    this.download(webmBlob, false);
                      
} else if (method === 'saveAndStream') {
    this.save(webmBlob, true);
            
} else if (method === 'downloadAndStream') {
    this.download(webmBlob, true);
            
} else {
    this.save(webmBlob, false);
}

//If specified, we show original video stream        
if (this.showStreamOnFinish) {
    this.showHideStream('show');
}
        
//Empty frames for next video capture
this.frames = [];
               
return true;

Right, now we can specify our “download”, “save” and “stream” methods.

Save method

var datas   = 'path='+this.mediaPath+'&format='+this.videoFormat;                  

var client = new XMLHttpRequest();
client.onreadystatechange = function() 
{
    if (client.readyState === 4 && client.status === 200) 
    {
        console.log(client.response);

        //Get the video link, so we can use it later in other scripts
        this.videoLink = client.response;

        if (stream) {
            this.stream(blob);
        }
    }
}.bind(this);                    
client.open("post", this.phpFile+'?'+datas, true);
client.setRequestHeader("X-Requested-With", "XMLHttpRequest");
client.setRequestHeader("cache-Control", "no-store, no-cache, must-revalidate");
client.setRequestHeader("cache-Control", "post-check=0, pre-check=0");
client.setRequestHeader("cache-Control", "max-age=0");
client.setRequestHeader("Pragma", "no-cache");            
client.setRequestHeader("X-File-Name", encodeURIComponent('1'));
client.setRequestHeader("Content-Type", "application/octet-stream");
client.send(blob);

Download method

var url             = window.URL.createObjectURL(blob);
//Create a link
var hf              = document.createElement('a');

var temporaryId     = new Date().toISOString();
        
//Define link attributes
hf.href             = url;
hf.id               = temporaryId;
hf.download         = temporaryId + this.videoFormat;
hf.innerHTML        = hf.download;
hf.style.display    = 'none';
hf.style.visibility = 'hidden';
//Append the link inside html code
document.body.appendChild(hf);

//Simulate click on link to download file, and instantly delete link
document.getElementById(hf.id).click();
document.getElementById(hf.id).remove();

if (stream) {
    this.stream(blob);
}

And finally we define the stream method

var url             = window.URL.createObjectURL(blob);
        
var videoResult = document.createElement('video');
videoResult.src = url;
videoResult.setAttribute('autoplay', false);         
videoResult.setAttribute('controls', true);        
videoResult.id  = this.resultTagId;
        
document.getElementById(this.resultTagIdHost).appendChild(videoResult);
        
videoResult.pause(); 

Thank you for reading this article.
 
To download the full code, click here: https://github.com/edouardkombo/jsHtml5VideoRecorder
 
To see a live demo, it’s here: https://edouardkombo.github.io/jsHtml5VideoRecorder/demo
 
To download directly from Bower:

bower install js-html5-video-recorder
Advertisements
jsHtml5VideoRecorder – Record video stream in html5 with automatic fps

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s