File: /var/www/studis.kauko.lt/wp-content/uploads/h5p/libraries/H5P.Video-1.6/scripts/html5.js
/** @namespace H5P */
H5P.VideoHtml5 = (function ($) {
/**
* HTML5 video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function Html5(sources, options, l10n) {
var self = this;
/**
* Small helper to ensure all video sources get the same cache buster.
*
* @private
* @param {Object} source
* @return {string}
*/
const getCrossOriginPath = function (source) {
let path = H5P.getPath(source.path, self.contentId);
if (video.crossOrigin !== null && H5P.addQueryParameter && H5PIntegration.crossoriginCacheBuster) {
path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
}
return path
};
/**
* Register track to video
*
* @param {Object} trackData Track object
* @param {string} trackData.kind Kind of track
* @param {Object} trackData.track Source path
* @param {string} [trackData.label] Label of track
* @param {string} [trackData.srcLang] Language code
*/
const addTrack = function (trackData) {
// Skip invalid tracks
if (!trackData.kind || !trackData.track.path) {
return;
}
var track = document.createElement('track');
track.kind = trackData.kind;
track.src = getCrossOriginPath(trackData.track); // Uses same crossOrigin as parent. You cannot mix.
if (trackData.label) {
track.label = trackData.label;
}
if (trackData.srcLang) {
track.srcLang = trackData.srcLang;
}
return track;
};
/**
* Small helper to set the inital video source.
* Useful if some of the loading happens asynchronously.
* NOTE: Setting the crossOrigin must happen before any of the
* sources(poster, tracks etc.) are loaded
*
* @private
*/
const setInitialSource = function () {
if (qualities[currentQuality] === undefined) {
return;
}
if (H5P.setSource !== undefined) {
H5P.setSource(video, qualities[currentQuality].source, self.contentId)
}
else {
// Backwards compatibility (H5P < v1.22)
const srcPath = H5P.getPath(qualities[currentQuality].source.path, self.contentId);
if (H5P.getCrossOrigin !== undefined) {
var crossOrigin = H5P.getCrossOrigin(srcPath);
video.setAttribute('crossorigin', crossOrigin !== null ? crossOrigin : 'anonymous');
}
video.src = srcPath;
}
// Add poster if provided
if (options.poster) {
video.poster = getCrossOriginPath(options.poster); // Uses same crossOrigin as parent. You cannot mix.
}
// Register tracks
options.tracks.forEach(function (track, i) {
var trackElement = addTrack(track);
if (i === 0) {
trackElement.default = true;
}
if (trackElement) {
video.appendChild(trackElement);
}
});
};
/**
* Displayed when the video is buffering
* @private
*/
var $throbber = $('<div/>', {
'class': 'h5p-video-loading'
});
/**
* Used to display error messages
* @private
*/
var $error = $('<div/>', {
'class': 'h5p-video-error'
});
/**
* Keep track of current state when changing quality.
* @private
*/
var stateBeforeChangingQuality;
var currentTimeBeforeChangingQuality;
/**
* Avoids firing the same event twice.
* @private
*/
var lastState;
/**
* Keeps track whether or not the video has been loaded.
* @private
*/
var isLoaded = false;
/**
*
* @private
*/
var playbackRate = 1;
var skipRateChange = false;
// Create player
var video = document.createElement('video');
// Sort sources into qualities
var qualities = getQualities(sources, video);
var currentQuality;
numQualities = 0;
for (let quality in qualities) {
numQualities++;
}
if (numQualities > 1 && H5P.VideoHtml5.getExternalQuality !== undefined) {
H5P.VideoHtml5.getExternalQuality(sources, function (chosenQuality) {
if (qualities[chosenQuality] !== undefined) {
currentQuality = chosenQuality;
}
setInitialSource();
});
}
else {
// Select quality and source
currentQuality = getPreferredQuality();
if (currentQuality === undefined || qualities[currentQuality] === undefined) {
// No preferred quality, pick the first.
for (currentQuality in qualities) {
if (qualities.hasOwnProperty(currentQuality)) {
break;
}
}
}
setInitialSource();
}
// Setting webkit-playsinline, which makes iOS 10 beeing able to play video
// inside browser.
video.setAttribute('webkit-playsinline', '');
video.setAttribute('playsinline', '');
video.setAttribute('preload', 'metadata');
// Remove buttons in Chrome's video player:
let controlsList = 'nodownload';
if (options.disableFullscreen) {
controlsList += ' nofullscreen';
}
if (options.disableRemotePlayback) {
controlsList += ' noremoteplayback';
}
video.setAttribute('controlsList', controlsList);
// Remove picture in picture as it interfers with other video players
video.disablePictureInPicture = true;
// Set options
video.disableRemotePlayback = (options.disableRemotePlayback ? true : false);
video.controls = (options.controls ? true : false);
// Hardcoded autoplay to false to avoid playing videos on init
video.autoplay = false;
video.loop = (options.loop ? true : false);
video.className = 'h5p-video';
video.style.display = 'block';
if (options.fit) {
// Style is used since attributes with relative sizes aren't supported by IE9.
video.style.width = '100%';
video.style.height = '100%';
}
/**
* Helps registering events.
*
* @private
* @param {String} native Event name
* @param {String} h5p Event name
* @param {String} [arg] Optional argument
*/
var mapEvent = function (native, h5p, arg) {
video.addEventListener(native, function () {
switch (h5p) {
case 'loaded':
isLoaded = true;
if (stateBeforeChangingQuality !== undefined) {
return; // Avoid loaded event when changing quality.
}
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
if (OLD_ANDROID_FIX) {
var andLoaded = function () {
video.removeEventListener('durationchange', andLoaded, false);
// On Android seeking isn't ready until after play.
self.trigger(h5p);
};
video.addEventListener('durationchange', andLoaded, false);
return;
}
if (video.poster) {
$(video).one('play', function () {
self.seek(self.getCurrentTime() || options.startAt);
});
}
else {
self.seek(options.startAt);
}
break;
case 'error':
// Handle error and get message.
arg = error(arguments[0], arguments[1]);
break;
case 'playbackRateChange':
// Fix for keeping playback rate in IE11
if (skipRateChange) {
skipRateChange = false;
return; // Avoid firing event when changing back
}
if (H5P.Video.IE11_PLAYBACK_RATE_FIX && playbackRate != video.playbackRate) { // Intentional
// Prevent change in playback rate not triggered by the user
video.playbackRate = playbackRate;
skipRateChange = true;
return;
}
// End IE11 fix
arg = self.getPlaybackRate();
break;
}
self.trigger(h5p, arg);
}, false);
};
/**
* Handle errors from the video player.
*
* @private
* @param {Object} code Error
* @param {String} [message]
* @returns {String} Human readable error message.
*/
var error = function (code, message) {
if (code instanceof Event) {
// No error code
if (!code.target.error) {
return '';
}
switch (code.target.error.code) {
case MediaError.MEDIA_ERR_ABORTED:
message = l10n.aborted;
break;
case MediaError.MEDIA_ERR_NETWORK:
message = l10n.networkFailure;
break;
case MediaError.MEDIA_ERR_DECODE:
message = l10n.cannotDecode;
break;
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
message = l10n.formatNotSupported;
break;
case MediaError.MEDIA_ERR_ENCRYPTED:
message = l10n.mediaEncrypted;
break;
}
}
if (!message) {
message = l10n.unknownError;
}
// Hide throbber
$throbber.remove();
// Display error message to user
$error.text(message).insertAfter(video);
// Pass message to our error event
return message;
};
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.append(video);
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
// Create reverse list
var options = [];
for (var q in qualities) {
if (qualities.hasOwnProperty(q)) {
options.splice(0, 0, {
name: q,
label: qualities[q].label
});
}
}
if (options.length < 2) {
// Do not return if only one quality.
return;
}
return options;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
return currentQuality;
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
if (qualities[quality] === undefined || quality === currentQuality) {
return; // Invalid quality
}
// Keep track of last choice
setPreferredQuality(quality);
// Avoid multiple loaded events if changing quality multiple times.
if (!stateBeforeChangingQuality) {
// Keep track of last state
stateBeforeChangingQuality = lastState;
// Keep track of current time
currentTimeBeforeChangingQuality = video.currentTime;
// Seek and start video again after loading.
var loaded = function () {
video.removeEventListener('loadedmetadata', loaded, false);
if (OLD_ANDROID_FIX) {
var andLoaded = function () {
video.removeEventListener('durationchange', andLoaded, false);
// On Android seeking isn't ready until after play.
self.seek(currentTimeBeforeChangingQuality);
};
video.addEventListener('durationchange', andLoaded, false);
}
else {
// Seek to current time.
self.seek(currentTimeBeforeChangingQuality);
}
// Always play to get image.
video.play();
if (stateBeforeChangingQuality !== H5P.Video.PLAYING) {
// Do not resume playing
video.pause();
}
// Done changing quality
stateBeforeChangingQuality = undefined;
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
};
video.addEventListener('loadedmetadata', loaded, false);
}
// Keep track of current quality
currentQuality = quality;
self.trigger('qualityChange', currentQuality);
// Display throbber
self.trigger('stateChange', H5P.Video.BUFFERING);
// Change source
video.src = getCrossOriginPath(qualities[quality].source); // (iPad does not support #t=).
// Note: Optional tracks use same crossOrigin as the original. You cannot mix.
// Remove poster so it will not show during quality change
video.removeAttribute('poster');
};
/**
* Starts the video.
*
* @public
* @return {Promise|undefined} May return a Promise that resolves when
* play has been processed.
*/
self.play = function () {
if ($error.is(':visible')) {
return;
}
if (!isLoaded) {
// Make sure video is loaded before playing
video.load();
video.addEventListener('loadeddata', function() {
video.play();
}, false);
}
else {
return video.play();
}
};
/**
* Pauses the video.
*
* @public
*/
self.pause = function () {
video.pause();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
video.currentTime = time;
// Use canplaythrough for IOs devices
// Use loadedmetadata for all other devices.
const eventName = navigator.userAgent.match(/iPad|iPod|iPhone/i) ? "canplaythrough" : "loadedmetadata";
function seekTo() {
video.currentTime = time;
video.removeEventListener(eventName, seekTo);
};
if (video.readyState === 4) {
seekTo();
}
else {
video.addEventListener(eventName, seekTo);
}
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
return video.currentTime;
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (isNaN(video.duration)) {
return;
}
return video.duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
// Find buffer currently playing from
var buffered = 0;
for (var i = 0; i < video.buffered.length; i++) {
var from = video.buffered.start(i);
var to = video.buffered.end(i);
if (video.currentTime > from && video.currentTime < to) {
buffered = to;
break;
}
}
// To percentage
return buffered ? (buffered / video.duration) * 100 : 0;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
video.muted = true;
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
video.muted = false;
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
return video.muted;
};
/**
* Returns the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
return video.volume * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
video.volume = level / 100;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
/*
* not sure if there's a common rule about determining good speeds
* using Google's standard options via a constant for setting
*/
var playbackRates = PLAYBACK_RATES;
return playbackRates;
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
return video.playbackRate;
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
playbackRate = newPlaybackRate;
video.playbackRate = newPlaybackRate;
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
for (var i = 0; i < video.textTracks.length; i++) {
video.textTracks[i].mode = (track && track.value === i ? 'showing' : 'disabled');
}
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
for (var i = 0; i < video.textTracks.length; i++) {
if (video.textTracks[i].mode === 'showing') {
return new H5P.Video.LabelValue(video.textTracks[i].label, i);
}
}
return null;
};
// Register event listeners
mapEvent('ended', 'stateChange', H5P.Video.ENDED);
mapEvent('playing', 'stateChange', H5P.Video.PLAYING);
mapEvent('pause', 'stateChange', H5P.Video.PAUSED);
mapEvent('waiting', 'stateChange', H5P.Video.BUFFERING);
mapEvent('loadedmetadata', 'loaded');
mapEvent('canplay', 'canplay');
mapEvent('error', 'error');
mapEvent('ratechange', 'playbackRateChange');
if (!video.controls) {
// Disable context menu(right click) to prevent controls.
video.addEventListener('contextmenu', function (event) {
event.preventDefault();
}, false);
}
// Display throbber when buffering/loading video.
self.on('stateChange', function (event) {
var state = event.data;
lastState = state;
if (state === H5P.Video.BUFFERING) {
$throbber.insertAfter(video);
}
else {
$throbber.remove();
}
});
// Load captions after the video is loaded
self.on('loaded', function () {
nextTick(function () {
var textTracks = [];
for (var i = 0; i < video.textTracks.length; i++) {
textTracks.push(new H5P.Video.LabelValue(video.textTracks[i].label, i));
}
if (textTracks.length) {
self.trigger('captions', textTracks);
}
});
});
// Alternative to 'canplay' event
/*self.on('resize', function () {
if (video.offsetParent === null) {
return;
}
video.style.width = '100%';
video.style.height = '100%';
var width = video.clientWidth;
var height = options.fit ? video.clientHeight : (width * (video.videoHeight / video.videoWidth));
video.style.width = width + 'px';
video.style.height = height + 'px';
});*/
// Video controls are ready
nextTick(function () {
self.trigger('ready');
});
/**
* Check video is loaded and ready play.
*
* @public
* @param {Boolean}
*/
self.isLoaded = function () {
return isLoaded;
};
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Html5.canPlay = function (sources) {
var video = document.createElement('video');
if (video.canPlayType === undefined) {
return false; // Not supported
}
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
var type = getType(sources[i]);
if (type && video.canPlayType(type) !== '') {
// We should be able to play this
return true;
}
}
return false;
};
/**
* Find source type.
*
* @private
* @param {Object} source
* @returns {String}
*/
var getType = function (source) {
var type = source.mime;
if (!type) {
// Try to get type from URL
var matches = source.path.match(/\.(\w+)$/);
if (matches && matches[1]) {
type = 'video/' + matches[1];
}
}
if (type && source.codecs) {
// Add codecs
type += '; codecs="' + source.codecs + '"';
}
return type;
};
/**
* Sort sources into qualities.
*
* @private
* @static
* @param {Array} sources
* @param {Object} video
* @returns {Object} Quality mapping
*/
var getQualities = function (sources, video) {
var qualities = {};
var qualityIndex = 1;
var lastQuality;
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// Find and update type.
var type = source.type = getType(source);
// Check if we support this type
var isPlayable = type && (type === 'video/unknown' || video.canPlayType(type) !== '');
if (!isPlayable) {
continue; // We cannot play this source
}
if (source.quality === undefined) {
/**
* No quality metadata. Create a quality tag to separate multiple sources of the same type,
* e.g. if two mp4 files with different quality has been uploaded
*/
if (lastQuality === undefined || qualities[lastQuality].source.type === type) {
// Create a new quality tag
source.quality = {
name: 'q' + qualityIndex,
label: (source.metadata && source.metadata.qualityName) ? source.metadata.qualityName : 'Quality ' + qualityIndex // TODO: l10n
};
qualityIndex++;
}
else {
/**
* Assumes quality already exists in a different format.
* Uses existing label for this quality.
*/
source.quality = qualities[lastQuality].source.quality;
}
}
// Log last quality
lastQuality = source.quality.name;
// Look to see if quality exists
var quality = qualities[lastQuality];
if (quality) {
// We have a source with this quality. Check if we have a better format.
if (source.mime.split('/')[1] === PREFERRED_FORMAT) {
quality.source = source;
}
}
else {
// Add new source with quality.
qualities[source.quality.name] = {
label: source.quality.label,
source: source
};
}
}
return qualities;
};
/**
* Set preferred video quality.
*
* @private
* @static
* @param {String} quality Index of preferred quality
*/
var setPreferredQuality = function (quality) {
try {
localStorage.setItem('h5pVideoQuality', quality);
}
catch (err) {
console.warn('Unable to set preferred video quality, localStorage is not available.');
}
};
/**
* Get preferred video quality.
*
* @private
* @static
* @returns {String} Index of preferred quality
*/
var getPreferredQuality = function () {
// First check localStorage
let quality;
try {
quality = localStorage.getItem('h5pVideoQuality');
}
catch (err) {
console.warn('Unable to retrieve preferred video quality from localStorage.');
}
if (!quality) {
try {
// The fallback to old cookie solution
var settings = document.cookie.split(';');
for (var i = 0; i < settings.length; i++) {
var setting = settings[i].split('=');
if (setting[0] === 'H5PVideoQuality') {
quality = setting[1];
break;
}
}
}
catch (err) {
console.warn('Unable to retrieve preferred video quality from cookie.');
}
}
return quality;
};
/**
* Helps schedule a task for the next tick.
* @param {function} task
*/
var nextTick = function (task) {
setTimeout(task, 0);
};
/** @constant {Boolean} */
var OLD_ANDROID_FIX = false;
/** @constant {Boolean} */
var PREFERRED_FORMAT = 'mp4';
/** @constant {Object} */
var PLAYBACK_RATES = [0.25, 0.5, 1, 1.25, 1.5, 2];
if (navigator.userAgent.indexOf('Android') !== -1) {
// We have Android, check version.
var version = navigator.userAgent.match(/AppleWebKit\/(\d+\.?\d*)/);
if (version && version[1] && Number(version[1]) <= 534.30) {
// Include fix for devices running the native Android browser.
// (We don't know when video was fixed, so the number is just the lastest
// native android browser we found.)
OLD_ANDROID_FIX = true;
}
}
else {
if (navigator.userAgent.indexOf('Chrome') !== -1) {
// If we're using chrome on a device that isn't Android, prefer the webm
// format. This is because Chrome has trouble with some mp4 codecs.
PREFERRED_FORMAT = 'webm';
}
}
return Html5;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoHtml5);