betajs/betajs-media

View on GitHub
src/webrtc/webrtc_recorder.js

Summary

Maintainability
F
6 days
Test Coverage
Scoped.define("module:WebRTC.RecorderWrapper", [
    "base:Classes.ConditionalInstance",
    "base:Events.EventsMixin",
    "base:Objs",
    "base:Async",
    "base:Promise",
    "base:Time",
    "module:WebRTC.Support",
    "module:Recorder.Support",
    "module:Common.Video.PixelSampleMixin",
    "browser:Events"
], function(ConditionalInstance, EventsMixin, Objs, Async, Promise, Time, Support, RecorderSupport, PixelSampleMixin, DomEvents, scoped) {
    return ConditionalInstance.extend({
        scoped: scoped
    }, [EventsMixin, PixelSampleMixin, function(inherited) {
        return {

            constructor: function(options) {
                inherited.constructor.call(this, options);
                this._video = options.video;
                this._localPlaybackRequested = options.localPlaybackRequested;
                this._recording = false;
                this._bound = false;
                this._hasAudio = false;
                this._hasVideo = false;
                this._screen = options.screen;
                this._resizeMode = options.resizeMode;
                this._flip = !!options.flip;
                if (this._screen && !options.flipscreen)
                    this._flip = false;
                this._videoTrackSettings = {
                    slippedFromOrigin: {
                        height: 1.00,
                        width: 1.00
                    }
                };
                if (this._options.fittodimensions) {
                    if (this._screen === null) {
                        this._initCanvasStreamSettings();
                        this._prepareRecorderStreamCanvas(true);
                    } else {
                        this._options.fittodimensions = false;
                    }
                }
            },

            _getConstraints: function() {
                return {
                    // Seems sourceId was deprecated, deviceId is most supported constraint (Fix changing audio source)
                    audio: this._options.recordAudio ? {
                        deviceId: this._options.audioId
                    } : false,
                    video: this._options.recordVideo ? {
                        frameRate: this._options.framerate,
                        sourceId: this._options.videoId,
                        width: this._options.recordResolution.width,
                        height: this._options.recordResolution.height,
                        cameraFaceFront: this._options.cameraFaceFront,
                        resizeMode: this._resizeMode
                    } : false,
                    screen: this._screen
                };
            },

            /**
             * Will add a new Stream to Existing one
             *
             * @param {object} device
             * @param {object} options
             */
            addNewSingleStream: function(device, options) {
                this._initCanvasStreamSettings();
                var _options, _positionX, _positionY, _height, _width, _aspectRatio, _constraints;
                var _isTrueHeight = true;
                _aspectRatio = this._options.video.aspectRatio;
                _positionX = options.positionX || 0;
                _positionY = options.positionY || 0;
                _width = options.width || (this._options.recordResolution.width * 0.20) || 120;
                _height = options.height;
                if (!_height) {
                    _height = _aspectRatio ? Math.floor(_width * _aspectRatio) : Math.floor(_width / 1.33);
                    _isTrueHeight = false;
                }
                _options = {
                    frameRate: this._options.framerate,
                    sourceId: device.id,
                    cameraFaceFront: this._options.cameraFaceFront
                };
                _constraints = {
                    video: _options
                };
                this._prepareRecorderStreamCanvas();
                this._multiStreamConstraints = _constraints;
                this.__addedStreamOptions = Objs.tree_merge(_options, {
                    positionX: _positionX,
                    positionY: _positionY,
                    width: _width,
                    height: _height,
                    isTrueHeight: _isTrueHeight
                });
                return this.addNewMediaStream();
            },

            /**
             * Update small screen dimensions and position
             *
             * @param x
             * @param y
             * @param w
             * @param h
             */
            updateMultiStreamPosition: function(x, y, w, h) {
                this.__addedStreamOptions.positionX = x || this.__addedStreamOptions.positionX;
                this.__addedStreamOptions.positionY = y || this.__addedStreamOptions.positionY;
                this.__addedStreamOptions.width = w || this.__addedStreamOptions.width;
                this.__addedStreamOptions.height = h || this.__addedStreamOptions.height;
            },


            /**
             * Add new stream to existing one
             * @return {Promise}
             */
            addNewMediaStream: function() {
                var promise = Promise.create();
                if (this._canvasStreams.length < 1)
                    this._canvasStreams.push(this._stream);
                return Support.userMedia2(this._multiStreamConstraints, this).success(function(stream) {
                    this._canvasStreams.push(stream);
                    this._buildVideoElementsArray(promise);
                    this.on("stream-canvas-drawn", function() {
                        return promise.asyncSuccess();
                    }, this);
                }, this);
            },


            recordDelay: function(opts) {
                return 0;
            },

            stream: function() {
                return this._stream;
            },

            isWebrtcStreaming: function() {
                return false;
            },

            canPause: function() {
                return false;
            },

            bindMedia: function() {
                if (this._bound)
                    return;
                return Support.userMedia2(this._getConstraints()).success(function(stream) {
                    if (!this._options) return;
                    this._hasAudio = this._options.recordAudio && stream.getAudioTracks().length > 0;
                    this._hasVideo = this._options.recordVideo && stream.getVideoTracks().length > 0;
                    var applyConstraints = {};
                    var settings = null;
                    var setConstraints = null;
                    var capabilities = null;
                    var vTrack = stream.getVideoTracks()[0] || {};

                    if (this._hasVideo && typeof vTrack.getSettings === 'function') {
                        settings = vTrack.getSettings();

                        if (typeof vTrack.onended !== 'undefined') {
                            var _onEndedEvent = this.auto_destroy(new DomEvents());
                            _onEndedEvent.on(vTrack, "ended", function(ev) {
                                this.trigger("mainvideostreamended");
                            }, this);
                        }
                    }

                    // Purpose is fix Overconstrained dimensions to correct one
                    // Firefox still not supports getCapabilities
                    // More details: https://bugzilla.mozilla.org/show_bug.cgi?id=1179084
                    if (this._hasVideo && typeof vTrack.getCapabilities === 'function' && settings) {
                        capabilities = vTrack.getCapabilities();
                        setConstraints = this._getConstraints().video;

                        if (capabilities.width.max && capabilities.height.max)
                            applyConstraints = this.__checkAndApplyCorrectConstraints(vTrack, capabilities, setConstraints, stream);
                    }

                    // If Browser will set aspect ratio correctly no need draw into canvas
                    if (settings && setConstraints && this._options.fittodimensions) {
                        var _setRatio = setConstraints.width / setConstraints.height;
                        var _appliedRatio = settings.width / settings.height;
                        if (Math.abs(_setRatio - _appliedRatio) <= 0.1) {
                            this._options.fittodimensions = false;
                        }
                    }

                    if (this._options.fittodimensions && settings) {
                        this._canvasStreams.push(stream);
                        if (!this.__initialVideoTrackSettings)
                            this.__calculateVideoTrackSettings(settings, this._video, true);
                        this._videoTrackSettings.capabilities = capabilities;
                        this._videoTrackSettings.constrainsts = this._getConstraints().video;
                        this._buildVideoElementsArray();
                    } else {
                        this._bound = true;
                        this._stream = stream;
                        this._setLocalTrackSettings(stream);
                        Support.bindStreamToVideo(stream, this._video, this._flip);
                        this.trigger("bound", stream);
                        this._boundMedia();
                    }
                }, this);
            },

            selectCamera: function(cameraId) {
                this._options.videoId = cameraId;
                if (this._bound) {
                    this.unbindMedia();
                    this.bindMedia();
                    this.trigger("rebound");
                }
            },

            selectMicrophone: function(microphoneId) {
                this._options.audioId = microphoneId;
                if (this._bound) {
                    this.unbindMedia();
                    this.bindMedia();
                    this.trigger("rebound");
                }
            },

            selectCameraFace: function(faceFront) {
                this._options.cameraFaceFront = faceFront;
                if (this._bound) {
                    this.unbindMedia();
                    this.bindMedia();
                    this.trigger("rebound");
                }
            },

            getCameraFacingMode: function() {
                if (this._options.cameraFaceFront === true)
                    return "user";
                if (this._options.cameraFaceFront === false)
                    return "environment";
                return undefined;
            },

            startRecord: function(options) {
                if (this._recording)
                    return Promise.value(true);
                this._recording = true;
                var promise = this._startRecord(options);
                promise.success(function() {
                    this._pausedDuration = 0;
                    this._startTime = Time.now();
                }, this);
                return promise;
            },

            pauseRecord: function() {
                if (!this.canPause() || this._paused)
                    return;
                this._paused = true;
                this._recorder.once("paused", function() {
                    this.trigger("paused");
                    this.__pauseStartTime = Time.now();
                }, this);
                this._recorder.pause();
            },

            resumeRecord: function() {
                if (!this.canPause() || !this._paused)
                    return;
                this._paused = false;
                this._recorder.once("resumed", function() {
                    this.trigger("resumed");
                    this._pausedDuration += Time.now() - this.__pauseStartTime;
                    delete this.__pauseStartTime;
                }, this);
                this._recorder.resume();
            },

            stopRecord: function() {
                if (!this._recording)
                    return;
                this._recording = false;
                this._stopRecord();
                this._stopTime = Time.now();
            },

            duration: function() {
                return (this._recording || !this._stopTime ? Time.now() : this._stopTime) - this._startTime -
                    this._pausedDuration -
                    (this.__pauseStartTime !== undefined ? Time.now() - this.__pauseStartTime : 0);
            },

            unbindMedia: function() {
                if (!this._bound || this._recording)
                    return;
                Support.stopUserMediaStream(this._stream, this._sourceTracks);
                this._bound = false;
                this.trigger("unbound");
                this._unboundMedia();
            },

            createSnapshot: function(type) {
                return RecorderSupport.createSnapshot(type, this._video);
            },

            /**
             * Initialize multi-stream related variables
             * @private
             */
            _initCanvasStreamSettings: function() {
                this._canvasStreams = [];
                this._videoElements = [];
                this._audioInputs = [];
                this._sourceTracks = [];
                this._multiStreamConstraints = {};
                this._drawingStream = false;
            },

            /**
             * Will prepare canvas element, to draw video streams inside
             * @param {Boolean =} feetDimension
             * @private
             */
            _prepareRecorderStreamCanvas: function(feetDimension) {
                if (this.__recorderStreamCanvas && this.__recorderStreamCtx)
                    return;
                var height = this._videoTrackSettings.height || this._video.clientHeight || this._video.videoHeight;
                var width = this._videoTrackSettings.width || this._video.clientWidth || this._video.videoWidth;
                if (typeof this.__recorderStreamCanvas === 'undefined') {
                    this.__recorderStreamCanvas = document.createElement('canvas');
                }
                this.__recorderStreamCanvas.setAttribute('width', width);
                this.__recorderStreamCanvas.setAttribute('height', height);
                this.__recorderStreamCanvas.setAttribute('style', 'position:fixed; left: 200%; pointer-events: none'); // Out off from the screen
                this.__recorderStreamCtx = this.__recorderStreamCanvas.getContext('2d');
                this._drawerSetting = {
                    fittodimensions: feetDimension || false,
                    streamsReversed: false,
                    isMultiStream: false
                };
                // document.body.append(this.__recorderStreamCanvas);
            },

            /**
             * Set local track setting values
             * @param {MediaStream} stream
             * @private
             */
            _setLocalTrackSettings: function(stream) {
                if (typeof stream.getVideoTracks() !== 'undefined') {
                    if (stream.getVideoTracks()[0]) {
                        var self = this;
                        this._videoTrack = stream.getVideoTracks()[0];
                        // Will fix older version Chrome Cropping
                        if (!this._options.getDisplayMediaSupported) {
                            if (typeof this._resizeMode === 'undefined')
                                this._resizeMode = 'none';
                            this._videoTrack.applyConstraints({
                                resizeMode: this._resizeMode
                            }).then(function() {
                                if (typeof self._videoTrack.getSettings !== 'undefined')
                                    self._videoTrackSettings = Objs.extend(sourceVideoSettings, self._videoTrackSettings);
                            });
                        } else {
                            if (typeof this._videoTrack.getSettings !== 'undefined') {
                                var sourceVideoSettings = this._videoTrack.getSettings();
                                if (typeof this._videoTrackSettings.videoInnerFrame === 'undefined')
                                    this._videoTrackSettings = Objs.extend(sourceVideoSettings, this._videoTrackSettings);

                                this._video.onloadedmetadata = function(ev) {
                                    self.__calculateVideoTrackSettings(sourceVideoSettings, ev.target, true);
                                };
                            }
                        }
                    }
                }
                if (typeof stream.getAudioTracks !== 'undefined') {
                    if (stream.getAudioTracks()[0]) {
                        this._audioTrack = stream.getAudioTracks()[0];
                        if (typeof this._audioTrack.getSettings() !== 'undefined')
                            this._audioTrackSettings = this._audioTrack.getSettings();
                    }
                }
            },

            /**
             *
             * @param videoTrack
             * @param capabilities
             * @param setConstraints
             * @private
             */
            __checkAndApplyCorrectConstraints: function(videoTrack, capabilities, setConstraints) {
                var maxWidth = capabilities.width.max;
                var maxHeight = capabilities.height.max;

                if (setConstraints.width > maxWidth || setConstraints.height > maxHeight) {
                    var _ar = maxWidth / maxHeight;
                    var _newWidth, _newHeight;
                    var _desiredAr = setConstraints.width / setConstraints.height;

                    if (capabilities.aspectRatio.min && capabilities.aspectRatio.max) {
                        var _minAr, _maxAr;
                        _desiredAr = setConstraints.width / setConstraints.height;
                        _minAr = capabilities.aspectRatio.min;
                        _maxAr = capabilities.aspectRatio.max;
                        if (_desiredAr > 1 && _desiredAr > _maxAr) {
                            _desiredAr = _maxAr;
                        } else if (_desiredAr < 1 && _desiredAr < _minAr) {
                            _desiredAr = _minAr;
                        }
                    } else {
                        _desiredAr = _ar;
                    }

                    if (setConstraints.width > maxWidth && setConstraints.height > maxHeight) {
                        _newWidth = _desiredAr > 1 ? maxWidth : maxHeight * _desiredAr;
                        _newHeight = _desiredAr > 1 ? maxWidth / _desiredAr : maxHeight;

                        // Only possible in below if above not fit correctly
                        if (_newHeight > maxHeight && _desiredAr > 1) {
                            _newHeight = maxHeight;
                            _newWidth = maxHeight / _desiredAr;
                        }
                        if (_newWidth > maxWidth && _desiredAr < 1) {
                            _newWidth = maxWidth;
                            _newHeight = maxWidth / _desiredAr;
                        }
                    } else if (setConstraints.width > maxWidth) {
                        _newHeight = _desiredAr > 1 ? maxWidth / _desiredAr : Math.min(maxHeight, setConstraints.height);
                        _newWidth = _desiredAr > 1 ? maxWidth : _newHeight * _desiredAr;
                    } else if (setConstraints.height > maxHeight) {
                        _newWidth = _desiredAr > 1 ? Math.min(maxWidth, setConstraints.width) : maxHeight * _desiredAr;
                        _newHeight = _desiredAr > 1 ? _newWidth / _desiredAr : maxHeight;
                    }

                    return {
                        width: _newWidth,
                        height: _newHeight
                    };
                }
            },

            /**
             * Calculate Video Element Settings Settings
             * @param {MediaTrackSettings} sourceVideoSettings
             * @param {HTMLVideoElement=} videoElement
             * @param {Boolean =} setInitialSettings
             * @private
             */
            __calculateVideoTrackSettings: function(sourceVideoSettings, videoElement, setInitialSettings) {
                videoElement = videoElement || this._video;
                var _lookedWidth, _lookedHeight, _slippedWidth, _slippedHeight, _dimensions;
                var _asR = sourceVideoSettings.aspectRatio || (sourceVideoSettings.width / sourceVideoSettings.height);
                if (!isNaN(_asR)) {
                    var _maxWidth = videoElement.offsetWidth;
                    var _maxHeight = videoElement.offsetHeight;
                    // FireFox don't calculates aspectRatio like Chrome does
                    _lookedWidth = _maxWidth <= _maxHeight * _asR ? _maxWidth : Math.round(_maxHeight * _asR);
                    _lookedHeight = _maxWidth > _maxHeight * _asR ? _maxHeight : Math.round(_maxWidth / _asR);

                    _slippedWidth = sourceVideoSettings.width > _lookedWidth ? sourceVideoSettings.width / _lookedWidth : _lookedWidth / sourceVideoSettings.width;
                    _slippedHeight = sourceVideoSettings.height > _lookedHeight ? sourceVideoSettings.height / _lookedHeight : _lookedHeight / sourceVideoSettings.height;

                    _dimensions = Objs.extend(sourceVideoSettings, {
                        videoElement: {
                            width: _maxWidth,
                            height: _maxHeight
                        },
                        videoInnerFrame: {
                            width: _lookedWidth,
                            height: _lookedHeight
                        },
                        slippedFromOrigin: {
                            width: _slippedWidth,
                            height: _slippedHeight
                        }
                    });

                    if (setInitialSettings) this.__initialVideoTrackSettings = _dimensions;
                    this._videoTrackSettings = _dimensions;
                    return _dimensions;
                }
            },

            /**
             * Will add new video DOM Element to draw inside Multi-Stream Canvas
             * @param {Promise =} promise
             * @private
             */
            _buildVideoElementsArray: function(promise) {
                this.__multiStreamVideoSettings = {
                    isMainStream: true,
                    mainStream: {},
                    smallStream: {}
                };
                Objs.iter(this._canvasStreams, function(stream, index) {
                    var _tracks = stream.getTracks();
                    var _additionalStream = false;
                    Objs.iter(_tracks, function(track) {
                        // Will require to stop all existing tracks after recorder stop
                        this._sourceTracks.push(track);
                        if (track.kind === 'video') {
                            if (this._videoTrack) {
                                _additionalStream = track.id !== this._videoTrack.id;
                                this._drawerSetting.isMultiStream = true;
                            }
                            this._videoElements.push(this._arraySingleVideoElement(this.__addedStreamOptions, stream, _additionalStream, track));
                        }
                        if (track.kind === 'audio') {
                            this._audioInputs.push(track);
                        }
                    }, this);
                    if ((this._videoElements.length + this._audioInputs.length) === _tracks.length) {
                        this._startDrawRecorderToCanvas(stream);
                    }

                    // If After 1 seconds, if we can not get required tracks, something wrong
                    Async.eventually(function() {
                        if (!this._drawingStream) {
                            if (promise)
                                return promise.asyncError({
                                    message: 'Could not be able to get required tracks'
                                });
                            return false;
                        }
                    }, this, 1000);
                }, this);
            },

            /**
             *
             * @param {MediaStream} stream
             * @private
             */
            _startDrawRecorderToCanvas: function(stream) {
                try {
                    var streamSettings = stream.getVideoTracks()[0].getSettings();
                    if (streamSettings.aspectRatio) {
                        if (Math.abs(streamSettings.aspectRatio - this._videoTrackSettings.aspectRatio) > 0.1) {
                            this._videoTrackSettings.aspectRatio = streamSettings.aspectRatio;
                            this.__calculateVideoTrackSettings(streamSettings, null, true);
                            this.__recorderStreamCanvas.setAttribute('width', streamSettings.width);
                            this.__recorderStreamCanvas.setAttribute('height', streamSettings.height);
                        }
                    }

                    this._drawingStream = true;

                    if (this._drawerSetting.fittodimensions && typeof this._videoTrackSettings.videoInnerFrame !== 'undefined') {
                        // Stream dimensions
                        var crop = false;
                        var settings = {};
                        var width = this._videoTrackSettings.width;
                        var height = this._videoTrackSettings.height;
                        this.__recorderStreamCanvas.width = width;
                        this.__recorderStreamCanvas.height = height;
                        var _settingRatio = width / height;

                        var innerFrame = this._videoTrackSettings.videoInnerFrame;
                        var videoElement = this._videoTrackSettings.videoElement;

                        var setConstraints = this._videoTrackSettings.constrainsts;
                        var _desiredRatio = setConstraints.width / setConstraints.height;

                        var _baseIsWidth = innerFrame.width === videoElement.width;
                        // var _baseIsHeight = innerFrame.height === videoElement.height;

                        // Dimensions what is expected
                        if (Math.abs(_settingRatio - _desiredRatio) >= 0.1) {
                            crop = true;
                            if (_baseIsWidth) {
                                settings.ch = Math.round(width / _desiredRatio);
                                if (settings.ch > height)
                                    _baseIsWidth = false;
                            } else {
                                settings.cw = _settingRatio > 1 ? Math.round(height * _desiredRatio) : Math.round(height / _desiredRatio);
                                if (settings.cw > width)
                                    _baseIsWidth = true;
                            }

                            if (_baseIsWidth) {
                                settings.cw = width;
                                settings.ch = Math.round(width / _desiredRatio);
                                settings.cx = 0;
                                settings.cy = (height - settings.ch) / 2;

                                settings.w = settings.cw;
                                settings.h = settings.ch;
                                settings.x = 0;
                                settings.y = (height - settings.h) / 2;

                            } else {
                                settings.ch = height;
                                settings.cw = _settingRatio > 1 ? Math.round(height * _desiredRatio) : Math.round(height / _desiredRatio);
                                settings.cy = 0;
                                settings.cx = (width - settings.cw) / 2;

                                settings.w = settings.cw;
                                settings.h = settings.ch;
                                settings.y = 0;
                                settings.x = (width - settings.w) / 2;
                            }
                        }
                        this.__cropSettings = settings;
                    }

                    this._drawTracksToCanvas();
                    this._startCanvasStreaming();
                } catch (e) {
                    console.warn(e);
                }
            },


            /**
             *
             * Merge streams and draw Canvas.
             * @private
             */
            _drawTracksToCanvas: function() {
                if (!this._drawingStream)
                    return;
                var videosCount = this._videoElements.length;
                var reversed = false;

                if (typeof this._drawerSetting !== 'undefined') {
                    if (this._drawerSetting.streamsReversed)
                        reversed = true;
                }

                for (var _i = 0; _i < this._videoElements.length; _i++) {
                    var video, constraints, width, height, positionX, positionY;
                    video = this._videoElements[_i];

                    if (this._drawerSetting.fittodimensions) {
                        // .videoInnerFrame
                        var _cropSettings = this.__cropSettings;
                        width = _cropSettings.w;
                        height = _cropSettings.h;
                        positionX = _cropSettings.x;
                        positionY = _cropSettings.y;
                        this.__recorderStreamCtx.drawImage(
                            video, _cropSettings.cx, _cropSettings.cy, _cropSettings.cw, _cropSettings.ch,
                            positionX, positionY, width, height
                        );
                    } else {
                        constraints = _i !== 0 ? this.__addedStreamOptions : {};
                        positionX = constraints.positionX || 0;
                        positionY = constraints.positionY || 0;
                        if (video.__multistreamElement) {
                            width = reversed ? this._drawerSetting.smallStreamWidth : constraints.width || 360;
                            height = reversed ? this._drawerSetting.smallStreamHeight : constraints.height || 240;
                        } else {
                            if (reversed) {
                                positionX = this._drawerSetting.positionX;
                                positionY = this._drawerSetting.positionY;
                                width = this._drawerSetting.width;
                                height = this._drawerSetting.height;
                                if (this.__multiStreamVideoSettings.mainStream) {
                                    if (Objs.keys(this.__multiStreamVideoSettings.mainStream).length > 0) {
                                        this.__recorderStreamCtx.fillStyle = "#000000";
                                        this.__recorderStreamCtx.fillRect(0, 0, this.__recorderStreamCanvas.width, this.__recorderStreamCanvas.height);
                                        this.__recorderStreamCtx.restore();
                                    }
                                }
                            } else {
                                width = this.__recorderStreamCanvas.width || constraints.width || 360;
                                height = this.__recorderStreamCanvas.height || constraints.height || 240;
                            }
                        }
                        this.__recorderStreamCtx.drawImage(video, positionX, positionY, width, height);
                    }

                    videosCount--;
                    if (videosCount === 0) {
                        Async.eventually(this._drawTracksToCanvas, this, 1000 / 50); // drawing at 50 fps
                    }
                }
            },

            /**
             * Will change screen sources during multi-record
             */
            reverseVideos: function() {
                var _self = this;
                var _isMainBecomeSmall = this.__multiStreamVideoSettings.isMainStream;
                this.__multiStreamVideoSettings.isMainStream = !_isMainBecomeSmall;

                // Get initial core information about stream dimensions
                var _mainStream = this.__multiStreamVideoSettings.mainStream.settings;
                var _smallStream = this.__multiStreamVideoSettings.smallStream.settings;

                var _x = 0,
                    _y = 0,
                    _w, _h, _smW, _smH;

                if (_isMainBecomeSmall) {
                    var _smallStreamAspectRatio = _smallStream.aspectRatio;
                    var _mainAspectRatio = this._videoTrackSettings.aspectRatio || (this._videoTrackSettings.width / this._videoTrackSettings.height);
                    var _fitFullWidth = this._videoTrackSettings.videoElement.width === this._videoTrackSettings.videoInnerFrame.width;
                    if (_fitFullWidth) {
                        _h = _mainStream.streamHeight;
                        _w = _h * _smallStreamAspectRatio;
                        _smW = _smallStream.videoWidth;
                        _smH = _smW / _mainAspectRatio;
                    } else {
                        _w = _mainStream.streamWidth;
                        _h = _w / _smallStreamAspectRatio;
                        _smH = _smallStream.videoHeight;
                        _smW = _smH * _mainAspectRatio;
                    }
                    _x = (_mainStream.streamWidth - _w) / 2;
                    _y = (_mainStream.streamHeight - _h) / 2;
                    this._drawerSetting = {
                        streamsReversed: true,
                        width: _w,
                        height: _h,
                        positionX: _x,
                        positionY: _y,
                        smallStreamHeight: _smH,
                        smallStreamWidth: _smW
                    };
                } else {
                    this._drawerSetting.streamsReversed = false;
                    _smW = _smallStream.videoWidth;
                    _smH = _smallStream.videoHeight;
                }

                if (this._drawingStream) {
                    var _temp_video = document.createElement('video');
                    for (var _i = 0; _i < this._videoElements.length; _i++) {
                        if (!_temp_video.srcObject) {
                            _temp_video.srcObject = this._videoElements[_i].srcObject;
                            this._videoElements[_i].srcObject = this._videoElements[_i + 1].srcObject;
                            this._videoElements[_i].load();
                            this._videoElements[_i].oncanplay = function() {
                                this.play();
                            };
                        } else if (_temp_video.srcObject) {
                            this._videoElements[_i].srcObject = _temp_video.srcObject;
                            this._videoElements[_i].load();
                            this._videoElements[_i].oncanplay = function() {
                                this.play();
                            };
                            var _dimensions = {
                                width: _smW,
                                height: _smH
                            };
                            if (_isMainBecomeSmall) {
                                _self.__calculateVideoTrackSettings(_dimensions);
                            } else {
                                _self._videoTrackSettings = _self.__initialVideoTrackSettings;
                            }
                            _self.trigger("multistream-camera-switched", _dimensions, _isMainBecomeSmall);
                            _temp_video.remove();
                        }
                    }
                }
            },

            /**
             * Start streaming from merged Canvas Element
             * @private
             */
            _startCanvasStreaming: function() {
                var stream = this.__recorderStreamCanvas.captureStream(25);
                if (this._audioInputs[0])
                    stream.addTrack(this._audioInputs[0]);
                this._stream = stream;
                Support.bindStreamToVideo(stream, this._video, this._flip);
                this.trigger("bound", stream);
                this.trigger("stream-canvas-drawn");
                this._setLocalTrackSettings(stream);
                this._boundMedia(stream);
            },

            /**
             * Generate single video DOM element to draw inside canvas
             * @param {object} options
             * @param stream
             * @param {Boolean=} additionalStream // This is newly added stream, small screen
             * @param {MediaStreamTrack=} videoTrack
             * @return {HTMLElement}
             * @private
             */
            _arraySingleVideoElement: function(options, stream, additionalStream, videoTrack) {
                var self = this;
                var video = Support.bindStreamToVideo(stream);
                additionalStream = additionalStream || false;
                if (additionalStream) {
                    video.__multistreamElement = true;
                    video.width = this.__addedStreamOptions.width || options.width || 360;
                    video.height = this.__addedStreamOptions.height || options.height || 240;
                } else {
                    var visibleDimensions = self._videoTrackSettings.videoInnerFrame;
                    var aspectRatio = self._videoTrackSettings.aspectRatio;
                }
                var slippedFromOrigin = self._videoTrackSettings.slippedFromOrigin;
                video.muted = true;
                video.volume = 0;
                video.oncanplay = function() {
                    var s = videoTrack.getSettings();
                    var width = this.width;
                    var height = this.height;
                    var aspectRatio = additionalStream ? (s.aspectRatio || (s.width / s.height)) : aspectRatio;
                    if (typeof self.__addedStreamOptions !== 'undefined') {
                        if (!self.__addedStreamOptions._isTrueHeight && additionalStream && aspectRatio) {
                            height = aspectRatio > 1.00 ?
                                (width / aspectRatio).toFixed(2) :
                                (width * aspectRatio).toFixed(2);
                            self.__addedStreamOptions.height = height;
                            self.updateMultiStreamPosition();
                        }
                    }
                    var values = {
                        track: videoTrack,
                        isMainScreen: additionalStream,
                        settings: {
                            videoWidth: additionalStream ? width : self._videoTrackSettings.videoElement.width,
                            videoHeight: additionalStream ? height : self._videoTrackSettings.videoElement.height,
                            streamWidth: additionalStream ? s.width : self._videoTrackSettings.width,
                            streamHeight: additionalStream ? s.height : self._videoTrackSettings.height,
                            visibleWidth: additionalStream ? (width / slippedFromOrigin.width) : visibleDimensions.width,
                            visibleHeight: additionalStream ? (height / slippedFromOrigin.height) : visibleDimensions.height,
                            deviceId: s.deviceId,
                            aspectRatio: aspectRatio
                        }
                    };

                    if (additionalStream)
                        self.__multiStreamVideoSettings.smallStream = values;
                    else
                        self.__multiStreamVideoSettings.mainStream = values;
                    this.play();
                };
                return video;
            },

            _boundMedia: function() {},

            _unboundMedia: function() {},

            _startRecord: function(options) {},

            _stopRecord: function() {},

            _error: function(errorType, errorData) {
                this.trigger("error", errorType, errorData);
            },

            getVolumeGain: function() {},

            setVolumeGain: function(volumeGain) {},

            _dataAvailable: function(videoBlob, audioBlob, noUploading) {
                if (this.destroyed())
                    return;
                this.trigger("data", videoBlob, audioBlob, noUploading);
            },

            destroy: function() {
                this.stopRecord();
                this.unbindMedia();
                inherited.destroy.call(this);
            },

            averageFrameRate: function() {
                return null;
            }

        };
    }], {

        _initializeOptions: function(options) {
            return Objs.extend({
                // video: null,
                recordAudio: true,
                recordVideo: true,
                recordResolution: {
                    width: 320,
                    height: 200
                }
            }, options);
        },

        supported: function(options) {
            return !!Support.globals().getUserMedia && !!Support.globals().URL;
        }

    });
});


