betajs/betajs-media

View on GitHub
src/webrtc/webrtc_support.js

Summary

Maintainability
F
6 days
Test Coverage
Scoped.define("module:WebRTC.Support", [
    "base:Promise",
    "base:Objs",
    "browser:Info",
    "browser:Dom",
    "base:Time"
], function(Promise, Objs, Info, Dom, Time) {
    return {

        canvasSupportsImageFormat: function(imageFormat) {
            try {
                var data = document.createElement('canvas').toDataURL(imageFormat);
                var headerIdx = data.indexOf(";");
                return data.substring(0, data.indexOf(";")).indexOf(imageFormat) != -1;
            } catch (e) {
                return false;
            }
        },

        getGlobals: function() {
            var getUserMedia = null;
            var getUserMediaCtx = null;

            if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
                getUserMedia = navigator.mediaDevices.getUserMedia;
                getUserMediaCtx = navigator.mediaDevices;
            } else {
                getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
                getUserMediaCtx = navigator;
            }

            var URL = window.URL || window.webkitURL;
            var MediaRecorder = window.MediaRecorder;
            var AudioContext = window.AudioContext || window.webkitAudioContext;
            if (AudioContext) {
                Dom.userInteraction(function() {
                    if (!this.__globals)
                        return;
                    this.__globals.audioContext = new AudioContext();
                }, this);
            }
            var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
            var RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
            var RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
            return {
                getUserMedia: getUserMedia,
                getUserMediaCtx: getUserMediaCtx,
                URL: URL,
                MediaRecorder: MediaRecorder,
                AudioContext: AudioContext,
                audioContextScriptProcessor: function() {
                    return (this.createScriptProcessor || this.createJavaScriptNode).apply(this, arguments);
                },
                webpSupport: this.canvasSupportsImageFormat("image/webp"),
                RTCPeerConnection: RTCPeerConnection,
                RTCIceCandidate: RTCIceCandidate,
                RTCSessionDescription: RTCSessionDescription,
                WebSocket: window.WebSocket,
                supportedConstraints: navigator.mediaDevices && navigator.mediaDevices.getSupportedConstraints ? navigator.mediaDevices.getSupportedConstraints() : {}
            };
        },

        globals: function() {
            if (!this.__globals)
                this.__globals = this.getGlobals();
            return this.__globals;
        },

        userMediaSupported: function() {
            return !!this.globals().getUserMedia;
        },

        enumerateMediaSources: function() {
            var promise = Promise.create();
            var promiseCallback = function(sources) {
                var result = {
                    audio: {},
                    audioCount: 0,
                    video: {},
                    videoCount: 0
                };
                Objs.iter(sources, function(source) {
                    // Capabilities method which will show more detailed information about device
                    // https://www.chromestatus.com/feature/5145556682801152 - Status of the feature
                    var _sourceCapabilities;
                    if (source.kind.indexOf("video") === 0) {
                        result.videoCount++;
                        if (typeof source.getCapabilities !== 'undefined')
                            _sourceCapabilities = source.getCapabilities();
                        result.video[source.id || source.deviceId] = {
                            id: source.id || source.deviceId,
                            label: source.label,
                            capabilities: _sourceCapabilities
                        };
                    }
                    if (source.kind.indexOf("audio") === 0) {
                        result.audioCount++;
                        if (typeof source.getCapabilities !== 'undefined')
                            _sourceCapabilities = source.getCapabilities();
                        result.audio[source.id || source.deviceId] = {
                            id: source.id || source.deviceId,
                            label: source.label,
                            capabilities: _sourceCapabilities
                        };
                    }
                });
                promise.asyncSuccess(result);
            };
            try {
                if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices)
                    navigator.mediaDevices.enumerateDevices().then(promiseCallback);
                else if (MediaStreamTrack && MediaStreamTrack.getSources)
                    MediaStreamTrack.getSources(promiseCallback);
                else
                    promise.asyncError("Unsupported");
            } catch (e) {
                promise.asyncError(e);
            }
            return promise;
        },

        streamQueryResolution: function(stream) {
            var promise = Promise.create();
            var video = this.bindStreamToVideo(stream);
            video.addEventListener("playing", function() {
                setTimeout(function() {
                    promise.asyncSuccess({
                        stream: stream,
                        width: video.videoWidth,
                        height: video.videoHeight
                    });
                    video.remove();
                }, 500);
            });
            return promise;
        },

        chromeExtensionMessage: function(extensionId, data) {
            var promise = Promise.create();
            chrome.runtime.sendMessage(extensionId, data, promise.asyncSuccessFunc());
            return promise;
        },

        chromeExtensionExtract: function(meta) {
            var result = {};
            if (Info.isChrome()) {
                result.extensionId = meta.chromeExtensionId;
                result.extensionInstallLink = meta.chromeExtensionInstallLink;
            } else if (Info.isOpera()) {
                result.extensionId = meta.operaExtensionId;
                result.extensionInstallLink = meta.operaExtensionInstallLink;
            }
            return result;
        },

        userMedia: function(options) {
            var promise = Promise.create();
            var result = this.globals().getUserMedia.call(this.globals().getUserMediaCtx, options, function(stream) {
                promise.asyncSuccess(stream);
            }, function(e) {
                promise.asyncError(e);
            });
            try {
                if (result.then) {
                    result.then(function(stream) {
                        promise.asyncSuccess(stream);
                    });
                }
                if (result["catch"]) {
                    result["catch"](function(e) {
                        promise.asyncError(e);
                    });
                }
            } catch (e) {}
            return promise;
        },

        /*
         * audio: {} | undefined
         * video: {} | undefined
         *       width, height, aspectRatio
         * screen: true | {chromeExtensionId, operaExtensionId} | false
         */
        userMedia2: function(options) {
            var opts = {};
            var promise;
            if (options.audio)
                opts.audio = options.audio;
            if (options.screen && !options.video)
                options.video = {};
            if (!options.video)
                return this.userMedia(opts);
            if (options.screen) {
                options.video.width = options.video.width || window.innerWidth || document.body.clientWidth;
                options.video.height = options.video.height || window.innerHeight || document.body.clientHeight;
            }
            if (Info.isiOS()) {
                opts.video = {};
                if (options.video.width)
                    opts.video.width = options.video.width;
                if (options.video.height)
                    opts.video.height = options.video.height;
                if (options.video.frameRate)
                    opts.video.frameRate = options.video.frameRate;
                if (options.video.cameraFaceFront !== undefined)
                    opts.video.facingMode = {
                        exact: options.video.cameraFaceFront ? "user" : "environment"
                    };
                return this.userMedia(opts);
            } else if (options.screen && typeof navigator.mediaDevices.getDisplayMedia !== 'undefined') {
                /**
                 * https://w3c.github.io/mediacapture-screen-share/#constrainable-properties-for-captured-display-surfaces
                 * partial interface MediaDevices {
                 *    Promise<MediaStream> getDisplayMedea(optional DisplayMediaStreamConstraints constraints = {});
                 * };
                 * enum DisplayCaptureSurfaceType { "monitor", "window", "application", "browser"};
                 * enum CursorCaptureConstraint { "never", "always", "motion" };
                 */
                promise = Promise.create();
                var _self = this;
                var audio = typeof options.audio === 'boolean' ? options.audio : true;
                if (typeof options.video.resizeMode === 'undefined')
                    options.video.resizeMode = 'none';
                var videoOptions = {
                    cursor: 'motion',
                    resizeMode: options.video.resizeMode,
                    displaySurface: 'application'
                };
                if (parseInt(options.video.width, 10) > 0) {
                    videoOptions.width = parseInt(options.video.width, 10);
                }
                if (parseInt(options.video.height, 10) > 0) {
                    videoOptions.height = parseInt(options.video.height, 10);
                }
                var displayMediaPromise = navigator.mediaDevices.getDisplayMedia({
                    video: videoOptions,
                    audio: audio
                });
                displayMediaPromise.then(function(videoStream) {
                    if (videoStream.getAudioTracks().length < 1 && audio) {
                        _self.userMedia({
                                video: false,
                                audio: true
                            })
                            .mapError(function(err) {
                                promise.asyncSuccess(videoStream);
                            })
                            .mapSuccess(function(audioStream) {
                                promise.asyncSuccess(new MediaStream([videoStream.getTracks()[0], audioStream.getAudioTracks()[0]]));
                            });
                    } else {
                        promise.asyncSuccess(videoStream);
                    }
                });

                // Declaring catch this way will fix IE8 related `SCRIPT1010: Expected identifier`
                displayMediaPromise['catch'](function(err) {
                    promise.asyncError(err);
                });
                return promise;
            } else if (Info.isFirefox()) {
                opts.video = {};
                if (options.screen) {
                    opts.video.mediaSource = "screen";
                    if (!navigator.mediaDevices || !navigator.mediaDevices.getSupportedConstraints || !navigator.mediaDevices.getSupportedConstraints().mediaSource)
                        return Promise.error("This browser does not support screen recording.");
                }
                if (options.video.aspectRatio && !(options.video.width && options.video.height)) {
                    if (options.video.width)
                        options.video.height = Math.round(options.video.width / options.video.aspectRatio);
                    else if (options.video.height)
                        options.video.width = Math.round(options.video.height * options.video.aspectRatio);
                }
                if (options.video.width) {
                    opts.video.width = {
                        ideal: options.video.width
                    };
                }
                if (options.video.height) {
                    opts.video.height = {
                        ideal: options.video.height
                    };
                }
                if (options.video.frameRate) {
                    opts.video.frameRate = {
                        ideal: options.video.frameRate
                    };
                }
                if (options.video.sourceId) {
                    opts.video.sourceId = options.video.sourceId;
                    // Will fix change camera on FF
                    opts.video.deviceId = {
                        exact: options.video.sourceId
                    };
                }
                if (options.video.cameraFaceFront !== undefined && Info.isMobile())
                    opts.video.facingMode = {
                        exact: options.video.cameraFaceFront ? "user" : "environment"
                    };
                return this.userMedia(opts);
            } else if (Info.isEdge() && options.screen) {
                if (navigator.getDisplayMedia) {
                    promise = Promise.create();
                    var pr = navigator.getDisplayMedia({
                        video: true
                    });
                    pr.then(promise.asyncSuccessFunc());
                    pr['catch'](promise.asyncErrorFunc());
                    return promise;
                } else
                    return Promise.error("This browser does not support screen recording.");
            } else {
                if (Info.isAndroid()) {
                    opts.video = {};
                } else {
                    opts.video = {
                        mandatory: {}
                    };
                }
                // Will fix camera selection on change
                if (Info.isSafari()) {
                    if (options.video.sourceId) {
                        opts.video.sourceId = options.video.sourceId;
                        opts.video.deviceId = {
                            exact: options.video.sourceId
                        };
                    }
                }
                var as = options.video.aspectRatio ? options.video.aspectRatio : (options.video.width && options.video.height ? options.video.width / options.video.height : null);
                if (typeof opts.video.mandatory !== 'undefined') {
                    if (options.video.width) {
                        opts.video.mandatory.minWidth = options.video.width;
                        opts.video.mandatory.maxWidth = options.video.width;
                    }

                    if (!options.video.width && options.video.height) {
                        opts.video.mandatory.minHeight = options.video.height;
                        opts.video.mandatory.maxHeight = options.video.height;
                    }

                    if (options.video.frameRate) {
                        opts.video.mandatory.minFrameRate = options.video.frameRate;
                        opts.video.mandatory.maxFrameRate = options.video.frameRate;
                    }

                    if (options.video.sourceId)
                        opts.video.mandatory.sourceId = options.video.sourceId;
                } else {
                    // We cannot use Mandatory and normal constraints like facingMode together for Chrome
                    // but Safari also supports it
                    if (options.video.cameraFaceFront !== undefined && Info.isMobile()) {
                        // The { exact: } syntax means the constraint is required, and things fail if the user doesn't have the right camera.
                        // If you leave it out then the constraint is optional, which in Firefox for Android means it only changes the default
                        // in the camera chooser in the permission prompt.
                        opts.video.facingMode = {
                            exact: options.video.cameraFaceFront ? "user" : "environment"
                        };
                    }

                    // dictionary MediaTrackConstraintSet
                    // https://w3c.github.io/mediacapture-main/getusermedia.html#media-track-constraints
                    if (options.video.width) {
                        opts.video.width = options.video.width;
                    }

                    if (!options.video.width && options.video.height) {
                        opts.video.height = options.video.height;
                    }

                    if (options.video.frameRate) {
                        opts.video.frameRate = options.video.frameRate;
                    }

                    if (options.video.sourceId)
                        // TODO: test it just in case
                        opts.video.deviceId = options.video.sourceId;
                }
                if (as) {
                    if (typeof opts.video.mandatory !== 'undefined') {
                        opts.video.mandatory.minAspectRatio = as;
                        opts.video.mandatory.maxAspectRatio = as;
                    } else {
                        if (!options.video.aspectRatio)
                            opts.video.aspectRatio = as;
                    }
                }
                var probe = function(count) {
                    var mandatory = typeof opts.video.mandatory !== 'undefined' ? opts.video.mandatory : opts.video;
                    return this.userMedia(opts).mapError(function(e) {
                        count--;
                        if (e.name !== "ConstraintNotSatisfiedError" && e.name !== "OverconstrainedError")
                            return e;
                        var c = (e.constraintName || e.constraint).toLowerCase();
                        Objs.iter(mandatory, function(value, key) {
                            var lkey = key.toLowerCase();
                            if (lkey.indexOf(c) >= 0) {
                                var flt = lkey.indexOf("aspect") > 0;
                                var d = lkey.indexOf("min") === 0 ? -1 : 1;
                                var u = Math.max(0, mandatory[key] * (1.0 + d / 10));
                                mandatory[key] = flt ? u : Math.round(u);
                                if (count < 0) {
                                    delete mandatory[key];
                                    count = 100;
                                }
                            }
                        }, this);
                        return probe.call(this, count);
                    }, this);
                };
                if (options.screen) {
                    var extensionId = this.chromeExtensionExtract(options.screen).extensionId;
                    if (!extensionId)
                        return Promise.error("This browser does not support screen recording.");
                    var pingTest = Time.now();
                    return this.chromeExtensionMessage(extensionId, {
                        type: "ping",
                        data: pingTest
                    }).mapSuccess(function(pingResponse) {
                        promise = Promise.create();
                        if (!pingResponse || pingResponse.type !== "success" || pingResponse.data !== pingTest)
                            return Promise.error("This browser does not support screen recording.");
                        else
                            promise.asyncSuccess(true);
                        return promise.mapSuccess(function() {
                            return this.chromeExtensionMessage(extensionId, {
                                type: "acquire",
                                sources: ['window', 'screen', 'tab'],
                                url: window.self !== window.top ? window.location.href : null // if recorder is inside of iframe
                            }).mapSuccess(function(acquireResponse) {
                                if (!acquireResponse || acquireResponse.type !== 'success')
                                    return Promise.error("Could not acquire permission to access screen.");
                                opts.video.mandatory.chromeMediaSource = 'desktop';
                                opts.video.mandatory.chromeMediaSourceId = acquireResponse.streamId;
                                delete opts.audio;
                                return probe.call(this, 100).mapSuccess(function(videoStream) {
                                    return !options.audio ? videoStream : this.userMedia({
                                        audio: true
                                    }).mapError(function() {
                                        return Promise.value(videoStream);
                                    }).mapSuccess(function(audioStream) {
                                        try {
                                            return new MediaStream([videoStream.getVideoTracks()[0], audioStream.getAudioTracks()[0]]);
                                        } catch (e) {
                                            return videoStream;
                                        }
                                    });
                                }, this);
                            }, this);
                        }, this);
                    }, this);
                }
                return probe.call(this, 100);
            }
        },

        /**
         * @param {MediaStream} stream
         * @param {Array} sourceTracks
         */
        stopUserMediaStream: function(stream, sourceTracks) {
            var stopped = false;
            try {
                if (stream.getTracks) {
                    stream.getTracks().forEach(function(track) {
                        track.stop();
                        stopped = true;
                    });
                }
                // In multi stream above stream contains newly generated canvas stream
                // but missing source streams which generated that canvas stream
                // So, we have to stop them also
                if (sourceTracks.length > 0) {
                    Objs.iter(sourceTracks, function(track) {
                        track.stop();
                        stopped = true;
                    }, this);
                }
            } catch (e) {}
            try {
                if (!stopped && stream.stop)
                    stream.stop();
            } catch (e) {}
        },

        bindStreamToVideo: function(stream, video, flip) {
            if (!video)
                video = document.createElement("video");
            video.volume = 0;
            video.muted = true;
            if ('mozSrcObject' in video)
                video.mozSrcObject = stream;
            else if ('srcObject' in video)
                video.srcObject = stream;
            else
                video.src = this.globals().URL.createObjectURL(stream);
            if (flip) {
                video.style["-moz-transform"] = "scale(-1, 1)";
                video.style["-webkit-transform"] = "scale(-1, 1)";
                video.style["-o-transform"] = "scale(-1, 1)";
                video.style.transform = "scale(-1, 1)";
                video.style.filter = "FlipH";
            } else {
                delete video.style["-moz-transform"];
                delete video.style["-webkit-transform"];
                delete video.style["-o-transform"];
                delete video.style.transform;
                delete video.style.filter;
            }
            video.autoplay = true;
            video.play();
            return video;
        },

        dataURItoBlob: function(dataURI) {
            // If dataURI is empty return empty
            if (dataURI === '') return;
            // convert base64 to raw binary data held in a string
            var byteString = atob(dataURI.split(',')[1]);

            // separate out the mime component
            var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

            // write the bytes of the string to an ArrayBuffer
            var arrayBuffer = new ArrayBuffer(byteString.length);
            var _ia = new Uint8Array(arrayBuffer);
            for (var i = 0; i < byteString.length; i++)
                _ia[i] = byteString.charCodeAt(i);
            var dataView = new DataView(arrayBuffer);
            var blob = new Blob([dataView], {
                type: mimeString
            });
            return blob;
        },

        errorHandler: function(err) {
            switch (err) {
                case 'NotReadableError':
                case 'TrackStartError':
                    return {
                        key: 'device-already-in-use',
                            message: 'Web camera or microphone are already in use',
                            userLevel: true
                    };
                case 'NotFoundError':
                case 'DevicesNotFoundError':
                    return {
                        key: 'missing-track',
                            message: 'Required audio or video track is missing',
                            userLevel: true
                    };
                case 'OverconstrainedError':
                case 'ConstraintNotSatisfiedError':
                    return {
                        key: 'constrains-error',
                            message: 'Constraints can not be satisfied by available devices',
                            userLevel: false
                    };
                case 'NotAllowedError':
                case 'PermissionDeniedError':
                    return {
                        key: 'browser-permission-denied',
                            message: 'Permission denied by browser, please grant access to proceed',
                            userLevel: true
                    };
                case 'TypeError':
                    return {
                        key: 'empty-constraints',
                            message: 'Empty constraints object',
                            userLevel: false
                    };
                default:
                    return {
                        key: 'unknown-error',
                            message: 'Unknown Error',
                            userLevel: false
                    };
            }
        }
    };
});