mirror of
https://github.com/h5p/h5p-interactive-video.git
synced 2026-03-06 12:32:55 +08:00
2139 lines
66 KiB
JavaScript
2139 lines
66 KiB
JavaScript
H5P.InteractiveVideo = (function ($, EventDispatcher, DragNBar, Interaction) {
|
||
|
||
/**
|
||
* @typedef {Object} InteractiveVideoParameters
|
||
* @property {Object} interactiveVideo View parameters
|
||
* @property {Object} override Override settings
|
||
* @property {number} startVideoAt Time-code to start video
|
||
*/
|
||
|
||
/**
|
||
* Initialize a new interactive video.
|
||
*
|
||
* @class H5P.InteractiveVideo
|
||
* @extends H5P.EventDispatcher
|
||
* @property {Object|undefined} editor Set when editing
|
||
* @param {InteractiveVideoParameters} params
|
||
* @param {number} id
|
||
* @param {Object} contentData
|
||
*/
|
||
function InteractiveVideo(params, id, contentData) {
|
||
var self = this;
|
||
var startAt;
|
||
|
||
// Inheritance
|
||
H5P.EventDispatcher.call(self);
|
||
|
||
// Keep track of content ID
|
||
self.contentId = id;
|
||
|
||
self.isMinimal = false;
|
||
|
||
// Insert default options
|
||
self.options = $.extend({ // Deep is not used since editor uses references.
|
||
video: {},
|
||
assets: {}
|
||
}, params.interactiveVideo);
|
||
self.options.video.startScreenOptions = self.options.video.startScreenOptions || {};
|
||
|
||
// Add default title
|
||
if (!self.options.video.startScreenOptions.title) {
|
||
self.options.video.startScreenOptions.title = 'Interactive Video';
|
||
}
|
||
|
||
// Set default splash options
|
||
self.startScreenOptions = $.extend({
|
||
hideStartTitle: false,
|
||
shortStartDescription: ''
|
||
}, self.options.video.startScreenOptions);
|
||
|
||
// Set overrides for interactions
|
||
if (params.override && (params.override.showSolutionButton || params.override.retryButton)) {
|
||
self.override = {};
|
||
|
||
if (params.override.showSolutionButton) {
|
||
// Force "Show solution" button to be on or off for all interactions
|
||
self.override.enableSolutionsButton = params.override.showSolutionButton === 'on';
|
||
}
|
||
|
||
if (params.override.retryButton) {
|
||
// Force "Retry" button to be on or off for all interactions
|
||
self.override.enableRetry = params.override.retryButton === 'on';
|
||
}
|
||
}
|
||
|
||
if (params.override !== undefined) {
|
||
self.showRewind10 = (params.override.showRewind10 !== undefined ? params.override.showRewind10 : false);
|
||
self.showBookmarksmenuOnLoad = (params.override.showBookmarksmenuOnLoad !== undefined ? params.override.showBookmarksmenuOnLoad : false);
|
||
self.preventSkipping = params.override.preventSkipping || false;
|
||
}
|
||
// Translated UI text defaults
|
||
self.l10n = $.extend({
|
||
interaction: 'Interaction',
|
||
play: 'Play',
|
||
pause: 'Pause',
|
||
mute: 'Mute',
|
||
quality: 'Video quality',
|
||
unmute: 'Unmute',
|
||
fullscreen: 'Fullscreen',
|
||
exitFullscreen: 'Exit fullscreen',
|
||
summary: 'Summary',
|
||
bookmarks: 'Bookmarks',
|
||
defaultAdaptivitySeekLabel: 'Continue',
|
||
continueWithVideo: 'Continue with video',
|
||
more: 'More',
|
||
playbackRate: 'Playback rate',
|
||
rewind10: 'Rewind 10 seconds',
|
||
navDisabled: 'Navigation is disabled',
|
||
requiresCompletionWarning: 'You need to answer all the questions correctly before continuing.',
|
||
back: 'Back'
|
||
}, params.l10n);
|
||
|
||
// Make it possible to restore from previous state
|
||
if (contentData &&
|
||
contentData.previousState !== undefined &&
|
||
contentData.previousState.progress !== undefined &&
|
||
contentData.previousState.answers !== undefined) {
|
||
self.previousState = contentData.previousState;
|
||
}
|
||
|
||
// Initial state
|
||
self.lastState = H5P.Video.ENDED;
|
||
|
||
// Listen for resize events to make sure we cover our container.
|
||
self.on('resize', function () {
|
||
self.resize();
|
||
});
|
||
|
||
// Detect whether to add interactivies or just display a plain video.
|
||
self.justVideo = false;
|
||
var iOSMatches = navigator.userAgent.match(/(iPhone|iPod) OS (\d*)_/i);
|
||
if(iOSMatches !== null && iOSMatches.length === 3) {
|
||
// If iOS < 10, let's play video only...
|
||
self.justVideo = iOSMatches[2] < 10;
|
||
}
|
||
|
||
// set start time
|
||
startAt = (self.previousState && self.previousState.progress) ? Math.floor(self.previousState.progress) : 0;
|
||
if (startAt === 0 && params.override && !!params.override.startVideoAt) {
|
||
startAt = params.override.startVideoAt;
|
||
}
|
||
|
||
// Start up the video player
|
||
self.video = H5P.newRunnable({
|
||
library: 'H5P.Video 1.2',
|
||
params: {
|
||
sources: self.options.video.files,
|
||
visuals: {
|
||
poster: self.options.video.startScreenOptions.poster,
|
||
controls: self.justVideo,
|
||
fit: false
|
||
},
|
||
startAt: startAt
|
||
}
|
||
}, self.contentId, undefined, undefined, {parent: self});
|
||
|
||
// Listen for video events
|
||
if (self.justVideo) {
|
||
self.video.on('loaded', function () {
|
||
// Make sure it fits
|
||
self.trigger('resize');
|
||
});
|
||
|
||
// Do nothing more if we're just displaying a video
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* Keep track if the video source is loaded.
|
||
* @private
|
||
*/
|
||
var isLoaded = false;
|
||
|
||
// Handle video source loaded events (metadata)
|
||
self.video.on('loaded', function () {
|
||
isLoaded = true;
|
||
// Update IV player UI
|
||
self.loaded();
|
||
});
|
||
|
||
self.video.on('error', function () {
|
||
// Make sure splash screen is removed so the error is visible.
|
||
self.removeSplash();
|
||
});
|
||
|
||
// We need to initialize some stuff the first time the video plays
|
||
var firstPlay = true;
|
||
self.video.on('stateChange', function (event) {
|
||
|
||
if (!self.controls && isLoaded) {
|
||
// Add controls if they're missing and 'loaded' has happened
|
||
self.addControls();
|
||
self.trigger('resize');
|
||
}
|
||
|
||
var state = event.data;
|
||
if (self.currentState === InteractiveVideo.SEEKING) {
|
||
return; // Prevent updateing UI while seeking
|
||
}
|
||
|
||
switch (state) {
|
||
case H5P.Video.ENDED:
|
||
self.currentState = H5P.Video.ENDED;
|
||
self.controls.$play.addClass('h5p-pause').attr('title', self.l10n.play);
|
||
self.timeUpdate(self.video.getCurrentTime());
|
||
self.controls.$currentTime.html(self.controls.$totalTime.html());
|
||
|
||
self.complete();
|
||
break;
|
||
|
||
case H5P.Video.PLAYING:
|
||
if (firstPlay) {
|
||
firstPlay = false;
|
||
|
||
// Qualities might not be available until after play.
|
||
self.addQualityChooser();
|
||
|
||
self.addPlaybackRateChooser();
|
||
|
||
// Make sure splash screen is removed.
|
||
self.removeSplash();
|
||
|
||
// Make sure we track buffering of the video.
|
||
self.startUpdatingBufferBar();
|
||
|
||
// Remove bookmarkchooser
|
||
self.toggleBookmarksChooser(false);
|
||
}
|
||
|
||
self.currentState = H5P.Video.PLAYING;
|
||
self.controls.$play.removeClass('h5p-pause').attr('title', self.l10n.pause);
|
||
self.timeUpdate(self.video.getCurrentTime());
|
||
break;
|
||
|
||
case H5P.Video.PAUSED:
|
||
self.currentState = H5P.Video.PAUSED;
|
||
self.controls.$play.addClass('h5p-pause').attr('title', self.l10n.play);
|
||
self.timeUpdate(self.video.getCurrentTime());
|
||
break;
|
||
|
||
case H5P.Video.BUFFERING:
|
||
self.currentState = H5P.Video.BUFFERING;
|
||
|
||
// Make sure splash screen is removed.
|
||
self.removeSplash();
|
||
|
||
// Make sure we track buffering of the video.
|
||
self.startUpdatingBufferBar();
|
||
|
||
break;
|
||
}
|
||
});
|
||
|
||
self.video.on('qualityChange', function (event) {
|
||
var quality = event.data;
|
||
if (self.controls.$qualityChooser) {
|
||
// Update quality selector
|
||
self.controls.$qualityChooser.find('li').removeClass('h5p-selected').filter('[data-quality="' + quality + '"]').addClass('h5p-selected');
|
||
}
|
||
});
|
||
|
||
self.video.on('playbackRateChange', function (event) {
|
||
var playbackRate = event.data;
|
||
// Firefox fires a "ratechange" event immediately upon changing source, at this
|
||
// point controls has not been initialized, so we must check for controls
|
||
if (self.controls && self.controls.$playbackRateChooser) {
|
||
// Update playbackRate selector
|
||
self.controls.$playbackRateChooser.find('li').removeClass('h5p-selected').filter('[playback-rate="' + playbackRate + '"]').addClass('h5p-selected');
|
||
}
|
||
});
|
||
|
||
// Handle entering fullscreen
|
||
self.on('enterFullScreen', function () {
|
||
self.hasFullScreen = true;
|
||
self.$container.parent('.h5p-content').css('height', '100%');
|
||
self.controls.$fullscreen.addClass('h5p-exit').attr('title', self.l10n.exitFullscreen);
|
||
self.resizeInteractions();
|
||
});
|
||
|
||
// Handle exiting fullscreen
|
||
self.on('exitFullScreen', function () {
|
||
if (self.$container.hasClass('h5p-standalone') && self.$container.hasClass('h5p-minimal')) {
|
||
self.pause();
|
||
}
|
||
|
||
self.hasFullScreen = false;
|
||
self.$container.parent('.h5p-content').css('height', '');
|
||
self.controls.$fullscreen.removeClass('h5p-exit').attr('title', self.l10n.fullscreen);
|
||
self.resizeInteractions();
|
||
|
||
// Close dialog
|
||
if (self.dnb && self.dnb.dialog && !self.hasUncompletedRequiredInteractions()) {
|
||
self.dnb.dialog.close();
|
||
}
|
||
});
|
||
|
||
// Initialize interactions
|
||
self.interactions = [];
|
||
if (self.options.assets.interactions) {
|
||
for (var i = 0; i < self.options.assets.interactions.length; i++) {
|
||
this.initInteraction(i);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Inheritance
|
||
InteractiveVideo.prototype = Object.create(H5P.EventDispatcher.prototype);
|
||
InteractiveVideo.prototype.constructor = InteractiveVideo;
|
||
|
||
/**
|
||
* Returns the current state of the interactions
|
||
*
|
||
* @returns {Object|undefined}
|
||
*/
|
||
InteractiveVideo.prototype.getCurrentState = function () {
|
||
var self = this;
|
||
|
||
if (!self.video.play) {
|
||
return; // Missing video
|
||
}
|
||
|
||
var state = {
|
||
progress: self.video.getCurrentTime(),
|
||
answers: []
|
||
};
|
||
|
||
for (var i = 0; i < self.interactions.length; i++) {
|
||
state.answers[i] = self.interactions[i].getCurrentState();
|
||
}
|
||
|
||
if (state.progress) {
|
||
return state;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Removes splash screen.
|
||
*/
|
||
InteractiveVideo.prototype.removeSplash = function () {
|
||
if (this.$splash === undefined) {
|
||
return;
|
||
}
|
||
|
||
this.$splash.remove();
|
||
delete this.$splash;
|
||
};
|
||
|
||
/**
|
||
* Attach interactive video to DOM element.
|
||
*
|
||
* @param {H5P.jQuery} $container
|
||
*/
|
||
InteractiveVideo.prototype.attach = function ($container) {
|
||
var that = this;
|
||
// isRoot is undefined in the editor
|
||
if (this.isRoot !== undefined && this.isRoot()) {
|
||
this.setActivityStarted();
|
||
}
|
||
this.$container = $container;
|
||
|
||
$container.addClass('h5p-interactive-video').html('<div class="h5p-video-wrapper"></div><div class="h5p-controls"></div>');
|
||
|
||
// Font size is now hardcoded, since some browsers (At least Android
|
||
// native browser) will have scaled down the original CSS font size by the
|
||
// time this is run. (It turned out to have become 13px) Hard coding it
|
||
// makes it be consistent with the intended size set in CSS.
|
||
this.fontSize = 16;
|
||
this.width = 640; // parseInt($container.css('width')); // Get width in px
|
||
|
||
// 'video only' fallback has no interactions
|
||
if (this.interactions) {
|
||
// interactions require parent $container, recreate with input
|
||
this.interactions.forEach(function (interaction) {
|
||
interaction.reCreate();
|
||
});
|
||
}
|
||
|
||
// Video with interactions
|
||
this.$videoWrapper = $container.children('.h5p-video-wrapper');
|
||
this.attachVideo(this.$videoWrapper);
|
||
|
||
if (this.justVideo) {
|
||
this.$videoWrapper.find('video').css('minHeight', '200px');
|
||
$container.children(':not(.h5p-video-wrapper)').remove();
|
||
return;
|
||
}
|
||
|
||
// Controls
|
||
this.$controls = $container.children('.h5p-controls').hide();
|
||
|
||
if (this.editor === undefined) {
|
||
this.dnb = new DragNBar([], this.$videoWrapper, this.$container, {disableEditor: true});
|
||
// Pause video when opening dialog
|
||
this.dnb.dialog.on('open', function () {
|
||
// Keep track of last state
|
||
that.lastState = that.currentState;
|
||
|
||
if (that.currentState !== H5P.Video.PAUSED && that.currentState !== H5P.Video.ENDED) {
|
||
// Pause video
|
||
that.video.pause();
|
||
}
|
||
});
|
||
|
||
// Resume playing when closing dialog
|
||
this.dnb.dialog.on('close', function () {
|
||
if (that.lastState !== H5P.Video.PAUSED && that.lastState !== H5P.Video.ENDED) {
|
||
that.video.play();
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
that.on('dnbEditorReady', function () {
|
||
that.dnb = that.editor.dnb;
|
||
that.dnb.dialog.disableOverlay = true;
|
||
});
|
||
}
|
||
|
||
if (!this.video.pressToPlay) {
|
||
if (this.currentState === InteractiveVideo.LOADED) {
|
||
// Add all controls
|
||
this.addControls();
|
||
}
|
||
else {
|
||
// Add splash to allow start playing before video load
|
||
// (play may be needed to trigger load incase preloaded="none" is default)
|
||
this.addSplash();
|
||
}
|
||
}
|
||
|
||
|
||
this.currentState = InteractiveVideo.ATTACHED;
|
||
};
|
||
|
||
/**
|
||
* Attach the video to the given wrapper.
|
||
*
|
||
* @param {H5P.jQuery} $wrapper
|
||
*/
|
||
InteractiveVideo.prototype.attachVideo = function ($wrapper) {
|
||
this.video.attach($wrapper);
|
||
if (!this.justVideo) {
|
||
this.$overlay = $('<div class="h5p-overlay h5p-ie-transparent-background"></div>').appendTo($wrapper);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Add splash screen
|
||
*/
|
||
InteractiveVideo.prototype.addSplash = function () {
|
||
var that = this;
|
||
if (this.editor !== undefined || this.video.pressToPlay || !this.video.play || this.$splash) {
|
||
return;
|
||
}
|
||
|
||
this.$splash = $(
|
||
'<div class="h5p-splash-wrapper">' +
|
||
'<div class="h5p-splash-outer">' +
|
||
'<div class="h5p-splash" role="button" tabindex="1" title="' + this.l10n.play + '">' +
|
||
'<div class="h5p-splash-main">' +
|
||
'<div class="h5p-splash-main-outer">' +
|
||
'<div class="h5p-splash-main-inner">' +
|
||
'<div class="h5p-splash-play-icon"></div>' +
|
||
'<div class="h5p-splash-title">' + this.options.video.startScreenOptions.title + '</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="h5p-splash-footer">' +
|
||
'<div class="h5p-splash-footer-holder">' +
|
||
'<div class="h5p-splash-description">' + that.startScreenOptions.shortStartDescription + '</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>')
|
||
.click(function () {
|
||
that.video.play();
|
||
})
|
||
.appendTo(this.$overlay)
|
||
.find('.h5p-interaction-button')
|
||
.click(function () {
|
||
return false;
|
||
})
|
||
.end();
|
||
|
||
// Add play functionality and title to play icon
|
||
$('.h5p-splash', this.$splash).keydown(function (e) {
|
||
var code = e.which;
|
||
// 32 = Space
|
||
if (code === 32) {
|
||
that.video.play();
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
|
||
if (this.startScreenOptions.shortStartDescription === undefined || !this.startScreenOptions.shortStartDescription.length) {
|
||
this.$splash.addClass('no-description');
|
||
}
|
||
|
||
if (this.startScreenOptions.hideStartTitle) {
|
||
this.$splash.addClass('no-title');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Update and show controls for the interactive video.
|
||
*/
|
||
InteractiveVideo.prototype.addControls = function () {
|
||
// Display splash screen
|
||
this.addSplash();
|
||
|
||
this.attachControls(this.$controls.show());
|
||
|
||
var duration = this.video.getDuration();
|
||
var time = InteractiveVideo.humanizeTime(duration);
|
||
this.controls.$totalTime.html(time);
|
||
this.controls.$slider.slider('option', 'max', duration);
|
||
|
||
// Set correct margins for timeline
|
||
this.controls.$slider.parent().css({
|
||
marginLeft: this.$controls.children('.h5p-controls-left').width(),
|
||
marginRight: this.$controls.children('.h5p-controls-right').width()
|
||
});
|
||
this.controls.$currentTime.html(InteractiveVideo.humanizeTime(0));
|
||
|
||
// Add dots above seeking line.
|
||
this.addSliderInteractions();
|
||
|
||
// Add bookmarks
|
||
this.addBookmarks();
|
||
|
||
this.trigger('controls');
|
||
};
|
||
|
||
/**
|
||
* Prepares the IV for playing.
|
||
*/
|
||
InteractiveVideo.prototype.loaded = function () {
|
||
// Get duration
|
||
var duration = this.video.getDuration();
|
||
|
||
// Determine how many percentage one second is.
|
||
this.oneSecondInPercentage = (100 / this.video.getDuration());
|
||
|
||
duration = Math.floor(duration);
|
||
|
||
if (this.editor !== undefined) {
|
||
var interactions = findField('interactions', this.editor.field.fields);
|
||
|
||
// Set max/min for editor duration fields
|
||
var durationFields = findField('duration', interactions.field.fields).fields;
|
||
durationFields[0].max = durationFields[1].max = duration;
|
||
durationFields[0].min = durationFields[1].min = 0;
|
||
|
||
// Set max value for adaptive seeking timecodes
|
||
var adaptivityFields = findField('adaptivity', interactions.field.fields).fields;
|
||
for (var i = 0; i < adaptivityFields.length; i++) {
|
||
if (adaptivityFields[i].fields) {
|
||
findField('seekTo', adaptivityFields[i].fields).max = duration;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add summary interaction
|
||
if (this.hasMainSummary()) {
|
||
var displayAt = duration - this.options.summary.displayAt;
|
||
if (displayAt < 0) {
|
||
displayAt = 0;
|
||
}
|
||
|
||
if (this.options.assets.interactions === undefined) {
|
||
this.options.assets.interactions = [];
|
||
}
|
||
|
||
this.options.assets.interactions.push({
|
||
action: this.options.summary.task,
|
||
x: 80,
|
||
y: 80,
|
||
duration: {
|
||
from: displayAt,
|
||
to: duration
|
||
},
|
||
displayType: 'button',
|
||
bigDialog: true,
|
||
className: 'h5p-summary-interaction h5p-end-summary',
|
||
label: '<p>' + this.l10n.summary + '</p>',
|
||
mainSummary: true
|
||
});
|
||
this.initInteraction(this.options.assets.interactions.length - 1);
|
||
}
|
||
|
||
if (this.currentState === InteractiveVideo.ATTACHED) {
|
||
if (!this.video.pressToPlay) {
|
||
this.addControls();
|
||
}
|
||
|
||
this.trigger('resize');
|
||
}
|
||
|
||
this.currentState = InteractiveVideo.LOADED;
|
||
};
|
||
|
||
/**
|
||
* Initialize interaction at the given index.
|
||
*
|
||
* @param {number} index
|
||
* @returns {H5P.InteractiveVideoInteraction}
|
||
*/
|
||
InteractiveVideo.prototype.initInteraction = function (index) {
|
||
var self = this;
|
||
var parameters = self.options.assets.interactions[index];
|
||
|
||
if (self.override) {
|
||
// Extend interaction parameters
|
||
var compatibilityLayer = {};
|
||
if (parameters.adaptivity && parameters.adaptivity.requireCompletion) {
|
||
compatibilityLayer.enableRetry = true;
|
||
}
|
||
H5P.jQuery.extend(parameters.action.params.behaviour, self.override, compatibilityLayer);
|
||
}
|
||
|
||
var previousState;
|
||
if (self.previousState !== undefined && self.previousState.answers !== undefined && self.previousState.answers[index] !== null) {
|
||
previousState = self.previousState.answers[index];
|
||
}
|
||
|
||
var interaction = new Interaction(parameters, self, previousState);
|
||
|
||
// handle display event
|
||
interaction.on('display', function (event) {
|
||
var $interaction = event.data;
|
||
$interaction.appendTo(self.$overlay);
|
||
|
||
// Make sure the interaction does not overflow videowrapper.
|
||
interaction.repositionToWrapper(self.$videoWrapper);
|
||
|
||
// Determine source type
|
||
var isYouTube = (self.video.pressToPlay !== undefined);
|
||
|
||
// Consider pausing the playback
|
||
delayWork(isYouTube ? 100 : null, function () {
|
||
var isPlaying = self.currentState === H5P.Video.PLAYING ||
|
||
self.currentState === H5P.Video.BUFFERING;
|
||
if (isPlaying && interaction.pause()) {
|
||
self.video.pause();
|
||
}
|
||
});
|
||
|
||
// Position label on next tick
|
||
setTimeout(function () {
|
||
interaction.positionLabel(self.$videoWrapper.width());
|
||
}, 0);
|
||
});
|
||
|
||
// handle xAPI event
|
||
interaction.on('xAPI', function(event) {
|
||
// update state
|
||
if ($.inArray(event.getVerb(), ['completed', 'answered']) !== -1) {
|
||
event.setVerb('answered');
|
||
if (interaction.isMainSummary()) {
|
||
self.complete();
|
||
}
|
||
}
|
||
if (event.data.statement.context.extensions === undefined) {
|
||
event.data.statement.context.extensions = {};
|
||
}
|
||
event.data.statement.context.extensions['http://id.tincanapi.com/extension/ending-point'] = 'PT' + Math.floor(self.video.getCurrentTime()) + 'S';
|
||
});
|
||
|
||
self.interactions.push(interaction);
|
||
|
||
return interaction;
|
||
};
|
||
|
||
/**
|
||
* Checks if the interactive video should have summary task scheduled at
|
||
* the end of the video.
|
||
*
|
||
* This is the summary created in the summary tab of the editor.
|
||
*
|
||
* @returns {boolean}
|
||
* true if this interactive video has a summary
|
||
* false otherwise
|
||
*/
|
||
InteractiveVideo.prototype.hasMainSummary = function() {
|
||
var summary = this.options.summary;
|
||
return !(summary === undefined ||
|
||
summary.displayAt === undefined ||
|
||
summary.task === undefined ||
|
||
summary.task.params === undefined ||
|
||
summary.task.params.summaries === undefined ||
|
||
!summary.task.params.summaries.length ||
|
||
summary.task.params.summaries[0].summary === undefined ||
|
||
!summary.task.params.summaries[0].summary.length);
|
||
};
|
||
|
||
/**
|
||
* Puts the tiny cute balls above the slider / seek bar.
|
||
*/
|
||
InteractiveVideo.prototype.addSliderInteractions = function () {
|
||
// Remove old dots
|
||
this.controls.$interactionsContainer.children().remove();
|
||
|
||
// Add new dots
|
||
for (var i = 0; i < this.interactions.length; i++) {
|
||
this.interactions[i].addDot(this.controls.$interactionsContainer);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Puts all the cool narrow lines around the slider / seek bar.
|
||
*/
|
||
InteractiveVideo.prototype.addBookmarks = function () {
|
||
this.bookmarksMap = {};
|
||
if (this.options.assets.bookmarks !== undefined && !this.preventSkipping) {
|
||
for (var i = 0; i < this.options.assets.bookmarks.length; i++) {
|
||
this.addBookmark(i);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Toggle bookmarks menu
|
||
*
|
||
* @method toggleBookmarksChooser
|
||
* @param {boolean} [show] Forces toggle state if set
|
||
*/
|
||
InteractiveVideo.prototype.toggleBookmarksChooser = function (show) {
|
||
if (this.controls.$bookmarks) {
|
||
show = (show === undefined ? !this.controls.$bookmarksChooser.hasClass('h5p-show') : show);
|
||
var hiding = this.controls.$bookmarksChooser.hasClass('h5p-show');
|
||
|
||
this.controls.$more.toggleClass('h5p-active', show);
|
||
this.controls.$minimalOverlay.toggleClass('h5p-show', show);
|
||
this.controls.$minimalOverlay.find('.h5p-minimal-button').toggleClass('h5p-hide', show);
|
||
this.controls.$bookmarks.toggleClass('h5p-active', show);
|
||
this.controls.$bookmarksChooser.css({maxHeight: show ? this.controlsCss.maxHeight : '32px'}).toggleClass('h5p-show', show);
|
||
|
||
// Add classes if changing visibility
|
||
this.controls.$bookmarksChooser.toggleClass('h5p-transitioning', show || hiding);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Show message saying that skipping in the video is not allowed.
|
||
*
|
||
* @param {number} offsetX offset in pixels from left side of the seek bar
|
||
*/
|
||
InteractiveVideo.prototype.showPreventSkippingMessage = function (offsetX) {
|
||
var self = this;
|
||
|
||
// Already displaying message
|
||
if (self.preventSkippingWarningTimeout) {
|
||
return;
|
||
}
|
||
|
||
// Create DOM element if not existing
|
||
if (!self.$preventSkippingMessage) {
|
||
self.$preventSkippingMessage = $('<div>', {
|
||
'class': 'h5p-prevent-skipping-message',
|
||
appendTo: self.controls.$bookmarksContainer
|
||
});
|
||
|
||
self.$preventSkippingMessageText = $('<div>', {
|
||
'class': 'h5p-prevent-skipping-message-text',
|
||
html: self.l10n.navDisabled,
|
||
appendTo: self.$preventSkippingMessage
|
||
});
|
||
}
|
||
|
||
// Move element to offset position
|
||
self.$preventSkippingMessage.css('left', offsetX);
|
||
|
||
// Show message
|
||
setTimeout(function () {
|
||
self.$preventSkippingMessage.addClass('h5p-show');
|
||
}, 0);
|
||
|
||
// Wait for a while before removing message
|
||
self.preventSkippingWarningTimeout = setTimeout(function () {
|
||
|
||
// Remove message
|
||
self.$preventSkippingMessage.removeClass('h5p-show');
|
||
|
||
// Wait a while before allowing to display warning again.
|
||
setTimeout(function () {
|
||
self.preventSkippingWarningTimeout = undefined;
|
||
}, 500);
|
||
}, 2000);
|
||
};
|
||
|
||
/**
|
||
* Puts a single cool narrow line around the slider / seek bar.
|
||
*
|
||
* @param {number} id
|
||
* @param {number} [tenth]
|
||
* @returns {H5P.jQuery}
|
||
*/
|
||
InteractiveVideo.prototype.addBookmark = function (id, tenth) {
|
||
var self = this;
|
||
var bookmark = self.options.assets.bookmarks[id];
|
||
|
||
// Avoid stacking of bookmarks.
|
||
if (tenth === undefined) {
|
||
tenth = Math.floor(bookmark.time * 10) / 10;
|
||
}
|
||
|
||
// Create bookmark element for the seek bar.
|
||
var $bookmark = self.bookmarksMap[tenth] = $('<div class="h5p-bookmark" style="left:' + (bookmark.time * self.oneSecondInPercentage) + '%"><div class="h5p-bookmark-label"><div class="h5p-bookmark-text">' + bookmark.label + '</div></div></div>')
|
||
.appendTo(self.controls.$bookmarksContainer)
|
||
.data('id', id)
|
||
.hover(function () {
|
||
if (self.bookmarkTimeout !== undefined) {
|
||
clearTimeout(self.bookmarkTimeout);
|
||
}
|
||
self.controls.$bookmarksContainer.children('.h5p-show').removeClass('h5p-show');
|
||
$bookmark.addClass('h5p-show');
|
||
}, function () {
|
||
self.bookmarkTimeout = setTimeout(function () {
|
||
$bookmark.removeClass('h5p-show');
|
||
}, 2000);
|
||
});
|
||
|
||
// Set max size of label to the size of the controls to the right.
|
||
$bookmark.find('.h5p-bookmark-label').css('maxWidth', parseInt(self.controls.$slider.parent().css('marginRight')) - 35);
|
||
|
||
// Creat list if non-existent (note that it isn't allowed to have empty lists in HTML)
|
||
if (self.controls.$bookmarksList === undefined) {
|
||
self.controls.$bookmarksList = $('<ol></ol>')
|
||
.insertAfter(self.controls.$bookmarksChooser.find('h3'));
|
||
}
|
||
|
||
// Create list element for bookmark
|
||
var $li = $('<li role="button" tabindex="1">' + bookmark.label + '</li>')
|
||
.click(function () {
|
||
if (self.currentState !== H5P.Video.PLAYING) {
|
||
$bookmark.mouseover().mouseout();
|
||
setTimeout(function () {self.timeUpdate(self.video.getCurrentTime());}, 0);
|
||
}
|
||
|
||
if (self.controls.$more.hasClass('h5p-active')) {
|
||
self.controls.$more.click();
|
||
}
|
||
else {
|
||
self.toggleBookmarksChooser(false);
|
||
}
|
||
self.video.play();
|
||
self.video.seek(bookmark.time);
|
||
});
|
||
|
||
// Insert bookmark in the correct place.
|
||
var $next = self.controls.$bookmarksList.children(':eq(' + id + ')');
|
||
if ($next.length !== 0) {
|
||
$li.insertBefore($next);
|
||
}
|
||
else {
|
||
$li.appendTo(self.controls.$bookmarksList);
|
||
}
|
||
|
||
// Listen for changes to our id.
|
||
self.on('bookmarksChanged', function (event) {
|
||
var index = event.data.index;
|
||
var number = event.data.number;
|
||
if (index === id && number < 0) {
|
||
// We are removing this item.
|
||
$li.remove();
|
||
delete self.bookmarksMap[tenth];
|
||
// self.off('bookmarksChanged');
|
||
}
|
||
else if (id >= index) {
|
||
// We must update our id.
|
||
id += number;
|
||
$bookmark.data('id', id);
|
||
}
|
||
});
|
||
|
||
// Tell others we have added a new bookmark.
|
||
self.trigger('bookmarkAdded', {'bookmark': $bookmark});
|
||
return $bookmark;
|
||
};
|
||
|
||
/**
|
||
* Attach video controls to the given wrapper.
|
||
*
|
||
* @param {H5P.jQuery} $wrapper
|
||
*/
|
||
InteractiveVideo.prototype.attachControls = function ($wrapper) {
|
||
var self = this;
|
||
|
||
// The controls consist of three different sections:
|
||
var $left = $('<div/>', {'class': 'h5p-controls-left', appendTo: $wrapper});
|
||
var $right = $('<div/>', {'class': 'h5p-controls-right', appendTo: $wrapper});
|
||
var $slider = $('<div/>', {'class': 'h5p-control h5p-slider', appendTo: $wrapper});
|
||
if (self.preventSkipping) {
|
||
$slider.addClass('disabled');
|
||
}
|
||
|
||
// Keep track of all controls
|
||
self.controls = {};
|
||
|
||
// Add play button/pause button
|
||
self.controls.$play = self.createButton('play', 'h5p-control h5p-pause', $left, function () {
|
||
if (self.controls.$play.hasClass('h5p-pause') && !self.controls.$play.hasClass('h5p-disabled')) {
|
||
|
||
// Auto toggle fullscreen on play if on a small device
|
||
var isSmallDevice = screen ? Math.min(screen.width, screen.height) <= self.width : true;
|
||
if (!self.hasFullScreen && isSmallDevice && self.$container.hasClass('h5p-standalone') && self.$container.hasClass('h5p-minimal')) {
|
||
self.toggleFullScreen();
|
||
}
|
||
self.video.play();
|
||
}
|
||
else {
|
||
self.video.pause();
|
||
}
|
||
});
|
||
|
||
// Add button for rewinding 10 seconds
|
||
|
||
if (self.showRewind10) {
|
||
self.controls.$rewind10 = self.createButton('rewind10', 'h5p-control', $left, function () {
|
||
if (self.video.getCurrentTime() > 0) { // video will play otherwise
|
||
var newTime = Math.max(self.video.getCurrentTime()-10, 0);
|
||
self.video.seek(newTime);
|
||
if (self.currentState === H5P.Video.PAUSED) {
|
||
self.timeUpdate(newTime);
|
||
}
|
||
if (self.currentState === H5P.Video.ENDED) {
|
||
self.video.play();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Wraps a specifc handler to do some generic operations each time the handler is triggered.
|
||
*
|
||
* @private
|
||
* @param {string} button Name of controls button
|
||
* @param {string} menu Name of controls menu
|
||
*/
|
||
var createPopupMenuHandler = function (button, menu) {
|
||
return function () {
|
||
var $button = self.controls[button];
|
||
if ($button.hasClass('h5p-disabled')) {
|
||
return; // Not active
|
||
}
|
||
|
||
var $menu = self.controls[menu];
|
||
if (!$button.hasClass('h5p-active')) {
|
||
// Opening
|
||
$button.addClass('h5p-active');
|
||
$menu.addClass('h5p-show');
|
||
}
|
||
else {
|
||
// Closing
|
||
$button.removeClass('h5p-active');
|
||
$menu.removeClass('h5p-show');
|
||
}
|
||
};
|
||
};
|
||
|
||
var isIpad = function () {
|
||
return navigator.userAgent.indexOf('iPad') !== -1;
|
||
};
|
||
|
||
var isAndroid = function () {
|
||
return navigator.userAgent.indexOf('Android') !== -1;
|
||
};
|
||
|
||
/**
|
||
* Indicates if bookmarks are available.
|
||
* Only available for controls.
|
||
* @private
|
||
*/
|
||
var hasBookmarks = self.options.assets.bookmarks && self.options.assets.bookmarks.length;
|
||
var bookmarksEnabled = self.editor || (hasBookmarks && !self.preventSkipping);
|
||
|
||
// Add bookmark controls
|
||
if (bookmarksEnabled) {
|
||
// Popup dialog for choosing bookmarks
|
||
self.controls.$bookmarksChooser = H5P.jQuery('<div/>', {
|
||
'class': 'h5p-chooser h5p-bookmarks',
|
||
html: '<h3>' + self.l10n.bookmarks + '</h3>',
|
||
appendTo: self.$container
|
||
});
|
||
|
||
// Adding close button to bookmarks-menu
|
||
self.controls.$bookmarksChooser.append($('<span>', {
|
||
'class': 'h5p-chooser-close-button',
|
||
click: function () {
|
||
self.toggleBookmarksChooser();
|
||
}
|
||
}));
|
||
|
||
if (self.showRewind10) {
|
||
self.controls.$bookmarksChooser.addClass('h5p-rewind-displacement');
|
||
}
|
||
|
||
// Button for opening bookmark popup
|
||
self.controls.$bookmarks = self.createButton('bookmarks', 'h5p-control', $left, function () {
|
||
self.toggleBookmarksChooser();
|
||
});
|
||
self.controls.$bookmarksChooser.bind('transitionend', function () {
|
||
self.controls.$bookmarksChooser.removeClass('h5p-transitioning');
|
||
})
|
||
}
|
||
|
||
// Current time for minimal display
|
||
var $time = $('<div class="h5p-control h5p-simple-time"><span class="h5p-current">0:00</span></div>').appendTo($left);
|
||
self.controls.$currentTime = $time.find('.h5p-current');
|
||
|
||
// Add fullscreen button
|
||
if (!self.editor && H5P.canHasFullScreen !== false) {
|
||
self.controls.$fullscreen = self.createButton('fullscreen', 'h5p-control', $right, function () {
|
||
self.toggleFullScreen();
|
||
});
|
||
}
|
||
|
||
// TODO: Do not add until qualities are present?
|
||
// Add popup for selecting video quality
|
||
self.controls.$qualityChooser = H5P.jQuery('<div/>', {
|
||
'class': 'h5p-chooser h5p-quality',
|
||
html: '<h3>' + self.l10n.quality + '</h3>',
|
||
appendTo: self.$container
|
||
});
|
||
|
||
// Adding close button to quality-menu
|
||
self.controls.$qualityChooser.append($('<span>', {
|
||
'class': 'h5p-chooser-close-button',
|
||
click: function () {
|
||
if (self.isMinimal) {
|
||
self.controls.$more.click();
|
||
}
|
||
else {
|
||
self.controls.$qualityButton.click();
|
||
}
|
||
}
|
||
}));
|
||
|
||
// Button for opening video quality selection dialog
|
||
self.controls.$qualityButton = self.createButton('quality', 'h5p-control h5p-disabled', $right, createPopupMenuHandler('$qualityButton', '$qualityChooser'));
|
||
|
||
// Add volume button control (toggle mute)
|
||
if (!isAndroid() && !isIpad()) {
|
||
self.controls.$volume = self.createButton('mute', 'h5p-control', $right, function () {
|
||
if (self.controls.$volume.hasClass('h5p-muted')) {
|
||
self.controls.$volume.removeClass('h5p-muted').attr('title', self.l10n.mute);
|
||
self.video.unMute();
|
||
}
|
||
else {
|
||
self.controls.$volume.addClass('h5p-muted').attr('title', self.l10n.unmute);
|
||
self.video.mute();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add popup for selecting playback rate
|
||
self.controls.$playbackRateChooser = H5P.jQuery('<div/>', {
|
||
'class': 'h5p-chooser h5p-playbackRate',
|
||
html: '<h3>' + self.l10n.playbackRate + '</h3>',
|
||
appendTo: self.$container
|
||
});
|
||
|
||
// Button for opening video playback rate selection dialog
|
||
self.controls.$playbackRateButton = self.createButton('playbackRate', 'h5p-control h5p-disabled', $right, createPopupMenuHandler('$playbackRateButton', '$playbackRateChooser'));
|
||
|
||
// Add more button for collapsing controls when there's little space
|
||
|
||
// Add overlay for display controls inside
|
||
self.controls.$minimalOverlay = H5P.jQuery('<div/>', {
|
||
'class': 'h5p-minimal-overlay',
|
||
appendTo: self.$container
|
||
});
|
||
|
||
// Use wrapper to center controls
|
||
var $minimalWrap = H5P.jQuery('<div/>', {
|
||
'class': 'h5p-minimal-wrap',
|
||
appendTo: self.controls.$minimalOverlay
|
||
});
|
||
|
||
// Add buttons to wrapper
|
||
var $buttons = H5P.jQuery([]);
|
||
|
||
// Bookmarks
|
||
if (bookmarksEnabled) {
|
||
$buttons = $buttons.add(self.createButton('bookmarks', 'h5p-minimal-button', $minimalWrap, function () {
|
||
$buttons.addClass('h5p-hide');
|
||
self.toggleBookmarksChooser(true);
|
||
}, true));
|
||
}
|
||
|
||
// Quality
|
||
self.controls.$qualityButtonMinimal = self.createButton('quality', 'h5p-minimal-button h5p-disabled', $minimalWrap, function () {
|
||
if (!self.controls.$qualityButton.hasClass('h5p-disabled')) {
|
||
$buttons.addClass('h5p-hide');
|
||
self.controls.$qualityButton.click();
|
||
}
|
||
}, true);
|
||
$buttons = $buttons.add(self.controls.$qualityButtonMinimal);
|
||
|
||
// Playback rate
|
||
self.controls.$playbackRateButtonMinimal = self.createButton('playbackRate', 'h5p-minimal-button h5p-disabled', $minimalWrap, function () {
|
||
if (!self.controls.$playbackRateButton.hasClass('h5p-disabled')) {
|
||
$buttons.addClass('h5p-hide');
|
||
self.controls.$playbackRateButton.click();
|
||
}
|
||
}, true);
|
||
$buttons = $buttons.add(self.controls.$playbackRateButtonMinimal);
|
||
|
||
// Add control for displaying overlay with buttons
|
||
self.controls.$more = self.createButton('more', 'h5p-control', $right, function () {
|
||
if (self.controls.$more.hasClass('h5p-active')) {
|
||
// Close overlay
|
||
self.controls.$minimalOverlay.removeClass('h5p-show');
|
||
self.controls.$more.removeClass('h5p-active');
|
||
self.toggleBookmarksChooser(false);
|
||
if (self.controls.$qualityButton && self.controls.$qualityButton.hasClass('h5p-active')) {
|
||
self.controls.$qualityButton.click();
|
||
}
|
||
if (self.controls.$playbackRateButton && self.controls.$playbackRateButton.hasClass('h5p-active')) {
|
||
self.controls.$playbackRateButton.click();
|
||
}
|
||
setTimeout(function () {
|
||
$buttons.removeClass('h5p-hide');
|
||
}, 150);
|
||
}
|
||
else {
|
||
// Open overlay
|
||
self.controls.$minimalOverlay.addClass('h5p-show');
|
||
self.controls.$more.addClass('h5p-active');
|
||
|
||
// Make sure splash screen is removed.
|
||
self.removeSplash();
|
||
}
|
||
|
||
// Make sure sub menus are closed
|
||
if (bookmarksEnabled) {
|
||
self.controls.$bookmarksChooser.add(self.controls.$qualityChooser).removeClass('h5p-show');
|
||
}
|
||
else {
|
||
self.controls.$qualityChooser.removeClass('h5p-show');
|
||
self.controls.$playbackRateChooser.removeClass('h5p-show');
|
||
}
|
||
});
|
||
|
||
self.addQualityChooser();
|
||
self.addPlaybackRateChooser();
|
||
|
||
// Add display for time elapsed and duration
|
||
$time = $('<div class="h5p-control h5p-time"><span class="h5p-current">0:00</span> / <span class="h5p-total">0:00</span></div>').appendTo($right);
|
||
self.controls.$currentTime = self.controls.$currentTime.add($time.find('.h5p-current'));
|
||
self.controls.$totalTime = $time.find('.h5p-total');
|
||
|
||
// Add containers for objects that will be displayed around the seekbar
|
||
self.controls.$interactionsContainer = $('<div/>', {'class': 'h5p-interactions-container', appendTo: $slider});
|
||
self.controls.$bookmarksContainer = $('<div/>', {'class': 'h5p-bookmarks-container', appendTo: $slider});
|
||
|
||
// Add seekbar/timeline
|
||
self.hasPlayPromise = false;
|
||
self.hasQueuedPause = false;
|
||
self.delayed = false;
|
||
self.controls.$slider = $('<div/>', {appendTo: $slider}).slider({
|
||
value: 0,
|
||
step: 0.01,
|
||
orientation: 'horizontal',
|
||
range: 'min',
|
||
max: 0,
|
||
start: function () {
|
||
if (self.currentState === InteractiveVideo.SEEKING) {
|
||
return; // Prevent double start on touch devies!
|
||
}
|
||
|
||
if (!self.delayedState) {
|
||
|
||
// Set play state if video was ended
|
||
if (self.currentState === H5P.Video.ENDED) {
|
||
self.lastState = H5P.Video.PLAYING;
|
||
}
|
||
// Set current state if it is not buffering, otherwise keep last state
|
||
else if (self.currentState !== H5P.Video.BUFFERING || !self.lastState) {
|
||
self.lastState = self.currentState;
|
||
}
|
||
}
|
||
|
||
// Delay state change to prevent double clicks registering
|
||
self.delayedState = true;
|
||
clearTimeout(self.delayTimeout);
|
||
self.delayTimeout = setTimeout(function () {
|
||
self.delayedState = false;
|
||
}, 200);
|
||
|
||
if (self.hasPlayPromise) {
|
||
// Queue pause if play has not been resolved
|
||
self.hasQueuedPause = true;
|
||
}
|
||
else {
|
||
self.video.pause();
|
||
}
|
||
self.currentState = InteractiveVideo.SEEKING;
|
||
|
||
// Make sure splash screen is removed.
|
||
self.removeSplash();
|
||
},
|
||
slide: function (e, ui) {
|
||
// Update elapsed time
|
||
self.video.seek(ui.value);
|
||
self.updateInteractions(ui.value);
|
||
self.controls.$currentTime.html(InteractiveVideo.humanizeTime(ui.value));
|
||
},
|
||
stop: function (e, ui) {
|
||
self.currentState = self.lastState;
|
||
self.video.seek(ui.value);
|
||
|
||
// Must recreate interactions because "continue" detaches them and they
|
||
// are not "re-updated" if they have only been detached (not completely removed)
|
||
self.recreateCurrentInteractions();
|
||
|
||
var startPlaying = self.lastState === H5P.Video.PLAYING ||
|
||
self.lastState === H5P.Video.VIDEO_CUED || self.hasQueuedPause;
|
||
if (self.hasPlayPromise) {
|
||
// Skip pausing when play promise is resolved
|
||
self.hasQueuedPause = false;
|
||
}
|
||
else if (startPlaying) {
|
||
self.hasQueuedPause = false;
|
||
var play = self.video.play();
|
||
self.hasQueuedPause = false;
|
||
|
||
// Handle play as a Promise
|
||
if (play && play.then) {
|
||
self.hasPlayPromise = true;
|
||
play.then(function () {
|
||
|
||
// Pause at next cycle to not conflict with play
|
||
setTimeout(function () {
|
||
|
||
// Pause on queue or on interactions without having to recreate them
|
||
if (self.hasQueuedPause || self.hasActivePauseInteraction()) {
|
||
self.video.pause();
|
||
}
|
||
self.hasPlayPromise = false;
|
||
}, 0);
|
||
});
|
||
}
|
||
else {
|
||
// Pause on interactions without having to recreate them
|
||
if (self.hasActivePauseInteraction()) {
|
||
// YouTube needs to play after seek to not get stuck buffering.
|
||
self.video.play();
|
||
setTimeout(function () {
|
||
self.video.pause();
|
||
}, 50);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
self.timeUpdate(ui.value);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Disable slider
|
||
if (self.preventSkipping) {
|
||
self.controls.$slider.slider('disable');
|
||
self.controls.$slider.click(function (e) {
|
||
self.showPreventSkippingMessage(e.offsetX);
|
||
return false;
|
||
});
|
||
}
|
||
|
||
/* Show bookmarks, except when youtube is used on iPad */
|
||
if (self.showBookmarksmenuOnLoad && self.video.pressToPlay === false) {
|
||
self.toggleBookmarksChooser(true);
|
||
}
|
||
|
||
// Add buffered status to seekbar
|
||
self.controls.$buffered = $('<div/>', {'class': 'h5p-buffered', prependTo: self.controls.$slider});
|
||
};
|
||
|
||
/**
|
||
* Check if any active interactions should be paused
|
||
* @return {boolean} True if an interaction should be paused
|
||
*/
|
||
InteractiveVideo.prototype.hasActivePauseInteraction = function () {
|
||
var hasActivePauseInteractions = false;
|
||
this.interactions.forEach(function (interaction) {
|
||
|
||
// Interaction is visible and should be paused
|
||
if (interaction.getElement() && interaction.pause()) {
|
||
hasActivePauseInteractions = true;
|
||
}
|
||
});
|
||
|
||
return hasActivePauseInteractions;
|
||
};
|
||
|
||
/**
|
||
* Help create control buttons.
|
||
*
|
||
* @param {string} type
|
||
* @param {string} extraClass
|
||
* @param {H5P.jQuery} $target
|
||
* @param {function} handler
|
||
* @param {boolean} [text] Determines if button should set text or title
|
||
*/
|
||
InteractiveVideo.prototype.createButton = function (type, extraClass, $target, handler, text) {
|
||
var self = this;
|
||
var options = {
|
||
role: 'button',
|
||
tabindex: 0,
|
||
'class': (extraClass === undefined ? '' : extraClass + ' ') + 'h5p-' + type,
|
||
on: {
|
||
click: function () {
|
||
handler.call(this);
|
||
},
|
||
keypress: function () {
|
||
if (event.which === 32) { // Space
|
||
handler.call(this);
|
||
}
|
||
}
|
||
},
|
||
appendTo: $target
|
||
};
|
||
options[text ? 'text' : 'title'] = self.l10n[type];
|
||
return H5P.jQuery('<div/>', options);
|
||
};
|
||
|
||
/**
|
||
* Add a dialog for selecting video quality.
|
||
*/
|
||
InteractiveVideo.prototype.addQualityChooser = function () {
|
||
var self = this;
|
||
|
||
if (!this.video.getQualities) {
|
||
return;
|
||
}
|
||
|
||
var qualities = this.video.getQualities();
|
||
if (!qualities || this.controls.$qualityButton === undefined ||
|
||
!this.controls.$qualityButton.hasClass('h5p-disabled')) {
|
||
return;
|
||
}
|
||
|
||
var currentQuality = this.video.getQuality();
|
||
|
||
var html = '';
|
||
for (var i = 0; i < qualities.length; i++) {
|
||
var quality = qualities[i];
|
||
html += '<li role="button" tabIndex="1" data-quality="' + quality.name + '" class="' + (quality.name === currentQuality ? 'h5p-selected' : '') + '">' + quality.label + '</li>';
|
||
}
|
||
|
||
var $list = $('<ol>' + html + '</ol>').appendTo(this.controls.$qualityChooser);
|
||
$list.children().click(function () {
|
||
self.video.setQuality($(this).attr('data-quality'));
|
||
if (self.controls.$more.hasClass('h5p-active')) {
|
||
self.controls.$more.click();
|
||
}
|
||
else {
|
||
self.controls.$qualityButton.click();
|
||
}
|
||
});
|
||
|
||
// Enable quality chooser button
|
||
this.controls.$qualityButton.add(this.controls.$qualityButtonMinimal).removeClass('h5p-disabled');
|
||
};
|
||
|
||
/**
|
||
* Add a dialog for selecting video playback rate.
|
||
*/
|
||
InteractiveVideo.prototype.addPlaybackRateChooser = function () {
|
||
var self = this;
|
||
|
||
if (!this.video.getPlaybackRates) {
|
||
return;
|
||
}
|
||
|
||
/*
|
||
* The IE 11 for no reason jumps to a playback rate of 1 if the slider is
|
||
* moved or if you pause and restart the video. Until a workaround has been
|
||
* found, the playback rate chooser is deactivated for the IE 11.
|
||
*/
|
||
var isIE11 = navigator.userAgent.match(/Trident.*rv[ :]*11\./) ? true : false;
|
||
if (isIE11) {
|
||
return;
|
||
}
|
||
|
||
var playbackRates = this.video.getPlaybackRates();
|
||
|
||
// don't enable playback rate chooser if only default rate can be chosen
|
||
if (playbackRates.length < 2) {
|
||
return;
|
||
}
|
||
|
||
if (!playbackRates || this.controls.$playbackRateButton === undefined ||
|
||
!this.controls.$playbackRateButton.hasClass('h5p-disabled')) {
|
||
return;
|
||
}
|
||
|
||
var currentPlaybackRate = this.video.getPlaybackRate();
|
||
|
||
var html = '';
|
||
for (var i = 0; i < playbackRates.length; i++) {
|
||
var playbackRate = playbackRates[i];
|
||
html += '<li role="button" tabIndex="1" playback-rate="' + playbackRate + '" class="' + (playbackRate === currentPlaybackRate ? 'h5p-selected' : '') + '">' + playbackRate + '</li>';
|
||
}
|
||
|
||
var $list = $('<ol>' + html + '</ol>').appendTo(this.controls.$playbackRateChooser);
|
||
$list.children().click(function () {
|
||
self.video.setPlaybackRate($(this).attr('playback-rate'));
|
||
if (self.controls.$more.hasClass('h5p-active')) {
|
||
self.controls.$more.click();
|
||
}
|
||
else {
|
||
self.controls.$playbackRateButton.click();
|
||
}
|
||
});
|
||
|
||
// Enable playback rate chooser button
|
||
this.controls.$playbackRateButton.add(this.controls.$playbackRateButtonMinimal).removeClass('h5p-disabled');
|
||
};
|
||
|
||
/**
|
||
* Create loop that constantly updates the buffer bar
|
||
*/
|
||
InteractiveVideo.prototype.startUpdatingBufferBar = function () {
|
||
var self = this;
|
||
if (self.bufferLoop) {
|
||
return;
|
||
}
|
||
|
||
var updateBufferBar = function () {
|
||
var buffered = self.video.getBuffered();
|
||
if (buffered && self.controls.$buffered) {
|
||
self.controls.$buffered.css('width', buffered + '%');
|
||
}
|
||
self.bufferLoop = setTimeout(updateBufferBar, 500);
|
||
};
|
||
updateBufferBar();
|
||
};
|
||
|
||
/**
|
||
* Resize the video to fit the wrapper.
|
||
*/
|
||
InteractiveVideo.prototype.resize = function () {
|
||
// Not yet attached
|
||
if (!this.$container) {
|
||
return;
|
||
}
|
||
|
||
var self = this;
|
||
var fullscreenOn = this.$container.hasClass('h5p-fullscreen') || this.$container.hasClass('h5p-semi-fullscreen');
|
||
|
||
// Resize the controls the first time we're visible
|
||
if (!this.justVideo && this.controlsSized === undefined) {
|
||
this.resizeControls();
|
||
}
|
||
|
||
this.$videoWrapper.css({
|
||
marginTop: '',
|
||
marginLeft: '',
|
||
width: '',
|
||
height: ''
|
||
});
|
||
this.video.trigger('resize');
|
||
|
||
var width;
|
||
var videoHeight;
|
||
var controlsHeight = this.justVideo ? 0 : this.$controls.height();
|
||
var containerHeight = this.$container.height();
|
||
if (fullscreenOn) {
|
||
videoHeight = this.$videoWrapper.height();
|
||
|
||
if (videoHeight + controlsHeight <= containerHeight) {
|
||
this.$videoWrapper.css('marginTop', (containerHeight - controlsHeight - videoHeight) / 2);
|
||
width = this.$videoWrapper.width();
|
||
}
|
||
else {
|
||
var ratio = this.$videoWrapper.width() / videoHeight;
|
||
var height = containerHeight - controlsHeight;
|
||
width = height * ratio;
|
||
this.$videoWrapper.css({
|
||
marginLeft: (this.$container.width() - width) / 2,
|
||
width: width,
|
||
height: height
|
||
});
|
||
}
|
||
|
||
// Resize again to fit the new container size.
|
||
this.video.trigger('resize');
|
||
}
|
||
else {
|
||
width = this.$container.width();
|
||
}
|
||
|
||
// Set base font size. Don't allow it to fall below original size.
|
||
this.scaledFontSize = (width > this.width) ? (this.fontSize * (width / this.width)) : this.fontSize;
|
||
this.$container.css('fontSize', this.scaledFontSize + 'px');
|
||
|
||
if (!this.editor) {
|
||
if (width < this.width) {
|
||
if (!this.$container.hasClass('h5p-minimal')) {
|
||
// Use minimal controls
|
||
this.$container.addClass('h5p-minimal');
|
||
this.resizeControls();
|
||
}
|
||
}
|
||
else if (this.$container.hasClass('h5p-minimal')) {
|
||
// Use normal controls
|
||
this.$container.removeClass('h5p-minimal');
|
||
this.resizeControls();
|
||
}
|
||
}
|
||
|
||
this.isMinimal = this.$container.hasClass('h5p-minimal');
|
||
|
||
// Reset control popup calculations
|
||
var popupControlsHeight = this.$videoWrapper.height();
|
||
this.controlsCss = {
|
||
bottom: '',
|
||
maxHeight: popupControlsHeight + 'px'
|
||
};
|
||
|
||
if (fullscreenOn) {
|
||
|
||
// Make sure popup controls are on top of video wrapper
|
||
var offsetBottom = controlsHeight;
|
||
|
||
// Center popup menus
|
||
if (videoHeight + controlsHeight <= containerHeight) {
|
||
offsetBottom = controlsHeight + ((containerHeight - controlsHeight - videoHeight) / 2);
|
||
}
|
||
this.controlsCss.bottom = offsetBottom + 'px';
|
||
}
|
||
|
||
if (this.controls && this.controls.$minimalOverlay) {
|
||
this.controls.$minimalOverlay.css(this.controlsCss);
|
||
}
|
||
this.$container.find('.h5p-chooser').css(this.controlsCss);
|
||
|
||
// Resize start screen
|
||
if (!this.editor) {
|
||
this.resizeMobileView();
|
||
if (this.$splash !== undefined) {
|
||
this.resizeStartScreen();
|
||
}
|
||
}
|
||
else if (this.editor.dnb) {
|
||
this.editor.dnb.dnr.setContainerEm(this.scaledFontSize);
|
||
}
|
||
|
||
this.resizeInteractions();
|
||
};
|
||
|
||
/**
|
||
* Make sure that the jQuery UI scrollbar fits between the controls
|
||
*/
|
||
InteractiveVideo.prototype.resizeControls = function () {
|
||
var left = this.$controls.children('.h5p-controls-left').width();
|
||
var right = this.$controls.children('.h5p-controls-right').width();
|
||
if (left || right) {
|
||
this.controlsSized = true;
|
||
|
||
// Set correct margins for timeline
|
||
this.controls.$slider.parent().css({
|
||
marginLeft: left,
|
||
marginRight: right
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Determine if interactive video should be in mobile view.
|
||
*/
|
||
InteractiveVideo.prototype.resizeMobileView = function () {
|
||
// IV not init
|
||
if (isNaN(this.currentState)) {
|
||
return;
|
||
}
|
||
// Width to font size ratio threshold
|
||
var widthToEmThreshold = 30;
|
||
var ivWidth = this.$container.width();
|
||
var fontSize = parseInt(this.$container.css('font-size'), 10);
|
||
var widthToEmRatio = ivWidth / fontSize;
|
||
if (widthToEmRatio < widthToEmThreshold) {
|
||
// Resize interactions in mobile view
|
||
this.resizeInteractions();
|
||
|
||
if (!this.isMobileView) {
|
||
this.$container.addClass('mobile');
|
||
this.isMobileView = true;
|
||
|
||
// should not close overlay for required interactions, but still show dialog
|
||
if(this.hasUncompletedRequiredInteractions()) {
|
||
var $dialog = $('.h5p-dialog', this.$container);
|
||
$dialog.show();
|
||
} else {
|
||
this.dnb.dialog.closeOverlay();
|
||
}
|
||
|
||
this.recreateCurrentInteractions();
|
||
}
|
||
}
|
||
else {
|
||
if (this.isMobileView) {
|
||
// Close dialog because we can not know if it will turn into a poster
|
||
if (this.dnb && this.dnb.dialog && !this.hasUncompletedRequiredInteractions()) {
|
||
this.dnb.dialog.close();
|
||
}
|
||
this.$container.removeClass('mobile');
|
||
this.isMobileView = false;
|
||
this.recreateCurrentInteractions();
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Resize all interactions.
|
||
*/
|
||
InteractiveVideo.prototype.resizeInteractions = function () {
|
||
// IV not init
|
||
if (isNaN(this.currentState)) {
|
||
return;
|
||
}
|
||
|
||
var self = this;
|
||
this.interactions.forEach(function (interaction) {
|
||
interaction.resizeInteraction();
|
||
interaction.repositionToWrapper(self.$videoWrapper);
|
||
interaction.positionLabel(self.$videoWrapper.width());
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Recreate interactions
|
||
*/
|
||
InteractiveVideo.prototype.recreateCurrentInteractions = function () {
|
||
this.dnb.blurAll();
|
||
this.interactions.forEach(function (interaction) {
|
||
interaction.reCreateInteraction();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Resizes the start screen.
|
||
*/
|
||
InteractiveVideo.prototype.resizeStartScreen = function () {
|
||
var descriptionSizeEm = 0.8;
|
||
var titleSizeEm = 1.5;
|
||
|
||
var playFontSizeThreshold = 10;
|
||
|
||
var staticWidthToFontRatio = 50;
|
||
var staticMobileViewThreshold = 510;
|
||
|
||
var hasDescription = true;
|
||
var hasTitle = true;
|
||
|
||
// Scale up width to font ratio if one component is missing
|
||
if (this.startScreenOptions.shortStartDescription === undefined ||
|
||
!this.startScreenOptions.shortStartDescription.length) {
|
||
hasDescription = false;
|
||
if (this.startScreenOptions.hideStartTitle) {
|
||
hasTitle = false;
|
||
staticWidthToFontRatio = 45;
|
||
}
|
||
}
|
||
|
||
var $splashDescription = $('.h5p-splash-description', this.$splash);
|
||
var $splashTitle = $('.h5p-splash-title', this.$splash);
|
||
var $tmpDescription = $splashDescription.clone()
|
||
.css('position', 'absolute')
|
||
.addClass('minimum-font-size')
|
||
.appendTo($splashDescription.parent());
|
||
var $tmpTitle = $splashTitle.clone()
|
||
.css('position', 'absolute')
|
||
.addClass('minimum-font-size')
|
||
.appendTo($splashTitle.parent());
|
||
var descriptionFontSizeThreshold = parseInt($tmpDescription.css('font-size'), 10);
|
||
var titleFontSizeThreshold = parseInt($tmpTitle.css('font-size'), 10);
|
||
|
||
// Determine new font size for splash screen from container width
|
||
var containerWidth = this.$container.width();
|
||
var newFontSize = parseInt(containerWidth / staticWidthToFontRatio, 10);
|
||
|
||
if (!hasDescription) {
|
||
if (hasTitle && newFontSize < descriptionFontSizeThreshold) {
|
||
newFontSize = descriptionFontSizeThreshold;
|
||
}
|
||
else if (newFontSize < playFontSizeThreshold) {
|
||
newFontSize = playFontSizeThreshold;
|
||
}
|
||
}
|
||
|
||
// Determine if we should add mobile view
|
||
if (containerWidth < staticMobileViewThreshold) {
|
||
this.$splash.addClass('mobile');
|
||
}
|
||
else {
|
||
this.$splash.removeClass('mobile');
|
||
}
|
||
|
||
// Minimum font sizes
|
||
if (newFontSize * descriptionSizeEm < descriptionFontSizeThreshold) {
|
||
$splashDescription.addClass('minimum-font-size');
|
||
}
|
||
else {
|
||
$splashDescription.removeClass('minimum-font-size');
|
||
}
|
||
|
||
if (newFontSize * titleSizeEm < titleFontSizeThreshold) {
|
||
$splashTitle.addClass('minimum-font-size');
|
||
}
|
||
else {
|
||
$splashTitle.removeClass('minimum-font-size');
|
||
}
|
||
|
||
// Set new font size
|
||
this.$splash.css('font-size', newFontSize);
|
||
$tmpDescription.remove();
|
||
$tmpTitle.remove();
|
||
};
|
||
|
||
/**
|
||
* Toggle enter or exit fullscreen mode.
|
||
*/
|
||
InteractiveVideo.prototype.toggleFullScreen = function () {
|
||
var self = this;
|
||
|
||
if (H5P.isFullscreen || this.$container.hasClass('h5p-fullscreen') || this.$container.hasClass('h5p-semi-fullscreen')) {
|
||
// Cancel fullscreen
|
||
if (H5P.exitFullScreen !== undefined && H5P.fullScreenBrowserPrefix !== undefined) {
|
||
H5P.exitFullScreen();
|
||
}
|
||
else {
|
||
// Use old system
|
||
if (H5P.fullScreenBrowserPrefix === undefined) {
|
||
// Click button to disable fullscreen
|
||
$('.h5p-disable-fullscreen').click();
|
||
}
|
||
else {
|
||
// Exit full screen
|
||
if (H5P.fullScreenBrowserPrefix === '') {
|
||
window.top.document.exitFullScreen();
|
||
}
|
||
else if (H5P.fullScreenBrowserPrefix === 'ms') {
|
||
window.top.document.msExitFullscreen();
|
||
}
|
||
else {
|
||
window.top.document[H5P.fullScreenBrowserPrefix + 'CancelFullScreen']();
|
||
}
|
||
}
|
||
|
||
// Manually trigger event that updates fullscreen icon
|
||
self.trigger('exitFullScreen');
|
||
}
|
||
}
|
||
else {
|
||
H5P.fullScreen(this.$container, this);
|
||
|
||
if (H5P.exitFullScreen === undefined) {
|
||
// Old system; manually trigger the event that updates the fullscreen icon
|
||
self.trigger('enterFullScreen');
|
||
}
|
||
}
|
||
|
||
// Resize all interactions
|
||
this.resizeInteractions();
|
||
};
|
||
|
||
/**
|
||
* Called when the time of the video changes.
|
||
* Makes sure to update all UI elements.
|
||
*
|
||
* @param {number} time
|
||
*/
|
||
InteractiveVideo.prototype.timeUpdate = function (time) {
|
||
var self = this;
|
||
|
||
// Scroll slider
|
||
if (time >= 0) {
|
||
try {
|
||
self.controls.$slider.slider('option', 'value', time);
|
||
}
|
||
catch (err) {
|
||
// Prevent crashing when changing lib. Exit function
|
||
return;
|
||
}
|
||
}
|
||
|
||
self.updateInteractions(time);
|
||
|
||
setTimeout(function () {
|
||
if (self.currentState === H5P.Video.PLAYING ||
|
||
(self.currentState === H5P.Video.BUFFERING && self.lastState === H5P.Video.PLAYING)
|
||
) {
|
||
self.timeUpdate(self.video.getCurrentTime());
|
||
}
|
||
}, 40); // 25 fps
|
||
};
|
||
|
||
InteractiveVideo.prototype.updateInteractions = function (time) {
|
||
var self = this;
|
||
|
||
// Some UI elements are updated every 10th of a second.
|
||
var tenth = Math.floor(time * 10) / 10;
|
||
if (tenth !== self.lastTenth) {
|
||
// Check for bookmark
|
||
if (self.bookmarksMap !== undefined && self.bookmarksMap[tenth] !== undefined) {
|
||
// Show bookmark
|
||
self.bookmarksMap[tenth].mouseover().mouseout();
|
||
}
|
||
}
|
||
self.lastTenth = tenth;
|
||
|
||
// Some UI elements are updated every second.
|
||
var second = Math.floor(time);
|
||
if (second !== self.lastSecond) {
|
||
self.toggleInteractions(second);
|
||
|
||
if (self.currentState === H5P.Video.PLAYING || self.currentState === H5P.Video.PAUSED) {
|
||
// Update elapsed time
|
||
self.controls.$currentTime.html(InteractiveVideo.humanizeTime(second < 0 ? 0 : second));
|
||
}
|
||
}
|
||
self.lastSecond = second;
|
||
};
|
||
|
||
/**
|
||
* Call xAPI completed only once
|
||
*
|
||
* @public
|
||
*/
|
||
InteractiveVideo.prototype.complete = function() {
|
||
// Skip for editor
|
||
if (this.editor) {
|
||
return;
|
||
}
|
||
|
||
if (!this.isCompleted) {
|
||
// Post user score. Max score is based on how many of the questions the user
|
||
// actually answered
|
||
this.triggerXAPIScored(this.getUsersScore(), this.getUsersMaxScore(), 'completed');
|
||
}
|
||
this.isCompleted = true;
|
||
};
|
||
|
||
/**
|
||
* Gets the users score
|
||
* @returns {number}
|
||
*/
|
||
InteractiveVideo.prototype.getUsersScore = function() {
|
||
var score = 0;
|
||
|
||
for (var i = 0; i < this.interactions.length; i++) {
|
||
if (this.interactions[i].score) {
|
||
score += this.interactions[i].score;
|
||
}
|
||
}
|
||
|
||
return score;
|
||
};
|
||
|
||
/**
|
||
* Gets the users max score
|
||
* @returns {number}
|
||
*/
|
||
InteractiveVideo.prototype.getUsersMaxScore = function() {
|
||
var maxScore = 0;
|
||
|
||
for (var i = 0; i < this.interactions.length; i++) {
|
||
if (this.interactions[i].maxScore) {
|
||
maxScore += this.interactions[i].maxScore;
|
||
}
|
||
}
|
||
|
||
return maxScore;
|
||
};
|
||
|
||
/**
|
||
* Implements getScore from the question type contract
|
||
* @returns {number}
|
||
*/
|
||
InteractiveVideo.prototype.getScore = function() {
|
||
return this.getUsersScore();
|
||
};
|
||
|
||
/**
|
||
* Implements getMaxScore from the question type contract
|
||
* @returns {number}
|
||
*/
|
||
InteractiveVideo.prototype.getMaxScore = function() {
|
||
return this.getUsersMaxScore();
|
||
};
|
||
|
||
|
||
/**
|
||
* Show a mask behind the interaction to prevent the user from clicking the video or controls
|
||
*
|
||
* @return {jQuery} the dialog wrapper element
|
||
*/
|
||
InteractiveVideo.prototype.showOverlayMask = function(){
|
||
var self = this;
|
||
|
||
self.$videoWrapper.addClass('h5p-disable-opt-out');
|
||
self.dnb.dialog.openOverlay();
|
||
|
||
var $dialogWrapper = self.$container.find('.h5p-dialog-wrapper');
|
||
$dialogWrapper.click(function(){
|
||
if(self.hasUncompletedRequiredInteractions()){
|
||
self.showWarningMask();
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Hides the mask behind the interaction
|
||
* @return {jQuery} the dialog wrapper element
|
||
*/
|
||
InteractiveVideo.prototype.hideOverlayMask = function(){
|
||
var self = this;
|
||
|
||
self.dnb.dialog.closeOverlay();
|
||
self.$videoWrapper.removeClass('h5p-disable-opt-out');
|
||
|
||
return self.$container.find('.h5p-dialog-wrapper');
|
||
};
|
||
|
||
|
||
/**
|
||
* Shows the warning mask.
|
||
* The mask is shared by all interactions
|
||
*/
|
||
InteractiveVideo.prototype.showWarningMask = function(){
|
||
var self = this;
|
||
|
||
// create mask if doesn't exist
|
||
if(!self.$mask) {
|
||
self.$mask = $(
|
||
'<div class="h5p-warning-mask">' +
|
||
'<div class="h5p-warning-mask-wrapper">' +
|
||
'<div class="h5p-warning-mask-content">' + self.l10n.requiresCompletionWarning + '</div>' +
|
||
'<button type="button" class="h5p-joubelui-button h5p-button-back">' + self.l10n.back + '</button>' +
|
||
'</div>' +
|
||
'</div>'
|
||
).click(function () {
|
||
self.$mask.hide();
|
||
}).appendTo(self.$container);
|
||
}
|
||
|
||
self.$mask.show();
|
||
};
|
||
|
||
/**
|
||
* Returns true if there are visible interactions that require completed
|
||
* and the user doesn't have full score
|
||
*
|
||
* @returns {boolean} If any required interaction is not completed with full score
|
||
*/
|
||
InteractiveVideo.prototype.hasUncompletedRequiredInteractions = function(){
|
||
var self = this;
|
||
|
||
return self.getVisibleInteractions().some(function(interaction){
|
||
return interaction.getRequiresCompletion() && !interaction.hasFullScore();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Returns an array of interactions currently visible
|
||
*
|
||
* @return {H5P.InteractiveVideoInteraction[]} visible interactions
|
||
*/
|
||
InteractiveVideo.prototype.getVisibleInteractions = function() {
|
||
return this.interactions.filter(function(interaction){
|
||
return interaction.isVisible();
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Implements showSolutions from the question type contract
|
||
*/
|
||
InteractiveVideo.prototype.showSolutions = function() {
|
||
// Intentionally left empty. Function makes IV pop up in CP summary
|
||
};
|
||
|
||
/**
|
||
* Implements getTitle from the question type contract
|
||
* @returns {string}
|
||
*/
|
||
InteractiveVideo.prototype.getTitle = function() {
|
||
return H5P.createTitle(this.options.video.startScreenOptions.title);
|
||
};
|
||
|
||
/**
|
||
* Display and remove interactions for the given second.
|
||
* @param {number} second
|
||
*/
|
||
InteractiveVideo.prototype.toggleInteractions = function (second) {
|
||
for (var i = 0; i < this.interactions.length; i++) {
|
||
this.interactions[i].toggle(second);
|
||
this.interactions[i].repositionToWrapper(this.$videoWrapper);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Start interactive video playback.
|
||
*/
|
||
InteractiveVideo.prototype.play = function () {
|
||
this.video.play();
|
||
};
|
||
|
||
/**
|
||
* Seek interactive video to the given time
|
||
* @param {number} time
|
||
*/
|
||
InteractiveVideo.prototype.seek = function (time) {
|
||
this.video.seek(time);
|
||
};
|
||
|
||
/**
|
||
* Pause interactive video playback.
|
||
*/
|
||
InteractiveVideo.prototype.pause = function () {
|
||
if (this.video && this.video.pause) {
|
||
this.video.pause();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Reset all interaction progress and answers
|
||
*/
|
||
InteractiveVideo.prototype.resetTask = function () {
|
||
if (this.controls === undefined) {
|
||
return; // Content has not been used
|
||
}
|
||
|
||
this.seek(0); // Rewind
|
||
this.timeUpdate(-1);
|
||
this.controls.$slider.slider('option', 'value', 0);
|
||
|
||
for (var i = 0; i < this.interactions.length; i++) {
|
||
this.interactions[i].resetTask();
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* Gather copyright information for the current content.
|
||
*
|
||
* @returns {H5P.ContentCopyrights}
|
||
*/
|
||
InteractiveVideo.prototype.getCopyrights = function () {
|
||
var self = this;
|
||
var info = new H5P.ContentCopyrights();
|
||
|
||
// Adding video file copyright info
|
||
if (self.options.video.files !== undefined && self.options.video.files[0] !== undefined) {
|
||
info.addMedia(new H5P.MediaCopyright(self.options.video.files[0].copyright, self.l10n));
|
||
}
|
||
|
||
// Adding info from copyright field
|
||
if (self.options.video.startScreenOptions.copyright !== undefined) {
|
||
info.addMedia(self.options.video.startScreenOptions.copyright);
|
||
}
|
||
|
||
// Adding copyrights for poster
|
||
var poster = self.options.video.startScreenOptions.poster;
|
||
if (poster && poster.copyright !== undefined) {
|
||
var image = new H5P.MediaCopyright(poster.copyright, self.l10n);
|
||
var imgSource = H5P.getPath(poster.path, self.contentId);
|
||
image.setThumbnail(new H5P.Thumbnail(imgSource, poster.width, poster.height));
|
||
info.addMedia(image);
|
||
}
|
||
|
||
// Adding copyrights for interactions
|
||
for (var i = 0; i < self.interactions.length; i++) {
|
||
var interactionCopyrights = self.interactions[i].getCopyrights();
|
||
if (interactionCopyrights) {
|
||
info.addContent(interactionCopyrights);
|
||
}
|
||
}
|
||
|
||
return info;
|
||
};
|
||
|
||
// Additional player states
|
||
/** @constant {number} */
|
||
InteractiveVideo.SEEKING = 4;
|
||
/** @constant {number} */
|
||
InteractiveVideo.LOADED = 5;
|
||
/** @constant {number} */
|
||
InteractiveVideo.ATTACHED = 6;
|
||
|
||
/**
|
||
* Formats time in H:MM:SS.
|
||
*
|
||
* @public
|
||
* @param {number} seconds
|
||
* @returns {string}
|
||
*/
|
||
InteractiveVideo.humanizeTime = function (seconds) {
|
||
var minutes = Math.floor(seconds / 60);
|
||
var hours = Math.floor(minutes / 60);
|
||
|
||
minutes = minutes % 60;
|
||
seconds = Math.floor(seconds % 60);
|
||
|
||
var time = '';
|
||
|
||
if (hours !== 0) {
|
||
time += hours + ':';
|
||
|
||
if (minutes < 10) {
|
||
time += '0';
|
||
}
|
||
}
|
||
|
||
time += minutes + ':';
|
||
|
||
if (seconds < 10) {
|
||
time += '0';
|
||
}
|
||
|
||
time += seconds;
|
||
|
||
return time;
|
||
};
|
||
|
||
/**
|
||
* Look for field with the given name in the given collection.
|
||
* Only used by editor.
|
||
*
|
||
* @private
|
||
* @param {string} name of field
|
||
* @param {Array} fields collection to look in
|
||
* @returns {Object} field object
|
||
*/
|
||
var findField = function (name, fields) {
|
||
for (var i = 0; i < fields.length; i++) {
|
||
if (fields[i].name === name) {
|
||
return fields[i];
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Generic elseif for when to delay work or run straight away.
|
||
*
|
||
* @param {number} time null to carry out straight away
|
||
* @param {function} job what to do
|
||
*/
|
||
var delayWork = function (time, job) {
|
||
if (time === null) {
|
||
job();
|
||
}
|
||
else {
|
||
setTimeout(job, time);
|
||
}
|
||
};
|
||
|
||
return InteractiveVideo;
|
||
})(H5P.jQuery, H5P.EventDispatcher, H5P.DragNBar, H5P.InteractiveVideoInteraction);
|