- Move Video Block JS files from xmodule/js/src/video/ to xmodule/assets/video/public/js/ - Update JavaScript files from RequireJS to ES6 import/export - test: Enable and fix Karma Js tests for Video XBlock (#37351) --------- Co-authored-by: salmannawaz <salman.nawaz@arbisoft.com>
846 lines
27 KiB
JavaScript
846 lines
27 KiB
JavaScript
/* eslint-disable no-console, no-param-reassign */
|
|
/**
|
|
* @file Initialize module works with the JSON config, and sets up various
|
|
* settings, parameters, variables. After all setup actions are performed, it
|
|
* invokes the video player to play the specified video. This module must be
|
|
* invoked first. It provides several functions which do not fit in with other
|
|
* modules.
|
|
*
|
|
* @external VideoPlayer
|
|
*
|
|
* @module Initialize
|
|
*/
|
|
|
|
import VideoPlayer from './03_video_player.js';
|
|
import i18n from './00_i18n.js';
|
|
import _ from 'underscore';
|
|
import moment from 'moment';
|
|
|
|
/**
|
|
* @function
|
|
*
|
|
* Initialize module exports this function.
|
|
*
|
|
* @param {object} state The object containg the state of the video player.
|
|
* All other modules, their parameters, public variables, etc. are
|
|
* available via this object.
|
|
* @param {DOM element} element Container of the entire Video DOM element.
|
|
*/
|
|
let Initialize = function(state, element) {
|
|
_makeFunctionsPublic(state);
|
|
|
|
state.initialize(element)
|
|
.done(function() {
|
|
if (state.isYoutubeType()) {
|
|
state.parseSpeed();
|
|
}
|
|
// On iPhones and iPods native controls are used.
|
|
if (/iP(hone|od)/i.test(state.isTouch[0])) {
|
|
_hideWaitPlaceholder(state);
|
|
state.el.trigger('initialize', arguments);
|
|
|
|
return false;
|
|
}
|
|
|
|
_initializeModules(state, i18n)
|
|
.done(function() {
|
|
// On iPad ready state occurs just after start playing.
|
|
// We hide controls before video starts playing.
|
|
if (/iPad|Android/i.test(state.isTouch[0])) {
|
|
state.el.on('play', _.once(function() {
|
|
state.trigger('videoControl.show', null);
|
|
}));
|
|
} else {
|
|
// On PC show controls immediately.
|
|
state.trigger('videoControl.show', null);
|
|
}
|
|
|
|
_hideWaitPlaceholder(state);
|
|
state.el.trigger('initialize', arguments);
|
|
});
|
|
});
|
|
};
|
|
|
|
/* eslint-disable no-use-before-define */
|
|
let methodsDict = {
|
|
bindTo: bindTo,
|
|
fetchMetadata: fetchMetadata,
|
|
getCurrentLanguage: getCurrentLanguage,
|
|
getDuration: getDuration,
|
|
getPlayerMode: getPlayerMode,
|
|
getVideoMetadata: getVideoMetadata,
|
|
initialize: initialize,
|
|
isHtml5Mode: isHtml5Mode,
|
|
isFlashMode: isFlashMode,
|
|
isYoutubeType: isYoutubeType,
|
|
parseSpeed: parseSpeed,
|
|
parseYoutubeStreams: parseYoutubeStreams,
|
|
setPlayerMode: setPlayerMode,
|
|
setSpeed: setSpeed,
|
|
setAutoAdvance: setAutoAdvance,
|
|
speedToString: speedToString,
|
|
trigger: trigger,
|
|
youtubeId: youtubeId,
|
|
loadHtmlPlayer: loadHtmlPlayer,
|
|
loadYoutubePlayer: loadYoutubePlayer,
|
|
loadYouTubeIFrameAPI: loadYouTubeIFrameAPI
|
|
};
|
|
/* eslint-enable no-use-before-define */
|
|
|
|
let _youtubeApiDeferred = null;
|
|
let _oldOnYouTubeIframeAPIReady;
|
|
|
|
Initialize.prototype = methodsDict;
|
|
|
|
export default Initialize;
|
|
|
|
// ***************************************************************
|
|
// Private functions start here. Private functions start with underscore.
|
|
// ***************************************************************
|
|
|
|
/**
|
|
* @function _makeFunctionsPublic
|
|
*
|
|
* Functions which will be accessible via 'state' object. When called,
|
|
* these functions will get the 'state'
|
|
* object as a context.
|
|
*
|
|
* @param {object} state The object containg the state (properties,
|
|
* methods, modules) of the Video player.
|
|
*/
|
|
function _makeFunctionsPublic(state) {
|
|
bindTo(methodsDict, state, state);
|
|
}
|
|
|
|
// function _renderElements(state)
|
|
//
|
|
// Create any necessary DOM elements, attach them, and set their
|
|
// initial configuration. Also make the created DOM elements available
|
|
// via the 'state' object. Much easier to work this way - you don't
|
|
// have to do repeated jQuery element selects.
|
|
function _renderElements(state) {
|
|
// Launch embedding of actual video content, or set it up so that it
|
|
// will be done as soon as the appropriate video player (YouTube or
|
|
// stand-alone HTML5) is loaded, and can handle embedding.
|
|
//
|
|
// Note that the loading of stand alone HTML5 player API is handled by
|
|
// Require JS. At the time when we reach this code, the stand alone
|
|
// HTML5 player is already loaded, so no further testing in that case
|
|
// is required.
|
|
let video;
|
|
let onYTApiReady;
|
|
let setupOnYouTubeIframeAPIReady;
|
|
|
|
if (state.videoType === 'youtube') {
|
|
state.youtubeApiAvailable = false;
|
|
|
|
onYTApiReady = function() {
|
|
console.log('[Video info]: YouTube API is available and is loaded.');
|
|
if (state.htmlPlayerLoaded) { return; }
|
|
|
|
console.log('[Video info]: Starting YouTube player.');
|
|
video = VideoPlayer(state);
|
|
|
|
state.modules.push(video);
|
|
state.__dfd__.resolve();
|
|
state.youtubeApiAvailable = true;
|
|
};
|
|
|
|
if (window.YT) {
|
|
// If we have a Deferred object responsible for calling OnYouTubeIframeAPIReady
|
|
// callbacks, make sure that they have all been called by trying to resolve the
|
|
// Deferred object. Upon resolving, all the OnYouTubeIframeAPIReady will be
|
|
// called. If the object has been already resolved, the callbacks will not
|
|
// be called a second time.
|
|
if (_youtubeApiDeferred) {
|
|
_youtubeApiDeferred.resolve();
|
|
}
|
|
|
|
window.YT.ready(onYTApiReady);
|
|
} else {
|
|
// There is only one global variable window.onYouTubeIframeAPIReady which
|
|
// is supposed to be a function that will be called by the YouTube API
|
|
// when it finished initializing. This function will update this global function
|
|
// so that it resolves our Deferred object, which will call all of the
|
|
// OnYouTubeIframeAPIReady callbacks.
|
|
//
|
|
// If this global function is already defined, we store it first, and make
|
|
// sure that it gets executed when our Deferred object is resolved.
|
|
setupOnYouTubeIframeAPIReady = function() {
|
|
_oldOnYouTubeIframeAPIReady = window.onYouTubeIframeAPIReady || undefined;
|
|
|
|
window.onYouTubeIframeAPIReady = function() {
|
|
_youtubeApiDeferred.resolve();
|
|
};
|
|
|
|
window.onYouTubeIframeAPIReady.done = _youtubeApiDeferred.done;
|
|
|
|
if (_oldOnYouTubeIframeAPIReady) {
|
|
window.onYouTubeIframeAPIReady.done(_oldOnYouTubeIframeAPIReady);
|
|
}
|
|
};
|
|
|
|
// If a Deferred object hasn't been created yet, create one now. It will
|
|
// be responsible for calling OnYouTubeIframeAPIReady callbacks once the
|
|
// YouTube API loads. After creating the Deferred object, load the YouTube
|
|
// API.
|
|
if (!_youtubeApiDeferred) {
|
|
_youtubeApiDeferred = $.Deferred();
|
|
setupOnYouTubeIframeAPIReady();
|
|
} else if (!window.onYouTubeIframeAPIReady || !window.onYouTubeIframeAPIReady.done) {
|
|
// The Deferred object could have been already defined in a previous
|
|
// initialization of the video module. However, since then the global variable
|
|
// window.onYouTubeIframeAPIReady could have been overwritten. If so,
|
|
// we should set it up again.
|
|
setupOnYouTubeIframeAPIReady();
|
|
}
|
|
|
|
// Attach a callback to our Deferred object to be called once the
|
|
// YouTube API loads.
|
|
window.onYouTubeIframeAPIReady.done(function() {
|
|
window.YT.ready(onYTApiReady);
|
|
});
|
|
}
|
|
} else {
|
|
video = VideoPlayer(state);
|
|
|
|
state.modules.push(video);
|
|
state.__dfd__.resolve();
|
|
state.htmlPlayerLoaded = true;
|
|
}
|
|
}
|
|
|
|
function _waitForYoutubeApi(state) {
|
|
console.log('[Video info]: Starting to wait for YouTube API to load.');
|
|
window.setTimeout(function() {
|
|
// If YouTube API will load OK, it will run `onYouTubeIframeAPIReady`
|
|
// callback, which will set `state.youtubeApiAvailable` to `true`.
|
|
// If something goes wrong at this stage, `state.youtubeApiAvailable` is
|
|
// `false`.
|
|
if (!state.youtubeApiAvailable) {
|
|
console.log('[Video info]: YouTube API is not available.');
|
|
if (!state.htmlPlayerLoaded) {
|
|
state.loadHtmlPlayer();
|
|
}
|
|
}
|
|
state.el.trigger('youtube_availability', [state.youtubeApiAvailable]);
|
|
}, state.config.ytTestTimeout);
|
|
}
|
|
|
|
function loadYouTubeIFrameAPI(scriptTag) {
|
|
let firstScriptTag = document.getElementsByTagName('script')[0];
|
|
firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag);
|
|
}
|
|
|
|
// function _parseYouTubeIDs(state)
|
|
// The function parse YouTube stream ID's.
|
|
// @return
|
|
// false: We don't have YouTube video IDs to work with; most likely
|
|
// we have HTML5 video sources.
|
|
// true: Parsing of YouTube video IDs went OK, and we can proceed
|
|
// onwards to play YouTube videos.
|
|
function _parseYouTubeIDs(state) {
|
|
if (state.parseYoutubeStreams(state.config.streams)) {
|
|
state.videoType = 'youtube';
|
|
|
|
return true;
|
|
}
|
|
|
|
console.log(
|
|
'[Video info]: Youtube Video IDs are incorrect or absent.'
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Extract HLS video URLs from available video URLs.
|
|
*
|
|
* @param {object} state The object contaning the state (properties, methods, modules) of the Video player.
|
|
* @returns Array of available HLS video source urls.
|
|
*/
|
|
function extractHLSVideoSources(state) {
|
|
return _.filter(state.config.sources, function(source) {
|
|
return /\.m3u8(\?.*)?$/.test(source);
|
|
});
|
|
}
|
|
|
|
// function _prepareHTML5Video(state)
|
|
// The function prepare HTML5 video, parse HTML5
|
|
// video sources etc.
|
|
function _prepareHTML5Video(state) {
|
|
state.speeds = ['0.75', '1.0', '1.25', '1.50', '2.0'];
|
|
// If none of the supported video formats can be played and there is no
|
|
// short-hand video links, than hide the spinner and show error message.
|
|
if (!state.config.sources.length) {
|
|
_hideWaitPlaceholder(state);
|
|
state.el
|
|
.find('.video-player div')
|
|
.addClass('hidden');
|
|
state.el
|
|
.find('.video-player .video-error')
|
|
.removeClass('is-hidden');
|
|
|
|
return false;
|
|
}
|
|
|
|
state.videoType = 'html5';
|
|
|
|
if (!_.keys(state.config.transcriptLanguages).length) {
|
|
state.config.showCaptions = false;
|
|
}
|
|
state.setSpeed(state.speed);
|
|
|
|
return true;
|
|
}
|
|
|
|
function _hideWaitPlaceholder(state) {
|
|
state.el
|
|
.addClass('is-initialized')
|
|
.find('.spinner')
|
|
.attr({
|
|
'aria-hidden': 'true',
|
|
tabindex: -1
|
|
});
|
|
}
|
|
|
|
function _setConfigurations(state) {
|
|
state.setPlayerMode(state.config.mode);
|
|
// Possible value are: 'visible', 'hiding', and 'invisible'.
|
|
state.controlState = 'visible';
|
|
state.controlHideTimeout = null;
|
|
state.captionState = 'invisible';
|
|
state.captionHideTimeout = null;
|
|
state.HLSVideoSources = extractHLSVideoSources(state);
|
|
}
|
|
|
|
// eslint-disable-next-line no-shadow
|
|
function _initializeModules(state, i18n) {
|
|
let dfd = $.Deferred(),
|
|
modulesList = $.map(state.modules, function(module) {
|
|
let options = state.options[module.moduleName] || {};
|
|
if (_.isFunction(module)) {
|
|
return module(state, i18n, options);
|
|
} else if ($.isPlainObject(module)) {
|
|
return module;
|
|
}
|
|
});
|
|
|
|
$.when.apply(null, modulesList)
|
|
.done(dfd.resolve);
|
|
|
|
return dfd.promise();
|
|
}
|
|
|
|
function _getConfiguration(data, storage) {
|
|
let isBoolean = function(value) {
|
|
let regExp = /^true$/i;
|
|
return regExp.test(value.toString());
|
|
},
|
|
// List of keys that will be extracted form the configuration.
|
|
extractKeys = [],
|
|
// Compatibility keys used to change names of some parameters in
|
|
// the final configuration.
|
|
compatKeys = {
|
|
start: 'startTime',
|
|
end: 'endTime'
|
|
},
|
|
// Conversions used to pre-process some configuration data.
|
|
conversions = {
|
|
showCaptions: isBoolean,
|
|
autoplay: isBoolean,
|
|
autohideHtml5: isBoolean,
|
|
autoAdvance: function(value) {
|
|
let shouldAutoAdvance = storage.getItem('auto_advance');
|
|
if (_.isUndefined(shouldAutoAdvance)) {
|
|
return isBoolean(value) || false;
|
|
} else {
|
|
return shouldAutoAdvance;
|
|
}
|
|
},
|
|
savedVideoPosition: function(value) {
|
|
return storage.getItem('savedVideoPosition', true)
|
|
|| Number(value)
|
|
|| 0;
|
|
},
|
|
speed: function(value) {
|
|
return storage.getItem('speed', true) || value;
|
|
},
|
|
generalSpeed: function(value) {
|
|
return storage.getItem('general_speed')
|
|
|| value
|
|
|| '1.0';
|
|
},
|
|
transcriptLanguage: function(value) {
|
|
return storage.getItem('language')
|
|
|| value
|
|
|| 'en';
|
|
},
|
|
ytTestTimeout: function(value) {
|
|
value = parseInt(value, 10);
|
|
|
|
if (!isFinite(value)) {
|
|
value = 1500;
|
|
}
|
|
|
|
return value;
|
|
},
|
|
startTime: function(value) {
|
|
value = parseInt(value, 10);
|
|
if (!isFinite(value) || value < 0) {
|
|
return 0;
|
|
}
|
|
|
|
return value;
|
|
},
|
|
endTime: function(value) {
|
|
value = parseInt(value, 10);
|
|
|
|
if (!isFinite(value) || value === 0) {
|
|
return null;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
},
|
|
config = {};
|
|
|
|
data = _.extend({
|
|
startTime: 0,
|
|
endTime: null,
|
|
sub: '',
|
|
streams: ''
|
|
}, data);
|
|
|
|
$.each(data, function(option, value) {
|
|
// Extract option that is in `extractKeys`.
|
|
if ($.inArray(option, extractKeys) !== -1) {
|
|
return;
|
|
}
|
|
|
|
// Change option name to key that is in `compatKeys`.
|
|
if (compatKeys[option]) {
|
|
option = compatKeys[option];
|
|
}
|
|
|
|
// Pre-process data.
|
|
if (conversions[option]) {
|
|
if (_.isFunction(conversions[option])) {
|
|
value = conversions[option].call(this, value);
|
|
} else {
|
|
throw new TypeError(option + ' is not a function.');
|
|
}
|
|
}
|
|
config[option] = value;
|
|
});
|
|
|
|
return config;
|
|
}
|
|
|
|
// ***************************************************************
|
|
// Public functions start here.
|
|
// These are available via the 'state' object. Their context ('this'
|
|
// keyword) is the 'state' object. The magic private function that makes
|
|
// them available and sets up their context is makeFunctionsPublic().
|
|
// ***************************************************************
|
|
|
|
// function bindTo(methodsDict, obj, context, rewrite)
|
|
// Creates a new function with specific context and assigns it to the provided
|
|
// object.
|
|
// eslint-disable-next-line no-shadow
|
|
function bindTo(methodsDict, obj, context, rewrite) {
|
|
$.each(methodsDict, function(name, method) {
|
|
if (_.isFunction(method)) {
|
|
if (_.isUndefined(rewrite)) {
|
|
rewrite = true;
|
|
}
|
|
|
|
if (_.isUndefined(obj[name]) || rewrite) {
|
|
obj[name] = _.bind(method, context);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadYoutubePlayer() {
|
|
if (this.htmlPlayerLoaded) { return; }
|
|
|
|
console.log(
|
|
'[Video info]: Fetch metadata for YouTube video.'
|
|
);
|
|
|
|
this.fetchMetadata();
|
|
this.parseSpeed();
|
|
}
|
|
|
|
function loadHtmlPlayer() {
|
|
// When the youtube link doesn't work for any reason
|
|
// (for example, firewall) any
|
|
// alternate sources should automatically play.
|
|
if (!_prepareHTML5Video(this)) {
|
|
console.log(
|
|
'[Video info]: Continue loading '
|
|
+ 'YouTube video.'
|
|
);
|
|
|
|
// Non-YouTube sources were not found either.
|
|
|
|
this.el.find('.video-player div')
|
|
.removeClass('hidden');
|
|
this.el.find('.video-player .video-error')
|
|
.addClass('is-hidden');
|
|
|
|
// If in reality the timeout was to short, try to
|
|
// continue loading the YouTube video anyways.
|
|
this.loadYoutubePlayer();
|
|
} else {
|
|
console.log(
|
|
'[Video info]: Start HTML5 player.'
|
|
);
|
|
|
|
// In-browser HTML5 player does not support quality
|
|
// control.
|
|
this.el.find('.quality_control').hide();
|
|
_renderElements(this);
|
|
}
|
|
}
|
|
|
|
// function initialize(element)
|
|
// The function set initial configuration and preparation.
|
|
|
|
function initialize(element) {
|
|
let self = this,
|
|
el = this.el,
|
|
id = this.id,
|
|
container = el.find('.video-wrapper'),
|
|
__dfd__ = $.Deferred(),
|
|
isTouch = onTouchBasedDevice() || '';
|
|
|
|
if (isTouch) {
|
|
el.addClass('is-touch');
|
|
}
|
|
|
|
$.extend(this, {
|
|
__dfd__: __dfd__,
|
|
container: container,
|
|
isFullScreen: false,
|
|
isTouch: isTouch
|
|
});
|
|
|
|
console.log('[Video info]: Initializing video with id "%s".', id);
|
|
|
|
// We store all settings passed to us by the server in one place. These
|
|
// are "read only", so don't modify them. All variable content lives in
|
|
// 'state' object.
|
|
// jQuery .data() return object with keys in lower camelCase format.
|
|
this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), {
|
|
element: element,
|
|
fadeOutTimeout: 1400,
|
|
captionsFreezeTime: 10000,
|
|
mode: $.cookie('edX_video_player_mode'),
|
|
// Available HD qualities will only be accessible once the video has
|
|
// been played once, via player.getAvailableQualityLevels.
|
|
availableHDQualities: []
|
|
});
|
|
|
|
if (this.config.endTime < this.config.startTime) {
|
|
this.config.endTime = null;
|
|
}
|
|
|
|
this.lang = this.config.transcriptLanguage;
|
|
this.speed = this.speedToString(
|
|
this.config.speed || this.config.generalSpeed
|
|
);
|
|
this.auto_advance = this.config.autoAdvance;
|
|
this.htmlPlayerLoaded = false;
|
|
this.duration = this.metadata.duration;
|
|
|
|
_setConfigurations(this);
|
|
|
|
// If `prioritizeHls` is set to true than `hls` is the primary playback
|
|
if (this.config.prioritizeHls || !(_parseYouTubeIDs(this))) {
|
|
// If we do not have YouTube ID's, try parsing HTML5 video sources.
|
|
if (!_prepareHTML5Video(this)) {
|
|
__dfd__.reject();
|
|
// Non-YouTube sources were not found either.
|
|
return __dfd__.promise();
|
|
}
|
|
|
|
console.log('[Video info]: Start player in HTML5 mode.');
|
|
_renderElements(this);
|
|
} else {
|
|
_renderElements(this);
|
|
|
|
_waitForYoutubeApi(this);
|
|
|
|
let scriptTag = document.createElement('script');
|
|
|
|
scriptTag.src = this.config.ytApiUrl;
|
|
scriptTag.async = true;
|
|
|
|
$(scriptTag).on('load', function() {
|
|
self.loadYoutubePlayer();
|
|
});
|
|
$(scriptTag).on('error', function() {
|
|
console.log(
|
|
'[Video info]: YouTube returned an error for '
|
|
+ 'video with id "' + self.id + '".'
|
|
);
|
|
// If the video is already loaded in `_waitForYoutubeApi` by the
|
|
// time we get here, then we shouldn't load it again.
|
|
if (!self.htmlPlayerLoaded) {
|
|
self.loadHtmlPlayer();
|
|
}
|
|
});
|
|
|
|
window.Video.loadYouTubeIFrameAPI(scriptTag);
|
|
}
|
|
return __dfd__.promise();
|
|
}
|
|
|
|
// function parseYoutubeStreams(state, youtubeStreams)
|
|
//
|
|
// Take a string in the form:
|
|
// "iCawTYPtehk:0.75,KgpclqP-LBA:1.0,9-2670d5nvU:1.5"
|
|
// parse it, and make it available via the 'state' object. If we are
|
|
// not given a string, or it's length is zero, then we return false.
|
|
//
|
|
// @return
|
|
// false: We don't have YouTube video IDs to work with; most likely
|
|
// we have HTML5 video sources.
|
|
// true: Parsing of YouTube video IDs went OK, and we can proceed
|
|
// onwards to play YouTube videos.
|
|
function parseYoutubeStreams(youtubeStreams) {
|
|
if (_.isUndefined(youtubeStreams) || !youtubeStreams.length) {
|
|
return false;
|
|
}
|
|
|
|
this.videos = {};
|
|
|
|
_.each(youtubeStreams.split(/,/), function(video) {
|
|
let speed;
|
|
video = video.split(/:/);
|
|
speed = this.speedToString(video[0]);
|
|
this.videos[speed] = video[1];
|
|
}, this);
|
|
|
|
return _.isString(this.videos['1.0']);
|
|
}
|
|
|
|
// function fetchMetadata()
|
|
//
|
|
// When dealing with YouTube videos, we must fetch meta data that has
|
|
// certain key facts not available while the video is loading. For
|
|
// example the length of the video can be determined from the meta
|
|
// data.
|
|
function fetchMetadata() {
|
|
let self = this,
|
|
metadataXHRs = [];
|
|
|
|
this.metadata = {};
|
|
|
|
metadataXHRs = _.map(this.videos, function(url, speed) {
|
|
return self.getVideoMetadata(url, function(data) {
|
|
if (data.items.length > 0) {
|
|
let metaDataItem = data.items[0];
|
|
self.metadata[metaDataItem.id] = metaDataItem.contentDetails;
|
|
}
|
|
});
|
|
});
|
|
|
|
$.when.apply(this, metadataXHRs).done(function() {
|
|
self.el.trigger('metadata_received');
|
|
|
|
// Not only do we trigger the "metadata_received" event, we also
|
|
// set a flag to notify that metadata has been received. This
|
|
// allows for code that will miss the "metadata_received" event
|
|
// to know that metadata has been received. This is important in
|
|
// cases when some code will subscribe to the "metadata_received"
|
|
// event after it has been triggered.
|
|
self.youtubeMetadataReceived = true;
|
|
});
|
|
}
|
|
|
|
// function parseSpeed()
|
|
//
|
|
// Create a separate array of available speeds.
|
|
function parseSpeed() {
|
|
this.speeds = _.keys(this.videos).sort();
|
|
}
|
|
|
|
function setSpeed(newSpeed) {
|
|
// Possible speeds for each player type.
|
|
// HTML5 = [0.75, 1, 1.25, 1.5, 2]
|
|
// Youtube Flash = [0.75, 1, 1.25, 1.5]
|
|
// Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2]
|
|
let map = {
|
|
0.25: '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
|
|
'0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
|
|
0.75: '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
|
|
1.25: '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
|
|
2.0: '1.50' // HTML5 or Youtube HTML5 -> Youtube Flash
|
|
};
|
|
|
|
if (_.contains(this.speeds, newSpeed)) {
|
|
this.speed = newSpeed;
|
|
} else {
|
|
newSpeed = map[newSpeed];
|
|
this.speed = _.contains(this.speeds, newSpeed) ? newSpeed : '1.0';
|
|
}
|
|
this.speed = parseFloat(this.speed);
|
|
}
|
|
|
|
function setAutoAdvance(enabled) {
|
|
this.auto_advance = enabled;
|
|
}
|
|
|
|
function getVideoMetadata(url, callback) {
|
|
let youTubeEndpoint;
|
|
if (!(_.isString(url))) {
|
|
url = this.videos['1.0'] || '';
|
|
}
|
|
// Will hit the API URL to get the youtube video metadata.
|
|
youTubeEndpoint = this.config.ytMetadataEndpoint; // The new runtime supports anonymous users
|
|
// and uses an XBlock handler to get YouTube metadata
|
|
if (!youTubeEndpoint) {
|
|
// The old runtime has a full/separate LMS API for getting YouTube metadata, but it doesn't
|
|
// support anonymous users nor videos that play in a sandboxed iframe.
|
|
youTubeEndpoint = [this.config.lmsRootURL, '/courses/yt_video_metadata', '?id=', url].join('');
|
|
}
|
|
return $.ajax({
|
|
url: youTubeEndpoint,
|
|
success: _.isFunction(callback) ? callback : null,
|
|
error: function() {
|
|
console.warn(
|
|
'Unable to get youtube video metadata. Some video metadata may be unavailable.'
|
|
);
|
|
},
|
|
notifyOnError: false
|
|
});
|
|
}
|
|
|
|
function youtubeId(speed) {
|
|
let currentSpeed = this.isFlashMode() ? this.speed : '1.0';
|
|
|
|
return this.videos[speed]
|
|
|| this.videos[currentSpeed]
|
|
|| this.videos['1.0'];
|
|
}
|
|
|
|
function getDuration() {
|
|
try {
|
|
let safeMoment = typeof moment !== 'undefined' ? moment : window.moment;
|
|
return safeMoment.duration(this.metadata[this.youtubeId()].duration, safeMoment.ISO_8601).asSeconds();
|
|
} catch (err) {
|
|
return _.result(this.metadata[this.youtubeId('1.0')], 'duration') || 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets player mode.
|
|
*
|
|
* @param {string} mode Mode to set for the video player if it is supported.
|
|
* Otherwise, `html5` is used by default.
|
|
*/
|
|
function setPlayerMode(mode) {
|
|
let supportedModes = ['html5', 'flash'];
|
|
|
|
mode = _.contains(supportedModes, mode) ? mode : 'html5';
|
|
this.currentPlayerMode = mode;
|
|
}
|
|
|
|
/**
|
|
* Returns current player mode.
|
|
*
|
|
* @return {string} Returns string that describes player mode
|
|
*/
|
|
function getPlayerMode() {
|
|
return this.currentPlayerMode;
|
|
}
|
|
|
|
/**
|
|
* Checks if current player mode is Flash.
|
|
*
|
|
* @return {boolean} Returns `true` if current mode is `flash`, otherwise
|
|
* it returns `false`
|
|
*/
|
|
function isFlashMode() {
|
|
return this.getPlayerMode() === 'flash';
|
|
}
|
|
|
|
/**
|
|
* Checks if current player mode is Html5.
|
|
*
|
|
* @return {boolean} Returns `true` if current mode is `html5`, otherwise
|
|
* it returns `false`
|
|
*/
|
|
function isHtml5Mode() {
|
|
return this.getPlayerMode() === 'html5';
|
|
}
|
|
|
|
function isYoutubeType() {
|
|
return this.videoType === 'youtube';
|
|
}
|
|
|
|
function speedToString(speed) {
|
|
return parseFloat(speed).toFixed(2).replace(/\.00$/, '.0');
|
|
}
|
|
|
|
function getCurrentLanguage() {
|
|
let keys = _.keys(this.config.transcriptLanguages);
|
|
|
|
if (keys.length) {
|
|
if (!_.contains(keys, this.lang)) {
|
|
if (_.contains(keys, 'en')) {
|
|
this.lang = 'en';
|
|
} else {
|
|
this.lang = keys.pop();
|
|
}
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
return this.lang;
|
|
}
|
|
|
|
/*
|
|
* The trigger() function will assume that the @objChain is a complete
|
|
* chain with a method (function) at the end. It will call this function.
|
|
* So for example, when trigger() is called like so:
|
|
*
|
|
* state.trigger('videoPlayer.pause', {'param1': 10});
|
|
*
|
|
* Then trigger() will execute:
|
|
*
|
|
* state.videoPlayer.pause({'param1': 10});
|
|
*/
|
|
function trigger(objChain) {
|
|
let extraParameters = Array.prototype.slice.call(arguments, 1),
|
|
i, tmpObj, chain;
|
|
|
|
// Remember that 'this' is the 'state' object.
|
|
tmpObj = this;
|
|
chain = objChain.split('.');
|
|
|
|
// At the end of the loop the variable 'tmpObj' will either be the
|
|
// correct object/function to trigger/invoke. If the 'chain' chain of
|
|
// object is incorrect (one of the link is non-existent), then the loop
|
|
// will immediately exit.
|
|
while (chain.length) {
|
|
i = chain.shift();
|
|
|
|
if (tmpObj.hasOwnProperty(i)) {
|
|
tmpObj = tmpObj[i];
|
|
} else {
|
|
// An incorrect object chain was specified.
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
tmpObj.apply(this, extraParameters);
|
|
|
|
return true;
|
|
}
|