Showing posts with label Tips & Tricks. Show all posts
Showing posts with label Tips & Tricks. Show all posts

Write Screen Sharing Application for Your Own WebSite

Do you want to write multi-user screen sharing application for your own website?

Note: This tutorial uses WebRTC for multi-user screen sharing.

First step, download all codes from this directory: github/Chrome-Extensions/desktopCapture

Second step, modify manifest.json file line 17, and replace with either localhost or your real domain name:

{
    "content_scripts": [ {
       "js": [ "content-script.js" ],
       "all_frames": true,
       "run_at": "document_end",
       "matches": ["https://your-domain.com/*"]
    }]
}

Fourth step, either max a zip and deploy new chrome extension to Google App Store, or install directly using "chrome://extensions" page.


Fifth step, download ONE_to_MANY screen sharing demo from this page: RTCMultiConnection/demos/Screen-Sharing.html


Sixth step, modify a few lines in the above demo:

Search for these two lines:

<script src="/dist/RTCMultiConnection.min.js"></script>
<script src="/socket.io/socket.io.js"></script>

Replace above two lines with:

<script src="https://rtcmulticonnection.herokuapp.com//dist/RTCMultiConnection.min.js"></script>
<script src="https://rtcmulticonnection.herokuapp.com//socket.io/socket.io.js"></script>

Now search for this line:

connection.socketURL = '/';

Replace above line with:

connection.socketURL = 'https://rtcmulticonnection.herokuapp.com:443/';

Now you are done.

Seventh and the last step, upload above HTML file on any HTTPs domain.

You can try on LocalHost as well.

Non-LocalHost domains requires HTTPs.

You can upload to Google Drive, DropBox or any free hosting website that supports HTTPs e.g. appspot.com.


How to test screen sharing?

First person shares his screen and about 10 people can join/see/view his screen.

You can open unlimited parallel rooms.

Maximum number of people who can view single screen is 14. We do not support more than 14 viewers/receivers.

There is a separate demo that allows us share screen over more than 14 users: https://rtcmulticonnection.herokuapp.com/demos/Scalable-Screen-Broadcast.html

Take photo from a webcam using JavaScript

Do you want to capture photos from your web-camera and generate images in any supported format e.g. PNG, JPEG, GIF, WebP etc.?

First method, capture photo using Canvas2D API:

Live demo: https://www.webrtc-experiment.com/takePhoto/

Below example captures photo from an HTML5 Video element:

var yourVideoElement = document.querySelector('video');

// pass your HTML5 Video Element
var photo = takePhoto(yourVideoElement);
window.open(photo);

function takePhoto(video) {
    var canvas = document.createElement('canvas');
    canvas.width = video.videoWidth || video.clientWidth;
    canvas.height = video.videoHeight || video.clientHeight;

    var context = canvas.getContext('2d');
    context.drawImage(video, 0, 0, canvas.width, canvas.height);

    return canvas.toDataURL('image/png');
}

Above code returns PNG. Here is how to get different formats:

var PNG = canvas.toDataURL('image/png');
var JPEG = canvas.toDataURL('image/jpeg');
var GIF = canvas.toDataURL('image/gif');
var WebP = canvas.toDataURL('image/webp', 1);


Second method, capture photo using "ImageCapture" API:

Live Demo: https://www.webrtc-experiment.com/ImageCapture/

You need to pass MediaStreamTrack (aka Video-Track) object:

if (typeof ImageCapture === 'function') {
    var firstVideoTrack = yourCameraStream.getVideoTracks()[0];
    var imageCapture = new ImageCapture(firstVideoTrack);
    imageCapture.takePhoto().then(function(blob) {
        var photo = URL.createObjectURL(blob);
        window.open(photo);
    });
}

Note: ImageCapture API requires Chrome version >= 60.

WebRTC captureStream API

You can use "captureStream" method to generate a realtime media stream from any HTML5 video or canvas-2d element.
var realtimeStream = canvasElement.captureStream(15);

