Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
600 lines
17 KiB
JavaScript
Executable file
600 lines
17 KiB
JavaScript
Executable file
/* 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);
|