diff --git a/libraries/H5P.Video-1.5/scripts/vimeo.js b/libraries/H5P.Video-1.5/scripts/vimeo.js index 2dcfd574..81dc67ea 100644 --- a/libraries/H5P.Video-1.5/scripts/vimeo.js +++ b/libraries/H5P.Video-1.5/scripts/vimeo.js @@ -1,6 +1,8 @@ /** @namespace H5P */ H5P.VideoVimeo = (function ($) { + let numInstances = 0; + /** * Vimeo video player for H5P. * @@ -9,342 +11,252 @@ H5P.VideoVimeo = (function ($) { * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ - function Vimeo(sources, options, l10n) { - var self = this; + function VimeoPlayer(sources, options, l10n) { + const self = this; + + let player; + + // Since all the methods of the Vimeo Player SDK are promise-based, we keep + // track of all relevant state variables so that we can implement the + // H5P.Video API where all methods return synchronously. + let buffered = 0; + let currentQuality; + let currentTextTrack; + let currentTime = 0; + let duration = 0; + let isMuted = 0; + let volume = 0; + let playbackRate = 1; + let qualities = []; + let loadingFailedTimeout; + let failedLoading = false; + let ratio = 9/16; + + const LOADING_TIMEOUT_IN_SECONDS = 8; + + const id = `h5p-vimeo-${++numInstances}`; + const $wrapper = $('
'); + const $placeholder = $('', { + id: id, + html: `` + }).appendTo($wrapper); /** - * Small helper to ensure all video sources get the same cache buster. + * Create a new player with the Vimeo Player SDK. * * @private - * @param {Object} source - * @return {string} - */ - const getCrossOriginPath = function (source) { - let path = H5P.getPath(source.path, self.contentId); - if (video.crossOrigin !== null && H5P.addQueryParameter && H5PIntegration.crossoriginCacheBuster) { - path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster); - } - return path - }; - - - /** - * Register track to video - * - * @param {Object} trackData Track object - * @param {string} trackData.kind Kind of track - * @param {Object} trackData.track Source path - * @param {string} [trackData.label] Label of track - * @param {string} [trackData.srcLang] Language code */ - const addTrack = function (trackData) { - // Skip invalid tracks - if (!trackData.kind || !trackData.track.path) { + const createVimeoPlayer = async () => { + if (!$placeholder.is(':visible') || player !== undefined) { return; } - var track = document.createElement('track'); - track.kind = trackData.kind; - track.src = getCrossOriginPath(trackData.track); // Uses same crossOrigin as parent. You cannot mix. - if (trackData.label) { - track.label = trackData.label; - } - - if (trackData.srcLang) { - track.srcLang = trackData.srcLang; - } + // Since the SDK is loaded asynchronously below, explicitly set player to + // null (unlike undefined) which indicates that creation has begun. This + // allows the guard statement above to be hit if this function is called + // more than once. + player = null; + + const Vimeo = await loadVimeoPlayerSDK(); + + const MIN_WIDTH = 200; + const width = Math.max($wrapper.width(), MIN_WIDTH); + + const canHasControls = options.controls || self.pressToPlay; + const embedOptions = { + url: sources[0].path, + controls: canHasControls, + responsive: true, + dnt: true, + // Hardcoded autoplay to false to avoid playing videos on init + autoplay: false, + loop: options.loop ? true : false, + playsinline: true, + quality: 'auto', + width: width, + muted: false, + keyboard: canHasControls, + }; + + // Create a new player + player = new Vimeo.Player(id, embedOptions); + + registerVimeoPlayerEventListeneners(player); + + // Failsafe timeout to handle failed loading of videos. + // This seems to happen for private videos even though the SDK docs + // suggests to catch PrivacyError when attempting play() + loadingFailedTimeout = setTimeout(() => { + failedLoading = true; + removeLoadingIndicator(); + $wrapper.html(`${l10n.vimeoLoadingError}
`); + $wrapper.css({ + width: null, + height: null + }); + self.trigger('resize'); + self.trigger('error', l10n.vimeoLoadingError); + }, LOADING_TIMEOUT_IN_SECONDS * 1000); + } - return track; + const removeLoadingIndicator = () => { + $placeholder.find('div.h5p-video-loading').remove(); }; /** - * Small helper to set the inital video source. - * Useful if some of the loading happens asynchronously. - * NOTE: Setting the crossOrigin must happen before any of the - * sources(poster, tracks etc.) are loaded + * Register event listeners on the given Vimeo player. * * @private + * @param {Vimeo.Player} player */ - const setInitialSource = function () { - var videoId = getId(qualities[currentQuality].source.path); - if (H5P.setSource !== undefined) { - if (videoId) { - $.ajax({ - type: "POST", - data: { - videoId: videoId, - type: 'vimeo' - }, - url: apiPath, - success: function (data) { - qualities[currentQuality].source.path = (data != '') ? data : qualities[currentQuality].source.path; - H5P.setSource(video, qualities[currentQuality].source, self.contentId) - }, - error: function (XMLHttpRequest, textStatus, errorThrown) { - console.log('Something went wrong with Vimeo!') - } - }); - } else { - H5P.setSource(video, qualities[currentQuality].source, self.contentId) - } - } - else { - // Backwards compatibility (H5P < v1.22) - const srcPath = H5P.getPath(qualities[currentQuality].source.path, self.contentId); - if (H5P.getCrossOrigin !== undefined) { - var crossOrigin = H5P.getCrossOrigin(srcPath); - video.setAttribute('crossorigin', crossOrigin !== null ? crossOrigin : 'anonymous'); + const registerVimeoPlayerEventListeneners = (player) => { + let isFirstPlay, tracks; + player.on('loaded', async () => { + isFirstPlay = true; + clearTimeout(loadingFailedTimeout); + + const videoDetails = await getVimeoVideoMetadata(player); + tracks = videoDetails.tracks.options; + currentTextTrack = tracks.current; + duration = videoDetails.duration; + qualities = videoDetails.qualities; + currentQuality = 'auto'; + try { + ratio = videoDetails.dimensions.height / videoDetails.dimensions.width; } - video.src = srcPath; - } + catch (e) { /* Intentionally ignore this, and fallback on the default ratio */ } - // Add poster if provided - if (options.poster) { - video.poster = getCrossOriginPath(options.poster); // Uses same crossOrigin as parent. You cannot mix. - } + removeLoadingIndicator(); - // Register tracks - options.tracks.forEach(function (track, i) { - var trackElement = addTrack(track); - if (i === 0) { - trackElement.default = true; - } - if (trackElement) { - video.appendChild(trackElement); + if (options.startAt) { + // Vimeo.Player doesn't have an option for setting start time upon + // instantiation, so we instead perform an initial seek here. + currentTime = await self.seek(options.startAt); } - }); - }; - - /** - * Displayed when the video is buffering - * @private - */ - var $throbber = $('', { - 'class': 'h5p-video-loading' - }); - - /** - * Used to display error messages - * @private - */ - var $error = $('', { - 'class': 'h5p-video-error' - }); - /** - * Keep track of current state when changing quality. - * @private - */ - var stateBeforeChangingQuality; - var currentTimeBeforeChangingQuality; - - /** - * Avoids firing the same event twice. - * @private - */ - var lastState; - - /** - * Keeps track whether or not the video has been loaded. - * @private - */ - var isLoaded = false; - - /** - * - * @private - */ - var playbackRate = 1; - var skipRateChange = false; - - // Create player - var video = document.createElement('video'); - - // Sort sources into qualities - var qualities = getQualities(sources, video); - var currentQuality; - - numQualities = 0; - for (let quality in qualities) { - numQualities++; - } - - if (numQualities > 1 && H5P.VideoVimeo.getExternalQuality !== undefined) { - H5P.VideoVimeo.getExternalQuality(sources, function (chosenQuality) { - if (qualities[chosenQuality] !== undefined) { - currentQuality = chosenQuality; - } - setInitialSource(); + self.trigger('ready'); + self.trigger('loaded'); + self.trigger('qualityChange', currentQuality); + self.trigger('resize'); }); - } - else { - // Select quality and source - currentQuality = getPreferredQuality(); - if (currentQuality === undefined || qualities[currentQuality] === undefined) { - // No preferred quality, pick the first. - for (currentQuality in qualities) { - if (qualities.hasOwnProperty(currentQuality)) { - break; + + player.on('play', () => { + if (isFirstPlay) { + isFirstPlay = false; + if (tracks.length) { + self.trigger('captions', tracks); } } - } - setInitialSource(); - } - - // Setting webkit-playsinline, which makes iOS 10 beeing able to play video - // inside browser. - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - video.setAttribute('preload', 'metadata'); - - // Remove buttons in Chrome's video player: - let controlsList = 'nodownload'; - if (options.disableFullscreen) { - controlsList += ' nofullscreen'; - } - if (options.disableRemotePlayback) { - controlsList += ' noremoteplayback'; - } - video.setAttribute('controlsList', controlsList); - - // Remove picture in picture as it interfers with other video players - video.disablePictureInPicture = true; - - // Set options - video.disableRemotePlayback = (options.disableRemotePlayback ? true : false); - video.controls = (options.controls ? true : false); - video.autoplay = (options.autoplay ? true : false); - video.loop = (options.loop ? true : false); - video.className = 'h5p-video'; - video.style.display = 'block'; - - if (options.fit) { - // Style is used since attributes with relative sizes aren't supported by IE9. - video.style.width = '100%'; - video.style.height = '100%'; - } - - /** - * Helps registering events. - * - * @private - * @param {String} native Event name - * @param {String} h5p Event name - * @param {String} [arg] Optional argument - */ - var mapEvent = function (native, h5p, arg) { - video.addEventListener(native, function () { - switch (h5p) { - case 'stateChange': - if (lastState === arg) { - return; // Avoid firing event twice. - } - - var validStartTime = options.startAt && options.startAt > 0; - if (arg === H5P.Video.PLAYING && validStartTime) { - video.currentTime = options.startAt; - delete options.startAt; - } - - break; + }); - case 'loaded': - isLoaded = true; - - if (stateBeforeChangingQuality !== undefined) { - return; // Avoid loaded event when changing quality. - } - - // Remove any errors - if ($error.is(':visible')) { - $error.remove(); - } - - if (OLD_ANDROID_FIX) { - var andLoaded = function () { - video.removeEventListener('durationchange', andLoaded, false); - // On Android seeking isn't ready until after play. - self.trigger(h5p); - }; - video.addEventListener('durationchange', andLoaded, false); - return; - } - break; + // Handle playback state changes. + player.on('playing', () => self.trigger('stateChange', H5P.Video.PLAYING)); + player.on('pause', () => self.trigger('stateChange', H5P.Video.PAUSED)); + player.on('ended', () => self.trigger('stateChange', H5P.Video.ENDED)); - case 'error': - // Handle error and get message. - arg = error(arguments[0], arguments[1]); - break; + // Track the percentage of video that has finished loading (buffered). + player.on('progress', (data) => { + buffered = data.percent * 100; + }); - case 'playbackRateChange': - - // Fix for keeping playback rate in IE11 - if (skipRateChange) { - skipRateChange = false; - return; // Avoid firing event when changing back - } - if (H5P.Video.IE11_PLAYBACK_RATE_FIX && playbackRate != video.playbackRate) { // Intentional - // Prevent change in playback rate not triggered by the user - video.playbackRate = playbackRate; - skipRateChange = true; - return; - } - // End IE11 fix - - arg = self.getPlaybackRate(); - break; - } - self.trigger(h5p, arg); - }, false); + // Track the current time. The update frequency may be browser-dependent, + // according to the official docs: + // https://developer.vimeo.com/player/sdk/reference#timeupdate + player.on('timeupdate', (time) => { + currentTime = time.seconds; + }); }; /** - * Handle errors from the video player. + * Get metadata about the video loaded in the given Vimeo player. + * + * Example resolved value: + * + * ``` + * { + * "duration": 39, + * "qualities": [ + * { + * "name": "auto", + * "label": "Auto" + * }, + * { + * "name": "1080p", + * "label": "1080p" + * }, + * { + * "name": "720p", + * "label": "720p" + * } + * ], + * "dimensions": { + * "width": 1920, + * "height": 1080 + * }, + * "tracks": { + * "current": { + * "label": "English", + * "value": "en" + * }, + * "options": [ + * { + * "label": "English", + * "value": "en" + * }, + * { + * "label": "Norsk bokmål", + * "value": "nb" + * } + * ] + * } + * } + * ``` * * @private - * @param {Object} code Error - * @param {String} [message] - * @returns {String} Human readable error message. + * @param {Vimeo.Player} player + * @returns {Promise} */ - var error = function (code, message) { - if (code instanceof Event) { - - // No error code - if (!code.target.error) { - return ''; - } + const getVimeoVideoMetadata = (player) => { + // Create an object for easy lookup of relevant metadata + const massageVideoMetadata = (data) => { + const duration = data[0]; + const qualities = data[1].map(q => ({ + name: q.id, + label: q.label + })); + const tracks = data[2].reduce((tracks, current) => { + const h5pVideoTrack = new H5P.Video.LabelValue(current.label, current.language); + tracks.options.push(h5pVideoTrack); + if (current.mode === 'showing') { + tracks.current = h5pVideoTrack; + } + return tracks; + }, { current: undefined, options: [] }); + const dimensions = { width: data[3], height: data[4] }; + + return { + duration, + qualities, + tracks, + dimensions + }; + }; + + return Promise.all([ + player.getDuration(), + player.getQualities(), + player.getTextTracks(), + player.getVideoWidth(), + player.getVideoHeight(), + ]).then(data => massageVideoMetadata(data)); + } - switch (code.target.error.code) { - case MediaError.MEDIA_ERR_ABORTED: - message = l10n.aborted; - break; - case MediaError.MEDIA_ERR_NETWORK: - message = l10n.networkFailure; - break; - case MediaError.MEDIA_ERR_DECODE: - message = l10n.cannotDecode; - break; - case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: - message = l10n.formatNotSupported; - break; - case MediaError.MEDIA_ERR_ENCRYPTED: - message = l10n.mediaEncrypted; - break; - } - } - if (!message) { - message = l10n.unknownError; + try { + if (document.featurePolicy.allowsFeature('autoplay') === false) { + self.pressToPlay = true; } - - // Hide throbber - $throbber.remove(); - - // Display error message to user - $error.text(message).insertAfter(video); - - // Pass message to our error event - return message; - }; + } + catch (err) {} /** * Appends the video player to the DOM. @@ -352,146 +264,81 @@ H5P.VideoVimeo = (function ($) { * @public * @param {jQuery} $container */ - self.appendTo = function ($container) { - $container.append(video); + self.appendTo = ($container) => { + $container.addClass('h5p-vimeo').append($wrapper); + createVimeoPlayer(); }; /** - * Get list of available qualities. Not available until after play. + * Get list of available qualities. * * @public * @returns {Array} */ - self.getQualities = function () { - // Create reverse list - var options = []; - for (var q in qualities) { - if (qualities.hasOwnProperty(q)) { - options.splice(0, 0, { - name: q, - label: qualities[q].label - }); - } - } - - if (options.length < 2) { - // Do not return if only one quality. - return; - } - - return options; + self.getQualities = () => { + return qualities; }; /** - * Get current playback quality. Not available until after play. + * Get the current quality. * - * @public - * @returns {String} + * @returns {String} Current quality identifier */ - self.getQuality = function () { + self.getQuality = () => { return currentQuality; }; /** - * Set current playback quality. Not available until after play. - * Listen to event "qualityChange" to check if successful. + * Set the playback quality. * * @public - * @params {String} [quality] + * @param {String} quality */ - self.setQuality = function (quality) { - if (qualities[quality] === undefined || quality === currentQuality) { - return; // Invalid quality - } - - // Keep track of last choice - setPreferredQuality(quality); - - // Avoid multiple loaded events if changing quality multiple times. - if (!stateBeforeChangingQuality) { - // Keep track of last state - stateBeforeChangingQuality = lastState; - - // Keep track of current time - currentTimeBeforeChangingQuality = video.currentTime; - - // Seek and start video again after loading. - var loaded = function () { - video.removeEventListener('loadedmetadata', loaded, false); - if (OLD_ANDROID_FIX) { - var andLoaded = function () { - video.removeEventListener('durationchange', andLoaded, false); - // On Android seeking isn't ready until after play. - self.seek(currentTimeBeforeChangingQuality); - }; - video.addEventListener('durationchange', andLoaded, false); - } - else { - // Seek to current time. - self.seek(currentTimeBeforeChangingQuality); - } - - // Always play to get image. - video.play(); - - if (stateBeforeChangingQuality !== H5P.Video.PLAYING) { - // Do not resume playing - video.pause(); - } - - // Done changing quality - stateBeforeChangingQuality = undefined; - - // Remove any errors - if ($error.is(':visible')) { - $error.remove(); - } - }; - video.addEventListener('loadedmetadata', loaded, false); - } - - // Keep track of current quality - currentQuality = quality; + self.setQuality = async (quality) => { + currentQuality = await player.setQuality(quality); self.trigger('qualityChange', currentQuality); - - // Display throbber - self.trigger('stateChange', H5P.Video.BUFFERING); - - // Change source - video.src = getCrossOriginPath(qualities[quality].source); // (iPad does not support #t=). - // Note: Optional tracks use same crossOrigin as the original. You cannot mix. - - // Remove poster so it will not show during quality change - video.removeAttribute('poster'); }; /** - * Starts the video. + * Start the video. * * @public - * @return {Promise|undefined} May return a Promise that resolves when - * play has been processed. */ - self.play = function () { - if ($error.is(':visible')) { + self.play = async () => { + if (!player) { + self.on('ready', self.play); return; } - if (!isLoaded) { - // Make sure video is loaded before playing - video.load(); + try { + await player.play(); } + catch (error) { + switch (error.name) { + case 'PasswordError': // The video is password-protected + self.trigger('error', l10n.vimeoPasswordError); + break; + + case 'PrivacyError': // The video is private + self.trigger('error', l10n.vimeoPrivacyError); + break; - return video.play(); + default: + self.trigger('error', l10n.unknownError); + break; + } + } }; /** - * Pauses the video. + * Pause the video. * * @public */ - self.pause = function () { - video.pause(); + self.pause = () => { + if (player) { + player.pause(); + } }; /** @@ -500,39 +347,25 @@ H5P.VideoVimeo = (function ($) { * @public * @param {Number} time */ - self.seek = function (time) { - if (lastState === undefined) { - // Make sure we always play before we seek to get an image. - // If not iOS devices will reset currentTime when pressing play. - video.play(); - video.pause(); - } - - video.currentTime = time; + self.seek = async (time) => { + currentTime = time; + await player.setCurrentTime(time); }; /** - * Get elapsed time since video beginning. - * * @public - * @returns {Number} + * @returns {Number} Seconds elapsed since beginning of video */ - self.getCurrentTime = function () { - return video.currentTime; + self.getCurrentTime = () => { + return currentTime; }; /** - * Get total video duration time. - * * @public - * @returns {Number} + * @returns {Number} Video duration in seconds */ - self.getDuration = function () { - if (isNaN(video.duration)) { - return; - } - - return video.duration; + self.getDuration = () => { + return duration; }; /** @@ -541,196 +374,145 @@ H5P.VideoVimeo = (function ($) { * @public * @returns {Number} Between 0 and 100 */ - self.getBuffered = function () { - // Find buffer currently playing from - var buffered = 0; - for (var i = 0; i < video.buffered.length; i++) { - var from = video.buffered.start(i); - var to = video.buffered.end(i); - - if (video.currentTime > from && video.currentTime < to) { - buffered = to; - break; - } - } - - // To percentage - return buffered ? (buffered / video.duration) * 100 : 0; + self.getBuffered = () => { + return buffered; }; /** - * Turn off video sound. + * Mute the video. * * @public */ - self.mute = function () { - video.muted = true; + self.mute = async () => { + isMuted = await player.setMuted(true); }; /** - * Turn on video sound. + * Unmute the video. * * @public */ - self.unMute = function () { - video.muted = false; + self.unMute = async () => { + isMuted = await player.setMuted(false); }; /** - * Check if video sound is turned on or off. + * Whether the video is muted. * * @public - * @returns {Boolean} + * @returns {Boolean} True if the video is muted, false otherwise */ - self.isMuted = function () { - return video.muted; + self.isMuted = () => { + return isMuted; }; /** - * Returns the video sound level. + * Get the video player's current sound volume. * * @public * @returns {Number} Between 0 and 100. */ - self.getVolume = function () { - return video.volume * 100; + self.getVolume = () => { + return volume; }; /** - * Set video sound level. + * Set the video player's sound volume. * * @public - * @param {Number} level Between 0 and 100. + * @param {Number} level */ - self.setVolume = function (level) { - video.volume = level / 100; + self.setVolume = async (level) => { + volume = await player.setVolume(level); }; /** * Get list of available playback rates. * * @public - * @returns {Array} available playback rates + * @returns {Array} Available playback rates */ - self.getPlaybackRates = function () { - /* - * not sure if there's a common rule about determining good speeds - * using Google's standard options via a constant for setting - */ - var playbackRates = PLAYBACK_RATES; - - return playbackRates; + self.getPlaybackRates = () => { + return [0.5, 1, 1.5, 2]; }; /** - * Get current playback rate. + * Get the current playback rate. * * @public - * @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2 + * @returns {Number} e.g. 0.5, 1, 1.5 or 2 */ - self.getPlaybackRate = function () { - return video.playbackRate; + self.getPlaybackRate = () => { + return playbackRate; }; /** - * Set current playback rate. - * Listen to event "playbackRateChange" to check if successful. + * Set the current playback rate. * * @public - * @params {Number} suggested rate that may be rounded to supported values + * @param {Number} rate Must be one of available rates from getPlaybackRates */ - self.setPlaybackRate = function (newPlaybackRate) { - playbackRate = newPlaybackRate; - video.playbackRate = newPlaybackRate; + self.setPlaybackRate = async (rate) => { + playbackRate = await player.setPlaybackRate(rate); + self.trigger('playbackRateChange', rate); }; /** * Set current captions track. * - * @param {H5P.Video.LabelValue} Captions track to show during playback + * @public + * @param {H5P.Video.LabelValue} track Captions to display */ - self.setCaptionsTrack = function (track) { - for (var i = 0; i < video.textTracks.length; i++) { - video.textTracks[i].mode = (track && track.value === i ? 'showing' : 'disabled'); + self.setCaptionsTrack = (track) => { + if (!track) { + return player.disableTextTrack().then(() => { + currentTextTrack = null; + }); } + + player.enableTextTrack(track.value).then(() => { + currentTextTrack = track; + }); }; /** - * Figure out which captions track is currently used. + * Get current captions track. * - * @return {H5P.Video.LabelValue} Captions track + * @public + * @returns {H5P.Video.LabelValue} */ - self.getCaptionsTrack = function () { - for (var i = 0; i < video.textTracks.length; i++) { - if (video.textTracks[i].mode === 'showing') { - return new H5P.Video.LabelValue(video.textTracks[i].label, i); - } - } - - return null; + self.getCaptionsTrack = () => { + return currentTextTrack; }; - // Register event listeners - mapEvent('ended', 'stateChange', H5P.Video.ENDED); - mapEvent('playing', 'stateChange', H5P.Video.PLAYING); - mapEvent('pause', 'stateChange', H5P.Video.PAUSED); - mapEvent('waiting', 'stateChange', H5P.Video.BUFFERING); - mapEvent('loadedmetadata', 'loaded'); - mapEvent('canplay', 'canplay'); - mapEvent('error', 'error'); - mapEvent('ratechange', 'playbackRateChange'); - - if (!video.controls) { - // Disable context menu(right click) to prevent controls. - video.addEventListener('contextmenu', function (event) { - event.preventDefault(); - }, false); - } - - // Display throbber when buffering/loading video. - self.on('stateChange', function (event) { - var state = event.data; - lastState = state; - if (state === H5P.Video.BUFFERING) { - $throbber.insertAfter(video); - } - else { - $throbber.remove(); + self.on('resize', () => { + if (failedLoading || !$wrapper.is(':visible')) { + return; } - }); - - // Load captions after the video is loaded - self.on('loaded', function () { - nextTick(function () { - var textTracks = []; - for (var i = 0; i < video.textTracks.length; i++) { - textTracks.push(new H5P.Video.LabelValue(video.textTracks[i].label, i)); - } - if (textTracks.length) { - self.trigger('captions', textTracks); - } - }); - }); - // Alternative to 'canplay' event - /*self.on('resize', function () { - if (video.offsetParent === null) { + if (player === undefined) { + // Player isn't created yet. Try again. + createVimeoPlayer(); return; } - video.style.width = '100%'; - video.style.height = '100%'; - - var width = video.clientWidth; - var height = options.fit ? video.clientHeight : (width * (video.videoHeight / video.videoWidth)); + // Use as much space as possible + $wrapper.css({ + width: '100%', + height: 'auto' + }); - video.style.width = width + 'px'; - video.style.height = height + 'px'; - });*/ + const width = $wrapper[0].clientWidth; + const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio)); - // Video controls are ready - nextTick(function () { - self.trigger('ready'); + // Validate height before setting + if (height > 0) { + // Set size + $wrapper.css({ + width: width + 'px', + height: height + 'px' + }); + } }); } @@ -742,7 +524,7 @@ H5P.VideoVimeo = (function ($) { * @param {Array} sources * @returns {Boolean} */ - Vimeo.canPlay = function (sources) { + VimeoPlayer.canPlay = (sources) => { return getId(sources[0].path); }; @@ -751,202 +533,42 @@ H5P.VideoVimeo = (function ($) { * * @private * @param {String} url - * @returns {String} Vimeo video identifier + * @returns {String} Vimeo video ID */ - - var getId = function (url) { - // Has some false positives, but should cover all regular URLs that people can find - var matches = url.match(/(?:http|https)?:?\/?\/?(?:www\.)?(?:player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/i); + const getId = (url) => { + // https://stackoverflow.com/a/11660798 + /*const matches = url.match(/^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/); + if (matches && matches[5]) { + return matches[5]; + }*/ + // Cover all regular URLs that people can find + const matches = url.match(/(?:http|https)?:?\/?\/?(?:www\.)?(?:player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/i); if (matches && matches[1]) { return matches[1]; } }; /** - * Find source type. - * - * @private - * @param {Object} source - * @returns {String} - */ - var getType = function (source) { - var type = source.mime; - if (!type) { - // Try to get type from URL - var matches = source.path.match(/\.(\w+)$/); - if (matches && matches[1]) { - type = 'video/' + matches[1]; - } - } - - if (type && source.codecs) { - // Add codecs - type += '; codecs="' + source.codecs + '"'; - } - - return type; - }; - - /** - * Sort sources into qualities. - * - * @private - * @static - * @param {Array} sources - * @param {Object} video - * @returns {Object} Quality mapping - */ - var getQualities = function (sources, video) { - var qualities = {}; - var qualityIndex = 1; - var lastQuality; - - // Cycle through sources - for (var i = 0; i < sources.length; i++) { - var source = sources[i]; - - // Find and update type. - var type = source.type = getType(source); - - // Check if we support this type - var isPlayable = type && (type === 'video/unknown' || video.canPlayType(type) !== ''); - if (!isPlayable) { - continue; // We cannot play this source - } - - if (source.quality === undefined) { - /** - * No quality metadata. Create a quality tag to separate multiple sources of the same type, - * e.g. if two mp4 files with different quality has been uploaded - */ - - if (lastQuality === undefined || qualities[lastQuality].source.type === type) { - // Create a new quality tag - source.quality = { - name: 'q' + qualityIndex, - label: (source.metadata && source.metadata.qualityName) ? source.metadata.qualityName : 'Quality ' + qualityIndex // TODO: l10n - }; - qualityIndex++; - } - else { - /** - * Assumes quality already exists in a different format. - * Uses existing label for this quality. - */ - source.quality = qualities[lastQuality].source.quality; - } - } - - // Log last quality - lastQuality = source.quality.name; - - // Look to see if quality exists - var quality = qualities[lastQuality]; - if (quality) { - // We have a source with this quality. Check if we have a better format. - if (source.mime.split('/')[1] === PREFERRED_FORMAT) { - quality.source = source; - } - } - else { - // Add new source with quality. - qualities[source.quality.name] = { - label: source.quality.label, - source: source - }; - } - } - - return qualities; - }; - - /** - * Set preferred video quality. + * Load the Vimeo Player SDK asynchronously. * * @private - * @static - * @param {String} quality Index of preferred quality + * @returns {Promise} Vimeo Player SDK object */ - var setPreferredQuality = function (quality) { - try { - localStorage.setItem('h5pVideoQuality', quality); + const loadVimeoPlayerSDK = async () => { + if (window.Vimeo) { + return await Promise.resolve(window.Vimeo); } - catch (err) { - console.warn('Unable to set preferred video quality, localStorage is not available.'); - } - }; - /** - * Get preferred video quality. - * - * @private - * @static - * @returns {String} Index of preferred quality - */ - var getPreferredQuality = function () { - // First check localStorage - let quality; - try { - quality = localStorage.getItem('h5pVideoQuality'); - } - catch (err) { - console.warn('Unable to retrieve preferred video quality from localStorage.'); - } - if (!quality) { - try { - // The fallback to old cookie solution - var settings = document.cookie.split(';'); - for (var i = 0; i < settings.length; i++) { - var setting = settings[i].split('='); - if (setting[0] === 'H5PVideoQuality') { - quality = setting[1]; - break; - } - } - } - catch (err) { - console.warn('Unable to retrieve preferred video quality from cookie.'); - } - } - return quality; - }; - - /** - * Helps schedule a task for the next tick. - * @param {function} task - */ - var nextTick = function (task) { - setTimeout(task, 0); + return await new Promise((resolve, reject) => { + const tag = document.createElement('script'); + tag.src = 'https://player.vimeo.com/api/player.js'; + tag.onload = () => resolve(window.Vimeo); + tag.onerror = reject; + document.querySelector('script').before(tag); + }); }; - /** @constant {Boolean} */ - var OLD_ANDROID_FIX = false; - - /** @constant {Boolean} */ - var PREFERRED_FORMAT = 'mp4'; - - /** @constant {Object} */ - var PLAYBACK_RATES = [0.25, 0.5, 1, 1.25, 1.5, 2]; - - if (navigator.userAgent.indexOf('Android') !== -1) { - // We have Android, check version. - var version = navigator.userAgent.match(/AppleWebKit\/(\d+\.?\d*)/); - if (version && version[1] && Number(version[1]) <= 534.30) { - // Include fix for devices running the native Android browser. - // (We don't know when video was fixed, so the number is just the lastest - // native android browser we found.) - OLD_ANDROID_FIX = true; - } - } - else { - if (navigator.userAgent.indexOf('Chrome') !== -1) { - // If we're using chrome on a device that isn't Android, prefer the webm - // format. This is because Chrome has trouble with some mp4 codecs. - PREFERRED_FORMAT = 'webm'; - } - } - - return Vimeo; + return VimeoPlayer; })(H5P.jQuery); // Register video handler diff --git a/libraries/H5P.Video-1.6/scripts/komodo.js b/libraries/H5P.Video-1.6/scripts/komodo.js index 0fe3edce..8e0d39dd 100644 --- a/libraries/H5P.Video-1.6/scripts/komodo.js +++ b/libraries/H5P.Video-1.6/scripts/komodo.js @@ -780,12 +780,24 @@ H5P.VideoKomodo = (function ($) { */ var getId = function (url) { - var urlBreak = new URL(url).href.split('/'); - if (urlBreak[2] == 'komododecks.com') { - return urlBreak[4]; - } + if(validateUrl(url)) { + var urlBreak = new URL(url).href.split('/'); + if (urlBreak[2] == 'komododecks.com') { + return urlBreak[4]; + } + }else + return false; }; + var validateUrl = function (urlString) { + try { + return Boolean(new URL(urlString)); + } + catch(e){ + return false; + } + } + /** * Find source type. * diff --git a/libraries/H5P.Video-1.6/scripts/vimeo.js b/libraries/H5P.Video-1.6/scripts/vimeo.js index 2dc48c07..81dc67ea 100644 --- a/libraries/H5P.Video-1.6/scripts/vimeo.js +++ b/libraries/H5P.Video-1.6/scripts/vimeo.js @@ -1,6 +1,8 @@ /** @namespace H5P */ H5P.VideoVimeo = (function ($) { + let numInstances = 0; + /** * Vimeo video player for H5P. * @@ -9,342 +11,252 @@ H5P.VideoVimeo = (function ($) { * @param {Object} options Settings for the player * @param {Object} l10n Localization strings */ - function Vimeo(sources, options, l10n) { - var self = this; + function VimeoPlayer(sources, options, l10n) { + const self = this; + + let player; + + // Since all the methods of the Vimeo Player SDK are promise-based, we keep + // track of all relevant state variables so that we can implement the + // H5P.Video API where all methods return synchronously. + let buffered = 0; + let currentQuality; + let currentTextTrack; + let currentTime = 0; + let duration = 0; + let isMuted = 0; + let volume = 0; + let playbackRate = 1; + let qualities = []; + let loadingFailedTimeout; + let failedLoading = false; + let ratio = 9/16; + + const LOADING_TIMEOUT_IN_SECONDS = 8; + + const id = `h5p-vimeo-${++numInstances}`; + const $wrapper = $(''); + const $placeholder = $('', { + id: id, + html: `` + }).appendTo($wrapper); /** - * Small helper to ensure all video sources get the same cache buster. + * Create a new player with the Vimeo Player SDK. * * @private - * @param {Object} source - * @return {string} - */ - const getCrossOriginPath = function (source) { - let path = H5P.getPath(source.path, self.contentId); - if (video.crossOrigin !== null && H5P.addQueryParameter && H5PIntegration.crossoriginCacheBuster) { - path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster); - } - return path - }; - - - /** - * Register track to video - * - * @param {Object} trackData Track object - * @param {string} trackData.kind Kind of track - * @param {Object} trackData.track Source path - * @param {string} [trackData.label] Label of track - * @param {string} [trackData.srcLang] Language code */ - const addTrack = function (trackData) { - // Skip invalid tracks - if (!trackData.kind || !trackData.track.path) { + const createVimeoPlayer = async () => { + if (!$placeholder.is(':visible') || player !== undefined) { return; } - var track = document.createElement('track'); - track.kind = trackData.kind; - track.src = getCrossOriginPath(trackData.track); // Uses same crossOrigin as parent. You cannot mix. - if (trackData.label) { - track.label = trackData.label; - } - - if (trackData.srcLang) { - track.srcLang = trackData.srcLang; - } + // Since the SDK is loaded asynchronously below, explicitly set player to + // null (unlike undefined) which indicates that creation has begun. This + // allows the guard statement above to be hit if this function is called + // more than once. + player = null; + + const Vimeo = await loadVimeoPlayerSDK(); + + const MIN_WIDTH = 200; + const width = Math.max($wrapper.width(), MIN_WIDTH); + + const canHasControls = options.controls || self.pressToPlay; + const embedOptions = { + url: sources[0].path, + controls: canHasControls, + responsive: true, + dnt: true, + // Hardcoded autoplay to false to avoid playing videos on init + autoplay: false, + loop: options.loop ? true : false, + playsinline: true, + quality: 'auto', + width: width, + muted: false, + keyboard: canHasControls, + }; + + // Create a new player + player = new Vimeo.Player(id, embedOptions); + + registerVimeoPlayerEventListeneners(player); + + // Failsafe timeout to handle failed loading of videos. + // This seems to happen for private videos even though the SDK docs + // suggests to catch PrivacyError when attempting play() + loadingFailedTimeout = setTimeout(() => { + failedLoading = true; + removeLoadingIndicator(); + $wrapper.html(`${l10n.vimeoLoadingError}
`); + $wrapper.css({ + width: null, + height: null + }); + self.trigger('resize'); + self.trigger('error', l10n.vimeoLoadingError); + }, LOADING_TIMEOUT_IN_SECONDS * 1000); + } - return track; + const removeLoadingIndicator = () => { + $placeholder.find('div.h5p-video-loading').remove(); }; /** - * Small helper to set the inital video source. - * Useful if some of the loading happens asynchronously. - * NOTE: Setting the crossOrigin must happen before any of the - * sources(poster, tracks etc.) are loaded + * Register event listeners on the given Vimeo player. * * @private + * @param {Vimeo.Player} player */ - const setInitialSource = function () { - var videoId = getId(qualities[currentQuality].source.path); - if (H5P.setSource !== undefined) { - if (videoId) { - $.ajax({ - type: "POST", - data: { - videoId: videoId, - type: 'vimeo' - }, - url: apiPath, - success: function (data) { - qualities[currentQuality].source.path = (data != '') ? data : qualities[currentQuality].source.path; - H5P.setSource(video, qualities[currentQuality].source, self.contentId) - }, - error: function (XMLHttpRequest, textStatus, errorThrown) { - console.log('Something went wrong with Vimeo!') - } - }); - } else { - H5P.setSource(video, qualities[currentQuality].source, self.contentId) - } - } - else { - // Backwards compatibility (H5P < v1.22) - const srcPath = H5P.getPath(qualities[currentQuality].source.path, self.contentId); - if (H5P.getCrossOrigin !== undefined) { - var crossOrigin = H5P.getCrossOrigin(srcPath); - video.setAttribute('crossorigin', crossOrigin !== null ? crossOrigin : 'anonymous'); + const registerVimeoPlayerEventListeneners = (player) => { + let isFirstPlay, tracks; + player.on('loaded', async () => { + isFirstPlay = true; + clearTimeout(loadingFailedTimeout); + + const videoDetails = await getVimeoVideoMetadata(player); + tracks = videoDetails.tracks.options; + currentTextTrack = tracks.current; + duration = videoDetails.duration; + qualities = videoDetails.qualities; + currentQuality = 'auto'; + try { + ratio = videoDetails.dimensions.height / videoDetails.dimensions.width; } - video.src = srcPath; - } + catch (e) { /* Intentionally ignore this, and fallback on the default ratio */ } - // Add poster if provided - if (options.poster) { - video.poster = getCrossOriginPath(options.poster); // Uses same crossOrigin as parent. You cannot mix. - } + removeLoadingIndicator(); - // Register tracks - options.tracks.forEach(function (track, i) { - var trackElement = addTrack(track); - if (i === 0) { - trackElement.default = true; - } - if (trackElement) { - video.appendChild(trackElement); + if (options.startAt) { + // Vimeo.Player doesn't have an option for setting start time upon + // instantiation, so we instead perform an initial seek here. + currentTime = await self.seek(options.startAt); } - }); - }; - - /** - * Displayed when the video is buffering - * @private - */ - var $throbber = $('', { - 'class': 'h5p-video-loading' - }); - - /** - * Used to display error messages - * @private - */ - var $error = $('', { - 'class': 'h5p-video-error' - }); - /** - * Keep track of current state when changing quality. - * @private - */ - var stateBeforeChangingQuality; - var currentTimeBeforeChangingQuality; - - /** - * Avoids firing the same event twice. - * @private - */ - var lastState; - - /** - * Keeps track whether or not the video has been loaded. - * @private - */ - var isLoaded = false; - - /** - * - * @private - */ - var playbackRate = 1; - var skipRateChange = false; - - // Create player - var video = document.createElement('video'); - - // Sort sources into qualities - var qualities = getQualities(sources, video); - var currentQuality; - - numQualities = 0; - for (let quality in qualities) { - numQualities++; - } - - if (numQualities > 1 && H5P.VideoVimeo.getExternalQuality !== undefined) { - H5P.VideoVimeo.getExternalQuality(sources, function (chosenQuality) { - if (qualities[chosenQuality] !== undefined) { - currentQuality = chosenQuality; - } - setInitialSource(); + self.trigger('ready'); + self.trigger('loaded'); + self.trigger('qualityChange', currentQuality); + self.trigger('resize'); }); - } - else { - // Select quality and source - currentQuality = getPreferredQuality(); - if (currentQuality === undefined || qualities[currentQuality] === undefined) { - // No preferred quality, pick the first. - for (currentQuality in qualities) { - if (qualities.hasOwnProperty(currentQuality)) { - break; + + player.on('play', () => { + if (isFirstPlay) { + isFirstPlay = false; + if (tracks.length) { + self.trigger('captions', tracks); } } - } - setInitialSource(); - } - - // Setting webkit-playsinline, which makes iOS 10 beeing able to play video - // inside browser. - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - video.setAttribute('preload', 'metadata'); - - // Remove buttons in Chrome's video player: - let controlsList = 'nodownload'; - if (options.disableFullscreen) { - controlsList += ' nofullscreen'; - } - if (options.disableRemotePlayback) { - controlsList += ' noremoteplayback'; - } - video.setAttribute('controlsList', controlsList); - - // Remove picture in picture as it interfers with other video players - video.disablePictureInPicture = true; - - // Set options - video.disableRemotePlayback = (options.disableRemotePlayback ? true : false); - video.controls = (options.controls ? true : false); - video.autoplay = (options.autoplay ? true : false); - video.loop = (options.loop ? true : false); - video.className = 'h5p-video'; - video.style.display = 'block'; - - if (options.fit) { - // Style is used since attributes with relative sizes aren't supported by IE9. - video.style.width = '100%'; - video.style.height = '100%'; - } - - /** - * Helps registering events. - * - * @private - * @param {String} native Event name - * @param {String} h5p Event name - * @param {String} [arg] Optional argument - */ - var mapEvent = function (native, h5p, arg) { - video.addEventListener(native, function () { - switch (h5p) { - case 'stateChange': - if (lastState === arg) { - return; // Avoid firing event twice. - } - - var validStartTime = options.startAt && options.startAt > 0; - if (arg === H5P.Video.PLAYING && validStartTime) { - video.currentTime = options.startAt; - delete options.startAt; - } - - break; + }); - case 'loaded': - isLoaded = true; - - if (stateBeforeChangingQuality !== undefined) { - return; // Avoid loaded event when changing quality. - } - - // Remove any errors - if ($error.is(':visible')) { - $error.remove(); - } - - if (OLD_ANDROID_FIX) { - var andLoaded = function () { - video.removeEventListener('durationchange', andLoaded, false); - // On Android seeking isn't ready until after play. - self.trigger(h5p); - }; - video.addEventListener('durationchange', andLoaded, false); - return; - } - break; + // Handle playback state changes. + player.on('playing', () => self.trigger('stateChange', H5P.Video.PLAYING)); + player.on('pause', () => self.trigger('stateChange', H5P.Video.PAUSED)); + player.on('ended', () => self.trigger('stateChange', H5P.Video.ENDED)); - case 'error': - // Handle error and get message. - arg = error(arguments[0], arguments[1]); - break; + // Track the percentage of video that has finished loading (buffered). + player.on('progress', (data) => { + buffered = data.percent * 100; + }); - case 'playbackRateChange': - - // Fix for keeping playback rate in IE11 - if (skipRateChange) { - skipRateChange = false; - return; // Avoid firing event when changing back - } - if (H5P.Video.IE11_PLAYBACK_RATE_FIX && playbackRate != video.playbackRate) { // Intentional - // Prevent change in playback rate not triggered by the user - video.playbackRate = playbackRate; - skipRateChange = true; - return; - } - // End IE11 fix - - arg = self.getPlaybackRate(); - break; - } - self.trigger(h5p, arg); - }, false); + // Track the current time. The update frequency may be browser-dependent, + // according to the official docs: + // https://developer.vimeo.com/player/sdk/reference#timeupdate + player.on('timeupdate', (time) => { + currentTime = time.seconds; + }); }; /** - * Handle errors from the video player. + * Get metadata about the video loaded in the given Vimeo player. + * + * Example resolved value: + * + * ``` + * { + * "duration": 39, + * "qualities": [ + * { + * "name": "auto", + * "label": "Auto" + * }, + * { + * "name": "1080p", + * "label": "1080p" + * }, + * { + * "name": "720p", + * "label": "720p" + * } + * ], + * "dimensions": { + * "width": 1920, + * "height": 1080 + * }, + * "tracks": { + * "current": { + * "label": "English", + * "value": "en" + * }, + * "options": [ + * { + * "label": "English", + * "value": "en" + * }, + * { + * "label": "Norsk bokmål", + * "value": "nb" + * } + * ] + * } + * } + * ``` * * @private - * @param {Object} code Error - * @param {String} [message] - * @returns {String} Human readable error message. + * @param {Vimeo.Player} player + * @returns {Promise} */ - var error = function (code, message) { - if (code instanceof Event) { - - // No error code - if (!code.target.error) { - return ''; - } + const getVimeoVideoMetadata = (player) => { + // Create an object for easy lookup of relevant metadata + const massageVideoMetadata = (data) => { + const duration = data[0]; + const qualities = data[1].map(q => ({ + name: q.id, + label: q.label + })); + const tracks = data[2].reduce((tracks, current) => { + const h5pVideoTrack = new H5P.Video.LabelValue(current.label, current.language); + tracks.options.push(h5pVideoTrack); + if (current.mode === 'showing') { + tracks.current = h5pVideoTrack; + } + return tracks; + }, { current: undefined, options: [] }); + const dimensions = { width: data[3], height: data[4] }; + + return { + duration, + qualities, + tracks, + dimensions + }; + }; + + return Promise.all([ + player.getDuration(), + player.getQualities(), + player.getTextTracks(), + player.getVideoWidth(), + player.getVideoHeight(), + ]).then(data => massageVideoMetadata(data)); + } - switch (code.target.error.code) { - case MediaError.MEDIA_ERR_ABORTED: - message = l10n.aborted; - break; - case MediaError.MEDIA_ERR_NETWORK: - message = l10n.networkFailure; - break; - case MediaError.MEDIA_ERR_DECODE: - message = l10n.cannotDecode; - break; - case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: - message = l10n.formatNotSupported; - break; - case MediaError.MEDIA_ERR_ENCRYPTED: - message = l10n.mediaEncrypted; - break; - } - } - if (!message) { - message = l10n.unknownError; + try { + if (document.featurePolicy.allowsFeature('autoplay') === false) { + self.pressToPlay = true; } - - // Hide throbber - $throbber.remove(); - - // Display error message to user - $error.text(message).insertAfter(video); - - // Pass message to our error event - return message; - }; + } + catch (err) {} /** * Appends the video player to the DOM. @@ -352,146 +264,81 @@ H5P.VideoVimeo = (function ($) { * @public * @param {jQuery} $container */ - self.appendTo = function ($container) { - $container.append(video); + self.appendTo = ($container) => { + $container.addClass('h5p-vimeo').append($wrapper); + createVimeoPlayer(); }; /** - * Get list of available qualities. Not available until after play. + * Get list of available qualities. * * @public * @returns {Array} */ - self.getQualities = function () { - // Create reverse list - var options = []; - for (var q in qualities) { - if (qualities.hasOwnProperty(q)) { - options.splice(0, 0, { - name: q, - label: qualities[q].label - }); - } - } - - if (options.length < 2) { - // Do not return if only one quality. - return; - } - - return options; + self.getQualities = () => { + return qualities; }; /** - * Get current playback quality. Not available until after play. + * Get the current quality. * - * @public - * @returns {String} + * @returns {String} Current quality identifier */ - self.getQuality = function () { + self.getQuality = () => { return currentQuality; }; /** - * Set current playback quality. Not available until after play. - * Listen to event "qualityChange" to check if successful. + * Set the playback quality. * * @public - * @params {String} [quality] + * @param {String} quality */ - self.setQuality = function (quality) { - if (qualities[quality] === undefined || quality === currentQuality) { - return; // Invalid quality - } - - // Keep track of last choice - setPreferredQuality(quality); - - // Avoid multiple loaded events if changing quality multiple times. - if (!stateBeforeChangingQuality) { - // Keep track of last state - stateBeforeChangingQuality = lastState; - - // Keep track of current time - currentTimeBeforeChangingQuality = video.currentTime; - - // Seek and start video again after loading. - var loaded = function () { - video.removeEventListener('loadedmetadata', loaded, false); - if (OLD_ANDROID_FIX) { - var andLoaded = function () { - video.removeEventListener('durationchange', andLoaded, false); - // On Android seeking isn't ready until after play. - self.seek(currentTimeBeforeChangingQuality); - }; - video.addEventListener('durationchange', andLoaded, false); - } - else { - // Seek to current time. - self.seek(currentTimeBeforeChangingQuality); - } - - // Always play to get image. - video.play(); - - if (stateBeforeChangingQuality !== H5P.Video.PLAYING) { - // Do not resume playing - video.pause(); - } - - // Done changing quality - stateBeforeChangingQuality = undefined; - - // Remove any errors - if ($error.is(':visible')) { - $error.remove(); - } - }; - video.addEventListener('loadedmetadata', loaded, false); - } - - // Keep track of current quality - currentQuality = quality; + self.setQuality = async (quality) => { + currentQuality = await player.setQuality(quality); self.trigger('qualityChange', currentQuality); - - // Display throbber - self.trigger('stateChange', H5P.Video.BUFFERING); - - // Change source - video.src = getCrossOriginPath(qualities[quality].source); // (iPad does not support #t=). - // Note: Optional tracks use same crossOrigin as the original. You cannot mix. - - // Remove poster so it will not show during quality change - video.removeAttribute('poster'); }; /** - * Starts the video. + * Start the video. * * @public - * @return {Promise|undefined} May return a Promise that resolves when - * play has been processed. */ - self.play = function () { - if ($error.is(':visible')) { + self.play = async () => { + if (!player) { + self.on('ready', self.play); return; } - if (!isLoaded) { - // Make sure video is loaded before playing - video.load(); + try { + await player.play(); } + catch (error) { + switch (error.name) { + case 'PasswordError': // The video is password-protected + self.trigger('error', l10n.vimeoPasswordError); + break; - return video.play(); + case 'PrivacyError': // The video is private + self.trigger('error', l10n.vimeoPrivacyError); + break; + + default: + self.trigger('error', l10n.unknownError); + break; + } + } }; /** - * Pauses the video. + * Pause the video. * * @public */ - self.pause = function () { - video.pause(); + self.pause = () => { + if (player) { + player.pause(); + } }; /** @@ -500,39 +347,25 @@ H5P.VideoVimeo = (function ($) { * @public * @param {Number} time */ - self.seek = function (time) { - if (lastState === undefined) { - // Make sure we always play before we seek to get an image. - // If not iOS devices will reset currentTime when pressing play. - video.play(); - video.pause(); - } - - video.currentTime = time; + self.seek = async (time) => { + currentTime = time; + await player.setCurrentTime(time); }; /** - * Get elapsed time since video beginning. - * * @public - * @returns {Number} + * @returns {Number} Seconds elapsed since beginning of video */ - self.getCurrentTime = function () { - return video.currentTime; + self.getCurrentTime = () => { + return currentTime; }; /** - * Get total video duration time. - * * @public - * @returns {Number} + * @returns {Number} Video duration in seconds */ - self.getDuration = function () { - if (isNaN(video.duration)) { - return; - } - - return video.duration; + self.getDuration = () => { + return duration; }; /** @@ -541,205 +374,145 @@ H5P.VideoVimeo = (function ($) { * @public * @returns {Number} Between 0 and 100 */ - self.getBuffered = function () { - // Find buffer currently playing from - var buffered = 0; - for (var i = 0; i < video.buffered.length; i++) { - var from = video.buffered.start(i); - var to = video.buffered.end(i); - - if (video.currentTime > from && video.currentTime < to) { - buffered = to; - break; - } - } - - // To percentage - return buffered ? (buffered / video.duration) * 100 : 0; + self.getBuffered = () => { + return buffered; }; /** - * Turn off video sound. + * Mute the video. * * @public */ - self.mute = function () { - video.muted = true; + self.mute = async () => { + isMuted = await player.setMuted(true); }; /** - * Turn on video sound. + * Unmute the video. * * @public */ - self.unMute = function () { - video.muted = false; + self.unMute = async () => { + isMuted = await player.setMuted(false); }; /** - * Check if video sound is turned on or off. + * Whether the video is muted. * * @public - * @returns {Boolean} + * @returns {Boolean} True if the video is muted, false otherwise */ - self.isMuted = function () { - return video.muted; + self.isMuted = () => { + return isMuted; }; /** - * Returns the video sound level. + * Get the video player's current sound volume. * * @public * @returns {Number} Between 0 and 100. */ - self.getVolume = function () { - return video.volume * 100; + self.getVolume = () => { + return volume; }; /** - * Set video sound level. + * Set the video player's sound volume. * * @public - * @param {Number} level Between 0 and 100. + * @param {Number} level */ - self.setVolume = function (level) { - video.volume = level / 100; + self.setVolume = async (level) => { + volume = await player.setVolume(level); }; /** * Get list of available playback rates. * * @public - * @returns {Array} available playback rates + * @returns {Array} Available playback rates */ - self.getPlaybackRates = function () { - /* - * not sure if there's a common rule about determining good speeds - * using Google's standard options via a constant for setting - */ - var playbackRates = PLAYBACK_RATES; - - return playbackRates; + self.getPlaybackRates = () => { + return [0.5, 1, 1.5, 2]; }; /** - * Get current playback rate. + * Get the current playback rate. * * @public - * @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2 + * @returns {Number} e.g. 0.5, 1, 1.5 or 2 */ - self.getPlaybackRate = function () { - return video.playbackRate; + self.getPlaybackRate = () => { + return playbackRate; }; /** - * Set current playback rate. - * Listen to event "playbackRateChange" to check if successful. + * Set the current playback rate. * * @public - * @params {Number} suggested rate that may be rounded to supported values + * @param {Number} rate Must be one of available rates from getPlaybackRates */ - self.setPlaybackRate = function (newPlaybackRate) { - playbackRate = newPlaybackRate; - video.playbackRate = newPlaybackRate; + self.setPlaybackRate = async (rate) => { + playbackRate = await player.setPlaybackRate(rate); + self.trigger('playbackRateChange', rate); }; /** * Set current captions track. * - * @param {H5P.Video.LabelValue} Captions track to show during playback + * @public + * @param {H5P.Video.LabelValue} track Captions to display */ - self.setCaptionsTrack = function (track) { - for (var i = 0; i < video.textTracks.length; i++) { - video.textTracks[i].mode = (track && track.value === i ? 'showing' : 'disabled'); + self.setCaptionsTrack = (track) => { + if (!track) { + return player.disableTextTrack().then(() => { + currentTextTrack = null; + }); } + + player.enableTextTrack(track.value).then(() => { + currentTextTrack = track; + }); }; /** - * Figure out which captions track is currently used. + * Get current captions track. * - * @return {H5P.Video.LabelValue} Captions track + * @public + * @returns {H5P.Video.LabelValue} */ - self.getCaptionsTrack = function () { - for (var i = 0; i < video.textTracks.length; i++) { - if (video.textTracks[i].mode === 'showing') { - return new H5P.Video.LabelValue(video.textTracks[i].label, i); - } - } - - return null; + self.getCaptionsTrack = () => { + return currentTextTrack; }; - // Register event listeners - mapEvent('ended', 'stateChange', H5P.Video.ENDED); - mapEvent('playing', 'stateChange', H5P.Video.PLAYING); - mapEvent('pause', 'stateChange', H5P.Video.PAUSED); - mapEvent('waiting', 'stateChange', H5P.Video.BUFFERING); - mapEvent('loadedmetadata', 'loaded'); - mapEvent('canplay', 'canplay'); - mapEvent('error', 'error'); - mapEvent('ratechange', 'playbackRateChange'); - - if (!video.controls) { - // Disable context menu(right click) to prevent controls. - video.addEventListener('contextmenu', function (event) { - event.preventDefault(); - }, false); - } - - // Display throbber when buffering/loading video. - self.on('stateChange', function (event) { - var state = event.data; - lastState = state; - if (state === H5P.Video.BUFFERING) { - $throbber.insertAfter(video); - } - else { - $throbber.remove(); - } - }); - - // Load captions after the video is loaded - self.on('loaded', function () { - nextTick(function () { - var textTracks = []; - for (var i = 0; i < video.textTracks.length; i++) { - textTracks.push(new H5P.Video.LabelValue(video.textTracks[i].label, i)); - } - if (textTracks.length) { - self.trigger('captions', textTracks); - } - }); - }); - - // play or pause on clicking video - video.addEventListener('click', function () { - if (video.paused) { - video.play(); - } else { - video.pause(); + self.on('resize', () => { + if (failedLoading || !$wrapper.is(':visible')) { + return; } - }); - // Alternative to 'canplay' event - /*self.on('resize', function () { - if (video.offsetParent === null) { + if (player === undefined) { + // Player isn't created yet. Try again. + createVimeoPlayer(); return; } - video.style.width = '100%'; - video.style.height = '100%'; - - var width = video.clientWidth; - var height = options.fit ? video.clientHeight : (width * (video.videoHeight / video.videoWidth)); + // Use as much space as possible + $wrapper.css({ + width: '100%', + height: 'auto' + }); - video.style.width = width + 'px'; - video.style.height = height + 'px'; - });*/ + const width = $wrapper[0].clientWidth; + const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio)); - // Video controls are ready - nextTick(function () { - self.trigger('ready'); + // Validate height before setting + if (height > 0) { + // Set size + $wrapper.css({ + width: width + 'px', + height: height + 'px' + }); + } }); } @@ -751,7 +524,7 @@ H5P.VideoVimeo = (function ($) { * @param {Array} sources * @returns {Boolean} */ - Vimeo.canPlay = function (sources) { + VimeoPlayer.canPlay = (sources) => { return getId(sources[0].path); }; @@ -760,202 +533,42 @@ H5P.VideoVimeo = (function ($) { * * @private * @param {String} url - * @returns {String} Vimeo video identifier + * @returns {String} Vimeo video ID */ - - var getId = function (url) { - // Has some false positives, but should cover all regular URLs that people can find - var matches = url.match(/(?:http|https)?:?\/?\/?(?:www\.)?(?:player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/i); + const getId = (url) => { + // https://stackoverflow.com/a/11660798 + /*const matches = url.match(/^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/); + if (matches && matches[5]) { + return matches[5]; + }*/ + // Cover all regular URLs that people can find + const matches = url.match(/(?:http|https)?:?\/?\/?(?:www\.)?(?:player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/i); if (matches && matches[1]) { return matches[1]; } }; /** - * Find source type. + * Load the Vimeo Player SDK asynchronously. * * @private - * @param {Object} source - * @returns {String} + * @returns {Promise} Vimeo Player SDK object */ - var getType = function (source) { - var type = source.mime; - if (!type) { - // Try to get type from URL - var matches = source.path.match(/\.(\w+)$/); - if (matches && matches[1]) { - type = 'video/' + matches[1]; - } - } - - if (type && source.codecs) { - // Add codecs - type += '; codecs="' + source.codecs + '"'; + const loadVimeoPlayerSDK = async () => { + if (window.Vimeo) { + return await Promise.resolve(window.Vimeo); } - return type; - }; - - /** - * Sort sources into qualities. - * - * @private - * @static - * @param {Array} sources - * @param {Object} video - * @returns {Object} Quality mapping - */ - var getQualities = function (sources, video) { - var qualities = {}; - var qualityIndex = 1; - var lastQuality; - - // Cycle through sources - for (var i = 0; i < sources.length; i++) { - var source = sources[i]; - - // Find and update type. - var type = source.type = getType(source); - - // Check if we support this type - var isPlayable = type && (type === 'video/unknown' || video.canPlayType(type) !== ''); - if (!isPlayable) { - continue; // We cannot play this source - } - - if (source.quality === undefined) { - /** - * No quality metadata. Create a quality tag to separate multiple sources of the same type, - * e.g. if two mp4 files with different quality has been uploaded - */ - - if (lastQuality === undefined || qualities[lastQuality].source.type === type) { - // Create a new quality tag - source.quality = { - name: 'q' + qualityIndex, - label: (source.metadata && source.metadata.qualityName) ? source.metadata.qualityName : 'Quality ' + qualityIndex // TODO: l10n - }; - qualityIndex++; - } - else { - /** - * Assumes quality already exists in a different format. - * Uses existing label for this quality. - */ - source.quality = qualities[lastQuality].source.quality; - } - } - - // Log last quality - lastQuality = source.quality.name; - - // Look to see if quality exists - var quality = qualities[lastQuality]; - if (quality) { - // We have a source with this quality. Check if we have a better format. - if (source.mime.split('/')[1] === PREFERRED_FORMAT) { - quality.source = source; - } - } - else { - // Add new source with quality. - qualities[source.quality.name] = { - label: source.quality.label, - source: source - }; - } - } - - return qualities; - }; - - /** - * Set preferred video quality. - * - * @private - * @static - * @param {String} quality Index of preferred quality - */ - var setPreferredQuality = function (quality) { - try { - localStorage.setItem('h5pVideoQuality', quality); - } - catch (err) { - console.warn('Unable to set preferred video quality, localStorage is not available.'); - } - }; - - /** - * Get preferred video quality. - * - * @private - * @static - * @returns {String} Index of preferred quality - */ - var getPreferredQuality = function () { - // First check localStorage - let quality; - try { - quality = localStorage.getItem('h5pVideoQuality'); - } - catch (err) { - console.warn('Unable to retrieve preferred video quality from localStorage.'); - } - if (!quality) { - try { - // The fallback to old cookie solution - var settings = document.cookie.split(';'); - for (var i = 0; i < settings.length; i++) { - var setting = settings[i].split('='); - if (setting[0] === 'H5PVideoQuality') { - quality = setting[1]; - break; - } - } - } - catch (err) { - console.warn('Unable to retrieve preferred video quality from cookie.'); - } - } - return quality; - }; - - /** - * Helps schedule a task for the next tick. - * @param {function} task - */ - var nextTick = function (task) { - setTimeout(task, 0); + return await new Promise((resolve, reject) => { + const tag = document.createElement('script'); + tag.src = 'https://player.vimeo.com/api/player.js'; + tag.onload = () => resolve(window.Vimeo); + tag.onerror = reject; + document.querySelector('script').before(tag); + }); }; - /** @constant {Boolean} */ - var OLD_ANDROID_FIX = false; - - /** @constant {Boolean} */ - var PREFERRED_FORMAT = 'mp4'; - - /** @constant {Object} */ - var PLAYBACK_RATES = [0.25, 0.5, 1, 1.25, 1.5, 2]; - - if (navigator.userAgent.indexOf('Android') !== -1) { - // We have Android, check version. - var version = navigator.userAgent.match(/AppleWebKit\/(\d+\.?\d*)/); - if (version && version[1] && Number(version[1]) <= 534.30) { - // Include fix for devices running the native Android browser. - // (We don't know when video was fixed, so the number is just the lastest - // native android browser we found.) - OLD_ANDROID_FIX = true; - } - } - else { - if (navigator.userAgent.indexOf('Chrome') !== -1) { - // If we're using chrome on a device that isn't Android, prefer the webm - // format. This is because Chrome has trouble with some mp4 codecs. - PREFERRED_FORMAT = 'webm'; - } - } - - return Vimeo; + return VimeoPlayer; })(H5P.jQuery); // Register video handler diff --git a/libraries/H5PEditor.CoursePresentation-1.24/scripts/cp-editor.js b/libraries/H5PEditor.CoursePresentation-1.24/scripts/cp-editor.js index 18535d38..229eab4f 100644 --- a/libraries/H5PEditor.CoursePresentation-1.24/scripts/cp-editor.js +++ b/libraries/H5PEditor.CoursePresentation-1.24/scripts/cp-editor.js @@ -2302,7 +2302,6 @@ H5PEditor.CoursePresentation.prototype.showConfirmationDialog = function (dialog var render_context = { canvasContext: ctx, viewport: viewport, - background: 'black', }; canvas.height = viewport.height; canvas.width = viewport.width; @@ -2312,10 +2311,10 @@ H5PEditor.CoursePresentation.prototype.showConfirmationDialog = function (dialog renderTask.promise.then(() => { // Adding black bars to the side to preserve aspect ratio ctx.drawImage(ctx.canvas, 0, 0, canvas.width-blackBarWidth, canvas.height, blackBarWidth, 0, canvas.width-blackBarWidth, canvas.height); - ctx.beginPath(); - ctx.rect(0, 0, blackBarWidth, canvas.height); ctx.fillStyle = "black"; - ctx.fill(); + ctx.fillRect(0, 0, blackBarWidth, canvas.height); + ctx.fillRect(ctx.canvas.width - blackBarWidth, 0, blackBarWidth, canvas.height); + canvas.toBlob((blob) => { const formData = new FormData(); formData.append("contentId", 0);