Scoped.define("module:WebRTC.PeerRecorderWrapper", [
    "module:WebRTC.RecorderWrapper",
    "module:WebRTC.PeerRecorder",
    "module:WebRTC.MediaRecorder",
    "browser:Info",
    "base:Async"
], function(RecorderWrapper, PeerRecorder, MediaRecorder, Info, Async, scoped) {
    return RecorderWrapper.extend({
        scoped: scoped
    }, {

        _boundMedia: function() {
            this._recorder = new PeerRecorder(this._stream, {
                recorderWidth: this._options.recordResolution.width,
                recorderHeight: this._options.recordResolution.height,
                videoBitrate: this._options.videoBitrate,
                audioBitrate: this._options.audioBitrate,
                framerate: this._options.framerate,
                audioonly: !this._options.recordVideo
            });
            if (this._localPlaybackRequested && MediaRecorder.supported())
                this.__localMediaRecorder = new MediaRecorder(this._stream);
            this._recorder.on("error", this._error, this);
        },

        _unboundMedia: function() {
            this._recorder.destroy();
            if (this.__localMediaRecorder)
                this.__localMediaRecorder.weakDestroy();
        },

        _startRecord: function(options) {
            if (this.__localMediaRecorder)
                this.__localMediaRecorder.start();
            return this._recorder.start(options.webrtcStreaming);
        },

        isWebrtcStreaming: function() {
            return true;
        },

        _stopRecord: function() {
            this._recorder.stop();
            var localBlob = null;
            if (this.__localMediaRecorder) {
                this.__localMediaRecorder.once("data", function(blob) {
                    localBlob = blob;
                });
                this.__localMediaRecorder.stop();
            }
            Async.eventually(function() {
                this._dataAvailable(localBlob, null, true);
            }, this, this.__stopDelay || this._options.webrtcStreaming.stopDelay || 0);
        },

        recordDelay: function(opts) {
            this.__stopDelay = opts.webrtcStreaming.stopDelay;
            return opts.webrtcStreaming.delay || 0;
        },

        getVolumeGain: function() {},

        setVolumeGain: function(volumeGain) {},

        averageFrameRate: function() {
            return null;
        }

    }, function(inherited) {
        return {

            supported: function(options) {
                if (!inherited.supported.call(this, options))
                    return false;
                /*
                if (!options.recordVideo)
                    return false;
                    */
                if (options.screen && Info.isFirefox())
                    return false;
                return options.webrtcStreaming && PeerRecorder.supported();
            }

        };
    });
});


