Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
220
gaia/tv_apps/fling-player/js/casting_message.js
Executable file
220
gaia/tv_apps/fling-player/js/casting_message.js
Executable file
|
|
@ -0,0 +1,220 @@
|
|||
/* global mDBG */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This object parses and process the message casted
|
||||
* between receiver and controller.
|
||||
*
|
||||
* The message casted is in JSON and shall carry properties below:
|
||||
* - seq: Number. The sequence of message sent. Increased by 1 when sending
|
||||
* one new message. The message sequences from controller and receiver
|
||||
* shall be independent to each other.
|
||||
* - type: String. Indicate the message type.
|
||||
* - other extra properties depending on the type
|
||||
*
|
||||
* For example, the message of loading request from a controller may be like:
|
||||
* {
|
||||
* 'seq': 3,
|
||||
* 'type': 'load',
|
||||
* 'url': 'http://www.example.com/foo.ogg'
|
||||
* }
|
||||
*/
|
||||
var castingMessage = {};
|
||||
|
||||
castingMessage.type = Object.freeze({
|
||||
|
||||
/**
|
||||
* Represent the device info.
|
||||
* The extra message properties:
|
||||
* - displayName: String. The device display name
|
||||
*/
|
||||
'device-info': {
|
||||
name: 'device-info',
|
||||
sanitizeMsg: function (msg) {
|
||||
|
||||
if (msg.displayName !== undefined &&
|
||||
typeof msg.displayName != 'string'
|
||||
) {
|
||||
throw new Error('Ilegal display name = ' + msg.displayName +
|
||||
' in casting message of type = ' + this.name);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Represent acknowledgement of message receiving.
|
||||
* The extra message properties:
|
||||
* - error: String. Present if there is error
|
||||
*/
|
||||
ack: {
|
||||
name: 'ack',
|
||||
sanitizeMsg: function (msg) {
|
||||
|
||||
if (msg.error !== null &&
|
||||
msg.error !== undefined &&
|
||||
typeof msg.error != 'string'
|
||||
) {
|
||||
throw new Error('Ilegal error = ' + msg.error +
|
||||
' in casting message of type = ' + this.name);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Represent video playing status. The extra message properties:
|
||||
* - time: Number. The current video time
|
||||
* - status: String. Describe status, could be
|
||||
* - buffering: Video buffering
|
||||
* - loaded: The media's metadata has finished loading;
|
||||
* all attributes now contain as much useful information
|
||||
* as they're going to.
|
||||
* - buffered: Video buffered and ready to play
|
||||
* - playing: Video is playing
|
||||
* - seeked: Video seeking is done
|
||||
* - stopped: Video is not playing (maybe ended or paused).
|
||||
* - error: Error on playing.
|
||||
* When error occurs, one more extra message properties:
|
||||
* - error: String. The error code.
|
||||
*/
|
||||
status: {
|
||||
name: 'status',
|
||||
sanitizeMsg: function (msg) {
|
||||
|
||||
if (typeof msg.time != 'number' || (msg.time >= 0) === false) {
|
||||
throw new Error('Ilegal time = ' + msg.time +
|
||||
' in casting message of type = ' + this.name);
|
||||
}
|
||||
|
||||
var validStatus = [
|
||||
'buffering', 'loaded', 'buffered',
|
||||
'playing', 'seeked', 'stopped', 'error'
|
||||
];
|
||||
if (validStatus.indexOf(msg.status) < 0) {
|
||||
throw new Error('Ilegal status = ' + msg.status +
|
||||
' in casting message of type = ' + this.name);
|
||||
}
|
||||
|
||||
if (msg.status == 'error' && typeof msg.error != 'string') {
|
||||
throw new Error('Ilegal error = ' + msg.error +
|
||||
' in casting message of type = ' + this.name);
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Request to load and play video. The extra message properties:
|
||||
* - url : String. The video url.
|
||||
*/
|
||||
load: {
|
||||
name: 'load',
|
||||
sanitizeMsg: function (msg) {
|
||||
if (typeof msg.url != 'string' || !msg.url) {
|
||||
throw new Error('Ilegal url = ' + msg.url +
|
||||
' in casting message of type = ' + this.name);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Request to play video
|
||||
*/
|
||||
play: {
|
||||
name: 'play'
|
||||
},
|
||||
|
||||
/**
|
||||
* Request to pause video
|
||||
*/
|
||||
pause: {
|
||||
name: 'pause'
|
||||
},
|
||||
|
||||
/**
|
||||
* Request to seek on video. The extra message properties:
|
||||
* - time: Number. The time to seek.
|
||||
*/
|
||||
seek: {
|
||||
name: 'seek',
|
||||
sanitizeMsg: function (msg) {
|
||||
if (typeof msg.time != 'number' || (msg.time >= 0) === false) {
|
||||
throw new Error('Ilegal time in casting message of type = ' +
|
||||
this.name, msg.time);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Validate and make sure the message format, datatype.
|
||||
*
|
||||
* @param {Object} content the message content to sanitize
|
||||
* @return {Object} Sanitized message
|
||||
*/
|
||||
castingMessage.sanitizeMsg = function (content) {
|
||||
mDBG.log('castingMessage#sanitizeMsg');
|
||||
mDBG.log('Sanitize message content:', content);
|
||||
|
||||
if (this.type[content.type]) {
|
||||
|
||||
if (typeof content.seq == 'number' && content.seq >= 0) {
|
||||
|
||||
if (typeof this.type[content.type].sanitizeMsg == 'function') {
|
||||
content = this.type[content.type].sanitizeMsg(content);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error('Missing sequence of casting message : ' +
|
||||
JSON.stringify(content));
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error('Unknown type of casting message : ' + content.type);
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} txt the message txt sent from controller
|
||||
* @return {Array.<Object>} the parsed messages
|
||||
*/
|
||||
castingMessage.parse = function (txt) {
|
||||
mDBG.log('castingMessage#parse');
|
||||
mDBG.log('Parsing : ', txt);
|
||||
|
||||
var data = '[' + txt.replace(/}{/g, '},{') + ']';
|
||||
mDBG.log('Transformed : ', data);
|
||||
|
||||
var msgs = JSON.parse(data);
|
||||
mDBG.log('JSON Parsed : ', msgs);
|
||||
|
||||
var results = [];
|
||||
msgs.forEach((m) => {
|
||||
try {
|
||||
results.push(this.sanitizeMsg(m));
|
||||
} catch (e) {
|
||||
mDBG.error(e);
|
||||
}
|
||||
});
|
||||
mDBG.log('Sanitized : ', results);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} content the message content being sent.
|
||||
* @return {String} the messsage txt to sent
|
||||
*/
|
||||
castingMessage.stringify = function (content) {
|
||||
return JSON.stringify(this.sanitizeMsg(content));
|
||||
};
|
||||
|
||||
exports.castingMessage = castingMessage;
|
||||
|
||||
})(window);
|
||||
216
gaia/tv_apps/fling-player/js/connector.js
Executable file
216
gaia/tv_apps/fling-player/js/connector.js
Executable file
|
|
@ -0,0 +1,216 @@
|
|||
/* global evt, castingMessage, mDBG */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This class handles the connection to controller via the presentation API
|
||||
* The events of controller's message are:
|
||||
* - loadRequest: Triggered when controller asks to load video.
|
||||
* Will be passed in one object with url property storing the
|
||||
* video url.
|
||||
* - playRequest: Triggered when controller asks to play video.
|
||||
* - pauseRequest: Triggered when controller asks to pause video.
|
||||
* - seekRequest: Triggered when controller asks to seek video.
|
||||
* Will be passed in one object with time property storing the
|
||||
* seeking time.
|
||||
*
|
||||
* @param {object} presentation The navigator's presentation object
|
||||
* @constructor
|
||||
*/
|
||||
function Connector(presentation) {
|
||||
this._presentation = presentation;
|
||||
this._msgSeq = 0; // This sequence of message sent to controller
|
||||
this._lastSeq = -1; // The sequence of the last message received
|
||||
this._isInit = false;
|
||||
this._isInitConnection = false;
|
||||
this._controllingDeviceInfo = null;
|
||||
}
|
||||
|
||||
var proto = evt(Connector.prototype);
|
||||
|
||||
proto.init = function () {
|
||||
|
||||
if (this._isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDBG.log('Connector#init');
|
||||
if (!this._presentation) {
|
||||
throw new Error('Init connection without the presentation object.');
|
||||
}
|
||||
|
||||
this._isInit = true;
|
||||
|
||||
this._presentation.receiver.getConnection().then(
|
||||
this._initConnection.bind(this)
|
||||
);
|
||||
|
||||
this._presentation.receiver.onconnectionavailable = (e) => {
|
||||
this._presentation.receiver.getConnection().then(
|
||||
this._initConnection.bind(this)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
proto._initConnection = function (connection) {
|
||||
|
||||
if (this._isInitConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
mDBG.log('Connector#_initConnection');
|
||||
if (!this._presentation) {
|
||||
throw new Error('Init connection without the presentation object.');
|
||||
}
|
||||
|
||||
this._isInitConnection = true;
|
||||
|
||||
mDBG.log('this._connection = ', connection);
|
||||
|
||||
this._connection = connection;
|
||||
this._connection.onmessage = this.onConnectionMessage.bind(this);
|
||||
this._connection.onstatechange = this.onConnectionStateChange.bind(this);
|
||||
};
|
||||
|
||||
proto.sendMsg = function (msg) {
|
||||
mDBG.log('Connector#sendMsg');
|
||||
mDBG.log('msg = ', msg);
|
||||
try {
|
||||
var txt = castingMessage.stringify(msg);
|
||||
this._connection.send(txt);
|
||||
} catch (e) {
|
||||
mDBG.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
proto.getControllingDeviceInfo = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this._controllingDeviceInfo) {
|
||||
resolve(this._controllingDeviceInfo);
|
||||
} else {
|
||||
var handle = () => {
|
||||
this.off('ControllingDeviceInfoUpdated', handle);
|
||||
resolve(this._controllingDeviceInfo);
|
||||
};
|
||||
this.on('ControllingDeviceInfoUpdated', handle);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
proto.replyACK = function (msg, error) {
|
||||
|
||||
mDBG.log('Connector#replyACK');
|
||||
|
||||
var reply = {
|
||||
'type': 'ack',
|
||||
'seq': msg.seq
|
||||
};
|
||||
|
||||
if (error) {
|
||||
reply.error = '' + error;
|
||||
}
|
||||
|
||||
this.sendMsg(reply);
|
||||
};
|
||||
|
||||
proto.reportStatus = function (status, data) {
|
||||
|
||||
mDBG.log('Connector#reportStatus');
|
||||
|
||||
var msg = {
|
||||
'type': 'status',
|
||||
'seq': this._msgSeq++,
|
||||
'status': status,
|
||||
'time': data.time
|
||||
};
|
||||
|
||||
if (data.error) {
|
||||
msg.error = '' + data.error;
|
||||
}
|
||||
|
||||
if (data.detail) { // TODO: Discuss should we need this ?
|
||||
msg.detail = data.detail;
|
||||
}
|
||||
|
||||
this.sendMsg(msg);
|
||||
};
|
||||
|
||||
proto.handleRemoteMessage = function (msg) {
|
||||
|
||||
mDBG.log('Connector#handleRemoteMessage');
|
||||
mDBG.log('msg = ', msg);
|
||||
|
||||
var err;
|
||||
try {
|
||||
|
||||
// We don't process the out of dated message.
|
||||
if (this._lastSeq >= msg.seq) {
|
||||
throw new Error('Receive outdated message with ' +
|
||||
'msg sequence = ' + msg.seq);
|
||||
}
|
||||
this._lastSeq = msg.seq;
|
||||
|
||||
switch(msg.type) {
|
||||
|
||||
case 'load':
|
||||
if (typeof msg.url != 'string' && !msg.url) {
|
||||
throw new Error('Controller dose not provide the url to load.');
|
||||
}
|
||||
this.fire('loadRequest', { url : msg.url });
|
||||
break;
|
||||
|
||||
case 'play':
|
||||
this.fire('playRequest');
|
||||
break;
|
||||
|
||||
case 'pause':
|
||||
this.fire('pauseRequest');
|
||||
break;
|
||||
|
||||
case 'seek':
|
||||
var time = +msg.time;
|
||||
if (time <= 0) {
|
||||
throw new Error('Controller asks to seek on invalid time = ' +
|
||||
time);
|
||||
}
|
||||
this.fire('seekRequest', { time : time });
|
||||
break;
|
||||
|
||||
case 'device-info':
|
||||
this._controllingDeviceInfo = {
|
||||
displayName: msg.displayName
|
||||
};
|
||||
this.fire('ControllingDeviceInfoUpdated');
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
err = e;
|
||||
mDBG.error(e);
|
||||
}
|
||||
|
||||
this.replyACK(msg, err);
|
||||
};
|
||||
|
||||
proto.onConnectionMessage = function (e) {
|
||||
mDBG.log('Connector#onConnectionMessage');
|
||||
|
||||
var messages = castingMessage.parse(e.data);
|
||||
|
||||
mDBG.log('messages = ', messages);
|
||||
|
||||
messages.sort((a, b) => { // Make sure message sequence
|
||||
return a.seq - b.seq;
|
||||
});
|
||||
|
||||
messages.forEach(message => this.handleRemoteMessage(message));
|
||||
};
|
||||
|
||||
proto.onConnectionStateChange = function () {
|
||||
mDBG.log('Connector#onConnectionStateChange');
|
||||
mDBG.log('State = ', this._connection.state);
|
||||
// TODO: How to do when presentation session is closed or terminated
|
||||
};
|
||||
|
||||
exports.Connector = Connector;
|
||||
|
||||
})(window);
|
||||
600
gaia/tv_apps/fling-player/js/fling_player.js
Executable file
600
gaia/tv_apps/fling-player/js/fling_player.js
Executable file
|
|
@ -0,0 +1,600 @@
|
|||
/* global VideoPlayer, Connector, mDBG, KeyNavigationAdapter,
|
||||
SimpleKeyNavigation
|
||||
*/
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
// Helping variables, methods
|
||||
|
||||
var uiID = {
|
||||
player : 'player',
|
||||
loadingUI : 'loading-section',
|
||||
initialMsgSection : 'initial-message-section',
|
||||
controlPanel : 'video-control-panel',
|
||||
backwardButton : 'backward-button',
|
||||
playButton : 'play-button',
|
||||
forwardButton : 'forward-button',
|
||||
bufferedTimeBar : 'buffered-time-bar',
|
||||
elapsedTimeBar : 'elapsed-time-bar',
|
||||
elapsedTime : 'elapsed-time',
|
||||
durationTime : 'duration-time'
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
// Helping variables, methods end
|
||||
/**
|
||||
* FlingPlayer could be controlled by 2 sources.
|
||||
* The 1st source is remote control device, like Fennec,
|
||||
* which sends command through the Presentation API.
|
||||
* The 2nd source is TV remote controller which sends command through
|
||||
* UI navigation by pressing TV remote controller's key
|
||||
*/
|
||||
function FlingPlayer(videoPlayer, connector) {
|
||||
this._player = videoPlayer;
|
||||
this._connector = connector;
|
||||
}
|
||||
|
||||
var proto = FlingPlayer.prototype;
|
||||
|
||||
proto.MAX_DISPLAYED_VIDEO_TIME_SEC = 3600 * 99 + 60 * 59 + 59;
|
||||
proto.CONTROL_PANEL_HIDE_DELAY_MS = 3000;
|
||||
proto.SEEK_ON_KEY_PRESS_INTERVAL_MS = 150;
|
||||
proto.SEEK_ON_LONG_KEY_PRESS_MS = 3000;
|
||||
proto.SEEK_ON_EXTRA_LONG_KEY_PRESS_MS = 6000;
|
||||
proto.SEEK_ON_KEY_PRESS_NORMAL_STEP_SEC = 5;
|
||||
proto.SEEK_ON_KEY_PRESS_LARGE_STEP_SEC = 10;
|
||||
proto.SEEK_ON_KEY_PRESS_EXTRA_LARGE_STEP_SEC = 30;
|
||||
proto.INITIAL_MSG_MIN_DISPLAY_TIME_MS = 2000;
|
||||
proto.REFOCUS_PLAY_BUTTON_AFTER_HIDDING_SEC = 60000;
|
||||
|
||||
proto.init = function () {
|
||||
|
||||
this._autoPlayOnSeeked = true;
|
||||
this._seekOnKeyPressTimer = null;
|
||||
this._seekOnKeyPressDirection = null; // 'backward' or 'forward'
|
||||
this._seekOnKeyPressStartTime = null; // in ms
|
||||
this._hideControlsTimer = null;
|
||||
this._initialMsgStartTime = null; // in ms
|
||||
|
||||
this._loadingUI = $(uiID.loadingUI);
|
||||
this._initialMsgSection = $(uiID.initialMsgSection);
|
||||
this._controlPanel = $(uiID.controlPanel);
|
||||
this._backwardButton = $(uiID.backwardButton);
|
||||
this._playButton = $(uiID.playButton);
|
||||
this._forwardButton = $(uiID.forwardButton);
|
||||
this._bufferedTimeBar = $(uiID.bufferedTimeBar);
|
||||
this._elapsedTimeBar = $(uiID.elapsedTimeBar);
|
||||
this._elapsedTime = $(uiID.elapsedTime);
|
||||
this._durationTime = $(uiID.durationTime);
|
||||
|
||||
this._keyNav = new SimpleKeyNavigation();
|
||||
this._keyNav.start(
|
||||
[this._backwardButton, this._playButton, this._forwardButton],
|
||||
SimpleKeyNavigation.DIRECTION.HORIZONTAL
|
||||
);
|
||||
this._keyNav.focusOn(this._playButton);
|
||||
this._keyNav.pause();
|
||||
|
||||
this._keyNavAdapter = new KeyNavigationAdapter();
|
||||
this._keyNavAdapter.init();
|
||||
this._keyNavAdapter.on('enter', this.onKeyEnterDown.bind(this));
|
||||
this._keyNavAdapter.on('enter-keyup', this.onKeyEnterUp.bind(this));
|
||||
this._keyNavAdapter.on('esc-keyup', this.onBackKeyUp.bind(this));
|
||||
this._keyNavAdapter.on('move-keyup',
|
||||
this.onDemandingControlPanel.bind(this)
|
||||
);
|
||||
this._keyNavAdapter.on('enter-keyup',
|
||||
this.onDemandingControlPanel.bind(this)
|
||||
);
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
// We don't need to restore the video while visibilityState goes back
|
||||
// because system app will kill the original one and relaunch a new one.
|
||||
if (document.visibilityState === 'hidden') {
|
||||
this._player.release();
|
||||
}
|
||||
});
|
||||
|
||||
this._initPlayer();
|
||||
this._initSession();
|
||||
};
|
||||
|
||||
proto._initSession = function () {
|
||||
this._connector.init();
|
||||
this._connector.on('loadRequest', this.onLoadRequest.bind(this));
|
||||
this._connector.on('playRequest', this.onPlayRequest.bind(this));
|
||||
this._connector.on('seekRequest', this.onSeekRequest.bind(this));
|
||||
this._connector.on('pauseRequest', this.onPauseRequest.bind(this));
|
||||
};
|
||||
|
||||
proto._initPlayer = function () {
|
||||
var video = this._player.getVideo();
|
||||
this._player.init();
|
||||
video.addEventListener('loadedmetadata', this);
|
||||
video.addEventListener('durationchange', this);
|
||||
video.addEventListener('timeupdate', this);
|
||||
video.addEventListener('waiting', this);
|
||||
video.addEventListener('playing', this);
|
||||
video.addEventListener('seeked', this);
|
||||
video.addEventListener('pause', this);
|
||||
video.addEventListener('ended', this);
|
||||
video.addEventListener('error', this);
|
||||
};
|
||||
|
||||
// UI handling
|
||||
|
||||
proto.resetUI = function () {
|
||||
this.moveTimeBar('elapsed', 0);
|
||||
this.moveTimeBar('buffered', 0);
|
||||
this.writeTimeInfo('elapsed', 0);
|
||||
this.writeTimeInfo('duration', 0);
|
||||
this.setPlayButtonState('paused');
|
||||
this._keyNav.focusOn(this._playButton);
|
||||
};
|
||||
|
||||
proto.showLoading = function (loading) {
|
||||
this._loadingUI.hidden = !loading;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {boolean} on True to toggle on and false to toggle off
|
||||
*/
|
||||
proto.toggleInitialMessage = function (on, opt = {}) {
|
||||
if (on) {
|
||||
this._initialMsgStartTime = (new Date()).getTime();
|
||||
this._initialMsgSection.classList.remove('fade-out');
|
||||
} else {
|
||||
if (opt.delayHiding) {
|
||||
// Make sure that the initial message has beed displayed
|
||||
// for at least the min duration
|
||||
var displayDuration = (new Date()).getTime() -
|
||||
this._initialMsgStartTime;
|
||||
var gap = this.INITIAL_MSG_MIN_DISPLAY_TIME_MS - displayDuration;
|
||||
if (gap < 0) {
|
||||
gap = 0;
|
||||
}
|
||||
setTimeout(() => this.toggleInitialMessage(false), gap);
|
||||
} else {
|
||||
this._initialMsgSection.classList.add('fade-out');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} state 'playing' or 'paused'
|
||||
*/
|
||||
proto.setPlayButtonState = function (state) {
|
||||
switch (state) {
|
||||
case 'playing':
|
||||
this._playButton.setAttribute('data-icon', 'fling-player-pause');
|
||||
break;
|
||||
|
||||
case 'paused':
|
||||
this._playButton.setAttribute('data-icon', 'fling-player-play');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.isControlPanelHiding = function () {
|
||||
return this._controlPanel.classList.contains('fade-out');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Boolean} autoHide? Auto hide the controls later. Default to false
|
||||
*/
|
||||
proto.showControlPanel = function (autoHide) {
|
||||
|
||||
if (this._hideControlsTimer) {
|
||||
clearTimeout(this._hideControlsTimer);
|
||||
this._hideControlsTimer = null;
|
||||
}
|
||||
|
||||
this._keyNav.resume();
|
||||
this._controlPanel.classList.remove('fade-out');
|
||||
|
||||
if (autoHide === true) {
|
||||
this.hideControlPanel();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Boolean} immediate? Hide immediately or later. Default to false.
|
||||
*/
|
||||
proto.hideControlPanel = function (immediate) {
|
||||
|
||||
if (this._hideControlsTimer) {
|
||||
clearTimeout(this._hideControlsTimer);
|
||||
this._hideControlsTimer = null;
|
||||
}
|
||||
|
||||
if (immediate === true) {
|
||||
|
||||
if (!this.isControlPanelHiding()) {
|
||||
this._keyNav.pause();
|
||||
this._controlPanel.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
if (this.isControlPanelHiding()) {
|
||||
this._keyNav.focusOn(this._playButton);
|
||||
}
|
||||
}, this.REFOCUS_PLAY_BUTTON_AFTER_HIDDING_SEC);
|
||||
}
|
||||
} else {
|
||||
|
||||
this._hideControlsTimer = setTimeout(() => {
|
||||
this.hideControlPanel(true);
|
||||
}, this.CONTROL_PANEL_HIDE_DELAY_MS);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} type 'buffered' or 'elapsed'
|
||||
* @param {number} sec the sec in video you want to move to
|
||||
*/
|
||||
proto.moveTimeBar = function (type, sec) {
|
||||
|
||||
mDBG.log('FlingPlayer#moveTimeBar');
|
||||
mDBG.log('Move type ', type);
|
||||
mDBG.log('Move to ', sec);
|
||||
|
||||
var timeBar = this[`_${type}TimeBar`];
|
||||
var duration = this._player.getRoundedDuration();
|
||||
sec = Math.round(sec);
|
||||
|
||||
if (!timeBar ||
|
||||
typeof sec != 'number' ||
|
||||
isNaN(sec) ||
|
||||
sec < 0 ||
|
||||
sec > duration
|
||||
) {
|
||||
mDBG.warn('Not moving due to corrupt type/sec', type, sec);
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
mDBG.log('Move to sec / duration = %d / %d', sec, duration);
|
||||
timeBar.style.width = (100 * sec / duration) + '%';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} type 'elapsed' or 'duration'
|
||||
* @param {number} sec
|
||||
*/
|
||||
proto.writeTimeInfo = function (type, sec) {
|
||||
|
||||
var timeInfo = this[`_${type}Time`];
|
||||
var duration = Math.min(this._player.getRoundedDuration(),
|
||||
this.MAX_DISPLAYED_VIDEO_TIME_SEC);
|
||||
sec = Math.min(Math.round(sec), this.MAX_DISPLAYED_VIDEO_TIME_SEC);
|
||||
|
||||
if (!timeInfo ||
|
||||
(sec >= 0) === false ||
|
||||
(sec <= duration) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
var t = this._player.parseTime(sec);
|
||||
t.hh = (t.hh <= 0) ? '' :
|
||||
(t.hh < 10) ? '0' + t.hh + ':' : t.hh + ':';
|
||||
t.mm = (t.mm < 10) ? '0' + t.mm + ':' : t.mm + ':';
|
||||
t.ss = (t.ss < 10) ? '0' + t.ss : t.ss;
|
||||
timeInfo.textContent = t.hh + t.mm + t.ss;
|
||||
};
|
||||
|
||||
// UI handling end
|
||||
|
||||
// Video handling
|
||||
|
||||
proto.play = function () {
|
||||
this._player.play();
|
||||
this.setPlayButtonState('playing');
|
||||
};
|
||||
|
||||
proto.pause = function () {
|
||||
this._player.pause();
|
||||
this.setPlayButtonState('paused');
|
||||
};
|
||||
|
||||
proto._updateControlPanel = function () {
|
||||
var buf = this._player.getVideo().buffered;
|
||||
var current = this._player.getRoundedCurrentTime();
|
||||
|
||||
this.writeTimeInfo('elapsed', current);
|
||||
this.moveTimeBar('elapsed', current);
|
||||
|
||||
for (var bufEnd, bufStart, i = 0; i < buf.length; ++i ) {
|
||||
bufEnd = buf.end(i);
|
||||
bufStart = buf.start(i);
|
||||
if (bufStart <= current && current <= bufEnd) {
|
||||
this.moveTimeBar('buffered', bufEnd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
proto._startSeekOnKeyPress = function (dir) {
|
||||
if (this._seekOnKeyPressStartTime == null) { // Do not double start
|
||||
this._seekOnKeyPressStartTime = (new Date()).getTime();
|
||||
this._seekOnKeyPressDirection = dir;
|
||||
this._seekOnKeyPress();
|
||||
}
|
||||
};
|
||||
|
||||
proto._stopSeekOnKeyPress = function () {
|
||||
clearTimeout(this._seekOnKeyPressTimer);
|
||||
this._seekOnKeyPressDirection = null;
|
||||
this._seekOnKeyPressStartTime = null;
|
||||
this._seekOnKeyPressTimer = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is to handle this case that user seeks on video by long pressing key.
|
||||
* The seeking policy would go based on duration of pressing
|
||||
*/
|
||||
proto._seekOnKeyPress = function () {
|
||||
|
||||
if (this._seekOnKeyPressStartTime != null) {
|
||||
|
||||
var time = this._player.getRoundedCurrentTime();
|
||||
var factor = (this._seekOnKeyPressDirection == 'backward') ? -1 : 1;
|
||||
var seekDuration = (new Date()).getTime() - this._seekOnKeyPressStartTime;
|
||||
var seekStep;
|
||||
if (seekDuration <= this.SEEK_ON_LONG_KEY_PRESS_MS) {
|
||||
seekStep = this.SEEK_ON_KEY_PRESS_NORMAL_STEP_SEC;
|
||||
} else if (seekDuration > this.SEEK_ON_LONG_KEY_PRESS_MS &&
|
||||
seekDuration <= this.SEEK_ON_EXTRA_LONG_KEY_PRESS_MS) {
|
||||
seekStep = this.SEEK_ON_KEY_PRESS_LARGE_STEP_SEC;
|
||||
} else {
|
||||
seekStep = this.SEEK_ON_KEY_PRESS_EXTRA_LARGE_STEP_SEC;
|
||||
}
|
||||
|
||||
time += factor * seekStep;
|
||||
time = Math.min(Math.max(0, time), this._player.getRoundedDuration());
|
||||
|
||||
this.seek(time);
|
||||
|
||||
this._seekOnKeyPressTimer = setTimeout(
|
||||
this._seekOnKeyPress.bind(this),
|
||||
this.SEEK_ON_KEY_PRESS_INTERVAL_MS
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
proto.seek = function (sec, opt) {
|
||||
if (!(opt instanceof Object)) {
|
||||
opt = {
|
||||
autoPlayOnSeeked: true,
|
||||
autoHideControlPanel: true
|
||||
};
|
||||
}
|
||||
this._player.seek(sec);
|
||||
this.moveTimeBar('elapsed', sec);
|
||||
this.writeTimeInfo('elapsed', sec);
|
||||
this._autoPlayOnSeeked = !!opt.autoPlayOnSeeked;
|
||||
this.showControlPanel(!!opt.autoHideControlPanel);
|
||||
};
|
||||
|
||||
// Video handling end
|
||||
|
||||
// Event handling
|
||||
|
||||
proto.handleEvent = function handleVideoEvent(e) {
|
||||
|
||||
mDBG.log('FlingPlayer#handleEvent: e.type = ' + e.type);
|
||||
|
||||
var data = { 'time': this._player.getRoundedCurrentTime() };
|
||||
|
||||
switch (e.type) {
|
||||
|
||||
case 'loadedmetadata':
|
||||
this.writeTimeInfo('elapsed', this._player.getRoundedCurrentTime());
|
||||
this.writeTimeInfo('duration', this._player.getRoundedDuration());
|
||||
this._connector.reportStatus('loaded', data);
|
||||
break;
|
||||
|
||||
// XXX: Bug 1238862.
|
||||
// On real TV, decoder would provide NaN duration at the loadedmetadata
|
||||
// event, so duration of 00:00 would be displayed.
|
||||
// However, valid duration would be available at the durationchange
|
||||
// event afterwards.
|
||||
// So we listen to the durationchange and display valid duration info
|
||||
// to workaround this NaN duration issue,
|
||||
case 'durationchange':
|
||||
this.writeTimeInfo('duration', this._player.getRoundedDuration());
|
||||
break;
|
||||
|
||||
case 'timeupdate':
|
||||
this._updateControlPanel();
|
||||
break;
|
||||
|
||||
case 'waiting':
|
||||
this.showLoading(true);
|
||||
this._connector.reportStatus('buffering', data);
|
||||
break;
|
||||
|
||||
case 'playing':
|
||||
this.showLoading(false);
|
||||
this.hideControlPanel();
|
||||
this.toggleInitialMessage(false, { delayHiding: true });
|
||||
this._connector.reportStatus('buffered', data);
|
||||
this._connector.reportStatus('playing', data);
|
||||
break;
|
||||
|
||||
case 'seeked':
|
||||
if (this._autoPlayOnSeeked) {
|
||||
this.play();
|
||||
}
|
||||
this._connector.reportStatus('seeked', data);
|
||||
break;
|
||||
|
||||
case 'pause':
|
||||
this.showControlPanel();
|
||||
this._connector.reportStatus('stopped', data);
|
||||
break;
|
||||
|
||||
case 'ended':
|
||||
// Stop updating the control panel and prevent from showing loading UI.
|
||||
// Restore these actions once back to the very 1st frame.
|
||||
var handle = () => {
|
||||
this._player.getVideo().addEventListener('waiting', this);
|
||||
this._player.getVideo().addEventListener('timeupdate', this);
|
||||
this._player.getVideo().removeEventListener('seeked', handle);
|
||||
};
|
||||
this._player.getVideo().addEventListener('seeked', handle);
|
||||
this._player.getVideo().removeEventListener('waiting', this);
|
||||
this._player.getVideo().removeEventListener('timeupdate', this);
|
||||
// Go back to the very 1st frame on ended and reset the control panel
|
||||
this.resetUI();
|
||||
this.writeTimeInfo('duration', this._player.getRoundedDuration());
|
||||
this.seek(0, {
|
||||
autoPlayOnSeeked: false,
|
||||
autoHideControlPanel: false
|
||||
});
|
||||
this._connector.reportStatus('stopped', data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this.showLoading(false);
|
||||
data.error = e.target.error.code;
|
||||
this._connector.reportStatus('error', data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.onLoadRequest = function (e) {
|
||||
this.resetUI();
|
||||
this.showLoading(true);
|
||||
this._player.load(e.url);
|
||||
this.play();
|
||||
this._connector.getControllingDeviceInfo().then(info => {
|
||||
var msg = this._initialMsgSection.textContent;
|
||||
var name = 'device';
|
||||
if (info.displayName && typeof info.displayName == 'string') {
|
||||
name = info.displayName;
|
||||
}
|
||||
this._initialMsgSection.textContent =
|
||||
msg.replace('{{ device }}', name);
|
||||
|
||||
this.toggleInitialMessage(true);
|
||||
if (this._player.isPlaying()) {
|
||||
this.toggleInitialMessage(false, { delayHiding: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
proto.onPlayRequest = function () {
|
||||
this.play();
|
||||
};
|
||||
|
||||
proto.onPauseRequest = function () {
|
||||
this.pause();
|
||||
};
|
||||
|
||||
proto.onSeekRequest = function (e) {
|
||||
this.seek(e.time);
|
||||
};
|
||||
|
||||
proto.onKeyEnterDown = function () {
|
||||
|
||||
mDBG.log('FlingPlayer#onKeyEnterDown');
|
||||
|
||||
if (this.isControlPanelHiding()) {
|
||||
mDBG.log('The control panel is hiding so no action is taken.');
|
||||
return;
|
||||
}
|
||||
|
||||
var focused = this._keyNav.getFocusedElement();
|
||||
if (focused) {
|
||||
|
||||
mDBG.log('control focused = ', focused);
|
||||
|
||||
switch (focused.id) {
|
||||
|
||||
case uiID.backwardButton:
|
||||
case uiID.forwardButton:
|
||||
if (focused === this._backwardButton) {
|
||||
this._startSeekOnKeyPress('backward');
|
||||
} else {
|
||||
this._startSeekOnKeyPress('forward');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
proto.onKeyEnterUp = function () {
|
||||
mDBG.log('FlingPlayer#onKeyEnterUp');
|
||||
|
||||
if (this.isControlPanelHiding()) {
|
||||
mDBG.log('The control panel is hiding so no action is taken.');
|
||||
return;
|
||||
}
|
||||
|
||||
var focused = this._keyNav.getFocusedElement();
|
||||
if (focused) {
|
||||
|
||||
mDBG.log('control focused = ', focused);
|
||||
|
||||
switch (focused.id) {
|
||||
|
||||
case uiID.playButton:
|
||||
if (this._player.isPlaying()) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
break;
|
||||
|
||||
case uiID.backwardButton:
|
||||
case uiID.forwardButton:
|
||||
this._stopSeekOnKeyPress();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
proto.onBackKeyUp = function () {
|
||||
navigator.mozL10n.formatValue('want-to-end')
|
||||
.then((txt) => {
|
||||
var end = window.confirm(txt);
|
||||
if (end) {
|
||||
this._connector.reportStatus('stopped', {
|
||||
'time': this._player.getRoundedCurrentTime() });
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
proto.onDemandingControlPanel = function () {
|
||||
mDBG.log('FlingPlayer#onDemandingControlPanel');
|
||||
this.showControlPanel(true);
|
||||
};
|
||||
|
||||
// Event handling end
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
|
||||
window.fp = new FlingPlayer(
|
||||
new VideoPlayer($(uiID.player)),
|
||||
new Connector(navigator.presentation)
|
||||
);
|
||||
|
||||
window.fp.init();
|
||||
|
||||
if (document.visibilityState === 'hidden') {
|
||||
navigator.mozApps.getSelf().onsuccess = function(evt) {
|
||||
var app = evt.target.result;
|
||||
if (app) {
|
||||
app.launch();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
exports.FlingPlayer = FlingPlayer;
|
||||
|
||||
})(window);
|
||||
65
gaia/tv_apps/fling-player/js/master_debug.js
Executable file
65
gaia/tv_apps/fling-player/js/master_debug.js
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var proto;
|
||||
|
||||
/**
|
||||
* This is the local debug mode controller.
|
||||
* @class
|
||||
* @constructor
|
||||
*/
|
||||
function MasterDebug(dbg) {
|
||||
|
||||
/**
|
||||
* the local debug mode flag
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._flag_dbg = false;
|
||||
|
||||
this.setDBG(dbg);
|
||||
}
|
||||
|
||||
proto = MasterDebug.prototype;
|
||||
|
||||
proto.isDBG = function () {
|
||||
return this._flag_dbg;
|
||||
};
|
||||
|
||||
proto.setDBG = function (dbg) {
|
||||
this._flag_dbg = !!dbg;
|
||||
};
|
||||
|
||||
proto.log = function (var_args) {
|
||||
if (this.isDBG()) {
|
||||
return console.log.apply(console, Array.prototype.slice.call(arguments));
|
||||
}
|
||||
};
|
||||
|
||||
proto.warn = function (var_args) {
|
||||
if (this.isDBG()) {
|
||||
return console.warn.apply(console, Array.prototype.slice.call(arguments));
|
||||
}
|
||||
};
|
||||
|
||||
proto.error = function (var_args) {
|
||||
if (this.isDBG()) {
|
||||
return console.error.apply(
|
||||
console,
|
||||
Array.prototype.slice.call(arguments)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Run provided testing behavior in the debug mode
|
||||
* @param {function} behavior a callback executing testing behavior
|
||||
*/
|
||||
proto.test = function (behavior) {
|
||||
if (this.isDBG() && typeof behavior === 'function') {
|
||||
behavior();
|
||||
}
|
||||
};
|
||||
|
||||
window.mDBG = new MasterDebug(false);
|
||||
|
||||
})(window);
|
||||
125
gaia/tv_apps/fling-player/js/video_player.js
Executable file
125
gaia/tv_apps/fling-player/js/video_player.js
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This class is a thin wrapper of HTMLVideoElement
|
||||
* @param {HTMLVideoElement}
|
||||
*/
|
||||
function VideoPlayer(video) {
|
||||
this._video = video;
|
||||
}
|
||||
|
||||
var proto = VideoPlayer.prototype;
|
||||
|
||||
proto.init = function vp_init() {
|
||||
this._video.mozAudioChannelType = 'content';
|
||||
|
||||
this._isLoaded = false;
|
||||
|
||||
this._video.addEventListener('loadedmetadata', this);
|
||||
this._video.addEventListener('playing', this);
|
||||
};
|
||||
|
||||
proto.getVideo = function () {
|
||||
return this._video;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {Number} The rounded video duration in sec.
|
||||
* Please read the video's duration property if
|
||||
* require accurate to the decimal place.
|
||||
*/
|
||||
proto.getRoundedDuration = function () {
|
||||
var s = Math.round(this._video.duration);
|
||||
return isNaN(s) ? 0 : s;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {Number} The rounded current video time in sec.
|
||||
* Please read the video's currentTime property if
|
||||
* require accurate to the decimal place.
|
||||
*/
|
||||
proto.getRoundedCurrentTime = function () {
|
||||
var s = Math.round(this._video.currentTime);
|
||||
return isNaN(s) ? 0 : s;
|
||||
};
|
||||
|
||||
proto.show = function () {
|
||||
this._video.hidden = false;
|
||||
};
|
||||
|
||||
proto.hide = function () {
|
||||
this._video.hidden = true;
|
||||
};
|
||||
|
||||
proto.load = function (url) {
|
||||
this._video.src = url;
|
||||
this._video.load();
|
||||
};
|
||||
|
||||
proto.release = function () {
|
||||
this._video.pause();
|
||||
this._video.removeAttribute('src');
|
||||
this._video.load();
|
||||
};
|
||||
|
||||
proto.play = function () {
|
||||
this._video.play();
|
||||
};
|
||||
|
||||
proto.pause = function () {
|
||||
this._video.pause();
|
||||
};
|
||||
|
||||
proto.seek = function (t) {
|
||||
if (!this._isLoaded) {
|
||||
return;
|
||||
}
|
||||
this._video.currentTime = t;
|
||||
};
|
||||
|
||||
proto.isPlaying = function () {
|
||||
return !this._video.paused && !this._video.ended;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Number} sec The time length in sec
|
||||
* @return {Object} One object carrys the pared time value:
|
||||
* - hh : hour
|
||||
* - mm : minute
|
||||
* - ss : sec
|
||||
*/
|
||||
proto.parseTime = function (sec) {
|
||||
|
||||
var t = {},
|
||||
s = Math.round(sec);
|
||||
|
||||
if ((s > 0) === false) {
|
||||
s = 0;
|
||||
}
|
||||
|
||||
t.ss = s % 3600 % 60;
|
||||
s -= t.ss;
|
||||
t.mm = (s % 3600) / 60;
|
||||
s -= t.mm * 60;
|
||||
t.hh = s / 3600;
|
||||
|
||||
return t;
|
||||
};
|
||||
|
||||
proto.handleEvent = function (evt) {
|
||||
|
||||
switch(evt.type) {
|
||||
case 'loadedmetadata':
|
||||
this._isLoaded = true;
|
||||
break;
|
||||
|
||||
case 'playing':
|
||||
this.show();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
exports.VideoPlayer = VideoPlayer;
|
||||
|
||||
})(window);
|
||||
Loading…
Reference in a new issue