Using captureStream API:

  • You can record WebGL based 3D games; you can also share in realtime with many users using RTCPeerConnection API
  • You can record canvas-2D animation or drawings; you can share in realtime as well using same RTCPeerConnection API
  • You can share your local video files e.g. WebM, Mp4, FLV, TS-M3U8 (hls-live-streaming) or audio files e.g. Wav, Ogg, Mp3 etc. You can record specific portions of these files as well.

How to record WebGL or Canvas2D animations?

You can use RecordRTC to record canvas-2D or 3D animations. You can try a few demos here:
First step, generate a realtime stream from HTML5 canvas element:
var realtimeStream = canvasElement.captureStream(15);
The parameter "15" is named as "frame-rates". We requested 15 fps stream.

Second step, record the resulting stream using RecordRTC or MediaStreamRecorder API:
var realtimeStream = canvasElement.captureStream();

var parameters = {
    mimeType: 'video/webm'
};

var recorder = RecordRTC(realtimeStream, parameters);

recorder.setRecordingDuration(5 * 1000).onRecordingStopped(function() {
    var recordedVideo = recorder.getBlob();
    var file = new File([recordedVideo], 'filename.webm', {
        type: 'video/webm'
    });
    var fileURL = URL.createObjectURL(file);
    window.open(fileURL);
});

recorder.startRecording();
Third and last step, repeatedly draw shapes or animations on canvas 2d surface:
var context = canvasElement.getContext('2d');

(function looper() {
    context.clearRect(0, 0, canvasElement.width, canvasElement.height);
    context.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);

    // draw shapes every 10 milliseconds
    setTimeout(looper, 10);
})();

How to broadcast/share WebGL or Canvas2D animations with other users?

You can "[RTCPeerConnection] addStream" method to share (broadcast) canvas-2D or 3D animations with remote users. You can try this demo:
First step, generate a realtime stream from HTML5 canvas element:
var realtimeStream = canvasElement.captureStream(15);
Second and last step, share with remote users using RTCPeerConnection API:
var peer = new webkitRTCPeerConnection(parameters);

// check this line
peer.addStream(canvasStream);

// now either create offer or answer descriptions
peer.createOffer(successCallback, failureCallback, parameters);

How to share or record microphone audio along with Canvas2D animations?

It is simple: use "addTrack" method to merge tracks.
var microphoneStream = captureUsingNavigatorDotGetUserMediaAPI({
    audio: true
});

// merge audio tracks into canvas stream
microphoneStream.getAudioTracks().forEach(function(track) {
    // check this line
    canvasStream.addTrack(track);
});

// now either record "canvasStream" or share using RTCPeerConnection API
peer.addStream(canvasStream); // microphone+canvas2D

Can I capture stream from an <audio> element?

Simply yes. You can capture audio either from <audio> tag or otherwise <video> element.

Remember: <video> tag also accepts audio-only stream. Your stream do NOT need to have video tracks. Here is how to capture audio stream from a <video> element:
videoElement.src = URL.createObjectURL(microphoneStream);

// capture from <video> tag
var capturedStream = videoElement.captureStream();

// initiate a standalone instance
var audioOnlyStream = new webkitMediaStream();

// merge only audio tracks
capturedStream.getAudioTracks().forEach(function(track) {
    audioOnlyStream.addTrack(track);
});

// either record "audioOnlyStream" or share using RTCPeerConnection API
peer.addStream(audioOnlyStream);

Unable to use "captureStream" API?

You may need to enable following chrome-flag; then relaunch the chrome browser:
chrome://flags/#enable-experimental-web-platform-features

References:

Disable ICE Trickling

ICE trickling is a process where candidate-pairs are shared as soon as gathered by the ICEAgent.

Its true that, there is NO JavaScript API "currently" available in RTCWeb drafts to disable ICE-trickling process however, there is a trick that can be used to merge all candidate pairs in the session-description, and then you merely need to share that "single" SDP only.

The trick is simple: Wait until "end-of-candidate" signal is fired.

Usually "onicecandidate" event returns "NULL" entry for "event.candidate" object.

