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:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

595
gaia/apps/video/js/view.js Executable file
View file

@ -0,0 +1,595 @@
'use strict';
/* global
getStorageIfAvailable,
getVideoRotation,
VideoUtils,
ForwardRewindController,
MimeMapper,
getUnusedFilename,
MediaUtils,
VideoLoadingChecker,
MediaError,
Seeker
*/
/*
* This is a stripped down version of video.js that handles the view activity
* and displays streaming videos specified by url.
*
* Unfortunately, there is a fair bit of code duplication between this
* file and video.js, but we are too close to the v1 deadline to refactor
* the shared code into a single shared file. If the video player UI changes
* those changes will have to be made in both video.js and view.js
*/
(function init() {
var dom = {}; // document elements
var seeker;
var playing = false;
var controlShowing = false;
var controlFadeTimeout = null;
var dragging = false;
var storage; // A device storage object used by the save button
var saved = false; // Did we save it?
var videoRotation = 0;
var activity; // The activity object we're handling
var data; // The data sent by the initiating app
var blob; // The blob we'll be displaying and maybe saving
var url; // URL string representing the blob object
var title; // What we call the video in the titlebar and banner
// touch start id is the identifier of touch event. we only need to process
// events related to this id.
var touchStartID = null;
var sliderRect;
window.pause = pause;
//
// 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)
// this code starts running while the regular video app 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 paused. And so, in the case of videos that require decoder hardware,
// the view activity cannot play the video. For this workaround, we have set
// a localStorage property here. The video.js file should receive an event
// when we do that and will use that as a signal to unload its video. We use
// Date.now() as the value of the property so we get a different value and
// generate an event each time we run.
//
localStorage.setItem('view-activity-wants-to-use-hardware', Date.now());
// Register a handler to receive the Activity object
navigator.mozSetMessageHandler('activity', handleViewActivity);
function handleViewActivity(request) {
activity = request;
data = activity.source.data;
blob = data.blob;
url = data.url;
title = data.title || '';
// Set up the UI, if it is not already set up
if (!dom.player) {
initUI();
}
// Hide save button
dom.save.hidden = true;
// If blob exists, video should be launched by open activity
if (blob) {
// The title we display for this video may be explicitly specified,
// or it might be the specified filename to save to or it might be
// the filename of the blob.
title = data.title || baseName(data.filename) || baseName(blob.name);
url = URL.createObjectURL(blob);
// If the app that initiated this activity wants us to allow the
// user to save this blob as a file, and if device storage is available
// and if there is enough free space, then display a save button.
if (data.allowSave && data.filename && checkFilename()) {
getStorageIfAvailable('videos', blob.size, function(ds) {
dom.save.hidden = false;
// HACK: Cause gaia-header to re-run font-fit logic
// now that the 'save' button is visible.
dom.videoTitle.textContent = dom.videoTitle.textContent;
storage = ds;
});
}
// to hide player because it shows in the wrong rotation.
dom.player.classList.add('hidden');
// video rotation is not parsed, parse it.
getVideoRotation(blob, function(rotation) {
// when error found, fallback to 0
if (typeof rotation === 'string') {
console.error('get video rotation error: ' + rotation);
videoRotation = 0;
} else {
videoRotation = rotation;
}
// show player when player size and rotation are correct.
dom.player.classList.remove('hidden');
// start to play the video that showPlayer also calls fitContainer.
showPlayer(url, title);
});
} else {
// In the url case, we don't need to calculate the rotation.
showPlayer(url, title);
}
}
// Terminate video playback when visibility is changed.
window.addEventListener('visibilitychange',
function onVisibilityChanged() {
if (document.hidden) {
done();
}
});
// 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);
}
});
}
function setVideoPlaying(playing) {
if (playing) {
play();
} else {
pause();
}
}
function initUI() {
// Fullscreen mode and inline activities don't seem to play well together
// so we'll play the video without going into fullscreen mode.
// Get all the elements we use by their id
var ids = ['player', 'player-view',
'player-header', 'play', 'playHead', 'video-container',
'elapsedTime', 'video-title', 'duration-text', 'elapsed-text',
'slider-wrapper', 'spinner-overlay', 'timeSlider',
'save', 'banner', 'message', 'seek-forward',
'seek-backward', 'videoControlBar', 'in-use-overlay',
'in-use-overlay-title', 'in-use-overlay-text'];
ids.forEach(function createElementRef(name) {
dom[toCamelCase(name)] = document.getElementById(name);
});
function toCamelCase(str) {
return str.replace(/\-(.)/g, function replacer(str, p1) {
return p1.toUpperCase();
});
}
seeker = new Seeker(dom.player);
dom.player.mozAudioChannelType = 'content';
// handling the dragging of slider
dom.sliderWrapper.addEventListener('touchstart', handleSliderTouchStart);
dom.sliderWrapper.addEventListener('touchmove', handleSliderTouchMove);
dom.sliderWrapper.addEventListener('touchend', handleSliderTouchEnd);
// Rescale when window size changes. This should get called when
// orientation changes.
window.addEventListener('resize', function() {
if (dom.player.readyState !== dom.player.HAVE_NOTHING) {
VideoUtils.fitContainer(dom.videoContainer, dom.player,
videoRotation || 0);
}
});
dom.player.addEventListener('timeupdate', timeUpdated);
dom.player.addEventListener('seeked', updateSlider);
// showing + hiding the loading spinner
dom.player.addEventListener('waiting', showSpinner);
dom.player.addEventListener('playing', hideSpinner);
dom.player.addEventListener('play', hideSpinner);
dom.player.addEventListener('pause', hideSpinner);
dom.player.addEventListener('ended', playerEnded);
dom.player.addEventListener('canplaythrough', hideSpinner);
// option buttons
dom.play.addEventListener('click', handlePlayButtonClick);
dom.playerHeader.addEventListener('action', done);
dom.save.addEventListener('click', save);
// show/hide controls
dom.videoContainer.addEventListener('click', toggleVideoControls);
// handle slider keypress, emitted by the screen reader
dom.timeSlider.addEventListener('keypress', handleSliderKeypress);
ForwardRewindController.init(dom.player, dom.seekForward, dom.seekBackward,
seeker);
}
function checkFilename() {
var dotIdx = data.filename.lastIndexOf('.');
if (dotIdx > -1) {
var ext = data.filename.substr(dotIdx + 1);
return MimeMapper.guessTypeFromExtension(ext) === blob.type;
} else {
return false;
}
}
function setControlsVisibility(visible) {
dom.playerView.classList[visible ? 'remove' : 'add'](
'video-controls-hidden');
controlShowing = visible;
// 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');
if (visible) {
// update elapsed time and slider while showing.
updateSlider();
}
}
function handlePlayButtonClick() {
setVideoPlaying(dom.player.paused);
}
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.
e.cancelBubble = !controlShowing;
setControlsVisibility(!controlShowing);
}
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 done() {
pause();
// Release any video resources
dom.player.removeAttribute('src');
dom.player.load();
// End the activity
activity.postResult({saved: saved});
// Undo the bug 1088456 workaround hack.
localStorage.removeItem('view-activity-wants-to-use-hardware');
}
function save() {
// Hide the menu that holds the save button: we can only save once
dom.save.hidden = true;
// XXX work around bug 870619
dom.videoTitle.textContent = dom.videoTitle.textContent;
getUnusedFilename(storage, data.filename, function(filename) {
var savereq = storage.addNamed(blob, filename);
savereq.onsuccess = function() {
// Remember that it has been saved so we can pass this back
// to the invoking app
saved = filename;
// And tell the user
showBanner({
id: 'saved',
args: { title: title }
});
};
savereq.onerror = function(e) {
// XXX we don't report this to the user because it is hard to
// localize.
console.error('Error saving', filename, e);
};
});
}
// show video player
function showPlayer(url, title) {
function handleLoadedMetadata() {
var formattedDuration = MediaUtils.formatDuration(dom.player.duration);
dom.durationText.textContent = formattedDuration;
timeUpdated();
setButtonPaused(false);
VideoUtils.fitContainer(dom.videoContainer, dom.player,
videoRotation || 0);
dom.player.currentTime = 0;
// Show the controls briefly then fade out
controlFadeTimeout = setTimeout(function() {
setControlsVisibility(false);
}, 2000);
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));
play();
}
dom.videoTitle.textContent = title || '';
setControlsVisibility(true);
var loadingChecker =
new VideoLoadingChecker(dom.player, dom.inUseOverlay,
dom.inUseOverlayTitle,
dom.inUseOverlayText);
loadingChecker.ensureVideoLoads(handleLoadedMetadata);
dom.player.src = url;
dom.player.onloadeddata = function(evt) { URL.revokeObjectURL(url); };
dom.player.onerror = function(evt) {
var errorid = '';
switch (evt.target.error.code) {
case MediaError.MEDIA_ERR_ABORTED:
// This aborted error should be triggered by the user
// so we don't have to show any error messages
return;
case MediaError.MEDIA_ERR_NETWORK:
errorid = 'error-network';
break;
case MediaError.MEDIA_ERR_DECODE:
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
// If users tap some video link in an offline page
// the error code will be MEDIA_ERR_SRC_NOT_SUPPORTED
// we also prompt the unsupported error message for it
errorid = 'error-unsupported';
break;
// Is it possible to be unknown errors?
default:
errorid = 'error-unknown';
break;
}
handleError(errorid);
};
}
function handleError(l10nId) {
document.l10n.formatValue(l10nId).then((msg) => {
alert(msg);
done();
});
}
function play() {
// Switch the button icon
setButtonPaused(false);
// Start playing
dom.player.play();
playing = true;
}
function pause() {
// Switch the button icon
setButtonPaused(true);
// Stop playing the video
dom.player.pause();
playing = false;
}
// 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;
}
updateSlider();
}
dom.timeSlider.setAttribute('aria-valuenow', dom.player.currentTime);
dom.timeSlider.setAttribute('aria-valuetext',
MediaUtils.formatDuration(dom.player.currentTime));
}
function playerEnded() {
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 movePlayHead(percent) {
if (document.documentElement.dir === 'ltr') {
dom.playHead.style.left = percent;
}
else {
dom.playHead.style.right = percent;
}
}
function updateSlider() {
// We update the slider when we get a 'seeked' event.
// Don't do updates while we're seeking because the position we fastSeek()
// to probably isn't exactly where we requested and we don't want jerky
// updates
if (dom.player.seeking) {
return;
}
var percent = (dom.player.currentTime / dom.player.duration) * 100;
if (isNaN(percent)) { // this happens when we end the activity
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 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.
updateSlider();
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 showBanner(l10n) {
document.l10n.setAttributes(
dom.message,
l10n.id,
l10n.args);
dom.banner.hidden = false;
setTimeout(function() {
dom.banner.hidden = true;
}, 3000);
}
// Strip directories and just return the base filename
function baseName(filename) {
if (!filename) {
return '';
}
return filename.substring(filename.lastIndexOf('/') + 1);
}
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 showSpinner() {
if (!blob) {
dom.spinnerOverlay.classList.remove('hidden');
setDisabled(dom.playerView, true);
}
}
function hideSpinner() {
dom.spinnerOverlay.classList.add('hidden');
setDisabled(dom.playerView, false);
}
})();