mirror of
https://github.com/h5p/h5p-interactive-video.git
synced 2026-03-06 12:32:55 +08:00
973 lines
30 KiB
JavaScript
973 lines
30 KiB
JavaScript
H5P.InteractiveVideo.Player = (function ($, EventDispatcher, Dialog, Interaction) {
|
|
|
|
/**
|
|
* Initialize a new interactive video.
|
|
*
|
|
* @class
|
|
* @namespace H5P.InteractiveVideo
|
|
* @extends H5P.EventDispatcher
|
|
* @param {Array} params
|
|
* @param {Number} id
|
|
*/
|
|
function Player(params, id) {
|
|
H5P.EventDispatcher.call(this);
|
|
var self = this;
|
|
self.$ = $(self);
|
|
this.params = $.extend({
|
|
video: {},
|
|
assets: {}
|
|
}, params.interactiveVideo);
|
|
this.contentId = id;
|
|
this.visibleInteractions = [];
|
|
|
|
this.l10n = {
|
|
interaction: 'Interaction',
|
|
play: 'Play',
|
|
pause: 'Pause',
|
|
mute: 'Mute',
|
|
quality: 'Video quality',
|
|
unmute: 'Unmute',
|
|
fullscreen: 'Fullscreen',
|
|
exitFullscreen: 'Exit fullscreen',
|
|
summary: 'Summary',
|
|
bookmarks: 'Bookmarks',
|
|
defaultAdaptivitySeekLabel: 'Continue'
|
|
};
|
|
|
|
this.justVideo = navigator.userAgent.match(/iPhone|iPod/i) ? true : false;
|
|
this.isCompleted = false;
|
|
|
|
this.video = H5P.newRunnable({
|
|
library: 'H5P.Video 1.1',
|
|
constructor: 'Player',
|
|
params: {
|
|
sources: this.params.video.files,
|
|
controls: this.justVideo,
|
|
fit: false
|
|
}
|
|
}, this.contentId);
|
|
|
|
this.video.on('error', function (event) {
|
|
// Make sure splash screen is removed so the error is visible.
|
|
self.removeSplash();
|
|
});
|
|
|
|
var firstPlay = true;
|
|
this.video.on('stateChange', function (event) {
|
|
var state = event.data
|
|
if (self.currentState === SEEKING) {
|
|
return; // Prevent updateing UI while seeking
|
|
}
|
|
|
|
switch (state) {
|
|
case H5P.Video.ENDED:
|
|
self.currentState = ENDED;
|
|
self.controls.$play.addClass('h5p-pause').attr('title', self.l10n.play);
|
|
|
|
self.complete();
|
|
break;
|
|
|
|
case H5P.Video.PLAYING:
|
|
if (firstPlay) {
|
|
firstPlay = false;
|
|
|
|
// Qualities might not be available until after play.
|
|
self.addQualityChooser();
|
|
|
|
// Make sure splash screen is removed.
|
|
self.removeSplash();
|
|
|
|
// Make sure we track buffering of the video.
|
|
self.startUpdatingBufferBar();
|
|
}
|
|
|
|
self.currentState = PLAYING;
|
|
self.controls.$play.removeClass('h5p-pause').attr('title', self.l10n.pause);
|
|
self.timeUpdate(self.video.getCurrentTime());
|
|
break;
|
|
|
|
case H5P.Video.PAUSED:
|
|
self.currentState = PAUSED;
|
|
self.controls.$play.addClass('h5p-pause').attr('title', self.l10n.play);
|
|
break;
|
|
|
|
case H5P.Video.BUFFERING:
|
|
self.currentState = BUFFERING;
|
|
|
|
// Make sure splash screen is removed.
|
|
self.removeSplash();
|
|
|
|
// Make sure we track buffering of the video.
|
|
self.startUpdatingBufferBar();
|
|
|
|
// Remove interactions while buffering
|
|
self.timeUpdate(-1);
|
|
break;
|
|
}
|
|
});
|
|
|
|
this.video.on('qualityChange', function (event) {
|
|
var quality = event.data;
|
|
if (self.controls.$qualityChooser) {
|
|
self.controls.$qualityChooser.find('li').removeClass('h5p-selected').filter('[data-quality="' + quality + '"]').addClass('h5p-selected');
|
|
}
|
|
});
|
|
|
|
this.video.on('loaded', function (event) {
|
|
self.loaded();
|
|
});
|
|
}
|
|
|
|
Player.prototype = Object.create(EventDispatcher.prototype);
|
|
Player.prototype.constructor = Player;
|
|
|
|
/**
|
|
* Removes splash screen.
|
|
*/
|
|
Player.prototype.removeSplash = function () {
|
|
if (this.$splash === undefined) {
|
|
return;
|
|
}
|
|
|
|
this.$splash.remove();
|
|
delete this.$splash;
|
|
};
|
|
|
|
/**
|
|
* Attach interactive video to DOM element.
|
|
*
|
|
* @param {jQuery} $container
|
|
* @returns {undefined}
|
|
*/
|
|
Player.prototype.attach = function ($container) {
|
|
var that = this;
|
|
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.
|
|
// TODO: For this to be used inside something else, we cannot assume that the font size will be 16.
|
|
this.fontSize = 16;
|
|
this.width = 640; // parseInt($container.css('width')); // Get width in px
|
|
|
|
// 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');
|
|
this.attachControls(this.$controls);
|
|
|
|
// Create a popup dialog
|
|
this.dialog = new Dialog($container, this.$videoWrapper);
|
|
|
|
if (this.editor === undefined) {
|
|
// Pause video when opening dialog
|
|
this.dialog.on('open', function () {
|
|
// Keep track of last state
|
|
that.lastState = that.currentState;
|
|
|
|
if (that.currentState !== PAUSED && that.currentState !== ENDED) {
|
|
// Pause video
|
|
that.video.pause();
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
this.dialog.disableOverlay = true;
|
|
}
|
|
|
|
// Resume playing when closing dialog
|
|
this.dialog.on('close', function () {
|
|
if (that.lastState !== PAUSED && that.lastState !== ENDED) {
|
|
that.video.play();
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Attach the video to the given wrapper.
|
|
*
|
|
* @param {jQuery} $wrapper
|
|
*/
|
|
Player.prototype.attachVideo = function ($wrapper) {
|
|
var that = this;
|
|
|
|
this.video.attach($wrapper);
|
|
if (this.justVideo) {
|
|
return;
|
|
}
|
|
|
|
this.$overlay = $('<div class="h5p-overlay h5p-ie-transparent-background"></div>').appendTo($wrapper);
|
|
|
|
if (this.editor === undefined) {
|
|
this.$splash = $('<div class="h5p-splash-wrapper"><div class="h5p-splash"><h2>Interactive Video</h2><p>Press the icons as the video plays for challenges and more information on the topics!</p><div class="h5p-interaction h5p-multichoice-interaction"><a href="#" class="h5p-interaction-button"></a><div class="h5p-interaction-label">Challenges</div></div><div class="h5p-interaction h5p-text-interaction"><a href="#" class="h5p-interaction-button"></a><div class="h5p-interaction-label">More information</div></div></div></div>')
|
|
.click(function () {
|
|
that.video.play();
|
|
})
|
|
.appendTo(this.$overlay)
|
|
.find('.h5p-interaction-button')
|
|
.click(function () {
|
|
return false;
|
|
})
|
|
.end();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Unbind event listeners.
|
|
*
|
|
* @returns {undefined}
|
|
*/
|
|
Player.prototype.loaded = function () {
|
|
var that = this;
|
|
|
|
var duration = this.video.getDuration();
|
|
var time = 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(humanizeTime(0));
|
|
|
|
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++) {
|
|
findField('seekTo', adaptivityFields[i].fields).max = duration;
|
|
}
|
|
}
|
|
|
|
// Add summary interaction
|
|
if (this.hasMainSummary()) {
|
|
var displayAt = duration - this.params.summary.displayAt;
|
|
if (displayAt < 0) {
|
|
displayAt = 0;
|
|
}
|
|
|
|
this.params.assets.interactions.push({
|
|
action: this.params.summary.task,
|
|
x: 80,
|
|
y: 80,
|
|
duration: {
|
|
from: displayAt,
|
|
to: duration
|
|
},
|
|
bigDialog: true,
|
|
className: 'h5p-summary-interaction h5p-end-summary',
|
|
label: this.l10n.summary,
|
|
mainSummary: true
|
|
});
|
|
}
|
|
|
|
// Determine how many percentage one second is.
|
|
this.oneSecondInPercentage = (100 / this.video.getDuration());
|
|
|
|
// Initialize interactions
|
|
this.interactions = [];
|
|
for (var i = 0; i < this.params.assets.interactions.length; i++) {
|
|
this.initInteraction(i);
|
|
}
|
|
|
|
// Add dots above seeking line.
|
|
this.addSliderInteractions();
|
|
|
|
// Add bookmarks
|
|
this.addBookmarks();
|
|
this.trigger('resize');
|
|
};
|
|
|
|
/**
|
|
* Initialize interaction at the given index.
|
|
*
|
|
* @public
|
|
* @param {Number} index
|
|
* @returns {Interaction}
|
|
*/
|
|
Player.prototype.initInteraction = function (index) {
|
|
var self = this;
|
|
var parameters = this.params.assets.interactions[index];
|
|
|
|
if (self.params.override && self.params.override.overrideButtons) {
|
|
// Extend interaction parameters
|
|
H5P.jQuery.extend(parameters.action.params.behaviour, {
|
|
enableSolutionsButton: self.params.override.overrideShowSolutionButton ? true : false,
|
|
enableRetry: self.params.override.overrideRetry ? true : false
|
|
});
|
|
}
|
|
|
|
var interaction = new Interaction(parameters, self);
|
|
interaction.on('display', function (event) {
|
|
var $interaction = event.data;
|
|
$interaction.appendTo(self.$overlay);
|
|
|
|
if (self.currentState === PLAYING && interaction.pause()) {
|
|
self.video.pause();
|
|
}
|
|
|
|
setTimeout(function () {
|
|
interaction.positionLabel(self.$videoWrapper.width());
|
|
}, 0);
|
|
});
|
|
interaction.on('xAPI', function(event) {
|
|
if (event.getVerb() === 'completed'
|
|
|| event.getMaxScore()
|
|
|| event.getScore() !== null) {
|
|
|
|
if (interaction.isMainSummary()) {
|
|
self.complete();
|
|
}
|
|
}
|
|
});
|
|
|
|
this.interactions.push(interaction);
|
|
|
|
if (this.editor !== undefined) {
|
|
this.editor.processInteraction(interaction, parameters);
|
|
}
|
|
|
|
return interaction;
|
|
};
|
|
|
|
/**
|
|
* Does the interactive video have a main summary?
|
|
*
|
|
* This is the summary created in the summary tab of the editor
|
|
*
|
|
* @returns {Boolean}
|
|
* true if this interactive video has a summary
|
|
* false otherwise
|
|
*/
|
|
Player.prototype.hasMainSummary = function() {
|
|
var summary = this.params.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.
|
|
*/
|
|
Player.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.
|
|
*/
|
|
Player.prototype.addBookmarks = function () {
|
|
this.bookmarksMap = {};
|
|
if (this.params.assets.bookmarks !== undefined) {
|
|
for (var i = 0; i < this.params.assets.bookmarks.length; i++) {
|
|
this.addBookmark(i);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Puts a single cool narrow line around the slider / seek bar.
|
|
*/
|
|
Player.prototype.addBookmark = function (id, tenth) {
|
|
var self = this;
|
|
var bookmark = self.params.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.playing === undefined || self.playing === false) {
|
|
$bookmark.mouseover().mouseout();
|
|
}
|
|
self.controls.$bookmarksChooser.removeClass('h5p-show');
|
|
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', this);
|
|
}
|
|
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 {jQuery} $wrapper
|
|
*/
|
|
Player.prototype.attachControls = function ($wrapper) {
|
|
var that = this;
|
|
|
|
$wrapper.html('<div class="h5p-controls-left"><a href="#" class="h5p-control h5p-play h5p-pause" title="' + that.l10n.play + '"></a><a href="#" class="h5p-control h5p-bookmarks" title="' + that.l10n.bookmarks + '"></a><div class="h5p-chooser h5p-bookmarks"><h3>' + that.l10n.bookmarks + '</h3></div></div><div class="h5p-controls-right"><a href="#" class="h5p-control h5p-fullscreen" title="' + that.l10n.fullscreen + '"></a><a href="#" class="h5p-control h5p-quality h5p-disabled" title="' + that.l10n.quality + '"></a><div class="h5p-chooser h5p-quality"><h3>' + that.l10n.quality + '</h3></div><a href="#" class="h5p-control h5p-volume" title="' + that.l10n.mute + '"></a><div class="h5p-control h5p-time"><span class="h5p-current">0:00</span> / <span class="h5p-total">0:00</span></div></div><div class="h5p-control h5p-slider"><div class="h5p-interactions-container"></div><div class="h5p-bookmarks-container"></div><div></div></div>');
|
|
this.controls = {};
|
|
|
|
// Play/pause button
|
|
this.controls.$play = $wrapper.find('.h5p-play').click(function () {
|
|
if (that.controls.$play.hasClass('h5p-pause')) {
|
|
that.video.play();
|
|
}
|
|
else {
|
|
that.video.pause();
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Bookmark selector
|
|
if ((this.params.assets.bookmarks === undefined || this.params.assets.bookmarks.length === 0) && this.editor === undefined) {
|
|
// No bookmarks and no editor, remove button.
|
|
$wrapper.find('.h5p-control.h5p-bookmarks').remove();
|
|
}
|
|
else {
|
|
this.controls.$bookmarksChooser = $wrapper.find('.h5p-chooser.h5p-bookmarks');
|
|
$wrapper.find('.h5p-control.h5p-bookmarks').click(function () {
|
|
// TODO: Mark chooser buttons as active when open. (missing design)
|
|
that.controls.$bookmarksChooser.toggleClass('h5p-show');
|
|
return false;
|
|
});
|
|
}
|
|
|
|
if (this.editor === undefined) {
|
|
// Fullscreen button
|
|
this.controls.$fullscreen = $wrapper.find('.h5p-fullscreen').click(function () {
|
|
that.toggleFullScreen();
|
|
return false;
|
|
});
|
|
|
|
// Video quality selector
|
|
this.controls.$qualityChooser = $wrapper.find('.h5p-chooser.h5p-quality');
|
|
this.controls.$qualityButton = $wrapper.find('.h5p-control.h5p-quality').click(function () {
|
|
if (!that.controls.$qualityButton.hasClass('h5p-disabled')) {
|
|
that.controls.$qualityChooser.toggleClass('h5p-show');
|
|
}
|
|
return false;
|
|
});
|
|
|
|
this.addQualityChooser();
|
|
}
|
|
else {
|
|
// Remove buttons in editor mode.
|
|
$wrapper.find('.h5p-fullscreen').remove();
|
|
$wrapper.find('.h5p-quality, .h5p-quality-chooser').remove();
|
|
}
|
|
|
|
// Volume/mute button
|
|
if (navigator.userAgent.indexOf('Android') === -1 && navigator.userAgent.indexOf('iPad') === -1) {
|
|
this.controls.$volume = $wrapper.find('.h5p-volume').click(function () {
|
|
if (that.controls.$volume.hasClass('h5p-muted')) {
|
|
that.controls.$volume.removeClass('h5p-muted').attr('title', that.l10n.mute);
|
|
that.video.unMute();
|
|
}
|
|
else {
|
|
that.controls.$volume.addClass('h5p-muted').attr('title', that.l10n.unmute);
|
|
that.video.mute();
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
else {
|
|
$wrapper.find('.h5p-volume').remove();
|
|
}
|
|
|
|
// Timer
|
|
var $time = $wrapper.find('.h5p-time');
|
|
this.controls.$currentTime = $time.children('.h5p-current');
|
|
this.controls.$totalTime = $time.children('.h5p-total');
|
|
|
|
// Timeline
|
|
var $slider = $wrapper.find('.h5p-slider');
|
|
this.controls.$slider = $slider.children(':last').slider({
|
|
value: 0,
|
|
step: 0.01,
|
|
orientation: 'horizontal',
|
|
range: 'min',
|
|
max: 0,
|
|
start: function () {
|
|
if (that.currentState === SEEKING) {
|
|
return; // Prevent double start on touch devies!
|
|
}
|
|
|
|
that.lastState = (that.currentState === ENDED ? PLAYING : that.currentState);
|
|
that.video.pause();
|
|
that.currentState = SEEKING;
|
|
|
|
// Make sure splash screen is removed.
|
|
that.removeSplash();
|
|
},
|
|
slide: function (e, ui) {
|
|
// Update elapsed time
|
|
that.controls.$currentTime.html(humanizeTime(ui.value));
|
|
},
|
|
stop: function (e, ui) {
|
|
that.currentState = that.lastState;
|
|
that.video.seek(ui.value);
|
|
if (that.lastState === PLAYING) {
|
|
that.video.play();
|
|
}
|
|
else {
|
|
that.timeUpdate(ui.value);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Slider bufferer
|
|
this.controls.$buffered = $('<div class="h5p-buffered"></div>').prependTo(this.controls.$slider);
|
|
|
|
// Slider containers
|
|
this.controls.$interactionsContainer = $slider.find('.h5p-interactions-container');
|
|
this.controls.$bookmarksContainer = $slider.find('.h5p-bookmarks-container');
|
|
};
|
|
|
|
/**
|
|
*
|
|
*/
|
|
Player.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);
|
|
var $options = $list.children().click(function () {
|
|
self.video.setQuality($(this).attr('data-quality'));
|
|
self.controls.$qualityChooser.removeClass('h5p-show');
|
|
});
|
|
|
|
// Enable quality chooser button
|
|
this.controls.$qualityButton.removeClass('h5p-disabled');
|
|
};
|
|
|
|
/**
|
|
* Create loop that constantly updates the buffer bar
|
|
*/
|
|
Player.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.
|
|
*
|
|
* @param {Boolean} fullScreen
|
|
* @returns {undefined}
|
|
*/
|
|
Player.prototype.resize = function () {
|
|
var fullscreenOn = this.$container.hasClass('h5p-fullscreen') || this.$container.hasClass('h5p-semi-fullscreen');
|
|
|
|
// Resize the controls the first time we're visible
|
|
if (this.controlsSized === undefined) {
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
this.$videoWrapper.css({
|
|
marginTop: '',
|
|
marginLeft: '',
|
|
width: '',
|
|
height: ''
|
|
});
|
|
this.video.trigger('resize');
|
|
|
|
var width;
|
|
var controlsHeight = this.$controls.height();
|
|
var containerHeight = this.$container.height();
|
|
if (fullscreenOn) {
|
|
var 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 {
|
|
if (this.controls.$fullscreen !== undefined && this.controls.$fullscreen.hasClass('h5p-exit')) {
|
|
// Update icon if we some how got out of fullscreen.
|
|
this.controls.$fullscreen.removeClass('h5p-exit').attr('title', this.l10n.fullscreen);
|
|
}
|
|
width = this.$container.width();
|
|
}
|
|
|
|
// Set base font size. Don't allow it to fall below original size.
|
|
this.$container.css('fontSize', (width > this.width) ? (this.fontSize * (width / this.width)) : this.fontSize + 'px');
|
|
|
|
this.$container.find('.h5p-chooser').css('maxHeight', (containerHeight - controlsHeight) + 'px');
|
|
};
|
|
|
|
/**
|
|
* Enter/exit fullscreen.
|
|
*
|
|
* @returns {undefined}
|
|
*/
|
|
Player.prototype.toggleFullScreen = function () {
|
|
if (this.controls.$fullscreen.hasClass('h5p-exit')) {
|
|
this.controls.$fullscreen.removeClass('h5p-exit').attr('title', this.l10n.fullscreen);
|
|
if (H5P.fullScreenBrowserPrefix === undefined) {
|
|
// Click button to disable fullscreen
|
|
$('.h5p-disable-fullscreen').click();
|
|
}
|
|
else {
|
|
if (H5P.fullScreenBrowserPrefix === '') {
|
|
window.top.document.exitFullScreen();
|
|
}
|
|
else if (H5P.fullScreenBrowserPrefix === 'ms') {
|
|
window.top.document.msExitFullscreen();
|
|
}
|
|
else {
|
|
window.top.document[H5P.fullScreenBrowserPrefix + 'CancelFullScreen']();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
this.controls.$fullscreen.addClass('h5p-exit').attr('title', this.l10n.exitFullscreen);
|
|
H5P.fullScreen(this.$container, this);
|
|
if (H5P.fullScreenBrowserPrefix === undefined) {
|
|
// Hide disable full screen button. We have our own!
|
|
$('.h5p-disable-fullscreen').hide();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called when the time of the video changes.
|
|
* Makes sure to update all UI elements.
|
|
*
|
|
* @param {Number} time
|
|
*/
|
|
Player.prototype.timeUpdate = function (time) {
|
|
var self = this;
|
|
|
|
// Scroll slider
|
|
if (time > 0) {
|
|
self.controls.$slider.slider('option', 'value', time);
|
|
}
|
|
|
|
// 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) {
|
|
// TODO: Is it possible to move interactions to tenth of a second instead?
|
|
// This would greatly improve precision of the interactions and UX. (now it feels a bit limited)
|
|
self.toggleInteractions(second);
|
|
|
|
if (self.editor !== undefined) {
|
|
self.editor.dnb.blur();
|
|
}
|
|
if (self.currentState === PLAYING) {
|
|
// Update elapsed time
|
|
self.controls.$currentTime.html(humanizeTime(second));
|
|
}
|
|
}
|
|
self.lastSecond = second;
|
|
|
|
setTimeout(function () {
|
|
if (self.currentState === PLAYING) {
|
|
self.timeUpdate(self.video.getCurrentTime());
|
|
}
|
|
}, 40); // 25 fps
|
|
};
|
|
|
|
Player.prototype.complete = function() {
|
|
if (!this.isCompleted) {
|
|
// Post user score. Max score is based on how many of the questions the user
|
|
// actually answered
|
|
this.triggerXAPICompleted(this.getUsersScore(), this.getUsersMaxScore());
|
|
}
|
|
this.isCompleted = true;
|
|
}
|
|
|
|
Player.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;
|
|
};
|
|
|
|
Player.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;
|
|
};
|
|
|
|
/**
|
|
* Display and remove interactions for the given second.
|
|
*
|
|
* @param {int} second
|
|
*/
|
|
Player.prototype.toggleInteractions = function (second) {
|
|
for (var i = 0; i < this.interactions.length; i++) {
|
|
this.interactions[i].toggle(second);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Start interactive video playback.
|
|
*
|
|
* @public
|
|
*/
|
|
Player.prototype.play = function () {
|
|
this.video.play();
|
|
};
|
|
|
|
/**
|
|
* Seek interactive video to the given time
|
|
*
|
|
* @public
|
|
* @param {Number} time
|
|
*/
|
|
Player.prototype.seek = function (time) {
|
|
this.video.seek(time);
|
|
};
|
|
|
|
/**
|
|
* Pause interactive video playback.
|
|
*
|
|
* @public
|
|
*/
|
|
Player.prototype.pause = function () {
|
|
this.video.pause();
|
|
};
|
|
|
|
/**
|
|
* Gather copyright information for the current content.
|
|
*
|
|
* @returns {H5P.ContentCopyrights}
|
|
*/
|
|
Player.prototype.getCopyrights = function () {
|
|
var self = this;
|
|
var info = new H5P.ContentCopyrights();
|
|
|
|
var videoRights, video = self.params.video.files[0];
|
|
if (video.copyright !== undefined) {
|
|
videoRights = new H5P.MediaCopyright(video.copyright, self.l10n);
|
|
}
|
|
|
|
if ((videoRights === undefined || videoRights.undisclosed()) && self.params.video.copyright !== undefined) {
|
|
// Use old copyright info as fallback.
|
|
videoRights = self.params.video.copyright;
|
|
}
|
|
info.addMedia(videoRights);
|
|
|
|
for (var i = 0; i < self.interactions.length; i++) {
|
|
var interactionCopyrights = self.interactions[i].getCopyrights();
|
|
if (interactionCopyrights) {
|
|
info.addContent(interactionCopyrights);
|
|
}
|
|
}
|
|
|
|
return info;
|
|
};
|
|
|
|
/** @constant {number} */
|
|
var ENDED = 0;
|
|
/** @constant {number} */
|
|
var PLAYING = 1;
|
|
/** @constant {number} */
|
|
var PAUSED = 2;
|
|
/** @constant {number} */
|
|
var BUFFERING = 3;
|
|
/** @constant {number} */
|
|
var SEEKING = 4;
|
|
|
|
/**
|
|
* Formats time in H:MM:SS.
|
|
*
|
|
* @private
|
|
* @param {float} seconds
|
|
* @returns {string}
|
|
*/
|
|
var 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. TODO: move
|
|
*
|
|
* @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];
|
|
}
|
|
}
|
|
};
|
|
|
|
return Player;
|
|
})(H5P.jQuery, H5P.EventDispatcher, H5P.InteractiveVideo.Dialog, H5P.InteractiveVideo.Interaction);
|