diff --git a/package-lock.json b/package-lock.json index 551b397d..19d17e4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-item-runner-qti", - "version": "0.6.2", + "version": "0.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 10a293ee..388dbe81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-item-runner-qti", - "version": "0.6.2", + "version": "0.7.0", "displayName": "TAO Item Runner QTI", "description": "TAO QTI Item Runner modules", "files": [ diff --git a/src/reviewRenderer/renderers/config.js b/src/reviewRenderer/renderers/config.js index 5aa2bf5c..3862b349 100644 --- a/src/reviewRenderer/renderers/config.js +++ b/src/reviewRenderer/renderers/config.js @@ -98,6 +98,7 @@ const locations = { graphicGapMatchInteraction: 'taoQtiItem/reviewRenderer/renderers/interactions/GraphicGapMatchInteraction', uploadInteraction: 'taoQtiItem/reviewRenderer/renderers/interactions/UploadInteraction', customInteraction: 'taoQtiItem/reviewRenderer/renderers/interactions/PortableCustomInteraction', + mediaInteraction: 'taoQtiItem/reviewRenderer/renderers/interactions/MediaInteraction', // Interactions/Choices inherited from qtiCommonRenderer @@ -108,7 +109,6 @@ const locations = { 'taoQtiItem/qtiCommonRenderer/renderers/choices/SimpleAssociableChoice.MatchInteraction', 'simpleAssociableChoice.associateInteraction': 'taoQtiItem/qtiCommonRenderer/renderers/choices/SimpleAssociableChoice.AssociateInteraction', - mediaInteraction: 'taoQtiItem/qtiCommonRenderer/renderers/interactions/MediaInteraction', gapImg: 'taoQtiItem/qtiCommonRenderer/renderers/choices/GapImg', endAttemptInteraction: 'taoQtiItem/qtiCommonRenderer/renderers/interactions/EndAttemptInteraction', }; diff --git a/src/reviewRenderer/renderers/interactions/MediaInteraction.js b/src/reviewRenderer/renderers/interactions/MediaInteraction.js new file mode 100644 index 00000000..129e6a6c --- /dev/null +++ b/src/reviewRenderer/renderers/interactions/MediaInteraction.js @@ -0,0 +1,136 @@ +/* + * 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) 2020 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); + * + */ + +/** + * @author Ansul Sharma + */ +import $ from 'jquery'; +import _ from 'lodash'; +import containerHelper from 'taoQtiItem/qtiCommonRenderer/helpers/container'; +import mediaInteraction from 'taoQtiItem/qtiCommonRenderer/renderers/interactions/MediaInteraction'; +import mediaplayer from 'ui/mediaplayer'; + +const defaults = { + type: 'video/mp4', + video: { + height: 270 + } +}; + +/** + * 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 + * @returns {Promise} + */ +const render = function render(interaction) { + return new Promise((resolve) => { + const $container = containerHelper.get(interaction); + const media = interaction.object; + const $item = $container.parents('.qti-item'); + const url = media.attr('data') || ''; + + /** + * Resize video player elements to fit container size + * @param {Object} mediaElement - player instance + */ + const resize = _.debounce(() => { + if (interaction.mediaElement) { + 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) { + interaction.mediaElement = mediaplayer({ + url: url && this.resolveUrl(url), + type: media.attr('type') || defaults.type, + canPause: true, + canSeek: true, + width: media.attr('width'), + height: media.attr('height'), + autoStart: false, + loop: false, + renderTo: $('.media-container', $container) + }) + .on('render', () => { + resize(); + + $(window) + .off('resize.mediaInteraction') + .on('resize.mediaInteraction', resize); + + $item.off('resize.gridEdit').on('resize.gridEdit', resize); + + /** + * @event playerrendered + */ + $container.trigger('playerrendered'); + }) + .on('ready', () => { + /** + * @event playerready + */ + $container.trigger('playerready'); + + // declare the item ready when player is ready to play. + resolve(); + }) + .on( + 'update', + _.throttle(() => { + containerHelper.triggerResponseChangeEvent(interaction); + }, 1000) + ) + .on('ended', () => { + containerHelper.triggerResponseChangeEvent(interaction); + }); + } + }; + + if (_.size(media.attributes) === 0) { + //TODO move to afterCreate + media.attr('type', defaults.type); + media.attr('width', $container.innerWidth()); + + media.attr('height', defaults.video.height); + media.attr('data', ''); + } + + //initialize the component + $container.on('responseSet', () => { + initMediaPlayer(); + }); + + //gives a small chance to the responseSet event before initializing the player + initMediaPlayer(); + }); +}; + +/** + * Expose the common renderer for the media interaction + * @exports reviewRenderer/renderers/interactions/mediaInteraction + */ +export default Object.assign({}, mediaInteraction, {render}); \ No newline at end of file