In "old-good" days, we were watching for "oniceconnectionstatechange" event, and checking for "peer.iceGatheringState === 'complete'" to return the SDP.

BTW, you can still listen for both "end-of-candidate" NULL value, as well as "peer.iceGatheringState === 'complete'".

peer.oniceconnectionstatechange = function(event) {
    if (peer.iceGatheringState === 'complete') {
        send_sdp_to_remote_peer();
    }
};

peer.onicecandidate = function(event) {
    if (event.candidate === null) {
        return send_sdp_to_remote_peer();
    }
};

var isSdpSent = false;

function send_sdp_to_remote_peer() {
    if (isSdpSent) return;
    isSdpSent = true;
  
    var sdp = peer.localDescription;
    socket.emit('remote-sdp', sdp);
}


WebRTC Tips & Tricks

This blog post is added for WebRTC newbies and beginners who wanna learn key-ideas; get code snippets and enjoy WebRTC life!

1. How to mute/unmute media streams?

Remember, mute/unmute isn't implemented as a default/native features in either media capturing draft i.e. getUserMedia API also in WebRTC draft i.e. RTCPeerConnection API.

Also, there is no "onmuted" and "onunmuted" event defined or fired in the WebRTC native implementations.

Usually, as per chromium team's suggestions, media-tracks are enabled/disabled to mute/unmute the streams.

Remember, "MediaStreamTrack.enabled=false" NEVER sends silence audio or blank/black video; it doesn't stop packet transmission. Although when you set "MediaStreamTracks.enabled=false", packets are devoid of meaningful data. A solution for this approach is to hold/unhold tracks from SDP and renegotiate the connections. See next section for more information.

MediaStream object is just a synchronous container. You shouldn't consider it an object interacting with the media source (audio/video input/output devices). That's why it was suggested to move "stop" method from "MediaStream" level to "MediaStreamTracks" level.

MediaStreamTracks are written to define input/output devices' kind. A single MediaStreamTrack object can contain multiple media devices.

MediaStreamTracks has "enabled" property as well as "stop" method. You can use "stop" method to leave relevant media sources — whether it is audio source or video one.

Mute/UnMute is usually happened by setting Boolean values for the "enabled" property per each MediaStreamTrack.

When a MediaStreamTrack is "enabled=true" it is unmuted; when a MediaStreamTrack is "enabled=false" it is muted.

var audioTracks = localMediaStream.getAudioTracks();
var videoTracks = localMediaStream.getVideoTracks();

// if MediaStream has reference to microphone
if (audioTracks[0]) {
    audioTracks[0].enabled = false;
}

// if MediaStream has reference to webcam
if (videoTracks[0]) {
    videoTracks[0].enabled = false;
}

Keep in mind that you're disabling a media track locally; it will not fire any event on target users side. If you disabled video track; then it will cause "blank-video" on target-users side.

You can manually fire events like "onmuted" or "onmediatrackdisabled" by using socket that was used for signaling. You can send/emit messages like:

yourSignalingSocket.send({
    isMediaStreamTrackDisabled: true,
    mediaStreamLabel: stream.label
});

Target users' "onmessage" handler can watch for "isMediaStreamTrackDisabled" Boolean and fire "onmediatrackdisabled" accordingly:

yourSignalingSocket.onmessage = function (event) {
    var data = event.data;

    if (data.isMediaStreamTrackDisabled == true) {
        emitEvent('mediatrackdisabled', true);
    } else emitEvent('mediatrackdisabled', false);
};

Last point; disabling "remote" media stream tracks are useless unless remote user disables his "local" media stream tracks. So, use signaling-socket, exchange messages between users to inform them enable/disable media tracks and fire relevant events that can be used to display video-posters or overlappers!

People usually asks about "video.pause()" and "video.muted=true". Remember, you're simply setting playback status of the local video element; it will NEVER synchronize changes to other peers until you listen for "video.onplay" and "video.onpause" handlers.

You can also listen for "video.onvolumechange" to synchronize volume among all users.

