From f46ed6bca8b0cc7c79d0737c4b7390ea6ac4a917 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 1 Aug 2023 09:51:46 +0800 Subject: [PATCH 01/47] Add support for EchoVideos from Echo360 as source --- library.json | 3 + scripts/echo360.js | 441 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 scripts/echo360.js diff --git a/library.json b/library.json index 2c59f9a8..1c2c621a 100644 --- a/library.json +++ b/library.json @@ -24,6 +24,9 @@ { "path": "scripts/panopto.js" }, + { + "path": "scripts/echo360.js" + }, { "path": "scripts/html5.js" }, diff --git a/scripts/echo360.js b/scripts/echo360.js new file mode 100644 index 00000000..bb63fc32 --- /dev/null +++ b/scripts/echo360.js @@ -0,0 +1,441 @@ +/** @namespace Echo */ +H5P.VideoEchoVideo = (function ($) { + + let numInstances = 0; + /** + * EchoVideo video player for H5P. + * + * @class + * @param {Array} sources Video files to use + * @param {Object} options Settings for the player + * @param {Object} l10n Localization strings + */ + function EchoPlayer(sources, options, l10n) { + const self = this; + let player; + // Since all the methods of the Echo 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 = 30; + const id = `h5p-echo-${++numInstances}`; + const $wrapper = $('
'); + const $placeholder = $('
', { + id: id, + html: `
` + }).appendTo($wrapper); + /** + * Create a new player by embedding an iframe. + * + * @private + */ + const createEchoPlayer = async () => { + if (!$placeholder.is(':visible') || player !== undefined) { + return; + } + // 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 MIN_WIDTH = 200; + const width = Math.max($wrapper.width(), MIN_WIDTH); + player = $wrapper.html(``)[0].firstChild; + // Create a new player + registerEchoPlayerEventListeneners(player); + loadingFailedTimeout = setTimeout(() => { + failedLoading = true; + removeLoadingIndicator(); + $wrapper.html(`

${l10n.unknownError}

`); + $wrapper.css({ + width: null, + height: null + }); + self.trigger('resize'); + self.trigger('error', l10n.unknownError); + }, LOADING_TIMEOUT_IN_SECONDS * 1000); + }; + const removeLoadingIndicator = () => { + $placeholder.find('div.h5p-video-loading').remove(); + }; + + const resolutions = { + 921600: "720p", //"1280x720" + 2073600: "1080p", //"1920x1080" + 2211840: "2K", //"2048x1080" + 3686400: "1440p", // "2560x1440" + 8294400: "4K", // "3840x2160" + 33177600: "8K" // "7680x4320" + } + + const mapToResToName = quality => { + const resolution = resolutions[quality.width * quality.height] + if (resolution) return resolution + return `${quality.height}p` + } + + function compareQualities(a, b) { + return b.width * b.height - a.width * a.height + } + + const auto = { label: "auto", name: "auto" } + + const mapQualityLevels = qualityLevels => { + const qualities = qualityLevels.sort(compareQualities).map((quality, index) => { + return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) } + }) + return [...qualities, auto] + } + + /** + * Register event listeners on the given Echo player. + * + * @private + * @param {Echo.Player} player + */ + const registerEchoPlayerEventListeneners = (player) => { + let isFirstPlay, tracks; + player.resolveLoading = null; + player.loadingPromise = new Promise(function(resolve) { + player.resolveLoading = resolve; + }); + player.onload = async () => { + isFirstPlay = true; + clearTimeout(loadingFailedTimeout); + player.loadingPromise.then(function() { + self.trigger('ready'); + self.trigger('loaded'); + self.trigger('qualityChange', 'auto'); + self.trigger('resize'); + if (options.startAt) { + // Echo.Player doesn't have an option for setting start time upon + // instantiation, so we instead perform an initial seek here. + self.seek(options.startAt); + } + }); + }; + window.addEventListener('message', function(event) { + let message = ""; + try { + message = JSON.parse(event.data); + } catch (e) { + return; + } + if (message.context !== 'Echo360') { + return; + } + if (message.event == 'init') { + duration = message.data.duration; + currentTime = message.data.currentTime ?? 0; + qualities = mapQualityLevels(message.data.qualityLevels); + currentQuality = qualities.length - 1; + player.resolveLoading(); + self.trigger('resize'); + if (message.data.playing) { + self.trigger('stateChange', H5P.Video.PLAYING); + } else { + self.trigger('stateChange', H5P.Video.PAUSED); + } + } else if (message.event == 'timeline') { + duration = message.data.duration + currentTime = message.data.currentTime ?? 0 + self.trigger('resize'); + if (message.data.playing) { + self.trigger('stateChange', H5P.Video.PLAYING); + } else { + self.trigger('stateChange', H5P.Video.PAUSED); + } + } + }); + }; + try { + if (document.featurePolicy.allowsFeature('autoplay') === false) { + self.pressToPlay = true; + } + } + catch (err) {} + /** + * Appends the video player to the DOM. + * + * @public + * @param {jQuery} $container + */ + self.appendTo = ($container) => { + $container.addClass('h5p-echo').append($wrapper); + createEchoPlayer(); + }; + /** + * Get list of available qualities. + * + * @public + * @returns {Array} + */ + self.getQualities = () => { + return qualities; + }; + /** + * Get the current quality. + * + * @returns {String} Current quality identifier + */ + self.getQuality = () => { + return currentQuality; + }; + /** + * Set the playback quality. + * + * @public + * @param {String} quality + */ + self.setQuality = async (quality) => { + self.post('quality', quality); + currentQuality = quality; + self.trigger('qualityChange', currentQuality); + }; + /** + * Start the video. + * + * @public + */ + self.play = async () => { + if (!player) { + self.on('ready', self.play); + return; + } + self.post('play', 0) + + }; + /** + * Pause the video. + * + * @public + */ + self.pause = () => { + self.post('pause', 0) + }; + /** + * Seek video to given time. + * + * @public + * @param {Number} time + */ + self.seek = (time) => { + self.post('seek', time); + currentTime = time; + }; + /** + * Post a window message to the iframe. + * + * @param event + * @param data + */ + self.post = (event, data) => { + if (player) { + player.contentWindow.postMessage(JSON.stringify({event: event, data: data}), '*'); + } + }; + /** + * @public + * @returns {Number} Seconds elapsed since beginning of video + */ + self.getCurrentTime = () => { + return currentTime; + }; + /** + * @public + * @returns {Number} Video duration in seconds + */ + self.getDuration = () => { + if (duration > 0) { + return duration; + } + return; + }; + /** + * Get percentage of video that is buffered. + * + * @public + * @returns {Number} Between 0 and 100 + */ + self.getBuffered = () => { + return buffered; + }; + /** + * Mute the video. + * + * @public + */ + self.mute = () => { + self.post('mute', 0); + isMuted = true; + }; + /** + * Unmute the video. + * + * @public + */ + self.unMute = () => { + self.post('unmute', 0); + isMuted = false; + }; + /** + * Whether the video is muted. + * + * @public + * @returns {Boolean} True if the video is muted, false otherwise + */ + self.isMuted = () => { + return isMuted; + }; + /** + * Get the video player's current sound volume. + * + * @public + * @returns {Number} Between 0 and 100. + */ + self.getVolume = () => { + return volume; + }; + /** + * Set the video player's sound volume. + * + * @public + * @param {Number} level + */ + self.setVolume = (level) => { + self.post('volume', level); + volume = level; + }; + /** + * Get list of available playback rates. + * + * @public + * @returns {Array} Available playback rates + */ + self.getPlaybackRates = () => { + return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; + }; + /** + * Get the current playback rate. + * + * @public + * @returns {Number} e.g. 0.5, 1, 1.5 or 2 + */ + self.getPlaybackRate = () => { + return playbackRate; + }; + /** + * Set the current playback rate. + * + * @public + * @param {Number} rate Must be one of available rates from getPlaybackRates + */ + self.setPlaybackRate = async (rate) => { + self.post('playbackrate', rate) + playbackRate = rate; + self.trigger('playbackRateChange', rate); + }; + /** + * Set current captions track. + * + * @public + * @param {H5P.Video.LabelValue} track Captions to display + */ + self.setCaptionsTrack = (track) => { + if (!track) { + self.post('texttrack', null); + currentTextTrack = null; + } + self.post('texttrack', track.value) + currentTextTrack = track; + }; + /** + * Get current captions track. + * + * @public + * @returns {H5P.Video.LabelValue} + */ + self.getCaptionsTrack = () => { + return currentTextTrack; + }; + self.on('resize', () => { + if (failedLoading || !$wrapper.is(':visible')) { + return; + } + if (player === undefined) { + // Player isn't created yet. Try again. + createEchoPlayer(); + return; + } + // Use as much space as possible + $wrapper.css({ + width: '100%', + height: 'auto' + }); + const width = $wrapper[0].clientWidth; + const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio)); + // Validate height before setting + if (height > 0) { + // Set size + $wrapper.css({ + width: width + 'px', + height: height + 'px' + }); + } + }); + } + /** + * Check to see if we can play any of the given sources. + * + * @public + * @static + * @param {Array} sources + * @returns {Boolean} + */ + EchoPlayer.canPlay = (sources) => { + return getId(sources[0].path); + }; + /** + * Find id of video from given URL. + * + * @private + * @param {String} url + * @returns {String} Echo video identifier + */ + const getId = (url) => { + const matches = url.match(/^[^\/]+:\/\/(echo360[^\/]+)\/media\/([^\/]+)\/h5p.*$/i); + if (matches && matches.length === 3) { + return [matches[2], matches[2]]; + } + }; + /** + * Load the Echo Player SDK asynchronously. + * + * @private + * @returns {Promise} Echo Player SDK object + */ + const loadEchoPlayerSDK = async () => { + if (window.Echo) { + return await Promise.resolve(window.Echo); + } + return await new Promise((resolve, reject) => { + resolve(window.Echo); + }); + }; + return EchoPlayer; +})(H5P.jQuery); + +// Register video handler +H5P.videoHandlers = H5P.videoHandlers || []; +H5P.videoHandlers.push(H5P.VideoEchoVideo); From 63dada9edb20309946ef36f0f896eda4f64f9ef7 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 09:59:21 +0800 Subject: [PATCH 02/47] Clean eslint warnings --- scripts/echo360.js | 272 ++++++++++++++++++++++----------------------- 1 file changed, 131 insertions(+), 141 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index bb63fc32..95066f79 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -1,7 +1,6 @@ /** @namespace Echo */ H5P.VideoEchoVideo = (function ($) { - let numInstances = 0; /** * EchoVideo video player for H5P. * @@ -11,11 +10,11 @@ H5P.VideoEchoVideo = (function ($) { * @param {Object} l10n Localization strings */ function EchoPlayer(sources, options, l10n) { - const self = this; - let player; // Since all the methods of the Echo 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. + var numInstances = 0; + let player = undefined; let buffered = 0; let currentQuality; let currentTextTrack; @@ -27,7 +26,7 @@ H5P.VideoEchoVideo = (function ($) { let qualities = []; let loadingFailedTimeout; let failedLoading = false; - let ratio = 9/16; + let ratio = 9 / 16; const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const $wrapper = $('
'); @@ -35,68 +34,39 @@ H5P.VideoEchoVideo = (function ($) { id: id, html: `
` }).appendTo($wrapper); - /** - * Create a new player by embedding an iframe. - * - * @private - */ - const createEchoPlayer = async () => { - if (!$placeholder.is(':visible') || player !== undefined) { - return; - } - // 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 MIN_WIDTH = 200; - const width = Math.max($wrapper.width(), MIN_WIDTH); - player = $wrapper.html(``)[0].firstChild; - // Create a new player - registerEchoPlayerEventListeneners(player); - loadingFailedTimeout = setTimeout(() => { - failedLoading = true; - removeLoadingIndicator(); - $wrapper.html(`

${l10n.unknownError}

`); - $wrapper.css({ - width: null, - height: null - }); - self.trigger('resize'); - self.trigger('error', l10n.unknownError); - }, LOADING_TIMEOUT_IN_SECONDS * 1000); - }; + + function compareQualities(a, b) { + return b.width * b.height - a.width * a.height; + } const removeLoadingIndicator = () => { $placeholder.find('div.h5p-video-loading').remove(); }; + const resolutions = { - 921600: "720p", //"1280x720" - 2073600: "1080p", //"1920x1080" - 2211840: "2K", //"2048x1080" - 3686400: "1440p", // "2560x1440" - 8294400: "4K", // "3840x2160" - 33177600: "8K" // "7680x4320" - } + 921600: '720p', //"1280x720" + 2073600: '1080p', //"1920x1080" + 2211840: '2K', //"2048x1080" + 3686400: '1440p', // "2560x1440" + 8294400: '4K', // "3840x2160" + 33177600: '8K' // "7680x4320" + }; - const mapToResToName = quality => { - const resolution = resolutions[quality.width * quality.height] - if (resolution) return resolution - return `${quality.height}p` - } + const auto = { label: 'auto', name: 'auto' }; - function compareQualities(a, b) { - return b.width * b.height - a.width * a.height - } + const mapToResToName = (quality) => { + const resolution = resolutions[quality.width * quality.height]; + if (resolution) return resolution; + return `${quality.height}p`; + }; - const auto = { label: "auto", name: "auto" } + const mapQualityLevels = (qualityLevels) => { + const qualities = qualityLevels.sort(compareQualities).map((quality) => { + return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) }; + }); + return [...qualities, auto]; + }; - const mapQualityLevels = qualityLevels => { - const qualities = qualityLevels.sort(compareQualities).map((quality, index) => { - return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) } - }) - return [...qualities, auto] - } /** * Register event listeners on the given Echo player. @@ -105,73 +75,107 @@ H5P.VideoEchoVideo = (function ($) { * @param {Echo.Player} player */ const registerEchoPlayerEventListeneners = (player) => { - let isFirstPlay, tracks; player.resolveLoading = null; - player.loadingPromise = new Promise(function(resolve) { + player.loadingPromise = new Promise(function (resolve) { player.resolveLoading = resolve; }); player.onload = async () => { - isFirstPlay = true; clearTimeout(loadingFailedTimeout); - player.loadingPromise.then(function() { - self.trigger('ready'); - self.trigger('loaded'); - self.trigger('qualityChange', 'auto'); - self.trigger('resize'); + player.loadingPromise.then(function () { + this.trigger('ready'); + this.trigger('loaded'); + this.trigger('qualityChange', 'auto'); + this.trigger('resize'); if (options.startAt) { // Echo.Player doesn't have an option for setting start time upon // instantiation, so we instead perform an initial seek here. - self.seek(options.startAt); + this.seek(options.startAt); } }); }; - window.addEventListener('message', function(event) { - let message = ""; + window.addEventListener('message', function (event) { + let message = ''; try { message = JSON.parse(event.data); - } catch (e) { + } + catch (e) { return; } if (message.context !== 'Echo360') { return; } - if (message.event == 'init') { + if (message.event === 'init') { duration = message.data.duration; currentTime = message.data.currentTime ?? 0; qualities = mapQualityLevels(message.data.qualityLevels); currentQuality = qualities.length - 1; player.resolveLoading(); - self.trigger('resize'); + this.trigger('resize'); if (message.data.playing) { - self.trigger('stateChange', H5P.Video.PLAYING); - } else { - self.trigger('stateChange', H5P.Video.PAUSED); + this.trigger('stateChange', H5P.Video.PLAYING); + } + else { + this.trigger('stateChange', H5P.Video.PAUSED); } - } else if (message.event == 'timeline') { - duration = message.data.duration - currentTime = message.data.currentTime ?? 0 - self.trigger('resize'); + } + else if (message.event === 'timeline') { + duration = message.data.duration; + currentTime = message.data.currentTime ?? 0; + this.trigger('resize'); if (message.data.playing) { - self.trigger('stateChange', H5P.Video.PLAYING); - } else { - self.trigger('stateChange', H5P.Video.PAUSED); + this.trigger('stateChange', H5P.Video.PLAYING); + } + else { + this.trigger('stateChange', H5P.Video.PAUSED); } } }); }; + /** + * Create a new player by embedding an iframe. + * + * @private + */ + const createEchoPlayer = async () => { + if (!$placeholder.is(':visible') || player !== undefined) { + return; + } + // 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; + player = $wrapper.html('')[0].firstChild; + // Create a new player + registerEchoPlayerEventListeneners(player); + loadingFailedTimeout = setTimeout(() => { + failedLoading = true; + removeLoadingIndicator(); + $wrapper.html(`

${l10n.unknownError}

`); + $wrapper.css({ + width: null, + height: null + }); + this.trigger('resize'); + this.trigger('error', l10n.unknownError); + }, LOADING_TIMEOUT_IN_SECONDS * 1000); + }; + try { if (document.featurePolicy.allowsFeature('autoplay') === false) { - self.pressToPlay = true; + this.pressToPlay = true; } } - catch (err) {} + catch (err) { + console.error(err); + } /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ - self.appendTo = ($container) => { + this.appendTo = ($container) => { $container.addClass('h5p-echo').append($wrapper); createEchoPlayer(); }; @@ -181,7 +185,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Array} */ - self.getQualities = () => { + this.getQualities = () => { return qualities; }; /** @@ -189,7 +193,7 @@ H5P.VideoEchoVideo = (function ($) { * * @returns {String} Current quality identifier */ - self.getQuality = () => { + this.getQuality = () => { return currentQuality; }; /** @@ -198,22 +202,22 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {String} quality */ - self.setQuality = async (quality) => { - self.post('quality', quality); + this.setQuality = async (quality) => { + this.post('quality', quality); currentQuality = quality; - self.trigger('qualityChange', currentQuality); + this.trigger('qualityChange', currentQuality); }; /** * Start the video. * * @public */ - self.play = async () => { + this.play = async () => { if (!player) { - self.on('ready', self.play); + this.on('ready', this.play); return; } - self.post('play', 0) + this.post('play', 0); }; /** @@ -221,8 +225,8 @@ H5P.VideoEchoVideo = (function ($) { * * @public */ - self.pause = () => { - self.post('pause', 0) + this.pause = () => { + this.post('pause', 0); }; /** * Seek video to given time. @@ -230,8 +234,8 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {Number} time */ - self.seek = (time) => { - self.post('seek', time); + this.seek = (time) => { + this.post('seek', time); currentTime = time; }; /** @@ -240,23 +244,23 @@ H5P.VideoEchoVideo = (function ($) { * @param event * @param data */ - self.post = (event, data) => { + this.post = (event, data) => { if (player) { - player.contentWindow.postMessage(JSON.stringify({event: event, data: data}), '*'); + player.contentWindow.postMessage(JSON.stringify({ event: event, data: data }), '*'); } }; /** * @public * @returns {Number} Seconds elapsed since beginning of video */ - self.getCurrentTime = () => { + this.getCurrentTime = () => { return currentTime; }; /** * @public * @returns {Number} Video duration in seconds */ - self.getDuration = () => { + this.getDuration = () => { if (duration > 0) { return duration; } @@ -268,7 +272,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Number} Between 0 and 100 */ - self.getBuffered = () => { + this.getBuffered = () => { return buffered; }; /** @@ -276,8 +280,8 @@ H5P.VideoEchoVideo = (function ($) { * * @public */ - self.mute = () => { - self.post('mute', 0); + this.mute = () => { + this.post('mute', 0); isMuted = true; }; /** @@ -285,8 +289,8 @@ H5P.VideoEchoVideo = (function ($) { * * @public */ - self.unMute = () => { - self.post('unmute', 0); + this.unMute = () => { + this.post('unmute', 0); isMuted = false; }; /** @@ -295,7 +299,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Boolean} True if the video is muted, false otherwise */ - self.isMuted = () => { + this.isMuted = () => { return isMuted; }; /** @@ -304,7 +308,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Number} Between 0 and 100. */ - self.getVolume = () => { + this.getVolume = () => { return volume; }; /** @@ -313,8 +317,8 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {Number} level */ - self.setVolume = (level) => { - self.post('volume', level); + this.setVolume = (level) => { + this.post('volume', level); volume = level; }; /** @@ -323,7 +327,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Array} Available playback rates */ - self.getPlaybackRates = () => { + this.getPlaybackRates = () => { return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; }; /** @@ -332,7 +336,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Number} e.g. 0.5, 1, 1.5 or 2 */ - self.getPlaybackRate = () => { + this.getPlaybackRate = () => { return playbackRate; }; /** @@ -341,10 +345,10 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {Number} rate Must be one of available rates from getPlaybackRates */ - self.setPlaybackRate = async (rate) => { - self.post('playbackrate', rate) + this.setPlaybackRate = async (rate) => { + this.post('playbackrate', rate); playbackRate = rate; - self.trigger('playbackRateChange', rate); + this.trigger('playbackRateChange', rate); }; /** * Set current captions track. @@ -352,12 +356,12 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {H5P.Video.LabelValue} track Captions to display */ - self.setCaptionsTrack = (track) => { + this.setCaptionsTrack = (track) => { if (!track) { - self.post('texttrack', null); + this.post('texttrack', null); currentTextTrack = null; } - self.post('texttrack', track.value) + this.post('texttrack', track.value); currentTextTrack = track; }; /** @@ -366,10 +370,10 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {H5P.Video.LabelValue} */ - self.getCaptionsTrack = () => { + this.getCaptionsTrack = () => { return currentTextTrack; }; - self.on('resize', () => { + this.on('resize', () => { if (failedLoading || !$wrapper.is(':visible')) { return; } @@ -395,17 +399,6 @@ H5P.VideoEchoVideo = (function ($) { } }); } - /** - * Check to see if we can play any of the given sources. - * - * @public - * @static - * @param {Array} sources - * @returns {Boolean} - */ - EchoPlayer.canPlay = (sources) => { - return getId(sources[0].path); - }; /** * Find id of video from given URL. * @@ -414,24 +407,21 @@ H5P.VideoEchoVideo = (function ($) { * @returns {String} Echo video identifier */ const getId = (url) => { - const matches = url.match(/^[^\/]+:\/\/(echo360[^\/]+)\/media\/([^\/]+)\/h5p.*$/i); + const matches = url.match(/^[^/]+:\/\/(echo360[^/]+)\/media\/([^/]+)\/h5p.*$/i); if (matches && matches.length === 3) { return [matches[2], matches[2]]; } }; /** - * Load the Echo Player SDK asynchronously. + * Check to see if we can play any of the given sources. * - * @private - * @returns {Promise} Echo Player SDK object + * @public + * @static + * @param {Array} sources + * @returns {Boolean} */ - const loadEchoPlayerSDK = async () => { - if (window.Echo) { - return await Promise.resolve(window.Echo); - } - return await new Promise((resolve, reject) => { - resolve(window.Echo); - }); + EchoPlayer.canPlay = (sources) => { + return getId(sources[0].path); }; return EchoPlayer; })(H5P.jQuery); From 98abfa567ce0eb4b89d1492836a275fd16a6b1bd Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 10:40:12 +0800 Subject: [PATCH 03/47] Remove (most) jQuery --- scripts/echo360.js | 50 ++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 95066f79..3a2f4b3b 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -1,5 +1,5 @@ /** @namespace Echo */ -H5P.VideoEchoVideo = (function ($) { +H5P.VideoEchoVideo = (function () { /** * EchoVideo video player for H5P. @@ -29,17 +29,17 @@ H5P.VideoEchoVideo = (function ($) { let ratio = 9 / 16; const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; - const $wrapper = $('
'); - const $placeholder = $('
', { - id: id, - html: `
` - }).appendTo($wrapper); + const wrapperElement = document.createElement('div'); + wrapperElement.setAttribute('id', id); + const placeholderElement = document.createElement('div'); + placeholderElement.innerHTML = `
`; + wrapperElement.append(placeholderElement); function compareQualities(a, b) { return b.width * b.height - a.width * a.height; } const removeLoadingIndicator = () => { - $placeholder.find('div.h5p-video-loading').remove(); + placeholderElement.replaceChildren(); }; @@ -131,13 +131,19 @@ H5P.VideoEchoVideo = (function ($) { } }); }; + + const isNodeVisible = (node) => { + let style = window.getComputedStyle(node); + return ((style.display !== 'none') && (style.visibility !== 'hidden')); + }; + /** * Create a new player by embedding an iframe. * * @private */ const createEchoPlayer = async () => { - if (!$placeholder.is(':visible') || player !== undefined) { + if (!isNodeVisible(placeholderElement) || player !== undefined) { return; } // Since the SDK is loaded asynchronously below, explicitly set player to @@ -145,17 +151,15 @@ H5P.VideoEchoVideo = (function ($) { // allows the guard statement above to be hit if this function is called // more than once. player = null; - player = $wrapper.html('')[0].firstChild; + wrapperElement.innerHTML = ''; + player = wrapperElement.firstChild; // Create a new player registerEchoPlayerEventListeneners(player); loadingFailedTimeout = setTimeout(() => { failedLoading = true; removeLoadingIndicator(); - $wrapper.html(`

${l10n.unknownError}

`); - $wrapper.css({ - width: null, - height: null - }); + wrapperElement.innerHTML = `

${l10n.unknownError}

`; + wrapperElement.style.cssText = 'width: null; height: null;'; this.trigger('resize'); this.trigger('error', l10n.unknownError); }, LOADING_TIMEOUT_IN_SECONDS * 1000); @@ -176,7 +180,7 @@ H5P.VideoEchoVideo = (function ($) { * @param {jQuery} $container */ this.appendTo = ($container) => { - $container.addClass('h5p-echo').append($wrapper); + $container.addClass('h5p-echo').append(wrapperElement); createEchoPlayer(); }; /** @@ -374,7 +378,7 @@ H5P.VideoEchoVideo = (function ($) { return currentTextTrack; }; this.on('resize', () => { - if (failedLoading || !$wrapper.is(':visible')) { + if (failedLoading || !isNodeVisible(wrapperElement)) { return; } if (player === undefined) { @@ -383,19 +387,13 @@ H5P.VideoEchoVideo = (function ($) { return; } // Use as much space as possible - $wrapper.css({ - width: '100%', - height: 'auto' - }); - const width = $wrapper[0].clientWidth; - const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio)); + wrapperElement.style.cssText = 'width: 100%; height: auto;'; + const width = wrapperElement.clientWidth; + const height = options.fit ? wrapperElement.clientHeight : (width * (ratio)); // Validate height before setting if (height > 0) { // Set size - $wrapper.css({ - width: width + 'px', - height: height + 'px' - }); + wrapperElement.style.cssText = 'width: ' + width + 'px; height: ' + height + 'px;'; } }); } From 5c020c3d1f24645b0faf9912dce824409c457edc Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 10:48:19 +0800 Subject: [PATCH 04/47] Consistent arrow functions --- scripts/echo360.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 3a2f4b3b..1a2ef3d6 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -27,6 +27,7 @@ H5P.VideoEchoVideo = (function () { let loadingFailedTimeout; let failedLoading = false; let ratio = 9 / 16; + const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const wrapperElement = document.createElement('div'); @@ -35,14 +36,13 @@ H5P.VideoEchoVideo = (function () { placeholderElement.innerHTML = `
`; wrapperElement.append(placeholderElement); - function compareQualities(a, b) { + const compareQualities = (a, b) => { return b.width * b.height - a.width * a.height; } const removeLoadingIndicator = () => { placeholderElement.replaceChildren(); }; - const resolutions = { 921600: '720p', //"1280x720" 2073600: '1080p', //"1920x1080" @@ -76,12 +76,12 @@ H5P.VideoEchoVideo = (function () { */ const registerEchoPlayerEventListeneners = (player) => { player.resolveLoading = null; - player.loadingPromise = new Promise(function (resolve) { + player.loadingPromise = new Promise((resolve) => { player.resolveLoading = resolve; }); player.onload = async () => { clearTimeout(loadingFailedTimeout); - player.loadingPromise.then(function () { + player.loadingPromise.then(() => { this.trigger('ready'); this.trigger('loaded'); this.trigger('qualityChange', 'auto'); @@ -91,6 +91,7 @@ H5P.VideoEchoVideo = (function () { // instantiation, so we instead perform an initial seek here. this.seek(options.startAt); } + return true; }); }; window.addEventListener('message', function (event) { From 3f4cb363b2aa9c7ae825319b58cfcca8fc007e88 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 10:57:09 +0800 Subject: [PATCH 05/47] Add isLoaded function --- scripts/echo360.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 1a2ef3d6..a21b24ad 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -10,9 +10,7 @@ H5P.VideoEchoVideo = (function () { * @param {Object} l10n Localization strings */ function EchoPlayer(sources, options, l10n) { - // Since all the methods of the Echo 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. + // State variables for the Player. var numInstances = 0; let player = undefined; let buffered = 0; @@ -21,6 +19,7 @@ H5P.VideoEchoVideo = (function () { let currentTime = 0; let duration = 0; let isMuted = 0; + let loadingComplete = false; let volume = 0; let playbackRate = 1; let qualities = []; @@ -28,6 +27,7 @@ H5P.VideoEchoVideo = (function () { let failedLoading = false; let ratio = 9 / 16; + // Player specific immutable variables. const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const wrapperElement = document.createElement('div'); @@ -36,13 +36,6 @@ H5P.VideoEchoVideo = (function () { placeholderElement.innerHTML = `
`; wrapperElement.append(placeholderElement); - const compareQualities = (a, b) => { - return b.width * b.height - a.width * a.height; - } - const removeLoadingIndicator = () => { - placeholderElement.replaceChildren(); - }; - const resolutions = { 921600: '720p', //"1280x720" 2073600: '1080p', //"1920x1080" @@ -54,6 +47,16 @@ H5P.VideoEchoVideo = (function () { const auto = { label: 'auto', name: 'auto' }; + /** + * + */ + const compareQualities = (a, b) => { + return b.width * b.height - a.width * a.height; + }; + const removeLoadingIndicator = () => { + placeholderElement.replaceChildren(); + }; + const mapToResToName = (quality) => { const resolution = resolutions[quality.width * quality.height]; if (resolution) return resolution; @@ -84,6 +87,7 @@ H5P.VideoEchoVideo = (function () { player.loadingPromise.then(() => { this.trigger('ready'); this.trigger('loaded'); + this.loadingComplete = true; this.trigger('qualityChange', 'auto'); this.trigger('resize'); if (options.startAt) { @@ -184,6 +188,11 @@ H5P.VideoEchoVideo = (function () { $container.addClass('h5p-echo').append(wrapperElement); createEchoPlayer(); }; + + this.isLoaded = () => { + return loadingComplete; + }; + /** * Get list of available qualities. * From ac0447fad47a83edeeb9a829c7580f9777162f51 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 13:27:00 +0800 Subject: [PATCH 06/47] Iframe permission policy delegation --- scripts/echo360.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index a21b24ad..3cec5d91 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -18,9 +18,9 @@ H5P.VideoEchoVideo = (function () { let currentTextTrack; let currentTime = 0; let duration = 0; - let isMuted = 0; + let isMuted = false; let loadingComplete = false; - let volume = 0; + let volume = 1; let playbackRate = 1; let qualities = []; let loadingFailedTimeout; @@ -98,7 +98,7 @@ H5P.VideoEchoVideo = (function () { return true; }); }; - window.addEventListener('message', function (event) { + window.addEventListener('message', (event) => { let message = ''; try { message = JSON.parse(event.data); @@ -156,7 +156,7 @@ H5P.VideoEchoVideo = (function () { // allows the guard statement above to be hit if this function is called // more than once. player = null; - wrapperElement.innerHTML = ''; + wrapperElement.innerHTML = ''; player = wrapperElement.firstChild; // Create a new player registerEchoPlayerEventListeneners(player); @@ -226,7 +226,7 @@ H5P.VideoEchoVideo = (function () { * * @public */ - this.play = async () => { + this.play = () => { if (!player) { this.on('ready', this.play); return; From 313e5d1221b747b8d97b392c52c940f6460b82d4 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 22 Feb 2024 11:01:23 +0800 Subject: [PATCH 07/47] JSDoc improvements and coding style --- scripts/echo360.js | 89 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 13 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 3cec5d91..a24c0f06 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -31,9 +31,10 @@ H5P.VideoEchoVideo = (function () { const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const wrapperElement = document.createElement('div'); - wrapperElement.setAttribute('id', id); const placeholderElement = document.createElement('div'); - placeholderElement.innerHTML = `
`; + + wrapperElement.setAttribute('id', id); + placeholderElement.innerHTML = `
`; wrapperElement.append(placeholderElement); const resolutions = { @@ -48,21 +49,43 @@ H5P.VideoEchoVideo = (function () { const auto = { label: 'auto', name: 'auto' }; /** - * + * Determine which quality is greater by counting the pixels. + * @private + * @param {Object} a - object with width and height properties + * @param {Object} b - object with width and height properties + * @returns {Number} positive if second parameter has more pixels */ const compareQualities = (a, b) => { return b.width * b.height - a.width * a.height; }; + + /** + * Remove all elements from the placeholder dom element. + * + * @private + */ const removeLoadingIndicator = () => { placeholderElement.replaceChildren(); }; + /** + * Generate a descriptive name for a resolution object with width and height. + * @private + * @param {Object} quality - object with width and height properties + * @returns {String} either a predefined name for the resolution or something like 1080p + */ const mapToResToName = (quality) => { const resolution = resolutions[quality.width * quality.height]; if (resolution) return resolution; return `${quality.height}p`; }; + /** + * Generate an array of objects for use in a dropdown from the list of resolutions. + * @private + * @param {Array} qualityLevels - list of objects with width and height properties + * @returns {Array} list of objects with label and name properties + */ const mapQualityLevels = (qualityLevels) => { const qualities = qualityLevels.sort(compareQualities).map((quality) => { return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) }; @@ -70,12 +93,11 @@ H5P.VideoEchoVideo = (function () { return [...qualities, auto]; }; - /** * Register event listeners on the given Echo player. * * @private - * @param {Echo.Player} player + * @param {HTMLElement} player */ const registerEchoPlayerEventListeneners = (player) => { player.resolveLoading = null; @@ -95,6 +117,9 @@ H5P.VideoEchoVideo = (function () { // instantiation, so we instead perform an initial seek here. this.seek(options.startAt); } + if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { + this.play(); + } return true; }); }; @@ -133,10 +158,21 @@ H5P.VideoEchoVideo = (function () { else { this.trigger('stateChange', H5P.Video.PAUSED); } + if (currentTime >== (duration - 1) && options.loop) { + this.seek(0); + this.play(); + } } }); }; + /** + * Determine if the element is visible by computing the styles. + * + * @private + * @param {HTMLElement} node - the element to check. + * @returns {Boolean} true if it is visible. + */ const isNodeVisible = (node) => { let style = window.getComputedStyle(node); return ((style.display !== 'none') && (style.visibility !== 'hidden')); @@ -146,6 +182,7 @@ H5P.VideoEchoVideo = (function () { * Create a new player by embedding an iframe. * * @private + * @returns {Promise} */ const createEchoPlayer = async () => { if (!isNodeVisible(placeholderElement) || player !== undefined) { @@ -170,14 +207,6 @@ H5P.VideoEchoVideo = (function () { }, LOADING_TIMEOUT_IN_SECONDS * 1000); }; - try { - if (document.featurePolicy.allowsFeature('autoplay') === false) { - this.pressToPlay = true; - } - } - catch (err) { - console.error(err); - } /** * Appends the video player to the DOM. * @@ -189,6 +218,12 @@ H5P.VideoEchoVideo = (function () { createEchoPlayer(); }; + /** + * Determine if the video has loaded. + * + * @public + * @returns {Boolean} + */ this.isLoaded = () => { return loadingComplete; }; @@ -202,14 +237,17 @@ H5P.VideoEchoVideo = (function () { this.getQualities = () => { return qualities; }; + /** * Get the current quality. * + * @public * @returns {String} Current quality identifier */ this.getQuality = () => { return currentQuality; }; + /** * Set the playback quality. * @@ -221,6 +259,7 @@ H5P.VideoEchoVideo = (function () { currentQuality = quality; this.trigger('qualityChange', currentQuality); }; + /** * Start the video. * @@ -234,6 +273,7 @@ H5P.VideoEchoVideo = (function () { this.post('play', 0); }; + /** * Pause the video. * @@ -242,6 +282,7 @@ H5P.VideoEchoVideo = (function () { this.pause = () => { this.post('pause', 0); }; + /** * Seek video to given time. * @@ -252,9 +293,11 @@ H5P.VideoEchoVideo = (function () { this.post('seek', time); currentTime = time; }; + /** * Post a window message to the iframe. * + * @public * @param event * @param data */ @@ -263,14 +306,20 @@ H5P.VideoEchoVideo = (function () { player.contentWindow.postMessage(JSON.stringify({ event: event, data: data }), '*'); } }; + /** + * Return the current play position. + * * @public * @returns {Number} Seconds elapsed since beginning of video */ this.getCurrentTime = () => { return currentTime; }; + /** + * Return the video duration. + * * @public * @returns {Number} Video duration in seconds */ @@ -280,6 +329,7 @@ H5P.VideoEchoVideo = (function () { } return; }; + /** * Get percentage of video that is buffered. * @@ -289,6 +339,7 @@ H5P.VideoEchoVideo = (function () { this.getBuffered = () => { return buffered; }; + /** * Mute the video. * @@ -298,6 +349,7 @@ H5P.VideoEchoVideo = (function () { this.post('mute', 0); isMuted = true; }; + /** * Unmute the video. * @@ -307,6 +359,7 @@ H5P.VideoEchoVideo = (function () { this.post('unmute', 0); isMuted = false; }; + /** * Whether the video is muted. * @@ -316,6 +369,7 @@ H5P.VideoEchoVideo = (function () { this.isMuted = () => { return isMuted; }; + /** * Get the video player's current sound volume. * @@ -325,6 +379,7 @@ H5P.VideoEchoVideo = (function () { this.getVolume = () => { return volume; }; + /** * Set the video player's sound volume. * @@ -335,6 +390,7 @@ H5P.VideoEchoVideo = (function () { this.post('volume', level); volume = level; }; + /** * Get list of available playback rates. * @@ -344,6 +400,7 @@ H5P.VideoEchoVideo = (function () { this.getPlaybackRates = () => { return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; }; + /** * Get the current playback rate. * @@ -353,6 +410,7 @@ H5P.VideoEchoVideo = (function () { this.getPlaybackRate = () => { return playbackRate; }; + /** * Set the current playback rate. * @@ -364,6 +422,7 @@ H5P.VideoEchoVideo = (function () { playbackRate = rate; this.trigger('playbackRateChange', rate); }; + /** * Set current captions track. * @@ -378,6 +437,7 @@ H5P.VideoEchoVideo = (function () { this.post('texttrack', track.value); currentTextTrack = track; }; + /** * Get current captions track. * @@ -387,6 +447,7 @@ H5P.VideoEchoVideo = (function () { this.getCaptionsTrack = () => { return currentTextTrack; }; + this.on('resize', () => { if (failedLoading || !isNodeVisible(wrapperElement)) { return; @@ -407,6 +468,7 @@ H5P.VideoEchoVideo = (function () { } }); } + /** * Find id of video from given URL. * @@ -420,6 +482,7 @@ H5P.VideoEchoVideo = (function () { return [matches[2], matches[2]]; } }; + /** * Check to see if we can play any of the given sources. * From f6b47e20898e54492700efc136716667b66213aa Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 22 Feb 2024 11:07:39 +0800 Subject: [PATCH 08/47] Duration is a number or undefined --- scripts/echo360.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index a24c0f06..3a482c8b 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -321,13 +321,13 @@ H5P.VideoEchoVideo = (function () { * Return the video duration. * * @public - * @returns {Number} Video duration in seconds + * @returns {?Number} Video duration in seconds */ this.getDuration = () => { if (duration > 0) { return duration; } - return; + return null; }; /** From 261340b12a20acb201b925c9dbdc24d5351ecce7 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 22 Feb 2024 13:57:28 +0800 Subject: [PATCH 09/47] Comparison operator --- scripts/echo360.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 3a482c8b..fe2f8d78 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -158,7 +158,7 @@ H5P.VideoEchoVideo = (function () { else { this.trigger('stateChange', H5P.Video.PAUSED); } - if (currentTime >== (duration - 1) && options.loop) { + if (currentTime >= (duration - 1) && options.loop) { this.seek(0); this.play(); } @@ -193,6 +193,16 @@ H5P.VideoEchoVideo = (function () { // allows the guard statement above to be hit if this function is called // more than once. player = null; + let queryString = '?'; + if (options.controls) { + queryString += 'controls=true&'; + } + if (options.disableFullscreen) { + queryString += 'disableFullscreen=true&'; + } + if (options.deactivateSound) { + queryString += 'deactivateSound=true&'; + } wrapperElement.innerHTML = ''; player = wrapperElement.firstChild; // Create a new player From cc460c1033207d6240b8f6994da2c9bad98b97f4 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 27 Feb 2024 09:04:31 +0800 Subject: [PATCH 10/47] Cancel resizing on timeline updates --- scripts/echo360.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index fe2f8d78..52570727 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -151,7 +151,6 @@ H5P.VideoEchoVideo = (function () { else if (message.event === 'timeline') { duration = message.data.duration; currentTime = message.data.currentTime ?? 0; - this.trigger('resize'); if (message.data.playing) { this.trigger('stateChange', H5P.Video.PLAYING); } From 671326e369b3f1917038165a8522d2406262b476 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 27 Feb 2024 10:08:35 +0800 Subject: [PATCH 11/47] Consistent playback rate handling --- scripts/echo360.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 52570727..8768b6bf 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -427,7 +427,8 @@ H5P.VideoEchoVideo = (function () { * @param {Number} rate Must be one of available rates from getPlaybackRates */ this.setPlaybackRate = async (rate) => { - this.post('playbackrate', rate); + const echoRate = parseFloat(rate); + this.post('playbackrate', echoRate); playbackRate = rate; this.trigger('playbackRateChange', rate); }; From 45e03c50778a3e36ef0258ae0a6d8e547237914e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 29 Feb 2024 08:14:39 +0800 Subject: [PATCH 12/47] Improve start time handling --- scripts/echo360.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 8768b6bf..8116e09b 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -112,11 +112,6 @@ H5P.VideoEchoVideo = (function () { this.loadingComplete = true; this.trigger('qualityChange', 'auto'); this.trigger('resize'); - if (options.startAt) { - // Echo.Player doesn't have an option for setting start time upon - // instantiation, so we instead perform an initial seek here. - this.seek(options.startAt); - } if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { this.play(); } @@ -202,6 +197,10 @@ H5P.VideoEchoVideo = (function () { if (options.deactivateSound) { queryString += 'deactivateSound=true&'; } + if (options.startAt) { + // Implicit conversion to millis + queryString += 'startTimeMillis=' + startAt + '000&'; + } wrapperElement.innerHTML = ''; player = wrapperElement.firstChild; // Create a new player From d4a217cc614ddf692f7bebd84e3cad353b8c1015 Mon Sep 17 00:00:00 2001 From: MHod-101 Date: Thu, 7 Mar 2024 13:44:31 +1100 Subject: [PATCH 13/47] PLT-2090: Update quality option mapping Use updated quality options --- scripts/echo360.js | 50 ++++++++-------------------------------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 8116e09b..cb13b26c 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -37,28 +37,6 @@ H5P.VideoEchoVideo = (function () { placeholderElement.innerHTML = `
`; wrapperElement.append(placeholderElement); - const resolutions = { - 921600: '720p', //"1280x720" - 2073600: '1080p', //"1920x1080" - 2211840: '2K', //"2048x1080" - 3686400: '1440p', // "2560x1440" - 8294400: '4K', // "3840x2160" - 33177600: '8K' // "7680x4320" - }; - - const auto = { label: 'auto', name: 'auto' }; - - /** - * Determine which quality is greater by counting the pixels. - * @private - * @param {Object} a - object with width and height properties - * @param {Object} b - object with width and height properties - * @returns {Number} positive if second parameter has more pixels - */ - const compareQualities = (a, b) => { - return b.width * b.height - a.width * a.height; - }; - /** * Remove all elements from the placeholder dom element. * @@ -68,29 +46,17 @@ H5P.VideoEchoVideo = (function () { placeholderElement.replaceChildren(); }; - /** - * Generate a descriptive name for a resolution object with width and height. - * @private - * @param {Object} quality - object with width and height properties - * @returns {String} either a predefined name for the resolution or something like 1080p - */ - const mapToResToName = (quality) => { - const resolution = resolutions[quality.width * quality.height]; - if (resolution) return resolution; - return `${quality.height}p`; - }; - /** * Generate an array of objects for use in a dropdown from the list of resolutions. * @private - * @param {Array} qualityLevels - list of objects with width and height properties + * @param {Array} qualityLevels - list of objects with supported qualities for the media * @returns {Array} list of objects with label and name properties */ const mapQualityLevels = (qualityLevels) => { - const qualities = qualityLevels.sort(compareQualities).map((quality) => { - return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) }; - }); - return [...qualities, auto]; + const qualities = qualityLevels.map((quality) => { + return { label: quality.label.toLowerCase(), name: quality.value } + }) + return qualities; }; /** @@ -110,7 +76,6 @@ H5P.VideoEchoVideo = (function () { this.trigger('ready'); this.trigger('loaded'); this.loadingComplete = true; - this.trigger('qualityChange', 'auto'); this.trigger('resize'); if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { this.play(); @@ -132,9 +97,10 @@ H5P.VideoEchoVideo = (function () { if (message.event === 'init') { duration = message.data.duration; currentTime = message.data.currentTime ?? 0; - qualities = mapQualityLevels(message.data.qualityLevels); - currentQuality = qualities.length - 1; + qualities = mapQualityLevels(message.data.qualityOptions); + currentQuality = qualities[0].name; player.resolveLoading(); + this.trigger('qualityChange', currentQuality); this.trigger('resize'); if (message.data.playing) { this.trigger('stateChange', H5P.Video.PLAYING); From fc1ccb98c6dcaa237a4b6bf8146a8c63b35663cd Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 7 Mar 2024 10:51:52 +0800 Subject: [PATCH 14/47] Fix startAt option --- scripts/echo360.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index cb13b26c..c6c62a93 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -165,7 +165,7 @@ H5P.VideoEchoVideo = (function () { } if (options.startAt) { // Implicit conversion to millis - queryString += 'startTimeMillis=' + startAt + '000&'; + queryString += 'startTimeMillis=' + options.startAt + '000&'; } wrapperElement.innerHTML = ''; player = wrapperElement.firstChild; From 6fd6021f06f58f1a420889059634cf43840c08bf Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 7 Mar 2024 11:39:38 +0800 Subject: [PATCH 15/47] Load player controls before the video starts --- scripts/echo360.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index c6c62a93..dd3200e1 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -79,6 +79,9 @@ H5P.VideoEchoVideo = (function () { this.trigger('resize'); if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { this.play(); + this.trigger('stateChange', H5P.Video.PLAYING); + } else { + this.trigger('stateChange', H5P.Video.PAUSED); } return true; }); @@ -245,7 +248,6 @@ H5P.VideoEchoVideo = (function () { return; } this.post('play', 0); - }; /** From 2f9f86d1638e1e71b50be46b198168d20e2edf8f Mon Sep 17 00:00:00 2001 From: devland <5208532+devland@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:37:15 +0100 Subject: [PATCH 16/47] JI-5798 fix auto pause when video ended (#121) --- scripts/video.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/video.js b/scripts/video.js index ecdbb4c3..8a5b3f2c 100644 --- a/scripts/video.js +++ b/scripts/video.js @@ -112,7 +112,7 @@ H5P.Video = (function ($, ContentCopyrights, MediaCopyright, handlers) { self.play(); } } - else if (state !== Video.PAUSED) { + else if (state !== Video.PAUSED && state !== Video.ENDED) { self.autoPaused = true; self.pause(); } @@ -272,7 +272,7 @@ H5P.Video = (function ($, ContentCopyrights, MediaCopyright, handlers) { * @constant {Number} */ Video.VIDEO_CUED = 5; - + // Used to convert between html and text, since URLs have html entities. var $cleaner = H5P.jQuery('
'); From 120b5cc5eea1207c0eb8cb1df874f71318a41567 Mon Sep 17 00:00:00 2001 From: Vilde Stabell Date: Tue, 19 Mar 2024 14:38:14 +0100 Subject: [PATCH 17/47] Bump patch version --- library.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.json b/library.json index c68a962f..fa075c51 100644 --- a/library.json +++ b/library.json @@ -7,7 +7,7 @@ "author": "Joubel", "majorVersion": 1, "minorVersion": 6, - "patchVersion": 41, + "patchVersion": 42, "runnable": 0, "coreApi": { "majorVersion": 1, From 5fa65368b0a2f6a578adc7e063d0306db811f7fd Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:13:39 +0100 Subject: [PATCH 18/47] Fix numInstances numInstances is supposed to reflect the number of instances of the handler for a unique element id, so it cannot be an instance property --- scripts/echo360.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index dd3200e1..ceb8f283 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -1,5 +1,6 @@ /** @namespace Echo */ -H5P.VideoEchoVideo = (function () { + + let numInstances = 0; /** * EchoVideo video player for H5P. @@ -11,7 +12,6 @@ H5P.VideoEchoVideo = (function () { */ function EchoPlayer(sources, options, l10n) { // State variables for the Player. - var numInstances = 0; let player = undefined; let buffered = 0; let currentQuality; From ea3989bb946f5252d00958f505b6104a99dc60c6 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:16:50 +0100 Subject: [PATCH 19/47] Stop triggering "paused" before video has been "played" Would cause trouble downstream, e.g. in Interactive Video --- scripts/echo360.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index ceb8f283..a9de5dd2 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -80,9 +80,8 @@ if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { this.play(); this.trigger('stateChange', H5P.Video.PLAYING); - } else { - this.trigger('stateChange', H5P.Video.PAUSED); } + return true; }); }; @@ -108,9 +107,6 @@ if (message.data.playing) { this.trigger('stateChange', H5P.Video.PLAYING); } - else { - this.trigger('stateChange', H5P.Video.PAUSED); - } } else if (message.event === 'timeline') { duration = message.data.duration; From c9d61ac8e3e9febeb79f10587c72fdd41844c7e3 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:19:36 +0100 Subject: [PATCH 20/47] Fix updating duration When the message event is "timeline", message.data.duration is `undefined`, the duration value which is used by `getDuration()`, too, would be overwritten and be `undefined` (bug there then), and here looping would not work. --- scripts/echo360.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index a9de5dd2..abdd37ae 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -109,7 +109,7 @@ } } else if (message.event === 'timeline') { - duration = message.data.duration; + duration = message.data.duration ?? this.getDuration(); currentTime = message.data.currentTime ?? 0; if (message.data.playing) { this.trigger('stateChange', H5P.Video.PLAYING); From 4bed4c86dc4df40c074511cb9115333e3158884e Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:20:24 +0100 Subject: [PATCH 21/47] Pass query string with iframe source --- scripts/echo360.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index abdd37ae..4c91ce3a 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -166,7 +166,8 @@ // Implicit conversion to millis queryString += 'startTimeMillis=' + options.startAt + '000&'; } - wrapperElement.innerHTML = ''; + + wrapperElement.innerHTML = ``; player = wrapperElement.firstChild; // Create a new player registerEchoPlayerEventListeneners(player); From 504c9cc6bbe9e3acbd41d87193f6300b2738c2ee Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:24:21 +0100 Subject: [PATCH 22/47] Make eslint happy --- scripts/echo360.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 4c91ce3a..2b3e007a 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -1,4 +1,5 @@ /** @namespace Echo */ +H5P.VideoEchoVideo = (() => { let numInstances = 0; @@ -54,8 +55,8 @@ */ const mapQualityLevels = (qualityLevels) => { const qualities = qualityLevels.map((quality) => { - return { label: quality.label.toLowerCase(), name: quality.value } - }) + return { label: quality.label.toLowerCase(), name: quality.value }; + }); return qualities; }; From bd3559542a91b8a80ae460b15da3f59938c99a3c Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:40:22 +0100 Subject: [PATCH 23/47] Make code robust for document.featurePolicy document.featurePolicy is experimental (no support in Firefox/Safari). Prevent potential crash. --- scripts/echo360.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 2b3e007a..43cd35de 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -78,7 +78,11 @@ H5P.VideoEchoVideo = (() => { this.trigger('loaded'); this.loadingComplete = true; this.trigger('resize'); - if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { + + if ( + options.autoplay && + document.featurePolicy?.allowsFeature('autoplay') + ) { this.play(); this.trigger('stateChange', H5P.Video.PLAYING); } From ea53f33edf9086f19f154a0c78c370c546cf304f Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 16:30:38 +0100 Subject: [PATCH 24/47] Fix startAt handling The parameter is passed as a float in seconds --- scripts/echo360.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 43cd35de..bbe99f82 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -169,7 +169,7 @@ H5P.VideoEchoVideo = (() => { } if (options.startAt) { // Implicit conversion to millis - queryString += 'startTimeMillis=' + options.startAt + '000&'; + queryString += `startTimeMillis=${options.startAt * 1000}&`; } wrapperElement.innerHTML = ``; From 4b25857a2195cfcbee45d46a08db51131239274f Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 1 Aug 2023 09:51:46 +0800 Subject: [PATCH 25/47] Add support for EchoVideos from Echo360 as source --- library.json | 3 + scripts/echo360.js | 441 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 scripts/echo360.js diff --git a/library.json b/library.json index fa075c51..108d42b2 100644 --- a/library.json +++ b/library.json @@ -23,6 +23,9 @@ { "path": "scripts/panopto.js" }, + { + "path": "scripts/echo360.js" + }, { "path": "scripts/html5.js" }, diff --git a/scripts/echo360.js b/scripts/echo360.js new file mode 100644 index 00000000..bb63fc32 --- /dev/null +++ b/scripts/echo360.js @@ -0,0 +1,441 @@ +/** @namespace Echo */ +H5P.VideoEchoVideo = (function ($) { + + let numInstances = 0; + /** + * EchoVideo video player for H5P. + * + * @class + * @param {Array} sources Video files to use + * @param {Object} options Settings for the player + * @param {Object} l10n Localization strings + */ + function EchoPlayer(sources, options, l10n) { + const self = this; + let player; + // Since all the methods of the Echo 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 = 30; + const id = `h5p-echo-${++numInstances}`; + const $wrapper = $('
'); + const $placeholder = $('
', { + id: id, + html: `
` + }).appendTo($wrapper); + /** + * Create a new player by embedding an iframe. + * + * @private + */ + const createEchoPlayer = async () => { + if (!$placeholder.is(':visible') || player !== undefined) { + return; + } + // 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 MIN_WIDTH = 200; + const width = Math.max($wrapper.width(), MIN_WIDTH); + player = $wrapper.html(``)[0].firstChild; + // Create a new player + registerEchoPlayerEventListeneners(player); + loadingFailedTimeout = setTimeout(() => { + failedLoading = true; + removeLoadingIndicator(); + $wrapper.html(`

${l10n.unknownError}

`); + $wrapper.css({ + width: null, + height: null + }); + self.trigger('resize'); + self.trigger('error', l10n.unknownError); + }, LOADING_TIMEOUT_IN_SECONDS * 1000); + }; + const removeLoadingIndicator = () => { + $placeholder.find('div.h5p-video-loading').remove(); + }; + + const resolutions = { + 921600: "720p", //"1280x720" + 2073600: "1080p", //"1920x1080" + 2211840: "2K", //"2048x1080" + 3686400: "1440p", // "2560x1440" + 8294400: "4K", // "3840x2160" + 33177600: "8K" // "7680x4320" + } + + const mapToResToName = quality => { + const resolution = resolutions[quality.width * quality.height] + if (resolution) return resolution + return `${quality.height}p` + } + + function compareQualities(a, b) { + return b.width * b.height - a.width * a.height + } + + const auto = { label: "auto", name: "auto" } + + const mapQualityLevels = qualityLevels => { + const qualities = qualityLevels.sort(compareQualities).map((quality, index) => { + return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) } + }) + return [...qualities, auto] + } + + /** + * Register event listeners on the given Echo player. + * + * @private + * @param {Echo.Player} player + */ + const registerEchoPlayerEventListeneners = (player) => { + let isFirstPlay, tracks; + player.resolveLoading = null; + player.loadingPromise = new Promise(function(resolve) { + player.resolveLoading = resolve; + }); + player.onload = async () => { + isFirstPlay = true; + clearTimeout(loadingFailedTimeout); + player.loadingPromise.then(function() { + self.trigger('ready'); + self.trigger('loaded'); + self.trigger('qualityChange', 'auto'); + self.trigger('resize'); + if (options.startAt) { + // Echo.Player doesn't have an option for setting start time upon + // instantiation, so we instead perform an initial seek here. + self.seek(options.startAt); + } + }); + }; + window.addEventListener('message', function(event) { + let message = ""; + try { + message = JSON.parse(event.data); + } catch (e) { + return; + } + if (message.context !== 'Echo360') { + return; + } + if (message.event == 'init') { + duration = message.data.duration; + currentTime = message.data.currentTime ?? 0; + qualities = mapQualityLevels(message.data.qualityLevels); + currentQuality = qualities.length - 1; + player.resolveLoading(); + self.trigger('resize'); + if (message.data.playing) { + self.trigger('stateChange', H5P.Video.PLAYING); + } else { + self.trigger('stateChange', H5P.Video.PAUSED); + } + } else if (message.event == 'timeline') { + duration = message.data.duration + currentTime = message.data.currentTime ?? 0 + self.trigger('resize'); + if (message.data.playing) { + self.trigger('stateChange', H5P.Video.PLAYING); + } else { + self.trigger('stateChange', H5P.Video.PAUSED); + } + } + }); + }; + try { + if (document.featurePolicy.allowsFeature('autoplay') === false) { + self.pressToPlay = true; + } + } + catch (err) {} + /** + * Appends the video player to the DOM. + * + * @public + * @param {jQuery} $container + */ + self.appendTo = ($container) => { + $container.addClass('h5p-echo').append($wrapper); + createEchoPlayer(); + }; + /** + * Get list of available qualities. + * + * @public + * @returns {Array} + */ + self.getQualities = () => { + return qualities; + }; + /** + * Get the current quality. + * + * @returns {String} Current quality identifier + */ + self.getQuality = () => { + return currentQuality; + }; + /** + * Set the playback quality. + * + * @public + * @param {String} quality + */ + self.setQuality = async (quality) => { + self.post('quality', quality); + currentQuality = quality; + self.trigger('qualityChange', currentQuality); + }; + /** + * Start the video. + * + * @public + */ + self.play = async () => { + if (!player) { + self.on('ready', self.play); + return; + } + self.post('play', 0) + + }; + /** + * Pause the video. + * + * @public + */ + self.pause = () => { + self.post('pause', 0) + }; + /** + * Seek video to given time. + * + * @public + * @param {Number} time + */ + self.seek = (time) => { + self.post('seek', time); + currentTime = time; + }; + /** + * Post a window message to the iframe. + * + * @param event + * @param data + */ + self.post = (event, data) => { + if (player) { + player.contentWindow.postMessage(JSON.stringify({event: event, data: data}), '*'); + } + }; + /** + * @public + * @returns {Number} Seconds elapsed since beginning of video + */ + self.getCurrentTime = () => { + return currentTime; + }; + /** + * @public + * @returns {Number} Video duration in seconds + */ + self.getDuration = () => { + if (duration > 0) { + return duration; + } + return; + }; + /** + * Get percentage of video that is buffered. + * + * @public + * @returns {Number} Between 0 and 100 + */ + self.getBuffered = () => { + return buffered; + }; + /** + * Mute the video. + * + * @public + */ + self.mute = () => { + self.post('mute', 0); + isMuted = true; + }; + /** + * Unmute the video. + * + * @public + */ + self.unMute = () => { + self.post('unmute', 0); + isMuted = false; + }; + /** + * Whether the video is muted. + * + * @public + * @returns {Boolean} True if the video is muted, false otherwise + */ + self.isMuted = () => { + return isMuted; + }; + /** + * Get the video player's current sound volume. + * + * @public + * @returns {Number} Between 0 and 100. + */ + self.getVolume = () => { + return volume; + }; + /** + * Set the video player's sound volume. + * + * @public + * @param {Number} level + */ + self.setVolume = (level) => { + self.post('volume', level); + volume = level; + }; + /** + * Get list of available playback rates. + * + * @public + * @returns {Array} Available playback rates + */ + self.getPlaybackRates = () => { + return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; + }; + /** + * Get the current playback rate. + * + * @public + * @returns {Number} e.g. 0.5, 1, 1.5 or 2 + */ + self.getPlaybackRate = () => { + return playbackRate; + }; + /** + * Set the current playback rate. + * + * @public + * @param {Number} rate Must be one of available rates from getPlaybackRates + */ + self.setPlaybackRate = async (rate) => { + self.post('playbackrate', rate) + playbackRate = rate; + self.trigger('playbackRateChange', rate); + }; + /** + * Set current captions track. + * + * @public + * @param {H5P.Video.LabelValue} track Captions to display + */ + self.setCaptionsTrack = (track) => { + if (!track) { + self.post('texttrack', null); + currentTextTrack = null; + } + self.post('texttrack', track.value) + currentTextTrack = track; + }; + /** + * Get current captions track. + * + * @public + * @returns {H5P.Video.LabelValue} + */ + self.getCaptionsTrack = () => { + return currentTextTrack; + }; + self.on('resize', () => { + if (failedLoading || !$wrapper.is(':visible')) { + return; + } + if (player === undefined) { + // Player isn't created yet. Try again. + createEchoPlayer(); + return; + } + // Use as much space as possible + $wrapper.css({ + width: '100%', + height: 'auto' + }); + const width = $wrapper[0].clientWidth; + const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio)); + // Validate height before setting + if (height > 0) { + // Set size + $wrapper.css({ + width: width + 'px', + height: height + 'px' + }); + } + }); + } + /** + * Check to see if we can play any of the given sources. + * + * @public + * @static + * @param {Array} sources + * @returns {Boolean} + */ + EchoPlayer.canPlay = (sources) => { + return getId(sources[0].path); + }; + /** + * Find id of video from given URL. + * + * @private + * @param {String} url + * @returns {String} Echo video identifier + */ + const getId = (url) => { + const matches = url.match(/^[^\/]+:\/\/(echo360[^\/]+)\/media\/([^\/]+)\/h5p.*$/i); + if (matches && matches.length === 3) { + return [matches[2], matches[2]]; + } + }; + /** + * Load the Echo Player SDK asynchronously. + * + * @private + * @returns {Promise} Echo Player SDK object + */ + const loadEchoPlayerSDK = async () => { + if (window.Echo) { + return await Promise.resolve(window.Echo); + } + return await new Promise((resolve, reject) => { + resolve(window.Echo); + }); + }; + return EchoPlayer; +})(H5P.jQuery); + +// Register video handler +H5P.videoHandlers = H5P.videoHandlers || []; +H5P.videoHandlers.push(H5P.VideoEchoVideo); From 58242c2347ec30e9aaa3827f995f0f36723313a4 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 09:59:21 +0800 Subject: [PATCH 26/47] Clean eslint warnings --- scripts/echo360.js | 272 ++++++++++++++++++++++----------------------- 1 file changed, 131 insertions(+), 141 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index bb63fc32..95066f79 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -1,7 +1,6 @@ /** @namespace Echo */ H5P.VideoEchoVideo = (function ($) { - let numInstances = 0; /** * EchoVideo video player for H5P. * @@ -11,11 +10,11 @@ H5P.VideoEchoVideo = (function ($) { * @param {Object} l10n Localization strings */ function EchoPlayer(sources, options, l10n) { - const self = this; - let player; // Since all the methods of the Echo 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. + var numInstances = 0; + let player = undefined; let buffered = 0; let currentQuality; let currentTextTrack; @@ -27,7 +26,7 @@ H5P.VideoEchoVideo = (function ($) { let qualities = []; let loadingFailedTimeout; let failedLoading = false; - let ratio = 9/16; + let ratio = 9 / 16; const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const $wrapper = $('
'); @@ -35,68 +34,39 @@ H5P.VideoEchoVideo = (function ($) { id: id, html: `
` }).appendTo($wrapper); - /** - * Create a new player by embedding an iframe. - * - * @private - */ - const createEchoPlayer = async () => { - if (!$placeholder.is(':visible') || player !== undefined) { - return; - } - // 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 MIN_WIDTH = 200; - const width = Math.max($wrapper.width(), MIN_WIDTH); - player = $wrapper.html(``)[0].firstChild; - // Create a new player - registerEchoPlayerEventListeneners(player); - loadingFailedTimeout = setTimeout(() => { - failedLoading = true; - removeLoadingIndicator(); - $wrapper.html(`

${l10n.unknownError}

`); - $wrapper.css({ - width: null, - height: null - }); - self.trigger('resize'); - self.trigger('error', l10n.unknownError); - }, LOADING_TIMEOUT_IN_SECONDS * 1000); - }; + + function compareQualities(a, b) { + return b.width * b.height - a.width * a.height; + } const removeLoadingIndicator = () => { $placeholder.find('div.h5p-video-loading').remove(); }; + const resolutions = { - 921600: "720p", //"1280x720" - 2073600: "1080p", //"1920x1080" - 2211840: "2K", //"2048x1080" - 3686400: "1440p", // "2560x1440" - 8294400: "4K", // "3840x2160" - 33177600: "8K" // "7680x4320" - } + 921600: '720p', //"1280x720" + 2073600: '1080p', //"1920x1080" + 2211840: '2K', //"2048x1080" + 3686400: '1440p', // "2560x1440" + 8294400: '4K', // "3840x2160" + 33177600: '8K' // "7680x4320" + }; - const mapToResToName = quality => { - const resolution = resolutions[quality.width * quality.height] - if (resolution) return resolution - return `${quality.height}p` - } + const auto = { label: 'auto', name: 'auto' }; - function compareQualities(a, b) { - return b.width * b.height - a.width * a.height - } + const mapToResToName = (quality) => { + const resolution = resolutions[quality.width * quality.height]; + if (resolution) return resolution; + return `${quality.height}p`; + }; - const auto = { label: "auto", name: "auto" } + const mapQualityLevels = (qualityLevels) => { + const qualities = qualityLevels.sort(compareQualities).map((quality) => { + return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) }; + }); + return [...qualities, auto]; + }; - const mapQualityLevels = qualityLevels => { - const qualities = qualityLevels.sort(compareQualities).map((quality, index) => { - return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) } - }) - return [...qualities, auto] - } /** * Register event listeners on the given Echo player. @@ -105,73 +75,107 @@ H5P.VideoEchoVideo = (function ($) { * @param {Echo.Player} player */ const registerEchoPlayerEventListeneners = (player) => { - let isFirstPlay, tracks; player.resolveLoading = null; - player.loadingPromise = new Promise(function(resolve) { + player.loadingPromise = new Promise(function (resolve) { player.resolveLoading = resolve; }); player.onload = async () => { - isFirstPlay = true; clearTimeout(loadingFailedTimeout); - player.loadingPromise.then(function() { - self.trigger('ready'); - self.trigger('loaded'); - self.trigger('qualityChange', 'auto'); - self.trigger('resize'); + player.loadingPromise.then(function () { + this.trigger('ready'); + this.trigger('loaded'); + this.trigger('qualityChange', 'auto'); + this.trigger('resize'); if (options.startAt) { // Echo.Player doesn't have an option for setting start time upon // instantiation, so we instead perform an initial seek here. - self.seek(options.startAt); + this.seek(options.startAt); } }); }; - window.addEventListener('message', function(event) { - let message = ""; + window.addEventListener('message', function (event) { + let message = ''; try { message = JSON.parse(event.data); - } catch (e) { + } + catch (e) { return; } if (message.context !== 'Echo360') { return; } - if (message.event == 'init') { + if (message.event === 'init') { duration = message.data.duration; currentTime = message.data.currentTime ?? 0; qualities = mapQualityLevels(message.data.qualityLevels); currentQuality = qualities.length - 1; player.resolveLoading(); - self.trigger('resize'); + this.trigger('resize'); if (message.data.playing) { - self.trigger('stateChange', H5P.Video.PLAYING); - } else { - self.trigger('stateChange', H5P.Video.PAUSED); + this.trigger('stateChange', H5P.Video.PLAYING); + } + else { + this.trigger('stateChange', H5P.Video.PAUSED); } - } else if (message.event == 'timeline') { - duration = message.data.duration - currentTime = message.data.currentTime ?? 0 - self.trigger('resize'); + } + else if (message.event === 'timeline') { + duration = message.data.duration; + currentTime = message.data.currentTime ?? 0; + this.trigger('resize'); if (message.data.playing) { - self.trigger('stateChange', H5P.Video.PLAYING); - } else { - self.trigger('stateChange', H5P.Video.PAUSED); + this.trigger('stateChange', H5P.Video.PLAYING); + } + else { + this.trigger('stateChange', H5P.Video.PAUSED); } } }); }; + /** + * Create a new player by embedding an iframe. + * + * @private + */ + const createEchoPlayer = async () => { + if (!$placeholder.is(':visible') || player !== undefined) { + return; + } + // 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; + player = $wrapper.html('')[0].firstChild; + // Create a new player + registerEchoPlayerEventListeneners(player); + loadingFailedTimeout = setTimeout(() => { + failedLoading = true; + removeLoadingIndicator(); + $wrapper.html(`

${l10n.unknownError}

`); + $wrapper.css({ + width: null, + height: null + }); + this.trigger('resize'); + this.trigger('error', l10n.unknownError); + }, LOADING_TIMEOUT_IN_SECONDS * 1000); + }; + try { if (document.featurePolicy.allowsFeature('autoplay') === false) { - self.pressToPlay = true; + this.pressToPlay = true; } } - catch (err) {} + catch (err) { + console.error(err); + } /** * Appends the video player to the DOM. * * @public * @param {jQuery} $container */ - self.appendTo = ($container) => { + this.appendTo = ($container) => { $container.addClass('h5p-echo').append($wrapper); createEchoPlayer(); }; @@ -181,7 +185,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Array} */ - self.getQualities = () => { + this.getQualities = () => { return qualities; }; /** @@ -189,7 +193,7 @@ H5P.VideoEchoVideo = (function ($) { * * @returns {String} Current quality identifier */ - self.getQuality = () => { + this.getQuality = () => { return currentQuality; }; /** @@ -198,22 +202,22 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {String} quality */ - self.setQuality = async (quality) => { - self.post('quality', quality); + this.setQuality = async (quality) => { + this.post('quality', quality); currentQuality = quality; - self.trigger('qualityChange', currentQuality); + this.trigger('qualityChange', currentQuality); }; /** * Start the video. * * @public */ - self.play = async () => { + this.play = async () => { if (!player) { - self.on('ready', self.play); + this.on('ready', this.play); return; } - self.post('play', 0) + this.post('play', 0); }; /** @@ -221,8 +225,8 @@ H5P.VideoEchoVideo = (function ($) { * * @public */ - self.pause = () => { - self.post('pause', 0) + this.pause = () => { + this.post('pause', 0); }; /** * Seek video to given time. @@ -230,8 +234,8 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {Number} time */ - self.seek = (time) => { - self.post('seek', time); + this.seek = (time) => { + this.post('seek', time); currentTime = time; }; /** @@ -240,23 +244,23 @@ H5P.VideoEchoVideo = (function ($) { * @param event * @param data */ - self.post = (event, data) => { + this.post = (event, data) => { if (player) { - player.contentWindow.postMessage(JSON.stringify({event: event, data: data}), '*'); + player.contentWindow.postMessage(JSON.stringify({ event: event, data: data }), '*'); } }; /** * @public * @returns {Number} Seconds elapsed since beginning of video */ - self.getCurrentTime = () => { + this.getCurrentTime = () => { return currentTime; }; /** * @public * @returns {Number} Video duration in seconds */ - self.getDuration = () => { + this.getDuration = () => { if (duration > 0) { return duration; } @@ -268,7 +272,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Number} Between 0 and 100 */ - self.getBuffered = () => { + this.getBuffered = () => { return buffered; }; /** @@ -276,8 +280,8 @@ H5P.VideoEchoVideo = (function ($) { * * @public */ - self.mute = () => { - self.post('mute', 0); + this.mute = () => { + this.post('mute', 0); isMuted = true; }; /** @@ -285,8 +289,8 @@ H5P.VideoEchoVideo = (function ($) { * * @public */ - self.unMute = () => { - self.post('unmute', 0); + this.unMute = () => { + this.post('unmute', 0); isMuted = false; }; /** @@ -295,7 +299,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Boolean} True if the video is muted, false otherwise */ - self.isMuted = () => { + this.isMuted = () => { return isMuted; }; /** @@ -304,7 +308,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Number} Between 0 and 100. */ - self.getVolume = () => { + this.getVolume = () => { return volume; }; /** @@ -313,8 +317,8 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {Number} level */ - self.setVolume = (level) => { - self.post('volume', level); + this.setVolume = (level) => { + this.post('volume', level); volume = level; }; /** @@ -323,7 +327,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Array} Available playback rates */ - self.getPlaybackRates = () => { + this.getPlaybackRates = () => { return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; }; /** @@ -332,7 +336,7 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {Number} e.g. 0.5, 1, 1.5 or 2 */ - self.getPlaybackRate = () => { + this.getPlaybackRate = () => { return playbackRate; }; /** @@ -341,10 +345,10 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {Number} rate Must be one of available rates from getPlaybackRates */ - self.setPlaybackRate = async (rate) => { - self.post('playbackrate', rate) + this.setPlaybackRate = async (rate) => { + this.post('playbackrate', rate); playbackRate = rate; - self.trigger('playbackRateChange', rate); + this.trigger('playbackRateChange', rate); }; /** * Set current captions track. @@ -352,12 +356,12 @@ H5P.VideoEchoVideo = (function ($) { * @public * @param {H5P.Video.LabelValue} track Captions to display */ - self.setCaptionsTrack = (track) => { + this.setCaptionsTrack = (track) => { if (!track) { - self.post('texttrack', null); + this.post('texttrack', null); currentTextTrack = null; } - self.post('texttrack', track.value) + this.post('texttrack', track.value); currentTextTrack = track; }; /** @@ -366,10 +370,10 @@ H5P.VideoEchoVideo = (function ($) { * @public * @returns {H5P.Video.LabelValue} */ - self.getCaptionsTrack = () => { + this.getCaptionsTrack = () => { return currentTextTrack; }; - self.on('resize', () => { + this.on('resize', () => { if (failedLoading || !$wrapper.is(':visible')) { return; } @@ -395,17 +399,6 @@ H5P.VideoEchoVideo = (function ($) { } }); } - /** - * Check to see if we can play any of the given sources. - * - * @public - * @static - * @param {Array} sources - * @returns {Boolean} - */ - EchoPlayer.canPlay = (sources) => { - return getId(sources[0].path); - }; /** * Find id of video from given URL. * @@ -414,24 +407,21 @@ H5P.VideoEchoVideo = (function ($) { * @returns {String} Echo video identifier */ const getId = (url) => { - const matches = url.match(/^[^\/]+:\/\/(echo360[^\/]+)\/media\/([^\/]+)\/h5p.*$/i); + const matches = url.match(/^[^/]+:\/\/(echo360[^/]+)\/media\/([^/]+)\/h5p.*$/i); if (matches && matches.length === 3) { return [matches[2], matches[2]]; } }; /** - * Load the Echo Player SDK asynchronously. + * Check to see if we can play any of the given sources. * - * @private - * @returns {Promise} Echo Player SDK object + * @public + * @static + * @param {Array} sources + * @returns {Boolean} */ - const loadEchoPlayerSDK = async () => { - if (window.Echo) { - return await Promise.resolve(window.Echo); - } - return await new Promise((resolve, reject) => { - resolve(window.Echo); - }); + EchoPlayer.canPlay = (sources) => { + return getId(sources[0].path); }; return EchoPlayer; })(H5P.jQuery); From fc3fa74793e5c2a03dbce678e52d00a050ce34a4 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 10:40:12 +0800 Subject: [PATCH 27/47] Remove (most) jQuery --- scripts/echo360.js | 50 ++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 95066f79..3a2f4b3b 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -1,5 +1,5 @@ /** @namespace Echo */ -H5P.VideoEchoVideo = (function ($) { +H5P.VideoEchoVideo = (function () { /** * EchoVideo video player for H5P. @@ -29,17 +29,17 @@ H5P.VideoEchoVideo = (function ($) { let ratio = 9 / 16; const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; - const $wrapper = $('
'); - const $placeholder = $('
', { - id: id, - html: `
` - }).appendTo($wrapper); + const wrapperElement = document.createElement('div'); + wrapperElement.setAttribute('id', id); + const placeholderElement = document.createElement('div'); + placeholderElement.innerHTML = `
`; + wrapperElement.append(placeholderElement); function compareQualities(a, b) { return b.width * b.height - a.width * a.height; } const removeLoadingIndicator = () => { - $placeholder.find('div.h5p-video-loading').remove(); + placeholderElement.replaceChildren(); }; @@ -131,13 +131,19 @@ H5P.VideoEchoVideo = (function ($) { } }); }; + + const isNodeVisible = (node) => { + let style = window.getComputedStyle(node); + return ((style.display !== 'none') && (style.visibility !== 'hidden')); + }; + /** * Create a new player by embedding an iframe. * * @private */ const createEchoPlayer = async () => { - if (!$placeholder.is(':visible') || player !== undefined) { + if (!isNodeVisible(placeholderElement) || player !== undefined) { return; } // Since the SDK is loaded asynchronously below, explicitly set player to @@ -145,17 +151,15 @@ H5P.VideoEchoVideo = (function ($) { // allows the guard statement above to be hit if this function is called // more than once. player = null; - player = $wrapper.html('')[0].firstChild; + wrapperElement.innerHTML = ''; + player = wrapperElement.firstChild; // Create a new player registerEchoPlayerEventListeneners(player); loadingFailedTimeout = setTimeout(() => { failedLoading = true; removeLoadingIndicator(); - $wrapper.html(`

${l10n.unknownError}

`); - $wrapper.css({ - width: null, - height: null - }); + wrapperElement.innerHTML = `

${l10n.unknownError}

`; + wrapperElement.style.cssText = 'width: null; height: null;'; this.trigger('resize'); this.trigger('error', l10n.unknownError); }, LOADING_TIMEOUT_IN_SECONDS * 1000); @@ -176,7 +180,7 @@ H5P.VideoEchoVideo = (function ($) { * @param {jQuery} $container */ this.appendTo = ($container) => { - $container.addClass('h5p-echo').append($wrapper); + $container.addClass('h5p-echo').append(wrapperElement); createEchoPlayer(); }; /** @@ -374,7 +378,7 @@ H5P.VideoEchoVideo = (function ($) { return currentTextTrack; }; this.on('resize', () => { - if (failedLoading || !$wrapper.is(':visible')) { + if (failedLoading || !isNodeVisible(wrapperElement)) { return; } if (player === undefined) { @@ -383,19 +387,13 @@ H5P.VideoEchoVideo = (function ($) { return; } // Use as much space as possible - $wrapper.css({ - width: '100%', - height: 'auto' - }); - const width = $wrapper[0].clientWidth; - const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio)); + wrapperElement.style.cssText = 'width: 100%; height: auto;'; + const width = wrapperElement.clientWidth; + const height = options.fit ? wrapperElement.clientHeight : (width * (ratio)); // Validate height before setting if (height > 0) { // Set size - $wrapper.css({ - width: width + 'px', - height: height + 'px' - }); + wrapperElement.style.cssText = 'width: ' + width + 'px; height: ' + height + 'px;'; } }); } From 0d5e8432c53cf0b8d23eb24943a8fd8731e7cbcf Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 10:48:19 +0800 Subject: [PATCH 28/47] Consistent arrow functions --- scripts/echo360.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 3a2f4b3b..1a2ef3d6 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -27,6 +27,7 @@ H5P.VideoEchoVideo = (function () { let loadingFailedTimeout; let failedLoading = false; let ratio = 9 / 16; + const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const wrapperElement = document.createElement('div'); @@ -35,14 +36,13 @@ H5P.VideoEchoVideo = (function () { placeholderElement.innerHTML = `
`; wrapperElement.append(placeholderElement); - function compareQualities(a, b) { + const compareQualities = (a, b) => { return b.width * b.height - a.width * a.height; } const removeLoadingIndicator = () => { placeholderElement.replaceChildren(); }; - const resolutions = { 921600: '720p', //"1280x720" 2073600: '1080p', //"1920x1080" @@ -76,12 +76,12 @@ H5P.VideoEchoVideo = (function () { */ const registerEchoPlayerEventListeneners = (player) => { player.resolveLoading = null; - player.loadingPromise = new Promise(function (resolve) { + player.loadingPromise = new Promise((resolve) => { player.resolveLoading = resolve; }); player.onload = async () => { clearTimeout(loadingFailedTimeout); - player.loadingPromise.then(function () { + player.loadingPromise.then(() => { this.trigger('ready'); this.trigger('loaded'); this.trigger('qualityChange', 'auto'); @@ -91,6 +91,7 @@ H5P.VideoEchoVideo = (function () { // instantiation, so we instead perform an initial seek here. this.seek(options.startAt); } + return true; }); }; window.addEventListener('message', function (event) { From d12e05a913a0b36fa846ac976917c1cd628ed472 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 10:57:09 +0800 Subject: [PATCH 29/47] Add isLoaded function --- scripts/echo360.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 1a2ef3d6..a21b24ad 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -10,9 +10,7 @@ H5P.VideoEchoVideo = (function () { * @param {Object} l10n Localization strings */ function EchoPlayer(sources, options, l10n) { - // Since all the methods of the Echo 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. + // State variables for the Player. var numInstances = 0; let player = undefined; let buffered = 0; @@ -21,6 +19,7 @@ H5P.VideoEchoVideo = (function () { let currentTime = 0; let duration = 0; let isMuted = 0; + let loadingComplete = false; let volume = 0; let playbackRate = 1; let qualities = []; @@ -28,6 +27,7 @@ H5P.VideoEchoVideo = (function () { let failedLoading = false; let ratio = 9 / 16; + // Player specific immutable variables. const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const wrapperElement = document.createElement('div'); @@ -36,13 +36,6 @@ H5P.VideoEchoVideo = (function () { placeholderElement.innerHTML = `
`; wrapperElement.append(placeholderElement); - const compareQualities = (a, b) => { - return b.width * b.height - a.width * a.height; - } - const removeLoadingIndicator = () => { - placeholderElement.replaceChildren(); - }; - const resolutions = { 921600: '720p', //"1280x720" 2073600: '1080p', //"1920x1080" @@ -54,6 +47,16 @@ H5P.VideoEchoVideo = (function () { const auto = { label: 'auto', name: 'auto' }; + /** + * + */ + const compareQualities = (a, b) => { + return b.width * b.height - a.width * a.height; + }; + const removeLoadingIndicator = () => { + placeholderElement.replaceChildren(); + }; + const mapToResToName = (quality) => { const resolution = resolutions[quality.width * quality.height]; if (resolution) return resolution; @@ -84,6 +87,7 @@ H5P.VideoEchoVideo = (function () { player.loadingPromise.then(() => { this.trigger('ready'); this.trigger('loaded'); + this.loadingComplete = true; this.trigger('qualityChange', 'auto'); this.trigger('resize'); if (options.startAt) { @@ -184,6 +188,11 @@ H5P.VideoEchoVideo = (function () { $container.addClass('h5p-echo').append(wrapperElement); createEchoPlayer(); }; + + this.isLoaded = () => { + return loadingComplete; + }; + /** * Get list of available qualities. * From 852212af4d821e9e4b0b0544b25405561bec94cb Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 21 Feb 2024 13:27:00 +0800 Subject: [PATCH 30/47] Iframe permission policy delegation --- scripts/echo360.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index a21b24ad..3cec5d91 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -18,9 +18,9 @@ H5P.VideoEchoVideo = (function () { let currentTextTrack; let currentTime = 0; let duration = 0; - let isMuted = 0; + let isMuted = false; let loadingComplete = false; - let volume = 0; + let volume = 1; let playbackRate = 1; let qualities = []; let loadingFailedTimeout; @@ -98,7 +98,7 @@ H5P.VideoEchoVideo = (function () { return true; }); }; - window.addEventListener('message', function (event) { + window.addEventListener('message', (event) => { let message = ''; try { message = JSON.parse(event.data); @@ -156,7 +156,7 @@ H5P.VideoEchoVideo = (function () { // allows the guard statement above to be hit if this function is called // more than once. player = null; - wrapperElement.innerHTML = ''; + wrapperElement.innerHTML = ''; player = wrapperElement.firstChild; // Create a new player registerEchoPlayerEventListeneners(player); @@ -226,7 +226,7 @@ H5P.VideoEchoVideo = (function () { * * @public */ - this.play = async () => { + this.play = () => { if (!player) { this.on('ready', this.play); return; From 2333668eaf18ff43f36b2d0fba9e09aac6941a62 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 22 Feb 2024 11:01:23 +0800 Subject: [PATCH 31/47] JSDoc improvements and coding style --- scripts/echo360.js | 89 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 13 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 3cec5d91..a24c0f06 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -31,9 +31,10 @@ H5P.VideoEchoVideo = (function () { const LOADING_TIMEOUT_IN_SECONDS = 30; const id = `h5p-echo-${++numInstances}`; const wrapperElement = document.createElement('div'); - wrapperElement.setAttribute('id', id); const placeholderElement = document.createElement('div'); - placeholderElement.innerHTML = `
`; + + wrapperElement.setAttribute('id', id); + placeholderElement.innerHTML = `
`; wrapperElement.append(placeholderElement); const resolutions = { @@ -48,21 +49,43 @@ H5P.VideoEchoVideo = (function () { const auto = { label: 'auto', name: 'auto' }; /** - * + * Determine which quality is greater by counting the pixels. + * @private + * @param {Object} a - object with width and height properties + * @param {Object} b - object with width and height properties + * @returns {Number} positive if second parameter has more pixels */ const compareQualities = (a, b) => { return b.width * b.height - a.width * a.height; }; + + /** + * Remove all elements from the placeholder dom element. + * + * @private + */ const removeLoadingIndicator = () => { placeholderElement.replaceChildren(); }; + /** + * Generate a descriptive name for a resolution object with width and height. + * @private + * @param {Object} quality - object with width and height properties + * @returns {String} either a predefined name for the resolution or something like 1080p + */ const mapToResToName = (quality) => { const resolution = resolutions[quality.width * quality.height]; if (resolution) return resolution; return `${quality.height}p`; }; + /** + * Generate an array of objects for use in a dropdown from the list of resolutions. + * @private + * @param {Array} qualityLevels - list of objects with width and height properties + * @returns {Array} list of objects with label and name properties + */ const mapQualityLevels = (qualityLevels) => { const qualities = qualityLevels.sort(compareQualities).map((quality) => { return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) }; @@ -70,12 +93,11 @@ H5P.VideoEchoVideo = (function () { return [...qualities, auto]; }; - /** * Register event listeners on the given Echo player. * * @private - * @param {Echo.Player} player + * @param {HTMLElement} player */ const registerEchoPlayerEventListeneners = (player) => { player.resolveLoading = null; @@ -95,6 +117,9 @@ H5P.VideoEchoVideo = (function () { // instantiation, so we instead perform an initial seek here. this.seek(options.startAt); } + if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { + this.play(); + } return true; }); }; @@ -133,10 +158,21 @@ H5P.VideoEchoVideo = (function () { else { this.trigger('stateChange', H5P.Video.PAUSED); } + if (currentTime >== (duration - 1) && options.loop) { + this.seek(0); + this.play(); + } } }); }; + /** + * Determine if the element is visible by computing the styles. + * + * @private + * @param {HTMLElement} node - the element to check. + * @returns {Boolean} true if it is visible. + */ const isNodeVisible = (node) => { let style = window.getComputedStyle(node); return ((style.display !== 'none') && (style.visibility !== 'hidden')); @@ -146,6 +182,7 @@ H5P.VideoEchoVideo = (function () { * Create a new player by embedding an iframe. * * @private + * @returns {Promise} */ const createEchoPlayer = async () => { if (!isNodeVisible(placeholderElement) || player !== undefined) { @@ -170,14 +207,6 @@ H5P.VideoEchoVideo = (function () { }, LOADING_TIMEOUT_IN_SECONDS * 1000); }; - try { - if (document.featurePolicy.allowsFeature('autoplay') === false) { - this.pressToPlay = true; - } - } - catch (err) { - console.error(err); - } /** * Appends the video player to the DOM. * @@ -189,6 +218,12 @@ H5P.VideoEchoVideo = (function () { createEchoPlayer(); }; + /** + * Determine if the video has loaded. + * + * @public + * @returns {Boolean} + */ this.isLoaded = () => { return loadingComplete; }; @@ -202,14 +237,17 @@ H5P.VideoEchoVideo = (function () { this.getQualities = () => { return qualities; }; + /** * Get the current quality. * + * @public * @returns {String} Current quality identifier */ this.getQuality = () => { return currentQuality; }; + /** * Set the playback quality. * @@ -221,6 +259,7 @@ H5P.VideoEchoVideo = (function () { currentQuality = quality; this.trigger('qualityChange', currentQuality); }; + /** * Start the video. * @@ -234,6 +273,7 @@ H5P.VideoEchoVideo = (function () { this.post('play', 0); }; + /** * Pause the video. * @@ -242,6 +282,7 @@ H5P.VideoEchoVideo = (function () { this.pause = () => { this.post('pause', 0); }; + /** * Seek video to given time. * @@ -252,9 +293,11 @@ H5P.VideoEchoVideo = (function () { this.post('seek', time); currentTime = time; }; + /** * Post a window message to the iframe. * + * @public * @param event * @param data */ @@ -263,14 +306,20 @@ H5P.VideoEchoVideo = (function () { player.contentWindow.postMessage(JSON.stringify({ event: event, data: data }), '*'); } }; + /** + * Return the current play position. + * * @public * @returns {Number} Seconds elapsed since beginning of video */ this.getCurrentTime = () => { return currentTime; }; + /** + * Return the video duration. + * * @public * @returns {Number} Video duration in seconds */ @@ -280,6 +329,7 @@ H5P.VideoEchoVideo = (function () { } return; }; + /** * Get percentage of video that is buffered. * @@ -289,6 +339,7 @@ H5P.VideoEchoVideo = (function () { this.getBuffered = () => { return buffered; }; + /** * Mute the video. * @@ -298,6 +349,7 @@ H5P.VideoEchoVideo = (function () { this.post('mute', 0); isMuted = true; }; + /** * Unmute the video. * @@ -307,6 +359,7 @@ H5P.VideoEchoVideo = (function () { this.post('unmute', 0); isMuted = false; }; + /** * Whether the video is muted. * @@ -316,6 +369,7 @@ H5P.VideoEchoVideo = (function () { this.isMuted = () => { return isMuted; }; + /** * Get the video player's current sound volume. * @@ -325,6 +379,7 @@ H5P.VideoEchoVideo = (function () { this.getVolume = () => { return volume; }; + /** * Set the video player's sound volume. * @@ -335,6 +390,7 @@ H5P.VideoEchoVideo = (function () { this.post('volume', level); volume = level; }; + /** * Get list of available playback rates. * @@ -344,6 +400,7 @@ H5P.VideoEchoVideo = (function () { this.getPlaybackRates = () => { return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; }; + /** * Get the current playback rate. * @@ -353,6 +410,7 @@ H5P.VideoEchoVideo = (function () { this.getPlaybackRate = () => { return playbackRate; }; + /** * Set the current playback rate. * @@ -364,6 +422,7 @@ H5P.VideoEchoVideo = (function () { playbackRate = rate; this.trigger('playbackRateChange', rate); }; + /** * Set current captions track. * @@ -378,6 +437,7 @@ H5P.VideoEchoVideo = (function () { this.post('texttrack', track.value); currentTextTrack = track; }; + /** * Get current captions track. * @@ -387,6 +447,7 @@ H5P.VideoEchoVideo = (function () { this.getCaptionsTrack = () => { return currentTextTrack; }; + this.on('resize', () => { if (failedLoading || !isNodeVisible(wrapperElement)) { return; @@ -407,6 +468,7 @@ H5P.VideoEchoVideo = (function () { } }); } + /** * Find id of video from given URL. * @@ -420,6 +482,7 @@ H5P.VideoEchoVideo = (function () { return [matches[2], matches[2]]; } }; + /** * Check to see if we can play any of the given sources. * From 029ca0953f7cf39a4e4ce3c877bc8ee0e32973c5 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 22 Feb 2024 11:07:39 +0800 Subject: [PATCH 32/47] Duration is a number or undefined --- scripts/echo360.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index a24c0f06..3a482c8b 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -321,13 +321,13 @@ H5P.VideoEchoVideo = (function () { * Return the video duration. * * @public - * @returns {Number} Video duration in seconds + * @returns {?Number} Video duration in seconds */ this.getDuration = () => { if (duration > 0) { return duration; } - return; + return null; }; /** From 05ad5696f11359f290e38b9610b55951917a358e Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 22 Feb 2024 13:57:28 +0800 Subject: [PATCH 33/47] Comparison operator --- scripts/echo360.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 3a482c8b..fe2f8d78 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -158,7 +158,7 @@ H5P.VideoEchoVideo = (function () { else { this.trigger('stateChange', H5P.Video.PAUSED); } - if (currentTime >== (duration - 1) && options.loop) { + if (currentTime >= (duration - 1) && options.loop) { this.seek(0); this.play(); } @@ -193,6 +193,16 @@ H5P.VideoEchoVideo = (function () { // allows the guard statement above to be hit if this function is called // more than once. player = null; + let queryString = '?'; + if (options.controls) { + queryString += 'controls=true&'; + } + if (options.disableFullscreen) { + queryString += 'disableFullscreen=true&'; + } + if (options.deactivateSound) { + queryString += 'deactivateSound=true&'; + } wrapperElement.innerHTML = ''; player = wrapperElement.firstChild; // Create a new player From f4c57fed70eaadd2d8ab5d6cea798d7b8ec50704 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 27 Feb 2024 09:04:31 +0800 Subject: [PATCH 34/47] Cancel resizing on timeline updates --- scripts/echo360.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index fe2f8d78..52570727 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -151,7 +151,6 @@ H5P.VideoEchoVideo = (function () { else if (message.event === 'timeline') { duration = message.data.duration; currentTime = message.data.currentTime ?? 0; - this.trigger('resize'); if (message.data.playing) { this.trigger('stateChange', H5P.Video.PLAYING); } From 6547fd3381799fbfde3713124d0aca50e2a57927 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 27 Feb 2024 10:08:35 +0800 Subject: [PATCH 35/47] Consistent playback rate handling --- scripts/echo360.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 52570727..8768b6bf 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -427,7 +427,8 @@ H5P.VideoEchoVideo = (function () { * @param {Number} rate Must be one of available rates from getPlaybackRates */ this.setPlaybackRate = async (rate) => { - this.post('playbackrate', rate); + const echoRate = parseFloat(rate); + this.post('playbackrate', echoRate); playbackRate = rate; this.trigger('playbackRateChange', rate); }; From 11195fe1be826236df42440ec12c5defcc3ac3a9 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 29 Feb 2024 08:14:39 +0800 Subject: [PATCH 36/47] Improve start time handling --- scripts/echo360.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 8768b6bf..8116e09b 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -112,11 +112,6 @@ H5P.VideoEchoVideo = (function () { this.loadingComplete = true; this.trigger('qualityChange', 'auto'); this.trigger('resize'); - if (options.startAt) { - // Echo.Player doesn't have an option for setting start time upon - // instantiation, so we instead perform an initial seek here. - this.seek(options.startAt); - } if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { this.play(); } @@ -202,6 +197,10 @@ H5P.VideoEchoVideo = (function () { if (options.deactivateSound) { queryString += 'deactivateSound=true&'; } + if (options.startAt) { + // Implicit conversion to millis + queryString += 'startTimeMillis=' + startAt + '000&'; + } wrapperElement.innerHTML = ''; player = wrapperElement.firstChild; // Create a new player From 97169949c6b093c19a61450ddc6811692cd3c1b7 Mon Sep 17 00:00:00 2001 From: MHod-101 Date: Thu, 7 Mar 2024 13:44:31 +1100 Subject: [PATCH 37/47] PLT-2090: Update quality option mapping Use updated quality options --- scripts/echo360.js | 50 ++++++++-------------------------------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 8116e09b..cb13b26c 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -37,28 +37,6 @@ H5P.VideoEchoVideo = (function () { placeholderElement.innerHTML = `
`; wrapperElement.append(placeholderElement); - const resolutions = { - 921600: '720p', //"1280x720" - 2073600: '1080p', //"1920x1080" - 2211840: '2K', //"2048x1080" - 3686400: '1440p', // "2560x1440" - 8294400: '4K', // "3840x2160" - 33177600: '8K' // "7680x4320" - }; - - const auto = { label: 'auto', name: 'auto' }; - - /** - * Determine which quality is greater by counting the pixels. - * @private - * @param {Object} a - object with width and height properties - * @param {Object} b - object with width and height properties - * @returns {Number} positive if second parameter has more pixels - */ - const compareQualities = (a, b) => { - return b.width * b.height - a.width * a.height; - }; - /** * Remove all elements from the placeholder dom element. * @@ -68,29 +46,17 @@ H5P.VideoEchoVideo = (function () { placeholderElement.replaceChildren(); }; - /** - * Generate a descriptive name for a resolution object with width and height. - * @private - * @param {Object} quality - object with width and height properties - * @returns {String} either a predefined name for the resolution or something like 1080p - */ - const mapToResToName = (quality) => { - const resolution = resolutions[quality.width * quality.height]; - if (resolution) return resolution; - return `${quality.height}p`; - }; - /** * Generate an array of objects for use in a dropdown from the list of resolutions. * @private - * @param {Array} qualityLevels - list of objects with width and height properties + * @param {Array} qualityLevels - list of objects with supported qualities for the media * @returns {Array} list of objects with label and name properties */ const mapQualityLevels = (qualityLevels) => { - const qualities = qualityLevels.sort(compareQualities).map((quality) => { - return { label: mapToResToName(quality), name: (quality.width + 'x' + quality.height) }; - }); - return [...qualities, auto]; + const qualities = qualityLevels.map((quality) => { + return { label: quality.label.toLowerCase(), name: quality.value } + }) + return qualities; }; /** @@ -110,7 +76,6 @@ H5P.VideoEchoVideo = (function () { this.trigger('ready'); this.trigger('loaded'); this.loadingComplete = true; - this.trigger('qualityChange', 'auto'); this.trigger('resize'); if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { this.play(); @@ -132,9 +97,10 @@ H5P.VideoEchoVideo = (function () { if (message.event === 'init') { duration = message.data.duration; currentTime = message.data.currentTime ?? 0; - qualities = mapQualityLevels(message.data.qualityLevels); - currentQuality = qualities.length - 1; + qualities = mapQualityLevels(message.data.qualityOptions); + currentQuality = qualities[0].name; player.resolveLoading(); + this.trigger('qualityChange', currentQuality); this.trigger('resize'); if (message.data.playing) { this.trigger('stateChange', H5P.Video.PLAYING); From ee55eedb6d206f81c1b12d795fd20aa7e4f093ad Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 7 Mar 2024 10:51:52 +0800 Subject: [PATCH 38/47] Fix startAt option --- scripts/echo360.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index cb13b26c..c6c62a93 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -165,7 +165,7 @@ H5P.VideoEchoVideo = (function () { } if (options.startAt) { // Implicit conversion to millis - queryString += 'startTimeMillis=' + startAt + '000&'; + queryString += 'startTimeMillis=' + options.startAt + '000&'; } wrapperElement.innerHTML = ''; player = wrapperElement.firstChild; From 1ecaaffe6b865899ed05ab3ec87879d0780992fa Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Thu, 7 Mar 2024 11:39:38 +0800 Subject: [PATCH 39/47] Load player controls before the video starts --- scripts/echo360.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index c6c62a93..dd3200e1 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -79,6 +79,9 @@ H5P.VideoEchoVideo = (function () { this.trigger('resize'); if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { this.play(); + this.trigger('stateChange', H5P.Video.PLAYING); + } else { + this.trigger('stateChange', H5P.Video.PAUSED); } return true; }); @@ -245,7 +248,6 @@ H5P.VideoEchoVideo = (function () { return; } this.post('play', 0); - }; /** From 0159b5eec1ca04faaca02573104fe7f915f262d5 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:13:39 +0100 Subject: [PATCH 40/47] Fix numInstances numInstances is supposed to reflect the number of instances of the handler for a unique element id, so it cannot be an instance property --- scripts/echo360.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index dd3200e1..ceb8f283 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -1,5 +1,6 @@ /** @namespace Echo */ -H5P.VideoEchoVideo = (function () { + + let numInstances = 0; /** * EchoVideo video player for H5P. @@ -11,7 +12,6 @@ H5P.VideoEchoVideo = (function () { */ function EchoPlayer(sources, options, l10n) { // State variables for the Player. - var numInstances = 0; let player = undefined; let buffered = 0; let currentQuality; From 4b747c04567ffea6af813578f89c4e3d1a9a3f84 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:16:50 +0100 Subject: [PATCH 41/47] Stop triggering "paused" before video has been "played" Would cause trouble downstream, e.g. in Interactive Video --- scripts/echo360.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index ceb8f283..a9de5dd2 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -80,9 +80,8 @@ if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { this.play(); this.trigger('stateChange', H5P.Video.PLAYING); - } else { - this.trigger('stateChange', H5P.Video.PAUSED); } + return true; }); }; @@ -108,9 +107,6 @@ if (message.data.playing) { this.trigger('stateChange', H5P.Video.PLAYING); } - else { - this.trigger('stateChange', H5P.Video.PAUSED); - } } else if (message.event === 'timeline') { duration = message.data.duration; From 27e310eb8621415f2a28bf9e60dcf37de5536be6 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:19:36 +0100 Subject: [PATCH 42/47] Fix updating duration When the message event is "timeline", message.data.duration is `undefined`, the duration value which is used by `getDuration()`, too, would be overwritten and be `undefined` (bug there then), and here looping would not work. --- scripts/echo360.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index a9de5dd2..abdd37ae 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -109,7 +109,7 @@ } } else if (message.event === 'timeline') { - duration = message.data.duration; + duration = message.data.duration ?? this.getDuration(); currentTime = message.data.currentTime ?? 0; if (message.data.playing) { this.trigger('stateChange', H5P.Video.PLAYING); From bd79cb24d89fdef477c9dc214c2cd4f1110b78f6 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:20:24 +0100 Subject: [PATCH 43/47] Pass query string with iframe source --- scripts/echo360.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index abdd37ae..4c91ce3a 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -166,7 +166,8 @@ // Implicit conversion to millis queryString += 'startTimeMillis=' + options.startAt + '000&'; } - wrapperElement.innerHTML = ''; + + wrapperElement.innerHTML = ``; player = wrapperElement.firstChild; // Create a new player registerEchoPlayerEventListeneners(player); From 543a1357626207ba50a5c8fcc7abc40deb08f0f7 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:24:21 +0100 Subject: [PATCH 44/47] Make eslint happy --- scripts/echo360.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 4c91ce3a..2b3e007a 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -1,4 +1,5 @@ /** @namespace Echo */ +H5P.VideoEchoVideo = (() => { let numInstances = 0; @@ -54,8 +55,8 @@ */ const mapQualityLevels = (qualityLevels) => { const qualities = qualityLevels.map((quality) => { - return { label: quality.label.toLowerCase(), name: quality.value } - }) + return { label: quality.label.toLowerCase(), name: quality.value }; + }); return qualities; }; From 89ffe8153ec757ed93b94eb31b955cb0a98d59e7 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 15:40:22 +0100 Subject: [PATCH 45/47] Make code robust for document.featurePolicy document.featurePolicy is experimental (no support in Firefox/Safari). Prevent potential crash. --- scripts/echo360.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 2b3e007a..43cd35de 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -78,7 +78,11 @@ H5P.VideoEchoVideo = (() => { this.trigger('loaded'); this.loadingComplete = true; this.trigger('resize'); - if (options.autoplay && document.featurePolicy.allowsFeature('autoplay')) { + + if ( + options.autoplay && + document.featurePolicy?.allowsFeature('autoplay') + ) { this.play(); this.trigger('stateChange', H5P.Video.PLAYING); } From 10d0b23aa369c80ca8d62e9a82bb2ad5eba50ff3 Mon Sep 17 00:00:00 2001 From: Oliver Tacke Date: Mon, 25 Mar 2024 16:30:38 +0100 Subject: [PATCH 46/47] Fix startAt handling The parameter is passed as a float in seconds --- scripts/echo360.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index 43cd35de..bbe99f82 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -169,7 +169,7 @@ H5P.VideoEchoVideo = (() => { } if (options.startAt) { // Implicit conversion to millis - queryString += 'startTimeMillis=' + options.startAt + '000&'; + queryString += `startTimeMillis=${options.startAt * 1000}&`; } wrapperElement.innerHTML = ``; From df8e267215432ad949bc63b131e0f293da586711 Mon Sep 17 00:00:00 2001 From: devland Date: Thu, 2 May 2024 14:04:32 +0200 Subject: [PATCH 47/47] HFP-3869 fix resizing in CP editor --- scripts/echo360.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/echo360.js b/scripts/echo360.js index bbe99f82..ce3c5edf 100644 --- a/scripts/echo360.js +++ b/scripts/echo360.js @@ -437,9 +437,10 @@ H5P.VideoEchoVideo = (() => { return; } // Use as much space as possible - wrapperElement.style.cssText = 'width: 100%; height: auto;'; + wrapperElement.style.cssText = 'width: 100%; height: 100%;'; const width = wrapperElement.clientWidth; const height = options.fit ? wrapperElement.clientHeight : (width * (ratio)); + console.log(`echo360 video height ${height}`); // Validate height before setting if (height > 0) { // Set size