UNPKG

@oat-sa/tao-item-runner-qti

Version:
444 lines (388 loc) 18.5 kB
define(['jquery', 'lodash', 'handlebars', 'lib/handlebars/helpers', 'taoQtiItem/qtiCommonRenderer/helpers/PciResponse', 'taoQtiItem/qtiCommonRenderer/helpers/container', 'ui/mediaplayer'], function ($$1, _, Handlebars, Helpers0, pciResponse, containerHelper, mediaplayer) { 'use strict'; $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1; _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _; Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars; Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0; pciResponse = pciResponse && Object.prototype.hasOwnProperty.call(pciResponse, 'default') ? pciResponse['default'] : pciResponse; containerHelper = containerHelper && Object.prototype.hasOwnProperty.call(containerHelper, 'default') ? containerHelper['default'] : containerHelper; mediaplayer = mediaplayer && Object.prototype.hasOwnProperty.call(mediaplayer, 'default') ? mediaplayer['default'] : mediaplayer; if (!Helpers0.__initialized) { Helpers0(Handlebars); Helpers0.__initialized = true; } var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) { this.compilerInfo = [4,'>= 1.0.0']; helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; var buffer = "", stack1, helper, functionType="function", escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { var buffer = "", stack1; buffer += "id=\"" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.id)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "\""; return buffer; } function program3(depth0,data) { var buffer = "", stack1; buffer += " " + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class'])),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)); return buffer; } function program5(depth0,data) { var buffer = "", stack1; buffer += " lang=\"" + escapeExpression(((stack1 = ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['xml:lang'])),typeof stack1 === functionType ? stack1.apply(depth0) : stack1)) + "\""; return buffer; } function program7(depth0,data) { var stack1, helper; if (helper = helpers.prompt) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.prompt); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } if(stack1 || stack1 === 0) { return stack1; } else { return ''; } } buffer += "<div "; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1.id), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += " class=\"qti-interaction qti-blockInteraction qti-mediaInteraction"; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['class']), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\" data-serial=\""; if (helper = helpers.serial) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.serial); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\""; stack1 = helpers['if'].call(depth0, ((stack1 = (depth0 && depth0.attributes)),stack1 == null || stack1 === false ? stack1 : stack1['xml:lang']), {hash:{},inverse:self.noop,fn:self.program(5, program5, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += ">\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.prompt), {hash:{},inverse:self.noop,fn:self.program(7, program7, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n <div class=\"instruction-container\"></div>\n <div class=\"media-container\"></div>\n</div>\n"; return buffer; }); function template(data, options, asString) { var html = Template(data, options); return (asString || true) ? html : $(html); } /* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; under version 2 * of the License (non-upgradable). * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Copyright (c) 2014-2018 (original work) Open Assessment Technlogies SA * */ const qtiClass = 'mediaInteraction'; const getContainer = containerHelper.get; //some default values const defaults = { type: 'video/mp4' }; //some patterns to match context in which disable the media preview const reWebM = /.*\.webm/i; const reFirefoxVersion = /firefox\/([0-9]+\.*[0-9]*)/i; /** * Checks if a media can be previewed safely * @param {String} type - The type of media * @param {String} url - The URL to the media * @returns {Boolean} */ function canPreviewMedia(type, url) { const firefox = reFirefoxVersion.exec(navigator.userAgent); const webm = reWebM.test(url); return !(webm && firefox && parseFloat(firefox[1]) >= 87); } /** * Init rendering, called after template injected into the DOM * All options are listed in the QTI v2.1 information model: * http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10391 * * @param {Object} interaction * @fires playerrendered when the player is at least rendered * @fires playerready when the player is sucessfully loaded and configured * @returns {Promise} */ function render(interaction) { return new Promise(resolve => { const $container = getContainer(interaction); const media = interaction.object; const $item = $container.parents('.qti-item'); const maxPlays = parseInt(interaction.attr('maxPlays'), 10) || 0; //check if the media can be played (using timesPlayed and maxPlays) const canBePlayed = () => maxPlays === 0 || maxPlays > parseInt($container.data('timesPlayed'), 10); /** * Resize video player elements to fit container size * @param {Object} mediaElement - player instance * @param {jQueryElement} $container - container element to adapt */ const resize = _.debounce(() => { // only resize when width in px // new version has width in % const currentWidth = media.attr('width'); if (interaction.mediaElement && currentWidth && !`${currentWidth}`.includes('%')) { const height = $container.find('.media-container').height(); const width = $container.find('.media-container').width(); interaction.mediaElement.resize(width, height); } }, 200); //intialize the player if not yet done const initMediaPlayer = () => { if (!interaction.mediaElement) { const type = media.attr('type') || defaults.type; const mediaUrl = media.attr('data') || ''; const url = mediaUrl && this.resolveUrl(mediaUrl); const preview = canPreviewMedia(type, url); interaction.mediaElement = mediaplayer({ url, type, preview, canPause: $container.hasClass('pause'), maxPlays: maxPlays, canSeek: !maxPlays, width: media.attr('width'), height: media.attr('height'), volume: 100, autoStart: !!interaction.attr('autostart') && canBePlayed(), loop: !!interaction.attr('loop'), renderTo: $$1('.media-container', $container), transcriptionUrl: interaction.object.metaData.transcriptionUrl }) .on('render', () => { resize(); $$1(window) .off('resize.mediaInteraction') .on('resize.mediaInteraction', resize); $item.off('resize.gridEdit').on('resize.gridEdit', resize); /** * @event playerrendered */ $container.trigger('playerrendered'); }) .on('ready', function () { /** * @event playerready */ $container.trigger('playerready'); if (!canBePlayed()) { this.disable(); } // on slow network the resize runs before ready, so it should be called again resize(); // declare the item ready when player is ready to play. resolve(); }) .on('update', _.throttle(() => containerHelper.triggerResponseChangeEvent(interaction), 1000)) .on('ended', function () { $container.data('timesPlayed', $container.data('timesPlayed') + 1); containerHelper.triggerResponseChangeEvent(interaction); if (!canBePlayed()) { this.disable(); } }); } }; //set up the number of times played if (!$container.data('timesPlayed')) { $container.data('timesPlayed', 0); } //initialize the component $container.on('responseSet', initMediaPlayer); //gives a small chance to the responseSet event before initializing the player initMediaPlayer(); }); } /** * Destroy the current interaction * @param {Object} interaction */ function destroy(interaction) { const $container = getContainer(interaction); if (interaction.mediaElement) { interaction.mediaElement.destroy(); interaction.mediaElement = null; } $$1('.instruction-container', $container).empty(); $$1('.media-container', $container).empty(); $container.removeData('timesPlayed'); $$1(window).off('resize.video'); //remove all references to a cache container containerHelper.reset(interaction); } /** * Get the responses from the interaction * @private * @param {Object} interaction * @returns {Array} of points */ function _getRawResponse(interaction) { return [getContainer(interaction).data('timesPlayed') || 0]; } /** * Set the response to the rendered interaction. * * The response format follows the IMS PCI recommendation : * http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343 * * Available base types are defined in the QTI v2.1 information model: * http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10321 * * Special value: the empty object value {} resets the interaction responses * * @param {Object} interaction * @param {Object} response */ function setResponse(interaction, response) { if (response) { try { const maxPlays = parseInt(interaction.attr('maxPlays'), 10) || 0; const responseValues = pciResponse.unserialize(response, interaction); const timesPlayed = parseInt(responseValues[0], 10) || 0; getContainer(interaction).data('timesPlayed', timesPlayed); if (interaction.mediaElement) { if (maxPlays !== 0 && maxPlays <= timesPlayed) { interaction.mediaElement.disable(); } else if (interaction.mediaElement.is('disabled')) { interaction.mediaElement.enable(); } } } catch (e) { // something went wrong } } } /** * Reset the current responses of the rendered interaction. * * The response format follows the IMS PCI recommendation : * http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343 * * Available base types are defined in the QTI v2.1 information model: * http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10321 * * Special value: the empty object value {} resets the interaction responses * * @param {Object} interaction */ function resetResponse(interaction) { getContainer(interaction).data('timesPlayed', 0); } /** * Return the response of the rendered interaction * * The response format follows the IMS PCI recommendation : * http://www.imsglobal.org/assessment/pciv1p0cf/imsPCIv1p0cf.html#_Toc353965343 * * Available base types are defined in the QTI v2.1 information model: * http://www.imsglobal.org/question/qtiv2p1/imsqti_infov2p1.html#element10321 * * @param {Object} interaction * @returns {Object} */ function getResponse(interaction) { if (!getContainer(interaction).data('timesPlayed')) { return { base: null }; } return pciResponse.serialize(_getRawResponse(interaction), interaction); } /** * Set the interaction state. It could be done anytime with any state. * * @param {Object} interaction - the interaction instance * @param {Object} state - the interaction state */ function setState(interaction, state) { /** * Restore the media player state * @private * @param {Object} [playerState] * @param {Boolean} [playerState.muted] - is the player muted * @param {Number} [playerState.volume] - the current volume * @param {Number} [playerState.position] - the position to seek to */ const restorePlayerState = playerState => { if (playerState && interaction.mediaElement) { //Volume if (_.isNumber(playerState.volume)) { interaction.mediaElement.setVolume(playerState.volume); } //Muted state (always after the volume) if (_.isBoolean(playerState.muted)) { interaction.mediaElement.mute(playerState.muted); interaction.mediaElement.startMuted = playerState.muted; } //Position if (playerState.position && playerState.position > 0) { interaction.mediaElement.seek(playerState.position); if (!interaction.attr('autostart')) { interaction.mediaElement.pause(); } } } }; if (_.isObject(state)) { if (state.response) { interaction.resetResponse(); interaction.setResponse(state.response); } if (_.isPlainObject(state.player) && interaction.mediaElement) { if (interaction.mediaElement.is('ready')) { restorePlayerState(state.player); } else { interaction.mediaElement.on('ready.state', () => { interaction.mediaElement.off('ready.state'); restorePlayerState(state.player); }); } } } } /** * Get the interaction state. * * @param {Object} interaction - the interaction instance * @returns {Object} the interaction current state */ function getState(interaction) { const state = {}; const response = interaction.getResponse(); if (response) { state.response = response; } //collect player's state if (interaction.mediaElement) { state.player = { position: interaction.mediaElement.getPosition(), muted: interaction.mediaElement.is('muted'), volume: interaction.mediaElement.getVolume() }; } return state; } /** * Expose the common renderer for the interaction * @exports qtiCommonRenderer/renderers/interactions/MediaInteraction */ var MediaInteraction = { qtiClass, template, render, getContainer, setResponse, getResponse, resetResponse, destroy, setState, getState }; return MediaInteraction; });