Remember, you can use WebRTC data channels or websocket or any other signaling mean to synchronize such statuses.

2. How to hold/unhold media calls?

Currently, WebRTC signaling mechanism is based on offer/answer model which in turns uses session-description protocol (SDP) to exchange metadata and key-session-requirements among peers.

SDP is formatted in a way that MediaStreamTracks are given unique media-line; and each media line can contain references to multiple similar media tracks.

Each media-line has "a=sendrecv" attribute; which is used to exchange incoming/outgoing media directions. You can easily replace "sendrecv" with "inactive" to make that media track on hold.

Don't forget that a single media-line can contain multiple media stream tracks; so if you're planning to hold all audio tracks; then search for audio media-line in the SDP; and replace "a=sendrecv" with "a=inactive".

Also, please keep in mind that "sendrecv" isn't the only value given to "a=" attribute. Possible values are:
  1. sendrecv ——— two-way media flow
  2. sendonly ——— one-way outgoing media flow
  3. recvonly ——— one-way incoming media flow
  4. inactive  ——— call on hold; i.e. no media flow

So, you need to replace first three values with "inactive" to make list of identical media tracks on hold; then you can replace "inactive" with previous value to leave the "hold" status.

After altering local-session-descriptions (peer.localDescription); you MUST renegotiate peer connections to make sure new changes are applied on both peers side.

Renegotiation is a process to recreate offer/answer descriptions and set remote descriptions again.

Remember, you're not creating new peer connections; you're using existing peer objects and invoking createOffer/createAnswer as well as setRemoteDescription again.

Renegotiation works only on chromium based browsers in the moment; so hold/unhold feature will work only on chrome/opera.

You can learn more about renegotiation here: https://www.webrtc-experiment.com/docs/how-to-switch-streams.html

3. How to check if a peer connection is established?

Simply set an event listener for "oniceconnectionstatechange" and check for "peer.iceConnectionState=='completed'".

ICE connection state flows like this:

(success) new => checking => connected => completed
(failure) new => checking => connected => disconnected => new => closed



Sometimes ICE connection gets dropped out of network loss or unexpected situations. In such cases, "ice-connection-state" is always changed to "disconnected".

There is another dangerous point is that if you're renegotiating peers; then ice-connection-state is always changed to "disconnected" then it restarts: "new => checking => connected => completed => disconnected => new => checking => connected => completed"..

var peer = new RTCPeerConnection(iceSerersArray, optionalArgsArray);
peer.oniceconnectionstatechange = function() {
   if(peer.iceConnectionState == 'completed') {
      var message = 'WebRTC RTP ports are connected to UDP. ';
      message += 'Wait a few seconds for remote stream to be started flowing.';
      alert(message);
   }
};

4. How to check if a peer connection is closed or dropped?

Again, watch for "oniceconnectionstatechange" event handler and check for "peer.iceConnectionState=='disconnected'".

ICE Agent changes relevant candidates' state to "disconnected" in following cases:
  1. If peer connection is closed.
  2. If you're renegotiating media.....in this case, previous peer connection is closed and re-established again.

var peer = new RTCPeerConnection(iceSerersArray, optionalArgsArray);
peer.oniceconnectionstatechange = function() {
   if(peer.iceConnectionState == 'disconnected') {
      var message = 'WebRTC RTP ports are closed. ';
      message += 'UDP connection is dropped.';
      alert(message);
   }
};

5. How to check if browser has microphone or webcam?

There is a JavaScript library named as "DetectRTC.js"; which uses "getSources/getMediaDevices" API to fetch list of all audio/video input devices.

If there is no audio input device; then it says that:

Your browser has NO microphone attached or you clicked deny button sometime and browser is still denying the webpage to access relevant media.

CheckDeviceSupport(function (chrome) {
    if (chrome.hasMicrophone) {}

    if (chrome.hasWebcam) {}
});

