Source: components/player.js

define('application/components/player', [
    'rofl/widgets/component',
    'rofl/lib/utils',
    'rofl/events/keyevent',
    'rofl/analytics/web/google',
    'antie/runtimecontext',
    'application/decorators/player/interfaces/playerinterface',
    'application/decorators/player/manipulation',
    'application/managers/api',
    'application/managers/bookmark',
    'application/constants',
    'application/managers/session',
    'application/utils',
    'rofl/lib/l10n',
    'rofl/logging/graylog',
    'product-layer/player/widgets/seeker'
], function (
    Component,
    Utils,
    KeyEvent,
    GoogleAnalytics,
    RuntimeContext,
    AppCorePlaybackInterface,
    PlayerManipulation,
    ApiManager,
    BookmarkManager,
    Constants,
    SessionManager,
    AppUtils,
    L10N,
    Graylog,
    Seeker
) {
    'use strict';

    var EVENTS = AppCorePlaybackInterface.EVENTS,
        application = RuntimeContext.getCurrentApplication(),
        configuration = application.getConfiguration(),
        api = ApiManager.getKPNAPI(),
        GA = GoogleAnalytics.getInstance(),
        device = RuntimeContext.getDevice(),
        KEY_HOLD_TIMEOUT = configuration.player.keyHoldTimeout,
        l10n = L10N.getInstance(),
        graylog = Graylog.getInstance(),
        ApiErrorCodes = ApiManager.getApiErrorCodes(),
        sessionManager = SessionManager.getInstance();

    return Component.extend({

        init: function init () {
            init.base.call(this, 'player');

            this.MEDIA_PLAYER_EVENTS = EVENTS;
            this._playbackStatus = {};
            this.bookmarkManager = BookmarkManager;
            this.playerInterface = this.getPlaybackInterface();

            this.decorate([this.getPlayerManipulation()]);

            this._setView();
            this._controlBar = this._view.getControlBar();
            this._CONTROLS = this._view.CONTROLS;
            this._deviceBrand = device.getBrand();


        },

        onBeforeRender: function () {
            this._multitaskHandler = Utils.bind(this._onVisibilityChanged, this);
            this._onKeyDownBound = Utils.bind(this._onKeyDown, this);
            this._onKeyUpBound = Utils.bind(this._onKeyUp, this);
            this._onSelectBound = Utils.bind(this._onSelect, this);
            this._onMouseSeekBound = Utils.bind(this._onMouseSeek, this);
            this._onSeekSpeedChangeBound = Utils.bind(this._onSpeedChanged, this);
            this._onSeekCurrentTimeChangeBound = Utils.bind(this._onCurrentTimeChanged, this);
            this._onSeekBound = Utils.bind(this._onSeek, this);
        },

        onBeforeShow: function (e) {
            this.playerInterface.listen(Utils.bind(this.onPlayerEvent, this));
            this._preparePlayer(e.args);

            GA.onPageView(Constants.ANALYTICS_VIEW_PLAYER);
        },

        onAfterShow: function () {
            this._addEventListeners();
        },

        onBeforeHide: function () {
            this._removeEventListeners();
        },

        /**
         * Remove the event listeners.
         */
        _addEventListeners: function () {
            this._setTizenMultitaskHandler();
            this.addEventListener('mouseseek', this._onMouseSeekBound);
            this.addEventListener('keydown', this._onKeyDownBound);
            this.addEventListener('keyup', this._onKeyUpBound);
            this.addEventListener('select', this._onSelectBound);
        },

        /**
         * Add the event listeners.
         */
        _removeEventListeners: function () {
            this._removeTizenMultitaskHandler();
            this.removeEventListener('mouseseek', this._onMouseSeekBound);
            this.removeEventListener('keydown', this._onKeyDownBound);
            this.removeEventListener('keyup', this._onKeyUpBound);
            this.removeEventListener('select', this._onSelectBound);
        },

        /**
         * Removes the tizen multitask handler.
         */
        _removeTizenMultitaskHandler: function removeTizenMultitaskHandlerFn () {

            document.removeEventListener('visibilitychange', this._multitaskHandler);
        },

        /**
         * Sets the tizen multitask handler.
         */
        _setTizenMultitaskHandler: function setTizenMultitaskHandlerFn () {

            document.addEventListener('visibilitychange', this._multitaskHandler);
        },

        /**
         * Visibility changed event.
         *
         * @private
         */
        _onVisibilityChanged: function () {
            if (!document.hidden) {

                if (this.isSessionLoggedIn()) {
                    this.refreshSessionToken()
                        .then(Utils.bind(function () {
                            this._preparePlayer({
                                data: this._program
                            });
                        }, this));
                }
            }
        },

        /**
         * Checks the session manager if user is logged in.
         *
         * @returns {boolean} - True if logged in.
         */
        isSessionLoggedIn: function () {
            return sessionManager.isLoggedIn();
        },

        /**
         * Attempts so refresh session token.
         *
         * @returns {Promise} - Refresh token promise.
         */
        refreshSessionToken: function () {
            return sessionManager.refreshToken();
        },

        /**
         * Gets the playback interface.
         *
         * @returns {Object} - The playback interface.
         */
        getPlaybackInterface: function () {
            return new AppCorePlaybackInterface();
        },

        /**
         * Builds the seeker.
         *
         * @private
         */
        _buildSeeker: function () {

            if (this._seeker && this._seeker.isActive()) {
                this._seeker.detach();
                this._seeker = null;
            }

            this._seeker = Seeker();

            this._seeker.attach(
                this._onSeekSpeedChangeBound,
                this._onSeekCurrentTimeChangeBound,
                this._onSeekBound,
                configuration.player.seekSteps,
                configuration.player.turnTrickplayImmediately
            );
        },

        /**
         * Attempts to set player properties.
         *
         * @param {Object} properties - Contains the player's properties to be set.
         *
         * @private
         */
        _startPlayback: function (properties) {
            this._view.showUI(true);
            this.playerInterface.initPlayer(properties);

            GA.onEvent(Constants.ANALYTICS_EVENT_CATEGORY_ACTION,
                Constants.ANALYTICS_EVENT_ACTION_START_VIDEO,
                {
                    eventLabel: AppUtils.getAnalyticsEventLabel(this._type)
                }
            );
            graylog.onPlaybackStart(properties.source.src, this._watchingVideoTimeStart);
        },

        /**
         * Prepares player to reset state and prepares playback.
         *
         * @param {Object} playbackData - Playback content data.
         * @private
         */
        _preparePlayer: function (playbackData) {
            this._view.focus();
            this._view.hideWarningBox(true);

            this.playerInterface.unload();
            this._playbackStatus.active = false;
            this._preparePlayback(playbackData);
        },

        /**
         * Prepares playback.
         *
         * @private
         */
        _preparePlayback: function () {
            throw 'Implement prepare playback';
        },

        /**
         * PlayerEvent.
         *
         * @param {Object} e - The player event data.
         * @private
         */
        onPlayerEvent: function (e) {

            switch (e.type) {
                case this.MEDIA_PLAYER_EVENTS.PLAYING:
                    this._currentProgram = this._program;

                    break;

                case this.MEDIA_PLAYER_EVENTS.PAUSED:
                    this.bookmarkManager.onPause();

                    // Don't focus on play pause button if seeking
                    this._view.onPause(!this._seeking);
                    this._view.showUI(true);
                    this._playbackStatus.active = false;
                    break;

                case this.MEDIA_PLAYER_EVENTS.COMPLETE:
                    this._onBack();
                    break;

                case this.MEDIA_PLAYER_EVENTS.ERROR:
                    this._playbackStatus.active = false;
                    this._onPlayerError(e.message);
                    break;

                case this.MEDIA_PLAYER_EVENTS.STATUS:
                    if (this._currentProgram === this._program) {
                        this._setProgress(e);
                        application.hideLoader();

                        if (!this._playbackStatus.active) {
                            this._view.showUI();
                            this._playbackStatus.active = true;
                        }
                    }
                    break;

                case this.MEDIA_PLAYER_EVENTS.BITRATE_CHANGED:
                    this._reportBitrateChange(e);
                    break;

                // No Default.
            }
        },

        getEvents: function () {
            return this.MEDIA_PLAYER_EVENTS;
        },

        /**
         * Returns the player manipulation decorator.
         *
         * @returns {Object} - Player manipulation decorator.
         */
        getPlayerManipulation: function () {
            return PlayerManipulation;
        },

        /**
         * KeyDown event.
         *
         * @param {Object} e - The event data.
         * @private
         */
        _onKeyDown: function (e) {

            switch (e.keyCode) {
                case KeyEvent.VK_LEFT:
                    this._view.showUI();
                    e.stopPropagation();
                    break;

                case KeyEvent.VK_RIGHT:
                    this._view.showUI();
                    e.stopPropagation();
                    break;

                case KeyEvent.VK_PAUSE:
                    this._onPause();
                    this._view.onPause();
                    break;

                case KeyEvent.VK_PLAY:
                    this._onPlay();
                    break;

                case KeyEvent.VK_PLAY_PAUSE:
                    this._onPlayPause();
                    break;

                case KeyEvent.VK_REWIND:
                    this._view.onRewind();
                    this._onRewind();
                    break;

                case KeyEvent.VK_FAST_FWD:
                    this._view.onFastForward();
                    this._onFastForward();
                    break;

                case KeyEvent.VK_BACK:
                case KeyEvent.VK_STOP:

                    if (this._view.isContentExpanded()) {
                        this._view.showUI();
                        this._view.closeExpandedContents();
                        this._view.focus();
                    } else {
                        this._onBack();
                    }

                    e.stopPropagation();
                    break;

                case KeyEvent.VK_UP:

                    if (!this._seeking) {
                        if (this._view.isUIVisible()) {
                            this._view.hideUI();
                        } else {
                            this._view.showUI();
                        }
                    }

                    e.stopPropagation();

                    break;

                case KeyEvent.VK_DOWN:

                    if (this._view.getMiniEPG().isExpanded()) {
                        this._view.getMiniEPG().focus();
                    } else if (this._view.isUIVisible()) {
                        this._view.showProgramInfo(false);
                        this._view.showMiniEPG(false);
                        this._view.getMiniEPG().focus();
                    }

                    this._view.showUI();

                    break;

                case KeyEvent.VK_INFO:

                    if (this._view) {
                        if (this._view.isUIVisible()) {
                            this._view.hideUI();
                        } else {
                            this._view.showUI();
                        }
                    }
                    break;
            }
        },

        /**
         * KeyUp event.
         *
         * @param {Object} e - The keyEvent data.
         * @private
         */
        _onKeyUp: function (e) {
            if (this._keyHoldTimeout) {
                this._onKeyHoldCancelled(e);
            }
        },

        /**
         * Sets the keyHold data.
         *
         * @param {Object} e - The keyEvent data.
         * @param {Function} keyHoldHandler - Function that handles the keyEvent.
         */
        _setKeyHoldData: function (e, keyHoldHandler) {
            var keyCode = e.keyCode;

            this._keyHoldData = {};
            this._keyHoldData[keyCode] = {
                keyHoldHandler: Utils.bind(keyHoldHandler, this, e)
            };
            this._keyHoldTimeout = setTimeout(Utils.bind(this._onKeyHoldTriggered, this, e), KEY_HOLD_TIMEOUT);
        },

        /**
         * This function is triggered when keyHold times out. KeyHoldHandler executed with boolean meaning keyhold
         * behaviour should be executed.
         *
         * @param {Object} e - The keyEvent data.
         * @private
         **/
        _onKeyHoldTriggered: function (e) {
            var keyCode = e.keyCode,
                keyHoldData = this._keyHoldData[keyCode];

            this._keyHoldTimeout = null;

            if (keyHoldData && Utils.isFunction(keyHoldData.keyHoldHandler)) {
                keyHoldData.keyHoldHandler(true);
                delete this._keyHoldData[keyCode];
            }
        },

        /**
         * This function is triggered when keyHold is cancelled. KeyHoldHandler executed with boolean meaning keyhold
         * behaviour should not be executed.
         *
         * @param {Object} e - The keyEvent data.
         * @private
         **/
        _onKeyHoldCancelled: function (e) {
            var keyCode = e.keyCode,
                keyHoldData = this._keyHoldData[keyCode];

            clearTimeout(this._keyHoldTimeout);
            this._keyHoldTimeout = null;

            if (keyHoldData && Utils.isFunction(keyHoldData.keyHoldHandler)) {
                keyHoldData.keyHoldHandler(false);
                delete this._keyHoldData[keyCode];
            }
        },

        /**
         * Select event.
         *
         * @param {Object} e - The event data.
         * @private
         */
        _onSelect: function (e) {
            var target = e.target,
                programDetails;

            if (this._view.isUIVisible()) {

                switch (target.id) {
                    case Constants.PLAYER_CONTROLS_MENU:
                        this._onShownMenu();
                        break;

                    case Constants.PLAYER_CONTROLS_INFO:
                        programDetails = this._view.getProgramDetails();

                        if (programDetails.isExpanded()) {
                            this._onInfo(false, null);
                        } else {
                            this._requestDetailInfo()
                                .then(Utils.bind(this._onInfo, this, true));
                        }
                        break;

                    case Constants.PLAYER_CONTROLS_RESTART:
                        this._onRestart();
                        break;

                        case Constants.PLAYER_CONTROLS_REWIND:
                        this._onRewind();
                        break;

                    case Constants.PLAYER_CONTROLS_PLAYPAUSE:
                        this._onPlayPause();
                        break;

                    case Constants.PLAYER_CONTROLS_FORWARD:
                        this._onFastForward();
                        break;

                    case Constants.PLAYER_CONTROLS_RECORD:
                        this._onRecord();
                        break;

                    case Constants.PLAYER_CONTROLS_MINIEPG:
                        this._onMiniEPG();
                        break;

                    case Constants.PLAYER_CONTROLS_NEXT:
                        if (this._playbackStatus.active) {
                            this._onNextEpisode(this._nextEpisodes.splice(0, 1)[0]);
                        }
                        this._view.closeExpandedContents();
                        break;

                    case Constants.PLAYER_CONTROLS_BACK:
                        this._view.closeExpandedContents();
                        this._onBack();
                        break;

                    case Constants.PLAYER_CONTROLS_LIVE:
                        this._onLive();
                        break;
                }
            } else {

                this._view.showUI(true);
            }
        },

        /**
         * Requests the content's detail info.
         *
         * @returns {Promise} - The detail info.
         * @private
         */
        _requestDetailInfo: function () {
            this._lastRequestedDetailInfoId = this._program.getId();

            return api.read('detail', {
                params: {
                    endpoint: this._program.getDetailsAction()
                },
                withCredentials: true
            });
        },

        /**
         * Reports the bitrate change.
         *
         * @param {Object} e - The event data.
         * @private
         */
        _reportBitrateChange: function (e) {

            GA.onEvent(Constants.ANALYTICS_EVENT_ACTION_PLAYER,
                Constants.ANALYTICS_EVENT_ACTION_BITRATE, {
                eventLabel: e.bitrate
            });
        },

        /**
         * Sends analytics for trickplays.
         *
         * @private
         */
        _sendTrickplayAnalytics: function () {
            if (this._seeking) {
                if (this._seeking === Constants.PLAYER_SEEK_DIRECTION_FORWARD) {
                    GA.onEvent(
                        Constants.ANALYTICS_EVENT_CATEGORY_ACTION,
                        Constants.ANALYTICS_EVENT_ACTION_TRICKPLAY,
                        {
                            eventLabel: Constants.ANALYTICS_EVENT_LABEL_TYPE_FORWARD
                        }
                    );
                } else if (this._seeking === Constants.PLAYER_SEEK_DIRECTION_BACKWARD) {
                    GA.onEvent(
                        Constants.ANALYTICS_EVENT_CATEGORY_ACTION,
                        Constants.ANALYTICS_EVENT_ACTION_TRICKPLAY,
                        {
                            eventLabel: Constants.ANALYTICS_EVENT_LABEL_TYPE_REWIND
                        }
                    );
                }

                this._seeking = null;
            }
        },

        /**
         * Sends analytics for next episode action.
         */
        _sendNextEpisodeAnalytics: function () {
            GA.onEvent(
                Constants.ANALYTICS_EVENT_CATEGORY_ACTION,
                Constants.ANALYTICS_EVENT_ACTION_NEXT_VIDEO,
                {
                    eventLabel: AppUtils.getAnalyticsEventLabel(this._type)
                }
            );
        },

        /**
         * Sends analytics after showing info details.
         */
        _sendOpenInfoAnalytics: function () {
            GA.onEvent(
                Constants.ANALYTICS_EVENT_CATEGORY_ACTION,
                Constants.ANALYTICS_EVENT_ACTION_OPEN_DETAILS,
                {
                    eventLabel: AppUtils.getAnalyticsEventLabel(this._type)
                }
            );
        },

        /**
         * Sends analytics on content paused.
         */
        _sendPauseAnalytics: function () {
            GA.onEvent(
                Constants.ANALYTICS_EVENT_CATEGORY_ACTION,
                Constants.ANALYTICS_EVENT_ACTION_PAUSE_BUTTON,
                {
                    eventLabel: AppUtils.getAnalyticsEventLabel(this._type)
                });
        },

        /**
         * Returns the player view.
         *
         * @returns {Object} - The player view.
         */
        getView: function () {
            return this._view;
        },

        /**
         * Gets executed when the player error triggers.
         *
         * @param {Object} e - The player error event.
         * @private
         */
        _onPlayerError: function (e) {
            var message;

            if (this._errorState) {
                return;
            }

            this._errorState = true;

            application.hideLoader();

            if (e && e.resultCode && e.resultCode === '406') {
                this._onPlaybackIssueMessage(e);
                return;
            }

            if (e && e.toString && (e.toString().indexOf('PLAYER_ERROR_INVALID_STATE') >= 0)) {

                /*
                 * We want to ignore this error from Samsung Tizen,
                 * which is caused when zapping fast.
                 */
                return;
            }

            if (e) {
                message = e.toString ? e.toString() : JSON.stringify(e);
                if (this._mediaSource) {
                    graylog.onPlaybackError(this._mediaSource.getMediaUrl(), 'n.a.', 'playready', message);
                } else {
                    graylog.onPlaybackError('n.a', 'n.a', 'n.a.', message);
                }
            }

            if (this._deviceBrand === 'default') {

                // We want to disable the player errors for the development browser.
                return;
            }

            if (application.getComponent('player').isFocussed()) {
                this._cannotLoadVideoError(Utils.getNested(e, 'errorDescription') || '');
            } else {
                this._showErrorOnPlayerFocus = true;
            }
        },

        /**
         * Playback issue message displayer.
         *
         * @private
         */
        _onPlaybackIssueMessage: function () {
            this._view.showWarningBox({
                icon: 'icon-alert-v2',
                text: l10n.get('player.warningbox.cantplay')
            });

            GA.onEvent(Constants.ANALYTICS_EVENT_CATEGORY_PLAYBACK_FAILED, Constants.ANALYTICS_EVENT_ACTION_PLAYER);
        },

        /**
         * Popups the error message.
         *
         * @param {string} errorCode - The errorcode.
         *
         * @private
         */
        _cannotLoadVideoError: function (errorCode) {
            var type = 'fullscreen',
                imgUrl = 'src/assets/images/error-icon.png',
                title,
                text,
                button;

            errorCode = errorCode || '';

            switch (errorCode) {
                case ApiErrorCodes.CONCURRENT_STREAM_LIMIT_REACHED_1:
                case ApiErrorCodes.CONCURRENT_STREAM_LIMIT_REACHED_2:
                case ApiErrorCodes.CONCURRENT_STREAM_LIMIT_REACHED_3:
                case ApiErrorCodes.CONCURRENT_STREAM_LIMIT_REACHED_4:
                    title = L10N.getInstance().get('errors.stream.max_concurrent_streams_title');
                    text = L10N.getInstance().get('errors.stream.max_concurrent_streams_text');
                    button = {
                        id: 'error-close-button',
                        label: L10N.getInstance().get('errors.ok')
                    };
                    break;
                case ApiErrorCodes.DISNEY:
                case ApiErrorCodes.DISNEYREF:
                    title = L10N.getInstance().get('errors.stream.disney');
                    button = {id: 'error-close-button', label: L10N.getInstance().get('errors.ok')};
                    errorCode = null;
                    break;
                default:
                    title = L10N.getInstance().get('errors.cannot_load_video');
                    button = {
                        id: 'error-close-button',
                        label: L10N.getInstance().get('errors.close')
                    };
            }

            this._showErrorOnPlayerFocus = false;
            application.hideLoader();
            application.route('error', {
                type: type,
                title: title,
                text: text,
                button: button,
                imgUrl: imgUrl,
                errorCode: errorCode,
                callback: Utils.bind(function () {
                    this._errorState = false;
                    this._onBack();
                }, this)
            });
        },

        /**
         * Checks if the content requires pin to send as header.
         *
         * @param {Object} program - The program to check if headers are needed.
         * @returns {Object} - Headers for the stream.
         */
        getStreamHeaders: function (program) {
        var headers = {};

            if (this._streamWithParental || program &&
                (program.isLocked() || program.streamRequiresPin())) {
                headers['pcPin'] = sessionManager.getUserPin();

                this._streamWithParental = false;
            }

            return headers;
        },

        /**
         * Shows parental ping with callback behaviours.
         *
         * @param {Object} parentalParams - Contains the callbacks needed for success, error, escape, and key event.
         * and key-event behaviours.
         *
         * @private
         */
        _showParental: function (parentalParams) {
            application.route('parentalpin', {
                successCallback: Utils.bind(this._onParentalCallback, this, parentalParams.successCallback),
                errorCallback: Utils.bind(this._onParentalCallback, this, parentalParams.errorCallback),
                escapeCallback: Utils.bind(this._onParentalCallback, this, parentalParams.escapeCallback),
                callingPageKeyEvent: {
                    keyCodes: [KeyEvent.VK_CHANNEL_UP, KeyEvent.VK_CHANNEL_DOWN],
                    keyEventCallback: Utils.bind(this._onParentalCallback, this, parentalParams.keyEventCallback)
                }
            });
        },

        /**
         * Determine direction of mouse seekening.
         *
         * @param {number} currentTime - Current time of the stream.
         * @param {number} newTime - New time of the stream that we want to play.
         * @returns {string} - Direction of the seeking.
         * @private
         */
        _getMouseSeekDirection: function (currentTime, newTime) {
            return currentTime > newTime ? Constants.PLAYER_SEEK_DIRECTION_BACKWARD : Constants.PLAYER_SEEK_DIRECTION_FORWARD;
        },

        /**
         * Proxies all callback actions to disable checking parental flag.
         *
         * @param {Function} callback - The parental action callback.
         * @param {Object} ev - Any event from the parental callback (key event callback).
         * @private
         */
        _onParentalCallback: function (callback, ev) {
            this._checkingParental = false;
            callback(ev);
        },

        /**
         * Shows the player's view UI.
         *
         * @private
         */
        _showUI: function () {
            this._view.showUI(this.playerInterface.isPaused());
        },

        /**
         * On back action.
         *
         * @private
         */
        _onBack: function () {
            this._errorState = false;

            this._removeEventListeners();
            application.hideLoader();
            application.hidePlayer(true);
            application.showMenu('', false, true);
        },

        /**
         * Close player.
         */
        closePlayer: function () {
            this._errorState = false;

            this._removeEventListeners();
            application.hideLoader();
        }
    });
});