var H5P = H5P || {};
/**
* Constructor.
*
* @param {Object} params Options for this library.
* @param {Number} id Content identifier
* @returns {undefined}
*/
(function ($) {
H5P.Image = function (params, id, extras) {
H5P.EventDispatcher.call(this);
this.extras = extras;
if (params.file === undefined || !(params.file instanceof Object)) {
this.placeholder = true;
}
else {
this.source = H5P.getPath(params.file.path, id);
this.width = params.file.width;
this.height = params.file.height;
}
this.alt = (!params.decorative && params.alt !== undefined) ?
this.stripHTML(this.htmlDecode(params.alt)) :
'';
if (params.title !== undefined) {
this.title = this.stripHTML(this.htmlDecode(params.title));
}
};
H5P.Image.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.Image.prototype.constructor = H5P.Image;
/**
* Wipe out the content of the wrapper and put our HTML in it.
*
* @param {jQuery} $wrapper
* @returns {undefined}
*/
H5P.Image.prototype.attach = function ($wrapper) {
var self = this;
var source = this.source;
if (self.$img === undefined) {
if(self.placeholder) {
self.$img = $('
', {
width: '100%',
height: '100%',
class: 'h5p-placeholder',
title: this.title === undefined ? '' : this.title,
on: {
load: function () {
self.trigger('loaded');
}
}
});
} else {
self.$img = $('', {
width: '100%',
height: '100%',
src: source,
alt: this.alt,
title: this.title === undefined ? '' : this.title,
on: {
load: function () {
self.trigger('loaded');
}
}
});
}
}
$wrapper.addClass('h5p-image').html(self.$img);
};
/**
* Retrieve decoded HTML encoded string.
*
* @param {string} input HTML encoded string.
* @returns {string} Decoded string.
*/
H5P.Image.prototype.htmlDecode = function (input) {
const dparser = new DOMParser().parseFromString(input, 'text/html');
return dparser.documentElement.textContent;
};
/**
* Retrieve string without HTML tags.
*
* @param {string} input Input string.
* @returns {string} Output string.
*/
H5P.Image.prototype.stripHTML = function (html) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
return H5P.Image;
}(H5P.jQuery));
;
var H5P = H5P || {};
/**
* H5P Link Library Module.
*/
H5P.Link = (function ($) {
/**
* Link constructor.
*
* @param {Object} parameters
*/
function Link(parameters) {
// Add default parameters
parameters = $.extend(true, {
title: 'New link',
linkWidget: {
protocol: '',
url: ''
}
}, parameters);
var url = '';
if (parameters.linkWidget.protocol !== 'other') {
url += parameters.linkWidget.protocol;
}
url += parameters.linkWidget.url;
/**
* Public. Attach.
*
* @param {jQuery} $container
*/
this.attach = function ($container) {
var sanitizedUrl = sanitizeUrlProtocol(url);
$container.addClass('h5p-link').html('' + parameters.title + '')
.keypress(function (event) {
if (event.which === 32) {
this.click();
}
});
};
/**
* Return url
*
* @returns {string}
*/
this.getUrl = function () {
return url;
};
/**
* Private. Remove illegal url protocols from uri
*/
var sanitizeUrlProtocol = function(uri) {
var allowedProtocols = ['http', 'https', 'ftp', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal'];
var first = true;
var before = '';
while (first || uri != before) {
first = false;
before = uri;
var colonPos = uri.indexOf(':');
if (colonPos > 0) {
// We found a possible protocol
var protocol = uri.substr(0, colonPos);
// If the colon is preceeded by a hash, slash or question mark it isn't a protocol
if (protocol.match(/[/?#]/g)) {
break;
}
// Is this a forbidden protocol?
if (allowedProtocols.indexOf(protocol.toLowerCase()) == -1) {
// If illegal, remove the protocol...
uri = uri.substr(colonPos + 1);
}
}
}
return uri;
};
}
return Link;
})(H5P.jQuery);
;
H5P.AdvancedText = (function ($, EventDispatcher) {
/**
* A simple library for displaying text with advanced styling.
*
* @class H5P.AdvancedText
* @param {Object} parameters
* @param {Object} [parameters.text='New text']
* @param {number} id
*/
function AdvancedText(parameters, id) {
var self = this;
EventDispatcher.call(this);
var html = (parameters.text === undefined ? 'New text' : parameters.text);
/**
* Wipe container and add text html.
*
* @alias H5P.AdvancedText#attach
* @param {H5P.jQuery} $container
*/
self.attach = function ($container) {
$container.addClass('h5p-advanced-text').html(html);
};
}
AdvancedText.prototype = Object.create(EventDispatcher.prototype);
AdvancedText.prototype.constructor = AdvancedText;
return AdvancedText;
})(H5P.jQuery, H5P.EventDispatcher);
;
var H5P = H5P || {};
/**
* H5P audio module
*
* @external {jQuery} $ H5P.jQuery
*/
H5P.Audio = (function ($) {
/**
* @param {Object} params Options for this library.
* @param {Number} id Content identifier.
* @param {Object} extras Extras.
* @returns {undefined}
*/
function C(params, id, extras) {
H5P.EventDispatcher.call(this);
this.contentId = id;
this.params = params;
this.extras = extras;
this.toggleButtonEnabled = true;
// Retrieve previous state
if (extras && extras.previousState !== undefined) {
this.oldTime = extras.previousState.currentTime;
}
this.params = $.extend({}, {
playerMode: 'minimalistic',
fitToWrapper: false,
controls: true,
autoplay: false,
audioNotSupported: "Your browser does not support this audio",
playAudio: "Play audio",
pauseAudio: "Pause audio",
propagateButtonClickEvents: true
}, params);
// Required if e.g. used in CoursePresentation as area to click on
if (this.params.playerMode === 'transparent') {
this.params.fitToWrapper = true;
}
this.on('resize', this.resize, this);
}
C.prototype = Object.create(H5P.EventDispatcher.prototype);
C.prototype.constructor = C;
/**
* Adds a minimalistic audio player with only "play" and "pause" functionality.
*
* @param {jQuery} $container Container for the player.
* @param {boolean} transparentMode true: the player is only visible when hovering over it; false: player's UI always visible
*/
C.prototype.addMinimalAudioPlayer = function ($container, transparentMode) {
var INNER_CONTAINER = 'h5p-audio-inner';
var AUDIO_BUTTON = 'h5p-audio-minimal-button';
var PLAY_BUTTON = 'h5p-audio-minimal-play';
var PLAY_BUTTON_PAUSED = 'h5p-audio-minimal-play-paused';
var PAUSE_BUTTON = 'h5p-audio-minimal-pause';
var self = this;
this.$container = $container;
self.$inner = $('', {
'class': INNER_CONTAINER + (transparentMode ? ' h5p-audio-transparent' : '')
}).appendTo($container);
var audioButton = $('', {
'class': AUDIO_BUTTON + " " + PLAY_BUTTON,
'aria-label': this.params.playAudio
}).appendTo(self.$inner)
.click( function (event) {
if (!self.params.propagateButtonClickEvents){
event.stopPropagation();
}
if (!self.isEnabledToggleButton()) {
return;
}
// Prevent ARIA from playing over audio on click
this.setAttribute('aria-hidden', 'true');
if (self.audio.paused) {
self.play();
}
else {
self.pause();
}
})
.on('focusout', function () {
// Restore ARIA, required when playing longer audio and tabbing out and back in
this.setAttribute('aria-hidden', 'false');
});
// Fit to wrapper
if (this.params.fitToWrapper) {
audioButton.css({
'width': '100%',
'height': '100%'
});
}
//Event listeners that change the look of the player depending on events.
self.audio.addEventListener('ended', function () {
audioButton
.attr('aria-hidden', false)
.attr('aria-label', self.params.playAudio)
.removeClass(PAUSE_BUTTON)
.removeClass(PLAY_BUTTON_PAUSED)
.addClass(PLAY_BUTTON);
});
self.audio.addEventListener('play', function () {
audioButton
.attr('aria-label', self.params.pauseAudio)
.removeClass(PLAY_BUTTON)
.removeClass(PLAY_BUTTON_PAUSED)
.addClass(PAUSE_BUTTON);
});
self.audio.addEventListener('pause', function () {
// Don't override if initial look is set
if (!audioButton.hasClass(PLAY_BUTTON)) {
audioButton
.attr('aria-hidden', false)
.attr('aria-label', self.params.playAudio)
.removeClass(PAUSE_BUTTON)
.addClass(PLAY_BUTTON_PAUSED);
}
});
H5P.Audio.MINIMAL_BUTTON = AUDIO_BUTTON + " " + PLAY_BUTTON;
H5P.Audio.MINIMAL_BUTTON_PAUSED = AUDIO_BUTTON + " " + PLAY_BUTTON_PAUSED;
this.$audioButton = audioButton;
// Scale icon to container
self.resize();
};
/**
* Resizes the audio player icon when the wrapper is resized.
*/
C.prototype.resize = function () {
// Find the smallest value of height and width, and use it to choose the font size.
if (this.params.fitToWrapper && this.$container && this.$container.width()) {
var w = this.$container.width();
var h = this.$container.height();
if (w < h) {
this.$audioButton.css({'font-size': w / 2 + 'px'});
}
else {
this.$audioButton.css({'font-size': h / 2 + 'px'});
}
}
};
return C;
})(H5P.jQuery);
/**
* Wipe out the content of the wrapper and put our HTML in it.
*
* @param {jQuery} $wrapper Our poor container.
*/
H5P.Audio.prototype.attach = function ($wrapper) {
const self = this;
$wrapper.addClass('h5p-audio-wrapper');
// Check if browser supports audio.
var audio = document.createElement('audio');
if (audio.canPlayType === undefined) {
this.attachNotSupportedMessage($wrapper);
return;
}
// Add supported source files.
if (this.params.files !== undefined && this.params.files instanceof Object) {
for (var i = 0; i < this.params.files.length; i++) {
var file = this.params.files[i];
if (audio.canPlayType(file.mime)) {
var source = document.createElement('source');
source.src = H5P.getPath(file.path, this.contentId);
source.type = file.mime;
audio.appendChild(source);
}
}
}
if (!audio.children.length) {
this.attachNotSupportedMessage($wrapper);
return;
}
if (this.endedCallback !== undefined) {
audio.addEventListener('ended', this.endedCallback, false);
}
audio.className = 'h5p-audio';
audio.controls = this.params.controls === undefined ? true : this.params.controls;
// Menu removed, because it's cut off if audio is used as H5P.Question intro
const controlsList = 'nodownload noplaybackrate';
audio.setAttribute('controlsList', controlsList);
audio.preload = 'auto';
audio.style.display = 'block';
if (this.params.fitToWrapper === undefined || this.params.fitToWrapper) {
audio.style.width = '100%';
if (!this.isRoot()) {
// Only set height if this isn't a root
audio.style.height = '100%';
}
}
this.audio = audio;
if (this.params.playerMode === 'minimalistic') {
audio.controls = false;
this.addMinimalAudioPlayer($wrapper, false);
}
else if (this.params.playerMode === 'transparent') {
audio.controls = false;
this.addMinimalAudioPlayer($wrapper, true);
}
else {
$wrapper.html(audio);
}
if (audio.controls) {
$wrapper.addClass('h5p-audio-controls');
}
// Set time to saved time from previous run
if (this.oldTime) {
if (this.$audioButton) {
this.$audioButton.attr('class', H5P.Audio.MINIMAL_BUTTON_PAUSED);
}
this.seekTo(this.oldTime);
}
// Avoid autoplaying in authoring tool
if (window.H5PEditor === undefined) {
// Keep record of autopauses.
// I.e: we don't wanna autoplay if the user has excplicitly paused.
self.autoPaused = true;
// Set up intersection observer
new IntersectionObserver(function (entries) {
const entry = entries[0];
if (entry.intersectionRatio == 0) {
if (!self.audio.paused) {
// Audio element is hidden, pause it
self.autoPaused = true;
self.audio.pause();
}
}
else if (self.params.autoplay && self.autoPaused) {
// Audio element is visible. Autoplay if autoplay is enabled and it was
// not explicitly paused by a user
self.autoPaused = false;
self.play();
}
}, {
root: document.documentElement,
threshold: [0, 1] // Get events when it is shown and hidden
}).observe($wrapper.get(0));
}
};
/**
* Attaches not supported message.
*
* @param {jQuery} $wrapper Our dear container.
*/
H5P.Audio.prototype.attachNotSupportedMessage = function ($wrapper) {
$wrapper.addClass('h5p-audio-not-supported');
$wrapper.html(
'
' +
'
' +
'' + this.params.audioNotSupported + '' +
'
'
);
if (this.endedCallback !== undefined) {
this.endedCallback();
}
}
/**
* Stop & reset playback.
*
* @returns {undefined}
*/
H5P.Audio.prototype.resetTask = function () {
this.stop();
this.seekTo(0);
if (this.$audioButton) {
this.$audioButton.attr('class', H5P.Audio.MINIMAL_BUTTON);
}
};
/**
* Stop the audio. TODO: Rename to pause?
*
* @returns {undefined}
*/
H5P.Audio.prototype.stop = function () {
if (this.audio !== undefined) {
this.audio.pause();
}
};
/**
* Play
*/
H5P.Audio.prototype.play = function () {
if (this.audio !== undefined) {
// play() returns a Promise that can fail, e.g. while autoplaying
this.audio.play().catch((error) => {
console.warn(error);
});
}
};
/**
* @public
* Pauses the audio.
*/
H5P.Audio.prototype.pause = function () {
if (this.audio !== undefined) {
this.audio.pause();
}
};
/**
* @public
* Seek to audio position.
*
* @param {number} seekTo Time to seek to in seconds.
*/
H5P.Audio.prototype.seekTo = function (seekTo) {
if (this.audio !== undefined) {
this.audio.currentTime = seekTo;
}
};
/**
* @public
* Get current state for resetting it later.
*
* @returns {object} Current state.
*/
H5P.Audio.prototype.getCurrentState = function () {
if (this.audio !== undefined && this.audio.currentTime > 0) {
const currentTime = this.audio.ended ? 0 : this.audio.currentTime;
return {
currentTime: currentTime
};
}
};
/**
* @public
* Disable button.
* Not using disabled attribute to block button activation, because it will
* implicitly set tabindex = -1 and confuse ChromeVox navigation. Clicks handled
* using "pointer-events: none" in CSS.
*/
H5P.Audio.prototype.disableToggleButton = function () {
this.toggleButtonEnabled = false;
if (this.$audioButton) {
this.$audioButton.addClass(H5P.Audio.BUTTON_DISABLED);
}
};
/**
* @public
* Enable button.
*/
H5P.Audio.prototype.enableToggleButton = function () {
this.toggleButtonEnabled = true;
if (this.$audioButton) {
this.$audioButton.removeClass(H5P.Audio.BUTTON_DISABLED);
}
};
/**
* @public
* Check if button is enabled.
* @return {boolean} True, if button is enabled. Else false.
*/
H5P.Audio.prototype.isEnabledToggleButton = function () {
return this.toggleButtonEnabled;
};
/** @constant {string} */
H5P.Audio.BUTTON_DISABLED = 'h5p-audio-disabled';
;
/** @namespace H5P */
H5P.VideoVimeo = (function ($) {
let numInstances = 0;
/**
* Vimeo 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 VimeoPlayer(sources, options, l10n) {
const self = this;
let player;
// Since all the methods of the Vimeo Player SDK are promise-based, we keep
// track of all relevant state variables so that we can implement the
// H5P.Video API where all methods return synchronously.
let buffered = 0;
let currentQuality;
let currentTextTrack;
let currentTime = 0;
let duration = 0;
let isMuted = 0;
let volume = 0;
let playbackRate = 1;
let qualities = [];
let loadingFailedTimeout;
let failedLoading = false;
let ratio = 9/16;
let isLoaded = false;
const LOADING_TIMEOUT_IN_SECONDS = 8;
const id = `h5p-vimeo-${++numInstances}`;
const $wrapper = $('');
const $placeholder = $('', {
id: id,
html: ``
}).appendTo($wrapper);
/**
* Create a new player with the Vimeo Player SDK.
*
* @private
*/
const createVimeoPlayer = async () => {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
// Since the SDK is loaded asynchronously below, explicitly set player to
// null (unlike undefined) which indicates that creation has begun. This
// allows the guard statement above to be hit if this function is called
// more than once.
player = null;
const Vimeo = await loadVimeoPlayerSDK();
const MIN_WIDTH = 200;
const width = Math.max($wrapper.width(), MIN_WIDTH);
const canHasControls = options.controls || self.pressToPlay;
const embedOptions = {
url: sources[0].path,
controls: canHasControls,
responsive: true,
dnt: true,
// Hardcoded autoplay to false to avoid playing videos on init
autoplay: false,
loop: options.loop ? true : false,
playsinline: true,
quality: 'auto',
width: width,
muted: false,
keyboard: canHasControls,
};
// Create a new player
player = new Vimeo.Player(id, embedOptions);
registerVimeoPlayerEventListeneners(player);
// Failsafe timeout to handle failed loading of videos.
// This seems to happen for private videos even though the SDK docs
// suggests to catch PrivacyError when attempting play()
loadingFailedTimeout = setTimeout(() => {
failedLoading = true;
removeLoadingIndicator();
$wrapper.html(`
${l10n.vimeoLoadingError}
`);
$wrapper.css({
width: null,
height: null
});
self.trigger('resize');
self.trigger('error', l10n.vimeoLoadingError);
}, LOADING_TIMEOUT_IN_SECONDS * 1000);
}
const removeLoadingIndicator = () => {
$placeholder.find('div.h5p-video-loading').remove();
};
/**
* Register event listeners on the given Vimeo player.
*
* @private
* @param {Vimeo.Player} player
*/
const registerVimeoPlayerEventListeneners = (player) => {
let isFirstPlay, tracks;
player.on('loaded', async () => {
isFirstPlay = true;
isLoaded = true;
clearTimeout(loadingFailedTimeout);
const videoDetails = await getVimeoVideoMetadata(player);
tracks = videoDetails.tracks.options;
currentTextTrack = tracks.current;
duration = videoDetails.duration;
qualities = videoDetails.qualities;
currentQuality = 'auto';
try {
ratio = videoDetails.dimensions.height / videoDetails.dimensions.width;
}
catch (e) { /* Intentionally ignore this, and fallback on the default ratio */ }
removeLoadingIndicator();
if (options.startAt) {
// Vimeo.Player doesn't have an option for setting start time upon
// instantiation, so we instead perform an initial seek here.
currentTime = await self.seek(options.startAt);
}
self.trigger('ready');
self.trigger('loaded');
self.trigger('qualityChange', currentQuality);
self.trigger('resize');
});
player.on('play', () => {
if (isFirstPlay) {
isFirstPlay = false;
if (tracks.length) {
self.trigger('captions', tracks);
}
}
});
// Handle playback state changes.
player.on('playing', () => self.trigger('stateChange', H5P.Video.PLAYING));
player.on('pause', () => self.trigger('stateChange', H5P.Video.PAUSED));
player.on('ended', () => self.trigger('stateChange', H5P.Video.ENDED));
// Track the percentage of video that has finished loading (buffered).
player.on('progress', (data) => {
buffered = data.percent * 100;
});
// Track the current time. The update frequency may be browser-dependent,
// according to the official docs:
// https://developer.vimeo.com/player/sdk/reference#timeupdate
player.on('timeupdate', (time) => {
currentTime = time.seconds;
});
};
/**
* Get metadata about the video loaded in the given Vimeo player.
*
* Example resolved value:
*
* ```
* {
* "duration": 39,
* "qualities": [
* {
* "name": "auto",
* "label": "Auto"
* },
* {
* "name": "1080p",
* "label": "1080p"
* },
* {
* "name": "720p",
* "label": "720p"
* }
* ],
* "dimensions": {
* "width": 1920,
* "height": 1080
* },
* "tracks": {
* "current": {
* "label": "English",
* "value": "en"
* },
* "options": [
* {
* "label": "English",
* "value": "en"
* },
* {
* "label": "Norsk bokmål",
* "value": "nb"
* }
* ]
* }
* }
* ```
*
* @private
* @param {Vimeo.Player} player
* @returns {Promise}
*/
const getVimeoVideoMetadata = (player) => {
// Create an object for easy lookup of relevant metadata
const massageVideoMetadata = (data) => {
const duration = data[0];
const qualities = data[1].map(q => ({
name: q.id,
label: q.label
}));
const tracks = data[2].reduce((tracks, current) => {
const h5pVideoTrack = new H5P.Video.LabelValue(current.label, current.language);
tracks.options.push(h5pVideoTrack);
if (current.mode === 'showing') {
tracks.current = h5pVideoTrack;
}
return tracks;
}, { current: undefined, options: [] });
const dimensions = { width: data[3], height: data[4] };
return {
duration,
qualities,
tracks,
dimensions
};
};
return Promise.all([
player.getDuration(),
player.getQualities(),
player.getTextTracks(),
player.getVideoWidth(),
player.getVideoHeight(),
]).then(data => massageVideoMetadata(data));
}
try {
if (document.featurePolicy.allowsFeature('autoplay') === false) {
self.pressToPlay = true;
}
}
catch (err) {}
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = ($container) => {
$container.addClass('h5p-vimeo').append($wrapper);
createVimeoPlayer();
};
/**
* Get list of available qualities.
*
* @public
* @returns {Array}
*/
self.getQualities = () => {
return qualities;
};
/**
* Get the current quality.
*
* @returns {String} Current quality identifier
*/
self.getQuality = () => {
return currentQuality;
};
/**
* Set the playback quality.
*
* @public
* @param {String} quality
*/
self.setQuality = async (quality) => {
currentQuality = await player.setQuality(quality);
self.trigger('qualityChange', currentQuality);
};
/**
* Start the video.
*
* @public
*/
self.play = async () => {
if (!player) {
self.on('ready', self.play);
return;
}
try {
await player.play();
}
catch (error) {
switch (error.name) {
case 'PasswordError': // The video is password-protected
self.trigger('error', l10n.vimeoPasswordError);
break;
case 'PrivacyError': // The video is private
self.trigger('error', l10n.vimeoPrivacyError);
break;
default:
self.trigger('error', l10n.unknownError);
break;
}
}
};
/**
* Pause the video.
*
* @public
*/
self.pause = () => {
if (player) {
player.pause();
}
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = async (time) => {
if (!player) {
return;
}
currentTime = time;
await player.setCurrentTime(time);
};
/**
* @public
* @returns {Number} Seconds elapsed since beginning of video
*/
self.getCurrentTime = () => {
return currentTime;
};
/**
* @public
* @returns {Number} Video duration in seconds
*/
self.getDuration = () => {
return duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = () => {
return buffered;
};
/**
* Mute the video.
*
* @public
*/
self.mute = async () => {
isMuted = await player.setMuted(true);
};
/**
* Unmute the video.
*
* @public
*/
self.unMute = async () => {
isMuted = await player.setMuted(false);
};
/**
* Whether the video is muted.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
self.isMuted = () => {
return isMuted;
};
/**
* Whether the video is loaded.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
self.isLoaded = () => {
return isLoaded;
};
/**
* Get the video player's current sound volume.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = () => {
return volume;
};
/**
* Set the video player's sound volume.
*
* @public
* @param {Number} level
*/
self.setVolume = async (level) => {
volume = await player.setVolume(level);
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} Available playback rates
*/
self.getPlaybackRates = () => {
return [0.5, 1, 1.5, 2];
};
/**
* Get the current playback rate.
*
* @public
* @returns {Number} e.g. 0.5, 1, 1.5 or 2
*/
self.getPlaybackRate = () => {
return playbackRate;
};
/**
* Set the current playback rate.
*
* @public
* @param {Number} rate Must be one of available rates from getPlaybackRates
*/
self.setPlaybackRate = async (rate) => {
playbackRate = await player.setPlaybackRate(rate);
self.trigger('playbackRateChange', rate);
};
/**
* Set current captions track.
*
* @public
* @param {H5P.Video.LabelValue} track Captions to display
*/
self.setCaptionsTrack = (track) => {
if (!track) {
return player.disableTextTrack().then(() => {
currentTextTrack = null;
});
}
player.enableTextTrack(track.value).then(() => {
currentTextTrack = track;
});
};
/**
* Get current captions track.
*
* @public
* @returns {H5P.Video.LabelValue}
*/
self.getCaptionsTrack = () => {
return currentTextTrack;
};
self.on('resize', () => {
if (failedLoading || !$wrapper.is(':visible')) {
return;
}
if (player === undefined) {
// Player isn't created yet. Try again.
createVimeoPlayer();
return;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: 'auto'
});
const width = $wrapper[0].clientWidth;
const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio));
// Validate height before setting
if (height > 0) {
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
VimeoPlayer.canPlay = (sources) => {
return getId(sources[0].path);
};
/**
* Find id of Vimeo video from given URL.
*
* @private
* @param {String} url
* @returns {String} Vimeo video ID
*/
const getId = (url) => {
// https://stackoverflow.com/a/11660798
const matches = url.match(/^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/);
if (matches && matches[5]) {
return matches[5];
}
};
/**
* Load the Vimeo Player SDK asynchronously.
*
* @private
* @returns {Promise} Vimeo Player SDK object
*/
const loadVimeoPlayerSDK = async () => {
if (window.Vimeo) {
return await Promise.resolve(window.Vimeo);
}
return await new Promise((resolve, reject) => {
const tag = document.createElement('script');
tag.src = 'https://player.vimeo.com/api/player.js';
tag.onload = () => resolve(window.Vimeo);
tag.onerror = reject;
document.querySelector('script').before(tag);
});
};
return VimeoPlayer;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoVimeo);
;
/** @namespace H5P */
H5P.VideoYouTube = (function ($) {
/**
* YouTube 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 YouTube(sources, options, l10n) {
var self = this;
var player;
var playbackRate = 1;
var id = 'h5p-youtube-' + numInstances;
numInstances++;
var ratio = 9/16;
var $wrapper = $('');
var $placeholder = $('', {
text: l10n.loading,
html: `
`
}).appendTo($wrapper);
// Optional placeholder
// var $placeholder = $('').appendTo($wrapper);
/**
* Use the YouTube API to create a new player
*
* @private
*/
var create = function () {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
if (window.YT === undefined) {
// Load API first
loadAPI(create);
return;
}
if (YT.Player === undefined) {
return;
}
var width = $wrapper.width();
if (width < 200) {
width = 200;
}
var loadCaptionsModule = true;
var videoId = getId(sources[0].path);
player = new YT.Player(id, {
width: width,
height: width * (9/16),
videoId: videoId,
playerVars: {
origin: ORIGIN,
// Hardcoded autoplay to false to avoid playing videos on init
autoplay: 0,
controls: options.controls ? 1 : 0,
disablekb: options.controls ? 0 : 1,
fs: 0,
loop: options.loop ? 1 : 0,
playlist: options.loop ? videoId : undefined,
rel: 0,
showinfo: 0,
iv_load_policy: 3,
wmode: "opaque",
start: Math.floor(options.startAt),
playsinline: 1
},
events: {
onReady: function () {
self.trigger('ready');
self.trigger('loaded');
if (!options.autoplay) {
self.toPause = true;
}
if (options.deactivateSound) {
self.mute();
}
},
onApiChange: function () {
if (loadCaptionsModule) {
loadCaptionsModule = false;
// Always load captions
player.loadModule('captions');
}
var trackList;
try {
// Grab tracklist from player
trackList = player.getOption('captions', 'tracklist');
}
catch (err) {}
if (trackList && trackList.length) {
// Format track list into valid track options
var trackOptions = [];
for (var i = 0; i < trackList.length; i++) {
trackOptions.push(new H5P.Video.LabelValue(trackList[i].displayName, trackList[i].languageCode));
}
// Captions are ready for loading
self.trigger('captions', trackOptions);
}
},
onStateChange: function (state) {
if (state.data > -1 && state.data < 4) {
if (self.toPause) {
// if video buffering, was likely paused already - skip
if (state.data === H5P.Video.BUFFERING) {
delete self.toPause;
}
else {
self.pause();
}
}
// Fix for keeping playback rate in IE11
if (H5P.Video.IE11_PLAYBACK_RATE_FIX && state.data === H5P.Video.PLAYING && playbackRate !== 1) {
// YT doesn't know that IE11 changed the rate so it must be reset before it's set to the correct value
player.setPlaybackRate(1);
player.setPlaybackRate(playbackRate);
}
// End IE11 fix
self.trigger('stateChange', state.data);
}
},
onPlaybackQualityChange: function (quality) {
self.trigger('qualityChange', quality.data);
},
onPlaybackRateChange: function (playbackRate) {
self.trigger('playbackRateChange', playbackRate.data);
},
onError: function (error) {
var message;
switch (error.data) {
case 2:
message = l10n.invalidYtId;
break;
case 100:
message = l10n.unknownYtId;
break;
case 101:
case 150:
message = l10n.restrictedYt;
break;
default:
message = l10n.unknownError + ' ' + error.data;
break;
}
self.trigger('error', message);
}
}
});
player.g.style = "position:absolute;top:0;left:0;width:100%;height:100%;";
};
/**
* Indicates if the video must be clicked for it to start playing.
* For instance YouTube videos on iPad must be pressed to start playing.
*
* @public
*/
if (navigator.userAgent.match(/iPad/i)) {
self.pressToPlay = true;
}
else {
try {
if (document.featurePolicy.allowsFeature('autoplay') === false) {
self.pressToPlay = true;
}
}
catch (err) {}
}
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.addClass('h5p-youtube').append($wrapper);
create();
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
if (!player || !player.getAvailableQualityLevels) {
return;
}
var qualities = player.getAvailableQualityLevels();
if (!qualities.length) {
return; // No qualities
}
// Add labels
for (var i = 0; i < qualities.length; i++) {
var quality = qualities[i];
var label = (LABELS[quality] !== undefined ? LABELS[quality] : 'Unknown'); // TODO: l10n
qualities[i] = {
name: quality,
label: LABELS[quality]
};
}
return qualities;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
if (!player || !player.getPlaybackQuality) {
return;
}
var quality = player.getPlaybackQuality();
return quality === 'unknown' ? undefined : quality;
};
/**
* 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 (!player || !player.setPlaybackQuality) {
return;
}
player.setPlaybackQuality(quality);
};
/**
* Start the video.
*
* @public
*/
self.play = function () {
if (!player || !player.playVideo) {
self.on('ready', self.play);
return;
}
player.playVideo();
};
/**
* Pause the video.
*
* @public
*/
self.pause = function () {
delete self.toPause;
self.off('ready', self.play);
if (!player || !player.pauseVideo) {
return;
}
player.pauseVideo();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (!player || !player.seekTo) {
return;
}
player.seekTo(time, true);
};
/**
* Recreate player with initial time
*
* @public
* @param {Number} time
*/
self.resetPlayback = function (time) {
options.startAt = time;
if (player) {
if (player.getPlayerState() === H5P.Video.PLAYING) {
player.pauseVideo();
self.trigger('stateChange', H5P.Video.PAUSED);
}
player.destroy();
player = undefined;
}
create();
}
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
if (!player || !player.getCurrentTime) {
return;
}
return player.getCurrentTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (!player || !player.getDuration) {
return;
}
return player.getDuration();
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
if (!player || !player.getVideoLoadedFraction) {
return;
}
return player.getVideoLoadedFraction() * 100;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
if (!player || !player.mute) {
return;
}
player.mute();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
if (!player || !player.unMute) {
return;
}
player.unMute();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* Check if video is loaded and ready to play.
*
* @public
* @returns {Boolean}
*/
self.isLoaded = function () {
if (!player || !player.getPlayerState) {
return;
}
return player.getPlayerState() === 5;
};
/**
* Return the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
if (!player || !player.getVolume) {
return;
}
return player.getVolume();
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
if (!player || !player.setVolume) {
return;
}
player.setVolume(level);
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
if (!player || !player.getAvailablePlaybackRates) {
return;
}
var playbackRates = player.getAvailablePlaybackRates();
if (!playbackRates.length) {
return; // No rates, but the array should contain at least 1
}
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 () {
if (!player || !player.getPlaybackRate) {
return;
}
return player.getPlaybackRate();
};
/**
* 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) {
if (!player || !player.setPlaybackRate) {
return;
}
playbackRate = Number(newPlaybackRate);
player.setPlaybackRate(playbackRate);
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
player.setOption('captions', 'track', track ? {languageCode: track.value} : {});
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
var track = player.getOption('captions', 'track');
return (track.languageCode ? new H5P.Video.LabelValue(track.displayName, track.languageCode) : null);
};
// Respond to resize events by setting the YT player size.
self.on('resize', function () {
if (!$wrapper.is(':visible')) {
return;
}
if (!player) {
// Player isn't created yet. Try again.
create();
return;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: 'auto'
});
var width = $wrapper[0].clientWidth;
var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
// Validate height before setting
if (height > 0) {
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
player.setSize(width, height);
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
YouTube.canPlay = function (sources) {
return getId(sources[0].path);
};
/**
* Find id of YouTube video from given URL.
*
* @private
* @param {String} url
* @returns {String} YouTube video identifier
*/
var getId = function (url) {
// Has some false positives, but should cover all regular URLs that people can find
var matches = url.match(/(?:(?:youtube.com\/(?:attribution_link\?(?:\S+))?(?:v\/|embed\/|watch\/|(?:user\/(?:\S+)\/)?watch(?:\S+)v\=))|(?:youtu.be\/|y2u.be\/))([A-Za-z0-9_-]{11})/i);
if (matches && matches[1]) {
return matches[1];
}
};
/**
* Load the IFrame Player API asynchronously.
*/
var loadAPI = function (loaded) {
if (window.onYouTubeIframeAPIReady !== undefined) {
// Someone else is loading, hook in
var original = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = function (id) {
loaded(id);
original(id);
};
}
else {
// Load the API our self
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = loaded;
}
};
/** @constant {Object} */
var LABELS = {
highres: '2160p', // Old API support
hd2160: '2160p', // (New API)
hd1440: '1440p',
hd1080: '1080p',
hd720: '720p',
large: '480p',
medium: '360p',
small: '240p',
tiny: '144p',
auto: 'Auto'
};
/** @private */
var numInstances = 0;
// Extract the current origin (used for security)
var ORIGIN = window.location.href.match(/http[s]?:\/\/[^\/]+/);
ORIGIN = !ORIGIN || ORIGIN[0] === undefined ? undefined : ORIGIN[0];
// ORIGIN = undefined is needed to support fetching file from device local storage
return YouTube;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoYouTube);
;
/** @namespace H5P */
H5P.VideoPanopto = (function ($) {
/**
* Panopto 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 Panopto(sources, options, l10n) {
var self = this;
self.volume = 100;
self.toSeek = undefined;
var player;
var playbackRate = 1;
let canHasAutoplay;
var id = 'h5p-panopto-' + numInstances;
numInstances++;
let isLoaded = false;
let isPlayerReady = false;
var $wrapper = $('');
var $placeholder = $('', {
id: id,
html: '
' + l10n.loading + '
'
}).appendTo($wrapper);
// Determine autoplay/play.
try {
if (document.featurePolicy.allowsFeature('autoplay') !== false) {
canHasAutoplay = true;
}
}
catch (err) {}
/**
* Use the Panopto API to create a new player
*
* @private
*/
var create = function () {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
if (window.EmbedApi === undefined) {
// Load API first
loadAPI(create);
return;
}
var width = $wrapper.width();
if (width < 200) {
width = 200;
}
const videoId = getId(sources[0].path);
player = new EmbedApi(id, {
width: width,
height: width * (9/16),
serverName: videoId[0],
sessionId: videoId[1],
videoParams: { // Optional
interactivity: 'none',
showtitle: false,
autohide: true,
offerviewer: false,
autoplay: false,
showbrand: false,
start: 0,
hideoverlay: !options.controls,
},
events: {
onIframeReady: function () {
isPlayerReady = true;
$placeholder.children(0).text('');
if (options.autoplay && canHasAutoplay) {
player.loadVideo();
isLoaded = true;
}
self.trigger('containerLoaded');
self.trigger('resize'); // Avoid black iframe if loading is slow
},
onReady: function () {
self.videoLoaded = true;
self.trigger('loaded');
if (typeof self.oldTime === 'number') {
self.seek(self.oldTime);
}
else if (typeof self.startAt === 'number' && self.startAt > 0) {
self.seek(self.startAt);
}
if (player.hasCaptions()) {
const captions = [];
const captionTracks = player.getCaptionTracks();
for (trackIndex in captionTracks) {
captions.push(new H5P.Video.LabelValue(captionTracks[trackIndex], trackIndex));
}
// Select active track
currentTrack = player.getSelectedCaptionTrack();
currentTrack = captions[currentTrack] ? captions[currentTrack] : null;
self.trigger('captions', captions);
}
},
onStateChange: function (state) {
if ([H5P.Video.PLAYING, H5P.Video.PAUSED].includes(state) && typeof self.seekToTime === 'number') {
player.seekTo(self.seekToTime);
delete self.seekToTime;
}
// since panopto has different load sequence in IV, need additional condition here
if (self.WAS_RESET) {
self.WAS_RESET = false;
}
// TODO: Playback rate fix for IE11?
if (state > -1 && state < 4) {
self.trigger('stateChange', state);
}
},
onPlaybackRateChange: function () {
self.trigger('playbackRateChange', self.getPlaybackRate());
},
onError: function (error) {
if (error === ApiError.PlayWithSoundNotAllowed) {
// pause and allow user to handle playing
self.pause();
self.unMute(); // because player is automuted on this error
}
else {
self.trigger('error', l10n.unknownError);
}
},
onLoginShown: function () {
$placeholder.children().first().remove(); // Remove loading message
self.trigger('loaded'); // Resize parent
}
}
});
};
/**
* Indicates if the video must be clicked for it to start playing.
* This is always true for Panopto since all videos auto play.
*
* @public
*/
self.pressToPlay = true;
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.addClass('h5p-panopto').append($wrapper);
create();
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
// Not available for Panopto
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
// Not available for Panopto
};
/**
* 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) {
// Not available for Panopto
};
/**
* Start the video.
*
* @public
*/
self.play = function () {
if (!player || !player.playVideo || !isPlayerReady) {
return;
}
if (isLoaded || self.videoLoaded) {
player.playVideo();
}
else {
player.loadVideo(); // Loads and starts playing
isLoaded = true;
}
};
/**
* Pause the video.
*
* @public
*/
self.pause = function () {
if (!player || !player.pauseVideo) {
return;
}
try {
player.pauseVideo();
}
catch (err) {
// Swallow Panopto throwing an error. This has been seen in the authoring
// tool if Panopto has been used inside Iv inside CP
}
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (!player || !player.seekTo || !self.videoLoaded) {
return;
}
if (!player.isReady) {
self.seekToTime = time;
return;
}
player.seekTo(time);
if (self.WAS_RESET) {
// need to check just to be sure, since state === 1 is unusable
delete self.seekToTime;
self.WAS_RESET = false;
}
};
/**
* Recreate player with initial time
*
* @public
* @param {Number} time
*/
self.resetPlayback = function (time) {
if (player && player.isReady && self.videoLoaded) {
self.seek(time);
self.pause();
}
else {
self.seekToTime = time;
}
}
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
if (!player || !player.getCurrentTime) {
return;
}
return player.getCurrentTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (!player || !player.getDuration) {
return;
}
return player.getDuration();
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
// Not available for Panopto
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
if (!player || !player.muteVideo) {
return;
}
player.muteVideo();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
if (!player || !player.unmuteVideo) {
return;
}
player.unmuteVideo();
// The volume is set to 0 when the browser prevents autoplay,
// causing there to be no sound despite unmuting
self.setVolume(self.volume);
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* Check video is loaded and ready to play
*
* @public
* @returns {Boolean}
*/
self.isLoaded = function () {
return isPlayerReady;
};
/**
* Return the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
if (!player || !player.getVolume) {
return;
}
return player.getVolume() * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
if (!player || !player.setVolume) {
return;
}
player.setVolume(level/100);
self.volume = level;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
return [0.25, 0.5, 1, 1.25, 1.5, 2];
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
if (!player || !player.getPlaybackRate) {
return;
}
return player.getPlaybackRate();
};
/**
* 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) {
if (!player || !player.setPlaybackRate) {
return;
}
player.setPlaybackRate(newPlaybackRate);
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
if (!track) {
player.disableCaptions();
currentTrack = null;
}
else {
player.enableCaptions(track.value + '');
currentTrack = track;
}
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
return currentTrack; // No function for getting active caption track?
};
// Respond to resize events by setting the player size.
self.on('resize', function () {
if (!$wrapper.is(':visible')) {
return;
}
if (!player) {
// Player isn't created yet. Try again.
create();
return;
}
$wrapper.removeAttr('font-size');
let width = $wrapper[0].clientWidth;
let height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
const $iframe = $placeholder.children('iframe');
if ($iframe.length) {
$iframe.attr('width', width);
$iframe.attr('height', height);
}
$wrapper.css({
'font-size': 0
});
});
let currentTrack;
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Panopto.canPlay = function (sources) {
return getId(sources[0].path);
};
/**
* Find id of YouTube video from given URL.
*
* @private
* @param {String} url
* @returns {String} Panopto video identifier
*/
var getId = function (url) {
const matches = url.match(/^[^\/]+:\/\/([^\/]*panopto\.[^\/]+)\/Panopto\/.+\?id=(.+)$/);
if (matches && matches.length === 3) {
return [matches[1], matches[2]];
}
};
/**
* Load the IFrame Player API asynchronously.
*/
var loadAPI = function (loaded) {
if (window.onPanoptoEmbedApiReady !== undefined) {
// Someone else is loading, hook in
var original = window.onPanoptoEmbedApiReady;
window.onPanoptoEmbedApiReady = function (id) {
loaded(id);
original(id);
};
}
else {
// Load the API our self
var tag = document.createElement('script');
tag.src = 'https://developers.panopto.com/scripts/embedapi.min.js';
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onPanoptoEmbedApiReady = loaded;
}
};
/** @private */
var numInstances = 0;
return Panopto;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoPanopto);
;
/** @namespace Echo */
H5P.VideoEchoVideo = (() => {
let numInstances = 0;
const CONTROLS_HEIGHT = 100;
/**
* EchoVideo 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 EchoPlayer(sources, options, l10n) {
// State variables for the Player.
let player = undefined;
let buffered = 0;
let currentQuality;
let trackOptions = [];
let currentTime = 0;
let duration = 0;
let isMuted = false;
let loadingComplete = false;
let volume = 1;
let playbackRate = 1;
let qualities = [];
let loadingFailedTimeout;
let failedLoading = false;
let ratio = 9 / 16;
let currentState = H5P.Video.VIDEO_CUED;
// Echo360 server doesn't sync seek time with regular play time fast enough
let timelineUpdatesToSkip = 0;
let timeUpdateTimeout;
/*
* Echo360 player does send time updates ~ 0.25 seconds by default and
* ends playing the video without sending a final time update or an
* Video Ended event. We take care of determining reaching the video end
* ourselves.
*/
const echoMinUncertaintyCompensationS = 0.3;
const timelineUpdateDeltaSlackMS = 50;
let echoUncertaintyCompensationS = echoMinUncertaintyCompensationS;
let previousTickMS;
// Player specific immutable variables.
const LOADING_TIMEOUT_IN_SECONDS = 30;
const id = `h5p-echo-${++numInstances}`;
const instanceId = H5P.createUUID();
const wrapperElement = document.createElement('div');
const placeholderElement = document.createElement('div');
placeholderElement.classList.add('h5p-video-loading');
placeholderElement.setAttribute('style', 'height: 100%; min-height: 200px; display: block; z-index: 100; border: none;');
placeholderElement.setAttribute('aria-label', l10n.loading);
wrapperElement.setAttribute('id', id);
wrapperElement.append(placeholderElement);
/**
* Remove all elements from the placeholder dom element.
*
* @private
*/
const removeLoadingIndicator = () => {
placeholderElement.replaceChildren();
};
/**
* Generate an array of objects for use in a dropdown from the list of resolutions.
* @private
* @param {Array} qualityLevels - list of objects with supported qualities for the media
* @returns {Array} list of objects with label and name properties
*/
const mapQualityLevels = (qualityLevels) => {
const qualities = qualityLevels.map((quality) => {
return { label: quality.label.toLowerCase(), name: quality.value };
});
return qualities;
};
/**
* Register event listeners on the given Echo player.
*
* @private
* @param {HTMLElement} player
*/
const registerEchoPlayerEventListeneners = (player) => {
player.resolveLoading = null;
player.loadingPromise = new Promise((resolve) => {
player.resolveLoading = resolve;
});
player.onload = async () => {
clearTimeout(loadingFailedTimeout);
player.loadingPromise.then(async () => {
this.trigger('ready');
this.trigger('loaded');
this.loadingComplete = true;
this.trigger('resize');
if (trackOptions.length) {
this.trigger('captions', trackOptions);
}
const autoplayIsAllowed = !window.H5PEditor &&
await H5P.Video.isAutoplayAllowed();
if (options.autoplay && autoplayIsAllowed) {
this.play();
}
return true;
});
};
window.addEventListener('message', (event) => {
let message = '';
try {
message = JSON.parse(event.data);
}
catch (e) {
return;
}
if (
message.context !== 'Echo360' || message.instanceId !== instanceId
) {
return;
}
if (message.event === 'init') {
// Set ratio if width and height is received from Echo360
if (message.data.width && message.data.height) {
// If controls are displayed we have to add a magic height to make it visible :(
ratio = ((message.data.height + (options.controls ? CONTROLS_HEIGHT : 0)) / message.data.width);
}
duration = message.data.duration;
this.setCurrentTime(message.data.currentTime ?? 0);
textTracks = message.data.textTracks ?? [];
if (message.data.captions) {
trackOptions = textTracks.map((track) =>
new H5P.Video.LabelValue(track.label, track.value)
);
}
player.resolveLoading();
// Player sends `init` event after rebuffering, unfortunately.
if (!this.wasInitialized) {
qualities = mapQualityLevels(message.data.qualityOptions);
currentQuality = qualities[0].name;
this.trigger('qualityChange', currentQuality);
}
this.trigger('resize');
if (message.data.playing) {
changeState(H5P.Video.PLAYING);
}
this.wasInitialized = true;
}
else if (message.event === 'timeline') {
updateUncertaintyCompensation();
duration = message.data.duration ?? this.getDuration();
if (timelineUpdatesToSkip === 0) {
this.setCurrentTime(message.data.currentTime ?? 0);
}
else {
timelineUpdatesToSkip--;
}
/*
* Should work, but it was better if the player itself clearly sent
* the state (playing, paused, ended) instead of us having to infer.
*/
const compensatedTime = this.getCurrentTime() +
echoUncertaintyCompensationS * this.getPlaybackRate()
if (
currentState === H5P.Video.PLAYING &&
Math.ceil(compensatedTime) >= duration
) {
changeState(H5P.Video.ENDED);
if (options.loop) {
this.seek(0);
this.play();
}
return;
}
if (message.data.playing) {
timeUpdate(currentTime);
changeState(H5P.Video.PLAYING);
}
else if (currentState === H5P.Video.PLAYING) {
// Condition prevents video to be paused on startup
changeState(H5P.Video.PAUSED);
window.clearTimeout(timeUpdateTimeout);
}
}
});
};
/**
* Update the uncertainty compensation value.
* Computes the delta time between the last two timeline events sent by the
* Echo360 player and updates the compensation value.
*/
const updateUncertaintyCompensation = () => {
if (currentState === H5P.Video.PLAYING) {
const time = Date.now();
if (previousTickMS) {
echoUncertaintyCompensationS = Math.max(
echoMinUncertaintyCompensationS,
(time - previousTickMS + timelineUpdateDeltaSlackMS) /
1000
)
} else {
echoUncertaintyCompensationS = echoMinUncertaintyCompensationS;
}
previousTickMS = time;
}
else {
delete previousTickMS;
}
}
/**
* Change state of the player.
* @param {number} state State id (H5P.Video[statename]).
*/
const changeState = (state) => {
if (state !== currentState) {
currentState = state;
this.trigger('stateChange', state);
}
};
/**
* Determine if the element is visible by computing the styles.
*
* @private
* @param {HTMLElement} node - the element to check.
* @returns {Boolean} true if it is visible.
*/
const isNodeVisible = (node) => {
let style = window.getComputedStyle(node);
if (node.offsetWidth === 0) {
return false;
}
return ((style.display !== 'none') && (style.visibility !== 'hidden'));
};
const timeUpdate = (time) => {
window.clearTimeout(timeUpdateTimeout);
this.lastTimeUpdate = Date.now();
timeUpdateTimeout = window.setTimeout(() => {
if (currentState !== H5P.Video.PLAYING) {
return;
}
const delta = (Date.now() - this.lastTimeUpdate) * this.getPlaybackRate();
this.setCurrentTime(currentTime + delta / 1000);
timeUpdate(currentTime);
}, 40); // 25 fps
}
/**
* Create a new player by embedding an iframe.
*
* @private
* @returns {Promise}
*/
const createEchoPlayer = async () => {
if (!isNodeVisible(placeholderElement) || player !== undefined) {
return;
}
// Since the SDK is loaded asynchronously below, explicitly set player to
// null (unlike undefined) which indicates that creation has begun. This
// allows the guard statement above to be hit if this function is called
// more than once.
player = null;
let queryString = '?';
queryString += `instanceId=${instanceId}&`;
if (options.controls) {
queryString += 'controls=true&';
}
if (options.disableFullscreen) {
queryString += 'disableFullscreen=true&';
}
if (options.deactivateSound) {
queryString += 'deactivateSound=true&';
}
if (options.startAt) {
queryString += `startTimeMillis=${Math.round(options.startAt * 1000)}&`;
}
wrapperElement.innerHTML = ``;
player = wrapperElement.firstChild;
// Create a new player
registerEchoPlayerEventListeneners(player);
loadingFailedTimeout = setTimeout(() => {
failedLoading = true;
removeLoadingIndicator();
wrapperElement.innerHTML = `
${l10n.unknownError}
`;
wrapperElement.style.cssText = 'width: null; height: null;';
this.trigger('resize');
this.trigger('error', l10n.unknownError);
}, LOADING_TIMEOUT_IN_SECONDS * 1000);
};
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
this.appendTo = ($container) => {
$container.addClass('h5p-echo').append(wrapperElement);
createEchoPlayer();
};
/**
* Determine if the video has loaded.
*
* @public
* @returns {Boolean}
*/
this.isLoaded = () => {
return loadingComplete;
};
/**
* Get list of available qualities.
*
* @public
* @returns {Array}
*/
this.getQualities = () => {
return qualities;
};
/**
* Get the current quality.
*
* @public
* @returns {String} Current quality identifier
*/
this.getQuality = () => {
return currentQuality;
};
/**
* Set the playback quality.
*
* @public
* @param {String} quality
*/
this.setQuality = async (quality) => {
this.post('quality', quality);
currentQuality = quality;
this.trigger('qualityChange', currentQuality);
};
/**
* Start the video.
*
* @public
*/
this.play = () => {
if (!player) {
this.on('ready', this.play);
return;
}
this.post('play', 0);
};
/**
* Pause the video.
*
* @public
*/
this.pause = () => {
// Compensate for Echo360's delayed time updates
timelineUpdatesToSkip = 1;
this.post('pause', 0);
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
this.seek = (time) => {
this.post('seek', time);
this.setCurrentTime(time);
// Compensate for Echo360's delayed time updates
timelineUpdatesToSkip = 1;
};
/**
* Post a window message to the iframe.
*
* @public
* @param event
* @param data
*/
this.post = (event, data) => {
player?.contentWindow?.postMessage(
JSON.stringify({
event: event,
context: 'Echo360',
instanceId: instanceId,
data: data
}),
'*'
);
};
/**
* Return the current play position.
*
* @public
* @returns {Number} Seconds elapsed since beginning of video
*/
this.getCurrentTime = () => {
return currentTime;
};
/**
* Set current time.
* @param {number} timeS Time in seconds.
*/
this.setCurrentTime = (timeS) => {
currentTime = timeS;
}
/**
* Return the video duration.
*
* @public
* @returns {?Number} Video duration in seconds
*/
this.getDuration = () => {
if (duration > 0) {
return duration;
}
return null;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
this.getBuffered = () => {
return buffered;
};
/**
* Mute the video.
*
* @public
*/
this.mute = () => {
this.post('mute', 0);
isMuted = true;
};
/**
* Unmute the video.
*
* @public
*/
this.unMute = () => {
this.post('unmute', 0);
isMuted = false;
};
/**
* Whether the video is muted.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
this.isMuted = () => {
return isMuted;
};
/**
* Get the video player's current sound volume.
*
* @public
* @returns {Number} Between 0 and 100.
*/
this.getVolume = () => {
return volume;
};
/**
* Set the video player's sound volume.
*
* @public
* @param {Number} level
*/
this.setVolume = (level) => {
this.post('volume', level);
volume = level;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} Available playback rates
*/
this.getPlaybackRates = () => {
return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
};
/**
* Get the current playback rate.
*
* @public
* @returns {Number} e.g. 0.5, 1, 1.5 or 2
*/
this.getPlaybackRate = () => {
return playbackRate;
};
/**
* Set the current playback rate.
*
* @public
* @param {Number} rate Must be one of available rates from getPlaybackRates
*/
this.setPlaybackRate = async (rate) => {
const echoRate = parseFloat(rate);
this.post('playbackrate', echoRate);
playbackRate = rate;
this.trigger('playbackRateChange', rate);
};
/**
* Set current captions track.
*
* @public
* @param {H5P.Video.LabelValue} track Captions to display
*/
this.setCaptionsTrack = (track) => {
const echoCaption = trackOptions.find(
(trackItem) => track?.value === trackItem.value
);
trackOptions.forEach(trackItem => {
trackItem.mode = (trackItem === echoCaption) ? 'showing' : 'disabled';
});
this.post('captions', echoCaption ? echoCaption.value : 'off');
};
/**
* Get current captions track.
*
* @public
* @returns {H5P.Video.LabelValue|null} Current captions track.
*/
this.getCaptionsTrack = () => {
return trackOptions.find(
(trackItem) => trackItem.mode === 'showing'
) ?? null;
};
this.on('resize', () => {
if (failedLoading || !isNodeVisible(wrapperElement)) {
return;
}
if (player === undefined) {
// Player isn't created yet. Try again.
createEchoPlayer();
return;
}
// Use as much space as possible
wrapperElement.style.cssText = 'width: 100%; height: 100%;';
const width = wrapperElement.clientWidth;
const height = options.fit ? wrapperElement.clientHeight : (width * (ratio));
// Validate height before setting
if (height > 0) {
// Set size
wrapperElement.style.cssText = 'width: ' + width + 'px; height: ' + height + 'px;';
}
});
}
/**
* Find id of video from given URL.
*
* @private
* @param {String} url
* @returns {String} Echo video identifier
*/
const getId = (url) => {
const matches = url.match(/^[^/]+:\/\/(echo360[^/]+)\/media\/([^/]+)\/h5p.*$/i);
if (matches && matches.length === 3) {
return [matches[2], matches[2]];
}
};
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
EchoPlayer.canPlay = (sources) => {
return getId(sources[0].path);
};
return EchoPlayer;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoEchoVideo);
;
/** @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 = $('', {
'class': 'h5p-video-loading'
});
/**
* Used to display error messages
* @private
*/
var $error = $('', {
'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);
;
/** @namespace H5P */
H5P.Video = (function ($, ContentCopyrights, MediaCopyright, handlers) {
/**
* The ultimate H5P video player!
*
* @class
* @param {Object} parameters Options for this library.
* @param {Object} parameters.visuals Visual options
* @param {Object} parameters.playback Playback options
* @param {Object} parameters.a11y Accessibility options
* @param {Boolean} [parameters.startAt] Start time of video
* @param {Number} id Content identifier
* @param {Object} [extras] Extra parameters.
*/
function Video(parameters, id, extras = {}) {
var self = this;
self.oldTime = extras.previousState?.time;
self.contentId = id;
self.WAS_RESET = false;
self.startAt = parameters.startAt || 0;
// Ref youtube.js - ipad & youtube - issue
self.pressToPlay = false;
// Reference to the handler
var handlerName = '';
// Initialize event inheritance
H5P.EventDispatcher.call(self);
// Default language localization
parameters = $.extend(true, parameters, {
l10n: {
name: 'Video',
loading: 'Video player loading...',
noPlayers: 'Found no video players that supports the given video format.',
noSources: 'Video source is missing.',
aborted: 'Media playback has been aborted.',
networkFailure: 'Network failure.',
cannotDecode: 'Unable to decode media.',
formatNotSupported: 'Video format not supported.',
mediaEncrypted: 'Media encrypted.',
unknownError: 'Unknown error.',
vimeoPasswordError: 'Password-protected Vimeo videos are not supported.',
vimeoPrivacyError: 'The Vimeo video cannot be used due to its privacy settings.',
vimeoLoadingError: 'The Vimeo video could not be loaded.',
invalidYtId: 'Invalid YouTube ID.',
unknownYtId: 'Unable to find video with the given YouTube ID.',
restrictedYt: 'The owner of this video does not allow it to be embedded.'
}
});
parameters.a11y = parameters.a11y || [];
parameters.playback = parameters.playback || {};
parameters.visuals = $.extend(
true, { disableFullscreen: false }, parameters.visuals
);
/** @private */
var sources = [];
if (parameters.sources) {
for (var i = 0; i < parameters.sources.length; i++) {
// Clone to avoid changing of parameters.
var source = $.extend(true, {}, parameters.sources[i]);
// Create working URL without html entities.
source.path = $cleaner.html(source.path).text();
sources.push(source);
}
}
/** @private */
var tracks = [];
parameters.a11y.forEach(function (track) {
// Clone to avoid changing of parameters.
var clone = $.extend(true, {}, track);
// Create working URL without html entities
if (clone.track && clone.track.path) {
clone.track.path = $cleaner.html(clone.track.path).text();
tracks.push(clone);
}
});
/**
* Handle autoplay. If autoplay is disabled, it will still autopause when
* video is not visible.
*
* @param {*} $container
*/
const handleAutoPlayPause = function ($container) {
// Keep the current state
let state;
self.on('stateChange', function(event) {
state = event.data;
});
// Keep record of autopauses.
// I.e: we don't wanna autoplay if the user has excplicitly paused.
self.autoPaused = !self.pressToPlay;
new IntersectionObserver(function (entries) {
const entry = entries[0];
// This video element became visible
if (entry.isIntersecting) {
// Autoplay if autoplay is enabled and it was not explicitly
// paused by a user
if (parameters.playback.autoplay && self.autoPaused) {
self.autoPaused = false;
self.play();
}
}
else if (state !== Video.PAUSED && state !== Video.ENDED) {
self.autoPaused = true;
self.pause();
}
}, {
root: null,
threshold: [0, 1] // Get events when it is shown and hidden
}).observe($container.get(0));
};
/**
* Attaches the video handler to the given container.
* Inserts text if no handler is found.
*
* @public
* @param {jQuery} $container
*/
self.attach = function ($container) {
$container.addClass('h5p-video').html('');
if (self.appendTo !== undefined) {
self.appendTo($container);
// Avoid autoplaying in authoring tool
if (window.H5PEditor === undefined) {
handleAutoPlayPause($container);
}
}
else if (sources.length) {
$container.text(parameters.l10n.noPlayers);
}
else {
$container.text(parameters.l10n.noSources);
}
};
/**
* Get name of the video handler
*
* @public
* @returns {string}
*/
self.getHandlerName = function() {
return handlerName;
};
/**
* @public
* Get current state for resume support.
*
* @returns {object} Current state.
*/
self.getCurrentState = function () {
if (self.getCurrentTime) {
return {
time: self.getCurrentTime() || self.oldTime,
};
}
};
/**
* The two functions below needs to be defined in this base class,
* since it is used in this class even if no handler was found.
*/
self.seek = () => {};
self.pause = () => {};
/**
* @public
* Reset current state (time).
*
*/
self.resetTask = function () {
delete self.oldTime;
self.resetPlayback(parameters.startAt || 0);
};
/**
* Default implementation of resetPlayback. May be overridden by sub classes.
*
* @param {*} startAt
*/
self.resetPlayback = startAt => {
self.seek(startAt);
self.pause();
self.WAS_RESET = true;
};
// Resize the video when we know its aspect ratio
self.on('loaded', function () {
self.trigger('resize');
// reset time if wasn't done immediately
if (self.WAS_RESET) {
self.seek(parameters.startAt || 0);
if (!parameters.playback.autoplay) {
self.pause();
}
self.WAS_RESET = false;
}
});
// Find player for video sources
if (sources.length) {
const options = {
controls: parameters.visuals.controls,
autoplay: parameters.playback.autoplay,
loop: parameters.playback.loop,
fit: parameters.visuals.fit,
poster: parameters.visuals.poster === undefined ? undefined : parameters.visuals.poster,
tracks: tracks,
disableRemotePlayback: parameters.visuals.disableRemotePlayback === true,
disableFullscreen: parameters.visuals.disableFullscreen === true,
deactivateSound: parameters.playback.deactivateSound,
}
if (!self.WAS_RESET) {
options.startAt = self.oldTime !== undefined ? self.oldTime : (parameters.startAt || 0);
}
var html5Handler;
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i];
if (handler.canPlay !== undefined && handler.canPlay(sources)) {
handler.call(self, sources, options, parameters.l10n);
handlerName = handler.name;
return;
}
if (handler === H5P.VideoHtml5) {
html5Handler = handler;
handlerName = handler.name;
}
}
// Fallback to trying HTML5 player
if (html5Handler) {
html5Handler.call(self, sources, options, parameters.l10n);
}
}
}
// Extends the event dispatcher
Video.prototype = Object.create(H5P.EventDispatcher.prototype);
Video.prototype.constructor = Video;
// Player states
/** @constant {Number} */
Video.ENDED = 0;
/** @constant {Number} */
Video.PLAYING = 1;
/** @constant {Number} */
Video.PAUSED = 2;
/** @constant {Number} */
Video.BUFFERING = 3;
/**
* When video is queued to start
* @constant {Number}
*/
Video.VIDEO_CUED = 5;
// Used to convert between html and text, since URLs have html entities.
var $cleaner = H5P.jQuery('');
/**
* Help keep track of key value pairs used by the UI.
*
* @class
* @param {string} label
* @param {string} value
*/
Video.LabelValue = function (label, value) {
this.label = label;
this.value = value;
};
/**
* Determine whether video can be autoplayed.
* @returns {Promise} Whether autoplay is allowed.
*/
Video.isAutoplayAllowed = async () => {
if (document.featurePolicy?.allowsFeature('autoplay')) {
return true; // Browser supports `featurePolicy` and can tell directly
}
const video = document.createElement('video');
/*
* Without a video source, the play Promise will be rejected with an error
* if it cannot be autoplayed, but not resolve at all if it can be
* autoplayed. Using a timeout to detect the latter case here.
*/
const timeoutMs = 50; // If play promise rejects, then within few ms
const timeoutPromise = new Promise((resolve) => {
window.setTimeout(() => {
resolve(true); // Timeout reached, autoplay is allowed
}, timeoutMs);
});
let result;
try {
result = (await Promise.race([video.play(), timeoutPromise])) ?? true;
} catch (error) {
result = false;
}
return result;
};
/** @constant {Boolean} */
Video.IE11_PLAYBACK_RATE_FIX = (navigator.userAgent.match(/Trident.*rv[ :]*11\./) ? true : false);
return Video;
})(H5P.jQuery, H5P.ContentCopyrights, H5P.MediaCopyright, H5P.videoHandlers || []);
;
!function(){"use strict";var e=JSON.parse('[{"name":"modeDoorImage","type":"select","label":"Mode for door images","description":"Select whether you want to set each custom door image yourself or let H5P do the work for you based on the calendar background image set in the behavioural settings.","options":[{"value":"manual","label":"I want to set each door image myself"},{"value":"automatic","label":"H5P shall set the door images based on the background image"}],"default":"automatic"},{"name":"doors","type":"list","label":"Doors","entity":"door","min":24,"max":24,"field":{"name":"contentGroup","type":"group","fields":[{"name":"type","type":"select","label":"Content type","description":"Content type that shoud optionally pop up when the door is opened.","options":[{"value":"audio","label":"Audio"},{"value":"image","label":"Image"},{"value":"link","label":"Link"},{"value":"text","label":"Text"},{"value":"video","label":"Video"}]},{"name":"audio","type":"audio","label":"Audio","widget":"showWhen","showWhen":{"rules":[{"field":"type","equals":"audio"}]}},{"name":"image","type":"library","label":"Image","options":["H5P.Image 1.1"],"widget":"showWhen","showWhen":{"rules":[{"field":"type","equals":"image"}]}},{"name":"link","type":"library","label":"Link","options":["H5P.Link 1.3"],"widget":"showWhen","showWhen":{"rules":[{"field":"type","equals":"link"}]}},{"name":"text","type":"library","label":"Text","options":["H5P.AdvancedText 1.1"],"widget":"showWhen","showWhen":{"rules":[{"field":"type","equals":"text"}]}},{"name":"video","type":"video","label":"Video","widget":"showWhen","showWhen":{"rules":[{"field":"type","equals":"video"}]}},{"name":"autoplay","type":"boolean","label":"Autoplay","default":false,"widget":"showWhen","showWhen":{"type":"or","rules":[{"field":"type","equals":"audio"},{"field":"type","equals":"video"}]}},{"name":"doorCover","type":"image","label":"Door image","description":"Image that will be used for the door. Needs to have a size ratio of 1:1 if you want the left half fit the right half.","optional":true,"widget":"showWhen","showWhen":{"rules":[{"field":"../../modeDoorImage","equals":"manual"}]}},{"name":"previewImage","type":"image","label":"Background image","description":"Image that should appear inside the door. Will be the door\'s number by default.","optional":true}]},"widgets":[{"name":"VerticalTabs","label":"Default"}]},{"name":"visuals","type":"group","label":"Visual settings","description":"These options will let you configure the visual appearance.","importance":"low","fields":[{"name":"backgroundImage","type":"image","label":"Calendar background image","optional":true},{"name":"doorImageTemplate","type":"image","label":"Door image template","description":"If an image is set, it will be used for every door unless a specific door image is set for a single door.","optional":true,"widget":"showWhen","showWhen":{"rules":[{"field":"../modeDoorImage","equals":"manual"}]}},{"name":"hideDoorBorder","type":"boolean","label":"Hide door border","default":false,"optional":true},{"name":"hideNumbers","type":"boolean","label":"Hide door numbers","default":false,"optional":true},{"name":"hideDoorKnobs","type":"boolean","label":"Hide door knobs","default":false,"optional":true},{"name":"hideDoorFrame","type":"boolean","label":"Hide door frame","default":false,"optional":true},{"name":"snow","type":"boolean","label":"Let it snow","description":"Will add some snow falling in front of the calendar. It never rains in Southern California, it never snows on IE11.","default":false,"optional":true}]},{"name":"audio","type":"group","label":"Audio settings","description":"These options will let you configure the audio appearance.","importance":"low","fields":[{"name":"backgroundMusic","type":"audio","label":"Background music","optional":true},{"name":"autoplay","type":"boolean","label":"Autoplay background music","description":"If set, the background music will play automatically once the content is opened. Please note: Some browsers\' media policy may prevent autoplay.","default":false,"optional":true}]},{"name":"behaviour","type":"group","label":"Behavioural settings","description":"These options will let you override behaviour settings.","importance":"low","fields":[{"name":"modeDoorPlacement","type":"select","label":"Mode for door placement","description":"Select whether you want to set a fixed number of columns and rows for the doors or if H5P should decide dynamically depending on the available space. Note that the latter may interfere with the position of the background image and custom door cover images that should be at a particular position.","options":[{"value":"fixed","label":"Fixed"},{"value":"dynamic","label":"Dynamic"}],"default":"dynamic"},{"name":"doorPlacementRatio","type":"select","label":"Row-to-column ratio","description":"Select how many columns and rows the calender should use.","options":[{"value":"6x4","label":"6 × 4"},{"value":"4x6","label":"4 × 6"}],"default":"6x4","widget":"showWhen","showWhen":{"rules":[{"field":"modeDoorPlacement","equals":"fixed"}]}},{"name":"randomize","type":"boolean","label":"Random order","description":"Shuffle the order of the doors. If the \\"save content state\\" option is set in the H5P settings, that order will stay the same when the user returns later.","default":false,"optional":true},{"name":"keepImageOrder","type":"boolean","label":"Keep order of images","description":"Shuffle doors, but keep the door cover images at their fixed positions starting with 1 in the upper left corner down to 24 in the lower right corner.","default":false,"optional":true,"widget":"showWhen","showWhen":{"rules":[{"field":"randomize","equals":true}]}},{"name":"designMode","type":"boolean","label":"Design mode","description":"When in design mode, all doors can be opened. Otherwise, the doors can only be opened in December on and after the respective day indicated by the door number.","default":true,"optional":true}]},{"name":"l10n","type":"group","common":true,"label":"Localization","importance":"low","fields":[{"name":"nothingToSee","type":"text","label":"Nothing to see","importance":"low","default":"There is nothing to see here!🎄"},{"name":"dummy","type":"text","label":"Dummy","importance":"low","default":"Dummy","widget":"none"}]},{"name":"a11y","type":"group","common":true,"label":"Accessibility","importance":"low","fields":[{"name":"door","type":"text","label":"Door","importance":"low","default":"Door"},{"name":"locked","type":"text","label":"Locked","importance":"low","default":"Locked. It is not time to open this one yet."},{"name":"content","description":"Announce the content. @door is a variable and will be replaced with the related door description.","type":"text","label":"Content of","importance":"low","default":"Content of @door"},{"name":"mute","type":"text","label":"Mute audio","importance":"low","default":"Mute audio"},{"name":"unmute","type":"text","label":"Unmute audio","importance":"low","default":"Unmute audio"},{"name":"closeWindow","type":"text","label":"Close window","importance":"low","default":"Close window"}]}]');let t=function(){function e(){}return e.extend=function(){for(let e=1;e0;o--)t=Math.floor(Math.random()*(o+1)),a=e[o],e[o]=e[t],e[t]=a},e.findSemanticsField=function(t,a){if(Array.isArray(a)){for(let o=0;o0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.params=t,this.callbacks=a.extend({onOpened:()=>{},onLoaded:()=>{}},o),this.opened=!1,this.toLoad=["door","previewImage"],this.container=document.createElement("div"),this.container.classList.add("h5p-advent-calendar-square-content"),this.container.classList.add(`h5p-advent-calendar-color-scheme-${e.colorSchemeNames[t.day%e.colorSchemeNames.length]}`),t.hideDoorBorder&&this.container.classList.add("h5p-advent-calendar-hide-door-border"),t.hideDoorFrame&&this.container.classList.add("h5p-advent-calendar-hide-door-frame"),this.container.setAttribute("role","button"),this.container.setAttribute("aria-label",t.day),this.container.setAttribute("tabIndex",0);let n=`${this.params.a11y.door} ${this.params.day}.`;this.canBeOpened()||(n+=this.params.a11y.locked),this.container.setAttribute("aria-label",n),this.canBeOpened()||this.container.classList.add("h5p-advent-calendar-disabled"),this.container.addEventListener("click",(e=>{this.handleClick(e)})),this.container.addEventListener("keypress",(e=>{this.handleKeypress(e)}));const i=document.createElement("div");i.classList.add("h5p-advent-calendar-door-container"),this.container.appendChild(i);const s=document.createElement("div");s.classList.add("h5p-advent-calendar-doorway"),s.classList.add("h5p-advent-calendar-left"),i.appendChild(s);const r=document.createElement("div");if(r.classList.add("h5p-advent-calendar-doorway"),r.classList.add("h5p-advent-calendar-right"),i.appendChild(r),this.doorLeft=document.createElement("div"),this.doorLeft.classList.add("h5p-advent-calendar-door"),this.doorLeft.classList.add("h5p-advent-calendar-left"),s.appendChild(this.doorLeft),!t.hideNumbers){const e=document.createElement("div");e.classList.add("h5p-advent-calendar-door-number"),e.innerText=t.day,this.doorLeft.appendChild(e)}if(!t.hideDoorKnobs){const e=document.createElement("div");e.classList.add("h5p-advent-calendar-doorknob"),this.doorLeft.appendChild(e)}if(this.doorRight=document.createElement("div"),this.doorRight.classList.add("h5p-advent-calendar-door"),this.doorRight.classList.add("h5p-advent-calendar-right"),r.appendChild(this.doorRight),!t.hideDoorKnobs){const e=document.createElement("div");e.classList.add("h5p-advent-calendar-doorknob"),this.doorRight.appendChild(e)}if(this.params.content.doorCover&&this.params.content.doorCover.path){const e=H5P.getPath(this.params.content.doorCover.path,this.params.contentId);if(e){let t=document.createElement("img");t.src=e,t.addEventListener("load",(()=>{this.doorLeft.style.backgroundImage=`url("${e}")`,this.doorRight.style.backgroundImage=`url("${e}")`,t=null,this.handleLoaded("door")})),this.container.classList.add("h5p-advent-calendar-cover-image")}}else this.toLoad=this.toLoad.filter((e=>"door"!==e));if(this.previewImage=document.createElement("button"),this.previewImage.setAttribute("aria-label",this.params.a11y.content.replace("@door",`${this.params.a11y.door} ${this.params.day}.`)),this.previewImage.setAttribute("tabIndex",-1),this.previewImage.classList.add("h5p-advent-calendar-preview-image"),this.previewImage.classList.add(`h5p-advent-calendar-${t.content.type}-symbol`),this.container.appendChild(this.previewImage),this.params.content.previewImage&&this.params.content.previewImage.path){const e=H5P.getPath(this.params.content.previewImage.path,this.params.contentId);if(e){let t=document.createElement("img");t.src=e,t.addEventListener("load",(()=>{this.previewImage.style.backgroundImage=`url("${e}")`,t=null,this.handleLoaded("previewImage")}))}}else this.toLoad=this.toLoad.filter((e=>"previewImage"!==e)),this.previewImage.innerText=t.day;0===this.toLoad.length&&this.callbacks.onLoaded(),this.previewImage.addEventListener("click",(()=>{this.open({delay:0})})),t.open&&this.open({skipCallback:!0})}var t=e.prototype;return t.getDOM=function(){return this.container},t.setDoorCover=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};e=a.extend({styles:{},offset:{left:0,top:0}},e),e.image=e.image||{src:""},this.container.classList.toggle("h5p-advent-calendar-cover-image",""!==e.image.src),this.doorLeft.style.backgroundImage=`url("${e.image.src}")`,this.doorRight.style.backgroundImage=`url("${e.image.src}")`;const t=this.doorLeft.getBoundingClientRect();this.doorLeft.style.backgroundPosition=`left ${e.offset.left-t.left}px top ${e.offset.top-t.top}px`;const o=this.doorRight.getBoundingClientRect(),n=getComputedStyle(this.doorRight),i=parseFloat(n.getPropertyValue("border-left").split(" ")[0]);this.doorRight.style.backgroundPosition=`left ${e.offset.left-o.left-i}px top ${e.offset.top-o.top}px`;for(let t in e.styles)this.doorLeft.style[t]=e.styles[t],this.doorRight.style[t]=e.styles[t]},t.handleKeypress=function(e){13!==e.keyCode&&32!==e.keyCode||this.handleClick(e)},t.handleClick=function(e){this.canBeOpened()&&!this.isOpen()&&(e.preventDefault(),this.open(),this.container.removeEventListener("click",(e=>{this.handleClick(e)})),this.container.removeEventListener("keypress",(e=>{this.handleKeypress(e)})))},t.handleLoaded=function(e){this.toLoad=this.toLoad.filter((t=>t!==e)),0===this.toLoad.length&&this.callbacks.onLoaded()},t.focus=function(){this.isOpen()?this.previewImage.focus():this.container.focus()},t.open=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.opened=!0,this.container.classList.add("h5p-advent-calendar-open"),this.container.setAttribute("tabIndex",-1),this.container.removeAttribute("role"),this.previewImage.setAttribute("tabIndex",0),e.skipCallback||this.callbacks.onOpened(this.params.day,e.delay)},t.lock=function(){this.locked=!0,this.container.classList.add("h5p-advent-calendar-disabled")},t.unlock=function(){this.container.classList.remove("h5p-advent-calendar-disabled"),this.locked=!1},t.isOpen=function(){return this.opened},t.canBeOpened=function(){if(this.locked)return!1;if(this.params.designMode)return!0;const e=new Date;return 11===e.getMonth()&&e.getDate()>=this.params.day},e}();o.colorSchemeNames=["red","white","lightgreen","darkgreen"];let n=function(){function e(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.params=a.extend({container:document.body,content:document.createElement("div"),styleBase:"h5p-advent-calendar-overlay",position:{offsetHorizontal:0,offsetVertical:0},a11y:{closeWindow:"Close"}},e),this.callbacks=a.extend({onClose:()=>{}},t),this.isVisible=!1,this.focusableElements=[],this.overlay=document.createElement("div"),this.overlay.classList.add(`${this.params.styleBase}-outer-wrapper`),this.overlay.classList.add("h5p-advent-calendar-invisible"),this.overlay.setAttribute("role","dialog"),this.params.a11y.title&&this.overlay.setAttribute("aria-label",this.params.a11y.title),this.overlay.setAttribute("aria-modal","true"),this.content=document.createElement("div"),this.content.classList.add(`${this.params.styleBase}-content`),this.content.appendChild(this.params.content),this.overlay.appendChild(this.content),this.buttonClose=document.createElement("button"),this.buttonClose.classList.add(`${this.params.styleBase}-button-close`),this.buttonClose.setAttribute("aria-label",this.params.a11y.closeWindow),this.buttonClose.addEventListener("click",(()=>{this.callbacks.onClose()})),this.overlay.appendChild(this.buttonClose),document.addEventListener("focus",(e=>{this.isVisible&&0!==this.focusableElements.length&&this.trapFocus(e)}),!0),this.blocker=document.createElement("div"),this.blocker.classList.add("h5p-advent-calendar-overlay-blocker"),this.blocker.classList.add("h5p-advent-calendar-display-none"),this.blocker.addEventListener("click",(()=>{this.callbacks.onClose()})),this.modifierClasses=[]}var t=e.prototype;return t.getDOM=function(){return this.overlay},t.setContent=function(e){for(;this.content.firstChild;)this.content.removeChild(this.content.firstChild);this.content.appendChild(e)},t.setModifierClass=function(e){(!(arguments.length>1&&void 0!==arguments[1])||arguments[1])&&(this.modifierClasses.forEach((e=>{this.overlay.classList.remove(e)})),this.modifierClasses=[]),-1===this.modifierClasses.indexOf(e)&&this.modifierClasses.push(e),this.overlay.classList.add(e)},t.trapFocus=function(e){this.isChild(e.target)?this.currentFocusElement=e.target:(this.currentFocusElement===this.focusableElements[0]?this.currentFocusElement=this.focusableElements[this.focusableElements.length-1]:this.currentFocusElement=this.focusableElements[0],this.currentFocusElement.focus())},t.isChild=function(e){const t=e.parentNode;return!!t&&(t===this.overlay||this.isChild(t))},t.updateFocusableElements=function(){this.focusableElements=[].slice.call(this.overlay.querySelectorAll('video, audio, button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter((e=>"true"!==e.getAttribute("disabled")&&!0!==e.getAttribute("disabled")))},t.show=function(){this.blockerAppended||(this.container=document.body.querySelector(".h5p-container"),this.container.appendChild(this.blocker)),this.blockerAppended=!0,this.overlay.classList.remove("h5p-advent-calendar-invisible"),this.blocker.classList.remove("h5p-advent-calendar-display-none"),setTimeout((()=>{this.updateFocusableElements(),this.focusableElements.length>0&&this.focusableElements[0].focus(),this.isVisible=!0}),0)},t.hide=function(){this.isVisible=!1,this.overlay.classList.add("h5p-advent-calendar-invisible"),this.blocker.classList.add("h5p-advent-calendar-display-none")},e}();var i=function(){function e(e){this.classNameBase=e,this.container=document.createElement("div"),this.container.classList.add(`${this.classNameBase}-container`),this.spinnerElement=document.createElement("div"),this.spinnerElement.classList.add(e);const t=document.createElement("div");t.classList.add(`${this.classNameBase}-circle-head`),this.spinnerElement.appendChild(t);const a=document.createElement("div");a.classList.add(`${this.classNameBase}-circle-neck-upper`),this.spinnerElement.appendChild(a);const o=document.createElement("div");o.classList.add(`${this.classNameBase}-circle-neck-lower`),this.spinnerElement.appendChild(o);const n=document.createElement("div");n.classList.add(`${this.classNameBase}-circle-body`),this.spinnerElement.appendChild(n),this.container.appendChild(this.spinnerElement)}var t=e.prototype;return t.getDOM=function(){return this.container},t.show=function(){this.container.classList.remove(`${this.classNameBase}-none`)},t.hide=function(){this.container.classList.add(`${this.classNameBase}-none`)},e}();function s(e,t){return s=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},s(e,t)}let r=function(t){var r,d;function l(){var s;let r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},d=arguments.length>1?arguments[1]:void 0,l=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};(s=t.call(this)||this).params=a.extend({modeDoorImage:"automatic",visuals:{hideDoorBorder:!1,hideNumbers:!1,hideDoorKnobs:!1,hideDoorFrame:!1,snow:!1},audio:{autoplay:!1},behaviour:{modeDoorPlacement:"fixed",doorPlacementRatio:"6x4",randomize:!1,keepImageOrder:!1},l10n:{nothingToSee:"There is nothing to see here!🎄"},a11y:{door:"Door",locked:"Locked. It is not time to open this one yet.",content:"Content of @door",closeWindow:"Close window",mute:"Mute audio",unmute:"Unmute audio"}},r),s.doorsLoaded=0,s.continueBackgroundMusic=null;for(let e in s.params.l10n)s.params.l10n[e]=a.htmlDecode(s.params.l10n[e]);for(let e in s.params.a11y)s.params.a11y[e]=a.htmlDecode(s.params.a11y[e]);const c=a.findSemanticsField("text",e),h=c?.options[0]||"H5P.AdvancedText 1.1";for(;s.params.doors.length<24;)s.params.doors.push({text:{library:h,params:{text:s.params.l10n.nothingToSee},subContentId:H5P.createUUID()},type:"text"});const u=r.visuals.doorImageTemplate?.path?r.visuals.doorImageTemplate:null;if(r.visuals?.backgroundImage?.path){const e=H5P.getPath(r.visuals.backgroundImage.path,d);e&&(s.backgroundImage=document.createElement("img"),s.backgroundImage.addEventListener("load",(()=>{s.trigger("resize")})),s.backgroundImage.src=e)}s.columns=[],s.doors=s.params.doors.map(((e,t)=>(u&&!e.doorCover&&(e.doorCover=u),e.type||(e.type="text",e.text.params.text=s.params.l10n.nothingToSee),{day:t+1,content:e}))),l.previousState&&Array.isArray(l.previousState)?s.doors=l.previousState.map((e=>{const t=s.doors[e.day-1];return t.open=e.open,t})):r.behaviour.randomize&&a.shuffleArray(s.doors);const p=s.doors.map((e=>({day:e.day,doorCover:e.content.doorCover})));r.behaviour.keepImageOrder&&s.doors.forEach(((e,t)=>{e.content.doorCover=p.filter((e=>e.day===t+1))[0].doorCover})),s.instances=Array(25),s.muted=!s.params.audio.autoplay,s.container=document.createElement("div"),s.container.classList.add("h5p-advent-calendar-container"),s.spinner=new i("h5p-advent-calendar-spinner"),s.container.appendChild(s.spinner.getDOM()),s.table=document.createElement("div"),s.table.classList.add("h5p-advent-calendar-table"),s.table.classList.add("h5p-advent-calendar-display-none"),s.backgroundImage&&(s.table.style.backgroundImage=`url('${s.backgroundImage.src}')`),s.container.appendChild(s.table),s.doors.forEach((e=>{const t=document.createElement("div");t.classList.add("h5p-advent-calendar-square"),e.door=new o({contentId:d,day:e.day,content:e.content,open:e.open,hideDoorBorder:r.visuals.hideDoorBorder,hideNumbers:r.visuals.hideNumbers,hideDoorKnobs:r.visuals.hideDoorKnobs,hideDoorFrame:r.visuals.hideDoorFrame,designMode:r.behaviour.designMode,a11y:{door:s.params.a11y.door,locked:s.params.a11y.locked,content:s.params.a11y.content}},{onOpened:(e,t)=>{s.handleOverlayOpened(e,t)},onLoaded:()=>{s.handleDoorLoaded()}}),t.appendChild(e.door.getDOM()),s.columns.push(t),s.table.appendChild(t)})),r.audio.backgroundMusic&&(s.backgroundMusic=s.createAudio(r.audio.backgroundMusic,d),s.buttonAudio=document.createElement("button"),s.buttonAudio.classList.add("h5p-advent-calendar-audio-button"),s.muted?(s.buttonAudio.classList.add("muted"),s.buttonAudio.setAttribute("aria-label",s.params.a11y.unmute)):(s.buttonAudio.classList.add("unmuted"),s.buttonAudio.setAttribute("aria-label",s.params.a11y.mute)),s.buttonAudio.addEventListener("click",(()=>{s.toggleButtonAudio()?(s.buttonAudio.setAttribute("aria-label",s.params.a11y.unmute),s.stopAudio()):(s.buttonAudio.setAttribute("aria-label",s.params.a11y.mute),s.playAudio())})),s.container.appendChild(s.buttonAudio),r.audio.autoplay&&s.playAudio());const m=new Date;if(r.visuals.snow||11===m.getMonth()&&m.getDate()>=24){const e=document.createElement("div");e.classList.add("h5p-advent-calendar-sky"),s.container.appendChild(e);const t=document.createElement("div");t.classList.add("h5p-advent-calendar-snow"),e.appendChild(t);for(let e=0;e<30;e++){const e=document.createElement("span");t.appendChild(e)}}return s.overlay=new n({a11y:{closeWindow:s.params.a11y.closeWindow}},{onClose:()=>{s.handleOverlayClosed()}}),s.container.appendChild(s.overlay.getDOM()),s.on("resize",(()=>{s.resize()})),s}d=t,(r=l).prototype=Object.create(d.prototype),r.prototype.constructor=r,s(r,d);var c=l.prototype;return c.attach=function(e){e.get(0).classList.add("h5p-advent-calendar"),e.get(0).appendChild(this.container),this.trigger("resize")},c.determineRowColumnRatio=function(){const e=this.container.getBoundingClientRect().width,t=Math.floor(e/l.COLUMN_WIDTH_MIN);return t>=6?"6x4":t>=4?"4x6":t>=3?"3x8":t>=2?"2x12":"6x4"},c.setRowColumnRatio=function(e){e&&["",...l.ROW_COLUMN_RATIOS].includes(e)&&this.columns.forEach((t=>{l.ROW_COLUMN_RATIOS.forEach((e=>{t.classList.remove(`h5p-advent-calendar-row-column-ratio-${e}`)})),""!==e&&t.classList.add(`h5p-advent-calendar-row-column-ratio-${e}`)}))},c.updateDoorCovers=function(){if("automatic"!==this.params.modeDoorImage||!this.backgroundImage||0===this.backgroundImage.naturalWidth||0===this.backgroundImage.naturalHeight)return;const e=this.container.getBoundingClientRect();if(0===e.height)return;const t=e.width/e.height{t.door.setDoorCover({image:this.backgroundImage,styles:{"background-size":`${a}px ${o}px`},offset:{left:e.left,top:e.top}})}))},c.resize=function(){if(this.table.style.fontSize=this.container.offsetWidth/48+"px",this.currentDayOpened&&(this.instances[this.currentDayOpened]&&this.instances[this.currentDayOpened].instance.trigger("resize"),this.h5pContainer=this.h5pContainer||document.body.querySelector(".h5p-container"),this.h5pContainer&&this.instances[this.currentDayOpened])){const e=parseInt(window.getComputedStyle(this.instances[this.currentDayOpened].wrapper).fontSize)||16;this.instances[this.currentDayOpened].wrapper.style.maxHeight=`calc(${this.h5pContainer.offsetHeight-7*e}px)`;const t=this.instances[this.currentDayOpened]?.instance;"H5P.Image"===t?.libraryInfo?.machineName&&t.$img?.get(0)&&(this.instances[this.currentDayOpened].wrapper.style.maxWidth=(this.h5pContainer.offsetHeight-7*e)/t.$img?.get(0).offsetHeight*t.$img?.get(0).offsetWidth+"px")}const e="fixed"===this.params.behaviour.modeDoorPlacement?this.params.behaviour.doorPlacementRatio:this.determineRowColumnRatio();this.setRowColumnRatio(e),clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout((()=>{this.updateDoorCovers()}),0)},c.getAnswerGiven=function(){return this.doors.some((e=>e.door.isOpen()))},c.getCurrentState=function(){if(this.getAnswerGiven())return this.doors.map((e=>({day:e.day,open:e.door.isOpen()})))},c.handleOverlayOpened=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1e3;this.doors.forEach((e=>e.door.lock())),this.currentDayOpened=e;const a=this.doors.filter((t=>t.day===e))[0];if(this.overlay.setModifierClass(`h5p-advent-calendar-content-type-${a.content.type}`),!this.instances[e]){const t=document.createElement("div");if(t.classList.add("h5p-advent-calendar-instance-wrapper"),!a.content[a.content.type])return this.doors.forEach((e=>e.door.unlock())),void a.door.focus();let o;if("audio"===a.content.type)o=new H5P.Audio({files:a.content.audio,playerMode:"full",fitToWrapper:!1,controls:!0,autoplay:a.content.autoplay||!1},this.contentId),o.attach(H5P.jQuery(t)),o.audio.style.width="100%",this.backgroundMusic&&(o.audio.addEventListener("play",(()=>{this.handleOverlayMedia("play")})),o.audio.addEventListener("pause",(()=>{this.handleOverlayMedia("stop")})),o.audio.addEventListener("ended",(()=>{this.handleOverlayMedia("stop")})));else if("video"===a.content.type){const e=!(a.content.video.length>0&&"video/YouTube"===a.content.video[0].mime);o=new H5P.Video({sources:a.content.video,visuals:{fit:e,controls:!0},playback:{autoplay:a.content.autoplay||!1,loop:!1}},this.contentId),o.attach(H5P.jQuery(t)),this.backgroundMusic&&o.on("stateChange",(e=>{e.data===H5P.Video.PLAYING?this.handleOverlayMedia("play"):e.data!==H5P.Video.PAUSED&&e.data!==H5P.Video.ENDED||this.handleOverlayMedia("stop")}))}else o=H5P.newRunnable(a.content[a.content.type],this.contentId,H5P.jQuery(t));"image"===a.content.type&&o.on("loaded",(()=>{this.trigger("resize")})),this.instances[e]={instance:o,wrapper:t}}this.popupWaiting&&clearTimeout(this.popupWaiting),this.popupWaiting=setTimeout((()=>{if("link"===a.content.type){const t=this.instances[e].wrapper.querySelector(".h5p-link a").getAttribute("href");window.open(t,"_blank")}else this.resize(),this.overlay.setContent(this.instances[e].wrapper),this.overlay.show(),this.instances[e].instance.trigger("resize"),a.content.type&&a.content.autoplay&&this.instances[e].instance.audio&&this.instances[e].instance.audio.paused&&this.instances[e].instance.play();this.doors.forEach((e=>e.door.unlock()))}),t)},c.handleOverlayClosed=function(){this.overlay.hide();const e=this.instances[this.currentDayOpened].instance;"function"==typeof e.pause&&e.pause(),this.doors.filter((e=>e.day===this.currentDayOpened))[0].door.focus()},c.handleOverlayMedia=function(e){this.backgroundMusic&&this.backgroundMusic.player&&("play"===e?(this.continueBackgroundMusic=!this.backgroundMusic.player.paused,this.continueBackgroundMusic&&this.stopAudio()):"stop"===e&&this.continueBackgroundMusic&&this.playAudio())},c.handleDoorLoaded=function(){this.doorsLoaded++,24===this.doorsLoaded&&(this.spinner.hide(),this.table.classList.remove("h5p-advent-calendar-display-none"),this.trigger("resize"))},c.createAudio=function(e,t){if(!e||e.length<1||!e[0].path)return null;const a=document.createElement("audio");return a.src=H5P.getPath(e[0].path,t),a.addEventListener("ended",(()=>{this.playAudio()})),{player:a,promise:null}},c.toggleButtonAudio=function(e){if(this.buttonAudio)return this.muted="boolean"==typeof e?e:!this.muted,this.muted?(this.buttonAudio.classList.remove("unmuted"),this.buttonAudio.classList.add("muted"),this.buttonAudio.setAttribute("aria-label",this.params.a11y.unmute)):(this.buttonAudio.classList.add("unmuted"),this.buttonAudio.classList.remove("muted"),this.buttonAudio.setAttribute("aria-label",this.params.a11y.mute)),this.muted},c.playAudio=function(){this.backgroundMusic&&(this.backgroundMusic.promise||(this.backgroundMusic.promise=this.backgroundMusic.player.play(),this.backgroundMusic.promise&&this.backgroundMusic.promise.then((()=>{this.backgroundMusic.promise=null,this.toggleButtonAudio(!1)})).catch((()=>{this.backgroundMusic.promise=null,this.toggleButtonAudio(!0)}))))},c.stopAudio=function(){this.backgroundMusic.promise?this.backgroundMusic.promise.then((()=>{this.backgroundMusic.player.pause(),this.backgroundMusic.promise=null,this.toggleButtonAudio(!0)})):(this.backgroundMusic.player.pause(),this.toggleButtonAudio(!0))},l}(H5P.EventDispatcher);r.ROW_COLUMN_RATIOS=["6x4","4x6","3x8","2x12"],r.COLUMN_WIDTH_MIN=120,H5P.AdventCalendar=r}();;