// above function is defined here
function CheckDeviceSupport(callback) {
    var Chrome = {};

    // This method is useful only for Chrome!

    // "navigator.getMediaDevices" will be next!
    // "MediaStreamTrack.getSources" will be removed.

    // 1st step: verify "MediaStreamTrack" support.
    if (!window.MediaStreamTrack) return;

    // 2nd step: verify "getSources" support which is planned to be removed soon!
    // "getSources" will be replaced with "getMediaDevices"
    if (!MediaStreamTrack.getSources) {
        MediaStreamTrack.getSources = MediaStreamTrack.getMediaDevices;
    }

    // if still no "getSources"; it MUST be firefox!
    if (!MediaStreamTrack.getSources) {
        // assuming that it is older chrome or chromium implementation
        if (!!navigator.webkitGetUserMedia) {
            Chrome.hasMicrophone = true;
            Chrome.hasWebcam = true;
        }

        return;
    }

    // loop over all audio/video input/output devices
    MediaStreamTrack.getSources(function (sources) {
        var result = {};

        for (var i = 0; i &lt; sources.length; i++) {
            result[sources[i].kind] = true;
        }

        Chrome.hasMicrophone = result.audio;
        Chrome.hasWebcam = result.video;
        
        callback(Chrome);
    });
}


6. How to fix echo/noise issues?

According to this page:
Echo is a distortion of voice that occurs either when you place input/output audio devices closed together; or audio output level is too high or CPU usage exceeded by other applications or by the same application.

Firefox 29 and upper builds has a nice echo cancellation. Echo is also improved in chrome 34 and upper builds.

Make sure that your speaker's kHz values matches with your application's kHz values. Mismatch will lead to echo.

If you're using Mac OSX; then you can easily recover echo issues by enabling "ambient noise reduction". You can search the Google engine for how to enable for build-in audio devices on Mac.

It is possible to watch audio RTP-packets' audio level using getStats API; however there is no API other than Media Processing API that allows setting volume from JavaScript applications. Media Processing draft isn't standardized yet and AFAIK, none of the browser vendors implemented volume-specific API. Though, Gecko team implemented captureStreamUntilEnded API in their Firefox product from the same draft.

Make sure that you're not using WebAudio API along with getUserMedia or RTCPeerConnection API; because sometimes you accidentally or intentionally connect input node with output node which obviously causes huge echo.

Developers were suggesting headphones/microphones to overcome echo issues however it is appeared that such solutions doesn't matters. One must check all relevant conditions to make sure there is nothing causing noise.

7. How to check estimated bandwidth?

Sometimes it is known as "available bandwidth" or "bandwidth consumed". You can setup a listener over getStats API to check bytesSent per second. Then you can easily find-out bandwidth that your application is currently consuming.

Remember, you're checking bandwidth in one-to-one scenario; you need to follow some deeper-level tricks to find estimated bandwidth for multi-peers scenarios.

Following example assumes that you opened audio-only connection between two users; and you're checking bandwidth consumed by outgoing RTP ports.

function getStats(peer) {
    _getStats(peer, function (results) {
        for (var i = 0; i &lt; results.length; ++i) {
            var res = results[i];

            if (res.googCodecName == 'opus') {
                if (!window.prevBytesSent) 
                    window.prevBytesSent = res.bytesSent;

                var bytes = res.bytesSent - window.prevBytesSent;
                window.prevBytesSent = res.bytesSent;

                var kilobytes = bytes / 1024;
                console.log(kilobytes.toFixed(1) + ' kbits/s');
            }
        }

        setTimeout(function () {
            getStats(peer);
        }, 1000);
    });
}

// a wrapper around getStats which hides the differences (where possible)
// following code-snippet is taken from somewhere on the github
function _getStats(peer, cb) {
    if (!!navigator.mozGetUserMedia) {
        peer.getStats(
            function (res) {
                var items = [];
                res.forEach(function (result) {
                    items.push(result);
                });
                cb(items);
            },
            cb
        );
    } else {
        peer.getStats(function (res) {
            var items = [];
            res.result().forEach(function (result) {
                var item = {};
                result.names().forEach(function (name) {
                    item[name] = result.stat(name);
                });
                item.id = result.id;
                item.type = result.type;
                item.timestamp = result.timestamp;
                items.push(item);
            });
            cb(items);
        });
    }
};

