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);
;
/*
* flowplayer.js 3.2.12. The Flowplayer API
*
* Copyright 2009-2011 Flowplayer Oy
*
* This file is part of Flowplayer.
*
* Flowplayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Flowplayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Flowplayer. If not, see .
*
* Date: ${date}
* Revision: ${revision}
*/
!function(){function h(p){console.log("$f.fireEvent",[].slice.call(p))}function l(r){if(!r||typeof r!="object"){return r}var p=new r.constructor();for(var q in r){if(r.hasOwnProperty(q)){p[q]=l(r[q])}}return p}function n(u,r){if(!u){return}var p,q=0,s=u.length;if(s===undefined){for(p in u){if(r.call(u[p],p,u[p])===false){break}}}else{for(var t=u[0];q1){var u=arguments[1],r=(arguments.length==3)?arguments[2]:{};if(typeof u=="string"){u={src:u}}u=j({bgcolor:"#000000",version:[10,1],expressInstall:"http://releases.flowplayer.org/swf/expressinstall.swf",cachebusting:false},u);if(typeof p=="string"){if(p.indexOf(".")!=-1){var t=[];n(o(p),function(){t.push(new b(this,l(u),l(r)))});return new d(t)}else{var s=c(p);return new b(s!==null?s:l(p),l(u),l(r))}}else{if(p){return new b(p,l(u),l(r))}}}return null};j(window.$f,{fireEvent:function(){var q=[].slice.call(arguments);var r=$f(q[0]);return r?r._fireEvent(q.slice(1)):null},addPlugin:function(p,q){b.prototype[p]=q;return $f},each:n,extend:j});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(r,q){if(!arguments.length||typeof arguments[0]=="number"){var p=[];this.each(function(){var s=$f(this);if(s){p.push(s)}});return arguments.length?p[arguments[0]]:new d(p)}return this.each(function(){$f(this,l(r),q?l(q):{})})}}}();!function(){var h=document.all,j="http://get.adobe.com/flashplayer",c=typeof jQuery=="function",e=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,b={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function i(m,l){if(l){for(var f in l){if(l.hasOwnProperty(f)){m[f]=l[f]}}}return m}function a(f,n){var m=[];for(var l in f){if(f.hasOwnProperty(l)){m[l]=n(f[l])}}return m}window.flashembed=function(f,m,l){if(typeof f=="string"){f=document.getElementById(f.replace("#",""))}if(!f){return}if(typeof m=="string"){m={src:m}}return new d(f,i(i({},b),m),l)};var g=i(window.flashembed,{conf:b,getVersion:function(){var m,f;try{f=navigator.plugins["Shockwave Flash"].description.slice(16)}catch(o){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");f=m&&m.GetVariable("$version")}catch(n){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");f=m&&m.GetVariable("$version")}catch(l){}}}f=e.exec(f);return f?[1*f[1],1*f[(f[1]*1>9?2:3)]*1]:[0,0]},asString:function(l){if(l===null||l===undefined){return null}var f=typeof l;if(f=="object"&&l.push){f="array"}switch(f){case"string":l=l.replace(new RegExp('(["\\\\])',"g"),"\\$1");l=l.replace(/^\s?(\d+\.?\d*)%/,"$1pct");return'"'+l+'"';case"array":return"["+a(l,function(o){return g.asString(o)}).join(",")+"]";case"function":return'"function()"';case"object":var m=[];for(var n in l){if(l.hasOwnProperty(n)){m.push('"'+n+'":'+g.asString(l[n]))}}return"{"+m.join(",")+"}"}return String(l).replace(/\s/g," ").replace(/\'/g,'"')},getHTML:function(o,l){o=i({},o);var n='";return n},isSupported:function(f){return k[0]>f[0]||k[0]==f[0]&&k[1]>=f[1]}});var k=g.getVersion();function d(f,n,m){if(g.isSupported(n.version)){f.innerHTML=g.getHTML(n,m)}else{if(n.expressInstall&&g.isSupported([6,65])){f.innerHTML=g.getHTML(i(n,{src:n.expressInstall}),{MMredirectURL:encodeURIComponent(location.href),MMplayerType:"PlugIn",MMdoctitle:document.title})}else{if(!f.innerHTML.replace(/\s/g,"")){f.innerHTML="
Flash version "+n.version+" or greater is required
"+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+"
");if(f.tagName=="A"||f.tagName=="DIV"){f.onclick=function(){location.href=j}}}if(n.onFail){var l=n.onFail.call(this);if(typeof l=="string"){f.innerHTML=l}}}}if(h){window[n.id]=document.getElementById(n.id)}i(this,{getRoot:function(){return f},getOptions:function(){return n},getConf:function(){return m},getApi:function(){return f.firstChild}})}if(c){jQuery.tools=jQuery.tools||{version:"3.2.12"};jQuery.tools.flashembed={conf:b};jQuery.fn.flashembed=function(l,f){return this.each(function(){$(this).data("flashembed",flashembed(this,l,f))})}}}();;
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"
}, params);
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 () {
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%'
});
}
// cpAutoplay is passed from coursepresentation
if (this.params.autoplay) {
self.play();
}
//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 () {
audioButton
.attr('aria-hidden', false)
.attr('aria-label', self.params.playAudio)
.removeClass(PAUSE_BUTTON)
.addClass(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) {
$wrapper.addClass('h5p-audio-wrapper');
// Check if browser supports audio.
var audio = document.createElement('audio');
if (audio.canPlayType === undefined) {
// Try flash
this.attachFlash($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) {
// Try flash
this.attachFlash($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;
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 {
audio.autoplay = this.params.autoplay === undefined ? false : this.params.autoplay;
$wrapper.html(audio);
}
// Set time to saved time from previous run
if (this.oldTime) {
this.seekTo(this.oldTime);
}
};
/**
* Attaches a flash audio player to the wrapper.
*
* @param {jQuery} $wrapper Our dear container.
*/
H5P.Audio.prototype.attachFlash = function ($wrapper) {
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 (file.mime === 'audio/mpeg' || file.mime === 'audio/mp3') {
var audioSource = H5P.getPath(file.path, this.contentId);
break;
}
}
}
if (audioSource === undefined) {
$wrapper.addClass('h5p-audio-not-supported');
$wrapper.html(
'
' +
'
' +
'' + this.params.audioNotSupported + '' +
'
'
);
if (this.endedCallback !== undefined) {
this.endedCallback();
}
return;
}
var options = {
buffering: true,
clip: {
url: window.location.protocol + '//' + window.location.host + audioSource,
autoPlay: this.params.autoplay === undefined ? false : this.params.autoplay,
scaling: 'fit'
},
plugins: {
controls: null
}
};
if (this.params.controls === undefined || this.params.controls) {
options.plugins.controls = {
fullscreen: false,
autoHide: false
};
}
if (this.endedCallback !== undefined) {
options.clip.onFinish = this.endedCallback;
options.clip.onError = this.endedCallback;
}
this.flowplayer = flowplayer($wrapper[0], {
src: "http://releases.flowplayer.org/swf/flowplayer-3.2.16.swf",
wmode: "opaque"
}, options);
};
/**
* Stop the audio. TODO: Rename to pause?
*
* @returns {undefined}
*/
H5P.Audio.prototype.stop = function () {
if (this.flowplayer !== undefined) {
this.flowplayer.stop().close().unload();
}
if (this.audio !== undefined) {
this.audio.pause();
}
};
/**
* Play
*/
H5P.Audio.prototype.play = function () {
if (this.flowplayer !== undefined) {
this.flowplayer.play();
}
if (this.audio !== undefined) {
this.audio.play();
}
};
/**
* @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) {
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.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 $wrapper = $('');
var $placeholder = $('', {
id: id,
text: l10n.loading
}).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,
autoplay: options.autoplay ? 1 : 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: options.startAt,
playsinline: 1
},
events: {
onReady: function () {
self.trigger('ready');
self.trigger('loaded');
},
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) {
// 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);
}
}
});
};
/**
* 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
*/
self.pressToPlay = navigator.userAgent.match(/iPad/i) ? true : false;
/**
* 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 () {
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);
};
/**
* 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();
};
/**
* 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: '100%'
});
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;
var player;
var playbackRate = 1;
var id = 'h5p-panopto-' + numInstances;
numInstances++;
var $wrapper = $('');
var $placeholder = $('', {
id: id,
html: '
' + l10n.loading + '
'
}).appendTo($wrapper);
/**
* 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: !!options.autoplay,
showbrand: false,
start: 0,
hideoverlay: !options.controls,
},
events: {
onIframeReady: function () {
$placeholder.children(0).text('');
player.loadVideo();
},
onReady: function () {
self.trigger('loaded');
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);
}
self.pause();
},
onStateChange: function (state) {
// TODO: Playback rate fix for IE11?
if (state > -1 && state < 4) {
self.trigger('stateChange', state);
}
},
onPlaybackRateChange: function () {
self.trigger('playbackRateChange', self.getPlaybackRate());
},
onError: function () {
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) {
return;
}
player.playVideo();
};
/**
* 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) {
return;
}
player.seekTo(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();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* 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);
};
/**
* 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) {
console.log('Disable captions');
player.disableCaptions();
currentTrack = null;
}
else {
console.log('Set captions', track.value);
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;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: '100%'
});
var width = $wrapper[0].clientWidth;
var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
const $iframe = $placeholder.children('iframe');
if ($iframe.length) {
$iframe.attr('width', width);
$iframe.attr('height', height);
}
});
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 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);
video.autoplay = (options.autoplay ? true : 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 'stateChange':
if (lastState === arg) {
return; // Avoid firing event twice.
}
var validStartTime = options.startAt && options.startAt > 0;
if (arg === H5P.Video.PLAYING && validStartTime) {
video.currentTime = options.startAt;
delete options.startAt;
}
break;
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;
}
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();
}
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) {
if (lastState === undefined) {
// Make sure we always play before we seek to get an image.
// If not iOS devices will reset currentTime when pressing play.
video.play();
video.pause();
}
video.currentTime = time;
};
/**
* 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 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.VideoFlash = (function ($) {
/**
* Flash video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
*/
function Flash(sources, options) {
var self = this;
// Player wrapper
var $wrapper = $('', {
'class': 'h5p-video-flash',
css: {
width: '100%',
height: '100%'
}
});
/**
* 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;
// Sort sources into qualities
//var qualities = getQualities(sources);
var currentQuality;
// Create player options
var playerOptions = {
buffering: true,
clip: {
url: sources[0].path, // getPreferredQuality(),
autoPlay: options.autoplay,
autoBuffering: true,
scaling: 'fit',
onSeek: function () {
if (stateBeforeChangingQuality) {
// ????
}
},
onMetaData: function () {
setTimeout(function () {
if (stateBeforeChangingQuality !== undefined) {
fp.seek(currentTimeBeforeChangingQuality);
if (stateBeforeChangingQuality === H5P.Video.PLAYING) {
// Resume play
fp.play();
}
// Done changing quality
stateBeforeChangingQuality = undefined;
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
}
else {
self.trigger('ready');
self.trigger('loaded');
}
}, 0); // Run on next tick
},
onBegin: function () {
self.trigger('stateChange', H5P.Video.PLAYING);
},
onResume: function () {
self.trigger('stateChange', H5P.Video.PLAYING);
},
onPause: function () {
self.trigger('stateChange', H5P.Video.PAUSED);
},
onFinish: function () {
self.trigger('stateChange', H5P.Video.ENDED);
},
onError: function (code, message) {
console.log('ERROR', code, message); // TODO
self.trigger('error', message);
}
},
plugins: {
controls: null
},
play: null, // Disable overlay controls
onPlaylistReplace: function () {
that.playlistReplaced();
}
};
if (options.controls) {
playerOptions.plugins.controls = {};
delete playerOptions.play;
}
var fp = flowplayer($wrapper[0], {
src: "http://releases.flowplayer.org/swf/flowplayer-3.2.16.swf",
wmode: "opaque"
}, playerOptions);
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$wrapper.appendTo($container);
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
return;
};
/**
* 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;
}
// Keep track of current quality
currentQuality = quality;
self.trigger('qualityChange', currentQuality);
// Display throbber
self.trigger('stateChange', H5P.Video.BUFFERING);
// Change source
fp.setClip(qualities[quality].source.path);
fp.startBuffering();
};
/**
* Starts the video.
*
* @public
*/
self.play = function () {
if ($error.is(':visible')) {
return;
}
fp.play();
};
/**
* Pauses the video.
*
* @public
*/
self.pause = function () {
fp.pause();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
fp.seek(time);
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
return fp.getTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
return fp.getClip().metaData.duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
return fp.getClip().buffer;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
fp.mute();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
fp.unmute();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
return fp.muted;
};
/**
* Returns the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
return fp.volumeLevel * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} volume Between 0 and 100.
*/
self.setVolume = function (level) {
fp.volume(level / 100);
};
// Handle resize events
self.on('resize', function () {
var $object = H5P.jQuery(fp.getParent()).children('object');
var clip = fp.getClip();
if (clip !== undefined) {
$object.css('height', $object.width() * (clip.metaData.height / clip.metaData.width));
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Flash.canPlay = function (sources) {
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
if (sources[i].mime === 'video/mp4' || /\.mp4$/.test(sources[i].mime)) {
return true; // We only play mp4
}
}
};
return Flash;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoFlash);
;
/** @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
*/
function Video(parameters, id) {
var self = this;
self.contentId = id;
// 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.',
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, parameters.visuals, {
disableFullscreen: false
});
/** @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);
}
});
/**
* 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);
}
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;
};
// Resize the video when we know its aspect ratio
self.on('loaded', function () {
self.trigger('resize');
});
// 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,
startAt: parameters.startAt || 0,
tracks: tracks,
disableRemotePlayback: parameters.visuals.disableRemotePlayback === true,
disableFullscreen: parameters.visuals.disableFullscreen === true
}
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;
};
/** @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(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={i:d,l:!1,exports:{}};return a[d].call(e.exports,e,e.exports,b),e.l=!0,e.exports}var c={};return b.m=a,b.c=c,b.d=function(a,c,d){b.o(a,c)||Object.defineProperty(a,c,{enumerable:!0,get:d})},b.r=function(a){'undefined'!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(a,Symbol.toStringTag,{value:'Module'}),Object.defineProperty(a,'__esModule',{value:!0})},b.t=function(a,c){if(1&c&&(a=b(a)),8&c)return a;if(4&c&&'object'==typeof a&&a&&a.__esModule)return a;var d=Object.create(null);if(b.r(d),Object.defineProperty(d,'default',{enumerable:!0,value:a}),2&c&&'string'!=typeof a)for(var e in a)b.d(d,e,function(b){return a[b]}.bind(null,e));return d},b.n=function(a){var c=a&&a.__esModule?function(){return a['default']}:function(){return a};return b.d(c,'a',c),c},b.o=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)},b.p='',b(b.s=1)})([function(){},function(a,b,c){'use strict';function d(a){'@babel/helpers - typeof';return d='function'==typeof Symbol&&'symbol'==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&'function'==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?'symbol':typeof a},d(a)}function e(a,b){a.prototype=Object.create(b.prototype),a.prototype.constructor=a,a.__proto__=b}c.r(b);var f=c(0),g=function(){function a(b,c){var d=this;this.params=b,this.callbacks=c||{},this.callbacks.onOpened=c.onOpened||function(){},this.callbacks.onLoaded=c.onLoaded||function(){},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-'.concat(a.colorSchemeNames[b.day%a.colorSchemeNames.length])),b.hideDoorBorder&&this.container.classList.add('h5p-advent-calendar-hide-door-border'),b.hideDoorFrame&&this.container.classList.add('h5p-advent-calendar-hide-door-frame'),this.container.setAttribute('role','button'),this.container.setAttribute('aria-label',b.day),this.container.setAttribute('tabIndex',0);var e=''.concat(this.params.a11y.door,' ').concat(this.params.day,'.');this.canBeOpened()||(e+=this.params.a11y.locked),this.container.setAttribute('aria-label',e),this.canBeOpened()||this.container.classList.add('h5p-advent-calendar-disabled'),this.container.addEventListener('click',function(a){d.handleClick(a)}),this.container.addEventListener('keypress',function(a){d.handleKeypress(a)});var f=document.createElement('div');f.classList.add('h5p-advent-calendar-door-container'),this.container.appendChild(f);var g=document.createElement('div');g.classList.add('h5p-advent-calendar-doorway'),g.classList.add('h5p-advent-calendar-left'),f.appendChild(g);var h=document.createElement('div');h.classList.add('h5p-advent-calendar-doorway'),h.classList.add('h5p-advent-calendar-right'),f.appendChild(h);var i=document.createElement('div');if(i.classList.add('h5p-advent-calendar-door'),i.classList.add('h5p-advent-calendar-left'),g.appendChild(i),!b.hideNumbers){var j=document.createElement('div');j.classList.add('h5p-advent-calendar-door-number'),j.innerText=b.day,i.appendChild(j)}if(!b.hideDoorKnobs){var k=document.createElement('div');k.classList.add('h5p-advent-calendar-doorknob'),i.appendChild(k)}var l=document.createElement('div');if(l.classList.add('h5p-advent-calendar-door'),l.classList.add('h5p-advent-calendar-right'),h.appendChild(l),!b.hideDoorKnobs){var m=document.createElement('div');m.classList.add('h5p-advent-calendar-doorknob'),l.appendChild(m)}if(this.params.content.doorCover&&this.params.content.doorCover.path){var n=H5P.getPath(this.params.content.doorCover.path,this.params.contentId);if(n){var o=document.createElement('img');o.src=n,o.addEventListener('load',function(){i.style.backgroundImage='url("'.concat(n,'")'),l.style.backgroundImage='url("'.concat(n,'")'),o=null,d.handleLoaded('door')}),this.container.classList.add('h5p-advent-calendar-cover-image')}}else this.toLoad=this.toLoad.filter(function(a){return'door'!==a});if(this.previewImage=document.createElement('button'),this.previewImage.setAttribute('aria-label',this.params.a11y.content.replace('@door',''.concat(this.params.a11y.door,' ').concat(this.params.day,'.'))),this.previewImage.setAttribute('tabIndex',-1),this.previewImage.classList.add('h5p-advent-calendar-preview-image'),this.previewImage.classList.add('h5p-advent-calendar-'.concat(b.content.type,'-symbol')),this.container.appendChild(this.previewImage),this.params.content.previewImage&&this.params.content.previewImage.path){var p=H5P.getPath(this.params.content.previewImage.path,this.params.contentId);if(p){var q=document.createElement('img');q.src=p,q.addEventListener('load',function(){d.previewImage.style.backgroundImage='url("'.concat(p,'")'),q=null,d.handleLoaded('previewImage')})}}else this.toLoad=this.toLoad.filter(function(a){return'previewImage'!==a}),this.previewImage.innerText=b.day;0===this.toLoad.length&&this.callbacks.onLoaded(),this.previewImage.addEventListener('click',function(){d.open({delay:0})}),b.open&&this.open({skipCallback:!0})}var b=a.prototype;return b.getDOM=function(){return this.container},b.handleKeypress=function(a){(13===a.keyCode||32===a.keyCode)&&this.handleClick(a)},b.handleClick=function(a){var b=this;!this.canBeOpened()||this.isOpen()||(a.preventDefault(),this.open(),this.container.removeEventListener('click',function(a){b.handleClick(a)}),this.container.removeEventListener('keypress',function(a){b.handleKeypress(a)}))},b.handleLoaded=function(a){this.toLoad=this.toLoad.filter(function(b){return b!==a}),0===this.toLoad.length&&this.callbacks.onLoaded()},b.focus=function(){this.isOpen()?this.previewImage.focus():this.container.focus()},b.open=function(){var a=0=this.params.day},a}();g.colorSchemeNames=['red','white','lightgreen','darkgreen'];var h=function(){function a(){}return a.extend=function(){for(var a=1;ab.doors.length;)b.doors.push({});var m=b.behaviour.doorImageTemplate&&b.behaviour.doorImageTemplate.path?b.behaviour.doorImageTemplate:null;d.doors=b.doors.map(function(a,b){return m&&!a.doorCover&&(a.doorCover=m),a.type||(a.type='text',a.text.params.text=d.params.l10n.nothingToSee),{day:b+1,content:a}}),e.previousState&&Array.isArray(e.previousState)?d.doors=e.previousState.map(function(a){var b=d.doors[a.day-1];return b.open=a.open,b}):b.behaviour.randomize&&j.shuffleArray(d.doors);var n=d.doors.map(function(a){return{day:a.day,doorCover:a.content.doorCover}});if(b.behaviour.keepImageOrder&&d.doors.forEach(function(a,b){a.content.doorCover=n.filter(function(a){return a.day===b+1})[0].doorCover}),d.instances=Array(25),d.muted=!d.params.behaviour.autoplay,d.container=document.createElement('div'),d.container.classList.add('h5p-advent-calendar-container'),d.spinner=new l('h5p-advent-calendar-spinner'),d.container.appendChild(d.spinner.getDOM()),d.table=document.createElement('div'),d.table.classList.add('h5p-advent-calendar-table'),d.table.classList.add('h5p-advent-calendar-display-none'),b.behaviour.backgroundImage&&b.behaviour.backgroundImage.path){var o=document.createElement('img');H5P.setSource(o,b.behaviour.backgroundImage,c),d.table.style.backgroundImage='url(\''.concat(o.src,'\')')}d.container.appendChild(d.table),d.doors.forEach(function(a){var e=document.createElement('div');e.classList.add('h5p-advent-calendar-square'),d.table.appendChild(e),a.door=new g({contentId:c,day:a.day,content:a.content,open:a.open,hideDoorBorder:b.behaviour.hideDoorBorder,hideNumbers:b.behaviour.hideNumbers,hideDoorKnobs:b.behaviour.hideDoorKnobs,hideDoorFrame:b.behaviour.hideDoorFrame,designMode:b.behaviour.designMode,a11y:{door:d.params.a11y.door,locked:d.params.a11y.locked,content:d.params.a11y.content}},{onOpened:function(a,b){d.handleOverlayOpened(a,b)},onLoaded:function(){d.handleDoorLoaded()}}),e.appendChild(a.door.getDOM())}),b.behaviour.backgroundMusic&&(d.backgroundMusic=d.createAudio(b.behaviour.backgroundMusic,c),d.buttonAudio=document.createElement('button'),d.buttonAudio.classList.add('h5p-advent-calendar-audio-button'),d.muted?(d.buttonAudio.classList.add('muted'),d.buttonAudio.setAttribute('aria-label',d.params.a11y.unmute)):(d.buttonAudio.classList.add('unmuted'),d.buttonAudio.setAttribute('aria-label',d.params.a11y.mute)),d.buttonAudio.addEventListener('click',function(){var a=d.toggleButtonAudio();a?(d.buttonAudio.setAttribute('aria-label',d.params.a11y.unmute),d.stopAudio()):(d.buttonAudio.setAttribute('aria-label',d.params.a11y.mute),d.playAudio())}),d.container.appendChild(d.buttonAudio),b.behaviour.autoplay&&d.playAudio());var p=new Date;if(b.behaviour.snow||11===p.getMonth()&&24<=p.getDate()){var q=document.createElement('div');q.classList.add('h5p-advent-calendar-sky'),d.container.appendChild(q);var r=document.createElement('div');r.classList.add('h5p-advent-calendar-snow'),q.appendChild(r);for(var s,t=0;30>t;t++)s=document.createElement('span'),r.appendChild(s)}return d.overlay=new k({a11y:{closeWindow:d.params.a11y.closeWindow}},{onClose:function(){d.handleOverlayClosed()}}),d.container.appendChild(d.overlay.getDOM()),d.on('resize',function(){d.resize()}),d}e(b,a);var c=b.prototype;return c.attach=function(a){a.get(0).classList.add('h5p-advent-calendar'),a.get(0).appendChild(this.container),this.trigger('resize')},c.resize=function(){this.table.style.fontSize=''.concat(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]&&(this.instances[this.currentDayOpened].wrapper.style.maxHeight='calc('.concat(this.h5pContainer.offsetHeight,'px - 7em)')))},c.getCurrentState=function(){return this.doors.map(function(a){return{day:a.day,open:a.door.isOpen()}})},c.handleOverlayOpened=function(a){var b=this,c=1a.length||!a[0].path)return null;var d=document.createElement('audio');return d.src=H5P.getPath(a[0].path,b),d.addEventListener('ended',function(){c.playAudio()}),{player:d,promise:null}},c.toggleButtonAudio=function(a){if(this.buttonAudio)return this.muted='boolean'==typeof a?a:!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(){var a=this;this.backgroundMusic&&(this.backgroundMusic.promise||(this.backgroundMusic.promise=this.backgroundMusic.player.play(),this.backgroundMusic.promise&&this.backgroundMusic.promise.then(function(){a.backgroundMusic.promise=null,a.toggleButtonAudio(!1)})['catch'](function(){a.backgroundMusic.promise=null,a.toggleButtonAudio(!0)})))},c.stopAudio=function(){var a=this;this.backgroundMusic.promise?this.backgroundMusic.promise.then(function(){a.backgroundMusic.player.pause(),a.backgroundMusic.promise=null,a.toggleButtonAudio(!0)}):(this.backgroundMusic.player.pause(),this.toggleButtonAudio(!0))},b}(H5P.EventDispatcher);H5P=H5P||{},H5P.AdventCalendar=i}]);;