Scoped.define("module:WebRTC.PeerRecorderWrapperIfNecessary", [
    "module:WebRTC.PeerRecorderWrapper",
    "base:Objs"
], function(PeerRecorderWrapper, Objs, scoped) {
    return PeerRecorderWrapper.extend({
        scoped: scoped
    }, {}, function(inherited) {
        return {

            supported: function(options) {
                if (options.webrtcStreamingIfNecessary) {
                    options = Objs.clone(options, 1);
                    options.webrtcStreamingIfNecessary = false;
                    options.webrtcStreaming = true;
                }
                return inherited.supported.call(this, options);
            }

        };
    });
});


Scoped.define("module:WebRTC.MediaRecorderWrapper", [
    "module:WebRTC.RecorderWrapper",
    "module:WebRTC.MediaRecorder"
], function(RecorderWrapper, MediaRecorder, scoped) {
    return RecorderWrapper.extend({
        scoped: scoped
    }, {

        _boundMedia: function(stream) {
            stream = stream || this._stream;
            this._recorder = new MediaRecorder(stream, {
                videoBitrate: this._options.videoBitrate,
                audioBitrate: this._options.audioBitrate,
                audioonly: !this._options.recordVideo,
                cpuFriendly: this._options.cpuFriendly
            });
            this._recorder.on("dataavailable", function(e) {
                this.trigger("dataavailable", e);
            }, this);
            this._recorder.on("data", function(blob) {
                this._dataAvailable(blob);
            }, this);
        },

        _unboundMedia: function() {
            this._recorder.destroy();
            if (this._drawingStream)
                this._initCanvasStreamSettings();
        },

        _startRecord: function() {
            return this._recorder.start();
        },

        _stopRecord: function() {
            this._recorder.stop();
        },

        canPause: function() {
            return true;
        },

        getVolumeGain: function() {},

        setVolumeGain: function(volumeGain) {},

        averageFrameRate: function() {
            return null;
        }

    }, function(inherited) {
        return {

            supported: function(options) {
                if (!inherited.supported.call(this, options))
                    return false;
                if (options.recordFakeVideo)
                    return false;
                return MediaRecorder.supported();
            }

        };
    });
});