You can use it like this:

peer.onaddstream = function(event) {
   getStats(peer);
};

8. How to listen Audio/Video elements native events?

You can easily override "onpause", "onplay", and "onvolumechange" events.

htmlVideoElement.onplay = function () {
    // this event is fired each time when you playback the video
    // via play() method or muted=false or paused=false
};

htmlVideoElement.onpause = function () {
    // this event is fired each time when you pause/stop playback
    // via pause() or muted=true or paused=true or stop()
};

htmlVideoElement.onvolumechange = function () {
    // htmlVideoElement.volume
};

9. How to get list of all audio/video input devices?

Note: Chromium team is implementing "navigator.getMediaDevices" interface which will allow you prefetch both input and output devices.

Following code snippet uses "MediaStreamTrack.getSources" to fetch-out all input audio/video devices.

function getInputDevices(callback) {
    // This method is useful only for Chrome!

    var devicesFetched = {};

    // 1st step: verify "MediaStreamTrack" support.
    if (!window.MediaStreamTrack &amp;&amp; !navigator.getMediaDevices) {
        return callback(devicesFetched);
    }

    if (!window.MediaStreamTrack &amp;&amp; navigator.getMediaDevices) {
        window.MediaStreamTrack = {};
    }

    // 2nd step: verify "getSources" supported which is planned to be removed soon!
    // "getSources" will be replaced with "getMediaDevices"
    if (!MediaStreamTrack.getSources) {
        MediaStreamTrack.getSources = MediaStreamTrack.getMediaDevices;
    }

    // todo: need to verify if this trick works
    // via: https://code.google.com/p/chromium/issues/detail?id=338511
    if (!MediaStreamTrack.getSources &amp;&amp; navigator.getMediaDevices) {
        MediaStreamTrack.getSources = navigator.getMediaDevices.bind(navigator);
    }

    // if still no "getSources"; it MUST be firefox!
    // or otherwise, it will be older chrome
    if (!MediaStreamTrack.getSources) {
        return callback(devicesFetched);
    }

    // loop over all audio/video input/output devices
    MediaStreamTrack.getSources(function (media_sources) {
        var sources = [];
        for (var i = 0; i &lt; media_sources.length; i++) {
            sources.push(media_sources[i]);
        }

        getAllUserMedias(sources);

        if (callback) callback(devicesFetched);
    });

    var index = 0;

    function getAllUserMedias(media_sources) {
        var media_source = media_sources[index];
        if (!media_source) return;

        // to prevent duplicated devices to be fetched.
        if (devicesFetched[media_source.id]) {
            index++;
            return getAllUserMedias(media_sources);
        }
      
        devicesFetched[media_source.id] = media_source;

        index++;
        getAllUserMedias(media_sources);
    }
}

You can use it like this:

getInputDevices(function (devices) {
    for (var device in devices) {
        device = devices[device];

        // device.kind == 'audio' || 'video'
        console.log(device.id, device.label);
    }
});

10. How to choose STUN or TURN and skip Host/Local candidates?

Currently it is not possible for chrome to fetch only STUN or TURN candidates.

A quick workaround is to skip calling "addIceCandidate" for candidates you wanna skip:

var host      = false;
var reflexive = false;
var relay     = true;

peer.onicecandidate = function(e) {
     var ice = e.candidate;
     if(!ice) return;
  
     if(host &amp;&amp; ice.candidate.indexOf('typ host ') == -1) return;
     if(reflexive &amp;&amp; ice.candidate.indexOf('typ srflx ') == -1) return;
     if(relay &amp;&amp; ice.candidate.indexOf('typ relay ') == -1) return;
  
     POST_to_Other_Peer(ice);
};

Above code snippet is taken from this link.

Updated at Jan 22, 2016

