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();
}
});
});