Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
1475 lines
47 KiB
JavaScript
Executable file
1475 lines
47 KiB
JavaScript
Executable file
/* global VideoLoadingChecker,stopParsingMetadata,startParsingMetadata,
|
|
ThumbnailItem,ThumbnailList,ThumbnailDateGroup,initDB,
|
|
ForwardRewindController,ScreenLayout,processingQueue,VideoUtils,MediaUtils,
|
|
MozActivity,MediaDB,metadataQueue,processingQueue,LazyLoader,Dialogs,
|
|
captureFrame,VideoStats,noMoreWorkCallback:true,IntlHelper,Seeker,Captions */
|
|
/* exported resetCurrentVideo,updateLoadingSpinner,thumbnailClickHandler,
|
|
showThrobber,hideThrobber,$ */
|
|
'use strict';
|
|
|
|
// Layout Mode Transition:
|
|
// list <-> selection
|
|
// list <-> fullscreen player
|
|
// Note: the mode text is also used as css style.
|
|
const LAYOUT_MODE = {
|
|
list: 'layout-list',
|
|
selection: 'layout-selection',
|
|
fullscreenPlayer: 'layout-fullscreen-player'
|
|
};
|
|
|
|
var dom = {};
|
|
|
|
var ids = ['thumbnail-list-title', 'thumbnails', 'thumbnails-video-button',
|
|
'thumbnails-select-button', 'thumbnails-delete-button',
|
|
'thumbnails-share-button', 'thumbnails-select-top',
|
|
'thumbnails-number-selected', 'player-view', 'fullscreen-button',
|
|
'spinner-overlay', 'thumbnails-single-delete-button',
|
|
'thumbnails-single-share-button', 'thumbnails-single-info-button',
|
|
'info-view', 'info-close-button', 'player', 'overlay',
|
|
'overlay-title', 'overlay-text', 'overlay-menu',
|
|
'overlay-action-button', 'player-header', 'video-container',
|
|
'videoBar', 'videoControlBar', 'close', 'play',
|
|
'playHead', 'timeSlider', 'elapsedTime', 'video-title',
|
|
'duration-text', 'elapsed-text', 'bufferedTime', 'slider-wrapper',
|
|
'throbber', 'picker-close', 'picker-title', 'picker-header',
|
|
'picker-done', 'options', 'options-view',
|
|
'seek-backward', 'seek-forward', 'in-use-overlay',
|
|
'in-use-overlay-title', 'in-use-overlay-text'];
|
|
|
|
ids.forEach(function createElementRef(name) {
|
|
dom[toCamelCase(name)] = document.getElementById(name);
|
|
});
|
|
|
|
dom.player.mozAudioChannelType = 'content';
|
|
|
|
function $(id) { return document.getElementById(id); }
|
|
|
|
var seeker = new Seeker(dom.player);
|
|
var captions = new Captions(dom.player);
|
|
|
|
var playing = false;
|
|
|
|
// if this is true then the video tag is showing
|
|
// if false, then the gallery is showing
|
|
var playerShowing = false;
|
|
|
|
// same thing for the controls
|
|
var controlShowing = false;
|
|
var controlFadeTimeout = null;
|
|
|
|
// In thumbnails select view, we allow the user to select thumbnails.
|
|
// These variables hold the names of the selected files, and map those
|
|
// names to the corresponding File objects
|
|
var selectedFileNames = [];
|
|
var selectedFileNamesToBlobs = {};
|
|
|
|
var videodb;
|
|
var currentVideo; // The data for the currently playing video
|
|
var currentVideoBlob; // The blob for the currently playing video
|
|
var firstScanEnded = false;
|
|
|
|
var THUMBNAIL_WIDTH;
|
|
var THUMBNAIL_HEIGHT;
|
|
|
|
// Enumerating the readyState for html5 video api
|
|
var HAVE_NOTHING = 0;
|
|
|
|
var storageState;
|
|
var currentOverlay;
|
|
|
|
var dragging = false;
|
|
// touch start id is the identifier of touch event. we only need to process
|
|
// events related to this id.
|
|
var touchStartID = null;
|
|
var sliderRect;
|
|
var thumbnailList;
|
|
|
|
var pendingPick;
|
|
|
|
// Before launching a share activity we may need to release the video hardware
|
|
// If so we need to remember the playback time so we can resume at the
|
|
// right time. See releaseVideo() and restoreVideo().
|
|
var videoHardwareReleased = false;
|
|
var restoreTime = null;
|
|
|
|
var isPhone;
|
|
var isPortrait;
|
|
var currentLayoutMode;
|
|
|
|
// When user entering fullscreen mode and rotate the screen, the text overflow
|
|
// calculation of video title should not be invoked. We pend this request when
|
|
// leaving fullscreen mode.
|
|
var pendingUpdateTitleText = false;
|
|
|
|
// Videos recorded by our own camera have filenames of this form
|
|
var FROMCAMERA = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.(?:3gp|mp4|webm)$/;
|
|
|
|
// We have a single instance of the loading checker because it is used
|
|
// across functions
|
|
var loadingChecker =
|
|
new VideoLoadingChecker(dom.player, dom.inUseOverlay, dom.inUseOverlayTitle,
|
|
dom.inUseOverlayText);
|
|
|
|
var lastPlayTime = 0;
|
|
|
|
init();
|
|
|
|
// Workaround to fix issue reported in Bug 1214157
|
|
// We would like to pause video playback when interrupted by competing audio.
|
|
// Audio Channel API throws interruptbegin event after play event (Bug 1192748)
|
|
// If performance.now() - lastPlayTime is small number we ignore interruptbegin
|
|
// and pause video playback only when interrupted by competing audio.
|
|
// This workaround should be removed with the fix of Bug 1206581
|
|
dom.player.addEventListener('play', function() {
|
|
lastPlayTime = window.performance.now();
|
|
});
|
|
|
|
dom.player.addEventListener('mozinterruptbegin', function() {
|
|
// Ignore mozinterruptbegin event if it immediately follows play event
|
|
if ((window.performance.now() - lastPlayTime) > 50) {
|
|
// Pause video when interrupted by competing audio.
|
|
pause();
|
|
lastPlayTime = 0;
|
|
}
|
|
});
|
|
|
|
// Pause on visibility change
|
|
document.addEventListener('visibilitychange', function visibilityChange() {
|
|
if (document.hidden) {
|
|
stopParsingMetadata();
|
|
if (playing) {
|
|
pause();
|
|
}
|
|
}
|
|
else {
|
|
if (playerShowing) {
|
|
// Bug 1151775
|
|
// If the app is coming to the foreground before a sharing activity is
|
|
// complete, restore the video (the video will have been released before
|
|
// initiating the sharing activity and is only restored when the sharing
|
|
// activity is completed). When the sharing activity is complete and
|
|
// restoreVideo is again called, the restoreVideo function takes care of
|
|
// not restoring the video when it doesn't need to be restored.
|
|
if (videoHardwareReleased) {
|
|
restoreVideo();
|
|
}
|
|
setControlsVisibility(true);
|
|
} else {
|
|
// We only start parsing metadata when player is not shown.
|
|
startParsingMetadata();
|
|
}
|
|
}
|
|
});
|
|
|
|
// We get headphoneschange event when the headphones is plugged or unplugged
|
|
var acm = navigator.mozAudioChannelManager;
|
|
if (acm) {
|
|
acm.addEventListener('headphoneschange', function onheadphoneschange() {
|
|
if (!acm.headphones && playing) {
|
|
setVideoPlaying(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
navigator.mozSetMessageHandler('activity', handleActivityEvents);
|
|
|
|
function init() {
|
|
|
|
// Tell performance monitors that our chrome is visible
|
|
window.performance.mark('navigationLoaded');
|
|
|
|
// Reformatting eventlistener is handled by ThumbnailList
|
|
IntlHelper.define('date-group', 'datetime', {
|
|
month: 'long',
|
|
year: 'numeric',
|
|
});
|
|
|
|
thumbnailList = new ThumbnailList(ThumbnailDateGroup, dom.thumbnails);
|
|
ThumbnailItem.titleMaxLines = isPhone ? 2 : (isPortrait ? 4 : 2);
|
|
|
|
// Initializing the database does not need to wait for l10n to be ready.
|
|
// Also, db initialization should start as soon as possible to reduce
|
|
// the time until the app is 'visuallyLoaded'.
|
|
initDB();
|
|
|
|
addUIEventListeners();
|
|
initLayout();
|
|
initThumbnailSize();
|
|
|
|
function addUIEventListeners() {
|
|
// if video is not start with activity mode, we need to wire all event
|
|
// handlers.
|
|
if (!navigator.mozHasPendingMessage('activity')) {
|
|
// options button only needed by normal mode. if we are under pick
|
|
// activity, there is only pick view enabled that is enabled at
|
|
// showPickView();
|
|
initOptionsButtons();
|
|
}
|
|
|
|
// Ensure code that accesses html dir is executed after
|
|
// `documentElement.dir` is set.
|
|
document.l10n.ready.then(function() {
|
|
initPlayerControls();
|
|
});
|
|
|
|
ForwardRewindController.init(dom.player, dom.seekForward, dom.seekBackward,
|
|
seeker);
|
|
|
|
// the overlay action button may be used in both normal mode and activity
|
|
// mode, we need to wire its event handler here.
|
|
dom.overlayActionButton.addEventListener('click', function() {
|
|
if (pendingPick) {
|
|
cancelPick();
|
|
} else if (currentOverlay === 'empty') {
|
|
launchCameraApp();
|
|
}
|
|
});
|
|
|
|
// Tell performance monitors that our chrome is ready to interact with.
|
|
window.performance.mark('navigationInteractive');
|
|
}
|
|
}
|
|
|
|
function initThumbnailSize() {
|
|
// use devicePixelRatio as the scale ratio for thumbnail creation.
|
|
if (isPhone) {
|
|
THUMBNAIL_WIDTH = 210 * window.devicePixelRatio;
|
|
THUMBNAIL_HEIGHT = 120 * window.devicePixelRatio;
|
|
} else {
|
|
var shortEdge = Math.min(window.innerWidth, window.innerHeight);
|
|
THUMBNAIL_WIDTH = 424 * window.devicePixelRatio * shortEdge / 800;
|
|
THUMBNAIL_HEIGHT = Math.round(THUMBNAIL_WIDTH * 4 / 7);
|
|
}
|
|
}
|
|
|
|
function initLayout() {
|
|
ScreenLayout.watch('portrait', '(orientation: portrait)');
|
|
isPhone = ScreenLayout.getCurrentLayout('tiny');
|
|
isPortrait = ScreenLayout.getCurrentLayout('portrait');
|
|
|
|
// We need to disable video playing and show a loading spinner when the first
|
|
// startup under tablet and landscape mode.
|
|
if (isPhone || isPortrait) {
|
|
dom.spinnerOverlay.classList.add('hidden');
|
|
setDisabled(dom.playerView, false);
|
|
} else {
|
|
dom.spinnerOverlay.classList.remove('hidden');
|
|
setDisabled(dom.playerView, true);
|
|
}
|
|
|
|
// We handle the isPortrait calculation here, because window dispatches
|
|
// multiple resize event with different width and height in Firefox nightly.
|
|
window.addEventListener('screenlayoutchange', handleScreenLayoutChange);
|
|
|
|
switchLayout(LAYOUT_MODE.list);
|
|
}
|
|
|
|
function initPlayerControls() {
|
|
// slider dragging
|
|
dom.sliderWrapper.addEventListener('touchstart', handleSliderTouchStart);
|
|
// handles slider dragging
|
|
dom.sliderWrapper.addEventListener('touchmove', handleSliderTouchMove);
|
|
dom.sliderWrapper.addEventListener('touchend', handleSliderTouchEnd);
|
|
|
|
// handle video player
|
|
dom.player.addEventListener('timeupdate', timeUpdated);
|
|
dom.player.addEventListener('seeked', updateVideoControlSlider);
|
|
dom.player.addEventListener('ended', playerEnded);
|
|
|
|
// handle user tapping events
|
|
dom.play.addEventListener('click', handlePlayButtonClick);
|
|
dom.playerHeader.addEventListener('action', handleCloseButtonClick);
|
|
dom.pickerDone.addEventListener('click', postPickResult);
|
|
dom.options.addEventListener('click', showOptionsView);
|
|
dom.videoContainer.addEventListener('click', toggleVideoControls);
|
|
|
|
// handle slider keypress, emitted by the screen reader
|
|
dom.timeSlider.addEventListener('keypress', handleSliderKeypress);
|
|
}
|
|
|
|
function initOptionsButtons() {
|
|
// button to switch to camera
|
|
dom.thumbnailsVideoButton.addEventListener('click', launchCameraApp);
|
|
// buttons for entering/exiting selection mode
|
|
dom.thumbnailsSelectButton.addEventListener('click', showSelectView);
|
|
dom.thumbnailsSelectTop.addEventListener('action', hideSelectView);
|
|
// action buttons for selection mode
|
|
dom.thumbnailsDeleteButton.addEventListener('click', deleteSelectedItems);
|
|
dom.thumbnailsShareButton.addEventListener('click', shareSelectedItems);
|
|
|
|
// info buttons
|
|
dom.infoCloseButton.addEventListener('click', hideInfoView);
|
|
// option button cancel
|
|
dom.optionsView.addEventListener('gaiamenu-cancel', hideOptionsView);
|
|
// fullscreen player
|
|
dom.fullscreenButton.addEventListener('click', toggleFullscreenPlayer);
|
|
// fullscreen toolbar
|
|
addEventListeners('.single-delete-button', 'click', deleteCurrentVideo);
|
|
addEventListeners('.single-share-button', 'click', shareCurrentVideo);
|
|
addEventListeners('.single-info-button', 'click', showInfoView);
|
|
}
|
|
|
|
function addEventListeners(selector, type, listener) {
|
|
var elements = document.body.querySelectorAll(selector);
|
|
for (var i = 0; i < elements.length; i++) {
|
|
elements[i].addEventListener(type, listener);
|
|
}
|
|
}
|
|
|
|
function toggleFullscreenPlayer() {
|
|
if (currentLayoutMode === LAYOUT_MODE.list) {
|
|
switchLayout(LAYOUT_MODE.fullscreenPlayer);
|
|
scheduleVideoControlsAutoHiding();
|
|
} else {
|
|
switchLayout(LAYOUT_MODE.list);
|
|
}
|
|
|
|
VideoUtils.fitContainer(dom.videoContainer, dom.player,
|
|
currentVideo.metadata.rotation || 0);
|
|
}
|
|
|
|
function toggleVideoControls(e) {
|
|
// When we change the visibility state of video controls, we need to check the
|
|
// timeout of auto hiding.
|
|
if (controlFadeTimeout) {
|
|
clearTimeout(controlFadeTimeout);
|
|
controlFadeTimeout = null;
|
|
}
|
|
// We cannot change the visibility state of video contorls when we are in
|
|
// picking mode.
|
|
if (!pendingPick) {
|
|
e.cancelBubble = !controlShowing;
|
|
setControlsVisibility(!controlShowing);
|
|
}
|
|
}
|
|
|
|
function handleScreenLayoutChange() {
|
|
// When resizing, we need to check the orientation change.
|
|
isPortrait = ScreenLayout.getCurrentLayout('portrait');
|
|
|
|
if (!isPhone) {
|
|
// In tablet, the landscape mode will show player and list at the same time.
|
|
// To keep only one hardware codec be used, we shows loading icon at the
|
|
// player view before first batch of files scanned and all their metadata
|
|
// are parsed.
|
|
if (!isPortrait && (!firstScanEnded || processingQueue)) {
|
|
// landscape mode and everything is waiting.
|
|
dom.spinnerOverlay.classList.remove('hidden');
|
|
setDisabled(dom.playerView, true);
|
|
} else {
|
|
dom.spinnerOverlay.classList.add('hidden');
|
|
setDisabled(dom.playerView, false);
|
|
}
|
|
// We need to hide player when rotating to portrait which release video
|
|
// element and load the video into player when rotating to landscape.
|
|
if (currentLayoutMode === LAYOUT_MODE.list) {
|
|
if (isPortrait) {
|
|
hidePlayer(true);
|
|
} else {
|
|
showPlayer(currentVideo, false, /* autoPlay */
|
|
false, /* enterFullscreen */
|
|
true); /* keepControls */
|
|
}
|
|
}
|
|
// the maximum lines of title field is different in portrait or landscape
|
|
// mode.
|
|
ThumbnailItem.titleMaxLines = isPortrait ? 4 : 2;
|
|
}
|
|
|
|
// When layout mode is list or selection mode, we need to update all title
|
|
// text to have correct ellipsis position. Otherwise, we update them when
|
|
// leaving fullscreen mode.
|
|
if (currentLayoutMode !== LAYOUT_MODE.fullscreenPlayer) {
|
|
// XXX: to workaround bug 961636 which fires two resize event at the app
|
|
// start-up,we need to check the existence of thumbnailList before using
|
|
// it.
|
|
if (!thumbnailList) {
|
|
return;
|
|
}
|
|
thumbnailList.updateAllThumbnailTitles();
|
|
} else {
|
|
pendingUpdateTitleText = true;
|
|
}
|
|
|
|
// Rescale when window size changes. This should get called when
|
|
// screen orientation changes
|
|
if (dom.player.readyState !== HAVE_NOTHING) {
|
|
VideoUtils.fitContainer(dom.videoContainer, dom.player,
|
|
currentVideo.metadata.rotation || 0);
|
|
}
|
|
}
|
|
|
|
function switchLayout(mode) {
|
|
var oldMode = currentLayoutMode;
|
|
if (oldMode) {
|
|
document.body.classList.remove(currentLayoutMode);
|
|
}
|
|
currentLayoutMode = mode;
|
|
document.body.classList.add(currentLayoutMode);
|
|
|
|
// Update title text when leaving fullscreen mode with pending task.
|
|
if (oldMode === LAYOUT_MODE.fullscreenPlayer && pendingUpdateTitleText) {
|
|
pendingUpdateTitleText = false;
|
|
thumbnailList.updateAllThumbnailTitles();
|
|
}
|
|
}
|
|
|
|
function handleActivityEvents(a) {
|
|
var activityName = a.source.name;
|
|
|
|
if (activityName === 'pick') {
|
|
pendingPick = a;
|
|
|
|
showPickView();
|
|
}
|
|
}
|
|
|
|
function showInfoView() {
|
|
hideOptionsView();
|
|
//Get the length of the playing video
|
|
var length = isFinite(currentVideo.metadata.duration) ?
|
|
MediaUtils.formatDuration(currentVideo.metadata.duration) : '';
|
|
//Check if video type has prefix 'video/' e.g. video/mp4
|
|
var type = currentVideo.type;
|
|
if (type) {
|
|
var index = currentVideo.type.indexOf('/');
|
|
type = index > -1 ?
|
|
currentVideo.type.slice(index + 1) : currentVideo.type;
|
|
}
|
|
//Get the resolution of the playing video
|
|
var resolution = (currentVideo.metadata.width &&
|
|
currentVideo.metadata.height) ? currentVideo.metadata.width + 'x' +
|
|
currentVideo.metadata.height : '';
|
|
//Get the video size
|
|
return MediaUtils.getLocalizedSizeTokens(currentVideo.size).then((args) => {
|
|
//Create data object to fill in the fields of info overlay view
|
|
var data = {
|
|
'info-name': {raw: currentVideo.metadata.title},
|
|
'info-length': {raw: length},
|
|
'info-size': {
|
|
id: 'fileSize',
|
|
args: args
|
|
},
|
|
'info-type': {raw: type},
|
|
'info-date': {raw: MediaUtils.formatDate(currentVideo.date)},
|
|
'info-resolution': {raw: resolution}
|
|
};
|
|
|
|
//Populate info overlay view
|
|
MediaUtils.populateMediaInfo(data);
|
|
// We need to disable NFC sharing when showing info view
|
|
setNFCSharing(false);
|
|
//Show the video info view
|
|
dom.infoView.classList.remove('hidden');
|
|
document.body.classList.add('info-view');
|
|
});
|
|
}
|
|
|
|
function hideInfoView() {
|
|
// Enable NFC sharing when user hides info and returns to fullscreen mode
|
|
setNFCSharing(true);
|
|
dom.infoView.classList.add('hidden');
|
|
document.body.classList.remove('info-view');
|
|
}
|
|
|
|
function showSelectView() {
|
|
// In tablet landscape mode, the video player may be playing. When entering
|
|
// selection mode, we need to hide player.
|
|
hidePlayer(true, function() {
|
|
switchLayout(LAYOUT_MODE.selection);
|
|
|
|
// styling for select view
|
|
thumbnailList.setSelectMode(true);
|
|
|
|
clearSelection();
|
|
});
|
|
}
|
|
|
|
function hideSelectView() {
|
|
clearSelection();
|
|
thumbnailList.setSelectMode(false);
|
|
switchLayout(LAYOUT_MODE.list);
|
|
if (!isPhone && !isPortrait && currentVideo) {
|
|
// We need to load the video while restoring to list mode
|
|
showPlayer(currentVideo, false, /* autoPlay */
|
|
false, /* enterFullscreen */
|
|
true); /* keepControls */
|
|
}
|
|
}
|
|
|
|
function showOptionsView() {
|
|
// If the user is about to share a video we should stop playing it because
|
|
// sometimes we won't go to the background when the activity starts and
|
|
// we keep playing. This will cause problems if the receiving app also tries
|
|
// to play it. Similarly, if the user is going to delete the video there is
|
|
// no point in continuing to play it. And if they care enough about it to
|
|
// look for more info about it, they probably don't want to miss anything.
|
|
if (playing) {
|
|
pause();
|
|
}
|
|
dom.optionsView.removeAttribute('hidden');
|
|
document.body.classList.add('options-view');
|
|
}
|
|
|
|
function hideOptionsView() {
|
|
dom.optionsView.setAttribute('hidden', true);
|
|
document.body.classList.remove('options-view');
|
|
}
|
|
|
|
function setDisabled(element, disabled) {
|
|
element.classList.toggle('disabled', disabled);
|
|
|
|
// Set ARIA disabled attribute to maintain semantic meaning for the assistive
|
|
// technologies like screen reader.
|
|
element.setAttribute('aria-disabled', disabled);
|
|
}
|
|
|
|
function setSelected(element, selected) {
|
|
element.classList.toggle('selected', selected);
|
|
|
|
// Set ARIA selected attribute to maintain semantic meaning for the assistive
|
|
// technologies like screen reader.
|
|
element.setAttribute('aria-selected', selected);
|
|
}
|
|
|
|
function clearSelection() {
|
|
// Clear the selection, if there is one
|
|
Array.forEach(selectedFileNames, function(name) {
|
|
setSelected(thumbnailList.thumbnailMap[name].htmlNode, false);
|
|
});
|
|
selectedFileNames = [];
|
|
selectedFileNamesToBlobs = {};
|
|
setDisabled(dom.thumbnailsDeleteButton, true);
|
|
setDisabled(dom.thumbnailsShareButton, true);
|
|
document.l10n.setAttributes(
|
|
dom.thumbnailsNumberSelected,
|
|
'number-selected2',
|
|
{ n: 0 });
|
|
}
|
|
|
|
// When we enter thumbnail selection mode, or when the selection changes
|
|
// we call this function to update the message the top of the screen and to
|
|
// enable or disable the Delete and Share buttons
|
|
function updateSelection(videodata) {
|
|
var thumbnail = thumbnailList.thumbnailMap[videodata.name];
|
|
|
|
var selected = !thumbnail.htmlNode.classList.contains('selected');
|
|
// First, update the visual appearance of the element
|
|
setSelected(thumbnail.htmlNode, selected);
|
|
|
|
// Now update the list of selected filenames and filename->blob map
|
|
// based on whether we selected or deselected the thumbnail
|
|
var filename = videodata.name;
|
|
if (selected) {
|
|
selectedFileNames.push(filename);
|
|
videodb.getFile(filename, function(blob) {
|
|
selectedFileNamesToBlobs[filename] = blob;
|
|
});
|
|
}
|
|
else {
|
|
delete selectedFileNamesToBlobs[filename];
|
|
var i = selectedFileNames.indexOf(filename);
|
|
if (i !== -1) {
|
|
selectedFileNames.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// Now update the UI based on the number of selected thumbnails
|
|
var numSelected = selectedFileNames.length;
|
|
document.l10n.setAttributes(
|
|
dom.thumbnailsNumberSelected,
|
|
'number-selected2',
|
|
{ n: numSelected });
|
|
|
|
var noneSelected = numSelected === 0;
|
|
setDisabled(dom.thumbnailsDeleteButton, noneSelected);
|
|
setDisabled(dom.thumbnailsShareButton, noneSelected);
|
|
}
|
|
|
|
function launchCameraApp() {
|
|
var a = new MozActivity({
|
|
name: 'record',
|
|
data: {
|
|
type: 'videos'
|
|
}
|
|
});
|
|
|
|
a.onerror = function() {
|
|
if (a.error.name === 'NO_PROVIDER') {
|
|
document.l10n.formatValue('share-noprovider').then((msg) => {
|
|
alert(msg);
|
|
});
|
|
} else {
|
|
console.warn('share activity error:', a.error.name);
|
|
}
|
|
};
|
|
}
|
|
|
|
// We need to call resetCurrentVideo before deleting a video. The variable
|
|
// currentVideo is used as the last or current playing video. We need to change
|
|
// the current video to preview one, next one or null.
|
|
function resetCurrentVideo() {
|
|
// no currentVideo means there is no video in the list, we don't need to focus
|
|
// any one.
|
|
if (!currentVideo) {
|
|
return;
|
|
}
|
|
var currentThumbnail = thumbnailList.thumbnailMap[currentVideo.name];
|
|
currentThumbnail.htmlNode.classList.remove('focused');
|
|
var nextThumbnail = thumbnailList.findNextThumbnail(currentVideo.name);
|
|
|
|
if (nextThumbnail) {
|
|
// set focused to current thumbnail and update current video
|
|
currentVideo = nextThumbnail.data;
|
|
nextThumbnail.htmlNode.classList.add('focused');
|
|
} else {
|
|
// no more videos and a dialog will be shown when we delete the video.
|
|
currentVideo = null;
|
|
}
|
|
}
|
|
|
|
function deleteSelectedItems() {
|
|
if (selectedFileNames.length === 0) {
|
|
return;
|
|
}
|
|
LazyLoader.load('shared/style/confirm.css', function() {
|
|
document.body.classList.add('confirm-dialog');
|
|
Dialogs.confirm({
|
|
messageId: 'delete-n-items?',
|
|
messageArgs: {n: selectedFileNames.length},
|
|
cancelId: 'cancel',
|
|
confirmId: 'delete',
|
|
danger: true
|
|
}, function() { // onSuccess
|
|
// deleteFile is O(n), so this loop is O(n*n). If used with really large
|
|
// selections, it might have noticably bad performance. If so, we
|
|
// can write a more efficient deleteFiles() function.
|
|
for (var i = 0; i < selectedFileNames.length; i++) {
|
|
deleteFile(selectedFileNames[i]);
|
|
}
|
|
clearSelection();
|
|
document.body.classList.remove('confirm-dialog');
|
|
}, function() { // onCancel
|
|
document.body.classList.remove('confirm-dialog');
|
|
});
|
|
});
|
|
}
|
|
|
|
function deleteFile(filename) {
|
|
// Delete the file from the MediaDB. This removes the db entry and
|
|
// deletes the file in device storage. This will generate an change
|
|
// event which will call imageDeleted()
|
|
|
|
if (FROMCAMERA.test(filename)) {
|
|
// If we're deleting a video file recorded by our camera,
|
|
// we also need to delete the poster image associated with
|
|
// that video.
|
|
var postername = filename.replace(/\.(?:3gp|mp4|webm)$/, '.jpg');
|
|
navigator.getDeviceStorage('pictures'). delete(postername);
|
|
}
|
|
|
|
// Whether or not there was a poster file to delete, delete the
|
|
// actual video file. This will cause the MediaDB to send a 'deleted'
|
|
// event, and the handler for that event will call videoDeleted() below.
|
|
videodb.deleteFile(filename);
|
|
}
|
|
|
|
// Clicking on the share button in select mode shares all selected images
|
|
function shareSelectedItems() {
|
|
var blobs = selectedFileNames.map(function(name) {
|
|
return selectedFileNamesToBlobs[name];
|
|
});
|
|
share(blobs);
|
|
}
|
|
|
|
// function from gallery/js/gallery.js
|
|
function share(blobs) {
|
|
if (blobs.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var names = [], fullpaths = [];
|
|
|
|
// Get the file name (minus path) and type of each blob
|
|
blobs.forEach(function(blob) {
|
|
// Discard the path, we just want the base name
|
|
var name = blob.name;
|
|
// We try to fix Bug 814323 by using
|
|
// current workaround of bluetooth transfer
|
|
// so we will pass both filenames and fullpaths
|
|
// The fullpaths can be removed after Bug 811615 is fixed
|
|
fullpaths.push(name);
|
|
name = name.substring(name.lastIndexOf('/') + 1);
|
|
names.push(name);
|
|
});
|
|
|
|
if (playerShowing) {
|
|
releaseVideo();
|
|
}
|
|
|
|
var a = new MozActivity({
|
|
name: 'share',
|
|
data: {
|
|
type: 'video/*', // Video app only supports video types (bug 1069885)
|
|
number: blobs.length,
|
|
blobs: blobs,
|
|
filenames: names,
|
|
filepaths: fullpaths
|
|
}
|
|
});
|
|
|
|
a.onsuccess = restoreVideo;
|
|
|
|
a.onerror = function() {
|
|
if (a.error.name === 'NO_PROVIDER') {
|
|
document.l10n.formatValue('share-noprovider').then((msg) => {
|
|
alert(msg);
|
|
});
|
|
} else {
|
|
console.warn('share activity error:', a.error.name);
|
|
}
|
|
restoreVideo();
|
|
};
|
|
}
|
|
|
|
function updateDialog() {
|
|
if (thumbnailList.count !== 0 && (!storageState || playerShowing)) {
|
|
showOverlay(null);
|
|
} else if (storageState === MediaDB.UPGRADING) {
|
|
showOverlay('upgrade');
|
|
} else if (storageState === MediaDB.NOCARD) {
|
|
showOverlay('nocard');
|
|
} else if (storageState === MediaDB.UNMOUNTED) {
|
|
showOverlay('pluggedin');
|
|
} else if (firstScanEnded &&
|
|
thumbnailList.count === 0 &&
|
|
metadataQueue.length === 0) {
|
|
showOverlay('empty');
|
|
}
|
|
}
|
|
|
|
function updateLoadingSpinner() {
|
|
// We show a loading spinner in tablet and landscape mode while first
|
|
// scanning of mediadb. When mediadb scanned and metadata parser parsed, we
|
|
// hide the loading spinner.
|
|
if (processingQueue) {
|
|
noMoreWorkCallback = updateLoadingSpinner;
|
|
} else {
|
|
window.performance.mark('scanEnd');
|
|
dom.spinnerOverlay.classList.add('hidden');
|
|
setDisabled(dom.playerView, false);
|
|
if (thumbnailList.count) {
|
|
// Initialize currentVideo to first video item if it doesn't have a value.
|
|
currentVideo = currentVideo ||
|
|
thumbnailList.itemGroups[0].thumbnails[0].data;
|
|
// Load current video to player when we are in tablet and landscape.
|
|
if (!isPhone && !isPortrait) {
|
|
showPlayer(currentVideo, false, /* autoPlay */
|
|
false, /* enterFullscreen */
|
|
true); /* keepControls */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function thumbnailClickHandler(videodata) {
|
|
if (!isPhone && !isPortrait) {
|
|
// if the screen is large and landscape, we need to lock the operations
|
|
// while scanning and metadata parsing.
|
|
if (!firstScanEnded || processingQueue) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (currentLayoutMode === LAYOUT_MODE.list) {
|
|
|
|
// The player is shown at landscape mode of tablet. We need to save the
|
|
// playing state before change to another video. Calling hidePlayer(true)
|
|
// can stop the video and save the state and hide everything.
|
|
// Calling hidePlayer(true) also releases hardware codec. And metadata
|
|
// parser can parse at least one video file.
|
|
hidePlayer(true, function() {
|
|
// Be certain that metadata parsing has stopped before we show the
|
|
// video player. Otherwise, we'll have contention for the video hardware
|
|
stopParsingMetadata(function() {
|
|
var fullscreen = pendingPick || isPhone || isPortrait;
|
|
showPlayer(videodata, !pendingPick, fullscreen, pendingPick);
|
|
});
|
|
});
|
|
}
|
|
else if (currentLayoutMode === LAYOUT_MODE.selection) {
|
|
updateSelection(videodata);
|
|
}
|
|
}
|
|
|
|
function showOverlay(id) {
|
|
LazyLoader.load('shared/style/confirm.css', function() {
|
|
currentOverlay = id;
|
|
|
|
if (id === null) {
|
|
document.body.classList.remove('overlay');
|
|
dom.overlay.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
var text, title;
|
|
if (pendingPick || id === 'empty') {
|
|
dom.overlayMenu.classList.remove('hidden');
|
|
dom.overlayActionButton.classList.remove('hidden');
|
|
dom.overlayActionButton.setAttribute('data-l10n-id',
|
|
pendingPick ?
|
|
'overlay-cancel-button' :
|
|
'overlay-camera-button');
|
|
} else {
|
|
dom.overlayMenu.classList.add('hidden');
|
|
dom.overlayActionButton.classList.add('hidden');
|
|
}
|
|
|
|
if (id === 'nocard') {
|
|
title = 'nocard2-title';
|
|
text = 'nocard3-text';
|
|
} else {
|
|
title = id + '-title';
|
|
text = id + '-text';
|
|
}
|
|
|
|
dom.overlayTitle.setAttribute('data-l10n-id', title);
|
|
dom.overlayText.setAttribute('data-l10n-id', text);
|
|
dom.overlay.classList.remove('hidden');
|
|
document.body.classList.add('overlay');
|
|
});
|
|
}
|
|
|
|
function setControlsVisibility(visible) {
|
|
// in tablet landscape mode, we always shows controls in list layout. We
|
|
// don't need to hide it.
|
|
if (isPhone || isPortrait ||
|
|
currentLayoutMode !== LAYOUT_MODE.list) {
|
|
|
|
dom.playerView.classList[visible ? 'remove' : 'add'](
|
|
'video-controls-hidden');
|
|
controlShowing = visible;
|
|
} else {
|
|
// always set it as shown.
|
|
controlShowing = true;
|
|
}
|
|
|
|
// Set the proper accessibility label for the video container based on
|
|
// controls showing.
|
|
dom.videoContainer.setAttribute('data-l10n-id', controlShowing ?
|
|
'hide-controls-button' : 'show-controls-button');
|
|
|
|
// to sync the slider under the case of auto-pause(unplugging headset), we
|
|
// need to update the slider when controls is visible.
|
|
if (controlShowing) {
|
|
updateVideoControlSlider();
|
|
}
|
|
}
|
|
|
|
function movePlayHead(percent) {
|
|
if (document.documentElement.dir === 'ltr') {
|
|
dom.playHead.style.left = percent;
|
|
}
|
|
else {
|
|
dom.playHead.style.right = percent;
|
|
}
|
|
}
|
|
|
|
function updateVideoControlSlider() {
|
|
var percent = (dom.player.currentTime / dom.player.duration) * 100;
|
|
if (isNaN(percent)) {
|
|
return;
|
|
}
|
|
|
|
percent += '%';
|
|
|
|
dom.elapsedText.textContent =
|
|
MediaUtils.formatDuration(dom.player.currentTime);
|
|
dom.elapsedTime.style.width = percent;
|
|
// Don't move the play head if the user is dragging it.
|
|
if (!dragging) {
|
|
movePlayHead(percent);
|
|
}
|
|
}
|
|
|
|
function setVideoPlaying(playing) {
|
|
if (playing) {
|
|
play();
|
|
} else {
|
|
pause();
|
|
}
|
|
}
|
|
|
|
function deleteCurrentVideo() {
|
|
hideOptionsView();
|
|
// We need to disable NFC sharing when showing delete confirmation dialog
|
|
setNFCSharing(false);
|
|
|
|
LazyLoader.load('shared/style/confirm.css', function() {
|
|
document.body.classList.add('confirm-dialog');
|
|
// If we're deleting the file shown in the player we've got to
|
|
// return to the thumbnail list. We pass false to hidePlayer() to tell it
|
|
// not to record new metadata for the file we're about to delete.
|
|
Dialogs.confirm({
|
|
messageId: 'delete-video?',
|
|
cancelId: 'cancel',
|
|
confirmId: 'delete',
|
|
danger: true
|
|
}, function _onSuccess() { // onSuccess
|
|
deleteFile(currentVideo.name);
|
|
if (!isPhone && !isPortrait) {
|
|
// If the file is deleted, we need to load another video file. This is
|
|
// only required at tablet and landscape mode. When there is no video in
|
|
// video app, the currentVideo is null and the overlay is shown.
|
|
if (currentVideo) {
|
|
showPlayer(currentVideo, false, true, true);
|
|
}
|
|
} else {
|
|
hidePlayer(false);
|
|
}
|
|
document.body.classList.remove('confirm-dialog');
|
|
}, function _onError() {
|
|
// Enable NFC sharing when cancels delete and returns to fullscreen mode
|
|
setNFCSharing(true);
|
|
document.body.classList.remove('confirm-dialog');
|
|
});
|
|
});
|
|
}
|
|
|
|
function handlePlayButtonClick() {
|
|
setVideoPlaying(dom.player.paused);
|
|
}
|
|
|
|
function handleCloseButtonClick() {
|
|
if (isPhone || isPortrait) {
|
|
hidePlayer(true);
|
|
} else {
|
|
// In tablet and landscape mode, close is view as a button to go back to
|
|
// player mode. And close button is only shown at fullscreen mode when it
|
|
// is tablet and landscape.
|
|
toggleFullscreenPlayer();
|
|
}
|
|
}
|
|
|
|
function postPickResult() {
|
|
pendingPick.postResult({
|
|
type: currentVideoBlob.type,
|
|
blob: currentVideoBlob
|
|
});
|
|
cleanupPick();
|
|
}
|
|
|
|
function shareCurrentVideo() {
|
|
hideOptionsView();
|
|
videodb.getFile(currentVideo.name, function(blob) {
|
|
share([blob]);
|
|
});
|
|
}
|
|
|
|
function handleSliderTouchStart(event) {
|
|
// If we have a touch start event, we don't need others.
|
|
if (null != touchStartID) {
|
|
return;
|
|
}
|
|
touchStartID = event.changedTouches[0].identifier;
|
|
|
|
dragging = true;
|
|
// calculate the slider wrapper size for slider dragging.
|
|
sliderRect = dom.sliderWrapper.getBoundingClientRect();
|
|
|
|
// We can't do anything if we don't know our duration
|
|
if (dom.player.duration === Infinity) {
|
|
return;
|
|
}
|
|
|
|
handleSliderTouchMove(event);
|
|
}
|
|
|
|
function setVideoUrl(player, video, callback) {
|
|
|
|
function handleLoadedMetadata() {
|
|
// We only want the 'loadedmetadata' handler to execute when the video
|
|
// app explicitly loads a video. To prevent unwanted side affects, for
|
|
// example, when the video app is sent to the background and then to the
|
|
// foreground, where gecko sends a 'loadedmetadata' event (among others),
|
|
// we clear the 'loadedmetadata' event handler after the event fires.
|
|
dom.player.onloadedmetadata = null;
|
|
callback();
|
|
}
|
|
|
|
function loadVideo(url) {
|
|
loadingChecker.ensureVideoLoads(handleLoadedMetadata);
|
|
player.src = url;
|
|
}
|
|
|
|
// Make sure we don't display captions from an old video
|
|
captions.remove();
|
|
|
|
if ('name' in video) {
|
|
videodb.getFile(video.name, function(file) {
|
|
captions.findAndDisplay(file.name);
|
|
var url = URL.createObjectURL(file);
|
|
loadVideo(url);
|
|
|
|
if (pendingPick) {
|
|
currentVideoBlob = file;
|
|
}
|
|
});
|
|
} else if ('url' in video) {
|
|
loadVideo(video.url);
|
|
}
|
|
}
|
|
|
|
function scheduleVideoControlsAutoHiding() {
|
|
controlFadeTimeout = setTimeout(function() {
|
|
setControlsVisibility(false);
|
|
}, 250);
|
|
}
|
|
|
|
function setNFCSharing(enable) {
|
|
if (!window.navigator.mozNfc) {
|
|
return;
|
|
}
|
|
|
|
if (enable) {
|
|
// If we have NFC, we need to put the callback to have shrinking UI.
|
|
window.navigator.mozNfc.onpeerready = function(event) {
|
|
// The callback function is called when user confirm to share the
|
|
// content, send it with NFC Peer.
|
|
videodb.getFile(currentVideo.name, function(file) {
|
|
event.peer.sendFile(file);
|
|
});
|
|
};
|
|
} else {
|
|
// We need to remove onpeerready while out of fullscreen view.
|
|
window.navigator.mozNfc.onpeerready = null;
|
|
}
|
|
}
|
|
|
|
// show video player
|
|
function showPlayer(video, autoPlay, enterFullscreen, keepControls) {
|
|
if (currentVideo) {
|
|
var old = thumbnailList.thumbnailMap[currentVideo.name];
|
|
old.htmlNode.classList.remove('focused');
|
|
}
|
|
currentVideo = video;
|
|
var thumbnail = thumbnailList.thumbnailMap[currentVideo.name];
|
|
thumbnail.htmlNode.classList.add('focused');
|
|
|
|
// switch to the video player view
|
|
updateDialog();
|
|
dom.player.preload = 'metadata';
|
|
|
|
function doneSeeking() {
|
|
dom.player.onseeked = null;
|
|
setControlsVisibility(true);
|
|
|
|
// to schedule auto hiding when caller don't need to keep controls always.
|
|
if (!keepControls) {
|
|
scheduleVideoControlsAutoHiding();
|
|
}
|
|
|
|
if (autoPlay) {
|
|
play();
|
|
} else {
|
|
pause();
|
|
}
|
|
|
|
//show video player after seeking is done
|
|
dom.player.hidden = false;
|
|
}
|
|
|
|
//hide video player before setVideoUrl
|
|
dom.player.hidden = true;
|
|
setVideoUrl(dom.player, currentVideo, function() {
|
|
|
|
if (enterFullscreen) {
|
|
switchLayout(LAYOUT_MODE.fullscreenPlayer);
|
|
}
|
|
|
|
var formattedDuration = MediaUtils.formatDuration(dom.player.duration);
|
|
dom.durationText.textContent = formattedDuration;
|
|
timeUpdated();
|
|
|
|
setButtonPaused(false);
|
|
playerShowing = true;
|
|
|
|
var rotation;
|
|
if ('metadata' in currentVideo) {
|
|
if (currentVideo.metadata.currentTime === dom.player.duration) {
|
|
currentVideo.metadata.currentTime = 0;
|
|
}
|
|
dom.videoTitle.textContent = currentVideo.metadata.title;
|
|
dom.player.currentTime = currentVideo.metadata.currentTime || 0;
|
|
rotation = currentVideo.metadata.rotation;
|
|
} else {
|
|
dom.videoTitle.textContent = currentVideo.title || '';
|
|
dom.player.currentTime = 0;
|
|
rotation = 0;
|
|
}
|
|
|
|
document.l10n.setAttributes(dom.timeSlider, 'seek-bar',
|
|
{ duration: formattedDuration });
|
|
dom.timeSlider.setAttribute('aria-valuemin', 0);
|
|
dom.timeSlider.setAttribute('aria-valuemax', dom.player.duration);
|
|
dom.timeSlider.setAttribute('aria-valuenow', dom.player.currentTime);
|
|
dom.timeSlider.setAttribute('aria-valuetext',
|
|
MediaUtils.formatDuration(dom.player.currentTime));
|
|
|
|
VideoUtils.fitContainer(dom.videoContainer, dom.player,
|
|
rotation || 0);
|
|
|
|
if (dom.player.seeking) {
|
|
dom.player.onseeked = doneSeeking;
|
|
} else {
|
|
doneSeeking();
|
|
}
|
|
// Enable NFC sharing in fullscreen player mode
|
|
setNFCSharing(true);
|
|
});
|
|
}
|
|
|
|
function hidePlayer(updateVideoMetadata, callback) {
|
|
if (!playerShowing) {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
return;
|
|
}
|
|
|
|
dom.player.pause();
|
|
// Disable NFC sharing when leaving player mode
|
|
setNFCSharing(false);
|
|
|
|
function completeHidingPlayer() {
|
|
// switch to the video gallery view
|
|
switchLayout(LAYOUT_MODE.list);
|
|
|
|
setButtonPaused(false);
|
|
playerShowing = false;
|
|
updateDialog();
|
|
|
|
// The video is no longer being played; unload the it.
|
|
captions.remove();
|
|
dom.player.removeAttribute('src');
|
|
dom.player.load();
|
|
|
|
// Now that we're done using the video hardware to play a video, we
|
|
// can start using it to parse metadata again, if we need to.
|
|
startParsingMetadata();
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
if (!('metadata' in currentVideo) || !updateVideoMetadata || pendingPick) {
|
|
completeHidingPlayer();
|
|
return;
|
|
}
|
|
|
|
var video = currentVideo;
|
|
var thumbnail = thumbnailList.thumbnailMap[video.name];
|
|
|
|
// If we reached the end of the video, then currentTime will have gone
|
|
// back to zero. If that is the case then we want to erase any bookmark
|
|
// image from the metadata and revert to the original poster image.
|
|
// Otherwise, if we've stopped in the middle of a video, we need to
|
|
// capture the current frame to use as a new bookmark. In either case
|
|
// we call updateMetadata() to update the thumbnail and update this and
|
|
// other modified metadata.
|
|
if (dom.player.currentTime === 0) {
|
|
video.metadata.bookmark = null; // Don't use delete here
|
|
updateMetadata();
|
|
}
|
|
else {
|
|
captureFrame(dom.player, video.metadata, function(bookmark) {
|
|
video.metadata.bookmark = bookmark;
|
|
updateMetadata();
|
|
});
|
|
}
|
|
|
|
function updateMetadata() {
|
|
// Update the thumbnail image for this video
|
|
thumbnail.updatePoster(video.metadata.bookmark || video.metadata.poster);
|
|
|
|
// If this is the first time the video was watched, record that it has
|
|
// been watched now and update the corresponding document element.
|
|
if (!video.metadata.watched) {
|
|
video.metadata.watched = true;
|
|
thumbnail.setWatched(true);
|
|
}
|
|
|
|
// Remember the current time so we can resume playback at this point
|
|
video.metadata.currentTime = dom.player.currentTime;
|
|
|
|
// Save the new metadata to the db, but don't wait for it to complete
|
|
videodb.updateMetadata(video.name, video.metadata);
|
|
|
|
// Finally, we can switch back to the thumbnails now
|
|
completeHidingPlayer();
|
|
}
|
|
}
|
|
|
|
function playerEnded() {
|
|
// Ignore ended events that occur while the user is dragging the slider
|
|
if (dragging) {
|
|
return;
|
|
}
|
|
|
|
// If we are still playing when this 'ended' event arrives, then the
|
|
// user played the video all the way to the end, and we skip to the
|
|
// beginning and pause so it is easy for the user to restart. If we
|
|
// reach the end because the user fast forwarded or dragged the slider
|
|
// to the end, then we will have paused the video before we get this
|
|
// event and in that case we will remain paused at the end of the video.
|
|
if (playing) {
|
|
dom.player.currentTime = 0;
|
|
pause();
|
|
}
|
|
}
|
|
|
|
function setButtonPaused(paused) {
|
|
dom.play.classList.toggle('paused', paused);
|
|
dom.play.setAttribute('data-l10n-id',
|
|
paused ? 'play-button' : 'pause-button');
|
|
}
|
|
|
|
function play() {
|
|
loadingChecker.ensureVideoPlays();
|
|
// Switch the button icon
|
|
setButtonPaused(false);
|
|
|
|
// Start recording statistics
|
|
//
|
|
// This requires getVideoPlaybackQuality() to be enabled
|
|
// on the video element which can be achieved
|
|
// by setting the media.mediasource.enabled pref to true.
|
|
VideoStats.start(dom.player);
|
|
|
|
dom.player.play();
|
|
playing = true;
|
|
}
|
|
|
|
function pause() {
|
|
loadingChecker.cancelEnsureVideoPlays();
|
|
|
|
// Switch the button icon
|
|
setButtonPaused(true);
|
|
|
|
// Check the dragging is true or not before pausing
|
|
if (dragging) {
|
|
dragging = false;
|
|
dom.playHead.classList.remove('active');
|
|
}
|
|
|
|
// Stop playing the video
|
|
dom.player.pause();
|
|
playing = false;
|
|
|
|
//stop recording statistics and print them
|
|
VideoStats.stop();
|
|
VideoStats.dump();
|
|
}
|
|
|
|
// Update the progress bar and play head as the video plays
|
|
function timeUpdated() {
|
|
if (controlShowing) {
|
|
// We can't update a progress bar if we don't know how long
|
|
// the video is. It is kind of a bug that the <video> element
|
|
// can't figure this out for ogv videos.
|
|
if (dom.player.duration === Infinity || dom.player.duration === 0) {
|
|
return;
|
|
}
|
|
|
|
updateVideoControlSlider();
|
|
}
|
|
|
|
dom.timeSlider.setAttribute('aria-valuenow', dom.player.currentTime);
|
|
dom.timeSlider.setAttribute('aria-valuetext',
|
|
MediaUtils.formatDuration(dom.player.currentTime));
|
|
}
|
|
|
|
function handleSliderTouchEnd(event) {
|
|
// We don't care the event not related to touchStartID
|
|
if (!event.changedTouches.identifiedTouch(touchStartID)) {
|
|
return;
|
|
}
|
|
touchStartID = null;
|
|
|
|
if (!dragging) {
|
|
// We don't need to do anything without dragging.
|
|
return;
|
|
}
|
|
|
|
dragging = false;
|
|
|
|
dom.playHead.classList.remove('active');
|
|
|
|
// We didn't move the play head while the user's finger was on it
|
|
// so now it may be in the wrong place.
|
|
updateVideoControlSlider();
|
|
|
|
if (dom.player.currentTime === dom.player.duration) {
|
|
pause();
|
|
}
|
|
}
|
|
|
|
function handleSliderTouchMove(event) {
|
|
if (!dragging) {
|
|
return;
|
|
}
|
|
|
|
var touch = event.changedTouches.identifiedTouch(touchStartID);
|
|
// We don't care the event not related to touchStartID
|
|
if (!touch) {
|
|
return;
|
|
}
|
|
|
|
function getTouchPos() {
|
|
return (document.documentElement.dir === 'ltr') ?
|
|
(touch.clientX - sliderRect.left) :
|
|
(sliderRect.right - touch.clientX);
|
|
}
|
|
|
|
var touchPos = getTouchPos();
|
|
|
|
var pos = touchPos / sliderRect.width;
|
|
pos = Math.max(pos, 0);
|
|
pos = Math.min(pos, 1);
|
|
|
|
// Update the slider to match the position of the user's finger.
|
|
// Note, however, that we don't update the displayed time until
|
|
// we actually get a 'seeked' event.
|
|
var percent = pos * 100 + '%';
|
|
dom.playHead.classList.add('active');
|
|
movePlayHead(percent);
|
|
dom.elapsedTime.style.width = percent;
|
|
seeker.seekTo(dom.player.duration * pos);
|
|
}
|
|
|
|
function handleSliderKeypress(event) {
|
|
// The standard accessible control for sliders is arrow up/down keys.
|
|
// Our screenreader synthesizes those events on swipe up/down gestures.
|
|
// Currently, we only allow screen reader users to adjust sliders with a
|
|
// constant step size (there is no page up/down equivalent). In the case
|
|
// of videos, we make sure that the maximum amount of steps for the entire
|
|
// duration is 20, or 2 second increments if the duration is less then 40
|
|
// seconds.
|
|
var step = Math.max(dom.player.duration / 20, 2);
|
|
if (event.keyCode === event.DOM_VK_DOWN) {
|
|
seeker.seekTo(dom.player.currentTime - step);
|
|
} else if (event.keyCode === event.DOM_VK_UP) {
|
|
seeker.seekTo(dom.player.currentTime + step);
|
|
}
|
|
}
|
|
|
|
function toCamelCase(str) {
|
|
return str.replace(/\-(.)/g, function replacer(str, p1) {
|
|
return p1.toUpperCase();
|
|
});
|
|
}
|
|
|
|
//
|
|
// Pick activity
|
|
//
|
|
function showPickView() {
|
|
thumbnailList.setPickMode(true);
|
|
document.body.classList.add('pick-activity');
|
|
|
|
dom.pickerHeader.addEventListener('action', cancelPick);
|
|
|
|
// In tablet, landscape mode, the pick view will have different UI from normal
|
|
// view.
|
|
if (!isPhone && !isPortrait) {
|
|
// update all title text when rotating.
|
|
thumbnailList.updateAllThumbnailTitles();
|
|
}
|
|
}
|
|
|
|
function cancelPick() {
|
|
pendingPick.postError('pick cancelled');
|
|
cleanupPick();
|
|
}
|
|
|
|
function cleanupPick() {
|
|
pendingPick = null;
|
|
currentVideoBlob = null;
|
|
hidePlayer(false);
|
|
}
|
|
|
|
function showThrobber() {
|
|
dom.throbber.classList.remove('hidden');
|
|
dom.throbber.classList.add('throb');
|
|
}
|
|
|
|
function hideThrobber() {
|
|
dom.throbber.classList.remove('throb');
|
|
// setTimeout is a workaround for Bug 1166500
|
|
setTimeout(function() { dom.throbber.classList.add('hidden'); }, 100);
|
|
}
|
|
|
|
// This function unloads the current video to release the decoder
|
|
// hardware. We use it when invoking a share activity because if the
|
|
// share activity is inline, then we won't go to the background and
|
|
// the receiving app won't be able to play the video.
|
|
function releaseVideo() {
|
|
if (videoHardwareReleased) {
|
|
return;
|
|
}
|
|
videoHardwareReleased = true;
|
|
|
|
// readyState = 0: no metadata loaded, we don't need to save the currentTime
|
|
// of player. It is always 0 and can't be used to restore the state of video.
|
|
if (dom.player.readyState > 0) {
|
|
restoreTime = dom.player.currentTime;
|
|
}
|
|
else {
|
|
restoreTime = 0;
|
|
}
|
|
captions.remove();
|
|
dom.player.removeAttribute('src');
|
|
dom.player.load();
|
|
}
|
|
|
|
// We call this to load and seek the video again when the share activity
|
|
// is complete.
|
|
function restoreVideo() {
|
|
if (!videoHardwareReleased) {
|
|
return;
|
|
}
|
|
videoHardwareReleased = false;
|
|
|
|
// When restoreVideo is called, we assume we have currentVideo because the
|
|
// playerShowing is true.
|
|
setVideoUrl(dom.player, currentVideo, function() {
|
|
VideoUtils.fitContainer(dom.videoContainer, dom.player,
|
|
currentVideo.metadata.rotation || 0);
|
|
dom.player.currentTime = restoreTime;
|
|
});
|
|
}
|
|
|
|
//
|
|
// Bug 1088456: when the view activity is launched by the bluetooth transfer
|
|
// app (when the user taps on a downloaded file in the notification tray) the
|
|
// view.html file can be launched while index.html is still running as the
|
|
// foreground app. Since the video app does not get sent to the background in
|
|
// this case, the currently playing video (if there is one) is not
|
|
// unloaded. And so, in the case of videos that require decoder hardware, the
|
|
// view activity cannot play the video. For this workaround, we have view.js
|
|
// set a localStorage property when it starts. And here we listen for changes
|
|
// to that property. When we see a change we unload the video so that the view
|
|
// activity can play its video. We intentionally do not make any effort to
|
|
// automatically restart the video.
|
|
//
|
|
// Bug 1085212: if we already released the video hardware (when starting a
|
|
// share activity) then we don't need to respond to this localStorage hack.
|
|
//
|
|
window.addEventListener('storage', function(e) {
|
|
if (e.key === 'view-activity-wants-to-use-hardware' && e.newValue &&
|
|
!document.hidden && playerShowing && !videoHardwareReleased) {
|
|
console.log('The video app view activity needs to play a video.');
|
|
console.log('Pausing the video and returning to the thumbnails.');
|
|
console.log('See bug 1088456.');
|
|
handleCloseButtonClick();
|
|
}
|
|
});
|