You can check tons of other WebRTC tips & tricks at webrtc-pedia page:

https://www.webrtc-experiment.com/webrtcpedia/

Save files on disk using JavaScript or JQuery!

You can save any file, or DataURL, or Blob on disk using HTML5's newly introduced "download" attribute.

Use cases:

1. Force browser to download/save files like PDF/HTML/PHP/ASPX/JS/CSS/etc. on disk
2. Concatenate all transmitted blobs and save them as file on disk - it is useful in file sharing applications

Microsoft Edge? (msSaveBlob/msSaveOrOpenBlob) https://msdn.microsoft.com/en-us/library/hh779016(v=vs.85).aspx

/**
 * @param {Blob} file - File or Blob object. This parameter is required.
 * @param {string} fileName - Optional file name e.g. "image.png"
 */
function invokeSaveAsDialog(file, fileName) {
    if (!file) {
        throw 'Blob object is required.';
    }

    if (!file.type) {
        try {
            file.type = 'video/webm';
        } catch (e) {}
    }

    var fileExtension = (file.type || 'video/webm').split('/')[1];

    if (fileName && fileName.indexOf('.') !== -1) {
        var splitted = fileName.split('.');
        fileName = splitted[0];
        fileExtension = splitted[1];
    }

    var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension;

    if (typeof navigator.msSaveOrOpenBlob !== 'undefined') {
        return navigator.msSaveOrOpenBlob(file, fileFullName);
    } else if (typeof navigator.msSaveBlob !== 'undefined') {
        return navigator.msSaveBlob(file, fileFullName);
    }

    var hyperlink = document.createElement('a');
    hyperlink.href = URL.createObjectURL(file);
    hyperlink.download = fileFullName;

    hyperlink.style = 'display:none;opacity:0;color:transparent;';
    (document.body || document.documentElement).appendChild(hyperlink);

    if (typeof hyperlink.click === 'function') {
        hyperlink.click();
    } else {
        hyperlink.target = '_blank';
        hyperlink.dispatchEvent(new MouseEvent('click', {
            view: window,
            bubbles: true,
            cancelable: true
        }));
    }

    (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href);
}

Here is how to use above function:

var textFile = new Blob(['Hello Sir'], {
   type: 'text/plain'
});
invokeSaveAsDialog(textFile, 'TextFile.txt');

You can pass two arguments over "SaveToDisk" function:

1. file-URL or blob or data-URL - it is mandatory
2. file name - it is optional

Here is "SaveToDisk" function uses new syntax of "createEvent" API:

function SaveToDisk(fileURL, fileName) {
    // for non-IE
    if (!window.ActiveXObject) {
        var save = document.createElement('a');
        save.href = fileURL;
        save.download = fileName || 'unknown';
        save.style = 'display:none;opacity:0;color:transparent;';
        (document.body || document.documentElement).appendChild(save);

        if (typeof save.click === 'function') {
            save.click();
        } else {
            save.target = '_blank';
            var event = document.createEvent('Event');
            event.initEvent('click', true, true);
            save.dispatchEvent(event);
        }

        (window.URL || window.webkitURL).revokeObjectURL(save.href);
    }

    // for IE
    else if (!!window.ActiveXObject && document.execCommand) {
        var _window = window.open(fileURL, '_blank');
        _window.document.close();
        _window.document.execCommand('SaveAs', true, fileName || fileURL)
        _window.close();
    }
}

Here is "SaveToDisk" function uses old syntax of "createEvent" API:

function SaveToDisk(fileURL, fileName) {
    // for non-IE
    if (!window.ActiveXObject) {
        var save = document.createElement('a');
        save.href = fileURL;
        save.target = '_blank';
        save.download = fileName || fileURL;
        var evt = document.createEvent('MouseEvents');
        evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0,
            false, false, false, false, 0, null);
        save.dispatchEvent(evt);
        (window.URL || window.webkitURL).revokeObjectURL(save.href);
    }

    // for IE
    else if ( !! window.ActiveXObject && document.execCommand)     {
        var _window = window.open(fileURL, "_blank");
        _window.document.close();
        _window.document.execCommand('SaveAs', true, fileName || fileURL)
        _window.close();
    }
}