Scoped.define("module:WebRTC.WhammyAudioRecorderWrapper", [
    "module:WebRTC.RecorderWrapper",
    "module:WebRTC.AudioRecorder",
    "module:WebRTC.WhammyRecorder",
    "browser:Info",
    "base:Promise"
], function(RecorderWrapper, AudioRecorder, WhammyRecorder, Info, Promise, scoped) {
    return RecorderWrapper.extend({
        scoped: scoped
    }, {
        /*
                _getConstraints: function () {
                    return {
                        audio: this._options.recordAudio,
                        video: this._options.recordVideo
                    }
                },
        */
        _createSnapshot: function(type) {
            return this._whammyRecorder.createSnapshot(type);
        },

        _boundMedia: function() {
            this._videoBlob = null;
            this._audioBlob = null;
            if (this._hasVideo) {
                this._whammyRecorder = new WhammyRecorder(this._stream, {
                    //recorderWidth: this._options.recordResolution.width,
                    //recorderHeight: this._options.recordResolution.height,
                    video: this._video,
                    framerate: this._options.framerate
                });
            } else {
                this._whammyRecorder = new WhammyRecorder(null, {
                    framerate: this._options.framerate
                });
            }
            if (this._hasAudio) {
                this._audioRecorder = new AudioRecorder(this._stream);
                this._audioRecorder.on("data", function(blob) {
                    this._audioBlob = blob;
                    if (this._videoBlob || !this._hasVideo)
                        this._dataAvailable(this._videoBlob, this._audioBlob);
                }, this);
            }
            //if (this._hasVideo) {
            this._whammyRecorder.on("data", function(blob) {
                this._videoBlob = blob;
                if (this._audioBlob || !this._hasAudio)
                    this._dataAvailable(this._videoBlob, this._audioBlob);
            }, this);
            //}
            /*
            this._whammyRecorder.on("onStartedDrawingNonBlankFrames", function () {
                if (this._recording)
                    this._audioRecorder.start();
            }, this);
            */
        },

        _unboundMedia: function() {
            if (this._hasAudio)
                this._audioRecorder.destroy();
            //if (this._hasVideo)
            this._whammyRecorder.destroy();
        },

        _startRecord: function() {
            var promises = [];
            //if (this._hasVideo)
            promises.push(this._whammyRecorder.start());
            if (this._hasAudio)
                promises.push(this._audioRecorder.start());
            return Promise.and(promises);
        },

        _stopRecord: function() {
            //if (this._hasVideo)
            this._whammyRecorder.stop();
            if (this._hasAudio)
                this._audioRecorder.stop();
        },

        getVolumeGain: function() {
            return this._audioRecorder ? this._audioRecorder.getVolumeGain() : 1.0;
        },

        setVolumeGain: function(volumeGain) {
            if (this._audioRecorder)
                this._audioRecorder.setVolumeGain(volumeGain);
        },

        averageFrameRate: function() {
            return this._whammyRecorder.averageFrameRate();
            //return this._hasVideo ? this._whammyRecorder.averageFrameRate() : 0;
        }


    }, function(inherited) {
        return {

            supported: function(options) {
                if (!inherited.supported.call(this, options))
                    return false;
                if (document.location.href.indexOf("https://") !== 0 && document.location.hostname !== "localhost") {
                    if (Info.isChrome() && Info.chromeVersion() >= 47)
                        return false;
                    if (Info.isOpera() && Info.operaVersion() >= 34)
                        return false;
                }
                return AudioRecorder.supported() && WhammyRecorder.supported(!options.recordVideo);
            }

        };
    });
});


Scoped.extend("module:WebRTC.RecorderWrapper", [
    "module:WebRTC.RecorderWrapper",
    "module:WebRTC.PeerRecorderWrapper",
    "module:WebRTC.MediaRecorderWrapper",
    "module:WebRTC.WhammyAudioRecorderWrapper",
    "module:WebRTC.PeerRecorderWrapperIfNecessary"
], function(RecorderWrapper, PeerRecorderWrapper, MediaRecorderWrapper, WhammyAudioRecorderWrapper, PeerRecorderWrapperIfNecessary) {
    RecorderWrapper.register(PeerRecorderWrapper, 4);
    RecorderWrapper.register(MediaRecorderWrapper, 3);
    RecorderWrapper.register(WhammyAudioRecorderWrapper, 2);
    RecorderWrapper.register(PeerRecorderWrapperIfNecessary, 1);
    return {};
});