You can use FileReader too, to save "Blob" on disk:

function SaveToDisk(blobURL, fileName) {
    var reader = new FileReader();
    reader.readAsDataURL(blobURL);
    reader.onload = function(event) {
        var save = document.createElement('a');
        save.href = event.target.result;
        save.download = fileName || 'unknown file';

        save.style = 'display:none;opacity:0;color:transparent;';
        (document.body || document.documentElement).appendChild(save);

        if (typeof save.click === 'function') {
            save.click();
        } else {
            save.target = '_blank';
            var event = document.createEvent('Event');
            event.initEvent('click', true, true);
            save.dispatchEvent(event);
        }

        (window.URL || window.webkitURL).revokeObjectURL(save.href);
    };
}

Using "SaveToDisk" function

Force an image to be saved on disk instead of rendered by the browser:

SaveToDisk('https://muazkh.appspot.com/images/Curvature.PNG', 'image.png');

Force downloading of pdf files (it will NEVER allow any browser specific pdf-reader to render/open your pdf files):

SaveToDisk('https://muazkh.googlecode.com/files/Muaz-Khan-CV.pdf');

Even you can enforce following web-browser specific files to be downloaded/saved on the disk instead of rendered within the browser!

JavaScript SaveToDisk('/javascript-file.js')
HTML SaveToDisk('/html-file.html')
CSS SaveToDisk('/css-file.css')
ASPX SaveToDisk('/aspx-file.aspx')
PHP SaveToDisk('/php-file.php')
MVC SaveToDisk('/controller/action-method')

Following apps are using "SaveToDisk" function to save files/blobs/etc. on disk:

DataChannel.js A library for realtime data/file sharing using WebRTC!
RTCMultiConnection.js A library for realtime audio/video/screen/data and file sharing using WebRTC!
Group File Sharing Sharing files over multiple peer connections concurrently
RecordRTC A library for WebRTC-Developers to record audio and video streams

Specification

1. https://developer.mozilla.org/en-US/docs/DOM/document.createEvent
2. http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-DocumentEvent-createEvent

function SaveToDisk(fileUrl, fileName) {
    var hyperlink = document.createElement('a');
    hyperlink.href = fileUrl;
    hyperlink.download = fileName || fileUrl;
    hyperlink.style = 'display:none;opacity:0;color:transparent;';
    (document.body || document.documentElement).appendChild(hyperlink);

    if (typeof hyperlink.click === 'function') {
        hyperlink.click();
    } else {
        hyperlink.target = '_blank';
        hyperlink.dispatchEvent(new MouseEvent('click', {
            view: window,
            bubbles: true,
            cancelable: true
        }));
    }

    (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href);
}

Remember, "SaveToDisk" works fine on Firefox nightly and aurora.

To support Firefox general release; use same workaround that is used for IE in this post (with a little bit changes).

If Firefox fails:

It seems that firefox doesn't allows dispatching (click) event handlers if HYPER-LINK element is NOT in the DOM-tree.

Don't call "revokeObjectURL" outside the "onclick" handler.

You simply need to append HYPER-LINK element  into DOM; and remove it after "onclick" is fired:

function SaveToDisk(fileUrl, fileName) {
    var hyperlink = document.createElement('a');
    hyperlink.href = fileUrl;
    hyperlink.download = fileName || fileUrl;

    hyperlink.style = 'display:none;opacity:0;color:transparent;';
    (document.body || document.documentElement).appendChild(hyperlink);

    hyperlink.onclick = function() {
        hyperlink.parentNode.removeChild(hyperlink);
        (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href);
    };

    if (typeof hyperlink.click === 'function') {
        hyperlink.click();
    } else {
        hyperlink.target = '_blank';
        hyperlink.dispatchEvent(new MouseEvent('click', {
            view: window,
            bubbles: true,
            cancelable: true
        }));
    }
}