refactor: Update and migrate Video Block JS files into xmodule/assets
- 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>
This commit is contained in:
committed by
GitHub
parent
e80317d814
commit
5c759f1e13
@@ -332,7 +332,11 @@ function getBaseConfig(config, useRequireJs) {
|
||||
base: 'Firefox',
|
||||
prefs: {
|
||||
'app.update.auto': false,
|
||||
'app.update.enabled': false
|
||||
'app.update.enabled': false,
|
||||
'media.autoplay.default': 0, // allow autoplay
|
||||
'media.autoplay.blocking_policy': 0, // disable autoplay blocking
|
||||
'media.autoplay.allow-extension-background-pages': true,
|
||||
'media.autoplay.enabled.user-gestures-needed': false,
|
||||
}
|
||||
},
|
||||
ChromeDocker: {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"watch-sass": "scripts/watch_sass.sh",
|
||||
"test": "npm run test-jest && npm run test-karma",
|
||||
"test-jest": "jest",
|
||||
"test-karma": "npm run test-karma-vanilla && npm run test-karma-require && echo 'WARNING: Skipped broken webpack tests. For details, see: https://github.com/openedx/edx-platform/issues/35956'",
|
||||
"test-karma": "npm run test-karma-vanilla && npm run test-karma-require && npm run test-xmodule-webpack && echo 'WARNING: Skipped broken lms-webpack and cms-webpack tests. For details, see: https://github.com/openedx/edx-platform/issues/35956'",
|
||||
"test-karma-vanilla": "npm run test-cms-vanilla && npm run test-xmodule-vanilla && npm run test-common-vanilla",
|
||||
"test-karma-require": "npm run test-cms-require && npm run test-common-require",
|
||||
"test-karma-webpack": "npm run test-cms-webpack && npm run test-lms-webpack && npm run test-xmodule-webpack",
|
||||
|
||||
@@ -79,14 +79,13 @@ module.exports = {
|
||||
'./xmodule/js/src/xmodule.js',
|
||||
'./xmodule/js/src/sequence/edit.js'
|
||||
],
|
||||
VideoBlockDisplay: [
|
||||
'./xmodule/js/src/xmodule.js',
|
||||
'./xmodule/js/src/video/10_main.js'
|
||||
],
|
||||
VideoBlockEditor: [
|
||||
'./xmodule/js/src/xmodule.js',
|
||||
'./xmodule/js/src/tabs/tabs-aggregator.js'
|
||||
],
|
||||
VideoBlockDisplay: [
|
||||
'./xmodule/assets/video/public/js/10_main.js'
|
||||
],
|
||||
WordCloudBlockDisplay: [
|
||||
'./xmodule/js/src/xmodule.js',
|
||||
'./xmodule/assets/word_cloud/src/js/word_cloud.js'
|
||||
|
||||
@@ -505,15 +505,6 @@ module.exports = Merge.merge({
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /xmodule\/js\/src\/video\/10_main.js/,
|
||||
use: [
|
||||
{
|
||||
loader: 'imports-loader',
|
||||
options: 'this=>window'
|
||||
}
|
||||
]
|
||||
},
|
||||
/*
|
||||
* END BUILT-IN XBLOCK ASSETS WITH GLOBAL DEFINITIONS
|
||||
***************************************************************************************************** */
|
||||
@@ -680,9 +671,11 @@ module.exports = Merge.merge({
|
||||
$: 'jQuery',
|
||||
backbone: 'Backbone',
|
||||
canvas: 'canvas',
|
||||
fs: 'fs',
|
||||
gettext: 'gettext',
|
||||
jquery: 'jQuery',
|
||||
logger: 'Logger',
|
||||
path: 'path',
|
||||
underscore: '_',
|
||||
URI: 'URI',
|
||||
XBlockToXModuleShim: 'XBlockToXModuleShim',
|
||||
|
||||
52
xmodule/assets/video/public/js/00_async_process.js
Normal file
52
xmodule/assets/video/public/js/00_async_process.js
Normal file
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides convenient way to process big amount of data without UI blocking.
|
||||
*
|
||||
* @param {array} list Array to process.
|
||||
* @param {function} process Calls this function on each item in the list.
|
||||
* @return {array} Returns a Promise object to observe when all actions of a
|
||||
* certain type bound to the collection, queued or not, have finished.
|
||||
*/
|
||||
let AsyncProcess = {
|
||||
array: function(list, process) {
|
||||
if (!_.isArray(list)) {
|
||||
return $.Deferred().reject().promise();
|
||||
}
|
||||
|
||||
if (!_.isFunction(process) || !list.length) {
|
||||
return $.Deferred().resolve(list).promise();
|
||||
}
|
||||
|
||||
let MAX_DELAY = 50, // maximum amount of time that js code should be allowed to run continuously
|
||||
dfd = $.Deferred();
|
||||
let result = [];
|
||||
let index = 0;
|
||||
let len = list.length;
|
||||
|
||||
let getCurrentTime = function() {
|
||||
return (new Date()).getTime();
|
||||
};
|
||||
|
||||
let handler = function() {
|
||||
let start = getCurrentTime();
|
||||
|
||||
do {
|
||||
result[index] = process(list[index], index);
|
||||
index++;
|
||||
} while (index < len && getCurrentTime() - start < MAX_DELAY);
|
||||
|
||||
if (index < len) {
|
||||
setTimeout(handler, 25);
|
||||
} else {
|
||||
dfd.resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(handler, 25);
|
||||
|
||||
return dfd.promise();
|
||||
}
|
||||
};
|
||||
|
||||
export default AsyncProcess;
|
||||
81
xmodule/assets/video/public/js/00_component.js
Normal file
81
xmodule/assets/video/public/js/00_component.js
Normal file
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new object with the specified prototype object and properties.
|
||||
* @param {Object} o The object which should be the prototype of the
|
||||
* newly-created object.
|
||||
* @private
|
||||
* @throws {TypeError, Error}
|
||||
* @return {Object}
|
||||
*/
|
||||
let inherit = Object.create || (function() {
|
||||
let F = function() {};
|
||||
|
||||
return function(o) {
|
||||
if (arguments.length > 1) {
|
||||
throw Error('Second argument not supported');
|
||||
}
|
||||
if (_.isNull(o) || _.isUndefined(o)) {
|
||||
throw Error('Cannot set a null [[Prototype]]');
|
||||
}
|
||||
if (!_.isObject(o)) {
|
||||
throw TypeError('Argument must be an object');
|
||||
}
|
||||
|
||||
F.prototype = o;
|
||||
|
||||
return new F();
|
||||
};
|
||||
}());
|
||||
|
||||
/**
|
||||
* Component module.
|
||||
* @exports video/00_component.js
|
||||
* @constructor
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let Component = function() {
|
||||
if ($.isFunction(this.initialize)) {
|
||||
// eslint-disable-next-line prefer-spread
|
||||
return this.initialize.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns new constructor that inherits form the current constructor.
|
||||
* @static
|
||||
* @param {Object} protoProps The object containing which will be added to
|
||||
* the prototype.
|
||||
* @return {Object}
|
||||
*/
|
||||
Component.extend = function(protoProps, staticProps) {
|
||||
let Parent = this;
|
||||
let Child = function() {
|
||||
if ($.isFunction(this.initialize)) {
|
||||
// eslint-disable-next-line prefer-spread
|
||||
return this.initialize.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// Inherit methods and properties from the Parent prototype.
|
||||
Child.prototype = inherit(Parent.prototype);
|
||||
Child.constructor = Parent;
|
||||
// Provide access to parent's methods and properties
|
||||
Child.__super__ = Parent.prototype;
|
||||
|
||||
// Extends inherited methods and properties by methods/properties
|
||||
// passed as argument.
|
||||
if (protoProps) {
|
||||
$.extend(Child.prototype, protoProps);
|
||||
}
|
||||
|
||||
// Inherit static methods and properties
|
||||
$.extend(Child, Parent, staticProps);
|
||||
|
||||
return Child;
|
||||
};
|
||||
|
||||
export default Component;
|
||||
35
xmodule/assets/video/public/js/00_i18n.js
Normal file
35
xmodule/assets/video/public/js/00_i18n.js
Normal file
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* i18n module.
|
||||
* @exports video/00_i18n.js
|
||||
* @return {object}
|
||||
*/
|
||||
|
||||
let i18n = {
|
||||
Play: gettext('Play'),
|
||||
Pause: gettext('Pause'),
|
||||
Mute: gettext('Mute'),
|
||||
Unmute: gettext('Unmute'),
|
||||
'Exit full browser': gettext('Exit full browser'),
|
||||
'Fill browser': gettext('Fill browser'),
|
||||
Speed: gettext('Speed'),
|
||||
'Auto-advance': gettext('Auto-advance'),
|
||||
Volume: gettext('Volume'),
|
||||
// Translators: Volume level equals 0%.
|
||||
Muted: gettext('Muted'),
|
||||
// Translators: Volume level in range ]0,20]%
|
||||
'Very low': gettext('Very low'),
|
||||
// Translators: Volume level in range ]20,40]%
|
||||
Low: gettext('Low'),
|
||||
// Translators: Volume level in range ]40,60]%
|
||||
Average: gettext('Average'),
|
||||
// Translators: Volume level in range ]60,80]%
|
||||
Loud: gettext('Loud'),
|
||||
// Translators: Volume level in range ]80,99]%
|
||||
'Very loud': gettext('Very loud'),
|
||||
// Translators: Volume level equals 100%.
|
||||
Maximum: gettext('Maximum')
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
83
xmodule/assets/video/public/js/00_iterator.js
Normal file
83
xmodule/assets/video/public/js/00_iterator.js
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides convenient way to work with iterable data.
|
||||
* @exports video/00_iterator.js
|
||||
* @constructor
|
||||
* @param {array} list Array to be iterated.
|
||||
*/
|
||||
let Iterator = function(list) {
|
||||
this.list = list;
|
||||
this.index = 0;
|
||||
this.size = this.list.length;
|
||||
this.lastIndex = this.list.length - 1;
|
||||
};
|
||||
|
||||
Iterator.prototype = {
|
||||
|
||||
/**
|
||||
* Checks validity of provided index for the iterator.
|
||||
* @access protected
|
||||
* @param {numebr} index
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isValid: function(index) {
|
||||
return _.isNumber(index) && index < this.size && index >= 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns next element.
|
||||
* @param {number} [index] Updates current position.
|
||||
* @return {any}
|
||||
*/
|
||||
next: function(index) {
|
||||
if (!(this._isValid(index))) {
|
||||
index = this.index;
|
||||
}
|
||||
|
||||
this.index = (index >= this.lastIndex) ? 0 : index + 1;
|
||||
|
||||
return this.list[this.index];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns previous element.
|
||||
* @param {number} [index] Updates current position.
|
||||
* @return {any}
|
||||
*/
|
||||
prev: function(index) {
|
||||
if (!(this._isValid(index))) {
|
||||
index = this.index;
|
||||
}
|
||||
|
||||
this.index = (index < 1) ? this.lastIndex : index - 1;
|
||||
|
||||
return this.list[this.index];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns last element in the list.
|
||||
* @return {any}
|
||||
*/
|
||||
last: function() {
|
||||
return this.list[this.lastIndex];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns first element in the list.
|
||||
* @return {any}
|
||||
*/
|
||||
first: function() {
|
||||
return this.list[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns `true` if current position is last for the iterator.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isEnd: function() {
|
||||
return this.index === this.lastIndex;
|
||||
}
|
||||
};
|
||||
|
||||
export default Iterator;
|
||||
236
xmodule/assets/video/public/js/00_resizer.js
Normal file
236
xmodule/assets/video/public/js/00_resizer.js
Normal file
@@ -0,0 +1,236 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
let Resizer = function(params) {
|
||||
let defaults = {
|
||||
container: window,
|
||||
element: null,
|
||||
containerRatio: null,
|
||||
elementRatio: null
|
||||
},
|
||||
callbacksList = [],
|
||||
delta = {
|
||||
height: 0,
|
||||
width: 0
|
||||
},
|
||||
module = {};
|
||||
let mode = null,
|
||||
config;
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
let initialize = function(params) {
|
||||
if (!config) {
|
||||
config = defaults;
|
||||
}
|
||||
|
||||
config = $.extend(true, {}, config, params);
|
||||
|
||||
if (!config.element) {
|
||||
console.log(
|
||||
'Required parameter `element` is not passed.'
|
||||
);
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let getData = function() {
|
||||
let $container = $(config.container),
|
||||
containerWidth = $container.width() + delta.width,
|
||||
containerHeight = $container.height() + delta.height;
|
||||
let containerRatio = config.containerRatio;
|
||||
|
||||
let $element = $(config.element);
|
||||
let elementRatio = config.elementRatio;
|
||||
|
||||
if (!containerRatio) {
|
||||
containerRatio = containerWidth / containerHeight;
|
||||
}
|
||||
|
||||
if (!elementRatio) {
|
||||
elementRatio = $element.width() / $element.height();
|
||||
}
|
||||
|
||||
return {
|
||||
containerWidth: containerWidth,
|
||||
containerHeight: containerHeight,
|
||||
containerRatio: containerRatio,
|
||||
element: $element,
|
||||
elementRatio: elementRatio
|
||||
};
|
||||
};
|
||||
|
||||
let align = function() {
|
||||
let data = getData();
|
||||
|
||||
switch (mode) {
|
||||
case 'height':
|
||||
alignByHeightOnly();
|
||||
break;
|
||||
|
||||
case 'width':
|
||||
alignByWidthOnly();
|
||||
break;
|
||||
|
||||
default:
|
||||
if (data.containerRatio >= data.elementRatio) {
|
||||
alignByHeightOnly();
|
||||
} else {
|
||||
alignByWidthOnly();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
fireCallbacks();
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let alignByWidthOnly = function() {
|
||||
let data = getData(),
|
||||
height = data.containerWidth / data.elementRatio;
|
||||
|
||||
data.element.css({
|
||||
height: height,
|
||||
width: data.containerWidth,
|
||||
top: 0.5 * (data.containerHeight - height),
|
||||
left: 0
|
||||
});
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let alignByHeightOnly = function() {
|
||||
let data = getData(),
|
||||
width = data.containerHeight * data.elementRatio;
|
||||
|
||||
data.element.css({
|
||||
height: data.containerHeight,
|
||||
width: data.containerHeight * data.elementRatio,
|
||||
top: 0,
|
||||
left: 0.5 * (data.containerWidth - width)
|
||||
});
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let setMode = function(param) {
|
||||
if (_.isString(param)) {
|
||||
mode = param;
|
||||
align();
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let setElement = function(element) {
|
||||
config.element = element;
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let addCallback = function(func) {
|
||||
if ($.isFunction(func)) {
|
||||
callbacksList.push(func);
|
||||
} else {
|
||||
console.error('[Video info]: TypeError: Argument is not a function.');
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let addOnceCallback = function(func) {
|
||||
if ($.isFunction(func)) {
|
||||
let decorator = function() {
|
||||
func();
|
||||
removeCallback(func);
|
||||
};
|
||||
|
||||
addCallback(decorator);
|
||||
} else {
|
||||
console.error('TypeError: Argument is not a function.');
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let fireCallbacks = function() {
|
||||
$.each(callbacksList, function(index, callback) {
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
let removeCallbacks = function() {
|
||||
callbacksList.length = 0;
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let removeCallback = function(func) {
|
||||
let index = $.inArray(func, callbacksList);
|
||||
|
||||
if (index !== -1) {
|
||||
return callbacksList.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
let resetDelta = function() {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
delta.height = delta.width = 0;
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let addDelta = function(value, side) {
|
||||
if (_.isNumber(value) && _.isNumber(delta[side])) {
|
||||
delta[side] += value;
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let substractDelta = function(value, side) {
|
||||
if (_.isNumber(value) && _.isNumber(delta[side])) {
|
||||
delta[side] -= value;
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
let destroy = function() {
|
||||
let data = getData();
|
||||
data.element.css({
|
||||
height: '', width: '', top: '', left: ''
|
||||
});
|
||||
removeCallbacks();
|
||||
resetDelta();
|
||||
mode = null;
|
||||
};
|
||||
|
||||
initialize.apply(module, arguments);
|
||||
|
||||
return $.extend(true, module, {
|
||||
align: align,
|
||||
alignByWidthOnly: alignByWidthOnly,
|
||||
alignByHeightOnly: alignByHeightOnly,
|
||||
destroy: destroy,
|
||||
setParams: initialize,
|
||||
setMode: setMode,
|
||||
setElement: setElement,
|
||||
callbacks: {
|
||||
add: addCallback,
|
||||
once: addOnceCallback,
|
||||
remove: removeCallback,
|
||||
removeAll: removeCallbacks
|
||||
},
|
||||
delta: {
|
||||
add: addDelta,
|
||||
substract: substractDelta,
|
||||
reset: resetDelta
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default Resizer;
|
||||
108
xmodule/assets/video/public/js/00_sjson.js
Normal file
108
xmodule/assets/video/public/js/00_sjson.js
Normal file
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
let Sjson = function(data) {
|
||||
let sjson = {
|
||||
start: data.start.concat(),
|
||||
text: data.text.concat()
|
||||
},
|
||||
module = {};
|
||||
|
||||
let getter = function(propertyName) {
|
||||
return function() {
|
||||
return sjson[propertyName];
|
||||
};
|
||||
};
|
||||
|
||||
let getStartTimes = getter('start');
|
||||
|
||||
let getCaptions = getter('text');
|
||||
|
||||
let size = function() {
|
||||
return sjson.text.length;
|
||||
};
|
||||
|
||||
function search(time, startTime, endTime) {
|
||||
let start = getStartTimes(),
|
||||
max = size() - 1,
|
||||
min = 0,
|
||||
results,
|
||||
index;
|
||||
|
||||
// if we specify a start and end time to search,
|
||||
// search the filtered list of captions in between
|
||||
// the start / end times.
|
||||
// Else, search the unfiltered list.
|
||||
if (typeof startTime !== 'undefined'
|
||||
&& typeof endTime !== 'undefined') {
|
||||
results = filter(startTime, endTime);
|
||||
start = results.start;
|
||||
max = results.captions.length - 1;
|
||||
} else {
|
||||
start = getStartTimes();
|
||||
}
|
||||
while (min < max) {
|
||||
index = Math.ceil((max + min) / 2);
|
||||
|
||||
if (time < start[index]) {
|
||||
max = index - 1;
|
||||
}
|
||||
|
||||
if (time >= start[index]) {
|
||||
min = index;
|
||||
}
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
function filter(start, end) {
|
||||
/* filters captions that occur between inputs
|
||||
* `start` and `end`. Start and end should
|
||||
* be Numbers (doubles) corresponding to the
|
||||
* number of seconds elapsed since the beginning
|
||||
* of the video.
|
||||
*
|
||||
* Returns an object with properties
|
||||
* "start" and "captions" representing
|
||||
* parallel arrays of start times and
|
||||
* their corresponding captions.
|
||||
*/
|
||||
let filteredTimes = [];
|
||||
let filteredCaptions = [];
|
||||
let startTimes = getStartTimes();
|
||||
let captions = getCaptions();
|
||||
|
||||
if (startTimes.length !== captions.length) {
|
||||
console.warn('video caption and start time arrays do not match in length');
|
||||
}
|
||||
|
||||
// if end is null, then it's been set to
|
||||
// some erroneous value, so filter using the
|
||||
// entire array as long as it's not empty
|
||||
if (end === null && startTimes.length) {
|
||||
end = startTimes[startTimes.length - 1];
|
||||
}
|
||||
|
||||
_.filter(startTimes, function(currentStartTime, i) {
|
||||
if (currentStartTime >= start && currentStartTime <= end) {
|
||||
filteredTimes.push(currentStartTime);
|
||||
filteredCaptions.push(captions[i]);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
start: filteredTimes,
|
||||
captions: filteredCaptions
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getCaptions: getCaptions,
|
||||
getStartTimes: getStartTimes,
|
||||
getSize: size,
|
||||
filter: filter,
|
||||
search: search
|
||||
};
|
||||
};
|
||||
|
||||
export default Sjson;
|
||||
96
xmodule/assets/video/public/js/00_video_storage.js
Normal file
96
xmodule/assets/video/public/js/00_video_storage.js
Normal file
@@ -0,0 +1,96 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides convenient way to store key value pairs.
|
||||
*
|
||||
* @param {string} namespace Namespace that is used to store data.
|
||||
* @return {object} VideoStorage API.
|
||||
*/
|
||||
let VideoStorage = function(namespace, id) {
|
||||
/**
|
||||
* Adds new value to the storage or rewrites existent.
|
||||
*
|
||||
* @param {string} name Identifier of the data.
|
||||
* @param {any} value Data to store.
|
||||
* @param {boolean} instanceSpecific Data with this flag will be added
|
||||
* to instance specific storage.
|
||||
*/
|
||||
let setItem = function(name, value, instanceSpecific) {
|
||||
if (name) {
|
||||
if (instanceSpecific) {
|
||||
window[namespace][id][name] = value;
|
||||
} else {
|
||||
window[namespace][name] = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current value associated with the given name.
|
||||
*
|
||||
* @param {string} name Identifier of the data.
|
||||
* @param {boolean} instanceSpecific Data with this flag will be added
|
||||
* to instance specific storage.
|
||||
* @return {any} The current value associated with the given name.
|
||||
* If the given key does not exist in the list
|
||||
* associated with the object then this method must return null.
|
||||
*/
|
||||
let getItem = function(name, instanceSpecific) {
|
||||
if (instanceSpecific) {
|
||||
return window[namespace][id][name];
|
||||
} else {
|
||||
return window[namespace][name];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the current value associated with the given name.
|
||||
*
|
||||
* @param {string} name Identifier of the data.
|
||||
* @param {boolean} instanceSpecific Data with this flag will be added
|
||||
* to instance specific storage.
|
||||
*/
|
||||
let removeItem = function(name, instanceSpecific) {
|
||||
if (instanceSpecific) {
|
||||
delete window[namespace][id][name];
|
||||
} else {
|
||||
delete window[namespace][name];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Empties the storage.
|
||||
*
|
||||
*/
|
||||
let clear = function() {
|
||||
window[namespace] = {};
|
||||
window[namespace][id] = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the module: creates a storage with proper namespace.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
(function initialize() {
|
||||
if (!namespace) {
|
||||
namespace = 'VideoStorage';
|
||||
}
|
||||
if (!id) {
|
||||
// Generate random alpha-numeric string.
|
||||
id = Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
window[namespace] = window[namespace] || {};
|
||||
window[namespace][id] = window[namespace][id] || {};
|
||||
}());
|
||||
|
||||
return {
|
||||
clear: clear,
|
||||
getItem: getItem,
|
||||
removeItem: removeItem,
|
||||
setItem: setItem
|
||||
};
|
||||
};
|
||||
|
||||
export default VideoStorage;
|
||||
845
xmodule/assets/video/public/js/01_initialize.js
Normal file
845
xmodule/assets/video/public/js/01_initialize.js
Normal file
@@ -0,0 +1,845 @@
|
||||
/* 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;
|
||||
}
|
||||
132
xmodule/assets/video/public/js/025_focus_grabber.js
Normal file
132
xmodule/assets/video/public/js/025_focus_grabber.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 025_focus_grabber.js
|
||||
*
|
||||
* Purpose: Provide a way to focus on autohidden Video controls.
|
||||
*
|
||||
*
|
||||
* Because in HTML player mode we have a feature of autohiding controls on
|
||||
* mouse inactivity, sometimes focus is lost from the currently selected
|
||||
* control. What's more, when all controls are autohidden, we can't get to any
|
||||
* of them because by default browser does not place hidden elements on the
|
||||
* focus chain.
|
||||
*
|
||||
* To get around this minor annoyance, this module will manage 2 placeholder
|
||||
* elements that will be invisible to the user's eye, but visible to the
|
||||
* browser. This will allow for a sneaky stealing of focus and placing it where
|
||||
* we need (on hidden controls).
|
||||
*
|
||||
* This code has been moved to a separate module because it provides a concrete
|
||||
* block of functionality that can be turned on (off).
|
||||
*/
|
||||
|
||||
/*
|
||||
* "If you want to climb a mountain, begin at the top."
|
||||
*
|
||||
* ~ Zen saying
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// FocusGrabber module.
|
||||
let FocusGrabber = function(state) {
|
||||
let dfd = $.Deferred();
|
||||
|
||||
state.focusGrabber = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// Private functions.
|
||||
|
||||
function _makeFunctionsPublic(state) {
|
||||
let methodsDict = {
|
||||
disableFocusGrabber: disableFocusGrabber,
|
||||
enableFocusGrabber: enableFocusGrabber,
|
||||
onFocus: onFocus
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.focusGrabber, state);
|
||||
}
|
||||
|
||||
function _renderElements(state) {
|
||||
state.focusGrabber.elFirst = state.el.find('.focus_grabber.first');
|
||||
state.focusGrabber.elLast = state.el.find('.focus_grabber.last');
|
||||
|
||||
// From the start, the Focus Grabber must be disabled so that
|
||||
// tabbing (switching focus) does not land the user on one of the
|
||||
// placeholder elements (elFirst, elLast).
|
||||
state.focusGrabber.disableFocusGrabber();
|
||||
}
|
||||
|
||||
function _bindHandlers(state) {
|
||||
state.focusGrabber.elFirst.on('focus', state.focusGrabber.onFocus);
|
||||
state.focusGrabber.elLast.on('focus', state.focusGrabber.onFocus);
|
||||
|
||||
// When the video container element receives programmatic focus, then
|
||||
// on un-focus ('blur' event) we should trigger a 'mousemove' event so
|
||||
// as to reveal autohidden controls.
|
||||
state.el.on('blur', function() {
|
||||
state.el.trigger('mousemove');
|
||||
});
|
||||
}
|
||||
|
||||
// Public functions.
|
||||
|
||||
function enableFocusGrabber() {
|
||||
let tabIndex;
|
||||
|
||||
// When the Focus Grabber is being enabled, there are two different
|
||||
// scenarios:
|
||||
//
|
||||
// 1.) Currently focused element was inside the video player.
|
||||
// 2.) Currently focused element was somewhere else on the page.
|
||||
//
|
||||
// In the first case we must make sure that the video player doesn't
|
||||
// loose focus, even though the controls are autohidden.
|
||||
if ($(document.activeElement).parents().hasClass('video')) {
|
||||
tabIndex = -1;
|
||||
} else {
|
||||
tabIndex = 0;
|
||||
}
|
||||
|
||||
this.focusGrabber.elFirst.attr('tabindex', tabIndex);
|
||||
this.focusGrabber.elLast.attr('tabindex', tabIndex);
|
||||
|
||||
// Don't loose focus. We are inside video player on some control, but
|
||||
// because we can't remain focused on a hidden element, we will shift
|
||||
// focus to the main video element.
|
||||
//
|
||||
// Once the main element will receive the un-focus ('blur') event, a
|
||||
// 'mousemove' event will be triggered, and the video controls will
|
||||
// receive focus once again.
|
||||
if (tabIndex === -1) {
|
||||
this.el.focus();
|
||||
|
||||
this.focusGrabber.elFirst.attr('tabindex', 0);
|
||||
this.focusGrabber.elLast.attr('tabindex', 0);
|
||||
}
|
||||
}
|
||||
|
||||
function disableFocusGrabber() {
|
||||
// Only programmatic focusing on these elements will be available.
|
||||
// We don't want the user to focus on them (for example with the 'Tab'
|
||||
// key).
|
||||
this.focusGrabber.elFirst.attr('tabindex', -1);
|
||||
this.focusGrabber.elLast.attr('tabindex', -1);
|
||||
}
|
||||
|
||||
function onFocus(event, params) {
|
||||
// Once the Focus Grabber placeholder elements will gain focus, we will
|
||||
// trigger 'mousemove' event so that the autohidden controls will
|
||||
// become visible.
|
||||
this.el.trigger('mousemove');
|
||||
|
||||
this.focusGrabber.disableFocusGrabber();
|
||||
}
|
||||
|
||||
export default FocusGrabber;
|
||||
145
xmodule/assets/video/public/js/02_html5_hls_video.js
Normal file
145
xmodule/assets/video/public/js/02_html5_hls_video.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable no-console, no-param-reassign */
|
||||
/**
|
||||
* HTML5 video player module to support HLS video playback.
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
import HTML5Video from './02_html5_video.js';
|
||||
import HLS from 'hls';
|
||||
|
||||
let HLSVideo = {};
|
||||
|
||||
HLSVideo.Player = (function() {
|
||||
/**
|
||||
* Initialize HLS video player.
|
||||
*
|
||||
* @param {jQuery} el Reference to video player container element
|
||||
* @param {Object} config Contains common config for video player
|
||||
*/
|
||||
function Player(el, config) {
|
||||
let self = this;
|
||||
|
||||
this.config = config;
|
||||
|
||||
// do common initialization independent of player type
|
||||
this.init(el, config);
|
||||
|
||||
_.bindAll(this, 'playVideo', 'pauseVideo', 'onReady');
|
||||
|
||||
// If we have only HLS sources and browser doesn't support HLS then show error message.
|
||||
if (config.HLSOnlySources && !config.canPlayHLS) {
|
||||
this.showErrorMessage(null, '.video-hls-error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.state.el.on('initialize', _.once(function() {
|
||||
console.log('[HLS Video]: HLS Player initialized');
|
||||
self.showPlayButton();
|
||||
}));
|
||||
|
||||
// Safari has native support to play HLS videos
|
||||
if (config.browserIsSafari) {
|
||||
this.videoEl.attr('src', config.videoSources[0]);
|
||||
} else {
|
||||
// load auto start if auto_advance is enabled
|
||||
if (config.state.auto_advance) {
|
||||
this.hls = new HLS({autoStartLoad: true});
|
||||
} else {
|
||||
this.hls = new HLS({autoStartLoad: false});
|
||||
}
|
||||
this.hls.loadSource(config.videoSources[0]);
|
||||
this.hls.attachMedia(this.video);
|
||||
|
||||
this.hls.on(HLS.Events.ERROR, this.onError.bind(this));
|
||||
|
||||
this.hls.on(HLS.Events.MANIFEST_PARSED, function(event, data) {
|
||||
console.log(
|
||||
'[HLS Video]: MANIFEST_PARSED, qualityLevelsInfo: ',
|
||||
data.levels.map(function(level) {
|
||||
return {
|
||||
bitrate: level.bitrate,
|
||||
resolution: level.width + 'x' + level.height
|
||||
};
|
||||
})
|
||||
);
|
||||
self.config.onReadyHLS();
|
||||
});
|
||||
this.hls.on(HLS.Events.LEVEL_SWITCHED, function(event, data) {
|
||||
let level = self.hls.levels[data.level];
|
||||
console.log(
|
||||
'[HLS Video]: LEVEL_SWITCHED, qualityLevelInfo: ',
|
||||
{
|
||||
bitrate: level.bitrate,
|
||||
resolution: level.width + 'x' + level.height
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Player.prototype = Object.create(HTML5Video.Player.prototype);
|
||||
Player.prototype.constructor = Player;
|
||||
|
||||
Player.prototype.playVideo = function() {
|
||||
HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['show']);
|
||||
if (!this.config.browserIsSafari) {
|
||||
this.hls.startLoad();
|
||||
}
|
||||
HTML5Video.Player.prototype.playVideo.apply(this);
|
||||
};
|
||||
|
||||
Player.prototype.pauseVideo = function() {
|
||||
HTML5Video.Player.prototype.pauseVideo.apply(this);
|
||||
HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['hide']);
|
||||
};
|
||||
|
||||
Player.prototype.onPlaying = function() {
|
||||
HTML5Video.Player.prototype.onPlaying.apply(this);
|
||||
HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['hide']);
|
||||
};
|
||||
|
||||
Player.prototype.onReady = function() {
|
||||
this.config.events.onReady(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler for HLS video errors. This only takes care of fatal erros, non-fatal errors
|
||||
* are automatically handled by hls.js
|
||||
*
|
||||
* @param {String} event `hlsError`
|
||||
* @param {Object} data Contains the information regarding error occurred.
|
||||
*/
|
||||
Player.prototype.onError = function(event, data) {
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case HLS.ErrorTypes.NETWORK_ERROR:
|
||||
console.error(
|
||||
'[HLS Video]: Fatal network error encountered, try to recover. Details: %s',
|
||||
data.details
|
||||
);
|
||||
this.hls.startLoad();
|
||||
break;
|
||||
case HLS.ErrorTypes.MEDIA_ERROR:
|
||||
console.error(
|
||||
'[HLS Video]: Fatal media error encountered, try to recover. Details: %s',
|
||||
data.details
|
||||
);
|
||||
this.hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
'[HLS Video]: Unrecoverable error encountered. Details: %s',
|
||||
data.details
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Player;
|
||||
}());
|
||||
|
||||
export default HLSVideo;
|
||||
380
xmodule/assets/video/public/js/02_html5_video.js
Normal file
380
xmodule/assets/video/public/js/02_html5_video.js
Normal file
@@ -0,0 +1,380 @@
|
||||
/* eslint-disable no-console, no-param-reassign */
|
||||
/**
|
||||
* @file HTML5 video player module. Provides methods to control the in-browser
|
||||
* HTML5 video player.
|
||||
*
|
||||
* The goal was to write this module so that it closely resembles the YouTube
|
||||
* API. The main reason for this is because initially the edX video player
|
||||
* supported only YouTube videos. When HTML5 support was added, for greater
|
||||
* compatibility, and to reduce the amount of code that needed to be modified,
|
||||
* it was decided to write a similar API as the one provided by YouTube.
|
||||
*
|
||||
* @module HTML5Video
|
||||
*/
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
let HTML5Video = {};
|
||||
|
||||
HTML5Video.Player = (function() {
|
||||
/*
|
||||
* Constructor function for HTML5 Video player.
|
||||
*
|
||||
* @param {String|Object} el A DOM element where the HTML5 player will
|
||||
* be inserted (as returned by jQuery(selector) function), or a
|
||||
* selector string which will be used to select an element. This is a
|
||||
* required parameter.
|
||||
*
|
||||
* @param config - An object whose properties will be used as
|
||||
* configuration options for the HTML5 video player. This is an
|
||||
* optional parameter. In the case if this parameter is missing, or
|
||||
* some of the config object's properties are missing, defaults will be
|
||||
* used. The available options (and their defaults) are as
|
||||
* follows:
|
||||
*
|
||||
* config = {
|
||||
*
|
||||
* videoSources: [], // An array with properties being video
|
||||
* // sources. The property name is the
|
||||
* // video format of the source. Supported
|
||||
* // video formats are: 'mp4', 'webm', and
|
||||
* // 'ogg'.
|
||||
* poster: Video poster URL
|
||||
*
|
||||
* browserIsSafari: Flag to tell if current browser is Safari
|
||||
*
|
||||
* events: { // Object's properties identify the
|
||||
* // events that the API fires, and the
|
||||
* // functions (event listeners) that the
|
||||
* // API will call when those events occur.
|
||||
* // If value is null, or property is not
|
||||
* // specified, then no callback will be
|
||||
* // called for that event.
|
||||
*
|
||||
* onReady: null,
|
||||
* onStateChange: null
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
function Player(el, config) {
|
||||
let errorMessage, lastSource, sourceList;
|
||||
|
||||
// Create HTML markup for individual sources of the HTML5 <video> element.
|
||||
sourceList = $.map(config.videoSources, function(source) {
|
||||
return [
|
||||
'<source ',
|
||||
'src="', source,
|
||||
// Following hack allows to open the same video twice
|
||||
// https://code.google.com/p/chromium/issues/detail?id=31014
|
||||
// Check whether the url already has a '?' inside, and if so,
|
||||
// use '&' instead of '?' to prevent breaking the url's integrity.
|
||||
(source.indexOf('?') === -1 ? '?' : '&'),
|
||||
(new Date()).getTime(), '" />'
|
||||
].join('');
|
||||
});
|
||||
|
||||
// do common initialization independent of player type
|
||||
this.init(el, config);
|
||||
|
||||
// Create HTML markup for the <video> element, populating it with
|
||||
// sources from previous step. Set playback not supported error message.
|
||||
errorMessage = [
|
||||
gettext('This browser cannot play .mp4, .ogg, or .webm files.'),
|
||||
gettext('Try using a different browser, such as Google Chrome.')
|
||||
].join('');
|
||||
this.video.innerHTML = sourceList.join('') + errorMessage;
|
||||
|
||||
lastSource = this.videoEl.find('source').last();
|
||||
lastSource.on('error', this.showErrorMessage.bind(this));
|
||||
lastSource.on('error', this.onError.bind(this));
|
||||
this.videoEl.on('error', this.onError.bind(this));
|
||||
}
|
||||
|
||||
Player.prototype.showPlayButton = function() {
|
||||
this.videoOverlayEl.removeClass('is-hidden');
|
||||
};
|
||||
|
||||
Player.prototype.hidePlayButton = function() {
|
||||
this.videoOverlayEl.addClass('is-hidden');
|
||||
};
|
||||
|
||||
Player.prototype.showLoading = function() {
|
||||
this.el
|
||||
.removeClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.removeAttr('tabindex')
|
||||
.attr({'aria-hidden': 'false'});
|
||||
};
|
||||
|
||||
Player.prototype.hideLoading = function() {
|
||||
this.el
|
||||
.addClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.attr({'aria-hidden': 'false', tabindex: -1});
|
||||
};
|
||||
|
||||
Player.prototype.updatePlayerLoadingState = function(state) {
|
||||
if (state === 'show') {
|
||||
this.hidePlayButton();
|
||||
this.showLoading();
|
||||
} else if (state === 'hide') {
|
||||
this.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.callStateChangeCallback = function() {
|
||||
if ($.isFunction(this.config.events.onStateChange)) {
|
||||
this.config.events.onStateChange({
|
||||
data: this.playerState
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.pauseVideo = function() {
|
||||
this.video.pause();
|
||||
};
|
||||
|
||||
Player.prototype.seekTo = function(value) {
|
||||
if (
|
||||
typeof value === 'number'
|
||||
&& value <= this.video.duration
|
||||
&& value >= 0
|
||||
) {
|
||||
this.video.currentTime = value;
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.setVolume = function(value) {
|
||||
if (typeof value === 'number' && value <= 100 && value >= 0) {
|
||||
this.video.volume = value * 0.01;
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.getCurrentTime = function() {
|
||||
return this.video.currentTime;
|
||||
};
|
||||
|
||||
Player.prototype.playVideo = function() {
|
||||
this.video.play();
|
||||
};
|
||||
|
||||
Player.prototype.getPlayerState = function() {
|
||||
return this.playerState;
|
||||
};
|
||||
|
||||
Player.prototype.getVolume = function() {
|
||||
return this.video.volume;
|
||||
};
|
||||
|
||||
Player.prototype.getDuration = function() {
|
||||
if (isNaN(this.video.duration)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.video.duration;
|
||||
};
|
||||
|
||||
Player.prototype.setPlaybackRate = function(value) {
|
||||
let newSpeed = parseFloat(value);
|
||||
|
||||
if (isFinite(newSpeed)) {
|
||||
if (this.video.playbackRate !== value) {
|
||||
this.video.playbackRate = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.getAvailablePlaybackRates = function() {
|
||||
return [0.75, 1.0, 1.25, 1.5, 2.0];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
Player.prototype._getLogs = function() {
|
||||
return this.logs;
|
||||
};
|
||||
|
||||
Player.prototype.showErrorMessage = function(event, css) {
|
||||
let cssSelecter = css || '.video-player .video-error';
|
||||
this.el
|
||||
.find('.video-player div')
|
||||
.addClass('hidden')
|
||||
.end()
|
||||
.find(cssSelecter)
|
||||
.removeClass('is-hidden')
|
||||
.end()
|
||||
.addClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.attr({
|
||||
'aria-hidden': 'true',
|
||||
tabindex: -1
|
||||
});
|
||||
};
|
||||
|
||||
Player.prototype.onError = function() {
|
||||
if ($.isFunction(this.config.events.onError)) {
|
||||
this.config.events.onError();
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.destroy = function() {
|
||||
this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false);
|
||||
this.video.removeEventListener('play', this.onPlay, false);
|
||||
this.video.removeEventListener('playing', this.onPlaying, false);
|
||||
this.video.removeEventListener('pause', this.onPause, false);
|
||||
this.video.removeEventListener('ended', this.onEnded, false);
|
||||
this.el
|
||||
.find('.video-player div')
|
||||
.removeClass('is-hidden')
|
||||
.end()
|
||||
.find('.video-player .video-error')
|
||||
.addClass('is-hidden')
|
||||
.end()
|
||||
.removeClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.attr({'aria-hidden': 'false'});
|
||||
this.videoEl.off('remove');
|
||||
this.videoEl.remove();
|
||||
};
|
||||
|
||||
Player.prototype.onReady = function() {
|
||||
this.config.events.onReady(null);
|
||||
this.showPlayButton();
|
||||
};
|
||||
|
||||
Player.prototype.onLoadedMetadata = function() {
|
||||
this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
if ($.isFunction(this.config.events.onReady)) {
|
||||
this.onReady();
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.onPlay = function() {
|
||||
this.playerState = HTML5Video.PlayerState.BUFFERING;
|
||||
this.callStateChangeCallback();
|
||||
this.videoOverlayEl.addClass('is-hidden');
|
||||
};
|
||||
|
||||
Player.prototype.onPlaying = function() {
|
||||
this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
this.callStateChangeCallback();
|
||||
this.videoOverlayEl.addClass('is-hidden');
|
||||
};
|
||||
|
||||
Player.prototype.onPause = function() {
|
||||
this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
this.callStateChangeCallback();
|
||||
this.showPlayButton();
|
||||
};
|
||||
|
||||
Player.prototype.onEnded = function() {
|
||||
this.playerState = HTML5Video.PlayerState.ENDED;
|
||||
this.callStateChangeCallback();
|
||||
};
|
||||
|
||||
Player.prototype.init = function(el, config) {
|
||||
let isTouch = window.onTouchBasedDevice() || '',
|
||||
events = ['loadstart', 'progress', 'suspend', 'abort', 'error',
|
||||
'emptied', 'stalled', 'play', 'pause', 'loadedmetadata',
|
||||
'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough',
|
||||
'seeking', 'seeked', 'timeupdate', 'ended', 'ratechange',
|
||||
'durationchange', 'volumechange'
|
||||
],
|
||||
self = this,
|
||||
callback;
|
||||
|
||||
this.config = config;
|
||||
this.logs = [];
|
||||
this.el = $(el);
|
||||
|
||||
// Because of problems with creating video element via jquery
|
||||
// (http://bugs.jquery.com/ticket/9174) we create it using native JS.
|
||||
this.video = document.createElement('video');
|
||||
|
||||
// Get the jQuery object and set error event handlers
|
||||
this.videoEl = $(this.video);
|
||||
|
||||
// Video player overlay play button
|
||||
this.videoOverlayEl = this.el.find('.video-wrapper .btn-play');
|
||||
|
||||
// The player state is used by other parts of the VideoPlayer to
|
||||
// determine what the video is currently doing.
|
||||
this.playerState = HTML5Video.PlayerState.UNSTARTED;
|
||||
|
||||
_.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded');
|
||||
|
||||
// Attach a 'click' event on the <video> element. It will cause the
|
||||
// video to pause/play.
|
||||
callback = function() {
|
||||
let PlayerState = HTML5Video.PlayerState;
|
||||
|
||||
if (self.playerState === PlayerState.PLAYING) {
|
||||
self.playerState = PlayerState.PAUSED;
|
||||
self.pauseVideo();
|
||||
} else {
|
||||
self.playerState = PlayerState.PLAYING;
|
||||
self.playVideo();
|
||||
}
|
||||
};
|
||||
this.videoEl.on('click', callback);
|
||||
this.videoOverlayEl.on('click', callback);
|
||||
|
||||
this.debug = false;
|
||||
$.each(events, function(index, eventName) {
|
||||
self.video.addEventListener(eventName, function() {
|
||||
self.logs.push({
|
||||
'event name': eventName,
|
||||
state: self.playerState
|
||||
});
|
||||
|
||||
if (self.debug) {
|
||||
console.log(
|
||||
'event name:', eventName,
|
||||
'state:', self.playerState,
|
||||
'readyState:', self.video.readyState,
|
||||
'networkState:', self.video.networkState
|
||||
);
|
||||
}
|
||||
|
||||
el.trigger('html5:' + eventName, arguments);
|
||||
});
|
||||
});
|
||||
|
||||
// When the <video> tag has been processed by the browser, and it
|
||||
// is ready for playback, notify other parts of the VideoPlayer,
|
||||
// and initially pause the video.
|
||||
this.video.addEventListener('loadedmetadata', this.onLoadedMetadata, false);
|
||||
this.video.addEventListener('play', this.onPlay, false);
|
||||
this.video.addEventListener('playing', this.onPlaying, false);
|
||||
this.video.addEventListener('pause', this.onPause, false);
|
||||
this.video.addEventListener('ended', this.onEnded, false);
|
||||
|
||||
if (/iP(hone|od)/i.test(isTouch[0])) {
|
||||
this.videoEl.prop('controls', true);
|
||||
}
|
||||
|
||||
// Set video poster
|
||||
if (this.config.poster) {
|
||||
this.videoEl.prop('poster', this.config.poster);
|
||||
}
|
||||
|
||||
// Place the <video> element on the page.
|
||||
this.videoEl.appendTo(el.find('.video-player > div:first-child'));
|
||||
};
|
||||
|
||||
return Player;
|
||||
})();
|
||||
|
||||
// The YouTube API presents several constants which describe the player's
|
||||
// state at a given moment. HTML5Video API will copy these constants so
|
||||
// that code which uses both the YouTube API and this API doesn't have to
|
||||
// change.
|
||||
HTML5Video.PlayerState = {
|
||||
UNSTARTED: -1,
|
||||
ENDED: 0,
|
||||
PLAYING: 1,
|
||||
PAUSED: 2,
|
||||
BUFFERING: 3,
|
||||
CUED: 5
|
||||
};
|
||||
|
||||
export default HTML5Video;
|
||||
65
xmodule/assets/video/public/js/035_video_accessible_menu.js
Normal file
65
xmodule/assets/video/public/js/035_video_accessible_menu.js
Normal file
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
/**
|
||||
* Video Download Transcript control module.
|
||||
* @exports video/035_video_accessible_menu.js
|
||||
* @constructor
|
||||
* @param {jquery Element} element
|
||||
* @param {Object} options
|
||||
*/
|
||||
let VideoTranscriptDownloadHandler = function(element, options) {
|
||||
if (!(this instanceof VideoTranscriptDownloadHandler)) {
|
||||
return new VideoTranscriptDownloadHandler(element, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'clickHandler');
|
||||
|
||||
this.container = element;
|
||||
this.options = options || {};
|
||||
|
||||
if (this.container.find('.wrapper-downloads .wrapper-download-transcripts')) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
VideoTranscriptDownloadHandler.prototype = {
|
||||
// Initializes the module.
|
||||
initialize: function() {
|
||||
this.value = this.options.storage.getItem('transcript_download_format');
|
||||
this.el = this.container.find('.list-download-transcripts');
|
||||
this.el.on('click', '.btn-link', this.clickHandler);
|
||||
},
|
||||
|
||||
// Event handler. We delay link clicks until the file type is set
|
||||
clickHandler: function(event) {
|
||||
let that = this;
|
||||
let fileType;
|
||||
let data;
|
||||
let downloadUrl;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
fileType = $(event.target).data('value');
|
||||
data = {transcript_download_format: fileType};
|
||||
downloadUrl = $(event.target).attr('href');
|
||||
|
||||
$.ajax({
|
||||
url: this.options.saveStateUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: function() {
|
||||
that.options.storage.setItem('transcript_download_format', fileType);
|
||||
},
|
||||
complete: function() {
|
||||
document.location.href = downloadUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoTranscriptDownloadHandler;
|
||||
85
xmodule/assets/video/public/js/036_video_social_sharing.js
Normal file
85
xmodule/assets/video/public/js/036_video_social_sharing.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// eslint-disable-next-line lines-around-directive
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
/**
|
||||
* Video Social Sharing control module.
|
||||
* @exports video/036_video_social_sharing.js
|
||||
* @constructor
|
||||
* @param {jquery Element} element
|
||||
* @param {Object} options
|
||||
*/
|
||||
let VideoSocialSharingHandler = function(element, options) {
|
||||
if (!(this instanceof VideoSocialSharingHandler)) {
|
||||
return new VideoSocialSharingHandler(element, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'clickHandler');
|
||||
_.bindAll(this, 'copyHandler');
|
||||
_.bindAll(this, 'hideHandler');
|
||||
_.bindAll(this, 'showHandler');
|
||||
|
||||
this.container = element;
|
||||
|
||||
if (this.container.find('.wrapper-downloads .wrapper-social-share')) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
VideoSocialSharingHandler.prototype = {
|
||||
|
||||
// Initializes the module.
|
||||
initialize: function() {
|
||||
this.el = this.container.find('.wrapper-social-share');
|
||||
this.baseVideoUrl = this.el.data('url');
|
||||
this.course_id = this.container.data('courseId');
|
||||
this.block_id = this.container.data('blockId');
|
||||
this.el.on('click', '.social-share-link', this.clickHandler);
|
||||
|
||||
this.closeBtn = this.el.find('.close-btn');
|
||||
this.toggleBtn = this.el.find('.social-toggle-btn');
|
||||
this.copyBtn = this.el.find('.public-video-copy-btn');
|
||||
this.shareContainer = this.el.find('.container-social-share');
|
||||
this.closeBtn.on('click', this.hideHandler);
|
||||
this.toggleBtn.on('click', this.showHandler);
|
||||
this.copyBtn.on('click', this.copyHandler);
|
||||
},
|
||||
|
||||
// Fire an analytics event on share button click.
|
||||
clickHandler: function(event) {
|
||||
let source = $(event.currentTarget).data('source');
|
||||
this.sendAnalyticsEvent(source);
|
||||
},
|
||||
|
||||
hideHandler: function(event) {
|
||||
this.shareContainer.hide();
|
||||
this.toggleBtn.show();
|
||||
},
|
||||
|
||||
showHandler: function(event) {
|
||||
this.shareContainer.show();
|
||||
this.toggleBtn.hide();
|
||||
},
|
||||
|
||||
copyHandler: function(event) {
|
||||
navigator.clipboard.writeText(this.copyBtn.data('url'));
|
||||
},
|
||||
|
||||
// Send an analytics event for share button tracking.
|
||||
sendAnalyticsEvent: function(source) {
|
||||
window.analytics.track(
|
||||
'edx.social.video.share_button.clicked',
|
||||
{
|
||||
source,
|
||||
video_block_id: this.container.data('blockId'),
|
||||
course_id: this.container.data('courseId'),
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoSocialSharingHandler;
|
||||
240
xmodule/assets/video/public/js/037_video_transcript_feedback.js
Normal file
240
xmodule/assets/video/public/js/037_video_transcript_feedback.js
Normal file
@@ -0,0 +1,240 @@
|
||||
// VideoTranscriptFeedbackHandler module.
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
/**
|
||||
* @desc VideoTranscriptFeedback module exports a function.
|
||||
*
|
||||
* @type {function}
|
||||
* @access public
|
||||
*
|
||||
* @param {object} state - The object containing the state of the video
|
||||
* player. All other modules, their parameters, public variables, etc.
|
||||
* are available via this object.
|
||||
*
|
||||
* @this {object} The global window object.
|
||||
*
|
||||
*/
|
||||
|
||||
let VideoTranscriptFeedback = function(state) {
|
||||
if (!(this instanceof VideoTranscriptFeedback)) {
|
||||
return new VideoTranscriptFeedback(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'destroy', 'getFeedbackForCurrentTranscript', 'markAsPositiveFeedback', 'markAsNegativeFeedback', 'markAsEmptyFeedback',
|
||||
'selectThumbsUp', 'selectThumbsDown', 'unselectThumbsUp', 'unselectThumbsDown', 'thumbsUpClickHandler', 'thumbsDownClickHandler',
|
||||
'sendFeedbackForCurrentTranscript', 'onHideLanguageMenu', 'getCurrentLanguage', 'loadAndSetVisibility', 'showWidget', 'hideWidget'
|
||||
);
|
||||
|
||||
this.state = state;
|
||||
this.state.videoTranscriptFeedback = this;
|
||||
this.currentTranscriptLanguage = this.state.lang;
|
||||
this.transcriptLanguages = this.state.config.transcriptLanguages;
|
||||
|
||||
if (this.state.el.find('.wrapper-transcript-feedback').length) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
VideoTranscriptFeedback.prototype = {
|
||||
destroy: function () {
|
||||
this.state.el.off(this.events);
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.el = this.state.el.find('.wrapper-transcript-feedback');
|
||||
|
||||
this.videoId = this.el.data('video-id');
|
||||
this.userId = this.el.data('user-id');
|
||||
this.aiTranslationsUrl = this.state.config.aiTranslationsUrl;
|
||||
|
||||
this.thumbsUpButton = this.el.find('.thumbs-up-btn');
|
||||
this.thumbsDownButton = this.el.find('.thumbs-down-btn');
|
||||
this.thumbsUpButton.on('click', this.thumbsUpClickHandler);
|
||||
this.thumbsDownButton.on('click', this.thumbsDownClickHandler);
|
||||
|
||||
this.events = {
|
||||
'language_menu:hide': this.onHideLanguageMenu,
|
||||
destroy: this.destroy
|
||||
};
|
||||
this.loadAndSetVisibility();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function () {
|
||||
this.state.el.on(this.events);
|
||||
},
|
||||
|
||||
getFeedbackForCurrentTranscript: function () {
|
||||
let self = this;
|
||||
let url = self.aiTranslationsUrl + '/transcript-feedback' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_id=' + self.videoId + '&user_id=' + self.userId;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
success: function (data) {
|
||||
if (data && data.value === true) {
|
||||
self.markAsPositiveFeedback();
|
||||
self.currentFeedback = true;
|
||||
} else {
|
||||
if (data && data.value === false) {
|
||||
self.markAsNegativeFeedback();
|
||||
self.currentFeedback = false;
|
||||
} else {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function (error) {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
markAsPositiveFeedback: function () {
|
||||
this.selectThumbsUp();
|
||||
this.unselectThumbsDown();
|
||||
},
|
||||
|
||||
markAsNegativeFeedback: function () {
|
||||
this.selectThumbsDown();
|
||||
this.unselectThumbsUp();
|
||||
},
|
||||
|
||||
markAsEmptyFeedback: function () {
|
||||
this.unselectThumbsUp();
|
||||
this.unselectThumbsDown();
|
||||
},
|
||||
|
||||
selectThumbsUp: function () {
|
||||
let thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
|
||||
if (thumbsUpIcon[0].classList.contains('fa-thumbs-o-up')) {
|
||||
thumbsUpIcon[0].classList.remove("fa-thumbs-o-up");
|
||||
thumbsUpIcon[0].classList.add("fa-thumbs-up");
|
||||
}
|
||||
},
|
||||
|
||||
selectThumbsDown: function () {
|
||||
let thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
|
||||
if (thumbsDownIcon[0].classList.contains('fa-thumbs-o-down')) {
|
||||
thumbsDownIcon[0].classList.remove("fa-thumbs-o-down");
|
||||
thumbsDownIcon[0].classList.add("fa-thumbs-down");
|
||||
}
|
||||
},
|
||||
|
||||
unselectThumbsUp: function () {
|
||||
let thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
|
||||
if (thumbsUpIcon[0].classList.contains('fa-thumbs-up')) {
|
||||
thumbsUpIcon[0].classList.remove("fa-thumbs-up");
|
||||
thumbsUpIcon[0].classList.add("fa-thumbs-o-up");
|
||||
}
|
||||
},
|
||||
|
||||
unselectThumbsDown: function () {
|
||||
let thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
|
||||
if (thumbsDownIcon[0].classList.contains('fa-thumbs-down')) {
|
||||
thumbsDownIcon[0].classList.remove("fa-thumbs-down");
|
||||
thumbsDownIcon[0].classList.add("fa-thumbs-o-down");
|
||||
}
|
||||
},
|
||||
|
||||
thumbsUpClickHandler: function () {
|
||||
if (this.currentFeedback) {
|
||||
this.sendFeedbackForCurrentTranscript(null);
|
||||
} else {
|
||||
this.sendFeedbackForCurrentTranscript(true);
|
||||
}
|
||||
},
|
||||
|
||||
thumbsDownClickHandler: function () {
|
||||
if (this.currentFeedback === false) {
|
||||
this.sendFeedbackForCurrentTranscript(null);
|
||||
} else {
|
||||
this.sendFeedbackForCurrentTranscript(false);
|
||||
}
|
||||
},
|
||||
|
||||
sendFeedbackForCurrentTranscript: function (feedbackValue) {
|
||||
let self = this;
|
||||
let url = self.aiTranslationsUrl + '/transcript-feedback/';
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
transcript_language: self.currentTranscriptLanguage,
|
||||
video_id: self.videoId,
|
||||
user_id: self.userId,
|
||||
value: feedbackValue,
|
||||
},
|
||||
success: function (data) {
|
||||
if (data && data.value === true) {
|
||||
self.markAsPositiveFeedback();
|
||||
self.currentFeedback = true;
|
||||
} else {
|
||||
if (data && data.value === false) {
|
||||
self.markAsNegativeFeedback();
|
||||
self.currentFeedback = false;
|
||||
} else {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onHideLanguageMenu: function () {
|
||||
let newLanguageSelected = this.getCurrentLanguage();
|
||||
if (this.currentTranscriptLanguage !== newLanguageSelected) {
|
||||
this.currentTranscriptLanguage = this.getCurrentLanguage();
|
||||
this.loadAndSetVisibility();
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentLanguage: function () {
|
||||
let language = this.state.lang;
|
||||
return language;
|
||||
},
|
||||
|
||||
loadAndSetVisibility: function () {
|
||||
let self = this;
|
||||
let url = self.aiTranslationsUrl + '/video-transcript' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_id=' + self.videoId;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
async: false,
|
||||
success: function (data) {
|
||||
if (data && data.status === 'Completed') {
|
||||
self.showWidget();
|
||||
self.getFeedbackForCurrentTranscript();
|
||||
} else {
|
||||
self.hideWidget();
|
||||
}
|
||||
},
|
||||
error: function (error) {
|
||||
self.hideWidget();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showWidget: function () {
|
||||
this.el.show();
|
||||
},
|
||||
|
||||
hideWidget: function () {
|
||||
this.el.hide();
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoTranscriptFeedback;
|
||||
911
xmodule/assets/video/public/js/03_video_player.js
Normal file
911
xmodule/assets/video/public/js/03_video_player.js
Normal file
@@ -0,0 +1,911 @@
|
||||
/* eslint-disable no-console, no-param-reassign */
|
||||
import HTML5Video from './02_html5_video.js';
|
||||
import HTML5HLSVideo from './02_html5_hls_video.js';
|
||||
import Resizer from './00_resizer.js';
|
||||
import HLS from 'hls';
|
||||
import _ from 'underscore';
|
||||
import * as Time from './utils/time.js';
|
||||
|
||||
let dfd = $.Deferred();
|
||||
|
||||
let VideoPlayer = function(state) {
|
||||
state.videoPlayer = {};
|
||||
_makeFunctionsPublic(state);
|
||||
_initialize(state);
|
||||
// No callbacks to DOM events (click, mousemove, etc.).
|
||||
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
let methodsDict = {
|
||||
destroy: destroy,
|
||||
duration: duration,
|
||||
handlePlaybackQualityChange: handlePlaybackQualityChange,
|
||||
|
||||
// Added for finer graded seeking control.
|
||||
// Please see:
|
||||
// https://developers.google.com/youtube/js_api_reference#Events
|
||||
isBuffering: isBuffering,
|
||||
// https://developers.google.com/youtube/js_api_reference#cueVideoById
|
||||
isCued: isCued,
|
||||
|
||||
isEnded: isEnded,
|
||||
isPlaying: isPlaying,
|
||||
isUnstarted: isUnstarted,
|
||||
onCaptionSeek: onSeek,
|
||||
onEnded: onEnded,
|
||||
onError: onError,
|
||||
onPause: onPause,
|
||||
onPlay: onPlay,
|
||||
runTimer: runTimer,
|
||||
stopTimer: stopTimer,
|
||||
onLoadMetadataHtml5: onLoadMetadataHtml5,
|
||||
onPlaybackQualityChange: onPlaybackQualityChange,
|
||||
onReady: onReady,
|
||||
onSlideSeek: onSeek,
|
||||
onSpeedChange: onSpeedChange,
|
||||
onAutoAdvanceChange: onAutoAdvanceChange,
|
||||
onStateChange: onStateChange,
|
||||
onUnstarted: onUnstarted,
|
||||
onVolumeChange: onVolumeChange,
|
||||
pause: pause,
|
||||
play: play,
|
||||
seekTo: seekTo,
|
||||
setPlaybackRate: setPlaybackRate,
|
||||
update: update,
|
||||
figureOutStartEndTime: figureOutStartEndTime,
|
||||
figureOutStartingTime: figureOutStartingTime,
|
||||
updatePlayTime: updatePlayTime
|
||||
};
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
VideoPlayer.prototype = methodsDict;
|
||||
|
||||
export default VideoPlayer;
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called,
|
||||
// these functions will get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
let debouncedF = _.debounce(
|
||||
function(params) {
|
||||
// Can't cancel a queued debounced function on destroy
|
||||
if (state.videoPlayer) {
|
||||
return onSeek.call(this, params);
|
||||
}
|
||||
}.bind(state),
|
||||
300
|
||||
);
|
||||
|
||||
state.bindTo(methodsDict, state.videoPlayer, state);
|
||||
|
||||
state.videoPlayer.onSlideSeek = debouncedF;
|
||||
state.videoPlayer.onCaptionSeek = debouncedF;
|
||||
}
|
||||
|
||||
// Updates players state, once metadata is loaded for html5 player.
|
||||
function onLoadMetadataHtml5() {
|
||||
let player = this.videoPlayer.player.videoEl;
|
||||
let videoWidth = player[0].videoWidth || player.width();
|
||||
let videoHeight = player[0].videoHeight || player.height();
|
||||
|
||||
_resize(this, videoWidth, videoHeight);
|
||||
_updateVcrAndRegion(this);
|
||||
}
|
||||
|
||||
// function _initialize(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.
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
function _initialize(state) {
|
||||
let youTubeId;
|
||||
let player;
|
||||
let userAgent;
|
||||
let commonPlayerConfig;
|
||||
let eventToBeTriggered = 'loadedmetadata';
|
||||
|
||||
// The function is called just once to apply pre-defined configurations
|
||||
// by student before video starts playing. Waits until the video's
|
||||
// metadata is loaded, which normally happens just after the video
|
||||
// starts playing. Just after that configurations can be applied.
|
||||
state.videoPlayer.ready = _.once(function() {
|
||||
if (!state.isFlashMode() && state.speed != '1.0') {
|
||||
state.videoPlayer.setPlaybackRate(state.speed);
|
||||
}
|
||||
});
|
||||
|
||||
if (state.isYoutubeType()) {
|
||||
state.videoPlayer.PlayerState = YT.PlayerState;
|
||||
state.videoPlayer.PlayerState.UNSTARTED = -1;
|
||||
} else {
|
||||
state.videoPlayer.PlayerState = HTML5Video.PlayerState;
|
||||
}
|
||||
|
||||
state.videoPlayer.currentTime = 0;
|
||||
|
||||
state.videoPlayer.goToStartTime = true;
|
||||
state.videoPlayer.stopAtEndTime = true;
|
||||
|
||||
state.videoPlayer.playerVars = {
|
||||
controls: 0,
|
||||
wmode: 'transparent',
|
||||
rel: 0,
|
||||
showinfo: 0,
|
||||
enablejsapi: 1,
|
||||
modestbranding: 1,
|
||||
cc_load_policy: 0
|
||||
};
|
||||
|
||||
if (!state.isFlashMode()) {
|
||||
state.videoPlayer.playerVars.html5 = 1;
|
||||
}
|
||||
|
||||
// Detect the current browser for several browser-specific work-arounds.
|
||||
userAgent = navigator.userAgent.toLowerCase();
|
||||
state.browserIsFirefox = userAgent.indexOf('firefox') > -1;
|
||||
state.browserIsChrome = userAgent.indexOf('chrome') > -1;
|
||||
// Chrome includes both "Chrome" and "Safari" in the user agent.
|
||||
state.browserIsSafari = (userAgent.indexOf('safari') > -1
|
||||
&& !state.browserIsChrome);
|
||||
|
||||
// Browser can play HLS videos if either `Media Source Extensions`
|
||||
// feature is supported or browser is safari (native HLS support)
|
||||
state.canPlayHLS = state.HLSVideoSources.length > 0 && (HLS.isSupported() || state.browserIsSafari);
|
||||
state.HLSOnlySources = state.config.sources.length > 0
|
||||
&& state.config.sources.length === state.HLSVideoSources.length;
|
||||
|
||||
commonPlayerConfig = {
|
||||
playerVars: state.videoPlayer.playerVars,
|
||||
videoSources: state.config.sources,
|
||||
poster: state.config.poster,
|
||||
browserIsSafari: state.browserIsSafari,
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
};
|
||||
|
||||
if (state.videoType === 'html5') {
|
||||
if (state.canPlayHLS || state.HLSOnlySources) {
|
||||
state.videoPlayer.player = new HTML5HLSVideo.Player(
|
||||
state.el,
|
||||
_.extend({}, commonPlayerConfig, {
|
||||
state: state,
|
||||
onReadyHLS: function() { dfd.resolve(); },
|
||||
videoSources: state.HLSVideoSources,
|
||||
canPlayHLS: state.canPlayHLS,
|
||||
HLSOnlySources: state.HLSOnlySources
|
||||
})
|
||||
);
|
||||
// `loadedmetadata` event triggered too early on Safari due
|
||||
// to which correct video dimensions were not calculated
|
||||
eventToBeTriggered = state.browserIsSafari ? 'loadeddata' : eventToBeTriggered;
|
||||
} else {
|
||||
state.videoPlayer.player = new HTML5Video.Player(state.el, commonPlayerConfig);
|
||||
}
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
player = state.videoEl = state.videoPlayer.player.videoEl;
|
||||
player[0].addEventListener(eventToBeTriggered, state.videoPlayer.onLoadMetadataHtml5, false);
|
||||
player.on('remove', state.videoPlayer.destroy);
|
||||
} else {
|
||||
youTubeId = state.youtubeId();
|
||||
|
||||
state.videoPlayer.player = new YT.Player(state.id, {
|
||||
playerVars: state.videoPlayer.playerVars,
|
||||
videoId: youTubeId,
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
});
|
||||
|
||||
state.el.on('initialize', function() {
|
||||
// eslint-disable-next-line no-shadow, no-multi-assign
|
||||
let player = state.videoEl = state.el.find('iframe');
|
||||
let videoWidth = player.attr('width') || player.width();
|
||||
let videoHeight = player.attr('height') || player.height();
|
||||
|
||||
player.on('remove', state.videoPlayer.destroy);
|
||||
|
||||
_resize(state, videoWidth, videoHeight);
|
||||
_updateVcrAndRegion(state, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (state.isTouch) {
|
||||
dfd.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function _updateVcrAndRegion(state, isYoutube) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
let update = function(state) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
let duration = state.videoPlayer.duration();
|
||||
let time = state.videoPlayer.figureOutStartingTime(duration);
|
||||
|
||||
// Update the VCR.
|
||||
state.trigger(
|
||||
'videoControl.updateVcrVidTime',
|
||||
{
|
||||
time: time,
|
||||
duration: duration
|
||||
}
|
||||
);
|
||||
|
||||
// Update the time slider.
|
||||
state.trigger(
|
||||
'videoProgressSlider.updateStartEndTimeRegion',
|
||||
{
|
||||
duration: duration
|
||||
}
|
||||
);
|
||||
state.trigger(
|
||||
'videoProgressSlider.updatePlayTime',
|
||||
{
|
||||
time: time,
|
||||
duration: duration
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// After initialization, update the VCR with total time.
|
||||
// At this point only the metadata duration is available (not
|
||||
// very precise), but it is better than having 00:00:00 for
|
||||
// total time.
|
||||
if (state.youtubeMetadataReceived || !isYoutube) {
|
||||
// Metadata was already received, and is available.
|
||||
update(state);
|
||||
} else {
|
||||
// We wait for metadata to arrive, before we request the update
|
||||
// of the VCR video time, and of the start-end time region.
|
||||
// Metadata contains duration of the video.
|
||||
state.el.on('metadata_received', function() {
|
||||
update(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _resize(state, videoWidth, videoHeight) {
|
||||
state.resizer = new Resizer({
|
||||
element: state.videoEl,
|
||||
elementRatio: videoWidth / videoHeight,
|
||||
container: state.container
|
||||
})
|
||||
.callbacks.once(function() {
|
||||
state.el.trigger('caption:resize');
|
||||
})
|
||||
.setMode('width');
|
||||
|
||||
// Update captions size when controls becomes visible on iPad or Android
|
||||
if (/iPad|Android/i.test(state.isTouch[0])) {
|
||||
state.el.on('controls:show', function() {
|
||||
state.el.trigger('caption:resize');
|
||||
});
|
||||
}
|
||||
|
||||
$(window).on('resize.video', _.debounce(function() {
|
||||
state.trigger('videoFullScreen.updateControlsHeight', null);
|
||||
state.el.trigger('caption:resize');
|
||||
state.resizer.align();
|
||||
}, 100));
|
||||
}
|
||||
|
||||
// function _restartUsingFlash(state)
|
||||
//
|
||||
// When we are about to play a YouTube video in HTML5 mode and discover
|
||||
// that we only have one available playback rate, we will switch to
|
||||
// Flash mode. In Flash speed switching is done by reloading videos
|
||||
// recorded at different frame rates.
|
||||
function _restartUsingFlash(state) {
|
||||
// Remove from the page current iFrame with HTML5 video.
|
||||
state.videoPlayer.player.destroy();
|
||||
|
||||
state.setPlayerMode('flash');
|
||||
|
||||
console.log('[Video info]: Changing YouTube player mode to "flash".');
|
||||
|
||||
// Removed configuration option that requests the HTML5 mode.
|
||||
delete state.videoPlayer.playerVars.html5;
|
||||
|
||||
// Request for the creation of a new Flash player
|
||||
state.videoPlayer.player = new YT.Player(state.id, {
|
||||
playerVars: state.videoPlayer.playerVars,
|
||||
videoId: state.youtubeId(),
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
});
|
||||
|
||||
_updateVcrAndRegion(state, true);
|
||||
state.el.trigger('caption:fetch');
|
||||
state.resizer.setElement(state.el.find('iframe')).align();
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 destroy() {
|
||||
let player = this.videoPlayer.player;
|
||||
this.el.removeClass([
|
||||
'is-unstarted', 'is-playing', 'is-paused', 'is-buffered',
|
||||
'is-ended', 'is-cued'
|
||||
].join(' '));
|
||||
$(window).off('.video');
|
||||
this.el.trigger('destroy');
|
||||
this.el.off();
|
||||
this.videoPlayer.stopTimer();
|
||||
if (this.resizer && this.resizer.destroy) {
|
||||
this.resizer.destroy();
|
||||
}
|
||||
if (player && player.video) {
|
||||
player.video.removeEventListener('loadedmetadata', this.videoPlayer.onLoadMetadataHtml5, false);
|
||||
}
|
||||
if (player && _.isFunction(player.destroy)) {
|
||||
player.destroy();
|
||||
}
|
||||
if (this.canPlayHLS && player.hls) {
|
||||
player.hls.destroy();
|
||||
}
|
||||
delete this.videoPlayer;
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (this.videoPlayer.player.pauseVideo) {
|
||||
this.videoPlayer.player.pauseVideo();
|
||||
}
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (this.videoPlayer.player.playVideo) {
|
||||
if (this.videoPlayer.isEnded()) {
|
||||
// When the video will start playing again from the start, the
|
||||
// start-time and end-time will come back into effect.
|
||||
this.videoPlayer.goToStartTime = true;
|
||||
}
|
||||
|
||||
this.videoPlayer.player.playVideo();
|
||||
}
|
||||
}
|
||||
|
||||
// This function gets the video's current play position in time
|
||||
// (currentTime) and its duration.
|
||||
// It is called at a regular interval when the video is playing.
|
||||
function update(time) {
|
||||
this.videoPlayer.currentTime = time || this.videoPlayer.player.getCurrentTime();
|
||||
|
||||
if (isFinite(this.videoPlayer.currentTime)) {
|
||||
this.videoPlayer.updatePlayTime(this.videoPlayer.currentTime);
|
||||
|
||||
// We need to pause the video if current time is smaller (or equal)
|
||||
// than end-time. Also, we must make sure that this is only done
|
||||
// once per video playing from start to end.
|
||||
if (
|
||||
this.videoPlayer.endTime !== null
|
||||
&& this.videoPlayer.endTime <= this.videoPlayer.currentTime
|
||||
) {
|
||||
this.videoPlayer.pause();
|
||||
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: true
|
||||
});
|
||||
|
||||
this.el.trigger('stop');
|
||||
}
|
||||
this.el.trigger('timeupdate', [this.videoPlayer.currentTime]);
|
||||
}
|
||||
}
|
||||
|
||||
function setPlaybackRate(newSpeed) {
|
||||
this.videoPlayer.player.setPlaybackRate(newSpeed);
|
||||
}
|
||||
|
||||
function onSpeedChange(newSpeed) {
|
||||
let time = this.videoPlayer.currentTime;
|
||||
|
||||
if (this.isFlashMode()) {
|
||||
this.videoPlayer.currentTime = Time.convert(
|
||||
time,
|
||||
parseFloat(this.speed),
|
||||
newSpeed
|
||||
);
|
||||
}
|
||||
|
||||
newSpeed = parseFloat(newSpeed);
|
||||
this.setSpeed(newSpeed);
|
||||
this.videoPlayer.setPlaybackRate(newSpeed);
|
||||
}
|
||||
|
||||
function onAutoAdvanceChange(enabled) {
|
||||
this.setAutoAdvance(enabled);
|
||||
}
|
||||
|
||||
// Every 200 ms, if the video is playing, we call the function update, via
|
||||
// clearInterval. This interval is called updateInterval.
|
||||
// It is created on a onPlay event. Cleared on a onPause event.
|
||||
// Reinitialized on a onSeek event.
|
||||
function onSeek(params) {
|
||||
let time = params.time,
|
||||
type = params.type,
|
||||
oldTime = this.videoPlayer.currentTime;
|
||||
// After the user seeks, the video will start playing from
|
||||
// the sought point, and stop playing at the end.
|
||||
this.videoPlayer.goToStartTime = false;
|
||||
|
||||
this.videoPlayer.seekTo(time);
|
||||
|
||||
this.el.trigger('seek', [time, oldTime, type]);
|
||||
}
|
||||
|
||||
function seekTo(time) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
let duration = this.videoPlayer.duration();
|
||||
|
||||
if ((typeof time !== 'number') || (time > duration) || (time < 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el.off('play.seek');
|
||||
|
||||
if (this.videoPlayer.isPlaying()) {
|
||||
this.videoPlayer.stopTimer();
|
||||
}
|
||||
let isUnplayed = this.videoPlayer.isUnstarted()
|
||||
|| this.videoPlayer.isCued();
|
||||
|
||||
// Use `cueVideoById` method for youtube video that is not played before.
|
||||
if (isUnplayed && this.isYoutubeType()) {
|
||||
this.videoPlayer.player.cueVideoById(this.youtubeId(), time);
|
||||
} else {
|
||||
// Youtube video cannot be rewinded during bufferization, so wait to
|
||||
// finish bufferization and then rewind the video.
|
||||
if (this.isYoutubeType() && this.videoPlayer.isBuffering()) {
|
||||
this.el.on('play.seek', function() {
|
||||
this.videoPlayer.player.seekTo(time, true);
|
||||
}.bind(this));
|
||||
} else {
|
||||
// Otherwise, just seek the video
|
||||
this.videoPlayer.player.seekTo(time, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.videoPlayer.updatePlayTime(time, true);
|
||||
|
||||
// the timer is stopped above; restart it.
|
||||
if (this.videoPlayer.isPlaying()) {
|
||||
this.videoPlayer.runTimer();
|
||||
}
|
||||
// Update the the current time when user seek. (YoutubePlayer)
|
||||
this.videoPlayer.currentTime = time;
|
||||
}
|
||||
|
||||
function runTimer() {
|
||||
if (!this.videoPlayer.updateInterval) {
|
||||
this.videoPlayer.updateInterval = window.setInterval(
|
||||
this.videoPlayer.update, 200
|
||||
);
|
||||
|
||||
this.videoPlayer.update();
|
||||
}
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
window.clearInterval(this.videoPlayer.updateInterval);
|
||||
delete this.videoPlayer.updateInterval;
|
||||
}
|
||||
|
||||
function onEnded() {
|
||||
let time = this.videoPlayer.duration();
|
||||
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: true
|
||||
});
|
||||
|
||||
if (this.videoPlayer.skipOnEndedStartEndReset) {
|
||||
this.videoPlayer.skipOnEndedStartEndReset = undefined;
|
||||
}
|
||||
// Sometimes `onEnded` events fires when `currentTime` not equal
|
||||
// `duration`. In this case, slider doesn't reach the end point of
|
||||
// timeline.
|
||||
this.videoPlayer.updatePlayTime(time);
|
||||
|
||||
// Emit 'pause_video' event when a video ends if Player is of Youtube
|
||||
if (this.isYoutubeType()) {
|
||||
this.el.trigger('pause', arguments);
|
||||
}
|
||||
this.el.trigger('ended', arguments);
|
||||
}
|
||||
|
||||
function onPause() {
|
||||
this.videoPlayer.stopTimer();
|
||||
this.el.trigger('pause', arguments);
|
||||
}
|
||||
|
||||
function onPlay() {
|
||||
this.videoPlayer.runTimer();
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: false
|
||||
});
|
||||
this.videoPlayer.ready();
|
||||
this.el.trigger('play', arguments);
|
||||
}
|
||||
|
||||
function onUnstarted() { }
|
||||
|
||||
function handlePlaybackQualityChange(value) {
|
||||
this.videoPlayer.player.setPlaybackQuality(value);
|
||||
}
|
||||
|
||||
function onPlaybackQualityChange() {
|
||||
let quality;
|
||||
|
||||
quality = this.videoPlayer.player.getPlaybackQuality();
|
||||
|
||||
this.trigger('videoQualityControl.onQualityChange', quality);
|
||||
this.el.trigger('qualitychange', arguments);
|
||||
}
|
||||
|
||||
function onReady() {
|
||||
let _this = this,
|
||||
availablePlaybackRates, baseSpeedSubs,
|
||||
player, videoWidth, videoHeight;
|
||||
|
||||
dfd.resolve();
|
||||
|
||||
this.el.on('speedchange', function(event, speed) {
|
||||
_this.videoPlayer.onSpeedChange(speed);
|
||||
});
|
||||
|
||||
this.el.on('autoadvancechange', function(event, enabled) {
|
||||
_this.videoPlayer.onAutoAdvanceChange(enabled);
|
||||
});
|
||||
|
||||
this.el.on('volumechange volumechange:silent', function(event, volume) {
|
||||
_this.videoPlayer.onVolumeChange(volume);
|
||||
});
|
||||
|
||||
availablePlaybackRates = this.videoPlayer.player
|
||||
.getAvailablePlaybackRates();
|
||||
|
||||
// Because of problems with muting sound outside of range 0.25 and
|
||||
// 5.0, we should filter our available playback rates.
|
||||
// Issues:
|
||||
// https://code.google.com/p/chromium/issues/detail?id=264341
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=840745
|
||||
// https://developer.mozilla.org/en-US/docs/DOM/HTMLMediaElement
|
||||
|
||||
availablePlaybackRates = _.filter(
|
||||
availablePlaybackRates,
|
||||
function(item) {
|
||||
let speed = Number(item);
|
||||
return speed > 0.25 && speed <= 5;
|
||||
}
|
||||
);
|
||||
|
||||
// Because of a recent change in the YouTube API (not documented), sometimes
|
||||
// HTML5 mode loads after Flash mode has been loaded. In this case we have
|
||||
// multiple speeds available but the variable `this.currentPlayerMode` is
|
||||
// set to "flash". This is impossible because in Flash mode we can have
|
||||
// only one speed available. Therefore we must execute the following code
|
||||
// block if we have multiple speeds or if `this.currentPlayerMode` is set to
|
||||
// "html5". If any of the two conditions are true, we then set the variable
|
||||
// `this.currentPlayerMode` to "html5".
|
||||
//
|
||||
// For more information, please see the PR that introduced this change:
|
||||
// https://github.com/openedx/edx-platform/pull/2841
|
||||
if (
|
||||
(this.isHtml5Mode() || availablePlaybackRates.length > 1)
|
||||
&& this.isYoutubeType()
|
||||
) {
|
||||
if (availablePlaybackRates.length === 1 && !this.isTouch) {
|
||||
// This condition is needed in cases when Firefox version is
|
||||
// less than 20. In those versions HTML5 playback could only
|
||||
// happen at 1 speed (no speed changing). Therefore, in this
|
||||
// case, we need to switch back to Flash.
|
||||
//
|
||||
// This might also happen in other browsers, therefore when we
|
||||
// have 1 speed available, we fall back to Flash.
|
||||
|
||||
_restartUsingFlash(this);
|
||||
return false;
|
||||
} else if (availablePlaybackRates.length > 1) {
|
||||
this.setPlayerMode('html5');
|
||||
|
||||
// We need to synchronize available frame rates with the ones
|
||||
// that the user specified.
|
||||
|
||||
baseSpeedSubs = this.videos['1.0'];
|
||||
// this.videos is a dictionary containing various frame rates
|
||||
// and their associated subs.
|
||||
|
||||
// First clear the dictionary.
|
||||
$.each(this.videos, function(index, value) {
|
||||
delete _this.videos[index];
|
||||
});
|
||||
this.speeds = [];
|
||||
// Recreate it with the supplied frame rates.
|
||||
$.each(availablePlaybackRates, function(index, value) {
|
||||
let key = value.toFixed(2).replace(/\.00$/, '.0');
|
||||
|
||||
_this.videos[key] = baseSpeedSubs;
|
||||
_this.speeds.push(key);
|
||||
});
|
||||
|
||||
this.setSpeed(this.speed);
|
||||
this.el.trigger('speed:render', [this.speeds, this.speed]);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isFlashMode()) {
|
||||
this.setSpeed(this.speed);
|
||||
this.el.trigger('speed:set', [this.speed]);
|
||||
}
|
||||
|
||||
if (this.isHtml5Mode()) {
|
||||
this.videoPlayer.player.setPlaybackRate(this.speed);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
let duration = this.videoPlayer.duration(),
|
||||
time = this.videoPlayer.figureOutStartingTime(duration);
|
||||
|
||||
// this.duration will be set initially only if duration is coming from edx-val
|
||||
this.duration = this.duration || duration;
|
||||
|
||||
if (time > 0 && this.videoPlayer.goToStartTime) {
|
||||
this.videoPlayer.seekTo(time);
|
||||
}
|
||||
|
||||
this.el.trigger('ready', arguments);
|
||||
|
||||
if (this.config.autoplay) {
|
||||
this.videoPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
function onStateChange(event) {
|
||||
this.el.removeClass([
|
||||
'is-unstarted', 'is-playing', 'is-paused', 'is-buffered',
|
||||
'is-ended', 'is-cued'
|
||||
].join(' '));
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.data) {
|
||||
case this.videoPlayer.PlayerState.UNSTARTED:
|
||||
this.el.addClass('is-unstarted');
|
||||
this.videoPlayer.onUnstarted();
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.PLAYING:
|
||||
this.el.addClass('is-playing');
|
||||
this.videoPlayer.onPlay();
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.PAUSED:
|
||||
this.el.addClass('is-paused');
|
||||
this.videoPlayer.onPause();
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.BUFFERING:
|
||||
this.el.addClass('is-buffered');
|
||||
this.el.trigger('buffering');
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.ENDED:
|
||||
this.el.addClass('is-ended');
|
||||
this.videoPlayer.onEnded();
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.CUED:
|
||||
this.el.addClass('is-cued');
|
||||
if (this.isFlashMode()) {
|
||||
this.videoPlayer.play();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function onError(code) {
|
||||
this.el.trigger('error', [code]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
function figureOutStartEndTime(duration) {
|
||||
let videoPlayer = this.videoPlayer;
|
||||
|
||||
videoPlayer.startTime = this.config.startTime;
|
||||
if (videoPlayer.startTime >= duration) {
|
||||
videoPlayer.startTime = 0;
|
||||
} else if (this.isFlashMode()) {
|
||||
videoPlayer.startTime /= Number(this.speed);
|
||||
}
|
||||
|
||||
videoPlayer.endTime = this.config.endTime;
|
||||
if (
|
||||
videoPlayer.endTime <= videoPlayer.startTime
|
||||
|| videoPlayer.endTime >= duration
|
||||
) {
|
||||
videoPlayer.endTime = null;
|
||||
} else if (this.isFlashMode()) {
|
||||
videoPlayer.endTime /= Number(this.speed);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
function figureOutStartingTime(duration) {
|
||||
let savedVideoPosition = this.config.savedVideoPosition,
|
||||
|
||||
// Default starting time is 0. This is the case when
|
||||
// there is not start-time, no previously saved position,
|
||||
// or one (or both) of those values is incorrect.
|
||||
time = 0,
|
||||
|
||||
startTime, endTime;
|
||||
|
||||
this.videoPlayer.figureOutStartEndTime(duration);
|
||||
|
||||
startTime = this.videoPlayer.startTime;
|
||||
endTime = this.videoPlayer.endTime;
|
||||
|
||||
if (startTime > 0) {
|
||||
if (
|
||||
startTime < savedVideoPosition
|
||||
&& (endTime > savedVideoPosition || endTime === null)
|
||||
|
||||
// We do not want to jump to the end of the video.
|
||||
// We subtract 1 from the duration for a 1 second
|
||||
// safety net.
|
||||
&& savedVideoPosition < duration - 1
|
||||
) {
|
||||
time = savedVideoPosition;
|
||||
} else {
|
||||
time = startTime;
|
||||
}
|
||||
} else if (
|
||||
savedVideoPosition > 0
|
||||
&& (endTime > savedVideoPosition || endTime === null)
|
||||
|
||||
// We do not want to jump to the end of the video.
|
||||
// We subtract 1 from the duration for a 1 second
|
||||
// safety net.
|
||||
&& savedVideoPosition < duration - 1
|
||||
) {
|
||||
time = savedVideoPosition;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
function updatePlayTime(time, skip_seek) {
|
||||
let videoPlayer = this.videoPlayer,
|
||||
endTime = this.videoPlayer.duration(),
|
||||
youTubeId;
|
||||
|
||||
if (this.config.endTime) {
|
||||
endTime = Math.min(this.config.endTime, endTime);
|
||||
}
|
||||
|
||||
this.trigger(
|
||||
'videoProgressSlider.updatePlayTime',
|
||||
{
|
||||
time: time,
|
||||
duration: endTime
|
||||
}
|
||||
);
|
||||
|
||||
this.trigger(
|
||||
'videoControl.updateVcrVidTime',
|
||||
{
|
||||
time: time,
|
||||
duration: endTime
|
||||
}
|
||||
);
|
||||
|
||||
this.el.trigger('caption:update', [time]);
|
||||
}
|
||||
|
||||
function isEnded() {
|
||||
let playerState = this.videoPlayer.player.getPlayerState(),
|
||||
ENDED = this.videoPlayer.PlayerState.ENDED;
|
||||
|
||||
return playerState === ENDED;
|
||||
}
|
||||
|
||||
function isPlaying() {
|
||||
let playerState = this.videoPlayer.player.getPlayerState();
|
||||
|
||||
return playerState === this.videoPlayer.PlayerState.PLAYING;
|
||||
}
|
||||
|
||||
function isBuffering() {
|
||||
let playerState = this.videoPlayer.player.getPlayerState();
|
||||
|
||||
return playerState === this.videoPlayer.PlayerState.BUFFERING;
|
||||
}
|
||||
|
||||
function isCued() {
|
||||
let playerState = this.videoPlayer.player.getPlayerState();
|
||||
|
||||
return playerState === this.videoPlayer.PlayerState.CUED;
|
||||
}
|
||||
|
||||
function isUnstarted() {
|
||||
let playerState = this.videoPlayer.player.getPlayerState();
|
||||
|
||||
return playerState === this.videoPlayer.PlayerState.UNSTARTED;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the duration of the video in seconds.
|
||||
*
|
||||
* First, try to use the native player API call to get the duration.
|
||||
* If the value returned by the native function is not valid, resort to
|
||||
* the value stored in the metadata for the video. Note that the metadata
|
||||
* is available only for YouTube videos.
|
||||
*
|
||||
* IMPORTANT! It has been observed that sometimes, after initial playback
|
||||
* of the video, when operations "pause" and "play" are performed (in that
|
||||
* sequence), the function will start returning a slightly different value.
|
||||
*
|
||||
* For example: While playing for the first time, the function returns 31.
|
||||
* After pausing the video and then resuming once more, the function will
|
||||
* start returning 31.950656.
|
||||
*
|
||||
* This instability is internal to the player API (or browser internals).
|
||||
*/
|
||||
function duration() {
|
||||
let dur;
|
||||
|
||||
// Sometimes the YouTube API doesn't finish instantiating all of it's
|
||||
// methods, but the execution point arrives here.
|
||||
//
|
||||
// This happens when you have start-time and end-time set, and click "Edit"
|
||||
// in Studio, and then "Save". The Video editor dialog closes, the
|
||||
// video reloads, but the start-end range is not visible.
|
||||
if (this.videoPlayer.player.getDuration) {
|
||||
dur = this.videoPlayer.player.getDuration();
|
||||
}
|
||||
|
||||
// For YouTube videos, before the video starts playing, the API
|
||||
// function player.getDuration() will return 0. This means that the VCR
|
||||
// will show total time as 0 when the page just loads (before the user
|
||||
// clicks the Play button).
|
||||
//
|
||||
// We can do betterin a case when dur is 0 (or less than 0). We can ask
|
||||
// the getDuration() function for total time, which will query the
|
||||
// metadata for a duration.
|
||||
//
|
||||
// Be careful! Often the metadata duration is not very precise. It
|
||||
// might differ by one or two seconds against the actual time as will
|
||||
// be reported later on by the player.getDuration() API function.
|
||||
if (!isFinite(dur) || dur <= 0) {
|
||||
if (this.isYoutubeType()) {
|
||||
dur = this.getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
// Just in case the metadata is garbled, or something went wrong, we
|
||||
// have a final check.
|
||||
if (!isFinite(dur) || dur <= 0) {
|
||||
dur = 0;
|
||||
}
|
||||
|
||||
return Math.floor(dur);
|
||||
}
|
||||
|
||||
function onVolumeChange(volume) {
|
||||
this.videoPlayer.player.setVolume(volume);
|
||||
}
|
||||
164
xmodule/assets/video/public/js/04_video_control.js
Normal file
164
xmodule/assets/video/public/js/04_video_control.js
Normal file
@@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
|
||||
import * as Time from './utils/time.js';
|
||||
|
||||
// VideoControl module.
|
||||
let VideoControl = function(state) {
|
||||
let dfd = $.Deferred();
|
||||
|
||||
state.videoControl = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called, these functions will
|
||||
// get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
let methodsDict = {
|
||||
destroy: destroy,
|
||||
hideControls: hideControls,
|
||||
show: show,
|
||||
showControls: showControls,
|
||||
focusFirst: focusFirst,
|
||||
updateVcrVidTime: updateVcrVidTime
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoControl, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
this.el.off({
|
||||
mousemove: this.videoControl.showControls,
|
||||
keydown: this.videoControl.showControls,
|
||||
destroy: this.videoControl.destroy,
|
||||
initialize: this.videoControl.focusFirst
|
||||
});
|
||||
|
||||
this.el.off('controls:show');
|
||||
if (this.controlHideTimeout) {
|
||||
clearTimeout(this.controlHideTimeout);
|
||||
}
|
||||
delete this.videoControl;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
state.videoControl.el = state.el.find('.video-controls');
|
||||
state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime');
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
state.videoControl.fadeOutTimeout = state.config.fadeOutTimeout;
|
||||
|
||||
state.videoControl.el.addClass('html5');
|
||||
state.controlHideTimeout = setTimeout(state.videoControl.hideControls, state.videoControl.fadeOutTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
// function _bindHandlers(state)
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
function _bindHandlers(state) {
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
state.el.on({
|
||||
mousemove: state.videoControl.showControls,
|
||||
keydown: state.videoControl.showControls
|
||||
});
|
||||
}
|
||||
|
||||
if (state.config.focusFirstControl) {
|
||||
state.el.on('initialize', state.videoControl.focusFirst);
|
||||
}
|
||||
state.el.on('destroy', state.videoControl.destroy);
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 focusFirst() {
|
||||
this.videoControl.el.find('.vcr a, .vcr button').first().focus();
|
||||
}
|
||||
|
||||
function show() {
|
||||
this.videoControl.el.removeClass('is-hidden');
|
||||
this.el.trigger('controls:show', arguments);
|
||||
}
|
||||
|
||||
function showControls(event) {
|
||||
if (!this.controlShowLock) {
|
||||
if (!this.captionsHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controlShowLock = true;
|
||||
|
||||
if (this.controlState === 'invisible') {
|
||||
this.videoControl.el.show();
|
||||
this.controlState = 'visible';
|
||||
} else if (this.controlState === 'hiding') {
|
||||
this.videoControl.el.stop(true, false).css('opacity', 1).show();
|
||||
this.controlState = 'visible';
|
||||
} else if (this.controlState === 'visible') {
|
||||
clearTimeout(this.controlHideTimeout);
|
||||
}
|
||||
|
||||
this.controlHideTimeout = setTimeout(this.videoControl.hideControls, this.videoControl.fadeOutTimeout);
|
||||
this.controlShowLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
let _this = this;
|
||||
|
||||
this.controlHideTimeout = null;
|
||||
|
||||
if (!this.captionsHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controlState = 'hiding';
|
||||
this.videoControl.el.fadeOut(this.videoControl.fadeOutTimeout, function() {
|
||||
_this.controlState = 'invisible';
|
||||
// If the focus was on the video control or the volume control,
|
||||
// then we must make sure to close these dialogs. Otherwise, after
|
||||
// next autofocus, these dialogs will be open, but the focus will
|
||||
// not be on them.
|
||||
_this.videoVolumeControl.el.removeClass('open');
|
||||
_this.videoSpeedControl.el.removeClass('open');
|
||||
|
||||
_this.focusGrabber.enableFocusGrabber();
|
||||
});
|
||||
}
|
||||
|
||||
function updateVcrVidTime(params) {
|
||||
let endTime = (this.config.endTime !== null) ? this.config.endTime : params.duration,
|
||||
startTime, currentTime;
|
||||
// in case endTime is accidentally specified as being greater than the video
|
||||
endTime = Math.min(endTime, params.duration);
|
||||
startTime = this.config.startTime > 0 ? this.config.startTime : 0;
|
||||
// if it's a subsection of video, use the clip duration as endTime
|
||||
if (startTime && this.config.endTime) {
|
||||
endTime = this.config.endTime - startTime;
|
||||
}
|
||||
currentTime = startTime ? params.time - startTime : params.time;
|
||||
this.videoControl.vidTimeEl.text(Time.format(currentTime) + ' / ' + Time.format(endTime));
|
||||
}
|
||||
|
||||
export default VideoControl;
|
||||
309
xmodule/assets/video/public/js/04_video_full_screen.js
Normal file
309
xmodule/assets/video/public/js/04_video_full_screen.js
Normal file
@@ -0,0 +1,309 @@
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
let template = [
|
||||
'<button class="control add-fullscreen" aria-disabled="false" title="',
|
||||
gettext('Fill browser'),
|
||||
'" aria-label="',
|
||||
gettext('Fill browser'),
|
||||
'">',
|
||||
'<span class="icon fa fa-arrows-alt" aria-hidden="true"></span>',
|
||||
'</button>'
|
||||
].join('');
|
||||
|
||||
// The following properties and functions enable cross-browser use of the
|
||||
// the Fullscreen Web API.
|
||||
//
|
||||
// function getVendorPrefixed(property)
|
||||
// function getFullscreenElement()
|
||||
// function exitFullscreen()
|
||||
// function requestFullscreen(element, options)
|
||||
//
|
||||
// For more information about the Fullscreen Web API see MDN:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
|
||||
let prefixedFullscreenProperties = (function() {
|
||||
if ('fullscreenEnabled' in document) {
|
||||
return {
|
||||
fullscreenElement: 'fullscreenElement',
|
||||
fullscreenEnabled: 'fullscreenEnabled',
|
||||
requestFullscreen: 'requestFullscreen',
|
||||
exitFullscreen: 'exitFullscreen',
|
||||
fullscreenchange: 'fullscreenchange',
|
||||
fullscreenerror: 'fullscreenerror'
|
||||
};
|
||||
}
|
||||
if ('webkitFullscreenEnabled' in document) {
|
||||
return {
|
||||
fullscreenElement: 'webkitFullscreenElement',
|
||||
fullscreenEnabled: 'webkitFullscreenEnabled',
|
||||
requestFullscreen: 'webkitRequestFullscreen',
|
||||
exitFullscreen: 'webkitExitFullscreen',
|
||||
fullscreenchange: 'webkitfullscreenchange',
|
||||
fullscreenerror: 'webkitfullscreenerror'
|
||||
};
|
||||
}
|
||||
if ('mozFullScreenEnabled' in document) {
|
||||
return {
|
||||
fullscreenElement: 'mozFullScreenElement',
|
||||
fullscreenEnabled: 'mozFullScreenEnabled',
|
||||
requestFullscreen: 'mozRequestFullScreen',
|
||||
exitFullscreen: 'mozCancelFullScreen',
|
||||
fullscreenchange: 'mozfullscreenchange',
|
||||
fullscreenerror: 'mozfullscreenerror'
|
||||
};
|
||||
}
|
||||
if ('msFullscreenEnabled' in document) {
|
||||
return {
|
||||
fullscreenElement: 'msFullscreenElement',
|
||||
fullscreenEnabled: 'msFullscreenEnabled',
|
||||
requestFullscreen: 'msRequestFullscreen',
|
||||
exitFullscreen: 'msExitFullscreen',
|
||||
fullscreenchange: 'MSFullscreenChange',
|
||||
fullscreenerror: 'MSFullscreenError'
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}());
|
||||
|
||||
function getVendorPrefixed(property) {
|
||||
return prefixedFullscreenProperties[property];
|
||||
}
|
||||
|
||||
function getFullscreenElement() {
|
||||
return document[getVendorPrefixed('fullscreenElement')];
|
||||
}
|
||||
|
||||
function exitFullscreen() {
|
||||
if (document[getVendorPrefixed('exitFullscreen')]) {
|
||||
return document[getVendorPrefixed('exitFullscreen')]();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function requestFullscreen(element, options) {
|
||||
if (element[getVendorPrefixed('requestFullscreen')]) {
|
||||
return element[getVendorPrefixed('requestFullscreen')](options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
function destroy() {
|
||||
$(document).off('keyup', this.videoFullScreen.exitHandler);
|
||||
this.videoFullScreen.fullScreenEl.remove();
|
||||
this.el.off({
|
||||
destroy: this.videoFullScreen.destroy
|
||||
});
|
||||
document.removeEventListener(
|
||||
getVendorPrefixed('fullscreenchange'),
|
||||
this.videoFullScreen.handleFullscreenChange
|
||||
);
|
||||
if (this.isFullScreen) {
|
||||
this.videoFullScreen.exit();
|
||||
}
|
||||
delete this.videoFullScreen;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
state.videoFullScreen.fullScreenEl = $(template);
|
||||
state.videoFullScreen.sliderEl = state.el.find('.slider');
|
||||
state.videoFullScreen.fullScreenState = false;
|
||||
HtmlUtils.append(state.el.find('.secondary-controls'), HtmlUtils.HTML(state.videoFullScreen.fullScreenEl));
|
||||
state.videoFullScreen.updateControlsHeight();
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
|
||||
// function bindHandlers(state)
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
function bindHandlers(state) {
|
||||
state.videoFullScreen.fullScreenEl.on('click', state.videoFullScreen.toggleHandler);
|
||||
state.el.on({
|
||||
destroy: state.videoFullScreen.destroy
|
||||
});
|
||||
$(document).on('keyup', state.videoFullScreen.exitHandler);
|
||||
document.addEventListener(
|
||||
getVendorPrefixed('fullscreenchange'),
|
||||
state.videoFullScreen.handleFullscreenChange
|
||||
);
|
||||
}
|
||||
|
||||
function getControlsHeight(controls, slider) {
|
||||
return controls.height() + 0.5 * slider.height();
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 handleFullscreenChange() {
|
||||
if (getFullscreenElement() !== this.el[0] && this.isFullScreen) {
|
||||
// The video was fullscreen so this event must relate to this video
|
||||
this.videoFullScreen.handleExit();
|
||||
}
|
||||
}
|
||||
|
||||
function updateControlsHeight() {
|
||||
let controls = this.el.find('.video-controls');
|
||||
let slider = this.videoFullScreen.sliderEl;
|
||||
this.videoFullScreen.height = getControlsHeight(controls, slider);
|
||||
return this.videoFullScreen.height;
|
||||
}
|
||||
|
||||
function notifyParent(fullscreenOpen) {
|
||||
if (window !== window.parent) {
|
||||
// This is used by the Learning MFE to know about changing fullscreen mode.
|
||||
// The MFE is then able to respond appropriately and scroll window to the previous position.
|
||||
window.parent.postMessage({
|
||||
type: 'plugin.videoFullScreen',
|
||||
payload: {
|
||||
open: fullscreenOpen
|
||||
}
|
||||
}, document.referrer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler to toggle fullscreen mode.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
function toggleHandler(event) {
|
||||
event.preventDefault();
|
||||
this.videoCommands.execute('toggleFullScreen');
|
||||
}
|
||||
|
||||
function handleExit() {
|
||||
let fullScreenClassNameEl = this.el.add(document.documentElement);
|
||||
let closedCaptionsEl = this.el.find('.closed-captions');
|
||||
|
||||
if (this.isFullScreen === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
this.videoFullScreen.fullScreenState = this.isFullScreen = false;
|
||||
fullScreenClassNameEl.removeClass('video-fullscreen');
|
||||
$(window).scrollTop(this.scrollPos);
|
||||
this.videoFullScreen.fullScreenEl
|
||||
.attr({title: gettext('Fill browser'), 'aria-label': gettext('Fill browser')})
|
||||
.find('.icon')
|
||||
.removeClass('fa-compress')
|
||||
.addClass('fa-arrows-alt');
|
||||
|
||||
$(closedCaptionsEl).css({top: '70%', left: '5%'});
|
||||
if (this.resizer) {
|
||||
this.resizer.delta.reset().setMode('width');
|
||||
}
|
||||
this.el.trigger('fullscreen', [this.isFullScreen]);
|
||||
|
||||
this.videoFullScreen.notifyParent(false);
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
let fullScreenClassNameEl = this.el.add(document.documentElement);
|
||||
let closedCaptionsEl = this.el.find('.closed-captions');
|
||||
|
||||
if (this.isFullScreen === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoFullScreen.notifyParent(true);
|
||||
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
this.videoFullScreen.fullScreenState = this.isFullScreen = true;
|
||||
fullScreenClassNameEl.addClass('video-fullscreen');
|
||||
this.videoFullScreen.fullScreenEl
|
||||
.attr({title: gettext('Exit full browser'), 'aria-label': gettext('Exit full browser')})
|
||||
.find('.icon')
|
||||
.removeClass('fa-arrows-alt')
|
||||
.addClass('fa-compress');
|
||||
|
||||
$(closedCaptionsEl).css({top: '70%', left: '5%'});
|
||||
if (this.resizer) {
|
||||
this.resizer.delta.substract(this.videoFullScreen.updateControlsHeight(), 'height').setMode('both');
|
||||
}
|
||||
this.el.trigger('fullscreen', [this.isFullScreen]);
|
||||
}
|
||||
|
||||
function exit() {
|
||||
if (getFullscreenElement() === this.el[0]) {
|
||||
exitFullscreen();
|
||||
} else {
|
||||
// Else some other element is fullscreen or the fullscreen api does not exist.
|
||||
this.videoFullScreen.handleExit();
|
||||
}
|
||||
}
|
||||
|
||||
function enter() {
|
||||
this.scrollPos = $(window).scrollTop();
|
||||
this.videoFullScreen.handleEnter();
|
||||
requestFullscreen(this.el[0]);
|
||||
}
|
||||
|
||||
/** Toggle fullscreen mode. */
|
||||
function toggle() {
|
||||
if (this.videoFullScreen.fullScreenState) {
|
||||
this.videoFullScreen.exit();
|
||||
} else {
|
||||
this.videoFullScreen.enter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler to exit from fullscreen mode.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
function exitHandler(event) {
|
||||
if ((this.isFullScreen) && (event.keyCode === 27)) {
|
||||
event.preventDefault();
|
||||
this.videoCommands.execute('toggleFullScreen');
|
||||
}
|
||||
}
|
||||
|
||||
// function makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called, these functions will
|
||||
// get the 'state' object as a context.
|
||||
function makeFunctionsPublic(state) {
|
||||
let methodsDict = {
|
||||
destroy: destroy,
|
||||
enter: enter,
|
||||
exit: exit,
|
||||
exitHandler: exitHandler,
|
||||
handleExit: handleExit,
|
||||
handleEnter: handleEnter,
|
||||
handleFullscreenChange: handleFullscreenChange,
|
||||
toggle: toggle,
|
||||
toggleHandler: toggleHandler,
|
||||
updateControlsHeight: updateControlsHeight,
|
||||
notifyParent: notifyParent
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoFullScreen, state);
|
||||
}
|
||||
|
||||
// VideoControl() function - what this module "exports".
|
||||
export default function(state) {
|
||||
let dfd = $.Deferred();
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
state.videoFullScreen = {};
|
||||
|
||||
makeFunctionsPublic(state);
|
||||
renderElements(state);
|
||||
bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
}
|
||||
176
xmodule/assets/video/public/js/05_video_quality_control.js
Normal file
176
xmodule/assets/video/public/js/05_video_quality_control.js
Normal file
@@ -0,0 +1,176 @@
|
||||
'use strict';
|
||||
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
let template = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<button class="control quality-control is-hidden" aria-disabled="false" title="',
|
||||
'{highDefinition}',
|
||||
'">',
|
||||
'<span class="icon icon-hd" aria-hidden="true">HD</span>',
|
||||
'<span class="sr text-translation">',
|
||||
'{highDefinition}',
|
||||
'</span> ',
|
||||
'<span class="sr control-text">',
|
||||
'{off}',
|
||||
'</span>',
|
||||
'</button>'
|
||||
].join('')),
|
||||
{
|
||||
highDefinition: gettext('High Definition'),
|
||||
off: gettext('off')
|
||||
}
|
||||
);
|
||||
|
||||
// VideoQualityControl() function - what this module "exports".
|
||||
let VideoQualityControl = function(state) {
|
||||
let dfd = $.Deferred();
|
||||
|
||||
// Changing quality for now only works for YouTube videos.
|
||||
if (state.videoType !== 'youtube') {
|
||||
return;
|
||||
}
|
||||
|
||||
state.videoQualityControl = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called, these functions will
|
||||
// get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
let methodsDict = {
|
||||
destroy: destroy,
|
||||
fetchAvailableQualities: fetchAvailableQualities,
|
||||
onQualityChange: onQualityChange,
|
||||
showQualityControl: showQualityControl,
|
||||
toggleQuality: toggleQuality
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoQualityControl, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
this.videoQualityControl.el.off({
|
||||
click: this.videoQualityControl.toggleQuality,
|
||||
destroy: this.videoQualityControl.destroy
|
||||
});
|
||||
this.el.off('.quality');
|
||||
this.videoQualityControl.el.remove();
|
||||
delete this.videoQualityControl;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
let element = state.videoQualityControl.el = $(template.toString());
|
||||
state.videoQualityControl.quality = 'large';
|
||||
HtmlUtils.append(state.el.find('.secondary-controls'), HtmlUtils.HTML(element));
|
||||
}
|
||||
|
||||
// function _bindHandlers(state)
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
function _bindHandlers(state) {
|
||||
state.videoQualityControl.el.on('click',
|
||||
state.videoQualityControl.toggleQuality
|
||||
);
|
||||
state.el.on('play.quality', _.once(
|
||||
state.videoQualityControl.fetchAvailableQualities
|
||||
));
|
||||
|
||||
state.el.on('destroy.quality', state.videoQualityControl.destroy);
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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().
|
||||
// ***************************************************************
|
||||
|
||||
/*
|
||||
* @desc Shows quality control. This function will only be called if HD
|
||||
* qualities are available.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function showQualityControl() {
|
||||
this.videoQualityControl.el.removeClass('is-hidden');
|
||||
}
|
||||
|
||||
// This function can only be called once as _.once has been used.
|
||||
/*
|
||||
* @desc Get the available qualities from YouTube API. Possible values are:
|
||||
['highres', 'hd1080', 'hd720', 'large', 'medium', 'small'].
|
||||
HD are: ['highres', 'hd1080', 'hd720'].
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function fetchAvailableQualities() {
|
||||
let qualities = this.videoPlayer.player.getAvailableQualityLevels();
|
||||
|
||||
this.config.availableHDQualities = _.intersection(
|
||||
qualities, ['highres', 'hd1080', 'hd720']
|
||||
);
|
||||
|
||||
// HD qualities are available, show video quality control.
|
||||
if (this.config.availableHDQualities.length > 0) {
|
||||
this.trigger('videoQualityControl.showQualityControl');
|
||||
this.trigger('videoQualityControl.onQualityChange', this.videoQualityControl.quality);
|
||||
}
|
||||
// On initialization, force the video quality to be 'large' instead of
|
||||
// 'default'. Otherwise, the player will sometimes switch to HD
|
||||
// automatically, for example when the iframe resizes itself.
|
||||
this.trigger('videoPlayer.handlePlaybackQualityChange',
|
||||
this.videoQualityControl.quality
|
||||
);
|
||||
}
|
||||
|
||||
function onQualityChange(value) {
|
||||
let controlStateStr;
|
||||
this.videoQualityControl.quality = value;
|
||||
if (_.contains(this.config.availableHDQualities, value)) {
|
||||
controlStateStr = gettext('on');
|
||||
this.videoQualityControl.el
|
||||
.addClass('active')
|
||||
.find('.control-text')
|
||||
.text(controlStateStr);
|
||||
} else {
|
||||
controlStateStr = gettext('off');
|
||||
this.videoQualityControl.el
|
||||
.removeClass('active')
|
||||
.find('.control-text')
|
||||
.text(controlStateStr);
|
||||
}
|
||||
}
|
||||
|
||||
// This function toggles the quality of video only if HD qualities are
|
||||
// available.
|
||||
function toggleQuality(event) {
|
||||
let value = this.videoQualityControl.quality;
|
||||
let isHD = _.contains(this.config.availableHDQualities, value);
|
||||
let newQuality = isHD ? 'large' : 'highres';
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality);
|
||||
}
|
||||
|
||||
export default VideoQualityControl;
|
||||
360
xmodule/assets/video/public/js/06_video_progress_slider.js
Normal file
360
xmodule/assets/video/public/js/06_video_progress_slider.js
Normal file
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
"This is as true in everyday life as it is in battle: we are given one life
|
||||
and the decision is ours whether to wait for circumstances to make up our
|
||||
mind, or whether to act, and in acting, to live."
|
||||
— Omar N. Bradley
|
||||
*/
|
||||
|
||||
// VideoProgressSlider module.
|
||||
let template = [
|
||||
'<div class="slider" role="application" title="',
|
||||
gettext('Video position. Press space to toggle playback'),
|
||||
'"></div>'
|
||||
].join('');
|
||||
|
||||
// VideoProgressSlider() function - what this module "exports".
|
||||
export default function(state) {
|
||||
let dfd = $.Deferred();
|
||||
|
||||
state.videoProgressSlider = {};
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called,
|
||||
// these functions will get the 'state' object as a context.
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
function _makeFunctionsPublic(state) {
|
||||
let methodsDict = {
|
||||
destroy: destroy,
|
||||
buildSlider: buildSlider,
|
||||
getRangeParams: getRangeParams,
|
||||
onSlide: onSlide,
|
||||
onStop: onStop,
|
||||
updatePlayTime: updatePlayTime,
|
||||
updateStartEndTimeRegion: updateStartEndTimeRegion,
|
||||
notifyThroughHandleEnd: notifyThroughHandleEnd,
|
||||
getTimeDescription: getTimeDescription,
|
||||
focusSlider: focusSlider
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoProgressSlider, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
this.videoProgressSlider.el.removeAttr('tabindex').slider('destroy');
|
||||
this.el.off('destroy', this.videoProgressSlider.destroy);
|
||||
delete this.videoProgressSlider;
|
||||
}
|
||||
|
||||
function bindHandlers(state) {
|
||||
state.videoProgressSlider.el.on('keypress', sliderToggle.bind(state));
|
||||
state.el.on('destroy', state.videoProgressSlider.destroy);
|
||||
}
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
// 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) {
|
||||
state.videoProgressSlider.el = $(template);
|
||||
|
||||
state.el.find('.video-controls').prepend(state.videoProgressSlider.el);
|
||||
state.videoProgressSlider.buildSlider();
|
||||
_buildHandle(state);
|
||||
bindHandlers(state);
|
||||
}
|
||||
|
||||
function _buildHandle(state) {
|
||||
state.videoProgressSlider.handle = state.videoProgressSlider.el
|
||||
.find('.ui-slider-handle');
|
||||
|
||||
// ARIA
|
||||
// We just want the knob to be selectable with keyboard
|
||||
state.videoProgressSlider.el.attr({
|
||||
tabindex: -1
|
||||
});
|
||||
|
||||
// Let screen readers know that this div, representing the slider
|
||||
// handle, behaves as a slider named 'video position'.
|
||||
state.videoProgressSlider.handle.attr({
|
||||
role: 'slider',
|
||||
'aria-disabled': false,
|
||||
'aria-valuetext': getTimeDescription(state.videoProgressSlider
|
||||
.slider.slider('option', 'value')),
|
||||
'aria-valuemax': state.videoPlayer.duration(),
|
||||
'aria-valuemin': '0',
|
||||
'aria-valuenow': state.videoPlayer.currentTime,
|
||||
tabindex: '0',
|
||||
'aria-label': gettext('Video position. Press space to toggle playback')
|
||||
});
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 buildSlider() {
|
||||
let sliderContents = edx.HtmlUtils.joinHtml(
|
||||
edx.HtmlUtils.HTML('<div class="ui-slider-handle progress-handle"></div>')
|
||||
);
|
||||
|
||||
// xss-lint: disable=javascript-jquery-append
|
||||
this.videoProgressSlider.el.append(sliderContents.text);
|
||||
|
||||
this.videoProgressSlider.slider = this.videoProgressSlider.el
|
||||
.slider({
|
||||
range: 'min',
|
||||
min: this.config.startTime,
|
||||
max: this.config.endTime,
|
||||
slide: this.videoProgressSlider.onSlide,
|
||||
stop: this.videoProgressSlider.onStop,
|
||||
step: 5
|
||||
});
|
||||
|
||||
this.videoProgressSlider.sliderProgress = this.videoProgressSlider
|
||||
.slider
|
||||
.find('.ui-slider-range.ui-widget-header.ui-slider-range-min');
|
||||
}
|
||||
|
||||
// Rebuild the slider start-end range (if it doesn't take up the
|
||||
// whole slider). Remember that endTime === null means the end-time
|
||||
// is set to the end of video by default.
|
||||
function updateStartEndTimeRegion(params) {
|
||||
let start, end, duration, rangeParams;
|
||||
|
||||
// We must have a duration in order to determine the area of range.
|
||||
// It also must be non-zero.
|
||||
if (!params.duration) {
|
||||
return;
|
||||
} else {
|
||||
duration = params.duration;
|
||||
}
|
||||
|
||||
start = this.config.startTime;
|
||||
end = this.config.endTime;
|
||||
|
||||
if (start > duration) {
|
||||
start = 0;
|
||||
} else if (this.isFlashMode()) {
|
||||
start /= Number(this.speed);
|
||||
}
|
||||
|
||||
// If end is set to null, or it is greater than the duration of the
|
||||
// video, then we set it to the end of the video.
|
||||
if (end === null || end > duration) {
|
||||
end = duration;
|
||||
} else if (this.isFlashMode()) {
|
||||
end /= Number(this.speed);
|
||||
}
|
||||
|
||||
// Don't build a range if it takes up the whole slider.
|
||||
if (start === 0 && end === duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Because JavaScript has weird rounding rules when a series of
|
||||
// mathematical operations are performed in a single statement, we will
|
||||
// split everything up into smaller statements.
|
||||
//
|
||||
// This will ensure that visually, the start-end range aligns nicely
|
||||
// with actual starting and ending point of the video.
|
||||
|
||||
rangeParams = getRangeParams(start, end, duration);
|
||||
}
|
||||
|
||||
function getRangeParams(startTime, endTime, duration) {
|
||||
let step = 100 / duration;
|
||||
let left = startTime * step;
|
||||
let width = endTime * step - left;
|
||||
|
||||
return {
|
||||
left: left + '%',
|
||||
width: width + '%'
|
||||
};
|
||||
}
|
||||
|
||||
function onSlide(event, ui) {
|
||||
let time = ui.value;
|
||||
let endTime = this.videoPlayer.duration();
|
||||
|
||||
if (this.config.endTime) {
|
||||
endTime = Math.min(this.config.endTime, endTime);
|
||||
}
|
||||
|
||||
this.videoProgressSlider.frozen = true;
|
||||
|
||||
// Remember the seek to value so that we don't repeat ourselves on the
|
||||
// 'stop' slider event.
|
||||
this.videoProgressSlider.lastSeekValue = time;
|
||||
|
||||
this.trigger(
|
||||
'videoControl.updateVcrVidTime',
|
||||
{
|
||||
time: time,
|
||||
duration: endTime
|
||||
}
|
||||
);
|
||||
|
||||
this.trigger(
|
||||
'videoPlayer.onSlideSeek',
|
||||
{type: 'onSlideSeek', time: time}
|
||||
);
|
||||
|
||||
// ARIA
|
||||
this.videoProgressSlider.handle.attr(
|
||||
'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
|
||||
);
|
||||
}
|
||||
|
||||
function onStop(event, ui) {
|
||||
let _this = this;
|
||||
|
||||
this.videoProgressSlider.frozen = true;
|
||||
|
||||
// Only perform a seek if we haven't made a seek for the new slider value.
|
||||
// This is necessary so that if the user only clicks on the slider, without
|
||||
// dragging it, then only one seek is made, even when a 'slide' and a 'stop'
|
||||
// events are triggered on the slider.
|
||||
if (this.videoProgressSlider.lastSeekValue !== ui.value) {
|
||||
this.trigger(
|
||||
'videoPlayer.onSlideSeek',
|
||||
{type: 'onSlideSeek', time: ui.value}
|
||||
);
|
||||
}
|
||||
|
||||
// ARIA
|
||||
this.videoProgressSlider.handle.attr(
|
||||
'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
|
||||
);
|
||||
|
||||
setTimeout(function() {
|
||||
_this.videoProgressSlider.frozen = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function updatePlayTime(params) {
|
||||
let time = Math.floor(params.time);
|
||||
// params.duration could accidentally be construed as a floating
|
||||
// point double. Since we're displaying this number, round down
|
||||
// to nearest second
|
||||
let endTime = Math.floor(params.duration);
|
||||
|
||||
if (this.config.endTime !== null) {
|
||||
endTime = Math.min(this.config.endTime, endTime);
|
||||
}
|
||||
|
||||
if (
|
||||
this.videoProgressSlider.slider
|
||||
&& !this.videoProgressSlider.frozen
|
||||
) {
|
||||
this.videoProgressSlider.slider
|
||||
.slider('option', 'max', endTime)
|
||||
.slider('option', 'value', time);
|
||||
}
|
||||
|
||||
// Update aria values.
|
||||
this.videoProgressSlider.handle.attr({
|
||||
'aria-valuemax': endTime,
|
||||
'aria-valuenow': time
|
||||
});
|
||||
}
|
||||
|
||||
// When the video stops playing (either because the end was reached, or
|
||||
// because endTime was reached), the screen reader must be notified that
|
||||
// the video is no longer playing. We do this by a little trick. Setting
|
||||
// the title attribute of the slider know to "video ended", and focusing
|
||||
// on it. The screen reader will read the attr text.
|
||||
//
|
||||
// The user can then tab their way forward, landing on the next control
|
||||
// element, the Play button.
|
||||
//
|
||||
// @param params - object with property `end`. If set to true, the
|
||||
// function must set the title attribute to
|
||||
// `video ended`;
|
||||
// if set to false, the function must reset the attr to
|
||||
// it's original state.
|
||||
//
|
||||
// This function will be triggered from VideoPlayer methods onEnded(),
|
||||
// onPlay(), and update() (update method handles endTime).
|
||||
function notifyThroughHandleEnd(params) {
|
||||
if (params.end) {
|
||||
this.videoProgressSlider.handle
|
||||
.attr('title', gettext('Video ended'))
|
||||
.focus();
|
||||
} else {
|
||||
this.videoProgressSlider.handle
|
||||
.attr('title', gettext('Video position'));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a string describing the current time of video in
|
||||
// `%d hours %d minutes %d seconds` format.
|
||||
function getTimeDescription(time) {
|
||||
let seconds = Math.floor(time);
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let i18n = function(value, word) {
|
||||
let msg;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (word) {
|
||||
case 'hour':
|
||||
msg = ngettext('%(value)s hour', '%(value)s hours', value);
|
||||
break;
|
||||
case 'minute':
|
||||
msg = ngettext('%(value)s minute', '%(value)s minutes', value);
|
||||
break;
|
||||
case 'second':
|
||||
msg = ngettext('%(value)s second', '%(value)s seconds', value);
|
||||
break;
|
||||
}
|
||||
return interpolate(msg, {value: value}, true);
|
||||
};
|
||||
|
||||
seconds %= 60;
|
||||
minutes %= 60;
|
||||
|
||||
if (hours) {
|
||||
return i18n(hours, 'hour') + ' '
|
||||
+ i18n(minutes, 'minute') + ' '
|
||||
+ i18n(seconds, 'second');
|
||||
} else if (minutes) {
|
||||
return i18n(minutes, 'minute') + ' '
|
||||
+ i18n(seconds, 'second');
|
||||
}
|
||||
|
||||
return i18n(seconds, 'second');
|
||||
}
|
||||
|
||||
// Shift focus to the progress slider container element.
|
||||
function focusSlider() {
|
||||
this.videoProgressSlider.handle.attr(
|
||||
'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
|
||||
);
|
||||
this.videoProgressSlider.el.trigger('focus');
|
||||
}
|
||||
|
||||
// Toggle video playback when the spacebar is pushed.
|
||||
function sliderToggle(e) {
|
||||
if (e.which === 32) {
|
||||
e.preventDefault();
|
||||
this.videoCommands.execute('togglePlayback');
|
||||
}
|
||||
}
|
||||
554
xmodule/assets/video/public/js/07_video_volume_control.js
Normal file
554
xmodule/assets/video/public/js/07_video_volume_control.js
Normal file
@@ -0,0 +1,554 @@
|
||||
'use strict';
|
||||
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
/**
|
||||
* Video volume control module.
|
||||
* @exports video/07_video_volume_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let VideoVolumeControl = function (state, i18n) {
|
||||
if (!(this instanceof VideoVolumeControl)) {
|
||||
return new VideoVolumeControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'keyDownHandler', 'updateVolumeSilently',
|
||||
'onVolumeChangeHandler', 'openMenu', 'closeMenu',
|
||||
'toggleMuteHandler', 'keyDownButtonHandler', 'destroy'
|
||||
);
|
||||
this.state = state;
|
||||
this.state.videoVolumeControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
|
||||
VideoVolumeControl.prototype = {
|
||||
/** Minimum value for the volume slider. */
|
||||
min: 0,
|
||||
/** Maximum value for the volume slider. */
|
||||
max: 100,
|
||||
/** Step to increase/decrease volume level via keyboard. */
|
||||
step: 20,
|
||||
|
||||
videoVolumeControlHtml: HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<div class="volume" role="application">',
|
||||
'<p class="sr instructions">',
|
||||
'{volumeInstructions}',
|
||||
'</p>',
|
||||
'<button class="control" aria-disabled="false"',
|
||||
'" aria-expanded="false" title="',
|
||||
'{adjustVideoVolume}',
|
||||
'" aria-label="',
|
||||
'{adjustVideoVolume}',
|
||||
'">',
|
||||
'<span class="icon fa fa-volume-up" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'<div class="volume-slider-container" aria-hidden="true" title="',
|
||||
'{adjustVideoVolume}',
|
||||
'">',
|
||||
'<div class="volume-slider" ',
|
||||
'role="slider"',
|
||||
'aria-orientation="vertical" ',
|
||||
'aria-valuemin="0" ',
|
||||
'aria-valuemax="100" ',
|
||||
'aria-valuenow="" ',
|
||||
'aria-label="',
|
||||
'{volumeText}',
|
||||
'"></div>',
|
||||
'</div>',
|
||||
'</div>'].join('')),
|
||||
{
|
||||
volumeInstructions: gettext('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.'), // eslint-disable-line max-len
|
||||
adjustVideoVolume: gettext('Adjust video volume'),
|
||||
volumeText: gettext('Volume')
|
||||
}
|
||||
),
|
||||
|
||||
destroy: function () {
|
||||
this.volumeSlider.slider('destroy');
|
||||
this.state.el.find('iframe').removeAttr('tabindex');
|
||||
this.a11y.destroy();
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
this.cookie = this.a11y = null;
|
||||
this.closeMenu();
|
||||
|
||||
this.state.el
|
||||
.off('play.volume')
|
||||
.off({
|
||||
keydown: this.keyDownHandler,
|
||||
volumechange: this.onVolumeChangeHandler
|
||||
});
|
||||
this.el.off({
|
||||
mouseenter: this.openMenu,
|
||||
mouseleave: this.closeMenu
|
||||
});
|
||||
this.button.off({
|
||||
mousedown: this.toggleMuteHandler,
|
||||
keydown: this.keyDownButtonHandler,
|
||||
focus: this.openMenu,
|
||||
blur: this.closeMenu
|
||||
});
|
||||
this.el.remove();
|
||||
delete this.state.videoVolumeControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function () {
|
||||
let volume;
|
||||
|
||||
if (this.state.isTouch) {
|
||||
// iOS doesn't support volume change
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el = $(this.videoVolumeControlHtml.toString());
|
||||
// Youtube iframe react on key buttons and has his own handlers.
|
||||
// So, we disallow focusing on iframe.
|
||||
this.state.el.find('iframe').attr('tabindex', -1);
|
||||
this.button = this.el.children('.control');
|
||||
this.cookie = new CookieManager(this.min, this.max);
|
||||
this.a11y = new Accessibility(
|
||||
this.button, this.min, this.max, this.i18n
|
||||
);
|
||||
volume = this.cookie.getVolume();
|
||||
this.storedVolume = this.max;
|
||||
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
this.setVolume(volume, true, false);
|
||||
this.checkMuteButtonStatus(volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function () {
|
||||
let container = this.el.find('.volume-slider'),
|
||||
instructionsId = 'volume-instructions-' + this.state.id;
|
||||
|
||||
HtmlUtils.append(container, HtmlUtils.HTML('<div class="ui-slider-handle volume-handle"></div>'));
|
||||
|
||||
this.volumeSlider = container.slider({
|
||||
orientation: 'vertical',
|
||||
range: 'min',
|
||||
min: this.min,
|
||||
max: this.max,
|
||||
slide: this.onSlideHandler.bind(this)
|
||||
});
|
||||
|
||||
// We provide an independent behavior to adjust volume level.
|
||||
// Therefore, we do not need redundant focusing on slider in TAB
|
||||
// order.
|
||||
container.find('.volume-handle').attr('tabindex', -1);
|
||||
this.state.el.find('.secondary-controls').append(this.el);
|
||||
|
||||
// set dynamic id for instruction element to avoid collisions
|
||||
this.el.find('.instructions').attr('id', instructionsId);
|
||||
this.button.attr('aria-describedby', instructionsId);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function () {
|
||||
this.state.el.on({
|
||||
'play.volume': _.once(this.updateVolumeSilently),
|
||||
volumechange: this.onVolumeChangeHandler
|
||||
});
|
||||
this.state.el.find('.volume').on({
|
||||
mouseenter: this.openMenu,
|
||||
mouseleave: this.closeMenu
|
||||
});
|
||||
this.button.on({
|
||||
keydown: this.keyDownHandler,
|
||||
click: false,
|
||||
mousedown: this.toggleMuteHandler,
|
||||
focus: this.openMenu,
|
||||
blur: this.closeMenu
|
||||
});
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates volume level without updating view and triggering
|
||||
* `volumechange` event.
|
||||
*/
|
||||
updateVolumeSilently: function () {
|
||||
this.state.el.trigger(
|
||||
'volumechange:silent', [this.getVolume()]
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns current volume level.
|
||||
* @return {Number}
|
||||
*/
|
||||
getVolume: function () {
|
||||
return this.volume;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets current volume level.
|
||||
* @param {Number} volume Suggested volume level
|
||||
* @param {Boolean} [silent] Sets the new volume level without
|
||||
* triggering `volumechange` event and updating the cookie.
|
||||
* @param {Boolean} [withoutSlider] Disables updating the slider.
|
||||
*/
|
||||
setVolume: function (volume, silent, withoutSlider) {
|
||||
if (volume === this.getVolume()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.volume = volume;
|
||||
this.a11y.update(this.getVolume());
|
||||
|
||||
if (!withoutSlider) {
|
||||
this.updateSliderView(this.getVolume());
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
this.cookie.setVolume(this.getVolume());
|
||||
this.state.el.trigger('volumechange', [this.getVolume()]);
|
||||
}
|
||||
},
|
||||
|
||||
/** Increases current volume level using previously defined step. */
|
||||
increaseVolume: function () {
|
||||
let volume = Math.min(this.getVolume() + this.step, this.max);
|
||||
|
||||
this.setVolume(volume, false, false);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/** Decreases current volume level using previously defined step. */
|
||||
decreaseVolume: function () {
|
||||
let volume = Math.max(this.getVolume() - this.step, this.min);
|
||||
|
||||
this.setVolume(volume, false, false);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/** Updates volume slider view. */
|
||||
updateSliderView: function (volume) {
|
||||
this.volumeSlider.slider('value', volume);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mutes or unmutes volume.
|
||||
* @param {Number} muteStatus Flag to mute/unmute volume.
|
||||
*/
|
||||
mute: function (muteStatus) {
|
||||
let volume;
|
||||
|
||||
this.updateMuteButtonView(muteStatus);
|
||||
|
||||
if (muteStatus) {
|
||||
this.storedVolume = this.getVolume() || this.max;
|
||||
}
|
||||
|
||||
volume = muteStatus ? 0 : this.storedVolume;
|
||||
this.setVolume(volume, false, false);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns current volume state (is it muted or not?).
|
||||
* @return {Boolean}
|
||||
*/
|
||||
getMuteStatus: function () {
|
||||
return this.getVolume() === 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the volume button view.
|
||||
* @param {Boolean} isMuted Flag to use muted or unmuted view.
|
||||
*/
|
||||
updateMuteButtonView: function (isMuted) {
|
||||
let action = isMuted ? 'addClass' : 'removeClass';
|
||||
|
||||
this.el[action]('is-muted');
|
||||
|
||||
if (isMuted) {
|
||||
this.el
|
||||
.find('.control .icon')
|
||||
.removeClass('fa-volume-up')
|
||||
.addClass('fa-volume-off');
|
||||
} else {
|
||||
this.el
|
||||
.find('.control .icon')
|
||||
.removeClass('fa-volume-off')
|
||||
.addClass('fa-volume-up');
|
||||
}
|
||||
},
|
||||
|
||||
/** Toggles the state of the volume button. */
|
||||
toggleMute: function () {
|
||||
this.mute(!this.getMuteStatus());
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks and updates the state of the volume button relatively to
|
||||
* volume level.
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
checkMuteButtonStatus: function (volume) {
|
||||
if (volume <= this.min) {
|
||||
this.updateMuteButtonView(true);
|
||||
this.state.el.off('volumechange.is-muted');
|
||||
this.state.el.on('volumechange.is-muted', _.once(function () {
|
||||
this.updateMuteButtonView(false);
|
||||
}.bind(this)));
|
||||
}
|
||||
},
|
||||
|
||||
/** Opens volume menu. */
|
||||
openMenu: function () {
|
||||
this.el.addClass('is-opened');
|
||||
this.button.attr('aria-expanded', 'true');
|
||||
},
|
||||
|
||||
/** Closes speed menu. */
|
||||
closeMenu: function () {
|
||||
this.el.removeClass('is-opened');
|
||||
this.button.attr('aria-expanded', 'false');
|
||||
},
|
||||
|
||||
/**
|
||||
* Keydown event handler for the video container.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
keyDownHandler: function (event) {
|
||||
// ALT key is used to change (alternate) the function of
|
||||
// other pressed keys. In this case, do nothing.
|
||||
if (event.altKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($(event.target).hasClass('ui-slider-handle')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.UP:
|
||||
// Shift + Arrows keyboard shortcut might be used by
|
||||
// screen readers. In this case, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.increaseVolume();
|
||||
return false;
|
||||
case KEY.DOWN:
|
||||
// Shift + Arrows keyboard shortcut might be used by
|
||||
// screen readers. In this case, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.decreaseVolume();
|
||||
return false;
|
||||
|
||||
case KEY.SPACE:
|
||||
case KEY.ENTER:
|
||||
// Shift + Enter keyboard shortcut might be used by
|
||||
// screen readers. In this case, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.toggleMute();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Keydown event handler for the volume button.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
keyDownButtonHandler: function (event) {
|
||||
// ALT key is used to change (alternate) the function of
|
||||
// other pressed keys. In this case, do nothing.
|
||||
if (event.altKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.toggleMute();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* onSlide callback for the video slider.
|
||||
* @param {jquery Event} event
|
||||
* @param {jqueryuiSlider ui} ui
|
||||
*/
|
||||
onSlideHandler: function (event, ui) {
|
||||
this.setVolume(ui.value, false, true);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', ui.volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mousedown event handler for the volume button.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
toggleMuteHandler: function (event) {
|
||||
this.toggleMute();
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Volumechange event handler.
|
||||
* @param {jquery Event} event
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
onVolumeChangeHandler: function (event, volume) {
|
||||
this.checkMuteButtonStatus(volume);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Module responsible for the accessibility of volume controls.
|
||||
* @constructor
|
||||
* @private
|
||||
* @param {jquery $} button The volume button.
|
||||
* @param {Number} min Minimum value for the volume slider.
|
||||
* @param {Number} max Maximum value for the volume slider.
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
*/
|
||||
let Accessibility = function(button, min, max, i18n) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.button = button;
|
||||
this.i18n = i18n;
|
||||
|
||||
this.initialize();
|
||||
};
|
||||
|
||||
|
||||
Accessibility.prototype = {
|
||||
destroy: function () {
|
||||
this.liveRegion.remove();
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function () {
|
||||
this.liveRegion = $('<div />', {
|
||||
class: 'sr video-live-region',
|
||||
'aria-hidden': 'false',
|
||||
'aria-live': 'polite'
|
||||
});
|
||||
|
||||
this.button.after(HtmlUtils.HTML(this.liveRegion).toString());
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates text of the live region.
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
update: function (volume) {
|
||||
this.liveRegion.text([
|
||||
this.getVolumeDescription(volume),
|
||||
this.i18n.Volume + '.'
|
||||
].join(' '));
|
||||
|
||||
$(this.button).parent().find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a string describing the level of volume.
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
getVolumeDescription: function (volume) {
|
||||
if (volume === 0) {
|
||||
return this.i18n.Muted;
|
||||
} else if (volume <= 20) {
|
||||
return this.i18n['Very low'];
|
||||
} else if (volume <= 40) {
|
||||
return this.i18n.Low;
|
||||
} else if (volume <= 60) {
|
||||
return this.i18n.Average;
|
||||
} else if (volume <= 80) {
|
||||
return this.i18n.Loud;
|
||||
} else if (volume <= 99) {
|
||||
return this.i18n['Very loud'];
|
||||
}
|
||||
|
||||
return this.i18n.Maximum;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Module responsible for the work with volume cookie.
|
||||
* @constructor
|
||||
* @private
|
||||
* @param {Number} min Minimum value for the volume slider.
|
||||
* @param {Number} max Maximum value for the volume slider.
|
||||
*/
|
||||
let CookieManager = function (min, max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.cookieName = 'video_player_volume_level';
|
||||
};
|
||||
|
||||
|
||||
CookieManager.prototype = {
|
||||
/**
|
||||
* Returns volume level from the cookie.
|
||||
* @return {Number} Volume level.
|
||||
*/
|
||||
getVolume: function () {
|
||||
let volume = parseInt($.cookie(this.cookieName), 10);
|
||||
|
||||
if (_.isFinite(volume)) {
|
||||
volume = Math.max(volume, this.min);
|
||||
volume = Math.min(volume, this.max);
|
||||
} else {
|
||||
volume = this.max;
|
||||
}
|
||||
|
||||
return volume;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates volume cookie.
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
setVolume: function (value) {
|
||||
$.cookie(this.cookieName, value, {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoVolumeControl;
|
||||
134
xmodule/assets/video/public/js/08_video_auto_advance_control.js
Normal file
134
xmodule/assets/video/public/js/08_video_auto_advance_control.js
Normal file
@@ -0,0 +1,134 @@
|
||||
'use strict';
|
||||
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
/**
|
||||
* Auto advance control module.
|
||||
* @exports video/08_video_auto_advance_control.js
|
||||
* @constructor
|
||||
* @param {object} state The object containing the state of the video player.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let AutoAdvanceControl = function(state) {
|
||||
if (!(this instanceof AutoAdvanceControl)) {
|
||||
return new AutoAdvanceControl(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onClick', 'destroy', 'autoPlay', 'autoAdvance');
|
||||
this.state = state;
|
||||
this.state.videoAutoAdvanceControl = this;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
AutoAdvanceControl.prototype = {
|
||||
template: HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<button class="control auto-advance" aria-disabled="false" title="',
|
||||
'{autoAdvanceText}',
|
||||
'">',
|
||||
'<span class="label" aria-hidden="true">',
|
||||
'{autoAdvanceText}',
|
||||
'</span>',
|
||||
'</button>'].join('')),
|
||||
{
|
||||
autoAdvanceText: gettext('Auto-advance')
|
||||
}
|
||||
).toString(),
|
||||
|
||||
destroy: function() {
|
||||
this.el.off({
|
||||
click: this.onClick
|
||||
});
|
||||
this.el.remove();
|
||||
this.state.el.off({
|
||||
ready: this.autoPlay,
|
||||
ended: this.autoAdvance,
|
||||
destroy: this.destroy
|
||||
});
|
||||
delete this.state.videoAutoAdvanceControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
let state = this.state;
|
||||
|
||||
this.el = $(this.template);
|
||||
this.render();
|
||||
this.setAutoAdvance(state.auto_advance);
|
||||
this.bindHandlers();
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
* @param {boolean} enabled Whether auto advance is enabled
|
||||
*/
|
||||
render: function() {
|
||||
this.state.el.find('.secondary-controls').prepend(this.el);
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind any necessary function callbacks to DOM events (click,
|
||||
* mousemove, etc.).
|
||||
*/
|
||||
bindHandlers: function() {
|
||||
this.el.on({
|
||||
click: this.onClick
|
||||
});
|
||||
this.state.el.on({
|
||||
ready: this.autoPlay,
|
||||
ended: this.autoAdvance,
|
||||
destroy: this.destroy
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(event) {
|
||||
let enabled = !this.state.auto_advance;
|
||||
event.preventDefault();
|
||||
this.setAutoAdvance(enabled);
|
||||
this.el.trigger('autoadvancechange', [enabled]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets or unsets auto advance.
|
||||
* @param {boolean} enabled Sets auto advance.
|
||||
*/
|
||||
setAutoAdvance: function(enabled) {
|
||||
if (enabled) {
|
||||
this.el.addClass('active');
|
||||
} else {
|
||||
this.el.removeClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
autoPlay: function() {
|
||||
// Only autoplay the video if it's the first component of the unit.
|
||||
// If a unit has more than one video, no more than one will autoplay.
|
||||
let isFirstComponent = this.state.el.parents('.vert-0').length === 1;
|
||||
if (this.state.auto_advance && isFirstComponent) {
|
||||
this.state.videoCommands.execute('play');
|
||||
}
|
||||
},
|
||||
|
||||
autoAdvance: function() {
|
||||
// We are posting a message to the MFE and then let the eventlistener
|
||||
// in the MFE handle the action taken.
|
||||
if (this.state.auto_advance) {
|
||||
if (window !== window.parent) {
|
||||
window.parent.postMessage({
|
||||
type: 'plugin.autoAdvance',
|
||||
payload: {}
|
||||
}, document.referrer
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default AutoAdvanceControl;
|
||||
417
xmodule/assets/video/public/js/08_video_speed_control.js
Normal file
417
xmodule/assets/video/public/js/08_video_speed_control.js
Normal file
@@ -0,0 +1,417 @@
|
||||
'use strict';
|
||||
|
||||
import Iterator from './00_iterator.js';
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
|
||||
/**
|
||||
* Video speed control module.
|
||||
* @exports video/08_video_speed_control.js
|
||||
* @constructor
|
||||
* @param {object} state The object containing the state of the video player.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
function VideoSpeedControl(state) {
|
||||
if (!(this instanceof VideoSpeedControl)) {
|
||||
return new VideoSpeedControl(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onSetSpeed', 'onRenderSpeed', 'clickLinkHandler',
|
||||
'keyDownLinkHandler', 'mouseEnterHandler', 'mouseLeaveHandler',
|
||||
'clickMenuHandler', 'keyDownMenuHandler', 'destroy'
|
||||
);
|
||||
this.state = state;
|
||||
this.state.videoSpeedControl = this;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VideoSpeedControl.prototype = {
|
||||
template: [
|
||||
'<div class="speeds menu-container" role="application">',
|
||||
'<p class="sr instructions">',
|
||||
gettext('Press UP to enter the speed menu then use the UP and DOWN arrow keys to navigate the different speeds, then press ENTER to change to the selected speed.'), // eslint-disable-line max-len, indent
|
||||
'</p>',
|
||||
'<button class="control speed-button" aria-disabled="false" aria-expanded="false"',
|
||||
'title="',
|
||||
gettext('Adjust video speed'),
|
||||
'">',
|
||||
'<span>',
|
||||
'<span class="icon fa fa-caret-right" aria-hidden="true"></span>',
|
||||
'</span>',
|
||||
'<span class="label">',
|
||||
gettext('Speed'),
|
||||
' </span>',
|
||||
'<span class="value"></span>',
|
||||
'</button>',
|
||||
'<ol class="video-speeds menu"></ol>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
|
||||
destroy: function () {
|
||||
this.el.off({
|
||||
mouseenter: this.mouseEnterHandler,
|
||||
mouseleave: this.mouseLeaveHandler,
|
||||
click: this.clickMenuHandler,
|
||||
keydown: this.keyDownMenuHandler
|
||||
});
|
||||
|
||||
this.state.el.off({
|
||||
'speed:set': this.onSetSpeed,
|
||||
'speed:render': this.onRenderSpeed
|
||||
});
|
||||
this.closeMenu(true);
|
||||
this.speedsContainer.remove();
|
||||
this.el.remove();
|
||||
delete this.state.videoSpeedControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function () {
|
||||
let state = this.state;
|
||||
|
||||
if (!this.isPlaybackRatesSupported(state)) {
|
||||
console.log(
|
||||
'[Video info]: playbackRate is not supported.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
this.el = $(this.template);
|
||||
this.speedsContainer = this.el.find('.video-speeds');
|
||||
this.speedButton = this.el.find('.speed-button');
|
||||
this.render(state.speeds, state.speed);
|
||||
this.setSpeed(state.speed, true, true);
|
||||
this.bindHandlers();
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
* @param {array} speeds List of speeds available for the player.
|
||||
* @param {string} currentSpeed The current speed set to the player.
|
||||
*/
|
||||
render: function (speeds, currentSpeed) {
|
||||
let speedsContainer = this.speedsContainer,
|
||||
reversedSpeeds = speeds.concat().reverse(),
|
||||
instructionsId = 'speed-instructions-' + this.state.id,
|
||||
speedsList = $.map(reversedSpeeds, function (speed) {
|
||||
return HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<li data-speed="{speed}">',
|
||||
'<button class="control speed-option" tabindex="-1" aria-pressed="false">',
|
||||
'{speed}x',
|
||||
'</button>',
|
||||
'</li>'
|
||||
].join('')
|
||||
),
|
||||
{
|
||||
speed: speed
|
||||
}
|
||||
).toString();
|
||||
});
|
||||
|
||||
HtmlUtils.setHtml(
|
||||
speedsContainer,
|
||||
HtmlUtils.HTML(speedsList)
|
||||
);
|
||||
this.speedLinks = new Iterator(speedsContainer.find('.speed-option'));
|
||||
HtmlUtils.prepend(
|
||||
this.state.el.find('.secondary-controls'),
|
||||
HtmlUtils.HTML(this.el)
|
||||
);
|
||||
this.setActiveSpeed(currentSpeed);
|
||||
|
||||
// set dynamic id for instruction element to avoid collisions
|
||||
this.el.find('.instructions').attr('id', instructionsId);
|
||||
this.speedButton.attr('aria-describedby', instructionsId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind any necessary function callbacks to DOM events (click,
|
||||
* mousemove, etc.).
|
||||
*/
|
||||
bindHandlers: function () {
|
||||
// Attach various events handlers to the speed menu button.
|
||||
this.el.on({
|
||||
mouseenter: this.mouseEnterHandler,
|
||||
mouseleave: this.mouseLeaveHandler,
|
||||
click: this.openMenu,
|
||||
keydown: this.keyDownMenuHandler
|
||||
});
|
||||
|
||||
// Attach click and keydown event handlers to the individual speed
|
||||
// entries.
|
||||
this.speedsContainer.on({
|
||||
click: this.clickLinkHandler,
|
||||
keydown: this.keyDownLinkHandler
|
||||
}, '.speed-option');
|
||||
|
||||
this.state.el.on({
|
||||
'speed:set': this.onSetSpeed,
|
||||
'speed:render': this.onRenderSpeed
|
||||
});
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
onSetSpeed: function (event, speed) {
|
||||
this.setSpeed(speed, true);
|
||||
},
|
||||
|
||||
onRenderSpeed: function (event, speeds, currentSpeed) {
|
||||
this.render(speeds, currentSpeed);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if playbackRate supports by browser. If browser supports, 1.0
|
||||
* should be returned by playbackRate property. In this case, function
|
||||
* return True. Otherwise, False will be returned.
|
||||
* iOS doesn't support speed change.
|
||||
* @param {object} state The object containing the state of the video
|
||||
* player.
|
||||
* @return {boolean}
|
||||
* true: Browser support playbackRate functionality.
|
||||
* false: Browser doesn't support playbackRate functionality.
|
||||
*/
|
||||
isPlaybackRatesSupported: function (state) {
|
||||
let isHtml5 = state.videoType === 'html5',
|
||||
isTouch = state.isTouch,
|
||||
video = document.createElement('video');
|
||||
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
return !isTouch || (isHtml5 && !Boolean(video.playbackRate));
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens speed menu.
|
||||
* @param {boolean} [bindEvent] Click event will be attached on window.
|
||||
*/
|
||||
openMenu: function (bindEvent) {
|
||||
// When speed entries have focus, the menu stays open on
|
||||
// mouseleave. A clickHandler is added to the window
|
||||
// element to have clicks close the menu when they happen
|
||||
// outside of it.
|
||||
if (bindEvent) {
|
||||
$(window).on('click.speedMenu', this.clickMenuHandler);
|
||||
}
|
||||
|
||||
this.el.addClass('is-opened');
|
||||
this.speedButton
|
||||
.attr('tabindex', -1)
|
||||
.attr('aria-expanded', 'true');
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes speed menu.
|
||||
* @param {boolean} [unBindEvent] Click event will be detached from window.
|
||||
*/
|
||||
closeMenu: function (unBindEvent) {
|
||||
// Remove the previously added clickHandler from window element.
|
||||
if (unBindEvent) {
|
||||
$(window).off('click.speedMenu');
|
||||
}
|
||||
|
||||
this.el.removeClass('is-opened');
|
||||
this.speedButton
|
||||
.attr('tabindex', 0)
|
||||
.attr('aria-expanded', 'false');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets new current speed for the speed control and triggers `speedchange`
|
||||
* event if needed.
|
||||
* @param {string|number} speed Speed to be set.
|
||||
* @param {boolean} [silent] Sets the new speed without triggering
|
||||
* `speedchange` event.
|
||||
* @param {boolean} [forceUpdate] Updates the speed even if it's
|
||||
* not differs from current speed.
|
||||
*/
|
||||
setSpeed: function (speed, silent, forceUpdate) {
|
||||
let newSpeed = this.state.speedToString(speed);
|
||||
if (newSpeed !== this.currentSpeed || forceUpdate) {
|
||||
this.speedsContainer
|
||||
.find('li')
|
||||
.siblings("li[data-speed='" + newSpeed + "']");
|
||||
|
||||
this.speedButton.find('.value').text(newSpeed + 'x');
|
||||
this.currentSpeed = newSpeed;
|
||||
|
||||
if (!silent) {
|
||||
this.el.trigger('speedchange', [newSpeed, this.state.speed]);
|
||||
}
|
||||
}
|
||||
|
||||
this.resetActiveSpeed();
|
||||
this.setActiveSpeed(newSpeed);
|
||||
},
|
||||
|
||||
resetActiveSpeed: function () {
|
||||
let speedOptions = this.speedsContainer.find('li');
|
||||
|
||||
$(speedOptions).each(function (index, el) {
|
||||
$(el).removeClass('is-active')
|
||||
.find('.speed-option')
|
||||
.attr('aria-pressed', 'false');
|
||||
});
|
||||
},
|
||||
|
||||
setActiveSpeed: function (speed) {
|
||||
let speedOption = this.speedsContainer.find('li[data-speed="' + this.state.speedToString(speed) + '"]');
|
||||
|
||||
speedOption.addClass('is-active')
|
||||
.find('.speed-option')
|
||||
.attr('aria-pressed', 'true');
|
||||
|
||||
this.speedButton.attr('title', gettext('Video speed: ') + this.state.speedToString(speed) + 'x');
|
||||
},
|
||||
|
||||
/**
|
||||
* Click event handler for the menu.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
clickMenuHandler: function () {
|
||||
this.closeMenu();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Click event handler for speed links.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
clickLinkHandler: function (event) {
|
||||
let el = $(event.currentTarget).parent(),
|
||||
speed = $(el).data('speed');
|
||||
|
||||
this.resetActiveSpeed();
|
||||
this.setActiveSpeed(speed);
|
||||
this.state.videoCommands.execute('speed', speed);
|
||||
this.closeMenu(true);
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mouseenter event handler for the menu.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
mouseEnterHandler: function () {
|
||||
this.openMenu();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mouseleave event handler for the menu.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
mouseLeaveHandler: function () {
|
||||
// Only close the menu is no speed entry has focus.
|
||||
if (!this.speedLinks.list.is(':focus')) {
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Keydown event handler for the menu.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
keyDownMenuHandler: function (event) {
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
// Open menu and focus on last element of list above it.
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
case KEY.UP:
|
||||
this.openMenu(true);
|
||||
this.speedLinks.last().focus();
|
||||
break;
|
||||
// Close menu.
|
||||
case KEY.ESCAPE:
|
||||
this.closeMenu(true);
|
||||
break;
|
||||
}
|
||||
// We do not stop propagation and default behavior on a TAB
|
||||
// keypress.
|
||||
return event.keyCode === KEY.TAB;
|
||||
},
|
||||
|
||||
/**
|
||||
* Keydown event handler for speed links.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
keyDownLinkHandler: function (event) {
|
||||
// ALT key is used to change (alternate) the function of
|
||||
// other pressed keys. In this, do nothing.
|
||||
if (event.altKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let KEY = $.ui.keyCode,
|
||||
self = this,
|
||||
parent = $(event.currentTarget).parent(),
|
||||
index = parent.index(),
|
||||
speed = parent.data('speed');
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.keyCode) {
|
||||
// Close menu.
|
||||
case KEY.TAB:
|
||||
// Closes menu after 25ms delay to change `tabindex` after
|
||||
// finishing default behavior.
|
||||
setTimeout(function () {
|
||||
self.closeMenu(true);
|
||||
}, 25);
|
||||
|
||||
return true;
|
||||
// Close menu and give focus to speed control.
|
||||
case KEY.ESCAPE:
|
||||
this.closeMenu(true);
|
||||
this.speedButton.focus();
|
||||
|
||||
return false;
|
||||
// Scroll up menu, wrapping at the top. Keep menu open.
|
||||
case KEY.UP:
|
||||
// Shift + Arrows keyboard shortcut might be used by
|
||||
// screen readers. In this, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.speedLinks.prev(index).focus();
|
||||
return false;
|
||||
// Scroll down menu, wrapping at the bottom. Keep menu
|
||||
// open.
|
||||
case KEY.DOWN:
|
||||
// Shift + Arrows keyboard shortcut might be used by
|
||||
// screen readers. In this, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.speedLinks.next(index).focus();
|
||||
return false;
|
||||
// Close menu, give focus to speed control and change
|
||||
// speed.
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.closeMenu(true);
|
||||
this.speedButton.focus();
|
||||
this.setSpeed(this.state.speedToString(speed));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoSpeedControl;
|
||||
698
xmodule/assets/video/public/js/095_video_context_menu.js
Normal file
698
xmodule/assets/video/public/js/095_video_context_menu.js
Normal file
@@ -0,0 +1,698 @@
|
||||
import _ from 'underscore';
|
||||
import Component from './00_component.js';
|
||||
|
||||
let AbstractItem = Component.extend({
|
||||
initialize: function (options) {
|
||||
this.options = $.extend(true, {
|
||||
label: '',
|
||||
prefix: 'edx-',
|
||||
dataAttrs: {menu: this},
|
||||
attrs: {},
|
||||
items: [],
|
||||
callback: $.noop,
|
||||
initialize: $.noop
|
||||
}, options);
|
||||
|
||||
this.id = _.uniqueId();
|
||||
this.element = this.createElement();
|
||||
this.element.attr(this.options.attrs).data(this.options.dataAttrs);
|
||||
this.children = [];
|
||||
this.delegateEvents();
|
||||
this.options.initialize.call(this, this);
|
||||
},
|
||||
destroy: function () {
|
||||
_.invoke(this.getChildren(), 'destroy');
|
||||
this.undelegateEvents();
|
||||
this.getElement().remove();
|
||||
},
|
||||
open: function () {
|
||||
this.getElement().addClass('is-opened');
|
||||
return this;
|
||||
},
|
||||
close: function () {
|
||||
},
|
||||
closeSiblings: function () {
|
||||
_.invoke(this.getSiblings(), 'close');
|
||||
return this;
|
||||
},
|
||||
getElement: function () {
|
||||
return this.element;
|
||||
},
|
||||
addChild: function (child) {
|
||||
let firstChild = null,
|
||||
lastChild = null;
|
||||
if (this.hasChildren()) {
|
||||
lastChild = this.getLastChild();
|
||||
lastChild.next = child;
|
||||
firstChild = this.getFirstChild();
|
||||
firstChild.prev = child;
|
||||
}
|
||||
child.parent = this;
|
||||
child.next = firstChild;
|
||||
child.prev = lastChild;
|
||||
this.children.push(child);
|
||||
return this;
|
||||
},
|
||||
getChildren: function () {
|
||||
// Returns the copy.
|
||||
return this.children.concat();
|
||||
},
|
||||
hasChildren: function () {
|
||||
return this.getChildren().length > 0;
|
||||
},
|
||||
getFirstChild: function () {
|
||||
return _.first(this.children);
|
||||
},
|
||||
getLastChild: function () {
|
||||
return _.last(this.children);
|
||||
},
|
||||
bindEvent: function (element, events, handler) {
|
||||
$(element).on(this.addNamespace(events), handler);
|
||||
return this;
|
||||
},
|
||||
getNext: function () {
|
||||
let item = this.next;
|
||||
while (item.isHidden() && this.id !== item.id) {
|
||||
item = item.next;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
getPrev: function () {
|
||||
let item = this.prev;
|
||||
while (item.isHidden() && this.id !== item.id) {
|
||||
item = item.prev;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
createElement: function () {
|
||||
return null;
|
||||
},
|
||||
getRoot: function () {
|
||||
let item = this;
|
||||
while (item.parent) {
|
||||
item = item.parent;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
populateElement: function () {
|
||||
},
|
||||
focus: function () {
|
||||
this.getElement().focus();
|
||||
this.closeSiblings();
|
||||
return this;
|
||||
},
|
||||
isHidden: function () {
|
||||
return this.getElement().is(':hidden');
|
||||
},
|
||||
getSiblings: function () {
|
||||
let items = [],
|
||||
item = this;
|
||||
while (item.next && item.next.id !== this.id) {
|
||||
item = item.next;
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
select: function () {
|
||||
},
|
||||
unselect: function () {
|
||||
},
|
||||
setLabel: function () {
|
||||
},
|
||||
itemHandler: function () {
|
||||
},
|
||||
keyDownHandler: function () {
|
||||
},
|
||||
delegateEvents: function () {
|
||||
},
|
||||
undelegateEvents: function () {
|
||||
this.getElement().off('.' + this.id);
|
||||
},
|
||||
addNamespace: function (events) {
|
||||
return _.map(events.split(/\s+/), function (event) {
|
||||
return event + '.' + this.id;
|
||||
}, this).join(' ');
|
||||
}
|
||||
});
|
||||
|
||||
let AbstractMenu = AbstractItem.extend({
|
||||
delegateEvents: function () {
|
||||
this.bindEvent(this.getElement(), 'keydown mouseleave mouseover', this.itemHandler.bind(this))
|
||||
.bindEvent(this.getElement(), 'contextmenu', function (event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
populateElement: function () {
|
||||
let fragment = document.createDocumentFragment();
|
||||
|
||||
_.each(this.getChildren(), function (child) {
|
||||
fragment.appendChild(child.populateElement()[0]);
|
||||
}, this);
|
||||
|
||||
this.appendContent([fragment]);
|
||||
this.isRendered = true;
|
||||
return this.getElement();
|
||||
},
|
||||
|
||||
close: function () {
|
||||
this.closeChildren();
|
||||
this.getElement().removeClass('is-opened');
|
||||
return this;
|
||||
},
|
||||
|
||||
closeChildren: function () {
|
||||
_.invoke(this.getChildren(), 'close');
|
||||
return this;
|
||||
},
|
||||
|
||||
itemHandler: function (event) {
|
||||
event.preventDefault();
|
||||
let item = $(event.target).data('menu');
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
this.keyDownHandler.call(this, event, item);
|
||||
break;
|
||||
case 'mouseover':
|
||||
this.mouseOverHandler.call(this, event, item);
|
||||
break;
|
||||
case 'mouseleave':
|
||||
this.mouseLeaveHandler.call(this, event, item);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
keyDownHandler: function () {
|
||||
},
|
||||
mouseOverHandler: function () {
|
||||
},
|
||||
mouseLeaveHandler: function () {
|
||||
}
|
||||
});
|
||||
|
||||
let Menu = AbstractMenu.extend({
|
||||
initialize: function (options, contextmenuElement, container) {
|
||||
this.contextmenuElement = $(contextmenuElement);
|
||||
this.container = $(container);
|
||||
this.overlay = this.getOverlay();
|
||||
AbstractMenu.prototype.initialize.apply(this, arguments);
|
||||
this.build(this, this.options.items);
|
||||
},
|
||||
|
||||
createElement: function () {
|
||||
return $('<ol />', {
|
||||
class: ['contextmenu', this.options.prefix + 'contextmenu'].join(' '),
|
||||
role: 'menu',
|
||||
tabindex: -1
|
||||
});
|
||||
},
|
||||
|
||||
delegateEvents: function () {
|
||||
AbstractMenu.prototype.delegateEvents.call(this);
|
||||
this.bindEvent(this.contextmenuElement, 'contextmenu', this.contextmenuHandler.bind(this))
|
||||
.bindEvent(window, 'resize', _.debounce(this.close.bind(this), 100));
|
||||
return this;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
AbstractMenu.prototype.destroy.call(this);
|
||||
this.overlay.destroy();
|
||||
this.contextmenuElement.removeData('contextmenu');
|
||||
return this;
|
||||
},
|
||||
|
||||
undelegateEvents: function () {
|
||||
AbstractMenu.prototype.undelegateEvents.call(this);
|
||||
this.contextmenuElement.off(this.addNamespace('contextmenu'));
|
||||
this.overlay.undelegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
appendContent: function (content) {
|
||||
let $content = $(content);
|
||||
this.getElement().append($content);
|
||||
return this;
|
||||
},
|
||||
|
||||
addChild: function () {
|
||||
AbstractMenu.prototype.addChild.apply(this, arguments);
|
||||
this.next = this.getFirstChild();
|
||||
this.prev = this.getLastChild();
|
||||
return this;
|
||||
},
|
||||
|
||||
build: function (container, items) {
|
||||
_.each(items, function (item) {
|
||||
let child;
|
||||
if (_.has(item, 'items')) {
|
||||
child = this.build((new Submenu(item, this.contextmenuElement)), item.items);
|
||||
} else {
|
||||
child = new MenuItem(item);
|
||||
}
|
||||
container.addChild(child);
|
||||
}, this);
|
||||
return container;
|
||||
},
|
||||
|
||||
focus: function () {
|
||||
this.getElement().focus();
|
||||
return this;
|
||||
},
|
||||
|
||||
open: function () {
|
||||
let $menu = (this.isRendered) ? this.getElement() : this.populateElement();
|
||||
this.container.append($menu);
|
||||
AbstractItem.prototype.open.call(this);
|
||||
this.overlay.show(this.container);
|
||||
return this;
|
||||
},
|
||||
|
||||
close: function () {
|
||||
AbstractMenu.prototype.close.call(this);
|
||||
this.getElement().detach();
|
||||
this.overlay.hide();
|
||||
return this;
|
||||
},
|
||||
|
||||
position: function (event) {
|
||||
this.getElement().position({
|
||||
my: 'left top',
|
||||
of: event,
|
||||
collision: 'flipfit flipfit',
|
||||
within: this.contextmenuElement
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
pointInContainerBox: function (x, y) {
|
||||
let containerOffset = this.contextmenuElement.offset(),
|
||||
containerBox = {
|
||||
x0: containerOffset.left,
|
||||
y0: containerOffset.top,
|
||||
x1: containerOffset.left + this.contextmenuElement.outerWidth(),
|
||||
y1: containerOffset.top + this.contextmenuElement.outerHeight()
|
||||
};
|
||||
return containerBox.x0 <= x && x <= containerBox.x1 && containerBox.y0 <= y && y <= containerBox.y1;
|
||||
},
|
||||
|
||||
getOverlay: function () {
|
||||
return new Overlay(
|
||||
this.close.bind(this),
|
||||
function (event) {
|
||||
event.preventDefault();
|
||||
if (this.pointInContainerBox(event.pageX, event.pageY)) {
|
||||
this.position(event).focus();
|
||||
this.closeChildren();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
contextmenuHandler: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.open().position(event).focus();
|
||||
},
|
||||
|
||||
keyDownHandler: function (event, item) {
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.UP:
|
||||
item.getPrev().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.DOWN:
|
||||
item.getNext().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.TAB:
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.ESCAPE:
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
let Overlay = Component.extend({
|
||||
ns: '.overlay',
|
||||
initialize: function (clickHandler, contextmenuHandler) {
|
||||
this.element = $('<div />', {
|
||||
class: 'overlay'
|
||||
});
|
||||
this.clickHandler = clickHandler;
|
||||
this.contextmenuHandler = contextmenuHandler;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.getElement().remove();
|
||||
this.undelegateEvents();
|
||||
},
|
||||
|
||||
getElement: function () {
|
||||
return this.element;
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.getElement().detach();
|
||||
this.undelegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function (container) {
|
||||
let $elem = $(this.getElement());
|
||||
$(container).append($elem);
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
delegateEvents: function () {
|
||||
let self = this;
|
||||
$(document)
|
||||
.on('click' + this.ns, function () {
|
||||
if (_.isFunction(self.clickHandler)) {
|
||||
self.clickHandler.apply(this, arguments);
|
||||
}
|
||||
self.hide();
|
||||
})
|
||||
.on('contextmenu' + this.ns, function () {
|
||||
if (_.isFunction(self.contextmenuHandler)) {
|
||||
self.contextmenuHandler.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
undelegateEvents: function () {
|
||||
$(document).off(this.ns);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
let Submenu = AbstractMenu.extend({
|
||||
initialize: function (options, contextmenuElement) {
|
||||
this.contextmenuElement = contextmenuElement;
|
||||
AbstractMenu.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
createElement: function () {
|
||||
let $spanElem,
|
||||
$listElem,
|
||||
$element = $('<li />', {
|
||||
class: ['submenu-item', 'menu-item', this.options.prefix + 'submenu-item'].join(' '),
|
||||
'aria-expanded': 'false',
|
||||
'aria-haspopup': 'true',
|
||||
'aria-labelledby': 'submenu-item-label-' + this.id,
|
||||
role: 'menuitem',
|
||||
tabindex: -1
|
||||
});
|
||||
|
||||
$spanElem = $('<span />', {
|
||||
id: 'submenu-item-label-' + this.id,
|
||||
text: this.options.label
|
||||
});
|
||||
this.label = $spanElem.appendTo($element);
|
||||
|
||||
$listElem = $('<ol />', {
|
||||
class: ['submenu', this.options.prefix + 'submenu'].join(' '),
|
||||
role: 'menu'
|
||||
});
|
||||
|
||||
this.list = $listElem.appendTo($element);
|
||||
|
||||
return $element;
|
||||
},
|
||||
|
||||
appendContent: function (content) {
|
||||
let $content = $(content);
|
||||
this.list.append($content);
|
||||
return this;
|
||||
},
|
||||
|
||||
setLabel: function (label) {
|
||||
this.label.text(label);
|
||||
return this;
|
||||
},
|
||||
|
||||
openKeyboard: function () {
|
||||
if (this.hasChildren()) {
|
||||
this.open();
|
||||
this.getFirstChild().focus();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
keyDownHandler: function (event) {
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.LEFT:
|
||||
this.close().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.RIGHT:
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.openKeyboard();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
open: function () {
|
||||
AbstractMenu.prototype.open.call(this);
|
||||
this.getElement().attr({'aria-expanded': 'true'});
|
||||
this.position();
|
||||
return this;
|
||||
},
|
||||
|
||||
close: function () {
|
||||
AbstractMenu.prototype.close.call(this);
|
||||
this.getElement().attr({'aria-expanded': 'false'});
|
||||
return this;
|
||||
},
|
||||
|
||||
position: function () {
|
||||
this.list.position({
|
||||
my: 'left top',
|
||||
at: 'right top',
|
||||
of: this.getElement(),
|
||||
collision: 'flipfit flipfit',
|
||||
within: this.contextmenuElement
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
mouseOverHandler: function () {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(this.open.bind(this), 200);
|
||||
this.focus();
|
||||
},
|
||||
|
||||
mouseLeaveHandler: function () {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(this.close.bind(this), 200);
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
|
||||
let MenuItem = AbstractItem.extend({
|
||||
createElement: function () {
|
||||
let classNames = [
|
||||
'menu-item', this.options.prefix + 'menu-item',
|
||||
this.options.isSelected ? 'is-selected' : ''
|
||||
].join(' ');
|
||||
|
||||
return $('<li />', {
|
||||
class: classNames,
|
||||
'aria-selected': this.options.isSelected ? 'true' : 'false',
|
||||
role: 'menuitem',
|
||||
tabindex: -1,
|
||||
text: this.options.label
|
||||
});
|
||||
},
|
||||
|
||||
populateElement: function () {
|
||||
return this.getElement();
|
||||
},
|
||||
|
||||
delegateEvents: function () {
|
||||
this.bindEvent(this.getElement(), 'click keydown contextmenu mouseover', this.itemHandler.bind(this));
|
||||
return this;
|
||||
},
|
||||
|
||||
setLabel: function (label) {
|
||||
this.getElement().text(label);
|
||||
return this;
|
||||
},
|
||||
|
||||
select: function (event) {
|
||||
this.options.callback.call(this, event, this, this.options);
|
||||
this.getElement()
|
||||
.addClass('is-selected')
|
||||
.attr({'aria-selected': 'true'});
|
||||
_.invoke(this.getSiblings(), 'unselect');
|
||||
// Hide the menu.
|
||||
this.getRoot().close();
|
||||
return this;
|
||||
},
|
||||
|
||||
unselect: function () {
|
||||
this.getElement()
|
||||
.removeClass('is-selected')
|
||||
.attr({'aria-selected': 'false'});
|
||||
return this;
|
||||
},
|
||||
|
||||
itemHandler: function (event) {
|
||||
event.preventDefault();
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.type) {
|
||||
case 'contextmenu':
|
||||
case 'click':
|
||||
this.select();
|
||||
break;
|
||||
case 'mouseover':
|
||||
this.focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case 'keydown':
|
||||
this.keyDownHandler.call(this, event, this);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
keyDownHandler: function (event) {
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.RIGHT:
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.select();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
let VideoContextMenu = function(state, i18n) {
|
||||
let speedCallback = function(event, menuitem, options) {
|
||||
let speed = parseFloat(options.label);
|
||||
state.videoCommands.execute('speed', speed);
|
||||
}
|
||||
let options = {
|
||||
items: [{
|
||||
label: i18n.Play,
|
||||
callback: function () {
|
||||
state.videoCommands.execute('togglePlayback');
|
||||
},
|
||||
initialize: function (menuitem) {
|
||||
state.el.on({
|
||||
play: function () {
|
||||
menuitem.setLabel(i18n.Pause);
|
||||
},
|
||||
pause: function () {
|
||||
menuitem.setLabel(i18n.Play);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: state.videoVolumeControl.getMuteStatus() ? i18n.Unmute : i18n.Mute,
|
||||
callback: function () {
|
||||
state.videoCommands.execute('toggleMute');
|
||||
},
|
||||
initialize: function (menuitem) {
|
||||
state.el.on({
|
||||
volumechange: function () {
|
||||
if (state.videoVolumeControl.getMuteStatus()) {
|
||||
menuitem.setLabel(i18n.Unmute);
|
||||
} else {
|
||||
menuitem.setLabel(i18n.Mute);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: i18n['Fill browser'],
|
||||
callback: function () {
|
||||
state.videoCommands.execute('toggleFullScreen');
|
||||
},
|
||||
initialize: function (menuitem) {
|
||||
state.el.on({
|
||||
fullscreen: function (event, isFullscreen) {
|
||||
if (isFullscreen) {
|
||||
menuitem.setLabel(i18n['Exit full browser']);
|
||||
} else {
|
||||
menuitem.setLabel(i18n['Fill browser']);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: i18n.Speed,
|
||||
items: _.map(state.speeds, function (speed) {
|
||||
let isSelected = parseFloat(speed) === state.speed;
|
||||
return {
|
||||
label: speed + 'x', callback: speedCallback, speed: speed, isSelected: isSelected
|
||||
};
|
||||
}),
|
||||
initialize: function (menuitem) {
|
||||
state.el.on({
|
||||
speedchange: function (event, speed) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
let item = menuitem.getChildren().filter(function (item) {
|
||||
return item.options.speed === speed;
|
||||
})[0];
|
||||
if (item) {
|
||||
item.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
$.fn.contextmenu = function(container, options) {
|
||||
return this.each(function () {
|
||||
$(this).data('contextmenu', new Menu(options, this, container));
|
||||
});
|
||||
};
|
||||
|
||||
if (!state.isYoutubeType()) {
|
||||
state.el.find('video').contextmenu(state.el, options);
|
||||
state.el.on('destroy', function () {
|
||||
let contextmenu = $(this).find('video').data('contextmenu');
|
||||
if (contextmenu) {
|
||||
contextmenu.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
}
|
||||
|
||||
export default VideoContextMenu
|
||||
108
xmodule/assets/video/public/js/09_bumper.js
Normal file
108
xmodule/assets/video/public/js/09_bumper.js
Normal file
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* VideoBumper module.
|
||||
* @exports video/09_bumper.js
|
||||
* @constructor
|
||||
* @param {Object} player The player factory.
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let VideoBumper = function(player, state) {
|
||||
if (!(this instanceof VideoBumper)) {
|
||||
return new VideoBumper(player, state);
|
||||
}
|
||||
|
||||
_.bindAll(
|
||||
this, 'showMainVideoHandler', 'destroy', 'skipByDuration', 'destroyAndResolve'
|
||||
);
|
||||
this.dfd = $.Deferred();
|
||||
this.element = state.el;
|
||||
this.element.addClass('is-bumper');
|
||||
this.player = player;
|
||||
this.state = state;
|
||||
this.doNotShowAgain = false;
|
||||
this.state.videoBumper = this;
|
||||
this.bindHandlers();
|
||||
this.initialize();
|
||||
this.maxBumperDuration = 35; // seconds
|
||||
};
|
||||
|
||||
VideoBumper.prototype = {
|
||||
initialize: function() {
|
||||
this.player();
|
||||
},
|
||||
|
||||
getPromise: function() {
|
||||
return this.dfd.promise();
|
||||
},
|
||||
|
||||
showMainVideoHandler: function() {
|
||||
this.state.storage.setItem('isBumperShown', true);
|
||||
setTimeout(function() {
|
||||
this.saveState();
|
||||
this.showMainVideo();
|
||||
}.bind(this), 20);
|
||||
},
|
||||
|
||||
destroyAndResolve: function() {
|
||||
this.destroy();
|
||||
this.dfd.resolve();
|
||||
},
|
||||
|
||||
showMainVideo: function() {
|
||||
if (this.state.videoPlayer) {
|
||||
this.destroyAndResolve();
|
||||
} else {
|
||||
this.state.el.on('initialize', this.destroyAndResolve);
|
||||
}
|
||||
},
|
||||
|
||||
skip: function() {
|
||||
this.element.trigger('skip', [this.doNotShowAgain]);
|
||||
this.showMainVideoHandler();
|
||||
},
|
||||
|
||||
skipAndDoNotShowAgain: function() {
|
||||
this.doNotShowAgain = true;
|
||||
this.skip();
|
||||
},
|
||||
|
||||
skipByDuration: function(event, time) {
|
||||
if (time > this.maxBumperDuration) {
|
||||
this.element.trigger('ended');
|
||||
}
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
let events = ['ended', 'error'].join(' ');
|
||||
this.element.on(events, this.showMainVideoHandler);
|
||||
this.element.on('timeupdate', this.skipByDuration);
|
||||
},
|
||||
|
||||
saveState: function() {
|
||||
let info = {bumper_last_view_date: true};
|
||||
if (this.doNotShowAgain) {
|
||||
_.extend(info, {bumper_do_not_show_again: true});
|
||||
}
|
||||
if (this.state.videoSaveStatePlugin) {
|
||||
this.state.videoSaveStatePlugin.saveState(true, info);
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
let events = ['ended', 'error'].join(' ');
|
||||
this.element.off(events, this.showMainVideoHandler);
|
||||
this.element.off({
|
||||
timeupdate: this.skipByDuration,
|
||||
initialize: this.destroyAndResolve
|
||||
});
|
||||
this.element.removeClass('is-bumper');
|
||||
if (_.isFunction(this.state.videoPlayer.destroy)) {
|
||||
this.state.videoPlayer.destroy();
|
||||
}
|
||||
delete this.state.videoBumper;
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoBumper;
|
||||
201
xmodule/assets/video/public/js/09_completion.js
Normal file
201
xmodule/assets/video/public/js/09_completion.js
Normal file
@@ -0,0 +1,201 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Completion handler
|
||||
* @exports video/09_completion.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let VideoCompletionHandler = function(state) {
|
||||
if (!(this instanceof VideoCompletionHandler)) {
|
||||
return new VideoCompletionHandler(state);
|
||||
}
|
||||
this.state = state;
|
||||
this.state.completionHandler = this;
|
||||
this.initialize();
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VideoCompletionHandler.prototype = {
|
||||
|
||||
/** Tears down the VideoCompletionHandler.
|
||||
*
|
||||
* * Removes backreferences from this.state to this.
|
||||
* * Turns off signal handlers.
|
||||
*/
|
||||
destroy: function() {
|
||||
this.el.remove();
|
||||
this.el.off('timeupdate.completion');
|
||||
this.el.off('ended.completion');
|
||||
delete this.state.completionHandler;
|
||||
},
|
||||
|
||||
/** Initializes the VideoCompletionHandler.
|
||||
*
|
||||
* This sets all the instance variables needed to perform
|
||||
* completion calculations.
|
||||
*/
|
||||
initialize: function() {
|
||||
// Attributes with "Time" in the name refer to the number of seconds since
|
||||
// the beginning of the video, except for lastSentTime, which refers to a
|
||||
// timestamp in seconds since the Unix epoch.
|
||||
this.lastSentTime = undefined;
|
||||
this.isComplete = false;
|
||||
this.completionPercentage = this.state.config.completionPercentage;
|
||||
this.startTime = this.state.config.startTime;
|
||||
this.endTime = this.state.config.endTime;
|
||||
this.isEnabled = this.state.config.completionEnabled;
|
||||
if (this.endTime) {
|
||||
this.completeAfterTime = this.calculateCompleteAfterTime(this.startTime, this.endTime);
|
||||
}
|
||||
if (this.isEnabled) {
|
||||
this.bindHandlers();
|
||||
}
|
||||
},
|
||||
|
||||
/** Bind event handler callbacks.
|
||||
*
|
||||
* When ended is triggered, mark the video complete
|
||||
* unconditionally.
|
||||
*
|
||||
* When timeupdate is triggered, check to see if the user has
|
||||
* passed the completeAfterTime in the video, and if so, mark the
|
||||
* video complete.
|
||||
*
|
||||
* When destroy is triggered, clean up outstanding resources.
|
||||
*/
|
||||
bindHandlers: function() {
|
||||
let self = this;
|
||||
|
||||
/** Event handler to check if the video is complete, and submit
|
||||
* a completion if it is.
|
||||
*
|
||||
* If the timeupdate handler doesn't fire after the required
|
||||
* percentage, this will catch any fully complete videos.
|
||||
*/
|
||||
this.state.el.on('ended.completion', function() {
|
||||
self.handleEnded();
|
||||
});
|
||||
|
||||
/** Event handler to check video progress, and mark complete if
|
||||
* greater than completionPercentage
|
||||
*/
|
||||
this.state.el.on('timeupdate.completion', function(ev, currentTime) {
|
||||
self.handleTimeUpdate(currentTime);
|
||||
});
|
||||
|
||||
/** Event handler to receive youtube metadata (if we even are a youtube link),
|
||||
* and mark complete, if youtube will insist on hosting the video itself.
|
||||
*/
|
||||
this.state.el.on('metadata_received', function() {
|
||||
self.checkMetadata();
|
||||
});
|
||||
|
||||
/** Event handler to clean up resources when the video player
|
||||
* is destroyed.
|
||||
*/
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
},
|
||||
|
||||
/** Handler to call when the ended event is triggered */
|
||||
handleEnded: function() {
|
||||
if (this.isComplete) {
|
||||
return;
|
||||
}
|
||||
this.markCompletion();
|
||||
},
|
||||
|
||||
/** Handler to call when a timeupdate event is triggered */
|
||||
handleTimeUpdate: function(currentTime) {
|
||||
let duration;
|
||||
if (this.isComplete) {
|
||||
return;
|
||||
}
|
||||
if (this.lastSentTime !== undefined && currentTime - this.lastSentTime < this.repostDelaySeconds()) {
|
||||
// Throttle attempts to submit in case of network issues
|
||||
return;
|
||||
}
|
||||
if (this.completeAfterTime === undefined) {
|
||||
// Duration is not available at initialization time
|
||||
duration = this.state.videoPlayer.duration();
|
||||
if (!duration) {
|
||||
// duration is not yet set. Wait for another event,
|
||||
// or fall back to 'ended' handler.
|
||||
return;
|
||||
}
|
||||
this.completeAfterTime = this.calculateCompleteAfterTime(this.startTime, duration);
|
||||
}
|
||||
|
||||
if (currentTime > this.completeAfterTime) {
|
||||
this.markCompletion(currentTime);
|
||||
}
|
||||
},
|
||||
|
||||
/** Handler to call when youtube metadata is received */
|
||||
checkMetadata: function() {
|
||||
let metadata = this.state.metadata[this.state.youtubeId()];
|
||||
|
||||
// https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating.ytRating
|
||||
if (metadata && metadata.contentRating && metadata.contentRating.ytRating === 'ytAgeRestricted') {
|
||||
// Age-restricted videos won't play in embedded players. Instead, they ask you to watch it on
|
||||
// youtube itself. Which means we can't notice if they complete it. Rather than leaving an
|
||||
// incompletable video in the course, let's just mark it complete right now.
|
||||
if (!this.isComplete) {
|
||||
this.markCompletion();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** Submit completion to the LMS */
|
||||
markCompletion: function(currentTime) {
|
||||
let self = this;
|
||||
let errmsg;
|
||||
this.isComplete = true;
|
||||
this.lastSentTime = currentTime;
|
||||
this.state.el.trigger('complete');
|
||||
if (this.state.config.publishCompletionUrl) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: this.state.config.publishCompletionUrl,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({completion: 1.0}),
|
||||
success: function() {
|
||||
self.state.el.off('timeupdate.completion');
|
||||
self.state.el.off('ended.completion');
|
||||
},
|
||||
error: function(xhr) {
|
||||
/* eslint-disable no-console */
|
||||
self.complete = false;
|
||||
errmsg = 'Failed to submit completion';
|
||||
if (xhr.responseJSON !== undefined) {
|
||||
errmsg += ': ' + xhr.responseJSON.error;
|
||||
}
|
||||
console.warn(errmsg);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
});
|
||||
} else {
|
||||
/* eslint-disable no-console */
|
||||
console.warn('publishCompletionUrl not defined');
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
|
||||
/** Determine what point in the video (in seconds from the
|
||||
* beginning) counts as complete.
|
||||
*/
|
||||
calculateCompleteAfterTime: function(startTime, endTime) {
|
||||
return startTime + (endTime - startTime) * this.completionPercentage;
|
||||
},
|
||||
|
||||
/** How many seconds to wait after a POST fails to try again. */
|
||||
repostDelaySeconds: function() {
|
||||
return 3.0;
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoCompletionHandler;
|
||||
112
xmodule/assets/video/public/js/09_events_bumper_plugin.js
Normal file
112
xmodule/assets/video/public/js/09_events_bumper_plugin.js
Normal file
@@ -0,0 +1,112 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
/**
|
||||
* Events module.
|
||||
* @exports video/09_events_bumper_plugin.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @param {Object} options
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let EventsBumperPlugin = function(state, i18n, options) {
|
||||
if (!(this instanceof EventsBumperPlugin)) {
|
||||
return new EventsBumperPlugin(state, i18n, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onReady', 'onPlay', 'onEnded', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip',
|
||||
'onShowCaptions', 'onHideCaptions', 'destroy');
|
||||
this.state = state;
|
||||
this.options = _.extend({}, options);
|
||||
this.state.videoEventsBumperPlugin = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
EventsBumperPlugin.moduleName = 'EventsBumperPlugin';
|
||||
EventsBumperPlugin.prototype = {
|
||||
destroy: function() {
|
||||
this.state.el.off(this.events);
|
||||
delete this.state.videoEventsBumperPlugin;
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.events = {
|
||||
ready: this.onReady,
|
||||
play: this.onPlay,
|
||||
'ended stop': this.onEnded,
|
||||
skip: this.onSkip,
|
||||
'language_menu:show': this.onShowLanguageMenu,
|
||||
'language_menu:hide': this.onHideLanguageMenu,
|
||||
'captions:show': this.onShowCaptions,
|
||||
'captions:hide': this.onHideCaptions,
|
||||
destroy: this.destroy
|
||||
};
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
this.state.el.on(this.events);
|
||||
},
|
||||
|
||||
onReady: function() {
|
||||
this.log('edx.video.bumper.loaded');
|
||||
},
|
||||
|
||||
onPlay: function() {
|
||||
this.log('edx.video.bumper.played', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onEnded: function() {
|
||||
this.log('edx.video.bumper.stopped', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onSkip: function(event, doNotShowAgain) {
|
||||
let info = {currentTime: this.getCurrentTime()};
|
||||
let eventName = 'edx.video.bumper.' + (doNotShowAgain ? 'dismissed' : 'skipped');
|
||||
this.log(eventName, info);
|
||||
},
|
||||
|
||||
onShowLanguageMenu: function() {
|
||||
this.log('edx.video.bumper.transcript.menu.shown');
|
||||
},
|
||||
|
||||
onHideLanguageMenu: function() {
|
||||
this.log('edx.video.bumper.transcript.menu.hidden');
|
||||
},
|
||||
|
||||
onShowCaptions: function() {
|
||||
this.log('edx.video.bumper.transcript.shown', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onHideCaptions: function() {
|
||||
this.log('edx.video.bumper.transcript.hidden', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
getCurrentTime: function() {
|
||||
let player = this.state.videoPlayer;
|
||||
return player ? player.currentTime : 0;
|
||||
},
|
||||
|
||||
getDuration: function() {
|
||||
let player = this.state.videoPlayer;
|
||||
return player ? player.duration() : 0;
|
||||
},
|
||||
|
||||
log: function(eventName, data) {
|
||||
let logInfo = _.extend({
|
||||
host_component_id: this.state.id,
|
||||
bumper_id: this.state.config.sources[0] || '',
|
||||
duration: this.getDuration(),
|
||||
code: 'html5'
|
||||
}, data, this.options.data);
|
||||
Logger.log(eventName, logInfo);
|
||||
}
|
||||
};
|
||||
|
||||
export default EventsBumperPlugin;
|
||||
177
xmodule/assets/video/public/js/09_events_plugin.js
Normal file
177
xmodule/assets/video/public/js/09_events_plugin.js
Normal file
@@ -0,0 +1,177 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
/**
|
||||
* Events module.
|
||||
* @exports video/09_events_plugin.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @param {Object} options
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let EventsPlugin = function(state, i18n, options) {
|
||||
if (!(this instanceof EventsPlugin)) {
|
||||
return new EventsPlugin(state, i18n, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onComplete', 'onEnded', 'onSeek',
|
||||
'onSpeedChange', 'onAutoAdvanceChange', 'onShowLanguageMenu', 'onHideLanguageMenu',
|
||||
'onSkip', 'onShowTranscript', 'onHideTranscript', 'onShowCaptions', 'onHideCaptions',
|
||||
'destroy');
|
||||
|
||||
this.state = state;
|
||||
this.options = _.extend({}, options);
|
||||
this.state.videoEventsPlugin = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
EventsPlugin.moduleName = 'EventsPlugin';
|
||||
EventsPlugin.prototype = {
|
||||
destroy: function() {
|
||||
this.state.el.off(this.events);
|
||||
delete this.state.videoEventsPlugin;
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.events = {
|
||||
ready: this.onReady,
|
||||
play: this.onPlay,
|
||||
pause: this.onPause,
|
||||
complete: this.onComplete,
|
||||
'ended stop': this.onEnded,
|
||||
seek: this.onSeek,
|
||||
skip: this.onSkip,
|
||||
speedchange: this.onSpeedChange,
|
||||
autoadvancechange: this.onAutoAdvanceChange,
|
||||
'language_menu:show': this.onShowLanguageMenu,
|
||||
'language_menu:hide': this.onHideLanguageMenu,
|
||||
'transcript:show': this.onShowTranscript,
|
||||
'transcript:hide': this.onHideTranscript,
|
||||
'captions:show': this.onShowCaptions,
|
||||
'captions:hide': this.onHideCaptions,
|
||||
destroy: this.destroy
|
||||
};
|
||||
this.bindHandlers();
|
||||
this.emitPlayVideoEvent = true;
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
this.state.el.on(this.events);
|
||||
},
|
||||
|
||||
onReady: function() {
|
||||
this.log('load_video');
|
||||
},
|
||||
|
||||
onPlay: function() {
|
||||
if (this.emitPlayVideoEvent) {
|
||||
this.log('play_video', {currentTime: this.getCurrentTime()});
|
||||
this.emitPlayVideoEvent = false;
|
||||
}
|
||||
},
|
||||
|
||||
onPause: function() {
|
||||
this.log('pause_video', {currentTime: this.getCurrentTime()});
|
||||
this.emitPlayVideoEvent = true;
|
||||
},
|
||||
|
||||
onComplete: function() {
|
||||
this.log('complete_video', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onEnded: function() {
|
||||
this.log('stop_video', {currentTime: this.getCurrentTime()});
|
||||
this.emitPlayVideoEvent = true;
|
||||
},
|
||||
|
||||
onSkip: function(event, doNotShowAgain) {
|
||||
let info = {currentTime: this.getCurrentTime()};
|
||||
let eventName = doNotShowAgain ? 'do_not_show_again_video' : 'skip_video';
|
||||
this.log(eventName, info);
|
||||
},
|
||||
|
||||
onSeek: function(event, time, oldTime, type) {
|
||||
this.log('seek_video', {
|
||||
old_time: oldTime,
|
||||
new_time: time,
|
||||
type: type
|
||||
});
|
||||
this.emitPlayVideoEvent = true;
|
||||
},
|
||||
|
||||
onSpeedChange: function(event, newSpeed, oldSpeed) {
|
||||
this.log('speed_change_video', {
|
||||
current_time: this.getCurrentTime(),
|
||||
old_speed: this.state.speedToString(oldSpeed),
|
||||
new_speed: this.state.speedToString(newSpeed)
|
||||
});
|
||||
},
|
||||
|
||||
onAutoAdvanceChange: function(event, enabled) {
|
||||
this.log('auto_advance_change_video', {
|
||||
enabled: enabled
|
||||
});
|
||||
},
|
||||
|
||||
onShowLanguageMenu: function() {
|
||||
this.log('edx.video.language_menu.shown');
|
||||
},
|
||||
|
||||
onHideLanguageMenu: function() {
|
||||
this.log('edx.video.language_menu.hidden', {language: this.getCurrentLanguage()});
|
||||
},
|
||||
|
||||
onShowTranscript: function() {
|
||||
this.log('show_transcript', {current_time: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onHideTranscript: function() {
|
||||
this.log('hide_transcript', {current_time: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onShowCaptions: function() {
|
||||
this.log('edx.video.closed_captions.shown', {current_time: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onHideCaptions: function() {
|
||||
this.log('edx.video.closed_captions.hidden', {current_time: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
getCurrentTime: function() {
|
||||
let player = this.state.videoPlayer;
|
||||
let startTime = this.state.config.startTime;
|
||||
let currentTime = player ? player.currentTime : 0;
|
||||
// if video didn't start from 0(it's a subsection of video), subtract the additional time at start
|
||||
if (startTime) {
|
||||
currentTime = currentTime ? currentTime - startTime : 0;
|
||||
}
|
||||
return currentTime;
|
||||
},
|
||||
|
||||
getCurrentLanguage: function() {
|
||||
let language = this.state.lang;
|
||||
return language;
|
||||
},
|
||||
|
||||
log: function(eventName, data) {
|
||||
// use startTime and endTime to calculate the duration to handle the case where only a subsection of video is used
|
||||
let endTime = this.state.config.endTime || this.state.duration;
|
||||
let startTime = this.state.config.startTime || 0;
|
||||
|
||||
let logInfo = _.extend({
|
||||
id: this.state.id,
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
code: this.state.isYoutubeType() ? this.state.youtubeId() : this.state.canPlayHLS ? 'hls' : 'html5',
|
||||
duration: endTime - startTime
|
||||
}, data, this.options.data);
|
||||
Logger.log(eventName, logInfo);
|
||||
}
|
||||
};
|
||||
|
||||
export default EventsPlugin;
|
||||
96
xmodule/assets/video/public/js/09_play_pause_control.js
Normal file
96
xmodule/assets/video/public/js/09_play_pause_control.js
Normal file
@@ -0,0 +1,96 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
/**
|
||||
* Play/pause control module.
|
||||
* @exports video/09_play_pause_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let PlayPauseControl = function(state, i18n) {
|
||||
if (!(this instanceof PlayPauseControl)) {
|
||||
return new PlayPauseControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'play', 'pause', 'onClick', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoPlayPauseControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
PlayPauseControl.prototype = {
|
||||
template: [
|
||||
'<button class="control video_control play" aria-disabled="false" aria-label="',
|
||||
gettext('Play'),
|
||||
'">',
|
||||
'<span class="icon fa fa-play" aria-hidden="true"></span>',
|
||||
'</button>'
|
||||
].join(''),
|
||||
|
||||
destroy: function() {
|
||||
this.el.remove();
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
delete this.state.videoPlayPauseControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.el = $(this.template);
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
this.state.el.find('.vcr').prepend(this.el);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on({
|
||||
click: this.onClick
|
||||
});
|
||||
this.state.el.on({
|
||||
play: this.play,
|
||||
'pause ended': this.pause,
|
||||
destroy: this.destroy
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(event) {
|
||||
event.preventDefault();
|
||||
this.state.videoCommands.execute('togglePlayback');
|
||||
},
|
||||
|
||||
play: function() {
|
||||
this.el
|
||||
.addClass('pause')
|
||||
.removeClass('play')
|
||||
.attr({title: gettext('Pause'), 'aria-label': gettext('Pause')})
|
||||
.find('.icon')
|
||||
.removeClass('fa-play')
|
||||
.addClass('fa-pause');
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
this.el
|
||||
.removeClass('pause')
|
||||
.addClass('play')
|
||||
.attr({title: gettext('Play'), 'aria-label': gettext('Play')})
|
||||
.find('.icon')
|
||||
.removeClass('fa-pause')
|
||||
.addClass('fa-play');
|
||||
}
|
||||
};
|
||||
|
||||
export default PlayPauseControl;
|
||||
84
xmodule/assets/video/public/js/09_play_placeholder.js
Normal file
84
xmodule/assets/video/public/js/09_play_placeholder.js
Normal file
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Play placeholder control module.
|
||||
* @exports video/09_play_placeholder.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let PlayPlaceholder = function(state, i18n) {
|
||||
if (!(this instanceof PlayPlaceholder)) {
|
||||
return new PlayPlaceholder(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onClick', 'hide', 'show', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoPlayPlaceholder = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
PlayPlaceholder.prototype = {
|
||||
destroy: function() {
|
||||
this.el.off('click', this.onClick);
|
||||
this.state.el.on({
|
||||
destroy: this.destroy,
|
||||
play: this.hide,
|
||||
'ended pause': this.show
|
||||
});
|
||||
this.hide();
|
||||
delete this.state.videoPlayPlaceholder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates whether the placeholder should be shown. We display it
|
||||
* for html5 videos on iPad and Android devices.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
shouldBeShown: function() {
|
||||
return /iPad|Android/i.test(this.state.isTouch[0]) && !this.state.isYoutubeType();
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
if (!this.shouldBeShown()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el = this.state.el.find('.btn-play');
|
||||
this.bindHandlers();
|
||||
this.show();
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.state.el.on({
|
||||
destroy: this.destroy,
|
||||
play: this.hide,
|
||||
'ended pause': this.show
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
this.state.videoCommands.execute('play');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.el
|
||||
.addClass('is-hidden')
|
||||
.attr({'aria-hidden': 'true', tabindex: -1});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.el
|
||||
.removeClass('is-hidden')
|
||||
.attr({'aria-hidden': 'false', tabindex: 0});
|
||||
}
|
||||
};
|
||||
|
||||
export default PlayPlaceholder;
|
||||
86
xmodule/assets/video/public/js/09_play_skip_control.js
Normal file
86
xmodule/assets/video/public/js/09_play_skip_control.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Play/skip control module.
|
||||
* @exports video/09_play_skip_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let PlaySkipControl = function(state, i18n) {
|
||||
if (!(this instanceof PlaySkipControl)) {
|
||||
return new PlaySkipControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'play', 'onClick', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoPlaySkipControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
PlaySkipControl.prototype = {
|
||||
template: [
|
||||
'<button class="control video_control play play-skip-control" title="',
|
||||
gettext('Play'),
|
||||
'">',
|
||||
'<span class="icon fa fa-play" aria-hidden="true"></span>',
|
||||
'</button>'
|
||||
].join(''),
|
||||
|
||||
destroy: function() {
|
||||
this.el.remove();
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
delete this.state.videoPlaySkipControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.el = $(this.template);
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
this.state.el.find('.vcr').prepend(this.el);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.state.el.on({
|
||||
play: this.play,
|
||||
destroy: this.destroy
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(event) {
|
||||
event.preventDefault();
|
||||
if (this.state.videoPlayer.isPlaying()) {
|
||||
this.state.videoCommands.execute('skip');
|
||||
} else {
|
||||
this.state.videoCommands.execute('play');
|
||||
}
|
||||
},
|
||||
|
||||
play: function() {
|
||||
this.el
|
||||
.removeClass('play')
|
||||
.addClass('skip')
|
||||
.attr('title', gettext('Skip'))
|
||||
.find('.icon')
|
||||
.removeClass('fa-play')
|
||||
.addClass('fa-step-forward');
|
||||
// Disable possibility to pause the video.
|
||||
this.state.el.find('video').off('click');
|
||||
}
|
||||
};
|
||||
|
||||
export default PlaySkipControl;
|
||||
62
xmodule/assets/video/public/js/09_poster.js
Normal file
62
xmodule/assets/video/public/js/09_poster.js
Normal file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
let VideoPoster = function(element, options) {
|
||||
if (!(this instanceof VideoPoster)) {
|
||||
return new VideoPoster(element, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onClick', 'destroy');
|
||||
this.element = element;
|
||||
this.container = element.find('.video-player');
|
||||
this.options = options || {};
|
||||
this.initialize();
|
||||
};
|
||||
|
||||
VideoPoster.moduleName = 'Poster';
|
||||
VideoPoster.prototype = {
|
||||
template: _.template([
|
||||
'<div class="video-pre-roll is-<%- type %> poster" ',
|
||||
'style="background-image: url(<%- url %>)">',
|
||||
'<button class="btn-play btn-pre-roll">',
|
||||
'<img src="/static/images/play.png" alt="">',
|
||||
'<span class="sr">', gettext('Play video'), '</span>',
|
||||
'</button>',
|
||||
'</div>'
|
||||
].join('')),
|
||||
|
||||
initialize: function() {
|
||||
this.el = $(this.template({
|
||||
url: this.options.poster.url,
|
||||
type: this.options.poster.type
|
||||
}));
|
||||
this.element.addClass('is-pre-roll');
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.element.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.container.append(this.el);
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
if (_.isFunction(this.options.onClick)) {
|
||||
this.options.onClick();
|
||||
}
|
||||
this.destroy();
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.element.off('destroy', this.destroy).removeClass('is-pre-roll');
|
||||
this.el.remove();
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoPoster;
|
||||
131
xmodule/assets/video/public/js/09_save_state_plugin.js
Normal file
131
xmodule/assets/video/public/js/09_save_state_plugin.js
Normal file
@@ -0,0 +1,131 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
import { convert, format, formatFull } from './utils/time.js';
|
||||
|
||||
|
||||
/**
|
||||
* Save state module.
|
||||
* @exports video/09_save_state_plugin.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @param {Object} options
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let SaveStatePlugin = function(state, i18n, options) {
|
||||
if (!(this instanceof SaveStatePlugin)) {
|
||||
return new SaveStatePlugin(state, i18n, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onSpeedChange', 'onAutoAdvanceChange', 'saveStateHandler', 'bindUnloadHandler', 'onUnload',
|
||||
'onYoutubeAvailability', 'onLanguageChange', 'destroy');
|
||||
this.state = state;
|
||||
this.options = _.extend({events: []}, options);
|
||||
this.state.videoSaveStatePlugin = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
SaveStatePlugin.moduleName = 'SaveStatePlugin';
|
||||
SaveStatePlugin.prototype = {
|
||||
destroy: function() {
|
||||
this.state.el.off(this.events).off('destroy', this.destroy);
|
||||
$(window).off('unload', this.onUnload);
|
||||
delete this.state.videoSaveStatePlugin;
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.events = {
|
||||
speedchange: this.onSpeedChange,
|
||||
autoadvancechange: this.onAutoAdvanceChange,
|
||||
play: this.bindUnloadHandler,
|
||||
'pause destroy': this.saveStateHandler,
|
||||
'language_menu:change': this.onLanguageChange,
|
||||
youtube_availability: this.onYoutubeAvailability
|
||||
};
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
if (this.options.events.length) {
|
||||
_.each(this.options.events, function(eventName) {
|
||||
let callback;
|
||||
if (_.has(this.events, eventName)) {
|
||||
callback = this.events[eventName];
|
||||
this.state.el.on(eventName, callback);
|
||||
}
|
||||
}, this);
|
||||
} else {
|
||||
this.state.el.on(this.events);
|
||||
}
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
bindUnloadHandler: _.once(function() {
|
||||
$(window).on('unload.video', this.onUnload);
|
||||
}),
|
||||
|
||||
onSpeedChange: function(event, newSpeed) {
|
||||
this.saveState(true, {speed: newSpeed});
|
||||
this.state.storage.setItem('speed', newSpeed, true);
|
||||
this.state.storage.setItem('general_speed', newSpeed);
|
||||
},
|
||||
|
||||
onAutoAdvanceChange: function(event, enabled) {
|
||||
this.saveState(true, {auto_advance: enabled});
|
||||
this.state.storage.setItem('auto_advance', enabled);
|
||||
},
|
||||
|
||||
saveStateHandler: function() {
|
||||
this.saveState(true);
|
||||
},
|
||||
|
||||
onUnload: function() {
|
||||
this.saveState();
|
||||
},
|
||||
|
||||
onLanguageChange: function(event, langCode) {
|
||||
this.state.storage.setItem('language', langCode);
|
||||
},
|
||||
|
||||
onYoutubeAvailability: function(event, youtubeIsAvailable) {
|
||||
// Compare what the client-side code has determined Youtube
|
||||
// availability to be (true/false) vs. what the LMS recorded for
|
||||
// this user. The LMS will assume YouTube is available by default.
|
||||
if (youtubeIsAvailable !== this.state.config.recordedYoutubeIsAvailable) {
|
||||
this.saveState(true, {youtube_is_available: youtubeIsAvailable});
|
||||
}
|
||||
},
|
||||
|
||||
saveState: function(async, data) {
|
||||
if (this.state.config.saveStateEnabled) {
|
||||
if (!($.isPlainObject(data))) {
|
||||
data = {
|
||||
saved_video_position: this.state.videoPlayer.currentTime
|
||||
};
|
||||
}
|
||||
|
||||
if (data.speed) {
|
||||
this.state.storage.setItem('speed', data.speed, true);
|
||||
}
|
||||
|
||||
if (_.has(data, 'saved_video_position')) {
|
||||
this.state.storage.setItem('savedVideoPosition', data.saved_video_position, true);
|
||||
data.saved_video_position = formatFull(data.saved_video_position);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.state.config.saveStateUrl,
|
||||
type: 'POST',
|
||||
async: !!async,
|
||||
dataType: 'json',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default SaveStatePlugin;
|
||||
72
xmodule/assets/video/public/js/09_skip_control.js
Normal file
72
xmodule/assets/video/public/js/09_skip_control.js
Normal file
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
/**
|
||||
* Video skip control module.
|
||||
* @exports video/09_skip_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let SkipControl = function(state, i18n) {
|
||||
if (!(this instanceof SkipControl)) {
|
||||
return new SkipControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onClick', 'render', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoSkipControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
SkipControl.prototype = {
|
||||
template: [
|
||||
'<button class="control video_control skip skip-control" aria-disabled="false" title="',
|
||||
gettext('Do not show again'),
|
||||
'">',
|
||||
'<span class="icon fa fa-step-forward" aria-hidden="true"></span>',
|
||||
'</button>'
|
||||
].join(''),
|
||||
|
||||
destroy: function() {
|
||||
this.el.remove();
|
||||
this.state.el.off('.skip');
|
||||
delete this.state.videoSkipControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.el = $(this.template);
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
this.state.el.find('.vcr .control').after(this.el);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.state.el.on({
|
||||
'play.skip': _.once(this.render),
|
||||
'destroy.skip': this.destroy
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(event) {
|
||||
event.preventDefault();
|
||||
this.state.videoCommands.execute('skip', true);
|
||||
}
|
||||
};
|
||||
|
||||
export default SkipControl;
|
||||
1459
xmodule/assets/video/public/js/09_video_caption.js
Normal file
1459
xmodule/assets/video/public/js/09_video_caption.js
Normal file
@@ -0,0 +1,1459 @@
|
||||
// VideoCaption module.
|
||||
|
||||
import Sjson from './00_sjson.js';
|
||||
import AsyncProcess from './00_async_process.js';
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
import Draggabilly from 'draggabilly';
|
||||
import { convert } from './utils/time.js';
|
||||
import _ from 'underscore';
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @desc VideoCaption module exports a function.
|
||||
*
|
||||
* @type {function}
|
||||
* @access public
|
||||
*
|
||||
* @param {object} state - The object containing the state of the video
|
||||
* player. All other modules, their parameters, public variables, etc.
|
||||
* are available via this object.
|
||||
*
|
||||
* @this {object} The global window object.
|
||||
*
|
||||
* @returns {jquery Promise}
|
||||
*/
|
||||
let VideoCaption = function(state) {
|
||||
if (!(this instanceof VideoCaption)) {
|
||||
return new VideoCaption(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'toggleTranscript', 'onMouseEnter', 'onMouseLeave', 'onMovement',
|
||||
'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption',
|
||||
'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy',
|
||||
'handleKeypress', 'handleKeypressLink', 'openLanguageMenu', 'closeLanguageMenu',
|
||||
'previousLanguageMenuItem', 'nextLanguageMenuItem', 'handleCaptionToggle',
|
||||
'showClosedCaptions', 'hideClosedCaptions', 'toggleClosedCaptions',
|
||||
'updateCaptioningCookie', 'handleCaptioningCookie', 'handleTranscriptToggle',
|
||||
'listenForDragDrop', 'setTranscriptVisibility', 'updateTranscriptCookie',
|
||||
'updateGoogleDisclaimer', 'toggleGoogleDisclaimer', 'updateProblematicCaptionsContent'
|
||||
);
|
||||
|
||||
this.state = state;
|
||||
this.state.videoCaption = this;
|
||||
this.renderElements();
|
||||
this.handleCaptioningCookie();
|
||||
this.setTranscriptVisibility();
|
||||
this.listenForDragDrop();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VideoCaption.prototype = {
|
||||
|
||||
destroy: function() {
|
||||
this.state.el
|
||||
.off({
|
||||
'caption:fetch': this.fetchCaption,
|
||||
'caption:resize': this.onResize,
|
||||
'caption:update': this.onCaptionUpdate,
|
||||
ended: this.pause,
|
||||
fullscreen: this.onResize,
|
||||
pause: this.pause,
|
||||
play: this.play,
|
||||
destroy: this.destroy
|
||||
})
|
||||
.removeClass('is-captions-rendered');
|
||||
if (this.fetchXHR && this.fetchXHR.abort) {
|
||||
this.fetchXHR.abort();
|
||||
}
|
||||
if (this.availableTranslationsXHR && this.availableTranslationsXHR.abort) {
|
||||
this.availableTranslationsXHR.abort();
|
||||
}
|
||||
this.subtitlesEl.remove();
|
||||
this.container.remove();
|
||||
delete this.state.videoCaption;
|
||||
},
|
||||
/**
|
||||
* @desc Initiate rendering of elements, and set their initial configuration.
|
||||
*
|
||||
*/
|
||||
renderElements: function() {
|
||||
let languages = this.state.config.transcriptLanguages;
|
||||
|
||||
let langHtml = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<div class="grouped-controls">',
|
||||
'<button class="control toggle-captions" aria-disabled="false">',
|
||||
'<span class="icon fa fa-cc" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'<button class="control toggle-transcript" aria-disabled="false">',
|
||||
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'<div class="lang menu-container" role="application">',
|
||||
'<p class="sr instructions" id="lang-instructions-{courseId}"></p>',
|
||||
'<button class="control language-menu" aria-disabled="false"',
|
||||
'aria-describedby="lang-instructions-{courseId}" ',
|
||||
'title="{langTitle}">',
|
||||
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join('')),
|
||||
{
|
||||
langTitle: gettext('Open language menu'),
|
||||
courseId: this.state.id
|
||||
}
|
||||
);
|
||||
|
||||
let subtitlesHtml = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<div class="subtitles" role="region" id="transcript-{courseId}">',
|
||||
'<h3 id="transcript-label-{courseId}" class="transcript-title sr"></h3>',
|
||||
'<ol id="transcript-captions-{courseId}" class="subtitles-menu" lang="{courseLang}"></ol>',
|
||||
'</div>'
|
||||
].join('')),
|
||||
{
|
||||
courseId: this.state.id,
|
||||
courseLang: this.state.lang
|
||||
}
|
||||
);
|
||||
|
||||
this.loaded = false;
|
||||
this.subtitlesEl = $(HtmlUtils.ensureHtml(subtitlesHtml).toString());
|
||||
this.subtitlesMenuEl = this.subtitlesEl.find('.subtitles-menu');
|
||||
this.container = $(HtmlUtils.ensureHtml(langHtml).toString());
|
||||
this.captionControlEl = this.container.find('.toggle-captions');
|
||||
this.captionDisplayEl = this.state.el.find('.closed-captions');
|
||||
this.transcriptControlEl = this.container.find('.toggle-transcript');
|
||||
this.languageChooserEl = this.container.find('.lang');
|
||||
this.menuChooserEl = this.languageChooserEl.parent();
|
||||
|
||||
if (_.keys(languages).length) {
|
||||
this.renderLanguageMenu(languages);
|
||||
this.fetchCaption();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Bind any necessary function callbacks to DOM events (click,
|
||||
* mousemove, etc.).
|
||||
*
|
||||
*/
|
||||
bindHandlers: function() {
|
||||
let state = this.state,
|
||||
events = [
|
||||
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
|
||||
'keydown'
|
||||
].join(' ');
|
||||
|
||||
this.captionControlEl.on({
|
||||
click: this.toggleClosedCaptions,
|
||||
keydown: this.handleCaptionToggle
|
||||
});
|
||||
this.transcriptControlEl.on({
|
||||
click: this.toggleTranscript,
|
||||
keydown: this.handleTranscriptToggle
|
||||
});
|
||||
this.subtitlesMenuEl.on({
|
||||
mouseenter: this.onMouseEnter,
|
||||
mouseleave: this.onMouseLeave,
|
||||
mousemove: this.onMovement,
|
||||
mousewheel: this.onMovement,
|
||||
DOMMouseScroll: this.onMovement
|
||||
})
|
||||
.on(events, 'span[data-index]', this.onCaptionHandler);
|
||||
this.container.on({
|
||||
mouseenter: this.onContainerMouseEnter,
|
||||
mouseleave: this.onContainerMouseLeave
|
||||
});
|
||||
|
||||
if (this.showLanguageMenu) {
|
||||
this.languageChooserEl.on({
|
||||
keydown: this.handleKeypress
|
||||
}, '.language-menu');
|
||||
|
||||
this.languageChooserEl.on({
|
||||
keydown: this.handleKeypressLink
|
||||
}, '.control-lang');
|
||||
}
|
||||
|
||||
state.el
|
||||
.on({
|
||||
'caption:fetch': this.fetchCaption,
|
||||
'caption:resize': this.onResize,
|
||||
'caption:update': this.onCaptionUpdate,
|
||||
ended: this.pause,
|
||||
fullscreen: this.onResize,
|
||||
pause: this.pause,
|
||||
play: this.play,
|
||||
destroy: this.destroy
|
||||
});
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
this.subtitlesMenuEl.on('scroll', state.videoControl.showControls);
|
||||
}
|
||||
},
|
||||
|
||||
onCaptionUpdate: function(event, time) {
|
||||
this.updatePlayTime(time);
|
||||
},
|
||||
|
||||
handleCaptionToggle: function(event) {
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.SPACE:
|
||||
case KEY.ENTER:
|
||||
event.preventDefault();
|
||||
this.toggleClosedCaptions(event);
|
||||
// no default
|
||||
}
|
||||
},
|
||||
|
||||
handleTranscriptToggle: function(event) {
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.SPACE:
|
||||
case KEY.ENTER:
|
||||
event.preventDefault();
|
||||
this.toggleTranscript(event);
|
||||
// no default
|
||||
}
|
||||
},
|
||||
|
||||
handleKeypressLink: function(event) {
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode,
|
||||
focused, index, total;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.UP:
|
||||
event.preventDefault();
|
||||
focused = $(':focus').parent();
|
||||
index = this.languageChooserEl.find('li').index(focused);
|
||||
total = this.languageChooserEl.find('li').size() - 1;
|
||||
|
||||
this.previousLanguageMenuItem(event, index);
|
||||
break;
|
||||
|
||||
case KEY.DOWN:
|
||||
event.preventDefault();
|
||||
focused = $(':focus').parent();
|
||||
index = this.languageChooserEl.find('li').index(focused);
|
||||
total = this.languageChooserEl.find('li').size() - 1;
|
||||
|
||||
this.nextLanguageMenuItem(event, index, total);
|
||||
break;
|
||||
|
||||
case KEY.ESCAPE:
|
||||
this.closeLanguageMenu(event);
|
||||
break;
|
||||
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
return true;
|
||||
// no default
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
handleKeypress: function(event) {
|
||||
let KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
// Handle keypresses
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
case KEY.UP:
|
||||
event.preventDefault();
|
||||
this.openLanguageMenu(event);
|
||||
break;
|
||||
|
||||
case KEY.ESCAPE:
|
||||
this.closeLanguageMenu(event);
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
return event.keyCode === KEY.TAB;
|
||||
},
|
||||
|
||||
nextLanguageMenuItem: function(event, index, total) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.altKey || event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index === total) {
|
||||
this.languageChooserEl
|
||||
.find('.control-lang').first()
|
||||
.focus();
|
||||
} else {
|
||||
this.languageChooserEl
|
||||
.find('li:eq(' + index + ')')
|
||||
.next()
|
||||
.find('.control-lang')
|
||||
.focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
previousLanguageMenuItem: function(event, index) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.altKey || event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
this.languageChooserEl
|
||||
.find('.control-lang').last()
|
||||
.focus();
|
||||
} else {
|
||||
this.languageChooserEl
|
||||
.find('li:eq(' + index + ')')
|
||||
.prev()
|
||||
.find('.control-lang')
|
||||
.focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
openLanguageMenu: function(event) {
|
||||
let button = this.languageChooserEl,
|
||||
menu = button.parent().find('.menu');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
button
|
||||
.addClass('is-opened');
|
||||
|
||||
menu
|
||||
.find('.control-lang').last()
|
||||
.focus();
|
||||
},
|
||||
|
||||
closeLanguageMenu: function(event) {
|
||||
let button = this.languageChooserEl;
|
||||
event.preventDefault();
|
||||
|
||||
button
|
||||
.removeClass('is-opened')
|
||||
.find('.language-menu')
|
||||
.focus();
|
||||
},
|
||||
|
||||
onCaptionHandler: function(event) {
|
||||
switch (event.type) {
|
||||
case 'mouseover':
|
||||
case 'mouseout':
|
||||
this.captionMouseOverOut(event);
|
||||
break;
|
||||
case 'mousedown':
|
||||
this.captionMouseDown(event);
|
||||
break;
|
||||
case 'click':
|
||||
this.captionClick(event);
|
||||
break;
|
||||
case 'focusin':
|
||||
this.captionFocus(event);
|
||||
break;
|
||||
case 'focusout':
|
||||
this.captionBlur(event);
|
||||
break;
|
||||
case 'keydown':
|
||||
this.captionKeyDown(event);
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Opens language menu.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onContainerMouseEnter: function(event) {
|
||||
event.preventDefault();
|
||||
$(event.currentTarget).find('.lang').addClass('is-opened');
|
||||
|
||||
// We only want to fire the analytics event if a menu is
|
||||
// present instead of on the container hover, since it wraps
|
||||
// the "CC" and "Transcript" buttons as well.
|
||||
if ($(event.currentTarget).find('.lang').length) {
|
||||
this.state.el.trigger('language_menu:show');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Closes language menu.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onContainerMouseLeave: function(event) {
|
||||
event.preventDefault();
|
||||
$(event.currentTarget).find('.lang').removeClass('is-opened');
|
||||
|
||||
// We only want to fire the analytics event if a menu is
|
||||
// present instead of on the container hover, since it wraps
|
||||
// the "CC" and "Transcript" buttons as well.
|
||||
if ($(event.currentTarget).find('.lang').length) {
|
||||
this.state.el.trigger('language_menu:hide');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Freezes moving of captions when mouse is over them.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMouseEnter: function() {
|
||||
if (this.frozen) {
|
||||
clearTimeout(this.frozen);
|
||||
}
|
||||
|
||||
this.frozen = setTimeout(
|
||||
this.onMouseLeave,
|
||||
this.state.config.captionsFreezeTime
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Unfreezes moving of captions when mouse go out.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMouseLeave: function() {
|
||||
if (this.frozen) {
|
||||
clearTimeout(this.frozen);
|
||||
}
|
||||
|
||||
this.frozen = null;
|
||||
|
||||
if (this.playing) {
|
||||
this.scrollCaption();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Freezes moving of captions when mouse is moving over them.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMovement: function() {
|
||||
this.onMouseEnter();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Gets the correct start and end times from the state configuration
|
||||
*
|
||||
* @returns {array} if [startTime, endTime] are defined
|
||||
*/
|
||||
getStartEndTimes: function() {
|
||||
// due to the way config.startTime/endTime are
|
||||
// processed in 03_video_player.js, we assume
|
||||
// endTime can be an integer or null,
|
||||
// and startTime is an integer > 0
|
||||
let config = this.state.config;
|
||||
let startTime = config.startTime * 1000;
|
||||
let endTime = (config.endTime !== null) ? config.endTime * 1000 : null;
|
||||
return [startTime, endTime];
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Gets captions within the start / end times stored within this.state.config
|
||||
*
|
||||
* @returns {object} {start, captions} parallel arrays of
|
||||
* start times and corresponding captions
|
||||
*/
|
||||
getBoundedCaptions: function() {
|
||||
// get start and caption. If startTime and endTime
|
||||
// are specified, filter by that range.
|
||||
let times = this.getStartEndTimes();
|
||||
// eslint-disable-next-line prefer-spread
|
||||
let results = this.sjson.filter.apply(this.sjson, times);
|
||||
let start = results.start;
|
||||
let captions = results.captions;
|
||||
|
||||
return {
|
||||
start: start,
|
||||
captions: captions
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sets whether or not the Google disclaimer should be shown based on captions
|
||||
* being AI generated, and shows/hides based on the above and if ClosedCaptions are being shown.
|
||||
*
|
||||
* @param {array} captions List of captions for the video.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
updateGoogleDisclaimer: function(captions) {
|
||||
const aIGeneratedSpanText = '<span id="captions-ai-generated"',
|
||||
aiProviderRegexp = /data\-provider=["'](?<provider>\w+)["']/;
|
||||
let self = this,
|
||||
state = this.state,
|
||||
aiGeneratedSpan = captions.find(caption => caption.includes(aIGeneratedSpanText)),
|
||||
captionsAIGenerated = !(aiGeneratedSpan === undefined),
|
||||
aiCaptionProviderIsGoogle = true;
|
||||
|
||||
if (captionsAIGenerated) {
|
||||
const providerMatch = aiProviderRegexp.exec(aiGeneratedSpan);
|
||||
if (providerMatch !== null) {
|
||||
aiCaptionProviderIsGoogle = providerMatch.groups['provider'] === 'gcp';
|
||||
}
|
||||
// If there is no provider tag, it was generated before we added those,
|
||||
// so it must be Google
|
||||
}
|
||||
// This field is whether or not, in general, this video should show the google disclaimer
|
||||
self.shouldShowGoogleDisclaimer = captionsAIGenerated && aiCaptionProviderIsGoogle;
|
||||
// Should we, right now, on load, show the google disclaimer
|
||||
self.toggleGoogleDisclaimer(!self.hideCaptionsOnLoad && !state.captionsHidden);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Show or hide the google translate disclaimer based on the passed param
|
||||
* and whether or not we are currently showing a google translated transcript.
|
||||
* @param {boolean} [show] Show if true, hide if false - if we are showing a google
|
||||
* translated transcript. If not, this will always hide.
|
||||
*/
|
||||
toggleGoogleDisclaimer: function(show) {
|
||||
let self = this,
|
||||
state = this.state;
|
||||
if (show && self.shouldShowGoogleDisclaimer) {
|
||||
state.el.find('.google-disclaimer').show();
|
||||
} else {
|
||||
state.el.find('.google-disclaimer').hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Replaces content in a caption
|
||||
*
|
||||
* @param {array} captions List of captions for the video.
|
||||
* @param {string} content content to be replaced
|
||||
* @param {string} replacementContent the replace string
|
||||
*
|
||||
* @returns {array} captions List of captions for the video.
|
||||
*/
|
||||
updateProblematicCaptionsContent: function(captions, content = '', replacementContent = '') {
|
||||
let updatedCaptions = captions.map(caption => caption.replace(content, replacementContent));
|
||||
|
||||
return updatedCaptions;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Fetch the caption file specified by the user. Upon successful
|
||||
* receipt of the file, the captions will be rendered.
|
||||
* @param {boolean} [fetchWithYoutubeId] Fetch youtube captions if true.
|
||||
* @returns {boolean}
|
||||
* true: The user specified a caption file. NOTE: if an error happens
|
||||
* while the specified file is being retrieved (for example the
|
||||
* file is missing on the server), this function will still return
|
||||
* true.
|
||||
* false: No caption file was specified, or an empty string was
|
||||
* specified for the Youtube type player.
|
||||
*/
|
||||
fetchCaption: function(fetchWithYoutubeId) {
|
||||
let self = this,
|
||||
state = this.state,
|
||||
language = state.getCurrentLanguage(),
|
||||
url = state.config.transcriptTranslationUrl.replace('__lang__', language),
|
||||
data, youtubeId;
|
||||
|
||||
if (this.loaded) {
|
||||
this.hideCaptions(false);
|
||||
}
|
||||
|
||||
if (this.fetchXHR && this.fetchXHR.abort) {
|
||||
this.fetchXHR.abort();
|
||||
}
|
||||
|
||||
if (state.videoType === 'youtube' || fetchWithYoutubeId) {
|
||||
try {
|
||||
youtubeId = state.youtubeId('1.0');
|
||||
} catch (err) {
|
||||
youtubeId = null;
|
||||
}
|
||||
|
||||
if (!youtubeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data = {videoId: youtubeId};
|
||||
}
|
||||
|
||||
state.el.removeClass('is-captions-rendered');
|
||||
// Fetch the captions file. If no file was specified, or if an error
|
||||
// occurred, then we hide the captions panel, and the "Transcript" button
|
||||
this.fetchXHR = $.ajaxWithPrefix({
|
||||
url: url,
|
||||
notifyOnError: false,
|
||||
data: data,
|
||||
success: function(sjson) {
|
||||
let results, start, captions;
|
||||
self.sjson = new Sjson(sjson);
|
||||
results = self.getBoundedCaptions();
|
||||
start = results.start;
|
||||
captions = results.captions;
|
||||
let contentToReplace = CAPTIONS_CONTENT_TO_REPLACE,
|
||||
replacementContent = CAPTIONS_CONTENT_REPLACEMENT;
|
||||
|
||||
captions = self.updateProblematicCaptionsContent(captions, contentToReplace, replacementContent);
|
||||
|
||||
self.updateGoogleDisclaimer(captions);
|
||||
|
||||
if (self.loaded) {
|
||||
if (self.rendered) {
|
||||
self.renderCaption(start, captions);
|
||||
self.updatePlayTime(state.videoPlayer.currentTime);
|
||||
}
|
||||
} else {
|
||||
if (state.isTouch) {
|
||||
HtmlUtils.setHtml(
|
||||
self.subtitlesEl.find('.subtitles-menu'),
|
||||
HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<li>'),
|
||||
gettext('Transcript will be displayed when you start playing the video.'),
|
||||
HtmlUtils.HTML('</li>')
|
||||
)
|
||||
);
|
||||
} else {
|
||||
self.renderCaption(start, captions);
|
||||
}
|
||||
self.hideCaptions(self.hideCaptionsOnLoad);
|
||||
HtmlUtils.append(
|
||||
self.state.el.find('.video-wrapper').parent(),
|
||||
HtmlUtils.HTML(self.subtitlesEl)
|
||||
);
|
||||
HtmlUtils.append(
|
||||
self.state.el.find('.secondary-controls'),
|
||||
HtmlUtils.HTML(self.container)
|
||||
);
|
||||
self.bindHandlers();
|
||||
}
|
||||
|
||||
self.loaded = true;
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
let canFetchWithYoutubeId;
|
||||
console.log('[Video info]: ERROR while fetching captions.');
|
||||
console.log(
|
||||
'[Video info]: STATUS:', textStatus
|
||||
+ ', MESSAGE:', '' + errorThrown
|
||||
);
|
||||
// If initial list of languages has more than 1 item, check
|
||||
// for availability other transcripts.
|
||||
// If player mode is html5 and there are no initial languages
|
||||
// then try to fetch youtube version of transcript with
|
||||
// youtubeId.
|
||||
if (_.keys(state.config.transcriptLanguages).length > 1) {
|
||||
self.fetchAvailableTranslations();
|
||||
} else if (!fetchWithYoutubeId && state.videoType === 'html5') {
|
||||
canFetchWithYoutubeId = self.fetchCaption(true);
|
||||
if (canFetchWithYoutubeId) {
|
||||
console.log('[Video info]: Html5 mode fetching caption with youtubeId.'); // eslint-disable-line max-len, no-console
|
||||
} else {
|
||||
self.hideCaptions(true);
|
||||
self.languageChooserEl.hide();
|
||||
self.hideClosedCaptions();
|
||||
}
|
||||
} else {
|
||||
self.hideCaptions(true);
|
||||
self.languageChooserEl.hide();
|
||||
self.hideClosedCaptions();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Fetch the list of available language codes. Upon successful receipt
|
||||
* the list of available languages will be updated.
|
||||
*
|
||||
* @returns {jquery Promise}
|
||||
*/
|
||||
fetchAvailableTranslations: function() {
|
||||
let self = this,
|
||||
state = this.state;
|
||||
|
||||
this.availableTranslationsXHR = $.ajaxWithPrefix({
|
||||
url: state.config.transcriptAvailableTranslationsUrl,
|
||||
notifyOnError: false,
|
||||
success: function(response) {
|
||||
let currentLanguages = state.config.transcriptLanguages,
|
||||
newLanguages = _.pick(currentLanguages, response);
|
||||
|
||||
// Update property with available currently translations.
|
||||
state.config.transcriptLanguages = newLanguages;
|
||||
// Remove an old language menu.
|
||||
self.container.find('.langs-list').remove();
|
||||
|
||||
if (_.keys(newLanguages).length) {
|
||||
self.renderLanguageMenu(newLanguages);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.hideCaptions(true);
|
||||
self.languageChooserEl.hide();
|
||||
}
|
||||
});
|
||||
|
||||
return this.availableTranslationsXHR;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Recalculates and updates the height of the container of captions.
|
||||
*
|
||||
*/
|
||||
onResize: function() {
|
||||
this.subtitlesEl
|
||||
.find('.spacing').first()
|
||||
.height(this.topSpacingHeight());
|
||||
|
||||
this.subtitlesEl
|
||||
.find('.spacing').last()
|
||||
.height(this.bottomSpacingHeight());
|
||||
|
||||
this.scrollCaption();
|
||||
this.setSubtitlesHeight();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Create any necessary DOM elements, attach them, and set their
|
||||
* initial configuration for the Language menu.
|
||||
*
|
||||
* @param {object} languages Dictionary where key is language code,
|
||||
* value - language label
|
||||
*
|
||||
*/
|
||||
renderLanguageMenu: function(languages) {
|
||||
let self = this,
|
||||
state = this.state,
|
||||
$menu = $('<ol class="langs-list menu">'),
|
||||
currentLang = state.getCurrentLanguage(),
|
||||
$li, $link, linkHtml;
|
||||
|
||||
if (_.keys(languages).length < 2) {
|
||||
// Remove the menu toggle button
|
||||
self.container.find('.lang').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLanguageMenu = true;
|
||||
|
||||
$.each(languages, function(code, label) {
|
||||
$li = $('<li />', {'data-lang-code': code});
|
||||
linkHtml = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<button class="control control-lang">'),
|
||||
label,
|
||||
HtmlUtils.HTML('</button>')
|
||||
);
|
||||
$link = $(linkHtml.toString());
|
||||
|
||||
if (currentLang === code) {
|
||||
$li.addClass('is-active');
|
||||
$link.attr('aria-pressed', 'true');
|
||||
}
|
||||
|
||||
$li.append($link);
|
||||
$menu.append($li);
|
||||
});
|
||||
|
||||
HtmlUtils.append(
|
||||
this.languageChooserEl,
|
||||
HtmlUtils.HTML($menu)
|
||||
);
|
||||
|
||||
$menu.on('click', '.control-lang', function(e) {
|
||||
let el = $(e.currentTarget).parent(),
|
||||
captionState = self.state,
|
||||
langCode = el.data('lang-code');
|
||||
|
||||
if (captionState.lang !== langCode) {
|
||||
captionState.lang = langCode;
|
||||
el.addClass('is-active')
|
||||
.siblings('li')
|
||||
.removeClass('is-active')
|
||||
.find('.control-lang')
|
||||
.attr('aria-pressed', 'false');
|
||||
|
||||
$(e.currentTarget).attr('aria-pressed', 'true');
|
||||
|
||||
captionState.el.trigger('language_menu:change', [langCode]);
|
||||
self.fetchCaption();
|
||||
|
||||
// update the closed-captions lang attribute
|
||||
self.captionDisplayEl.attr('lang', langCode);
|
||||
|
||||
// update the transcript lang attribute
|
||||
self.subtitlesMenuEl.attr('lang', langCode);
|
||||
self.closeLanguageMenu(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Create any necessary DOM elements, attach them, and set their
|
||||
* initial configuration.
|
||||
*
|
||||
* @param {jQuery element} container Element in which captions will be
|
||||
* inserted.
|
||||
* @param {array} start List of start times for the video.
|
||||
* @param {array} captions List of captions for the video.
|
||||
* @returns {object} jQuery's Promise object
|
||||
*
|
||||
*/
|
||||
buildCaptions: function(container, start, captions) {
|
||||
let process = function(text, index) {
|
||||
let $spanEl = $('<span>', {
|
||||
role: 'link',
|
||||
'data-index': index,
|
||||
'data-start': start[index],
|
||||
tabindex: 0
|
||||
});
|
||||
|
||||
HtmlUtils.setHtml($($spanEl), HtmlUtils.HTML(text.toString()));
|
||||
|
||||
return $spanEl.wrap('<li>').parent()[0]; // xss-lint: disable=javascript-jquery-insertion
|
||||
};
|
||||
|
||||
return AsyncProcess.array(captions, process).done(function(list) {
|
||||
HtmlUtils.append(
|
||||
container,
|
||||
HtmlUtils.HTML(list)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Initiates creating of captions and set their initial configuration.
|
||||
*
|
||||
* @param {array} start List of start times for the video.
|
||||
* @param {array} captions List of captions for the video.
|
||||
*
|
||||
*/
|
||||
renderCaption: function(start, captions) {
|
||||
let self = this;
|
||||
|
||||
let onRender = function() {
|
||||
self.addPaddings();
|
||||
// Enables or disables automatic scrolling of the captions when the
|
||||
// video is playing. This feature has to be disabled when tabbing
|
||||
// through them as it interferes with that action. Initially, have
|
||||
// this flag enabled as we assume mouse use. Then, if the first
|
||||
// caption (through forward tabbing) or the last caption (through
|
||||
// backwards tabbing) gets the focus, disable that feature.
|
||||
// Re-enable it if tabbing then cycles out of the the captions.
|
||||
self.autoScrolling = true;
|
||||
// Keeps track of where the focus is situated in the array of
|
||||
// captions. Used to implement the automatic scrolling behavior and
|
||||
// decide if the outline around a caption has to be hidden or shown
|
||||
// on a mouseenter or mouseleave. Initially, no caption has the
|
||||
// focus, set the index to -1.
|
||||
self.currentCaptionIndex = -1;
|
||||
// Used to track if the focus is coming from a click or tabbing. This
|
||||
// has to be known to decide if, when a caption gets the focus, an
|
||||
// outline has to be drawn (tabbing) or not (mouse click).
|
||||
self.isMouseFocus = false;
|
||||
self.rendered = true;
|
||||
self.state.el.addClass('is-captions-rendered');
|
||||
|
||||
self.subtitlesEl
|
||||
.attr('aria-label', gettext('Activating a link in this group will skip to the corresponding point in the video.')); // eslint-disable-line max-len
|
||||
|
||||
self.subtitlesEl.find('.transcript-title')
|
||||
.text(gettext('Video transcript'));
|
||||
|
||||
self.subtitlesEl.find('.transcript-start')
|
||||
.text(gettext('Start of transcript. Skip to the end.'))
|
||||
.attr('lang', $('html').attr('lang'));
|
||||
|
||||
self.subtitlesEl.find('.transcript-end')
|
||||
.text(gettext('End of transcript. Skip to the start.'))
|
||||
.attr('lang', $('html').attr('lang'));
|
||||
|
||||
self.container.find('.menu-container .instructions')
|
||||
.text(gettext('Press the UP arrow key to enter the language menu then use UP and DOWN arrow keys to navigate language options. Press ENTER to change to the selected language.')); // eslint-disable-line max-len
|
||||
};
|
||||
|
||||
this.rendered = false;
|
||||
this.subtitlesMenuEl.empty();
|
||||
this.setSubtitlesHeight();
|
||||
this.buildCaptions(this.subtitlesMenuEl, start, captions).done(onRender);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sets top and bottom spacing height and make sure they are taken
|
||||
* out of the tabbing order.
|
||||
*
|
||||
*/
|
||||
addPaddings: function() {
|
||||
let topSpacer = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<li class="spacing" style="height: {height}px">',
|
||||
'<a href="#transcript-end-{id}" id="transcript-start-{id}" class="transcript-start"></a>', // eslint-disable-line max-len, indent
|
||||
'</li>'
|
||||
].join('')),
|
||||
{
|
||||
id: this.state.id,
|
||||
height: this.topSpacingHeight()
|
||||
}
|
||||
);
|
||||
|
||||
let bottomSpacer = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<li class="spacing" style="height: {height}px">',
|
||||
'<a href="#transcript-start-{id}" id="transcript-end-{id}" class="transcript-end"></a>', // eslint-disable-line max-len, indent
|
||||
'</li>'
|
||||
].join('')),
|
||||
{
|
||||
id: this.state.id,
|
||||
height: this.bottomSpacingHeight()
|
||||
}
|
||||
);
|
||||
|
||||
HtmlUtils.prepend(
|
||||
this.subtitlesMenuEl,
|
||||
topSpacer
|
||||
);
|
||||
|
||||
HtmlUtils.append(
|
||||
this.subtitlesMenuEl,
|
||||
bottomSpacer
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc
|
||||
* On mouseOver: Hides the outline of a caption that has been tabbed to.
|
||||
* On mouseOut: Shows the outline of a caption that has been tabbed to.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionMouseOverOut: function(event) {
|
||||
let $caption = $(event.target),
|
||||
captionIndex = parseInt($caption.attr('data-index'), 10);
|
||||
|
||||
if (captionIndex === this.currentCaptionIndex) {
|
||||
if (event.type === 'mouseover') {
|
||||
$caption.removeClass('focused');
|
||||
} else { // mouseout
|
||||
$caption.addClass('focused');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles mousedown event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionMouseDown: function(event) {
|
||||
let $caption = $(event.target);
|
||||
|
||||
this.isMouseFocus = true;
|
||||
this.autoScrolling = true;
|
||||
$caption.removeClass('focused');
|
||||
this.currentCaptionIndex = -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles click event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionClick: function(event) {
|
||||
this.seekPlayer(event);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles focus event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionFocus: function(event) {
|
||||
let $caption = $(event.target),
|
||||
container = $caption.parent(),
|
||||
captionIndex = parseInt($caption.attr('data-index'), 10);
|
||||
// If the focus comes from a mouse click, hide the outline, turn on
|
||||
// automatic scrolling and set currentCaptionIndex to point outside of
|
||||
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
|
||||
if (this.isMouseFocus) {
|
||||
this.autoScrolling = true;
|
||||
container.removeClass('focused');
|
||||
this.currentCaptionIndex = -1;
|
||||
} else {
|
||||
// If the focus comes from tabbing, show the outline and turn off
|
||||
// automatic scrolling.
|
||||
|
||||
this.currentCaptionIndex = captionIndex;
|
||||
container.addClass('focused');
|
||||
// The second and second to last elements turn automatic scrolling
|
||||
// off again as it may have been enabled in captionBlur.
|
||||
if (
|
||||
captionIndex <= 1
|
||||
|| captionIndex >= this.sjson.getSize() - 2
|
||||
) {
|
||||
this.autoScrolling = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles blur event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionBlur: function(event) {
|
||||
let $caption = $(event.target),
|
||||
container = $caption.parent(),
|
||||
captionIndex = parseInt($caption.attr('data-index'), 10);
|
||||
|
||||
container.removeClass('focused');
|
||||
// If we are on first or last index, we have to turn automatic scroll
|
||||
// on again when losing focus. There is no way to know in what
|
||||
// direction we are tabbing. So we could be on the first element and
|
||||
// tabbing back out of the captions or on the last element and tabbing
|
||||
// forward out of the captions.
|
||||
if (captionIndex === 0
|
||||
|| captionIndex === this.sjson.getSize() - 1) {
|
||||
this.autoScrolling = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles keydown event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionKeyDown: function(event) {
|
||||
this.isMouseFocus = false;
|
||||
if (event.which === 13) { // Enter key
|
||||
this.seekPlayer(event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Scrolls caption container to make active caption visible.
|
||||
*
|
||||
*/
|
||||
scrollCaption: function() {
|
||||
let el = this.subtitlesEl.find('.current:first');
|
||||
|
||||
// Automatic scrolling gets disabled if one of the captions has
|
||||
// received focus through tabbing.
|
||||
if (
|
||||
!this.frozen
|
||||
&& el.length
|
||||
&& this.autoScrolling
|
||||
) {
|
||||
this.subtitlesEl.scrollTo(
|
||||
el,
|
||||
{
|
||||
offset: -1 * this.calculateOffset(el)
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates flags on play
|
||||
*
|
||||
*/
|
||||
play: function() {
|
||||
let captions, startAndCaptions, start;
|
||||
if (this.loaded) {
|
||||
if (!this.rendered) {
|
||||
startAndCaptions = this.getBoundedCaptions();
|
||||
start = startAndCaptions.start;
|
||||
captions = startAndCaptions.captions;
|
||||
this.renderCaption(start, captions);
|
||||
}
|
||||
|
||||
this.playing = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates flags on pause
|
||||
*
|
||||
*/
|
||||
pause: function() {
|
||||
if (this.loaded) {
|
||||
this.playing = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates captions UI on paying.
|
||||
*
|
||||
* @param {number} time Time in seconds.
|
||||
*
|
||||
*/
|
||||
updatePlayTime: function(time) {
|
||||
let state = this.state,
|
||||
params, newIndex, times;
|
||||
|
||||
if (this.loaded) {
|
||||
if (state.isFlashMode()) {
|
||||
time = convert(time, state.speed, '1.0');
|
||||
}
|
||||
|
||||
time = Math.round(time * 1000 + 100);
|
||||
times = this.getStartEndTimes();
|
||||
// if start and end times are defined, limit search.
|
||||
// else, use the entire list of video captions
|
||||
params = [time].concat(times);
|
||||
// eslint-disable-next-line prefer-spread
|
||||
newIndex = this.sjson.search.apply(this.sjson, params);
|
||||
|
||||
if (
|
||||
typeof newIndex !== 'undefined'
|
||||
&& newIndex !== -1
|
||||
&& this.currentIndex !== newIndex
|
||||
) {
|
||||
if (typeof this.currentIndex !== 'undefined') {
|
||||
this.subtitlesEl
|
||||
.find('li.current')
|
||||
.attr('aria-current', 'false')
|
||||
.removeClass('current');
|
||||
}
|
||||
this.subtitlesEl
|
||||
.find("span[data-index='" + newIndex + "']")
|
||||
.parent()
|
||||
.attr('aria-current', 'true')
|
||||
.addClass('current');
|
||||
|
||||
this.currentIndex = newIndex;
|
||||
this.captionDisplayEl.text(this.subtitlesEl.find("span[data-index='" + newIndex + "']").text());
|
||||
this.scrollCaption();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sends log to the server on caption seek.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
seekPlayer: function(event) {
|
||||
let state = this.state,
|
||||
time = parseInt($(event.target).data('start'), 10);
|
||||
|
||||
if (state.isFlashMode()) {
|
||||
time = Math.round(Time.convert(time, '1.0', state.speed));
|
||||
}
|
||||
|
||||
state.trigger(
|
||||
'videoPlayer.onCaptionSeek',
|
||||
{
|
||||
type: 'onCaptionSeek',
|
||||
time: time / 1000
|
||||
}
|
||||
);
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for paddings.
|
||||
*
|
||||
* @param {jquery element} element Top or bottom padding element.
|
||||
* @returns {number} Offset for the passed padding element.
|
||||
*
|
||||
*/
|
||||
calculateOffset: function(element) {
|
||||
return this.captionHeight() / 2 - element.height() / 2;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for the top padding element.
|
||||
*
|
||||
* @returns {number} Offset for the passed top padding element.
|
||||
*
|
||||
*/
|
||||
topSpacingHeight: function() {
|
||||
return this.calculateOffset(
|
||||
this.subtitlesEl.find('li:not(.spacing)').first()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for the bottom padding element.
|
||||
*
|
||||
* @returns {number} Offset for the passed bottom padding element.
|
||||
*
|
||||
*/
|
||||
bottomSpacingHeight: function() {
|
||||
return this.calculateOffset(
|
||||
this.subtitlesEl.find('li:not(.spacing)').last()
|
||||
);
|
||||
},
|
||||
|
||||
handleCaptioningCookie: function() {
|
||||
if ($.cookie('show_closed_captions') === 'true') {
|
||||
this.state.showClosedCaptions = true;
|
||||
this.showClosedCaptions();
|
||||
|
||||
// keep it going until turned off
|
||||
$.cookie('show_closed_captions', 'true', {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
this.hideClosedCaptions();
|
||||
}
|
||||
},
|
||||
|
||||
toggleClosedCaptions: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.el.hasClass('has-captions')) {
|
||||
this.state.showClosedCaptions = false;
|
||||
this.updateCaptioningCookie(false);
|
||||
this.hideClosedCaptions();
|
||||
this.toggleGoogleDisclaimer(false);
|
||||
} else {
|
||||
this.state.showClosedCaptions = true;
|
||||
this.updateCaptioningCookie(true);
|
||||
this.showClosedCaptions();
|
||||
this.toggleGoogleDisclaimer(true);
|
||||
}
|
||||
},
|
||||
|
||||
showClosedCaptions: function() {
|
||||
let text = gettext('Hide closed captions');
|
||||
this.state.el.addClass('has-captions');
|
||||
|
||||
this.captionDisplayEl
|
||||
.show()
|
||||
.addClass('is-visible')
|
||||
.attr('lang', this.state.lang);
|
||||
|
||||
this.captionControlEl
|
||||
.addClass('is-active')
|
||||
.attr('title', text)
|
||||
.attr('aria-label', text);
|
||||
|
||||
if (this.subtitlesEl.find('.current').text()) {
|
||||
this.captionDisplayEl
|
||||
.text(this.subtitlesEl.find('.current').text());
|
||||
} else {
|
||||
this.captionDisplayEl
|
||||
.text(gettext('(Caption will be displayed when you start playing the video.)'));
|
||||
}
|
||||
|
||||
this.state.el.trigger('captions:show');
|
||||
},
|
||||
|
||||
hideClosedCaptions: function() {
|
||||
let text = gettext('Turn on closed captioning');
|
||||
this.state.el.removeClass('has-captions');
|
||||
|
||||
this.captionDisplayEl
|
||||
.hide()
|
||||
.removeClass('is-visible');
|
||||
|
||||
this.captionControlEl
|
||||
.removeClass('is-active')
|
||||
.attr('title', text)
|
||||
.attr('aria-label', text);
|
||||
|
||||
this.state.el.trigger('captions:hide');
|
||||
},
|
||||
|
||||
updateCaptioningCookie: function(method) {
|
||||
if (method) {
|
||||
$.cookie('show_closed_captions', 'true', {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
$.cookie('show_closed_captions', null, {
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This runs when the video block is first rendered and sets the initial visibility
|
||||
* of the transcript panel based on the value of the 'show_transcript' cookie and/or
|
||||
* the block's showCaptions setting.
|
||||
*/
|
||||
setTranscriptVisibility: function() {
|
||||
let hideCaptionsOnRender = !this.state.config.showCaptions;
|
||||
|
||||
if ($.cookie('show_transcript') === 'true') {
|
||||
this.hideCaptionsOnLoad = false;
|
||||
// Keep it going until turned off.
|
||||
this.updateTranscriptCookie(true);
|
||||
} else if ($.cookie('show_transcript') === 'false') {
|
||||
hideCaptionsOnRender = true;
|
||||
this.hideCaptionsOnLoad = true;
|
||||
} else {
|
||||
this.hideCaptionsOnLoad = !this.state.config.showCaptions;
|
||||
}
|
||||
|
||||
if (hideCaptionsOnRender) {
|
||||
this.state.el.addClass('closed');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Shows/Hides transcript on click `transcript` button
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
toggleTranscript: function(event) {
|
||||
event.preventDefault();
|
||||
if (this.state.el.hasClass('closed')) {
|
||||
this.hideCaptions(false, true);
|
||||
this.updateTranscriptCookie(true);
|
||||
} else {
|
||||
this.hideCaptions(true, true);
|
||||
this.updateTranscriptCookie(false);
|
||||
}
|
||||
},
|
||||
|
||||
updateTranscriptCookie: function(showTranscript) {
|
||||
if (showTranscript) {
|
||||
$.cookie('show_transcript', 'true', {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
$.cookie('show_transcript', 'false', {
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
listenForDragDrop: function() {
|
||||
let captions = this.captionDisplayEl['0'];
|
||||
|
||||
if (typeof Draggabilly === 'function') {
|
||||
// eslint-disable-next-line no-new
|
||||
new Draggabilly(captions, {containment: true});
|
||||
} else {
|
||||
console.log('Closed captioning available but not draggable');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Shows/Hides the transcript panel.
|
||||
*
|
||||
* @param {boolean} hideCaptions if `true` hides the transcript panel,
|
||||
* otherwise - show.
|
||||
*/
|
||||
hideCaptions: function(hideCaptions, triggerEvent) {
|
||||
let transcriptControlEl = this.transcriptControlEl,
|
||||
self = this,
|
||||
state = this.state,
|
||||
text;
|
||||
|
||||
if (hideCaptions) {
|
||||
state.captionsHidden = true;
|
||||
state.el.addClass('closed');
|
||||
text = gettext('Turn on transcripts');
|
||||
if (triggerEvent) {
|
||||
this.state.el.trigger('transcript:hide');
|
||||
}
|
||||
|
||||
self.toggleGoogleDisclaimer(false);
|
||||
|
||||
transcriptControlEl
|
||||
.removeClass('is-active')
|
||||
.attr('title', gettext(text))
|
||||
.attr('aria-label', text);
|
||||
} else {
|
||||
state.captionsHidden = false;
|
||||
state.el.removeClass('closed');
|
||||
this.scrollCaption();
|
||||
text = gettext('Turn off transcripts');
|
||||
if (triggerEvent) {
|
||||
this.state.el.trigger('transcript:show');
|
||||
}
|
||||
|
||||
self.toggleGoogleDisclaimer(true);
|
||||
|
||||
transcriptControlEl
|
||||
.addClass('is-active')
|
||||
.attr('title', gettext(text))
|
||||
.attr('aria-label', text);
|
||||
}
|
||||
|
||||
if (state.resizer) {
|
||||
if (state.isFullScreen) {
|
||||
state.resizer.setMode('both');
|
||||
} else {
|
||||
state.resizer.alignByWidthOnly();
|
||||
}
|
||||
}
|
||||
|
||||
this.setSubtitlesHeight();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Return the caption container height.
|
||||
*
|
||||
* @returns {number} event Height of the container in pixels.
|
||||
*
|
||||
*/
|
||||
captionHeight: function() {
|
||||
let state = this.state;
|
||||
if (state.isFullScreen) {
|
||||
return state.container.height() - state.videoFullScreen.height;
|
||||
} else {
|
||||
return state.container.height();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sets the height of the caption container element.
|
||||
*
|
||||
*/
|
||||
setSubtitlesHeight: function() {
|
||||
let height = 0,
|
||||
state = this.state;
|
||||
// on page load captionHidden = undefined
|
||||
if ((state.captionsHidden === undefined && this.hideCaptionsOnLoad)
|
||||
|| state.captionsHidden === true
|
||||
) {
|
||||
// In case of html5 autoshowing subtitles, we adjust height of
|
||||
// subs, by height of scrollbar.
|
||||
height = state.el.find('.video-controls').height()
|
||||
+ 0.5 * state.el.find('.slider').height();
|
||||
// Height of videoControl does not contain height of slider.
|
||||
// css is set to absolute, to avoid yanking when slider
|
||||
// autochanges its height.
|
||||
}
|
||||
|
||||
this.subtitlesEl.css({
|
||||
maxHeight: this.captionHeight() - height
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default VideoCaption;
|
||||
108
xmodule/assets/video/public/js/10_commands.js
Normal file
108
xmodule/assets/video/public/js/10_commands.js
Normal file
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
import _ from 'underscore';
|
||||
|
||||
|
||||
/**
|
||||
* Video commands module.
|
||||
* @exports video/10_commands.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
let VideoCommands = function(state, i18n) {
|
||||
if (!(this instanceof VideoCommands)) {
|
||||
return new VideoCommands(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoCommands = this;
|
||||
this.i18n = i18n;
|
||||
this.commands = [];
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VideoCommands.prototype = {
|
||||
destroy: function() {
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
delete this.state.videoCommands;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.commands = this.getCommands();
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
execute: function(command) {
|
||||
let args = [].slice.call(arguments, 1) || [];
|
||||
|
||||
if (_.has(this.commands, command)) {
|
||||
this.commands[command].execute.apply(this, [this.state].concat(args));
|
||||
} else {
|
||||
console.log('Command "' + command + '" is not available.');
|
||||
}
|
||||
},
|
||||
|
||||
getCommands: function() {
|
||||
let commands = {};
|
||||
let commandsList = [
|
||||
playCommand, pauseCommand, togglePlaybackCommand,
|
||||
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand,
|
||||
skipCommand
|
||||
];
|
||||
|
||||
_.each(commandsList, function(command) {
|
||||
commands[command.name] = command;
|
||||
}, this);
|
||||
|
||||
return commands;
|
||||
}
|
||||
};
|
||||
|
||||
let Command = function(name, execute) {
|
||||
this.name = name;
|
||||
this.execute = execute;
|
||||
};
|
||||
|
||||
let playCommand = new Command('play', function(state) {
|
||||
state.videoPlayer.play();
|
||||
});
|
||||
|
||||
let pauseCommand = new Command('pause', function(state) {
|
||||
state.videoPlayer.pause();
|
||||
});
|
||||
|
||||
let togglePlaybackCommand = new Command('togglePlayback', function(state) {
|
||||
if (state.videoPlayer.isPlaying()) {
|
||||
pauseCommand.execute(state);
|
||||
} else {
|
||||
playCommand.execute(state);
|
||||
}
|
||||
});
|
||||
|
||||
let toggleMuteCommand = new Command('toggleMute', function(state) {
|
||||
state.videoVolumeControl.toggleMute();
|
||||
});
|
||||
|
||||
let toggleFullScreenCommand = new Command('toggleFullScreen', function(state) {
|
||||
state.videoFullScreen.toggle();
|
||||
});
|
||||
|
||||
let setSpeedCommand = new Command('speed', function(state, speed) {
|
||||
state.videoSpeedControl.setSpeed(state.speedToString(speed));
|
||||
});
|
||||
|
||||
let skipCommand = new Command('skip', function(state, doNotShowAgain) {
|
||||
if (doNotShowAgain) {
|
||||
state.videoBumper.skipAndDoNotShowAgain();
|
||||
} else {
|
||||
state.videoBumper.skip();
|
||||
}
|
||||
});
|
||||
|
||||
export default VideoCommands;
|
||||
133
xmodule/assets/video/public/js/10_main.js
Normal file
133
xmodule/assets/video/public/js/10_main.js
Normal file
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
|
||||
import VideoStorage from './00_video_storage.js';
|
||||
import initialize from './01_initialize.js';
|
||||
import FocusGrabber from './025_focus_grabber.js';
|
||||
import VideoAccessibleMenu from './035_video_accessible_menu.js';
|
||||
import VideoControl from './04_video_control.js';
|
||||
import VideoFullScreen from './04_video_full_screen.js';
|
||||
import VideoQualityControl from './05_video_quality_control.js';
|
||||
import VideoProgressSlider from './06_video_progress_slider.js';
|
||||
import VideoVolumeControl from './07_video_volume_control.js';
|
||||
import VideoSpeedControl from './08_video_speed_control.js';
|
||||
import VideoAutoAdvanceControl from './08_video_auto_advance_control.js';
|
||||
import VideoCaption from './09_video_caption.js';
|
||||
import VideoPlayPlaceholder from './09_play_placeholder.js';
|
||||
import VideoPlayPauseControl from './09_play_pause_control.js';
|
||||
import VideoPlaySkipControl from './09_play_skip_control.js';
|
||||
import VideoSkipControl from './09_skip_control.js';
|
||||
import VideoBumper from './09_bumper.js';
|
||||
import VideoSaveStatePlugin from './09_save_state_plugin.js';
|
||||
import VideoEventsPlugin from './09_events_plugin.js';
|
||||
import VideoEventsBumperPlugin from './09_events_bumper_plugin.js';
|
||||
import VideoPoster from './09_poster.js';
|
||||
import VideoCompletionHandler from './09_completion.js';
|
||||
import VideoCommands from './10_commands.js';
|
||||
import VideoContextMenu from './095_video_context_menu.js';
|
||||
import VideoSocialSharing from './036_video_social_sharing.js';
|
||||
import VideoTranscriptFeedback from './037_video_transcript_feedback.js';
|
||||
|
||||
let youtubeXhr = null;
|
||||
|
||||
window.Video = function (runtime, element) {
|
||||
let el = $(element).find('.video'),
|
||||
id = el.attr('id').replace(/video_/, ''),
|
||||
storage = VideoStorage('VideoState', id),
|
||||
bumperMetadata = el.data('bumper-metadata'),
|
||||
autoAdvanceEnabled = el.data('autoadvance-enabled') === 'True',
|
||||
mainVideoModules = [
|
||||
FocusGrabber, VideoControl, VideoPlayPlaceholder,
|
||||
VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl,
|
||||
VideoVolumeControl, VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands,
|
||||
VideoContextMenu, VideoSaveStatePlugin, VideoEventsPlugin, VideoCompletionHandler, VideoTranscriptFeedback
|
||||
].concat(autoAdvanceEnabled ? [VideoAutoAdvanceControl] : []),
|
||||
bumperVideoModules = [VideoControl, VideoPlaySkipControl, VideoSkipControl,
|
||||
VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin, VideoTranscriptFeedback,
|
||||
VideoEventsBumperPlugin, VideoCompletionHandler],
|
||||
state = {
|
||||
el: el,
|
||||
id: id,
|
||||
metadata: el.data('metadata'),
|
||||
storage: storage,
|
||||
options: {},
|
||||
youtubeXhr: youtubeXhr,
|
||||
modules: mainVideoModules
|
||||
};
|
||||
|
||||
let getBumperState = function (metadata) {
|
||||
let bumperState = $.extend(true, {
|
||||
el: el,
|
||||
id: id,
|
||||
storage: storage,
|
||||
options: {},
|
||||
youtubeXhr: youtubeXhr
|
||||
}, {metadata: metadata});
|
||||
|
||||
bumperState.modules = bumperVideoModules;
|
||||
bumperState.options = {
|
||||
SaveStatePlugin: {events: ['language_menu:change']}
|
||||
};
|
||||
return bumperState;
|
||||
};
|
||||
|
||||
let player = function (innerState) {
|
||||
return function () {
|
||||
_.extend(innerState.metadata, {autoplay: true, focusFirstControl: true});
|
||||
initialize(innerState, element);
|
||||
};
|
||||
};
|
||||
let onSequenceChange;
|
||||
|
||||
VideoAccessibleMenu(el, {
|
||||
storage: storage,
|
||||
saveStateUrl: state.metadata.saveStateUrl
|
||||
});
|
||||
|
||||
VideoSocialSharing(el);
|
||||
|
||||
if (bumperMetadata) {
|
||||
VideoPoster(el, {
|
||||
poster: el.data('poster'),
|
||||
onClick: _.once(function () {
|
||||
let mainVideoPlayer = player(state);
|
||||
let bumper, bumperState;
|
||||
if (storage.getItem('isBumperShown')) {
|
||||
mainVideoPlayer();
|
||||
} else {
|
||||
bumperState = getBumperState(bumperMetadata);
|
||||
bumper = new VideoBumper(player(bumperState), bumperState);
|
||||
state.bumperState = bumperState;
|
||||
bumper.getPromise().done(function () {
|
||||
delete state.bumperState;
|
||||
mainVideoPlayer();
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
initialize(state, element);
|
||||
}
|
||||
|
||||
if (!youtubeXhr) {
|
||||
youtubeXhr = state.youtubeXhr;
|
||||
}
|
||||
|
||||
el.data('video-player-state', state);
|
||||
onSequenceChange = function () {
|
||||
if (state && state.videoPlayer) {
|
||||
state.videoPlayer.destroy();
|
||||
}
|
||||
$('.sequence').off('sequence:change', onSequenceChange);
|
||||
};
|
||||
$('.sequence').on('sequence:change', onSequenceChange);
|
||||
|
||||
// Because the 'state' object is only available inside this closure, we will also make it available to
|
||||
// the caller by returning it. This is necessary so that we can test Video with Jasmine.
|
||||
return state;
|
||||
}
|
||||
|
||||
window.Video.clearYoutubeXhr = function () {
|
||||
youtubeXhr = null;
|
||||
};
|
||||
|
||||
window.Video.loadYouTubeIFrameAPI = initialize.prototype.loadYouTubeIFrameAPI;
|
||||
@@ -1,6 +1,6 @@
|
||||
// eslint-disable-next-line no-shadow
|
||||
function format(time, formatFull) {
|
||||
var hours, minutes, seconds;
|
||||
export function format(time, formatFull) {
|
||||
let hours, minutes, seconds;
|
||||
|
||||
if (!_.isFinite(time) || time < 0) {
|
||||
time = 0;
|
||||
@@ -21,23 +21,21 @@ function format(time, formatFull) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatFull(time) {
|
||||
export function formatFull(time) {
|
||||
// The returned value will not be user-facing. So no need for
|
||||
// internationalization.
|
||||
return format(time, true);
|
||||
}
|
||||
|
||||
function convert(time, oldSpeed, newSpeed) {
|
||||
export function convert(time, oldSpeed, newSpeed) {
|
||||
// eslint-disable-next-line no-mixed-operators
|
||||
return (time * oldSpeed / newSpeed).toFixed(3);
|
||||
}
|
||||
|
||||
function _pad(number) {
|
||||
export function _pad(number) {
|
||||
if (number < 10) {
|
||||
return '0' + number;
|
||||
} else {
|
||||
return '' + number;
|
||||
}
|
||||
}
|
||||
|
||||
export {format, formatFull, convert};
|
||||
@@ -27,12 +27,16 @@ import '../../common/static/common/js/spec_helpers/jasmine-waituntil.js';
|
||||
import '../../common/static/common/js/spec_helpers/jasmine-extensions.js';
|
||||
import '../../common/static/common/js/vendor/sinon.js';
|
||||
|
||||
import HtmlUtils from 'edx-ui-toolkit/js/utils/html-utils';
|
||||
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||
|
||||
|
||||
// These libraries are used by the tests (and the code under test)
|
||||
// but not explicitly imported
|
||||
import 'jquery.ui';
|
||||
|
||||
// These
|
||||
import './src/video/10_main.js';
|
||||
import '../assets/video/public/js/10_main.js';
|
||||
import './spec/helper.js';
|
||||
import './spec/video_helper.js';
|
||||
|
||||
@@ -71,11 +75,18 @@ import './spec/video/video_transcript_feedback_spec.js';
|
||||
import './spec/video/video_volume_control_spec.js';
|
||||
import './spec/time_spec.js';
|
||||
|
||||
|
||||
// overwrite the loaded method and manually start the karma after a delay
|
||||
// Somehow the code initialized in jQuery's onready doesn't get called before karma auto starts
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
'use strict';
|
||||
|
||||
window._ = _;
|
||||
window.edx = window.edx || {};
|
||||
window.edx.HtmlUtils = HtmlUtils;
|
||||
window.edx.StringUtils = StringUtils;
|
||||
|
||||
window.__karma__.loaded = function() {
|
||||
setTimeout(function() {
|
||||
window.__karma__.start();
|
||||
|
||||
@@ -23,8 +23,8 @@ var options = {
|
||||
specFiles: [],
|
||||
|
||||
fixtureFiles: [
|
||||
{pattern: 'fixtures/*.*'},
|
||||
{pattern: 'fixtures/hls/**/*.*'}
|
||||
{pattern: path.join(__dirname, 'fixtures/*.*')},
|
||||
{pattern: path.join(__dirname, 'fixtures/hls/**/*.*')}
|
||||
],
|
||||
|
||||
runFiles: [
|
||||
|
||||
@@ -255,7 +255,8 @@
|
||||
}
|
||||
|
||||
jasmine.stubRequests();
|
||||
state = new window.Video('#example');
|
||||
let runtime = jasmine.createSpyObj('TestRuntime', ['handlerUrl']);
|
||||
state = new window.Video(runtime, '#example');
|
||||
|
||||
state.resizer = (function() {
|
||||
var methods = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import * as Time from 'time.js';
|
||||
import * as Time from '../../assets/video/public/js/utils/time.js';
|
||||
|
||||
describe('Time', function() {
|
||||
describe('format', function() {
|
||||
|
||||
@@ -1,81 +1,77 @@
|
||||
(function(require) {
|
||||
require(
|
||||
['video/00_async_process.js'],
|
||||
function(AsyncProcess) {
|
||||
var getArrayNthLength = function(n, multiplier) {
|
||||
var result = [],
|
||||
mul = multiplier || 1;
|
||||
import AsyncProcess from '../../../assets/video/public/js/00_async_process.js';
|
||||
|
||||
for (var i = 0; i < n; i++) {
|
||||
result[i] = i * mul;
|
||||
}
|
||||
let getArrayNthLength = function(n, multiplier) {
|
||||
let result = [],
|
||||
mul = multiplier || 1;
|
||||
|
||||
return result;
|
||||
},
|
||||
items = getArrayNthLength(1000);
|
||||
for (let i = 0; i < n; i++) {
|
||||
result[i] = i * mul;
|
||||
}
|
||||
|
||||
describe('AsyncProcess', function() {
|
||||
it('Array is processed successfully', function(done) {
|
||||
var processedArray,
|
||||
expectedArray = getArrayNthLength(1000, 2),
|
||||
process = function(item) {
|
||||
return 2 * item;
|
||||
};
|
||||
return result;
|
||||
},
|
||||
items = getArrayNthLength(1000);
|
||||
|
||||
AsyncProcess.array(items, process).done(function(result) {
|
||||
processedArray = result;
|
||||
});
|
||||
describe('AsyncProcess', function() {
|
||||
it('Array is processed successfully', function(done) {
|
||||
var processedArray,
|
||||
expectedArray = getArrayNthLength(1000, 2),
|
||||
process = function(item) {
|
||||
return 2 * item;
|
||||
};
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
return processedArray;
|
||||
}).then(function() {
|
||||
expect(processedArray).toEqual(expectedArray);
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
it('If non-array is passed, error callback is called', function(done) {
|
||||
var isError,
|
||||
process = function() {};
|
||||
|
||||
AsyncProcess.array('string', process).fail(function() {
|
||||
isError = true;
|
||||
});
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
return isError;
|
||||
}).then(function() {
|
||||
expect(isError).toBeTruthy();
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
it('If an empty array is passed, returns initial array', function(done) {
|
||||
var processedArray,
|
||||
process = function() {};
|
||||
|
||||
AsyncProcess.array([], process).done(function(result) {
|
||||
processedArray = result;
|
||||
});
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
return processedArray;
|
||||
}).then(function() {
|
||||
expect(processedArray).toEqual([]);
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
it('If no process function passed, returns initial array', function(done) {
|
||||
var processedArray;
|
||||
|
||||
AsyncProcess.array(items).done(function(result) {
|
||||
processedArray = result;
|
||||
});
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
return processedArray;
|
||||
}).then(function() {
|
||||
expect(processedArray).toEqual(items);
|
||||
}).always(done);
|
||||
});
|
||||
});
|
||||
AsyncProcess.array(items, process).done(function(result) {
|
||||
processedArray = result;
|
||||
});
|
||||
}(require));
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
return processedArray;
|
||||
}).then(function() {
|
||||
expect(processedArray).toEqual(expectedArray);
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
it('If non-array is passed, error callback is called', function(done) {
|
||||
var isError,
|
||||
process = function() {};
|
||||
|
||||
AsyncProcess.array('string', process).fail(function() {
|
||||
isError = true;
|
||||
});
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
return isError;
|
||||
}).then(function() {
|
||||
expect(isError).toBeTruthy();
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
it('If an empty array is passed, returns initial array', function(done) {
|
||||
var processedArray,
|
||||
process = function() {};
|
||||
|
||||
AsyncProcess.array([], process).done(function(result) {
|
||||
processedArray = result;
|
||||
});
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
return processedArray;
|
||||
}).then(function() {
|
||||
expect(processedArray).toEqual([]);
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
it('If no process function passed, returns initial array', function(done) {
|
||||
var processedArray;
|
||||
|
||||
AsyncProcess.array(items).done(function(result) {
|
||||
processedArray = result;
|
||||
});
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
return processedArray;
|
||||
}).then(function() {
|
||||
expect(processedArray).toEqual(items);
|
||||
}).always(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,330 +1,324 @@
|
||||
(function(require) {
|
||||
'use strict';
|
||||
import Initialize from '../../../assets/video/public/js/01_initialize.js';
|
||||
|
||||
require(
|
||||
['video/01_initialize.js'],
|
||||
function(Initialize) {
|
||||
describe('Initialize', function() {
|
||||
var state = {};
|
||||
describe('Initialize', function() {
|
||||
var state = {};
|
||||
|
||||
afterEach(function() {
|
||||
state = {};
|
||||
});
|
||||
afterEach(function() {
|
||||
state = {};
|
||||
});
|
||||
|
||||
describe('getCurrentLanguage', function() {
|
||||
var msg;
|
||||
describe('getCurrentLanguage', function() {
|
||||
var msg;
|
||||
|
||||
beforeEach(function() {
|
||||
state.config = {};
|
||||
state.config.transcriptLanguages = {
|
||||
de: 'German',
|
||||
en: 'English',
|
||||
uk: 'Ukrainian'
|
||||
};
|
||||
});
|
||||
beforeEach(function() {
|
||||
state.config = {};
|
||||
state.config.transcriptLanguages = {
|
||||
de: 'German',
|
||||
en: 'English',
|
||||
uk: 'Ukrainian'
|
||||
};
|
||||
});
|
||||
|
||||
it('returns current language', function() {
|
||||
var expected;
|
||||
it('returns current language', function() {
|
||||
var expected;
|
||||
|
||||
state.lang = 'de';
|
||||
expected = Initialize.prototype.getCurrentLanguage.call(state);
|
||||
expect(expected).toBe('de');
|
||||
});
|
||||
state.lang = 'de';
|
||||
expected = Initialize.prototype.getCurrentLanguage.call(state);
|
||||
expect(expected).toBe('de');
|
||||
});
|
||||
|
||||
msg = 'returns `en`, if language isn\'t available for the video';
|
||||
it(msg, function() {
|
||||
var expected;
|
||||
msg = 'returns `en`, if language isn\'t available for the video';
|
||||
it(msg, function() {
|
||||
var expected;
|
||||
|
||||
state.lang = 'zh';
|
||||
expected = Initialize.prototype.getCurrentLanguage.call(state);
|
||||
expect(expected).toBe('en');
|
||||
});
|
||||
state.lang = 'zh';
|
||||
expected = Initialize.prototype.getCurrentLanguage.call(state);
|
||||
expect(expected).toBe('en');
|
||||
});
|
||||
|
||||
msg = 'returns any available language, if current and `en` '
|
||||
+ 'languages aren\'t available for the video';
|
||||
it(msg, function() {
|
||||
var expected;
|
||||
msg = 'returns any available language, if current and `en` '
|
||||
+ 'languages aren\'t available for the video';
|
||||
it(msg, function() {
|
||||
var expected;
|
||||
|
||||
state.lang = 'zh';
|
||||
state.config.transcriptLanguages = {
|
||||
de: 'German',
|
||||
uk: 'Ukrainian'
|
||||
};
|
||||
expected = Initialize.prototype.getCurrentLanguage.call(state);
|
||||
expect(expected).toBe('uk');
|
||||
});
|
||||
state.lang = 'zh';
|
||||
state.config.transcriptLanguages = {
|
||||
de: 'German',
|
||||
uk: 'Ukrainian'
|
||||
};
|
||||
expected = Initialize.prototype.getCurrentLanguage.call(state);
|
||||
expect(expected).toBe('uk');
|
||||
});
|
||||
|
||||
it('returns `null`, if transcript unavailable', function() {
|
||||
var expected;
|
||||
it('returns `null`, if transcript unavailable', function() {
|
||||
var expected;
|
||||
|
||||
state.lang = 'zh';
|
||||
state.config.transcriptLanguages = {};
|
||||
expected = Initialize.prototype.getCurrentLanguage.call(state);
|
||||
expect(expected).toBeNull();
|
||||
});
|
||||
});
|
||||
state.lang = 'zh';
|
||||
state.config.transcriptLanguages = {};
|
||||
expected = Initialize.prototype.getCurrentLanguage.call(state);
|
||||
expect(expected).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDuration', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
speed: '1.50',
|
||||
metadata: {
|
||||
testId: {
|
||||
duration: 'PT6M40S'
|
||||
},
|
||||
videoId: {
|
||||
duration: 'PT1M40S'
|
||||
}
|
||||
},
|
||||
videos: {
|
||||
'1.0': 'testId',
|
||||
'1.50': 'videoId'
|
||||
},
|
||||
youtubeId: Initialize.prototype.youtubeId,
|
||||
isFlashMode: jasmine.createSpy().and.returnValue(false)
|
||||
};
|
||||
});
|
||||
describe('getDuration', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
speed: '1.50',
|
||||
metadata: {
|
||||
testId: {
|
||||
duration: 'PT6M40S'
|
||||
},
|
||||
videoId: {
|
||||
duration: 'PT1M40S'
|
||||
}
|
||||
},
|
||||
videos: {
|
||||
'1.0': 'testId',
|
||||
'1.50': 'videoId'
|
||||
},
|
||||
youtubeId: Initialize.prototype.youtubeId,
|
||||
isFlashMode: jasmine.createSpy().and.returnValue(false)
|
||||
};
|
||||
});
|
||||
|
||||
it('returns duration for the 1.0 speed if speed is not 1.0', function() {
|
||||
var expected;
|
||||
it('returns duration for the 1.0 speed if speed is not 1.0', function() {
|
||||
var expected;
|
||||
|
||||
state.speed = '1.50';
|
||||
expected = Initialize.prototype.getDuration.call(state);
|
||||
state.speed = '1.50';
|
||||
expected = Initialize.prototype.getDuration.call(state);
|
||||
|
||||
expect(expected).toEqual(400);
|
||||
});
|
||||
expect(expected).toEqual(400);
|
||||
});
|
||||
|
||||
describe('Flash mode', function() {
|
||||
it('returns duration for current video', function() {
|
||||
var expected;
|
||||
describe('Flash mode', function() {
|
||||
it('returns duration for current video', function() {
|
||||
var expected;
|
||||
|
||||
state.isFlashMode.and.returnValue(true);
|
||||
expected = Initialize.prototype.getDuration.call(state);
|
||||
state.isFlashMode.and.returnValue(true);
|
||||
expected = Initialize.prototype.getDuration.call(state);
|
||||
|
||||
expect(expected).toEqual(100);
|
||||
});
|
||||
expect(expected).toEqual(100);
|
||||
});
|
||||
|
||||
it('returns duration for the 1.0 speed as a fall-back', function() {
|
||||
var expected;
|
||||
it('returns duration for the 1.0 speed as a fall-back', function() {
|
||||
var expected;
|
||||
|
||||
state.isFlashMode.and.returnValue(true);
|
||||
state.speed = '2.0';
|
||||
expected = Initialize.prototype.getDuration.call(state);
|
||||
state.isFlashMode.and.returnValue(true);
|
||||
state.speed = '2.0';
|
||||
expected = Initialize.prototype.getDuration.call(state);
|
||||
|
||||
expect(expected).toEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(expected).toEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('youtubeId', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
speed: '1.50',
|
||||
videos: {
|
||||
'0.50': '7tqY6eQzVhE',
|
||||
'1.0': 'cogebirgzzM',
|
||||
'1.50': 'abcdefghijkl'
|
||||
},
|
||||
isFlashMode: jasmine.createSpy().and.returnValue(false)
|
||||
};
|
||||
});
|
||||
describe('youtubeId', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
speed: '1.50',
|
||||
videos: {
|
||||
'0.50': '7tqY6eQzVhE',
|
||||
'1.0': 'cogebirgzzM',
|
||||
'1.50': 'abcdefghijkl'
|
||||
},
|
||||
isFlashMode: jasmine.createSpy().and.returnValue(false)
|
||||
};
|
||||
});
|
||||
|
||||
describe('with speed', function() {
|
||||
it('return the video id for given speed', function() {
|
||||
$.each(state.videos, function(speed, videoId) {
|
||||
var expected = Initialize.prototype.youtubeId.call(
|
||||
state, speed
|
||||
);
|
||||
describe('with speed', function() {
|
||||
it('return the video id for given speed', function() {
|
||||
$.each(state.videos, function(speed, videoId) {
|
||||
var expected = Initialize.prototype.youtubeId.call(
|
||||
state, speed
|
||||
);
|
||||
|
||||
expect(videoId).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without speed for flash mode', function() {
|
||||
it('return the video id for current speed', function() {
|
||||
var expected;
|
||||
|
||||
state.isFlashMode.and.returnValue(true);
|
||||
expected = Initialize.prototype.youtubeId.call(state);
|
||||
|
||||
expect(expected).toEqual('abcdefghijkl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without speed for youtube html5 mode', function() {
|
||||
it('return the video id for 1.0x speed', function() {
|
||||
var expected = Initialize.prototype.youtubeId.call(state);
|
||||
|
||||
expect(expected).toEqual('cogebirgzzM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('speed is absent in the list of video speeds', function() {
|
||||
it('return the video id for 1.0x speed', function() {
|
||||
var expected = Initialize.prototype.youtubeId.call(state, '0.0');
|
||||
|
||||
expect(expected).toEqual('cogebirgzzM');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSpeed', function() {
|
||||
describe('YT', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
speeds: ['0.25', '0.50', '1.0', '1.50', '2.0'],
|
||||
storage: jasmine.createSpyObj('storage', ['setItem'])
|
||||
};
|
||||
});
|
||||
|
||||
it('check mapping', function() {
|
||||
var map = {
|
||||
0.75: '0.50',
|
||||
1.25: '1.50'
|
||||
};
|
||||
|
||||
$.each(map, function(key, expected) {
|
||||
Initialize.prototype.setSpeed.call(state, key);
|
||||
expect(state.speed).toBe(parseFloat(expected));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTML5', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
speeds: ['0.75', '1.0', '1.25', '1.50', '2.0'],
|
||||
storage: jasmine.createSpyObj('storage', ['setItem'])
|
||||
};
|
||||
});
|
||||
|
||||
describe('when 0.75 speed is available', function() {
|
||||
beforeEach(function() {
|
||||
Initialize.prototype.setSpeed.call(state, '0.75');
|
||||
});
|
||||
|
||||
it('set new speed', function() {
|
||||
expect(state.speed).toEqual(0.75);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when 2.0 speed is available', function() {
|
||||
beforeEach(function() {
|
||||
Initialize.prototype.setSpeed.call(state, '2.0');
|
||||
});
|
||||
|
||||
it('set new speed', function() {
|
||||
expect(state.speed).toEqual(2.0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when new speed is not available', function() {
|
||||
beforeEach(function() {
|
||||
Initialize.prototype.setSpeed.call(state, '1.75');
|
||||
});
|
||||
|
||||
it('set speed to 1.0x', function() {
|
||||
expect(state.speed).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('check mapping', function() {
|
||||
var map = {
|
||||
0.25: '0.75',
|
||||
'0.50': '0.75'
|
||||
};
|
||||
|
||||
$.each(map, function(key, expected) {
|
||||
Initialize.prototype.setSpeed.call(state, key);
|
||||
expect(state.speed).toBe(parseFloat(expected));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPlayerMode', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
currentPlayerMode: 'flash'
|
||||
};
|
||||
});
|
||||
|
||||
it('updates player mode', function() {
|
||||
var setPlayerMode = Initialize.prototype.setPlayerMode;
|
||||
|
||||
setPlayerMode.call(state, 'html5');
|
||||
expect(state.currentPlayerMode).toBe('html5');
|
||||
setPlayerMode.call(state, 'flash');
|
||||
expect(state.currentPlayerMode).toBe('flash');
|
||||
});
|
||||
|
||||
it('sets default mode if passed is not supported', function() {
|
||||
var setPlayerMode = Initialize.prototype.setPlayerMode;
|
||||
|
||||
setPlayerMode.call(state, '77html77');
|
||||
expect(state.currentPlayerMode).toBe('html5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlayerMode', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
currentPlayerMode: 'flash'
|
||||
};
|
||||
});
|
||||
|
||||
it('returns current player mode', function() {
|
||||
var getPlayerMode = Initialize.prototype.getPlayerMode,
|
||||
actual = getPlayerMode.call(state);
|
||||
|
||||
expect(actual).toBe(state.currentPlayerMode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFlashMode', function() {
|
||||
it('returns `true` if player in `flash` mode', function() {
|
||||
var testState = {
|
||||
getPlayerMode: jasmine.createSpy().and.returnValue('flash')
|
||||
},
|
||||
isFlashMode = Initialize.prototype.isFlashMode,
|
||||
actual = isFlashMode.call(testState);
|
||||
|
||||
expect(actual).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns `false` if player is not in `flash` mode', function() {
|
||||
var testState = {
|
||||
getPlayerMode: jasmine.createSpy().and.returnValue('html5')
|
||||
},
|
||||
isFlashMode = Initialize.prototype.isFlashMode,
|
||||
actual = isFlashMode.call(testState);
|
||||
|
||||
expect(actual).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHtml5Mode', function() {
|
||||
it('returns `true` if player in `html5` mode', function() {
|
||||
var testState = {
|
||||
getPlayerMode: jasmine.createSpy().and.returnValue('html5')
|
||||
},
|
||||
isHtml5Mode = Initialize.prototype.isHtml5Mode,
|
||||
actual = isHtml5Mode.call(testState);
|
||||
|
||||
expect(actual).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns `false` if player is not in `html5` mode', function() {
|
||||
var testState = {
|
||||
getPlayerMode: jasmine.createSpy().and.returnValue('flash')
|
||||
},
|
||||
isHtml5Mode = Initialize.prototype.isHtml5Mode,
|
||||
actual = isHtml5Mode.call(testState);
|
||||
|
||||
expect(actual).toBeFalsy();
|
||||
});
|
||||
expect(videoId).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
|
||||
describe('without speed for flash mode', function() {
|
||||
it('return the video id for current speed', function() {
|
||||
var expected;
|
||||
|
||||
state.isFlashMode.and.returnValue(true);
|
||||
expected = Initialize.prototype.youtubeId.call(state);
|
||||
|
||||
expect(expected).toEqual('abcdefghijkl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without speed for youtube html5 mode', function() {
|
||||
it('return the video id for 1.0x speed', function() {
|
||||
var expected = Initialize.prototype.youtubeId.call(state);
|
||||
|
||||
expect(expected).toEqual('cogebirgzzM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('speed is absent in the list of video speeds', function() {
|
||||
it('return the video id for 1.0x speed', function() {
|
||||
var expected = Initialize.prototype.youtubeId.call(state, '0.0');
|
||||
|
||||
expect(expected).toEqual('cogebirgzzM');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSpeed', function() {
|
||||
describe('YT', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
speeds: ['0.25', '0.50', '1.0', '1.50', '2.0'],
|
||||
storage: jasmine.createSpyObj('storage', ['setItem'])
|
||||
};
|
||||
});
|
||||
|
||||
it('check mapping', function() {
|
||||
var map = {
|
||||
0.75: '0.50',
|
||||
1.25: '1.50'
|
||||
};
|
||||
|
||||
$.each(map, function(key, expected) {
|
||||
Initialize.prototype.setSpeed.call(state, key);
|
||||
expect(state.speed).toBe(parseFloat(expected));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTML5', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
speeds: ['0.75', '1.0', '1.25', '1.50', '2.0'],
|
||||
storage: jasmine.createSpyObj('storage', ['setItem'])
|
||||
};
|
||||
});
|
||||
|
||||
describe('when 0.75 speed is available', function() {
|
||||
beforeEach(function() {
|
||||
Initialize.prototype.setSpeed.call(state, '0.75');
|
||||
});
|
||||
|
||||
it('set new speed', function() {
|
||||
expect(state.speed).toEqual(0.75);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when 2.0 speed is available', function() {
|
||||
beforeEach(function() {
|
||||
Initialize.prototype.setSpeed.call(state, '2.0');
|
||||
});
|
||||
|
||||
it('set new speed', function() {
|
||||
expect(state.speed).toEqual(2.0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when new speed is not available', function() {
|
||||
beforeEach(function() {
|
||||
Initialize.prototype.setSpeed.call(state, '1.75');
|
||||
});
|
||||
|
||||
it('set speed to 1.0x', function() {
|
||||
expect(state.speed).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('check mapping', function() {
|
||||
var map = {
|
||||
0.25: '0.75',
|
||||
'0.50': '0.75'
|
||||
};
|
||||
|
||||
$.each(map, function(key, expected) {
|
||||
Initialize.prototype.setSpeed.call(state, key);
|
||||
expect(state.speed).toBe(parseFloat(expected));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPlayerMode', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
currentPlayerMode: 'flash'
|
||||
};
|
||||
});
|
||||
|
||||
it('updates player mode', function() {
|
||||
var setPlayerMode = Initialize.prototype.setPlayerMode;
|
||||
|
||||
setPlayerMode.call(state, 'html5');
|
||||
expect(state.currentPlayerMode).toBe('html5');
|
||||
setPlayerMode.call(state, 'flash');
|
||||
expect(state.currentPlayerMode).toBe('flash');
|
||||
});
|
||||
|
||||
it('sets default mode if passed is not supported', function() {
|
||||
var setPlayerMode = Initialize.prototype.setPlayerMode;
|
||||
|
||||
setPlayerMode.call(state, '77html77');
|
||||
expect(state.currentPlayerMode).toBe('html5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlayerMode', function() {
|
||||
beforeEach(function() {
|
||||
state = {
|
||||
currentPlayerMode: 'flash'
|
||||
};
|
||||
});
|
||||
|
||||
it('returns current player mode', function() {
|
||||
var getPlayerMode = Initialize.prototype.getPlayerMode,
|
||||
actual = getPlayerMode.call(state);
|
||||
|
||||
expect(actual).toBe(state.currentPlayerMode);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFlashMode', function() {
|
||||
it('returns `true` if player in `flash` mode', function() {
|
||||
var testState = {
|
||||
getPlayerMode: jasmine.createSpy().and.returnValue('flash')
|
||||
},
|
||||
isFlashMode = Initialize.prototype.isFlashMode,
|
||||
actual = isFlashMode.call(testState);
|
||||
|
||||
expect(actual).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns `false` if player is not in `flash` mode', function() {
|
||||
var testState = {
|
||||
getPlayerMode: jasmine.createSpy().and.returnValue('html5')
|
||||
},
|
||||
isFlashMode = Initialize.prototype.isFlashMode,
|
||||
actual = isFlashMode.call(testState);
|
||||
|
||||
expect(actual).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHtml5Mode', function() {
|
||||
it('returns `true` if player in `html5` mode', function() {
|
||||
var testState = {
|
||||
getPlayerMode: jasmine.createSpy().and.returnValue('html5')
|
||||
},
|
||||
isHtml5Mode = Initialize.prototype.isHtml5Mode,
|
||||
actual = isHtml5Mode.call(testState);
|
||||
|
||||
expect(actual).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns `false` if player is not in `html5` mode', function() {
|
||||
var testState = {
|
||||
getPlayerMode: jasmine.createSpy().and.returnValue('flash')
|
||||
},
|
||||
isHtml5Mode = Initialize.prototype.isHtml5Mode,
|
||||
actual = isHtml5Mode.call(testState);
|
||||
|
||||
expect(actual).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,105 +1,99 @@
|
||||
(function(require) {
|
||||
'use strict';
|
||||
import Iterator from '../../../assets/video/public/js/00_iterator.js';
|
||||
|
||||
require(
|
||||
['video/00_iterator.js'],
|
||||
function(Iterator) {
|
||||
describe('Iterator', function() {
|
||||
var list = ['a', 'b', 'c', 'd', 'e'],
|
||||
iterator;
|
||||
describe('Iterator', function() {
|
||||
var list = ['a', 'b', 'c', 'd', 'e'],
|
||||
iterator;
|
||||
|
||||
beforeEach(function() {
|
||||
iterator = new Iterator(list);
|
||||
});
|
||||
beforeEach(function() {
|
||||
iterator = new Iterator(list);
|
||||
});
|
||||
|
||||
it('size contains correct list length', function() {
|
||||
expect(iterator.size).toBe(list.length);
|
||||
expect(iterator.lastIndex).toBe(list.length - 1);
|
||||
});
|
||||
it('size contains correct list length', function() {
|
||||
expect(iterator.size).toBe(list.length);
|
||||
expect(iterator.lastIndex).toBe(list.length - 1);
|
||||
});
|
||||
|
||||
describe('next', function() {
|
||||
describe('with passed `index`', function() {
|
||||
it('returns next item in the list', function() {
|
||||
expect(iterator.next(2)).toBe('d');
|
||||
expect(iterator.next(0)).toBe('b');
|
||||
});
|
||||
describe('next', function() {
|
||||
describe('with passed `index`', function() {
|
||||
it('returns next item in the list', function() {
|
||||
expect(iterator.next(2)).toBe('d');
|
||||
expect(iterator.next(0)).toBe('b');
|
||||
});
|
||||
|
||||
it('returns first item if index equal last item', function() {
|
||||
expect(iterator.next(4)).toBe('a');
|
||||
});
|
||||
it('returns first item if index equal last item', function() {
|
||||
expect(iterator.next(4)).toBe('a');
|
||||
});
|
||||
|
||||
it('returns next item if index is not valid', function() {
|
||||
expect(iterator.next(-4)).toBe('b'); // index < 0
|
||||
expect(iterator.next(100)).toBe('c'); // index > size
|
||||
expect(iterator.next('99')).toBe('d'); // incorrect Type
|
||||
});
|
||||
});
|
||||
|
||||
describe('without passed `index`', function() {
|
||||
it('returns next item in the list', function() {
|
||||
expect(iterator.next()).toBe('b');
|
||||
expect(iterator.next()).toBe('c');
|
||||
});
|
||||
|
||||
it('returns first item if index equal last item', function() {
|
||||
expect(iterator.next()).toBe('b');
|
||||
expect(iterator.next()).toBe('c');
|
||||
expect(iterator.next()).toBe('d');
|
||||
expect(iterator.next()).toBe('e');
|
||||
expect(iterator.next()).toBe('a');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prev', function() {
|
||||
describe('with passed `index`', function() {
|
||||
it('returns previous item in the list', function() {
|
||||
expect(iterator.prev(3)).toBe('c');
|
||||
expect(iterator.prev(1)).toBe('a');
|
||||
});
|
||||
|
||||
it('returns last item if index equal first item', function() {
|
||||
expect(iterator.prev(0)).toBe('e');
|
||||
});
|
||||
|
||||
it('returns previous item if index is not valid', function() {
|
||||
expect(iterator.prev(-4)).toBe('e'); // index < 0
|
||||
expect(iterator.prev(100)).toBe('d'); // index > size
|
||||
expect(iterator.prev('99')).toBe('c'); // incorrect Type
|
||||
});
|
||||
});
|
||||
|
||||
describe('without passed `index`', function() {
|
||||
it('returns previous item in the list', function() {
|
||||
expect(iterator.prev()).toBe('e');
|
||||
expect(iterator.prev()).toBe('d');
|
||||
});
|
||||
|
||||
it('returns last item if index equal first item', function() {
|
||||
expect(iterator.prev()).toBe('e');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns last item in the list', function() {
|
||||
expect(iterator.last()).toBe('e');
|
||||
});
|
||||
|
||||
it('returns first item in the list', function() {
|
||||
expect(iterator.first()).toBe('a');
|
||||
});
|
||||
|
||||
it('isEnd works correctly', function() {
|
||||
expect(iterator.isEnd()).toBeFalsy();
|
||||
iterator.next(); // => index 1
|
||||
expect(iterator.isEnd()).toBeFalsy();
|
||||
iterator.next(); // => index 2
|
||||
expect(iterator.isEnd()).toBeFalsy();
|
||||
iterator.next(); // => index 3
|
||||
expect(iterator.isEnd()).toBeFalsy();
|
||||
iterator.next(); // => index 4 == last
|
||||
expect(iterator.isEnd()).toBeTruthy();
|
||||
});
|
||||
it('returns next item if index is not valid', function() {
|
||||
expect(iterator.next(-4)).toBe('b'); // index < 0
|
||||
expect(iterator.next(100)).toBe('c'); // index > size
|
||||
expect(iterator.next('99')).toBe('d'); // incorrect Type
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
|
||||
describe('without passed `index`', function() {
|
||||
it('returns next item in the list', function() {
|
||||
expect(iterator.next()).toBe('b');
|
||||
expect(iterator.next()).toBe('c');
|
||||
});
|
||||
|
||||
it('returns first item if index equal last item', function() {
|
||||
expect(iterator.next()).toBe('b');
|
||||
expect(iterator.next()).toBe('c');
|
||||
expect(iterator.next()).toBe('d');
|
||||
expect(iterator.next()).toBe('e');
|
||||
expect(iterator.next()).toBe('a');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prev', function() {
|
||||
describe('with passed `index`', function() {
|
||||
it('returns previous item in the list', function() {
|
||||
expect(iterator.prev(3)).toBe('c');
|
||||
expect(iterator.prev(1)).toBe('a');
|
||||
});
|
||||
|
||||
it('returns last item if index equal first item', function() {
|
||||
expect(iterator.prev(0)).toBe('e');
|
||||
});
|
||||
|
||||
it('returns previous item if index is not valid', function() {
|
||||
expect(iterator.prev(-4)).toBe('e'); // index < 0
|
||||
expect(iterator.prev(100)).toBe('d'); // index > size
|
||||
expect(iterator.prev('99')).toBe('c'); // incorrect Type
|
||||
});
|
||||
});
|
||||
|
||||
describe('without passed `index`', function() {
|
||||
it('returns previous item in the list', function() {
|
||||
expect(iterator.prev()).toBe('e');
|
||||
expect(iterator.prev()).toBe('d');
|
||||
});
|
||||
|
||||
it('returns last item if index equal first item', function() {
|
||||
expect(iterator.prev()).toBe('e');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns last item in the list', function() {
|
||||
expect(iterator.last()).toBe('e');
|
||||
});
|
||||
|
||||
it('returns first item in the list', function() {
|
||||
expect(iterator.first()).toBe('a');
|
||||
});
|
||||
|
||||
it('isEnd works correctly', function() {
|
||||
expect(iterator.isEnd()).toBeFalsy();
|
||||
iterator.next(); // => index 1
|
||||
expect(iterator.isEnd()).toBeFalsy();
|
||||
iterator.next(); // => index 2
|
||||
expect(iterator.isEnd()).toBeFalsy();
|
||||
iterator.next(); // => index 3
|
||||
expect(iterator.isEnd()).toBeFalsy();
|
||||
iterator.next(); // => index 4 == last
|
||||
expect(iterator.isEnd()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,270 +1,265 @@
|
||||
(function(require) {
|
||||
'use strict';
|
||||
import Resizer from '../../../assets/video/public/js/00_resizer.js';
|
||||
import _ from 'underscore';
|
||||
|
||||
require(
|
||||
['video/00_resizer.js', 'underscore'],
|
||||
function(Resizer, _) {
|
||||
describe('Resizer', function() {
|
||||
var html = [
|
||||
'<div '
|
||||
+ 'class="rszr-wrapper" '
|
||||
+ 'style="width:200px; height: 200px;"'
|
||||
+ '>',
|
||||
'<div '
|
||||
+ 'class="rszr-el" '
|
||||
+ 'style="width:100px; height: 150px;"'
|
||||
+ '>',
|
||||
'Content',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
config, $container, $element;
|
||||
describe('Resizer', function() {
|
||||
var html = [
|
||||
'<div '
|
||||
+ 'class="rszr-wrapper" '
|
||||
+ 'style="width:200px; height: 200px;"'
|
||||
+ '>',
|
||||
'<div '
|
||||
+ 'class="rszr-el" '
|
||||
+ 'style="width:100px; height: 150px;"'
|
||||
+ '>',
|
||||
'Content',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
config, $container, $element;
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures(html);
|
||||
beforeEach(function() {
|
||||
setFixtures(html);
|
||||
|
||||
$container = $('.rszr-wrapper');
|
||||
$element = $('.rszr-el');
|
||||
config = {
|
||||
container: $container,
|
||||
element: $element
|
||||
};
|
||||
$container = $('.rszr-wrapper');
|
||||
$element = $('.rszr-el');
|
||||
config = {
|
||||
container: $container,
|
||||
element: $element
|
||||
};
|
||||
|
||||
spyOn(console, 'log');
|
||||
});
|
||||
spyOn(console, 'log');
|
||||
});
|
||||
|
||||
it('When Initialize without required parameters, log message is shown',
|
||||
function() {
|
||||
// eslint-disable-next-line no-new
|
||||
new Resizer({ });
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
it('When Initialize without required parameters, log message is shown',
|
||||
function() {
|
||||
// eslint-disable-next-line no-new
|
||||
new Resizer({ });
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
it('`alignByWidthOnly` works correctly', function() {
|
||||
var resizer = new Resizer(config).alignByWidthOnly(),
|
||||
expectedWidth = $container.width(),
|
||||
realWidth = $element.width();
|
||||
it('`alignByWidthOnly` works correctly', function() {
|
||||
var resizer = new Resizer(config).alignByWidthOnly(),
|
||||
expectedWidth = $container.width(),
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
|
||||
it('`alignByHeightOnly` works correctly', function() {
|
||||
var resizer = new Resizer(config).alignByHeightOnly(),
|
||||
expectedHeight = $container.height(),
|
||||
realHeight = $element.height();
|
||||
it('`alignByHeightOnly` works correctly', function() {
|
||||
var resizer = new Resizer(config).alignByHeightOnly(),
|
||||
expectedHeight = $container.height(),
|
||||
realHeight = $element.height();
|
||||
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
});
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
});
|
||||
|
||||
it('`align` works correctly', function() {
|
||||
var resizer = new Resizer(config).align(),
|
||||
expectedHeight = $container.height(),
|
||||
realHeight = $element.height(),
|
||||
expectedWidth = 50,
|
||||
realWidth;
|
||||
it('`align` works correctly', function() {
|
||||
var resizer = new Resizer(config).align(),
|
||||
expectedHeight = $container.height(),
|
||||
realHeight = $element.height(),
|
||||
expectedWidth = 50,
|
||||
realWidth;
|
||||
|
||||
// containerRatio >= elementRatio
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
// containerRatio >= elementRatio
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
|
||||
// containerRatio < elementRatio
|
||||
$container.width(expectedWidth);
|
||||
resizer.align();
|
||||
realWidth = $element.width();
|
||||
// containerRatio < elementRatio
|
||||
$container.width(expectedWidth);
|
||||
resizer.align();
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
|
||||
it('`setMode` works correctly', function() {
|
||||
var resizer = new Resizer(config).setMode('height'),
|
||||
expectedHeight = $container.height(),
|
||||
realHeight = $element.height(),
|
||||
expectedWidth = 50,
|
||||
realWidth;
|
||||
it('`setMode` works correctly', function() {
|
||||
var resizer = new Resizer(config).setMode('height'),
|
||||
expectedHeight = $container.height(),
|
||||
realHeight = $element.height(),
|
||||
expectedWidth = 50,
|
||||
realWidth;
|
||||
|
||||
// containerRatio >= elementRatio
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
// containerRatio >= elementRatio
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
|
||||
// containerRatio < elementRatio
|
||||
$container.width(expectedWidth);
|
||||
resizer.setMode('width');
|
||||
realWidth = $element.width();
|
||||
// containerRatio < elementRatio
|
||||
$container.width(expectedWidth);
|
||||
resizer.setMode('width');
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
|
||||
it('`setElement` works correctly', function() {
|
||||
var $newElement,
|
||||
expectedHeight;
|
||||
it('`setElement` works correctly', function() {
|
||||
var $newElement,
|
||||
expectedHeight;
|
||||
|
||||
$container.append('<div '
|
||||
+ 'id="Another-el" '
|
||||
+ 'style="width:100px; height: 150px;"'
|
||||
+ '>');
|
||||
$newElement = $('#Another-el');
|
||||
expectedHeight = $container.height();
|
||||
$container.append('<div '
|
||||
+ 'id="Another-el" '
|
||||
+ 'style="width:100px; height: 150px;"'
|
||||
+ '>');
|
||||
$newElement = $('#Another-el');
|
||||
expectedHeight = $container.height();
|
||||
|
||||
new Resizer(config).setElement($newElement).alignByHeightOnly();
|
||||
expect($element.height()).not.toBe(expectedHeight);
|
||||
expect($newElement.height()).toBe(expectedHeight);
|
||||
});
|
||||
new Resizer(config).setElement($newElement).alignByHeightOnly();
|
||||
expect($element.height()).not.toBe(expectedHeight);
|
||||
expect($newElement.height()).toBe(expectedHeight);
|
||||
});
|
||||
|
||||
describe('Callbacks', function() {
|
||||
var resizer,
|
||||
spiesList = [];
|
||||
describe('Callbacks', function() {
|
||||
var resizer,
|
||||
spiesList = [];
|
||||
|
||||
beforeEach(function() {
|
||||
var spiesCount = _.range(3);
|
||||
beforeEach(function() {
|
||||
var spiesCount = _.range(3);
|
||||
|
||||
spiesList = $.map(spiesCount, function() {
|
||||
return jasmine.createSpy();
|
||||
});
|
||||
spiesList = $.map(spiesCount, function() {
|
||||
return jasmine.createSpy();
|
||||
});
|
||||
|
||||
resizer = new Resizer(config);
|
||||
});
|
||||
resizer = new Resizer(config);
|
||||
});
|
||||
|
||||
it('callbacks are called', function() {
|
||||
$.each(spiesList, function(index, spy) {
|
||||
resizer.callbacks.add(spy);
|
||||
});
|
||||
it('callbacks are called', function() {
|
||||
$.each(spiesList, function(index, spy) {
|
||||
resizer.callbacks.add(spy);
|
||||
});
|
||||
|
||||
resizer.align();
|
||||
resizer.align();
|
||||
|
||||
$.each(spiesList, function(index, spy) {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('callback called just once', function() {
|
||||
resizer.callbacks.once(spiesList[0]);
|
||||
|
||||
resizer
|
||||
.align()
|
||||
.alignByHeightOnly();
|
||||
|
||||
expect(spiesList[0].calls.count()).toEqual(1);
|
||||
});
|
||||
|
||||
it('all callbacks are removed', function() {
|
||||
$.each(spiesList, function(index, spy) {
|
||||
resizer.callbacks.add(spy);
|
||||
});
|
||||
|
||||
resizer.callbacks.removeAll();
|
||||
resizer.align();
|
||||
|
||||
$.each(spiesList, function(index, spy) {
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('specific callback is removed', function() {
|
||||
$.each(spiesList, function(index, spy) {
|
||||
resizer.callbacks.add(spy);
|
||||
});
|
||||
|
||||
resizer.callbacks.remove(spiesList[1]);
|
||||
resizer.align();
|
||||
|
||||
expect(spiesList[1]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(
|
||||
'Error message is shown when wrong argument type is passed',
|
||||
function() {
|
||||
var methods = ['add', 'once'],
|
||||
errorMessage = '[Video info]: TypeError: Argument is not a function.',
|
||||
arg = {};
|
||||
|
||||
spyOn(console, 'error');
|
||||
|
||||
$.each(methods, function(index, methodName) {
|
||||
resizer.callbacks[methodName](arg);
|
||||
expect(console.error).toHaveBeenCalledWith(errorMessage);
|
||||
// reset spy
|
||||
console.log.calls.reset();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delta', function() {
|
||||
var resizer;
|
||||
|
||||
beforeEach(function() {
|
||||
resizer = new Resizer(config);
|
||||
});
|
||||
|
||||
it('adding delta align correctly by height', function() {
|
||||
var delta = 100,
|
||||
expectedHeight = $container.height() + delta,
|
||||
realHeight;
|
||||
|
||||
resizer
|
||||
.delta.add(delta, 'height')
|
||||
.setMode('height');
|
||||
|
||||
realHeight = $element.height();
|
||||
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
});
|
||||
|
||||
it('adding delta align correctly by width', function() {
|
||||
var delta = 100,
|
||||
expectedWidth = $container.width() + delta,
|
||||
realWidth;
|
||||
|
||||
resizer
|
||||
.delta.add(delta, 'width')
|
||||
.setMode('width');
|
||||
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
|
||||
it('substract delta align correctly by height', function() {
|
||||
var delta = 100,
|
||||
expectedHeight = $container.height() - delta,
|
||||
realHeight;
|
||||
|
||||
resizer
|
||||
.delta.substract(delta, 'height')
|
||||
.setMode('height');
|
||||
|
||||
realHeight = $element.height();
|
||||
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
});
|
||||
|
||||
it('substract delta align correctly by width', function() {
|
||||
var delta = 100,
|
||||
expectedWidth = $container.width() - delta,
|
||||
realWidth;
|
||||
|
||||
resizer
|
||||
.delta.substract(delta, 'width')
|
||||
.setMode('width');
|
||||
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
|
||||
it('reset delta', function() {
|
||||
var delta = 100,
|
||||
expectedWidth = $container.width(),
|
||||
realWidth;
|
||||
|
||||
resizer
|
||||
.delta.substract(delta, 'width')
|
||||
.delta.reset()
|
||||
.setMode('width');
|
||||
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
});
|
||||
$.each(spiesList, function(index, spy) {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
|
||||
it('callback called just once', function() {
|
||||
resizer.callbacks.once(spiesList[0]);
|
||||
|
||||
resizer
|
||||
.align()
|
||||
.alignByHeightOnly();
|
||||
|
||||
expect(spiesList[0].calls.count()).toEqual(1);
|
||||
});
|
||||
|
||||
it('all callbacks are removed', function() {
|
||||
$.each(spiesList, function(index, spy) {
|
||||
resizer.callbacks.add(spy);
|
||||
});
|
||||
|
||||
resizer.callbacks.removeAll();
|
||||
resizer.align();
|
||||
|
||||
$.each(spiesList, function(index, spy) {
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('specific callback is removed', function() {
|
||||
$.each(spiesList, function(index, spy) {
|
||||
resizer.callbacks.add(spy);
|
||||
});
|
||||
|
||||
resizer.callbacks.remove(spiesList[1]);
|
||||
resizer.align();
|
||||
|
||||
expect(spiesList[1]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(
|
||||
'Error message is shown when wrong argument type is passed',
|
||||
function() {
|
||||
var methods = ['add', 'once'],
|
||||
errorMessage = '[Video info]: TypeError: Argument is not a function.',
|
||||
arg = {};
|
||||
|
||||
spyOn(console, 'error');
|
||||
|
||||
$.each(methods, function(index, methodName) {
|
||||
resizer.callbacks[methodName](arg);
|
||||
expect(console.error).toHaveBeenCalledWith(errorMessage);
|
||||
// reset spy
|
||||
console.log.calls.reset();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delta', function() {
|
||||
var resizer;
|
||||
|
||||
beforeEach(function() {
|
||||
resizer = new Resizer(config);
|
||||
});
|
||||
|
||||
it('adding delta align correctly by height', function() {
|
||||
var delta = 100,
|
||||
expectedHeight = $container.height() + delta,
|
||||
realHeight;
|
||||
|
||||
resizer
|
||||
.delta.add(delta, 'height')
|
||||
.setMode('height');
|
||||
|
||||
realHeight = $element.height();
|
||||
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
});
|
||||
|
||||
it('adding delta align correctly by width', function() {
|
||||
var delta = 100,
|
||||
expectedWidth = $container.width() + delta,
|
||||
realWidth;
|
||||
|
||||
resizer
|
||||
.delta.add(delta, 'width')
|
||||
.setMode('width');
|
||||
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
|
||||
it('substract delta align correctly by height', function() {
|
||||
var delta = 100,
|
||||
expectedHeight = $container.height() - delta,
|
||||
realHeight;
|
||||
|
||||
resizer
|
||||
.delta.substract(delta, 'height')
|
||||
.setMode('height');
|
||||
|
||||
realHeight = $element.height();
|
||||
|
||||
expect(realHeight).toBe(expectedHeight);
|
||||
});
|
||||
|
||||
it('substract delta align correctly by width', function() {
|
||||
var delta = 100,
|
||||
expectedWidth = $container.width() - delta,
|
||||
realWidth;
|
||||
|
||||
resizer
|
||||
.delta.substract(delta, 'width')
|
||||
.setMode('width');
|
||||
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
|
||||
it('reset delta', function() {
|
||||
var delta = 100,
|
||||
expectedWidth = $container.width(),
|
||||
realWidth;
|
||||
|
||||
resizer
|
||||
.delta.substract(delta, 'width')
|
||||
.delta.reset()
|
||||
.setMode('width');
|
||||
|
||||
realWidth = $element.width();
|
||||
|
||||
expect(realWidth).toBe(expectedWidth);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,67 +1,63 @@
|
||||
(function(require) {
|
||||
require(
|
||||
['video/00_sjson.js'],
|
||||
function(Sjson) {
|
||||
describe('Sjson', function() {
|
||||
var data = jasmine.stubbedCaption,
|
||||
sjson;
|
||||
var videoStops = [0, 3120, 6270, 8490, 21620, 24920];
|
||||
var OUT_OF_BOUNDS_STOP = 10024920;
|
||||
import Sjson from '../../../assets/video/public/js/00_sjson.js';
|
||||
|
||||
beforeEach(function() {
|
||||
sjson = new Sjson(data);
|
||||
});
|
||||
describe('Sjson', function() {
|
||||
var data = jasmine.stubbedCaption,
|
||||
sjson;
|
||||
var videoStops = [0, 3120, 6270, 8490, 21620, 24920];
|
||||
var OUT_OF_BOUNDS_STOP = 10024920;
|
||||
|
||||
it('returns captions', function() {
|
||||
expect(sjson.getCaptions()).toEqual(data.text);
|
||||
});
|
||||
beforeEach(function() {
|
||||
sjson = new Sjson(data);
|
||||
});
|
||||
|
||||
it('returns start times', function() {
|
||||
expect(sjson.getStartTimes()).toEqual(data.start);
|
||||
});
|
||||
it('returns captions', function() {
|
||||
expect(sjson.getCaptions()).toEqual(data.text);
|
||||
});
|
||||
|
||||
it('returns correct length', function() {
|
||||
expect(sjson.getSize()).toEqual(data.text.length);
|
||||
});
|
||||
it('returns start times', function() {
|
||||
expect(sjson.getStartTimes()).toEqual(data.start);
|
||||
});
|
||||
|
||||
it('search returns a correct caption index', function() {
|
||||
expect(sjson.search(videoStops[0])).toEqual(0);
|
||||
expect(sjson.search(videoStops[1])).toEqual(1);
|
||||
expect(sjson.search(videoStops[2])).toEqual(2);
|
||||
expect(sjson.search(videoStops[3])).toEqual(2);
|
||||
expect(sjson.search(videoStops[4])).toEqual(4);
|
||||
expect(sjson.search(videoStops[5])).toEqual(5);
|
||||
});
|
||||
it('returns correct length', function() {
|
||||
expect(sjson.getSize()).toEqual(data.text.length);
|
||||
});
|
||||
|
||||
it('search returns the last entry for a value outside the bounds of the array', function() {
|
||||
expect(sjson.search(OUT_OF_BOUNDS_STOP)).toEqual(sjson.getCaptions().length - 1);
|
||||
});
|
||||
it('search returns a correct caption index', function() {
|
||||
expect(sjson.search(videoStops[0])).toEqual(0);
|
||||
expect(sjson.search(videoStops[1])).toEqual(1);
|
||||
expect(sjson.search(videoStops[2])).toEqual(2);
|
||||
expect(sjson.search(videoStops[3])).toEqual(2);
|
||||
expect(sjson.search(videoStops[4])).toEqual(4);
|
||||
expect(sjson.search(videoStops[5])).toEqual(5);
|
||||
});
|
||||
|
||||
it('search returns the first entry for a negative index in the array', function() {
|
||||
expect(sjson.search(-1)).toEqual(0);
|
||||
});
|
||||
it('search returns the last entry for a value outside the bounds of the array', function() {
|
||||
expect(sjson.search(OUT_OF_BOUNDS_STOP)).toEqual(sjson.getCaptions().length - 1);
|
||||
});
|
||||
|
||||
it('search only searches through a subrange of times if start / end times are specified', function() {
|
||||
var start = videoStops[2] - 100;
|
||||
var end = videoStops[5] - 100;
|
||||
var results = sjson.filter(start, end);
|
||||
var expectedLength = results.captions.length - 1;
|
||||
it('search returns the first entry for a negative index in the array', function() {
|
||||
expect(sjson.search(-1)).toEqual(0);
|
||||
});
|
||||
|
||||
expect(sjson.search(videoStops[0], start, end)).toEqual(0);
|
||||
expect(sjson.search(videoStops[1], start, end)).toEqual(0);
|
||||
expect(sjson.search(videoStops[2], start, end)).toEqual(0);
|
||||
expect(sjson.search(videoStops[3], start, end)).toEqual(0);
|
||||
expect(sjson.search(OUT_OF_BOUNDS_STOP, start, end)).toEqual(expectedLength);
|
||||
});
|
||||
it('search only searches through a subrange of times if start / end times are specified', function() {
|
||||
var start = videoStops[2] - 100;
|
||||
var end = videoStops[5] - 100;
|
||||
var results = sjson.filter(start, end);
|
||||
var expectedLength = results.captions.length - 1;
|
||||
|
||||
it('filters results correctly given a start and end time', function() {
|
||||
var start = videoStops[1] - 100;
|
||||
var end = videoStops[4] - 100;
|
||||
var results = sjson.filter(start, end);
|
||||
expect(sjson.search(videoStops[0], start, end)).toEqual(0);
|
||||
expect(sjson.search(videoStops[1], start, end)).toEqual(0);
|
||||
expect(sjson.search(videoStops[2], start, end)).toEqual(0);
|
||||
expect(sjson.search(videoStops[3], start, end)).toEqual(0);
|
||||
expect(sjson.search(OUT_OF_BOUNDS_STOP, start, end)).toEqual(expectedLength);
|
||||
});
|
||||
|
||||
expect(results.start.length).toEqual(3);
|
||||
expect(results.captions.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require));
|
||||
it('filters results correctly given a start and end time', function() {
|
||||
var start = videoStops[1] - 100;
|
||||
var end = videoStops[4] - 100;
|
||||
var results = sjson.filter(start, end);
|
||||
|
||||
expect(results.start.length).toEqual(3);
|
||||
expect(results.captions.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,6 +67,11 @@
|
||||
spyOnEvent($nextButton, 'click');
|
||||
expect('click').not.toHaveBeenTriggeredOn($nextButton);
|
||||
|
||||
// Spy on window.parent.postMessage and make it trigger a click
|
||||
spyOn(window.parent, 'postMessage').and.callFake(function() {
|
||||
$nextButton.trigger('click');
|
||||
});
|
||||
|
||||
state.el.trigger('ended');
|
||||
jasmine.clock().tick(2);
|
||||
|
||||
@@ -101,7 +106,7 @@
|
||||
state.el.trigger('ended');
|
||||
jasmine.clock().tick(2);
|
||||
|
||||
// still not auto-clicked
|
||||
// still not auto-clicked (auto-advance is disabled)
|
||||
expect('click').not.toHaveBeenTriggeredOn($nextButton);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -447,10 +447,10 @@
|
||||
$(this).trigger('mousedown');
|
||||
expect(state.videoCaption.captionMouseDown).toHaveBeenCalled();
|
||||
|
||||
$(this).trigger('focus');
|
||||
$(this).trigger('focusin');
|
||||
expect(state.videoCaption.captionFocus).toHaveBeenCalled();
|
||||
|
||||
$(this).trigger('blur');
|
||||
$(this).trigger('focusout');
|
||||
expect(state.videoCaption.captionBlur).toHaveBeenCalled();
|
||||
|
||||
$(this).trigger('keydown');
|
||||
@@ -1245,7 +1245,7 @@
|
||||
beforeEach(function() {
|
||||
state.videoCaption.isMouseFocus = false;
|
||||
$('.subtitles li span[data-index=0]').trigger(
|
||||
jQuery.Event('focus')
|
||||
jQuery.Event('focusin')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1353,25 +1353,25 @@
|
||||
state = jasmine.initializePlayer();
|
||||
Caption = state.videoCaption;
|
||||
})
|
||||
|
||||
|
||||
it('not shown when captions are not ai generated', () => {
|
||||
Caption.updateGoogleDisclaimer(BASE_CAPTIONS)
|
||||
expect(state.shouldShowGoogleDisclaimer).toBe(false);
|
||||
expect(Caption.shouldShowGoogleDisclaimer).toBe(false);
|
||||
});
|
||||
|
||||
it('not shown when captions are not generated by gcp', () => {
|
||||
Caption.updateGoogleDisclaimer(makeCaptions('someoneElse'))
|
||||
expect(state.shouldShowGoogleDisclaimer).toBe(false);
|
||||
expect(Caption.shouldShowGoogleDisclaimer).toBe(false);
|
||||
});
|
||||
|
||||
it('shown when captions are generated by gcp', () => {
|
||||
Caption.updateGoogleDisclaimer(makeCaptions('gcp'))
|
||||
expect(state.shouldShowGoogleDisclaimer).toBe(true);
|
||||
expect(Caption.shouldShowGoogleDisclaimer).toBe(true);
|
||||
});
|
||||
|
||||
it('shown when captions have no provider', () => {
|
||||
Caption.updateGoogleDisclaimer(makeCaptions())
|
||||
expect(state.shouldShowGoogleDisclaimer).toBe(true);
|
||||
expect(Caption.shouldShowGoogleDisclaimer).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
return isFinite(duration) && duration > 0 && isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:10 / 1:00');
|
||||
expect(expectedValue).toHaveText('0:00 / 1:00');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
@@ -85,7 +85,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:15 / 1:00');
|
||||
expect(expectedValue).toHaveText('0:05 / 1:00');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(15);
|
||||
@@ -115,7 +115,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:10 / 1:00');
|
||||
expect(expectedValue).toHaveText('0:00 / 1:00');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
@@ -145,7 +145,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:10 / 1:00');
|
||||
expect(expectedValue).toHaveText('0:00 / 1:00');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
@@ -175,7 +175,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:10 / 1:00');
|
||||
expect(expectedValue).toHaveText('0:00 / 1:00');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
@@ -187,22 +187,29 @@
|
||||
|
||||
describe('constructor with end-time', function() {
|
||||
it('displays the correct time when startTime and endTime are specified', function(done) {
|
||||
let duration;
|
||||
state = jasmine.initializePlayer({
|
||||
start: 10,
|
||||
end: 20
|
||||
});
|
||||
spyOn(state.videoPlayer, 'duration').and.returnValue(60);
|
||||
|
||||
state.videoControl.updateVcrVidTime({
|
||||
time: 15,
|
||||
duration: 60
|
||||
});
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
var expectedValue = $('.video-controls').find('.vidtime');
|
||||
return expectedValue.text().indexOf('0:05 / 0:20') !== -1; // Expecting 15 seconds - 10 seconds = 5 seconds
|
||||
duration = state.videoPlayer.duration();
|
||||
return isFinite(duration) && duration > 0 && isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expect($('.video-controls').find('.vidtime')).toHaveText('0:05 / 0:20');
|
||||
// Update the video control time display
|
||||
state.videoControl.updateVcrVidTime({
|
||||
time: 15,
|
||||
duration: 60
|
||||
});
|
||||
|
||||
jasmine.waitUntil(function() {
|
||||
var expectedValue = $('.video-controls').find('.vidtime');
|
||||
return expectedValue.text().indexOf('0:05 / 0:10') !== -1; // Expecting 15 seconds - 10 seconds = 5 seconds
|
||||
}).then(function() {
|
||||
expect($('.video-controls').find('.vidtime')).toHaveText('0:05 / 0:10');
|
||||
}).always(done);
|
||||
}).always(done);
|
||||
});
|
||||
|
||||
@@ -380,7 +387,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:10 / 0:20');
|
||||
expect(expectedValue).toHaveText('0:00 / 0:10');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
@@ -411,7 +418,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:15 / 0:20');
|
||||
expect(expectedValue).toHaveText('0:05 / 0:10');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(15);
|
||||
@@ -442,7 +449,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:10 / 0:20');
|
||||
expect(expectedValue).toHaveText('0:00 / 0:10');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
@@ -473,7 +480,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:10 / 0:20');
|
||||
expect(expectedValue).toHaveText('0:00 / 0:10');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
@@ -504,7 +511,7 @@
|
||||
&& isFinite(state.videoPlayer.startTime);
|
||||
}).then(function() {
|
||||
expectedValue = $('.video-controls').find('.vidtime');
|
||||
expect(expectedValue).toHaveText('0:10 / 0:20');
|
||||
expect(expectedValue).toHaveText('0:00 / 0:10');
|
||||
|
||||
expectedValue = sliderEl.slider('option', 'value');
|
||||
expect(expectedValue).toBe(10);
|
||||
|
||||
@@ -11,7 +11,6 @@ import '../helper.js';
|
||||
|
||||
beforeEach(function() {
|
||||
spyOn(Logger, 'log');
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -32,221 +31,27 @@ import '../helper.js';
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "play_video" event when emitPlayVideoEvent is true', function() {
|
||||
state.videoEventsPlugin.emitPlayVideoEvent = true;
|
||||
state.el.trigger('play');
|
||||
expect(Logger.log).toHaveBeenCalledWith('play_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can not emit "play_video" event when emitPlayVideoEvent is false', function() {
|
||||
state.videoEventsPlugin.emitPlayVideoEvent = false;
|
||||
state.el.trigger('play');
|
||||
expect(Logger.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can emit "pause_video" event', function() {
|
||||
state.el.trigger('pause');
|
||||
expect(Logger.log).toHaveBeenCalledWith('pause_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can emit "complete_video" event when video is marked as complete', function() {
|
||||
state.el.trigger('complete');
|
||||
expect(Logger.log).toHaveBeenCalledWith('complete_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "speed_change_video" event', function() {
|
||||
state.el.trigger('speedchange', ['2.0', '1.0']);
|
||||
expect(Logger.log).toHaveBeenCalledWith('speed_change_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
old_speed: '1.0',
|
||||
new_speed: '2.0',
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "seek_video" event', function() {
|
||||
state.el.trigger('seek', [1, 0, 'any']);
|
||||
expect(Logger.log).toHaveBeenCalledWith('seek_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
old_time: 0,
|
||||
new_time: 1,
|
||||
type: 'any',
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can emit "play_video" event after "seek_video" event ', function() {
|
||||
state.videoEventsPlugin.emitPlayVideoEvent = false;
|
||||
state.el.trigger('seek', [1, 0, 'any']);
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can emit "stop_video" event', function() {
|
||||
state.el.trigger('ended');
|
||||
expect(Logger.log).toHaveBeenCalledWith('stop_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
|
||||
Logger.log.calls.reset();
|
||||
state.el.trigger('stop');
|
||||
expect(Logger.log).toHaveBeenCalledWith('stop_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can emit "skip_video" event', function() {
|
||||
state.el.trigger('skip', [false]);
|
||||
expect(Logger.log).toHaveBeenCalledWith('skip_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "do_not_show_again_video" event', function() {
|
||||
state.el.trigger('skip', [true]);
|
||||
expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "edx.video.language_menu.shown" event', function() {
|
||||
state.el.trigger('language_menu:show');
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.video.language_menu.shown', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "edx.video.language_menu.hidden" event', function() {
|
||||
state.el.trigger('language_menu:hide');
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.video.language_menu.hidden', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
language: 'en',
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "show_transcript" event', function() {
|
||||
state.el.trigger('transcript:show');
|
||||
expect(Logger.log).toHaveBeenCalledWith('show_transcript', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "hide_transcript" event', function() {
|
||||
state.el.trigger('transcript:hide');
|
||||
expect(Logger.log).toHaveBeenCalledWith('hide_transcript', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "edx.video.closed_captions.shown" event', function() {
|
||||
state.el.trigger('captions:show');
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.video.closed_captions.shown', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "edx.video.closed_captions.hidden" event', function() {
|
||||
state.el.trigger('captions:hide');
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.video.closed_captions.hidden', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can destroy itself', function() {
|
||||
var plugin = state.videoEventsPlugin;
|
||||
spyOn($.fn, 'off').and.callThrough();
|
||||
state.videoEventsPlugin.destroy();
|
||||
expect(state.videoEventsPlugin).toBeUndefined();
|
||||
expect($.fn.off).toHaveBeenCalledWith({
|
||||
ready: plugin.onReady,
|
||||
play: plugin.onPlay,
|
||||
pause: plugin.onPause,
|
||||
complete: plugin.onComplete,
|
||||
'ended stop': plugin.onEnded,
|
||||
seek: plugin.onSeek,
|
||||
skip: plugin.onSkip,
|
||||
speedchange: plugin.onSpeedChange,
|
||||
autoadvancechange: plugin.onAutoAdvanceChange,
|
||||
'language_menu:show': plugin.onShowLanguageMenu,
|
||||
'language_menu:hide': plugin.onHideLanguageMenu,
|
||||
'transcript:show': plugin.onShowTranscript,
|
||||
'transcript:hide': plugin.onHideTranscript,
|
||||
'captions:show': plugin.onShowCaptions,
|
||||
'captions:hide': plugin.onHideCaptions,
|
||||
destroy: plugin.destroy
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentTime method', function() {
|
||||
it('returns current time adjusted by startTime if video starts from a subsection', function() {
|
||||
spyOn(state.videoPlayer, 'currentTime', 'get').and.returnValue(120);
|
||||
state.config.startTime = 30;
|
||||
state.videoPlayer.currentTime = 120;
|
||||
state.config.startTime = 30;
|
||||
expect(state.videoEventsPlugin.getCurrentTime()).toBe(90); // 120 - 30 = 90
|
||||
});
|
||||
|
||||
it('returns 0 if currentTime is undefined', function() {
|
||||
spyOn(state.videoPlayer, 'currentTime', 'get').and.returnValue(undefined);
|
||||
state.config.startTime = 30; // Start time is irrelevant since current time is undefined
|
||||
state.videoPlayer.currentTime = undefined;
|
||||
expect(state.videoEventsPlugin.getCurrentTime()).toBe(0);
|
||||
});
|
||||
|
||||
it('returns unadjusted current time if startTime is not defined', function() {
|
||||
spyOn(state.videoPlayer, 'currentTime', 'get').and.returnValue(60);
|
||||
state.videoPlayer.currentTime = 60;
|
||||
expect(state.videoEventsPlugin.getCurrentTime()).toBe(60); // Returns current time as is
|
||||
});
|
||||
});
|
||||
|
||||
describe('log method', function() {
|
||||
|
||||
it('logs event with adjusted duration when startTime and endTime are defined', function() {
|
||||
state.config.startTime = 30;
|
||||
state.config.endTime = 150;
|
||||
@@ -261,6 +66,212 @@ import '../helper.js';
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "play_video" event when emitPlayVideoEvent is true', function() {
|
||||
state.videoEventsPlugin.emitPlayVideoEvent = true;
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('play');
|
||||
expect(Logger.log).toHaveBeenCalledWith('play_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can not emit "play_video" event when emitPlayVideoEvent is false', function() {
|
||||
state.videoEventsPlugin.emitPlayVideoEvent = false;
|
||||
state.el.trigger('play');
|
||||
expect(Logger.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can emit "pause_video" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('pause');
|
||||
expect(Logger.log).toHaveBeenCalledWith('pause_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can emit "complete_video" event when video is marked as complete', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('complete');
|
||||
expect(Logger.log).toHaveBeenCalledWith('complete_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "speed_change_video" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('speedchange', ['2.0', '1.0']);
|
||||
expect(Logger.log).toHaveBeenCalledWith('speed_change_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
old_speed: '1.0',
|
||||
new_speed: '2.0',
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "seek_video" event', function() {
|
||||
state.el.trigger('seek', [1, 0, 'any']);
|
||||
expect(Logger.log).toHaveBeenCalledWith('seek_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
old_time: 0,
|
||||
new_time: 1,
|
||||
type: 'any',
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can emit "play_video" event after "seek_video" event ', function() {
|
||||
state.videoEventsPlugin.emitPlayVideoEvent = false;
|
||||
state.el.trigger('seek', [1, 0, 'any']);
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can emit "stop_video" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('ended');
|
||||
expect(Logger.log).toHaveBeenCalledWith('stop_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
|
||||
Logger.log.calls.reset();
|
||||
state.el.trigger('stop');
|
||||
expect(Logger.log).toHaveBeenCalledWith('stop_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
expect(state.videoEventsPlugin.emitPlayVideoEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('can emit "skip_video" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('skip', [false]);
|
||||
expect(Logger.log).toHaveBeenCalledWith('skip_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "do_not_show_again_video" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('skip', [true]);
|
||||
expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
currentTime: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "edx.video.language_menu.shown" event', function() {
|
||||
state.el.trigger('language_menu:show');
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.video.language_menu.shown', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "edx.video.language_menu.hidden" event', function() {
|
||||
state.el.trigger('language_menu:hide');
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.video.language_menu.hidden', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
language: 'en',
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "show_transcript" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('transcript:show');
|
||||
expect(Logger.log).toHaveBeenCalledWith('show_transcript', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "hide_transcript" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('transcript:hide');
|
||||
expect(Logger.log).toHaveBeenCalledWith('hide_transcript', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "edx.video.closed_captions.shown" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('captions:show');
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.video.closed_captions.shown', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can emit "edx.video.closed_captions.hidden" event', function() {
|
||||
spyOn(state.videoEventsPlugin, 'getCurrentTime').and.returnValue(10);
|
||||
state.el.trigger('captions:hide');
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.video.closed_captions.hidden', {
|
||||
id: 'id',
|
||||
code: this.code,
|
||||
current_time: 10,
|
||||
duration: this.duration
|
||||
});
|
||||
});
|
||||
|
||||
it('can destroy itself', function() {
|
||||
var plugin = state.videoEventsPlugin;
|
||||
spyOn($.fn, 'off').and.callThrough();
|
||||
state.videoEventsPlugin.destroy();
|
||||
expect(state.videoEventsPlugin).toBeUndefined();
|
||||
expect($.fn.off).toHaveBeenCalledWith({
|
||||
ready: plugin.onReady,
|
||||
play: plugin.onPlay,
|
||||
pause: plugin.onPause,
|
||||
complete: plugin.onComplete,
|
||||
'ended stop': plugin.onEnded,
|
||||
seek: plugin.onSeek,
|
||||
skip: plugin.onSkip,
|
||||
speedchange: plugin.onSpeedChange,
|
||||
autoadvancechange: plugin.onAutoAdvanceChange,
|
||||
'language_menu:show': plugin.onShowLanguageMenu,
|
||||
'language_menu:hide': plugin.onHideLanguageMenu,
|
||||
'transcript:show': plugin.onShowTranscript,
|
||||
'transcript:hide': plugin.onHideTranscript,
|
||||
'captions:show': plugin.onShowCaptions,
|
||||
'captions:hide': plugin.onHideCaptions,
|
||||
destroy: plugin.destroy
|
||||
});
|
||||
});
|
||||
|
||||
it('logs event with full duration when startTime and endTime are not defined', function() {
|
||||
state.config.startTime = undefined;
|
||||
state.config.endTime = undefined;
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
|
||||
jasmine.stubRequests();
|
||||
loadFixtures('video_html5.html');
|
||||
state = new Video('#example');
|
||||
let runtime = jasmine.createSpyObj('TestRuntime', ['handlerUrl']);
|
||||
state = new Video(runtime, '#example');
|
||||
|
||||
spyOnEvent(state.el, 'mousemove');
|
||||
spyOn(state.focusGrabber, 'disableFocusGrabber').and.callThrough();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* global YT */
|
||||
|
||||
// eslint-disable-next-line no-shadow-restricted-names
|
||||
(function(require, define, undefined) {
|
||||
import VideoPlayer from "../../../assets/video/public/js/03_video_player.js";
|
||||
import HLS from 'hls';
|
||||
import _ from 'underscore';
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
require(
|
||||
['video/03_video_player.js', 'hls', 'underscore'],
|
||||
function(VideoPlayer, HLS, _) {
|
||||
describe('VideoPlayer', function() {
|
||||
var STATUS = window.STATUS,
|
||||
state,
|
||||
@@ -1097,5 +1097,5 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}(require, define));
|
||||
|
||||
}).call(this);
|
||||
|
||||
@@ -153,8 +153,7 @@
|
||||
spyOn(state.videoPlayer, 'onSlideSeek').and.callThrough();
|
||||
});
|
||||
|
||||
// Disabled 12/30/13 due to flakiness in master
|
||||
xit('freeze the slider', function() {
|
||||
it('freeze the slider', function() {
|
||||
state.videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), {value: 20}
|
||||
);
|
||||
@@ -162,8 +161,7 @@
|
||||
expect(state.videoProgressSlider.frozen).toBeTruthy();
|
||||
});
|
||||
|
||||
// Disabled 12/30/13 due to flakiness in master
|
||||
xit('trigger seek event', function() {
|
||||
it('trigger seek event', function() {
|
||||
state.videoProgressSlider.onSlide(
|
||||
jQuery.Event('slide'), {value: 20}
|
||||
);
|
||||
@@ -185,8 +183,7 @@
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
// Disabled 12/30/13 due to flakiness in master
|
||||
xit('freeze the slider', function() {
|
||||
it('freeze the slider', function() {
|
||||
state.videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), {value: 20}
|
||||
);
|
||||
@@ -194,8 +191,7 @@
|
||||
expect(state.videoProgressSlider.frozen).toBeTruthy();
|
||||
});
|
||||
|
||||
// Disabled 12/30/13 due to flakiness in master
|
||||
xit('trigger seek event', function() {
|
||||
it('trigger seek event', function() {
|
||||
state.videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), {value: 20}
|
||||
);
|
||||
@@ -203,8 +199,7 @@
|
||||
expect(state.videoPlayer.onSlideSeek).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Disabled 12/30/13 due to flakiness in master
|
||||
xit('set timeout to unfreeze the slider', function() {
|
||||
it('set timeout to unfreeze the slider', function() {
|
||||
state.videoProgressSlider.onStop(
|
||||
jQuery.Event('stop'), {value: 20}
|
||||
);
|
||||
@@ -259,14 +254,14 @@
|
||||
spyOnEvent(state.videoProgressSlider.handle, 'focus');
|
||||
spyOn(state.videoProgressSlider, 'notifyThroughHandleEnd')
|
||||
.and.callThrough();
|
||||
|
||||
});
|
||||
|
||||
it('params.end = true', function() {
|
||||
state.videoProgressSlider.notifyThroughHandleEnd({end: true});
|
||||
|
||||
expect(state.videoProgressSlider.handle.attr('title'))
|
||||
.toBe('Video ended');
|
||||
|
||||
state.videoProgressSlider.handle.trigger('focus');
|
||||
expect('focus').toHaveBeenTriggeredOn(
|
||||
state.videoProgressSlider.handle
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Time from 'time.js';
|
||||
import * as Time from '../../../assets/video/public/js/utils/time.js';
|
||||
|
||||
// eslint-disable-next-line no-shadow-restricted-names
|
||||
(function(undefined) {
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
+ 'entry has focus', function() {
|
||||
// Open speed meenu. Focus is on last speed entry.
|
||||
$speedControl.trigger(keyPressEvent(KEY.ENTER));
|
||||
$speedControl.mouseenter().mouseleave();
|
||||
$speedControl.mouseenter();
|
||||
expect(speedEntries.last()).toBeFocused();
|
||||
expect($speedControl).toHaveClass('is-opened');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,86 +1,81 @@
|
||||
// eslint-disable-next-line no-shadow-restricted-names
|
||||
(function(require, define, undefined) {
|
||||
require(
|
||||
['video/00_video_storage.js'],
|
||||
function(VideoStorage) {
|
||||
describe('VideoStorage', function() {
|
||||
var namespace = 'test_storage',
|
||||
id = 'video_id';
|
||||
import VideoStorage from '../../../assets/video/public/js/00_video_storage.js';
|
||||
|
||||
afterEach(function() {
|
||||
VideoStorage(namespace, id).clear();
|
||||
});
|
||||
describe('VideoStorage', function() {
|
||||
var namespace = 'test_storage',
|
||||
id = 'video_id';
|
||||
|
||||
describe('initialize', function() {
|
||||
it('with namespace and id', function() {
|
||||
var storage = VideoStorage(namespace, id);
|
||||
afterEach(function() {
|
||||
VideoStorage(namespace, id).clear();
|
||||
});
|
||||
|
||||
expect(window[namespace]).toBeDefined();
|
||||
expect(window[namespace][id]).toBeDefined();
|
||||
});
|
||||
describe('initialize', function() {
|
||||
it('with namespace and id', function() {
|
||||
var storage = VideoStorage(namespace, id);
|
||||
|
||||
it('without namespace and id', function() {
|
||||
spyOn(Number.prototype, 'toString').and.returnValue('0.abcdedg');
|
||||
var storage = VideoStorage();
|
||||
|
||||
expect(window.VideoStorage).toBeDefined();
|
||||
expect(window.VideoStorage.abcdedg).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods: ', function() {
|
||||
var data, storage;
|
||||
|
||||
beforeEach(function() {
|
||||
data = {
|
||||
item_2: 'value_2'
|
||||
};
|
||||
data[id] = {
|
||||
item_1: 'value_1'
|
||||
};
|
||||
|
||||
window[namespace] = data;
|
||||
storage = VideoStorage(namespace, id);
|
||||
});
|
||||
|
||||
it('setItem', function() {
|
||||
var expected = $.extend(true, {}, data, {item_4: 'value_4'});
|
||||
|
||||
expected[id].item_3 = 'value_3';
|
||||
storage.setItem('item_3', 'value_3', true);
|
||||
storage.setItem('item_4', 'value_4');
|
||||
expect(window[namespace]).toEqual(expected);
|
||||
});
|
||||
|
||||
it('getItem', function() {
|
||||
// eslint-disable-next-line no-shadow
|
||||
var data = window[namespace],
|
||||
getItem = storage.getItem;
|
||||
|
||||
expect(getItem('item_1', true)).toBe(data[id].item_1);
|
||||
expect(getItem('item_2')).toBe(data.item_2);
|
||||
expect(getItem('item_3')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removeItem', function() {
|
||||
// eslint-disable-next-line no-shadow
|
||||
var data = window[namespace],
|
||||
removeItem = storage.removeItem;
|
||||
|
||||
removeItem('item_1', true);
|
||||
removeItem('item_2');
|
||||
expect(data[id].item_1).toBeUndefined();
|
||||
expect(data.item_2).toBeUndefined();
|
||||
});
|
||||
|
||||
it('clear', function() {
|
||||
var expected = {};
|
||||
|
||||
expected[id] = {};
|
||||
storage.clear();
|
||||
expect(window[namespace]).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(window[namespace]).toBeDefined();
|
||||
expect(window[namespace][id]).toBeDefined();
|
||||
});
|
||||
}(require, define));
|
||||
|
||||
it('without namespace and id', function() {
|
||||
spyOn(Number.prototype, 'toString').and.returnValue('0.abcdedg');
|
||||
var storage = VideoStorage();
|
||||
|
||||
expect(window.VideoStorage).toBeDefined();
|
||||
expect(window.VideoStorage.abcdedg).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods: ', function() {
|
||||
var data, storage;
|
||||
|
||||
beforeEach(function() {
|
||||
data = {
|
||||
item_2: 'value_2'
|
||||
};
|
||||
data[id] = {
|
||||
item_1: 'value_1'
|
||||
};
|
||||
|
||||
window[namespace] = data;
|
||||
storage = VideoStorage(namespace, id);
|
||||
});
|
||||
|
||||
it('setItem', function() {
|
||||
var expected = $.extend(true, {}, data, {item_4: 'value_4'});
|
||||
|
||||
expected[id].item_3 = 'value_3';
|
||||
storage.setItem('item_3', 'value_3', true);
|
||||
storage.setItem('item_4', 'value_4');
|
||||
expect(window[namespace]).toEqual(expected);
|
||||
});
|
||||
|
||||
it('getItem', function() {
|
||||
// eslint-disable-next-line no-shadow
|
||||
var data = window[namespace],
|
||||
getItem = storage.getItem;
|
||||
|
||||
expect(getItem('item_1', true)).toBe(data[id].item_1);
|
||||
expect(getItem('item_2')).toBe(data.item_2);
|
||||
expect(getItem('item_3')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removeItem', function() {
|
||||
// eslint-disable-next-line no-shadow
|
||||
var data = window[namespace],
|
||||
removeItem = storage.removeItem;
|
||||
|
||||
removeItem('item_1', true);
|
||||
removeItem('item_2');
|
||||
expect(data[id].item_1).toBeUndefined();
|
||||
expect(data.item_2).toBeUndefined();
|
||||
});
|
||||
|
||||
it('clear', function() {
|
||||
var expected = {};
|
||||
|
||||
expected[id] = {};
|
||||
storage.clear();
|
||||
expect(window[namespace]).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
(function(define) {
|
||||
define(
|
||||
'video/00_async_process.js',
|
||||
[],
|
||||
function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides convenient way to process big amount of data without UI blocking.
|
||||
*
|
||||
* @param {array} list Array to process.
|
||||
* @param {function} process Calls this function on each item in the list.
|
||||
* @return {array} Returns a Promise object to observe when all actions of a
|
||||
* certain type bound to the collection, queued or not, have finished.
|
||||
*/
|
||||
var AsyncProcess = {
|
||||
array: function(list, process) {
|
||||
if (!_.isArray(list)) {
|
||||
return $.Deferred().reject().promise();
|
||||
}
|
||||
|
||||
if (!_.isFunction(process) || !list.length) {
|
||||
return $.Deferred().resolve(list).promise();
|
||||
}
|
||||
|
||||
var MAX_DELAY = 50, // maximum amount of time that js code should be allowed to run continuously
|
||||
dfd = $.Deferred(),
|
||||
result = [],
|
||||
index = 0,
|
||||
len = list.length;
|
||||
|
||||
var getCurrentTime = function() {
|
||||
return (new Date()).getTime();
|
||||
};
|
||||
|
||||
var handler = function() {
|
||||
var start = getCurrentTime();
|
||||
|
||||
do {
|
||||
result[index] = process(list[index], index);
|
||||
index++;
|
||||
} while (index < len && getCurrentTime() - start < MAX_DELAY);
|
||||
|
||||
if (index < len) {
|
||||
setTimeout(handler, 25);
|
||||
} else {
|
||||
dfd.resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(handler, 25);
|
||||
|
||||
return dfd.promise();
|
||||
}
|
||||
};
|
||||
|
||||
return AsyncProcess;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,83 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/00_component.js', [],
|
||||
function() {
|
||||
/**
|
||||
* Creates a new object with the specified prototype object and properties.
|
||||
* @param {Object} o The object which should be the prototype of the
|
||||
* newly-created object.
|
||||
* @private
|
||||
* @throws {TypeError, Error}
|
||||
* @return {Object}
|
||||
*/
|
||||
var inherit = Object.create || (function() {
|
||||
var F = function() {};
|
||||
|
||||
return function(o) {
|
||||
if (arguments.length > 1) {
|
||||
throw Error('Second argument not supported');
|
||||
}
|
||||
if (_.isNull(o) || _.isUndefined(o)) {
|
||||
throw Error('Cannot set a null [[Prototype]]');
|
||||
}
|
||||
if (!_.isObject(o)) {
|
||||
throw TypeError('Argument must be an object');
|
||||
}
|
||||
|
||||
F.prototype = o;
|
||||
|
||||
return new F();
|
||||
};
|
||||
}());
|
||||
|
||||
/**
|
||||
* Component module.
|
||||
* @exports video/00_component.js
|
||||
* @constructor
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var Component = function() {
|
||||
if ($.isFunction(this.initialize)) {
|
||||
// eslint-disable-next-line prefer-spread
|
||||
return this.initialize.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns new constructor that inherits form the current constructor.
|
||||
* @static
|
||||
* @param {Object} protoProps The object containing which will be added to
|
||||
* the prototype.
|
||||
* @return {Object}
|
||||
*/
|
||||
Component.extend = function(protoProps, staticProps) {
|
||||
var Parent = this,
|
||||
Child = function() {
|
||||
if ($.isFunction(this.initialize)) {
|
||||
// eslint-disable-next-line prefer-spread
|
||||
return this.initialize.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
// Inherit methods and properties from the Parent prototype.
|
||||
Child.prototype = inherit(Parent.prototype);
|
||||
Child.constructor = Parent;
|
||||
// Provide access to parent's methods and properties
|
||||
Child.__super__ = Parent.prototype;
|
||||
|
||||
// Extends inherited methods and properties by methods/properties
|
||||
// passed as argument.
|
||||
if (protoProps) {
|
||||
$.extend(Child.prototype, protoProps);
|
||||
}
|
||||
|
||||
// Inherit static methods and properties
|
||||
$.extend(Child, Parent, staticProps);
|
||||
|
||||
return Child;
|
||||
};
|
||||
|
||||
return Component;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,40 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
'video/00_i18n.js',
|
||||
[],
|
||||
function() {
|
||||
/**
|
||||
* i18n module.
|
||||
* @exports video/00_i18n.js
|
||||
* @return {object}
|
||||
*/
|
||||
|
||||
return {
|
||||
Play: gettext('Play'),
|
||||
Pause: gettext('Pause'),
|
||||
Mute: gettext('Mute'),
|
||||
Unmute: gettext('Unmute'),
|
||||
'Exit full browser': gettext('Exit full browser'),
|
||||
'Fill browser': gettext('Fill browser'),
|
||||
Speed: gettext('Speed'),
|
||||
'Auto-advance': gettext('Auto-advance'),
|
||||
Volume: gettext('Volume'),
|
||||
// Translators: Volume level equals 0%.
|
||||
Muted: gettext('Muted'),
|
||||
// Translators: Volume level in range ]0,20]%
|
||||
'Very low': gettext('Very low'),
|
||||
// Translators: Volume level in range ]20,40]%
|
||||
Low: gettext('Low'),
|
||||
// Translators: Volume level in range ]40,60]%
|
||||
Average: gettext('Average'),
|
||||
// Translators: Volume level in range ]60,80]%
|
||||
Loud: gettext('Loud'),
|
||||
// Translators: Volume level in range ]80,99]%
|
||||
'Very loud': gettext('Very loud'),
|
||||
// Translators: Volume level equals 100%.
|
||||
Maximum: gettext('Maximum')
|
||||
};
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,90 +0,0 @@
|
||||
(function(define) {
|
||||
define(
|
||||
'video/00_iterator.js',
|
||||
[],
|
||||
function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides convenient way to work with iterable data.
|
||||
* @exports video/00_iterator.js
|
||||
* @constructor
|
||||
* @param {array} list Array to be iterated.
|
||||
*/
|
||||
var Iterator = function(list) {
|
||||
this.list = list;
|
||||
this.index = 0;
|
||||
this.size = this.list.length;
|
||||
this.lastIndex = this.list.length - 1;
|
||||
};
|
||||
|
||||
Iterator.prototype = {
|
||||
|
||||
/**
|
||||
* Checks validity of provided index for the iterator.
|
||||
* @access protected
|
||||
* @param {numebr} index
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isValid: function(index) {
|
||||
return _.isNumber(index) && index < this.size && index >= 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns next element.
|
||||
* @param {number} [index] Updates current position.
|
||||
* @return {any}
|
||||
*/
|
||||
next: function(index) {
|
||||
if (!(this._isValid(index))) {
|
||||
index = this.index;
|
||||
}
|
||||
|
||||
this.index = (index >= this.lastIndex) ? 0 : index + 1;
|
||||
|
||||
return this.list[this.index];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns previous element.
|
||||
* @param {number} [index] Updates current position.
|
||||
* @return {any}
|
||||
*/
|
||||
prev: function(index) {
|
||||
if (!(this._isValid(index))) {
|
||||
index = this.index;
|
||||
}
|
||||
|
||||
this.index = (index < 1) ? this.lastIndex : index - 1;
|
||||
|
||||
return this.list[this.index];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns last element in the list.
|
||||
* @return {any}
|
||||
*/
|
||||
last: function() {
|
||||
return this.list[this.lastIndex];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns first element in the list.
|
||||
* @return {any}
|
||||
*/
|
||||
first: function() {
|
||||
return this.list[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns `true` if current position is last for the iterator.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isEnd: function() {
|
||||
return this.index === this.lastIndex;
|
||||
}
|
||||
};
|
||||
|
||||
return Iterator;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,238 +0,0 @@
|
||||
(function(requirejs, require, define) {
|
||||
define(
|
||||
'video/00_resizer.js',
|
||||
[],
|
||||
function() {
|
||||
var Resizer = function(params) {
|
||||
var defaults = {
|
||||
container: window,
|
||||
element: null,
|
||||
containerRatio: null,
|
||||
elementRatio: null
|
||||
},
|
||||
callbacksList = [],
|
||||
delta = {
|
||||
height: 0,
|
||||
width: 0
|
||||
},
|
||||
module = {},
|
||||
mode = null,
|
||||
config;
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
var initialize = function(params) {
|
||||
if (!config) {
|
||||
config = defaults;
|
||||
}
|
||||
|
||||
config = $.extend(true, {}, config, params);
|
||||
|
||||
if (!config.element) {
|
||||
console.log(
|
||||
'Required parameter `element` is not passed.'
|
||||
);
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var getData = function() {
|
||||
var $container = $(config.container),
|
||||
containerWidth = $container.width() + delta.width,
|
||||
containerHeight = $container.height() + delta.height,
|
||||
containerRatio = config.containerRatio,
|
||||
|
||||
$element = $(config.element),
|
||||
elementRatio = config.elementRatio;
|
||||
|
||||
if (!containerRatio) {
|
||||
containerRatio = containerWidth / containerHeight;
|
||||
}
|
||||
|
||||
if (!elementRatio) {
|
||||
elementRatio = $element.width() / $element.height();
|
||||
}
|
||||
|
||||
return {
|
||||
containerWidth: containerWidth,
|
||||
containerHeight: containerHeight,
|
||||
containerRatio: containerRatio,
|
||||
element: $element,
|
||||
elementRatio: elementRatio
|
||||
};
|
||||
};
|
||||
|
||||
var align = function() {
|
||||
var data = getData();
|
||||
|
||||
switch (mode) {
|
||||
case 'height':
|
||||
alignByHeightOnly();
|
||||
break;
|
||||
|
||||
case 'width':
|
||||
alignByWidthOnly();
|
||||
break;
|
||||
|
||||
default:
|
||||
if (data.containerRatio >= data.elementRatio) {
|
||||
alignByHeightOnly();
|
||||
} else {
|
||||
alignByWidthOnly();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
fireCallbacks();
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var alignByWidthOnly = function() {
|
||||
var data = getData(),
|
||||
height = data.containerWidth / data.elementRatio;
|
||||
|
||||
data.element.css({
|
||||
height: height,
|
||||
width: data.containerWidth,
|
||||
top: 0.5 * (data.containerHeight - height),
|
||||
left: 0
|
||||
});
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var alignByHeightOnly = function() {
|
||||
var data = getData(),
|
||||
width = data.containerHeight * data.elementRatio;
|
||||
|
||||
data.element.css({
|
||||
height: data.containerHeight,
|
||||
width: data.containerHeight * data.elementRatio,
|
||||
top: 0,
|
||||
left: 0.5 * (data.containerWidth - width)
|
||||
});
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var setMode = function(param) {
|
||||
if (_.isString(param)) {
|
||||
mode = param;
|
||||
align();
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var setElement = function(element) {
|
||||
config.element = element;
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var addCallback = function(func) {
|
||||
if ($.isFunction(func)) {
|
||||
callbacksList.push(func);
|
||||
} else {
|
||||
console.error('[Video info]: TypeError: Argument is not a function.');
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var addOnceCallback = function(func) {
|
||||
if ($.isFunction(func)) {
|
||||
var decorator = function() {
|
||||
func();
|
||||
removeCallback(func);
|
||||
};
|
||||
|
||||
addCallback(decorator);
|
||||
} else {
|
||||
console.error('TypeError: Argument is not a function.');
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var fireCallbacks = function() {
|
||||
$.each(callbacksList, function(index, callback) {
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
var removeCallbacks = function() {
|
||||
callbacksList.length = 0;
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var removeCallback = function(func) {
|
||||
var index = $.inArray(func, callbacksList);
|
||||
|
||||
if (index !== -1) {
|
||||
return callbacksList.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
var resetDelta = function() {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
delta.height = delta.width = 0;
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var addDelta = function(value, side) {
|
||||
if (_.isNumber(value) && _.isNumber(delta[side])) {
|
||||
delta[side] += value;
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var substractDelta = function(value, side) {
|
||||
if (_.isNumber(value) && _.isNumber(delta[side])) {
|
||||
delta[side] -= value;
|
||||
}
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
var destroy = function() {
|
||||
var data = getData();
|
||||
data.element.css({
|
||||
height: '', width: '', top: '', left: ''
|
||||
});
|
||||
removeCallbacks();
|
||||
resetDelta();
|
||||
mode = null;
|
||||
};
|
||||
|
||||
initialize.apply(module, arguments);
|
||||
|
||||
return $.extend(true, module, {
|
||||
align: align,
|
||||
alignByWidthOnly: alignByWidthOnly,
|
||||
alignByHeightOnly: alignByHeightOnly,
|
||||
destroy: destroy,
|
||||
setParams: initialize,
|
||||
setMode: setMode,
|
||||
setElement: setElement,
|
||||
callbacks: {
|
||||
add: addCallback,
|
||||
once: addOnceCallback,
|
||||
remove: removeCallback,
|
||||
removeAll: removeCallbacks
|
||||
},
|
||||
delta: {
|
||||
add: addDelta,
|
||||
substract: substractDelta,
|
||||
reset: resetDelta
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return Resizer;
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,115 +0,0 @@
|
||||
(function(define) {
|
||||
define(
|
||||
'video/00_sjson.js',
|
||||
[],
|
||||
function() {
|
||||
'use strict';
|
||||
|
||||
var Sjson = function(data) {
|
||||
var sjson = {
|
||||
start: data.start.concat(),
|
||||
text: data.text.concat()
|
||||
},
|
||||
module = {};
|
||||
|
||||
var getter = function(propertyName) {
|
||||
return function() {
|
||||
return sjson[propertyName];
|
||||
};
|
||||
};
|
||||
|
||||
var getStartTimes = getter('start');
|
||||
|
||||
var getCaptions = getter('text');
|
||||
|
||||
var size = function() {
|
||||
return sjson.text.length;
|
||||
};
|
||||
|
||||
function search(time, startTime, endTime) {
|
||||
var start = getStartTimes(),
|
||||
max = size() - 1,
|
||||
min = 0,
|
||||
results,
|
||||
index;
|
||||
|
||||
// if we specify a start and end time to search,
|
||||
// search the filtered list of captions in between
|
||||
// the start / end times.
|
||||
// Else, search the unfiltered list.
|
||||
if (typeof startTime !== 'undefined'
|
||||
&& typeof endTime !== 'undefined') {
|
||||
results = filter(startTime, endTime);
|
||||
start = results.start;
|
||||
max = results.captions.length - 1;
|
||||
} else {
|
||||
start = getStartTimes();
|
||||
}
|
||||
while (min < max) {
|
||||
index = Math.ceil((max + min) / 2);
|
||||
|
||||
if (time < start[index]) {
|
||||
max = index - 1;
|
||||
}
|
||||
|
||||
if (time >= start[index]) {
|
||||
min = index;
|
||||
}
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
function filter(start, end) {
|
||||
/* filters captions that occur between inputs
|
||||
* `start` and `end`. Start and end should
|
||||
* be Numbers (doubles) corresponding to the
|
||||
* number of seconds elapsed since the beginning
|
||||
* of the video.
|
||||
*
|
||||
* Returns an object with properties
|
||||
* "start" and "captions" representing
|
||||
* parallel arrays of start times and
|
||||
* their corresponding captions.
|
||||
*/
|
||||
var filteredTimes = [];
|
||||
var filteredCaptions = [];
|
||||
var startTimes = getStartTimes();
|
||||
var captions = getCaptions();
|
||||
|
||||
if (startTimes.length !== captions.length) {
|
||||
console.warn('video caption and start time arrays do not match in length');
|
||||
}
|
||||
|
||||
// if end is null, then it's been set to
|
||||
// some erroneous value, so filter using the
|
||||
// entire array as long as it's not empty
|
||||
if (end === null && startTimes.length) {
|
||||
end = startTimes[startTimes.length - 1];
|
||||
}
|
||||
|
||||
_.filter(startTimes, function(currentStartTime, i) {
|
||||
if (currentStartTime >= start && currentStartTime <= end) {
|
||||
filteredTimes.push(currentStartTime);
|
||||
filteredCaptions.push(captions[i]);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
start: filteredTimes,
|
||||
captions: filteredCaptions
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getCaptions: getCaptions,
|
||||
getStartTimes: getStartTimes,
|
||||
getSize: size,
|
||||
filter: filter,
|
||||
search: search
|
||||
};
|
||||
};
|
||||
|
||||
return Sjson;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,103 +0,0 @@
|
||||
(function(requirejs, require, define) {
|
||||
define(
|
||||
'video/00_video_storage.js',
|
||||
[],
|
||||
function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides convenient way to store key value pairs.
|
||||
*
|
||||
* @param {string} namespace Namespace that is used to store data.
|
||||
* @return {object} VideoStorage API.
|
||||
*/
|
||||
var VideoStorage = function(namespace, id) {
|
||||
/**
|
||||
* Adds new value to the storage or rewrites existent.
|
||||
*
|
||||
* @param {string} name Identifier of the data.
|
||||
* @param {any} value Data to store.
|
||||
* @param {boolean} instanceSpecific Data with this flag will be added
|
||||
* to instance specific storage.
|
||||
*/
|
||||
var setItem = function(name, value, instanceSpecific) {
|
||||
if (name) {
|
||||
if (instanceSpecific) {
|
||||
window[namespace][id][name] = value;
|
||||
} else {
|
||||
window[namespace][name] = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current value associated with the given name.
|
||||
*
|
||||
* @param {string} name Identifier of the data.
|
||||
* @param {boolean} instanceSpecific Data with this flag will be added
|
||||
* to instance specific storage.
|
||||
* @return {any} The current value associated with the given name.
|
||||
* If the given key does not exist in the list
|
||||
* associated with the object then this method must return null.
|
||||
*/
|
||||
var getItem = function(name, instanceSpecific) {
|
||||
if (instanceSpecific) {
|
||||
return window[namespace][id][name];
|
||||
} else {
|
||||
return window[namespace][name];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the current value associated with the given name.
|
||||
*
|
||||
* @param {string} name Identifier of the data.
|
||||
* @param {boolean} instanceSpecific Data with this flag will be added
|
||||
* to instance specific storage.
|
||||
*/
|
||||
var removeItem = function(name, instanceSpecific) {
|
||||
if (instanceSpecific) {
|
||||
delete window[namespace][id][name];
|
||||
} else {
|
||||
delete window[namespace][name];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Empties the storage.
|
||||
*
|
||||
*/
|
||||
var clear = function() {
|
||||
window[namespace] = {};
|
||||
window[namespace][id] = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the module: creates a storage with proper namespace.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
(function initialize() {
|
||||
if (!namespace) {
|
||||
namespace = 'VideoStorage';
|
||||
}
|
||||
if (!id) {
|
||||
// Generate random alpha-numeric string.
|
||||
id = Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
window[namespace] = window[namespace] || {};
|
||||
window[namespace][id] = window[namespace][id] || {};
|
||||
}());
|
||||
|
||||
return {
|
||||
clear: clear,
|
||||
getItem: getItem,
|
||||
removeItem: removeItem,
|
||||
setItem: setItem
|
||||
};
|
||||
};
|
||||
|
||||
return VideoStorage;
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,845 +0,0 @@
|
||||
/* 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
|
||||
*/
|
||||
|
||||
(function(requirejs, require, define) {
|
||||
define(
|
||||
'video/01_initialize.js',
|
||||
['video/03_video_player.js', 'video/00_i18n.js', 'moment', 'underscore'],
|
||||
function(VideoPlayer, i18n, moment, _) {
|
||||
var moment = moment || window.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.
|
||||
*/
|
||||
var 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 */
|
||||
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 */
|
||||
|
||||
_youtubeApiDeferred = null,
|
||||
_oldOnYouTubeIframeAPIReady;
|
||||
|
||||
Initialize.prototype = methodsDict;
|
||||
|
||||
return 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.
|
||||
var video, onYTApiReady, 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) {
|
||||
var 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) {
|
||||
var dfd = $.Deferred(),
|
||||
modulesList = $.map(state.modules, function(module) {
|
||||
var 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) {
|
||||
var isBoolean = function(value) {
|
||||
var 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) {
|
||||
var 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) {
|
||||
var 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);
|
||||
|
||||
var 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) {
|
||||
var 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() {
|
||||
var self = this,
|
||||
metadataXHRs = [];
|
||||
|
||||
this.metadata = {};
|
||||
|
||||
metadataXHRs = _.map(this.videos, function(url, speed) {
|
||||
return self.getVideoMetadata(url, function(data) {
|
||||
if (data.items.length > 0) {
|
||||
var 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]
|
||||
var 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) {
|
||||
var 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) {
|
||||
var currentSpeed = this.isFlashMode() ? this.speed : '1.0';
|
||||
|
||||
return this.videos[speed]
|
||||
|| this.videos[currentSpeed]
|
||||
|| this.videos['1.0'];
|
||||
}
|
||||
|
||||
function getDuration() {
|
||||
try {
|
||||
return moment.duration(this.metadata[this.youtubeId()].duration, moment.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) {
|
||||
var 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() {
|
||||
var 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) {
|
||||
var 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;
|
||||
}
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
* 025_focus_grabber.js
|
||||
*
|
||||
* Purpose: Provide a way to focus on autohidden Video controls.
|
||||
*
|
||||
*
|
||||
* Because in HTML player mode we have a feature of autohiding controls on
|
||||
* mouse inactivity, sometimes focus is lost from the currently selected
|
||||
* control. What's more, when all controls are autohidden, we can't get to any
|
||||
* of them because by default browser does not place hidden elements on the
|
||||
* focus chain.
|
||||
*
|
||||
* To get around this minor annoyance, this module will manage 2 placeholder
|
||||
* elements that will be invisible to the user's eye, but visible to the
|
||||
* browser. This will allow for a sneaky stealing of focus and placing it where
|
||||
* we need (on hidden controls).
|
||||
*
|
||||
* This code has been moved to a separate module because it provides a concrete
|
||||
* block of functionality that can be turned on (off).
|
||||
*/
|
||||
|
||||
/*
|
||||
* "If you want to climb a mountain, begin at the top."
|
||||
*
|
||||
* ~ Zen saying
|
||||
*/
|
||||
|
||||
(function(requirejs, require, define) {
|
||||
// FocusGrabber module.
|
||||
define(
|
||||
'video/025_focus_grabber.js',
|
||||
[],
|
||||
function() {
|
||||
return function(state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
state.focusGrabber = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// Private functions.
|
||||
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
disableFocusGrabber: disableFocusGrabber,
|
||||
enableFocusGrabber: enableFocusGrabber,
|
||||
onFocus: onFocus
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.focusGrabber, state);
|
||||
}
|
||||
|
||||
function _renderElements(state) {
|
||||
state.focusGrabber.elFirst = state.el.find('.focus_grabber.first');
|
||||
state.focusGrabber.elLast = state.el.find('.focus_grabber.last');
|
||||
|
||||
// From the start, the Focus Grabber must be disabled so that
|
||||
// tabbing (switching focus) does not land the user on one of the
|
||||
// placeholder elements (elFirst, elLast).
|
||||
state.focusGrabber.disableFocusGrabber();
|
||||
}
|
||||
|
||||
function _bindHandlers(state) {
|
||||
state.focusGrabber.elFirst.on('focus', state.focusGrabber.onFocus);
|
||||
state.focusGrabber.elLast.on('focus', state.focusGrabber.onFocus);
|
||||
|
||||
// When the video container element receives programmatic focus, then
|
||||
// on un-focus ('blur' event) we should trigger a 'mousemove' event so
|
||||
// as to reveal autohidden controls.
|
||||
state.el.on('blur', function() {
|
||||
state.el.trigger('mousemove');
|
||||
});
|
||||
}
|
||||
|
||||
// Public functions.
|
||||
|
||||
function enableFocusGrabber() {
|
||||
var tabIndex;
|
||||
|
||||
// When the Focus Grabber is being enabled, there are two different
|
||||
// scenarios:
|
||||
//
|
||||
// 1.) Currently focused element was inside the video player.
|
||||
// 2.) Currently focused element was somewhere else on the page.
|
||||
//
|
||||
// In the first case we must make sure that the video player doesn't
|
||||
// loose focus, even though the controls are autohidden.
|
||||
if ($(document.activeElement).parents().hasClass('video')) {
|
||||
tabIndex = -1;
|
||||
} else {
|
||||
tabIndex = 0;
|
||||
}
|
||||
|
||||
this.focusGrabber.elFirst.attr('tabindex', tabIndex);
|
||||
this.focusGrabber.elLast.attr('tabindex', tabIndex);
|
||||
|
||||
// Don't loose focus. We are inside video player on some control, but
|
||||
// because we can't remain focused on a hidden element, we will shift
|
||||
// focus to the main video element.
|
||||
//
|
||||
// Once the main element will receive the un-focus ('blur') event, a
|
||||
// 'mousemove' event will be triggered, and the video controls will
|
||||
// receive focus once again.
|
||||
if (tabIndex === -1) {
|
||||
this.el.focus();
|
||||
|
||||
this.focusGrabber.elFirst.attr('tabindex', 0);
|
||||
this.focusGrabber.elLast.attr('tabindex', 0);
|
||||
}
|
||||
}
|
||||
|
||||
function disableFocusGrabber() {
|
||||
// Only programmatic focusing on these elements will be available.
|
||||
// We don't want the user to focus on them (for example with the 'Tab'
|
||||
// key).
|
||||
this.focusGrabber.elFirst.attr('tabindex', -1);
|
||||
this.focusGrabber.elLast.attr('tabindex', -1);
|
||||
}
|
||||
|
||||
function onFocus(event, params) {
|
||||
// Once the Focus Grabber placeholder elements will gain focus, we will
|
||||
// trigger 'mousemove' event so that the autohidden controls will
|
||||
// become visible.
|
||||
this.el.trigger('mousemove');
|
||||
|
||||
this.focusGrabber.disableFocusGrabber();
|
||||
}
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,146 +0,0 @@
|
||||
/* eslint-disable no-console, no-param-reassign */
|
||||
/**
|
||||
* HTML5 video player module to support HLS video playback.
|
||||
*
|
||||
*/
|
||||
|
||||
(function(requirejs, require, define) {
|
||||
'use strict';
|
||||
|
||||
define('video/02_html5_hls_video.js', ['underscore', 'video/02_html5_video.js', 'hls'],
|
||||
function(_, HTML5Video, HLS) {
|
||||
var HLSVideo = {};
|
||||
|
||||
HLSVideo.Player = (function() {
|
||||
/**
|
||||
* Initialize HLS video player.
|
||||
*
|
||||
* @param {jQuery} el Reference to video player container element
|
||||
* @param {Object} config Contains common config for video player
|
||||
*/
|
||||
function Player(el, config) {
|
||||
var self = this;
|
||||
|
||||
this.config = config;
|
||||
|
||||
// do common initialization independent of player type
|
||||
this.init(el, config);
|
||||
|
||||
_.bindAll(this, 'playVideo', 'pauseVideo', 'onReady');
|
||||
|
||||
// If we have only HLS sources and browser doesn't support HLS then show error message.
|
||||
if (config.HLSOnlySources && !config.canPlayHLS) {
|
||||
this.showErrorMessage(null, '.video-hls-error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.state.el.on('initialize', _.once(function() {
|
||||
console.log('[HLS Video]: HLS Player initialized');
|
||||
self.showPlayButton();
|
||||
}));
|
||||
|
||||
// Safari has native support to play HLS videos
|
||||
if (config.browserIsSafari) {
|
||||
this.videoEl.attr('src', config.videoSources[0]);
|
||||
} else {
|
||||
// load auto start if auto_advance is enabled
|
||||
if (config.state.auto_advance) {
|
||||
this.hls = new HLS({autoStartLoad: true});
|
||||
} else {
|
||||
this.hls = new HLS({autoStartLoad: false});
|
||||
}
|
||||
this.hls.loadSource(config.videoSources[0]);
|
||||
this.hls.attachMedia(this.video);
|
||||
|
||||
this.hls.on(HLS.Events.ERROR, this.onError.bind(this));
|
||||
|
||||
this.hls.on(HLS.Events.MANIFEST_PARSED, function(event, data) {
|
||||
console.log(
|
||||
'[HLS Video]: MANIFEST_PARSED, qualityLevelsInfo: ',
|
||||
data.levels.map(function(level) {
|
||||
return {
|
||||
bitrate: level.bitrate,
|
||||
resolution: level.width + 'x' + level.height
|
||||
};
|
||||
})
|
||||
);
|
||||
self.config.onReadyHLS();
|
||||
});
|
||||
this.hls.on(HLS.Events.LEVEL_SWITCHED, function(event, data) {
|
||||
var level = self.hls.levels[data.level];
|
||||
console.log(
|
||||
'[HLS Video]: LEVEL_SWITCHED, qualityLevelInfo: ',
|
||||
{
|
||||
bitrate: level.bitrate,
|
||||
resolution: level.width + 'x' + level.height
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Player.prototype = Object.create(HTML5Video.Player.prototype);
|
||||
Player.prototype.constructor = Player;
|
||||
|
||||
Player.prototype.playVideo = function() {
|
||||
HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['show']);
|
||||
if (!this.config.browserIsSafari) {
|
||||
this.hls.startLoad();
|
||||
}
|
||||
HTML5Video.Player.prototype.playVideo.apply(this);
|
||||
};
|
||||
|
||||
Player.prototype.pauseVideo = function() {
|
||||
HTML5Video.Player.prototype.pauseVideo.apply(this);
|
||||
HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['hide']);
|
||||
};
|
||||
|
||||
Player.prototype.onPlaying = function() {
|
||||
HTML5Video.Player.prototype.onPlaying.apply(this);
|
||||
HTML5Video.Player.prototype.updatePlayerLoadingState.apply(this, ['hide']);
|
||||
};
|
||||
|
||||
Player.prototype.onReady = function() {
|
||||
this.config.events.onReady(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler for HLS video errors. This only takes care of fatal erros, non-fatal errors
|
||||
* are automatically handled by hls.js
|
||||
*
|
||||
* @param {String} event `hlsError`
|
||||
* @param {Object} data Contains the information regarding error occurred.
|
||||
*/
|
||||
Player.prototype.onError = function(event, data) {
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case HLS.ErrorTypes.NETWORK_ERROR:
|
||||
console.error(
|
||||
'[HLS Video]: Fatal network error encountered, try to recover. Details: %s',
|
||||
data.details
|
||||
);
|
||||
this.hls.startLoad();
|
||||
break;
|
||||
case HLS.ErrorTypes.MEDIA_ERROR:
|
||||
console.error(
|
||||
'[HLS Video]: Fatal media error encountered, try to recover. Details: %s',
|
||||
data.details
|
||||
);
|
||||
this.hls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
'[HLS Video]: Unrecoverable error encountered. Details: %s',
|
||||
data.details
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Player;
|
||||
}());
|
||||
|
||||
return HLSVideo;
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,390 +0,0 @@
|
||||
/* eslint-disable no-console, no-param-reassign */
|
||||
/**
|
||||
* @file HTML5 video player module. Provides methods to control the in-browser
|
||||
* HTML5 video player.
|
||||
*
|
||||
* The goal was to write this module so that it closely resembles the YouTube
|
||||
* API. The main reason for this is because initially the edX video player
|
||||
* supported only YouTube videos. When HTML5 support was added, for greater
|
||||
* compatibility, and to reduce the amount of code that needed to be modified,
|
||||
* it was decided to write a similar API as the one provided by YouTube.
|
||||
*
|
||||
* @external RequireJS
|
||||
*
|
||||
* @module HTML5Video
|
||||
*/
|
||||
|
||||
(function(requirejs, require, define) {
|
||||
define(
|
||||
'video/02_html5_video.js',
|
||||
['underscore'],
|
||||
function(_) {
|
||||
var HTML5Video = {};
|
||||
|
||||
HTML5Video.Player = (function() {
|
||||
/*
|
||||
* Constructor function for HTML5 Video player.
|
||||
*
|
||||
* @param {String|Object} el A DOM element where the HTML5 player will
|
||||
* be inserted (as returned by jQuery(selector) function), or a
|
||||
* selector string which will be used to select an element. This is a
|
||||
* required parameter.
|
||||
*
|
||||
* @param config - An object whose properties will be used as
|
||||
* configuration options for the HTML5 video player. This is an
|
||||
* optional parameter. In the case if this parameter is missing, or
|
||||
* some of the config object's properties are missing, defaults will be
|
||||
* used. The available options (and their defaults) are as
|
||||
* follows:
|
||||
*
|
||||
* config = {
|
||||
*
|
||||
* videoSources: [], // An array with properties being video
|
||||
* // sources. The property name is the
|
||||
* // video format of the source. Supported
|
||||
* // video formats are: 'mp4', 'webm', and
|
||||
* // 'ogg'.
|
||||
* poster: Video poster URL
|
||||
*
|
||||
* browserIsSafari: Flag to tell if current browser is Safari
|
||||
*
|
||||
* events: { // Object's properties identify the
|
||||
* // events that the API fires, and the
|
||||
* // functions (event listeners) that the
|
||||
* // API will call when those events occur.
|
||||
* // If value is null, or property is not
|
||||
* // specified, then no callback will be
|
||||
* // called for that event.
|
||||
*
|
||||
* onReady: null,
|
||||
* onStateChange: null
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
function Player(el, config) {
|
||||
var errorMessage, lastSource, sourceList;
|
||||
|
||||
// Create HTML markup for individual sources of the HTML5 <video> element.
|
||||
sourceList = $.map(config.videoSources, function(source) {
|
||||
return [
|
||||
'<source ',
|
||||
'src="', source,
|
||||
// Following hack allows to open the same video twice
|
||||
// https://code.google.com/p/chromium/issues/detail?id=31014
|
||||
// Check whether the url already has a '?' inside, and if so,
|
||||
// use '&' instead of '?' to prevent breaking the url's integrity.
|
||||
(source.indexOf('?') === -1 ? '?' : '&'),
|
||||
(new Date()).getTime(), '" />'
|
||||
].join('');
|
||||
});
|
||||
|
||||
// do common initialization independent of player type
|
||||
this.init(el, config);
|
||||
|
||||
// Create HTML markup for the <video> element, populating it with
|
||||
// sources from previous step. Set playback not supported error message.
|
||||
errorMessage = [
|
||||
gettext('This browser cannot play .mp4, .ogg, or .webm files.'),
|
||||
gettext('Try using a different browser, such as Google Chrome.')
|
||||
].join('');
|
||||
this.video.innerHTML = sourceList.join('') + errorMessage;
|
||||
|
||||
lastSource = this.videoEl.find('source').last();
|
||||
lastSource.on('error', this.showErrorMessage.bind(this));
|
||||
lastSource.on('error', this.onError.bind(this));
|
||||
this.videoEl.on('error', this.onError.bind(this));
|
||||
}
|
||||
|
||||
Player.prototype.showPlayButton = function() {
|
||||
this.videoOverlayEl.removeClass('is-hidden');
|
||||
};
|
||||
|
||||
Player.prototype.hidePlayButton = function() {
|
||||
this.videoOverlayEl.addClass('is-hidden');
|
||||
};
|
||||
|
||||
Player.prototype.showLoading = function() {
|
||||
this.el
|
||||
.removeClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.removeAttr('tabindex')
|
||||
.attr({'aria-hidden': 'false'});
|
||||
};
|
||||
|
||||
Player.prototype.hideLoading = function() {
|
||||
this.el
|
||||
.addClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.attr({'aria-hidden': 'false', tabindex: -1});
|
||||
};
|
||||
|
||||
Player.prototype.updatePlayerLoadingState = function(state) {
|
||||
if (state === 'show') {
|
||||
this.hidePlayButton();
|
||||
this.showLoading();
|
||||
} else if (state === 'hide') {
|
||||
this.hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.callStateChangeCallback = function() {
|
||||
if ($.isFunction(this.config.events.onStateChange)) {
|
||||
this.config.events.onStateChange({
|
||||
data: this.playerState
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.pauseVideo = function() {
|
||||
this.video.pause();
|
||||
};
|
||||
|
||||
Player.prototype.seekTo = function(value) {
|
||||
if (
|
||||
typeof value === 'number'
|
||||
&& value <= this.video.duration
|
||||
&& value >= 0
|
||||
) {
|
||||
this.video.currentTime = value;
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.setVolume = function(value) {
|
||||
if (typeof value === 'number' && value <= 100 && value >= 0) {
|
||||
this.video.volume = value * 0.01;
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.getCurrentTime = function() {
|
||||
return this.video.currentTime;
|
||||
};
|
||||
|
||||
Player.prototype.playVideo = function() {
|
||||
this.video.play();
|
||||
};
|
||||
|
||||
Player.prototype.getPlayerState = function() {
|
||||
return this.playerState;
|
||||
};
|
||||
|
||||
Player.prototype.getVolume = function() {
|
||||
return this.video.volume;
|
||||
};
|
||||
|
||||
Player.prototype.getDuration = function() {
|
||||
if (isNaN(this.video.duration)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.video.duration;
|
||||
};
|
||||
|
||||
Player.prototype.setPlaybackRate = function(value) {
|
||||
var newSpeed;
|
||||
|
||||
newSpeed = parseFloat(value);
|
||||
|
||||
if (isFinite(newSpeed)) {
|
||||
if (this.video.playbackRate !== value) {
|
||||
this.video.playbackRate = value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.getAvailablePlaybackRates = function() {
|
||||
return [0.75, 1.0, 1.25, 1.5, 2.0];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
Player.prototype._getLogs = function() {
|
||||
return this.logs;
|
||||
};
|
||||
|
||||
Player.prototype.showErrorMessage = function(event, css) {
|
||||
var cssSelecter = css || '.video-player .video-error';
|
||||
this.el
|
||||
.find('.video-player div')
|
||||
.addClass('hidden')
|
||||
.end()
|
||||
.find(cssSelecter)
|
||||
.removeClass('is-hidden')
|
||||
.end()
|
||||
.addClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.attr({
|
||||
'aria-hidden': 'true',
|
||||
tabindex: -1
|
||||
});
|
||||
};
|
||||
|
||||
Player.prototype.onError = function() {
|
||||
if ($.isFunction(this.config.events.onError)) {
|
||||
this.config.events.onError();
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.destroy = function() {
|
||||
this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false);
|
||||
this.video.removeEventListener('play', this.onPlay, false);
|
||||
this.video.removeEventListener('playing', this.onPlaying, false);
|
||||
this.video.removeEventListener('pause', this.onPause, false);
|
||||
this.video.removeEventListener('ended', this.onEnded, false);
|
||||
this.el
|
||||
.find('.video-player div')
|
||||
.removeClass('is-hidden')
|
||||
.end()
|
||||
.find('.video-player .video-error')
|
||||
.addClass('is-hidden')
|
||||
.end()
|
||||
.removeClass('is-initialized')
|
||||
.find('.spinner')
|
||||
.attr({'aria-hidden': 'false'});
|
||||
this.videoEl.off('remove');
|
||||
this.videoEl.remove();
|
||||
};
|
||||
|
||||
Player.prototype.onReady = function() {
|
||||
this.config.events.onReady(null);
|
||||
this.showPlayButton();
|
||||
};
|
||||
|
||||
Player.prototype.onLoadedMetadata = function() {
|
||||
this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
if ($.isFunction(this.config.events.onReady)) {
|
||||
this.onReady();
|
||||
}
|
||||
};
|
||||
|
||||
Player.prototype.onPlay = function() {
|
||||
this.playerState = HTML5Video.PlayerState.BUFFERING;
|
||||
this.callStateChangeCallback();
|
||||
this.videoOverlayEl.addClass('is-hidden');
|
||||
};
|
||||
|
||||
Player.prototype.onPlaying = function() {
|
||||
this.playerState = HTML5Video.PlayerState.PLAYING;
|
||||
this.callStateChangeCallback();
|
||||
this.videoOverlayEl.addClass('is-hidden');
|
||||
};
|
||||
|
||||
Player.prototype.onPause = function() {
|
||||
this.playerState = HTML5Video.PlayerState.PAUSED;
|
||||
this.callStateChangeCallback();
|
||||
this.showPlayButton();
|
||||
};
|
||||
|
||||
Player.prototype.onEnded = function() {
|
||||
this.playerState = HTML5Video.PlayerState.ENDED;
|
||||
this.callStateChangeCallback();
|
||||
};
|
||||
|
||||
Player.prototype.init = function(el, config) {
|
||||
var isTouch = window.onTouchBasedDevice() || '',
|
||||
events = ['loadstart', 'progress', 'suspend', 'abort', 'error',
|
||||
'emptied', 'stalled', 'play', 'pause', 'loadedmetadata',
|
||||
'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough',
|
||||
'seeking', 'seeked', 'timeupdate', 'ended', 'ratechange',
|
||||
'durationchange', 'volumechange'
|
||||
],
|
||||
self = this,
|
||||
callback;
|
||||
|
||||
this.config = config;
|
||||
this.logs = [];
|
||||
this.el = $(el);
|
||||
|
||||
// Because of problems with creating video element via jquery
|
||||
// (http://bugs.jquery.com/ticket/9174) we create it using native JS.
|
||||
this.video = document.createElement('video');
|
||||
|
||||
// Get the jQuery object and set error event handlers
|
||||
this.videoEl = $(this.video);
|
||||
|
||||
// Video player overlay play button
|
||||
this.videoOverlayEl = this.el.find('.video-wrapper .btn-play');
|
||||
|
||||
// The player state is used by other parts of the VideoPlayer to
|
||||
// determine what the video is currently doing.
|
||||
this.playerState = HTML5Video.PlayerState.UNSTARTED;
|
||||
|
||||
_.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded');
|
||||
|
||||
// Attach a 'click' event on the <video> element. It will cause the
|
||||
// video to pause/play.
|
||||
callback = function() {
|
||||
var PlayerState = HTML5Video.PlayerState;
|
||||
|
||||
if (self.playerState === PlayerState.PLAYING) {
|
||||
self.playerState = PlayerState.PAUSED;
|
||||
self.pauseVideo();
|
||||
} else {
|
||||
self.playerState = PlayerState.PLAYING;
|
||||
self.playVideo();
|
||||
}
|
||||
};
|
||||
this.videoEl.on('click', callback);
|
||||
this.videoOverlayEl.on('click', callback);
|
||||
|
||||
this.debug = false;
|
||||
$.each(events, function(index, eventName) {
|
||||
self.video.addEventListener(eventName, function() {
|
||||
self.logs.push({
|
||||
'event name': eventName,
|
||||
state: self.playerState
|
||||
});
|
||||
|
||||
if (self.debug) {
|
||||
console.log(
|
||||
'event name:', eventName,
|
||||
'state:', self.playerState,
|
||||
'readyState:', self.video.readyState,
|
||||
'networkState:', self.video.networkState
|
||||
);
|
||||
}
|
||||
|
||||
el.trigger('html5:' + eventName, arguments);
|
||||
});
|
||||
});
|
||||
|
||||
// When the <video> tag has been processed by the browser, and it
|
||||
// is ready for playback, notify other parts of the VideoPlayer,
|
||||
// and initially pause the video.
|
||||
this.video.addEventListener('loadedmetadata', this.onLoadedMetadata, false);
|
||||
this.video.addEventListener('play', this.onPlay, false);
|
||||
this.video.addEventListener('playing', this.onPlaying, false);
|
||||
this.video.addEventListener('pause', this.onPause, false);
|
||||
this.video.addEventListener('ended', this.onEnded, false);
|
||||
|
||||
if (/iP(hone|od)/i.test(isTouch[0])) {
|
||||
this.videoEl.prop('controls', true);
|
||||
}
|
||||
|
||||
// Set video poster
|
||||
if (this.config.poster) {
|
||||
this.videoEl.prop('poster', this.config.poster);
|
||||
}
|
||||
|
||||
// Place the <video> element on the page.
|
||||
this.videoEl.appendTo(el.find('.video-player > div:first-child'));
|
||||
};
|
||||
|
||||
return Player;
|
||||
}());
|
||||
|
||||
// The YouTube API presents several constants which describe the player's
|
||||
// state at a given moment. HTML5Video API will copy these constants so
|
||||
// that code which uses both the YouTube API and this API doesn't have to
|
||||
// change.
|
||||
HTML5Video.PlayerState = {
|
||||
UNSTARTED: -1,
|
||||
ENDED: 0,
|
||||
PLAYING: 1,
|
||||
PAUSED: 2,
|
||||
BUFFERING: 3,
|
||||
CUED: 5
|
||||
};
|
||||
|
||||
// HTML5Video object - what this module exports.
|
||||
return HTML5Video;
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,70 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
// VideoTranscriptDownloadHandler module.
|
||||
define(
|
||||
'video/035_video_accessible_menu.js', ['underscore'],
|
||||
function(_) {
|
||||
/**
|
||||
* Video Download Transcript control module.
|
||||
* @exports video/035_video_accessible_menu.js
|
||||
* @constructor
|
||||
* @param {jquery Element} element
|
||||
* @param {Object} options
|
||||
*/
|
||||
var VideoTranscriptDownloadHandler = function(element, options) {
|
||||
if (!(this instanceof VideoTranscriptDownloadHandler)) {
|
||||
return new VideoTranscriptDownloadHandler(element, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'clickHandler');
|
||||
|
||||
this.container = element;
|
||||
this.options = options || {};
|
||||
|
||||
if (this.container.find('.wrapper-downloads .wrapper-download-transcripts')) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
VideoTranscriptDownloadHandler.prototype = {
|
||||
// Initializes the module.
|
||||
initialize: function() {
|
||||
this.value = this.options.storage.getItem('transcript_download_format');
|
||||
this.el = this.container.find('.list-download-transcripts');
|
||||
this.el.on('click', '.btn-link', this.clickHandler);
|
||||
},
|
||||
|
||||
// Event handler. We delay link clicks until the file type is set
|
||||
clickHandler: function(event) {
|
||||
var that = this,
|
||||
fileType,
|
||||
data,
|
||||
downloadUrl;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
fileType = $(event.target).data('value');
|
||||
data = {transcript_download_format: fileType};
|
||||
downloadUrl = $(event.target).attr('href');
|
||||
|
||||
$.ajax({
|
||||
url: this.options.saveStateUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
success: function() {
|
||||
that.options.storage.setItem('transcript_download_format', fileType);
|
||||
},
|
||||
complete: function() {
|
||||
document.location.href = downloadUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return VideoTranscriptDownloadHandler;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,92 +0,0 @@
|
||||
(function(define) {
|
||||
// eslint-disable-next-line lines-around-directive
|
||||
'use strict';
|
||||
|
||||
// VideoSocialSharingHandler module.
|
||||
define(
|
||||
'video/036_video_social_sharing.js', ['underscore'],
|
||||
function(_) {
|
||||
var VideoSocialSharingHandler;
|
||||
|
||||
/**
|
||||
* Video Social Sharing control module.
|
||||
* @exports video/036_video_social_sharing.js
|
||||
* @constructor
|
||||
* @param {jquery Element} element
|
||||
* @param {Object} options
|
||||
*/
|
||||
VideoSocialSharingHandler = function(element, options) {
|
||||
if (!(this instanceof VideoSocialSharingHandler)) {
|
||||
return new VideoSocialSharingHandler(element, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'clickHandler');
|
||||
_.bindAll(this, 'copyHandler');
|
||||
_.bindAll(this, 'hideHandler');
|
||||
_.bindAll(this, 'showHandler');
|
||||
|
||||
this.container = element;
|
||||
|
||||
if (this.container.find('.wrapper-downloads .wrapper-social-share')) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
VideoSocialSharingHandler.prototype = {
|
||||
|
||||
// Initializes the module.
|
||||
initialize: function() {
|
||||
this.el = this.container.find('.wrapper-social-share');
|
||||
this.baseVideoUrl = this.el.data('url');
|
||||
this.course_id = this.container.data('courseId');
|
||||
this.block_id = this.container.data('blockId');
|
||||
this.el.on('click', '.social-share-link', this.clickHandler);
|
||||
|
||||
this.closeBtn = this.el.find('.close-btn');
|
||||
this.toggleBtn = this.el.find('.social-toggle-btn');
|
||||
this.copyBtn = this.el.find('.public-video-copy-btn');
|
||||
this.shareContainer = this.el.find('.container-social-share');
|
||||
this.closeBtn.on('click', this.hideHandler);
|
||||
this.toggleBtn.on('click', this.showHandler);
|
||||
this.copyBtn.on('click', this.copyHandler);
|
||||
},
|
||||
|
||||
// Fire an analytics event on share button click.
|
||||
clickHandler: function(event) {
|
||||
var self = this;
|
||||
var source = $(event.currentTarget).data('source');
|
||||
self.sendAnalyticsEvent(source);
|
||||
},
|
||||
|
||||
hideHandler: function(event) {
|
||||
this.shareContainer.hide();
|
||||
this.toggleBtn.show();
|
||||
},
|
||||
|
||||
showHandler: function(event) {
|
||||
this.shareContainer.show();
|
||||
this.toggleBtn.hide();
|
||||
},
|
||||
|
||||
copyHandler: function(event) {
|
||||
navigator.clipboard.writeText(this.copyBtn.data('url'));
|
||||
},
|
||||
|
||||
// Send an analytics event for share button tracking.
|
||||
sendAnalyticsEvent: function(source) {
|
||||
window.analytics.track(
|
||||
'edx.social.video.share_button.clicked',
|
||||
{
|
||||
source: source,
|
||||
video_block_id: this.container.data('blockId'),
|
||||
course_id: this.container.data('courseId'),
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return VideoSocialSharingHandler;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,247 +0,0 @@
|
||||
(function(define) {
|
||||
// VideoTranscriptFeedbackHandler module.
|
||||
|
||||
'use strict';
|
||||
|
||||
define('video/037_video_caption.js', ['underscore'],
|
||||
function(_) {
|
||||
/**
|
||||
* @desc VideoTranscriptFeedbackHandler module exports a function.
|
||||
*
|
||||
* @type {function}
|
||||
* @access public
|
||||
*
|
||||
* @param {object} state - The object containing the state of the video
|
||||
* player. All other modules, their parameters, public variables, etc.
|
||||
* are available via this object.
|
||||
*
|
||||
* @this {object} The global window object.
|
||||
*
|
||||
*/
|
||||
|
||||
var VideoTranscriptFeedbackHandler = function(state) {
|
||||
if (!(this instanceof VideoTranscriptFeedbackHandler)) {
|
||||
return new VideoTranscriptFeedbackHandler(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'destroy', 'getFeedbackForCurrentTranscript', 'markAsPositiveFeedback', 'markAsNegativeFeedback', 'markAsEmptyFeedback',
|
||||
'selectThumbsUp', 'selectThumbsDown', 'unselectThumbsUp', 'unselectThumbsDown', 'thumbsUpClickHandler', 'thumbsDownClickHandler',
|
||||
'sendFeedbackForCurrentTranscript', 'onHideLanguageMenu', 'getCurrentLanguage', 'loadAndSetVisibility', 'showWidget', 'hideWidget'
|
||||
);
|
||||
|
||||
this.state = state;
|
||||
this.state.videoTranscriptFeedback = this;
|
||||
this.currentTranscriptLanguage = this.state.lang;
|
||||
this.transcriptLanguages = this.state.config.transcriptLanguages;
|
||||
|
||||
if (this.state.el.find('.wrapper-transcript-feedback').length) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
VideoTranscriptFeedbackHandler.prototype = {
|
||||
|
||||
destroy: function() {
|
||||
this.state.el.off(this.events);
|
||||
},
|
||||
|
||||
// Initializes the module.
|
||||
initialize: function() {
|
||||
this.el = this.state.el.find('.wrapper-transcript-feedback');
|
||||
|
||||
this.videoId = this.el.data('video-id');
|
||||
this.userId = this.el.data('user-id');
|
||||
this.aiTranslationsUrl = this.state.config.aiTranslationsUrl;
|
||||
|
||||
this.thumbsUpButton = this.el.find('.thumbs-up-btn');
|
||||
this.thumbsDownButton = this.el.find('.thumbs-down-btn');
|
||||
this.thumbsUpButton.on('click', this.thumbsUpClickHandler);
|
||||
this.thumbsDownButton.on('click', this.thumbsDownClickHandler);
|
||||
|
||||
this.events = {
|
||||
'language_menu:hide': this.onHideLanguageMenu,
|
||||
destroy: this.destroy
|
||||
};
|
||||
this.loadAndSetVisibility();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
this.state.el.on(this.events);
|
||||
},
|
||||
|
||||
getFeedbackForCurrentTranscript: function() {
|
||||
var self = this;
|
||||
var url = self.aiTranslationsUrl + '/transcript-feedback' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_id=' + self.videoId + '&user_id=' + self.userId;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
if (data && data.value === true) {
|
||||
self.markAsPositiveFeedback();
|
||||
self.currentFeedback = true;
|
||||
} else {
|
||||
if (data && data.value === false) {
|
||||
self.markAsNegativeFeedback();
|
||||
self.currentFeedback = false;
|
||||
} else {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
markAsPositiveFeedback: function() {
|
||||
this.selectThumbsUp();
|
||||
this.unselectThumbsDown();
|
||||
},
|
||||
|
||||
markAsNegativeFeedback: function() {
|
||||
this.selectThumbsDown();
|
||||
this.unselectThumbsUp();
|
||||
},
|
||||
|
||||
markAsEmptyFeedback: function() {
|
||||
this.unselectThumbsUp();
|
||||
this.unselectThumbsDown();
|
||||
},
|
||||
|
||||
selectThumbsUp: function() {
|
||||
var thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
|
||||
if (thumbsUpIcon[0].classList.contains('fa-thumbs-o-up')) {
|
||||
thumbsUpIcon[0].classList.remove("fa-thumbs-o-up");
|
||||
thumbsUpIcon[0].classList.add("fa-thumbs-up");
|
||||
}
|
||||
},
|
||||
|
||||
selectThumbsDown: function() {
|
||||
var thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
|
||||
if (thumbsDownIcon[0].classList.contains('fa-thumbs-o-down')) {
|
||||
thumbsDownIcon[0].classList.remove("fa-thumbs-o-down");
|
||||
thumbsDownIcon[0].classList.add("fa-thumbs-down");
|
||||
}
|
||||
},
|
||||
|
||||
unselectThumbsUp: function() {
|
||||
var thumbsUpIcon = this.thumbsUpButton.find('.thumbs-up-icon');
|
||||
if (thumbsUpIcon[0].classList.contains('fa-thumbs-up')) {
|
||||
thumbsUpIcon[0].classList.remove("fa-thumbs-up");
|
||||
thumbsUpIcon[0].classList.add("fa-thumbs-o-up");
|
||||
}
|
||||
},
|
||||
|
||||
unselectThumbsDown: function() {
|
||||
var thumbsDownIcon = this.thumbsDownButton.find('.thumbs-down-icon');
|
||||
if (thumbsDownIcon[0].classList.contains('fa-thumbs-down')) {
|
||||
thumbsDownIcon[0].classList.remove("fa-thumbs-down");
|
||||
thumbsDownIcon[0].classList.add("fa-thumbs-o-down");
|
||||
}
|
||||
},
|
||||
|
||||
thumbsUpClickHandler: function() {
|
||||
if (this.currentFeedback) {
|
||||
this.sendFeedbackForCurrentTranscript(null);
|
||||
} else {
|
||||
this.sendFeedbackForCurrentTranscript(true);
|
||||
}
|
||||
},
|
||||
|
||||
thumbsDownClickHandler: function() {
|
||||
if (this.currentFeedback === false) {
|
||||
this.sendFeedbackForCurrentTranscript(null);
|
||||
} else {
|
||||
this.sendFeedbackForCurrentTranscript(false);
|
||||
}
|
||||
},
|
||||
|
||||
sendFeedbackForCurrentTranscript: function(feedbackValue) {
|
||||
var self = this;
|
||||
var url = self.aiTranslationsUrl + '/transcript-feedback/';
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
transcript_language: self.currentTranscriptLanguage,
|
||||
video_id: self.videoId,
|
||||
user_id: self.userId,
|
||||
value: feedbackValue,
|
||||
},
|
||||
success: function(data) {
|
||||
if (data && data.value === true) {
|
||||
self.markAsPositiveFeedback();
|
||||
self.currentFeedback = true;
|
||||
} else {
|
||||
if (data && data.value === false) {
|
||||
self.markAsNegativeFeedback();
|
||||
self.currentFeedback = false;
|
||||
} else {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.markAsEmptyFeedback();
|
||||
self.currentFeedback = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onHideLanguageMenu: function() {
|
||||
var newLanguageSelected = this.getCurrentLanguage();
|
||||
if (this.currentTranscriptLanguage !== newLanguageSelected) {
|
||||
this.currentTranscriptLanguage = this.getCurrentLanguage();
|
||||
this.loadAndSetVisibility();
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentLanguage: function() {
|
||||
var language = this.state.lang;
|
||||
return language;
|
||||
},
|
||||
|
||||
loadAndSetVisibility: function() {
|
||||
var self = this;
|
||||
var url = self.aiTranslationsUrl + '/video-transcript' + '?transcript_language=' + self.currentTranscriptLanguage + '&video_id=' + self.videoId;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
async: false,
|
||||
success: function(data) {
|
||||
if (data && data.status === 'Completed') {
|
||||
self.showWidget();
|
||||
self.getFeedbackForCurrentTranscript();
|
||||
} else {
|
||||
self.hideWidget();
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
self.hideWidget();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showWidget: function() {
|
||||
this.el.show();
|
||||
},
|
||||
|
||||
hideWidget: function() {
|
||||
this.el.hide();
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return VideoTranscriptFeedbackHandler;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,914 +0,0 @@
|
||||
/* eslint-disable no-console, no-param-reassign */
|
||||
(function(requirejs, require, define) {
|
||||
// VideoPlayer module.
|
||||
define(
|
||||
'video/03_video_player.js',
|
||||
['video/02_html5_video.js', 'video/02_html5_hls_video.js', 'video/00_resizer.js', 'hls', 'underscore', '../time.js'],
|
||||
function(HTML5Video, HTML5HLSVideo, Resizer, HLS, _, Time) {
|
||||
var dfd = $.Deferred(),
|
||||
VideoPlayer = function(state) {
|
||||
state.videoPlayer = {};
|
||||
_makeFunctionsPublic(state);
|
||||
_initialize(state);
|
||||
// No callbacks to DOM events (click, mousemove, etc.).
|
||||
|
||||
return dfd.promise();
|
||||
},
|
||||
/* eslint-disable no-use-before-define */
|
||||
methodsDict = {
|
||||
destroy: destroy,
|
||||
duration: duration,
|
||||
handlePlaybackQualityChange: handlePlaybackQualityChange,
|
||||
|
||||
// Added for finer graded seeking control.
|
||||
// Please see:
|
||||
// https://developers.google.com/youtube/js_api_reference#Events
|
||||
isBuffering: isBuffering,
|
||||
// https://developers.google.com/youtube/js_api_reference#cueVideoById
|
||||
isCued: isCued,
|
||||
|
||||
isEnded: isEnded,
|
||||
isPlaying: isPlaying,
|
||||
isUnstarted: isUnstarted,
|
||||
onCaptionSeek: onSeek,
|
||||
onEnded: onEnded,
|
||||
onError: onError,
|
||||
onPause: onPause,
|
||||
onPlay: onPlay,
|
||||
runTimer: runTimer,
|
||||
stopTimer: stopTimer,
|
||||
onLoadMetadataHtml5: onLoadMetadataHtml5,
|
||||
onPlaybackQualityChange: onPlaybackQualityChange,
|
||||
onReady: onReady,
|
||||
onSlideSeek: onSeek,
|
||||
onSpeedChange: onSpeedChange,
|
||||
onAutoAdvanceChange: onAutoAdvanceChange,
|
||||
onStateChange: onStateChange,
|
||||
onUnstarted: onUnstarted,
|
||||
onVolumeChange: onVolumeChange,
|
||||
pause: pause,
|
||||
play: play,
|
||||
seekTo: seekTo,
|
||||
setPlaybackRate: setPlaybackRate,
|
||||
update: update,
|
||||
figureOutStartEndTime: figureOutStartEndTime,
|
||||
figureOutStartingTime: figureOutStartingTime,
|
||||
updatePlayTime: updatePlayTime
|
||||
};
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
VideoPlayer.prototype = methodsDict;
|
||||
|
||||
// VideoPlayer() function - what this module "exports".
|
||||
return VideoPlayer;
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called,
|
||||
// these functions will get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var debouncedF = _.debounce(
|
||||
function(params) {
|
||||
// Can't cancel a queued debounced function on destroy
|
||||
if (state.videoPlayer) {
|
||||
return onSeek.call(this, params);
|
||||
}
|
||||
}.bind(state),
|
||||
300
|
||||
);
|
||||
|
||||
state.bindTo(methodsDict, state.videoPlayer, state);
|
||||
|
||||
state.videoPlayer.onSlideSeek = debouncedF;
|
||||
state.videoPlayer.onCaptionSeek = debouncedF;
|
||||
}
|
||||
|
||||
// Updates players state, once metadata is loaded for html5 player.
|
||||
function onLoadMetadataHtml5() {
|
||||
var player = this.videoPlayer.player.videoEl,
|
||||
videoWidth = player[0].videoWidth || player.width(),
|
||||
videoHeight = player[0].videoHeight || player.height();
|
||||
|
||||
_resize(this, videoWidth, videoHeight);
|
||||
_updateVcrAndRegion(this);
|
||||
}
|
||||
|
||||
// function _initialize(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.
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
function _initialize(state) {
|
||||
var youTubeId,
|
||||
player,
|
||||
userAgent,
|
||||
commonPlayerConfig,
|
||||
eventToBeTriggered = 'loadedmetadata';
|
||||
|
||||
// The function is called just once to apply pre-defined configurations
|
||||
// by student before video starts playing. Waits until the video's
|
||||
// metadata is loaded, which normally happens just after the video
|
||||
// starts playing. Just after that configurations can be applied.
|
||||
state.videoPlayer.ready = _.once(function() {
|
||||
if (!state.isFlashMode() && state.speed != '1.0') {
|
||||
state.videoPlayer.setPlaybackRate(state.speed);
|
||||
}
|
||||
});
|
||||
|
||||
if (state.isYoutubeType()) {
|
||||
state.videoPlayer.PlayerState = YT.PlayerState;
|
||||
state.videoPlayer.PlayerState.UNSTARTED = -1;
|
||||
} else {
|
||||
state.videoPlayer.PlayerState = HTML5Video.PlayerState;
|
||||
}
|
||||
|
||||
state.videoPlayer.currentTime = 0;
|
||||
|
||||
state.videoPlayer.goToStartTime = true;
|
||||
state.videoPlayer.stopAtEndTime = true;
|
||||
|
||||
state.videoPlayer.playerVars = {
|
||||
controls: 0,
|
||||
wmode: 'transparent',
|
||||
rel: 0,
|
||||
showinfo: 0,
|
||||
enablejsapi: 1,
|
||||
modestbranding: 1,
|
||||
cc_load_policy: 0
|
||||
};
|
||||
|
||||
if (!state.isFlashMode()) {
|
||||
state.videoPlayer.playerVars.html5 = 1;
|
||||
}
|
||||
|
||||
// Detect the current browser for several browser-specific work-arounds.
|
||||
userAgent = navigator.userAgent.toLowerCase();
|
||||
state.browserIsFirefox = userAgent.indexOf('firefox') > -1;
|
||||
state.browserIsChrome = userAgent.indexOf('chrome') > -1;
|
||||
// Chrome includes both "Chrome" and "Safari" in the user agent.
|
||||
state.browserIsSafari = (userAgent.indexOf('safari') > -1
|
||||
&& !state.browserIsChrome);
|
||||
|
||||
// Browser can play HLS videos if either `Media Source Extensions`
|
||||
// feature is supported or browser is safari (native HLS support)
|
||||
state.canPlayHLS = state.HLSVideoSources.length > 0 && (HLS.isSupported() || state.browserIsSafari);
|
||||
state.HLSOnlySources = state.config.sources.length > 0
|
||||
&& state.config.sources.length === state.HLSVideoSources.length;
|
||||
|
||||
commonPlayerConfig = {
|
||||
playerVars: state.videoPlayer.playerVars,
|
||||
videoSources: state.config.sources,
|
||||
poster: state.config.poster,
|
||||
browserIsSafari: state.browserIsSafari,
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
};
|
||||
|
||||
if (state.videoType === 'html5') {
|
||||
if (state.canPlayHLS || state.HLSOnlySources) {
|
||||
state.videoPlayer.player = new HTML5HLSVideo.Player(
|
||||
state.el,
|
||||
_.extend({}, commonPlayerConfig, {
|
||||
state: state,
|
||||
onReadyHLS: function() { dfd.resolve(); },
|
||||
videoSources: state.HLSVideoSources,
|
||||
canPlayHLS: state.canPlayHLS,
|
||||
HLSOnlySources: state.HLSOnlySources
|
||||
})
|
||||
);
|
||||
// `loadedmetadata` event triggered too early on Safari due
|
||||
// to which correct video dimensions were not calculated
|
||||
eventToBeTriggered = state.browserIsSafari ? 'loadeddata' : eventToBeTriggered;
|
||||
} else {
|
||||
state.videoPlayer.player = new HTML5Video.Player(state.el, commonPlayerConfig);
|
||||
}
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
player = state.videoEl = state.videoPlayer.player.videoEl;
|
||||
player[0].addEventListener(eventToBeTriggered, state.videoPlayer.onLoadMetadataHtml5, false);
|
||||
player.on('remove', state.videoPlayer.destroy);
|
||||
} else {
|
||||
youTubeId = state.youtubeId();
|
||||
|
||||
state.videoPlayer.player = new YT.Player(state.id, {
|
||||
playerVars: state.videoPlayer.playerVars,
|
||||
videoId: youTubeId,
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
});
|
||||
|
||||
state.el.on('initialize', function() {
|
||||
// eslint-disable-next-line no-shadow, no-multi-assign
|
||||
var player = state.videoEl = state.el.find('iframe'),
|
||||
videoWidth = player.attr('width') || player.width(),
|
||||
videoHeight = player.attr('height') || player.height();
|
||||
|
||||
player.on('remove', state.videoPlayer.destroy);
|
||||
|
||||
_resize(state, videoWidth, videoHeight);
|
||||
_updateVcrAndRegion(state, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (state.isTouch) {
|
||||
dfd.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function _updateVcrAndRegion(state, isYoutube) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
var update = function(state) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
var duration = state.videoPlayer.duration(),
|
||||
time;
|
||||
|
||||
time = state.videoPlayer.figureOutStartingTime(duration);
|
||||
|
||||
// Update the VCR.
|
||||
state.trigger(
|
||||
'videoControl.updateVcrVidTime',
|
||||
{
|
||||
time: time,
|
||||
duration: duration
|
||||
}
|
||||
);
|
||||
|
||||
// Update the time slider.
|
||||
state.trigger(
|
||||
'videoProgressSlider.updateStartEndTimeRegion',
|
||||
{
|
||||
duration: duration
|
||||
}
|
||||
);
|
||||
state.trigger(
|
||||
'videoProgressSlider.updatePlayTime',
|
||||
{
|
||||
time: time,
|
||||
duration: duration
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// After initialization, update the VCR with total time.
|
||||
// At this point only the metadata duration is available (not
|
||||
// very precise), but it is better than having 00:00:00 for
|
||||
// total time.
|
||||
if (state.youtubeMetadataReceived || !isYoutube) {
|
||||
// Metadata was already received, and is available.
|
||||
update(state);
|
||||
} else {
|
||||
// We wait for metadata to arrive, before we request the update
|
||||
// of the VCR video time, and of the start-end time region.
|
||||
// Metadata contains duration of the video.
|
||||
state.el.on('metadata_received', function() {
|
||||
update(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _resize(state, videoWidth, videoHeight) {
|
||||
state.resizer = new Resizer({
|
||||
element: state.videoEl,
|
||||
elementRatio: videoWidth / videoHeight,
|
||||
container: state.container
|
||||
})
|
||||
.callbacks.once(function() {
|
||||
state.el.trigger('caption:resize');
|
||||
})
|
||||
.setMode('width');
|
||||
|
||||
// Update captions size when controls becomes visible on iPad or Android
|
||||
if (/iPad|Android/i.test(state.isTouch[0])) {
|
||||
state.el.on('controls:show', function() {
|
||||
state.el.trigger('caption:resize');
|
||||
});
|
||||
}
|
||||
|
||||
$(window).on('resize.video', _.debounce(function() {
|
||||
state.trigger('videoFullScreen.updateControlsHeight', null);
|
||||
state.el.trigger('caption:resize');
|
||||
state.resizer.align();
|
||||
}, 100));
|
||||
}
|
||||
|
||||
// function _restartUsingFlash(state)
|
||||
//
|
||||
// When we are about to play a YouTube video in HTML5 mode and discover
|
||||
// that we only have one available playback rate, we will switch to
|
||||
// Flash mode. In Flash speed switching is done by reloading videos
|
||||
// recorded at different frame rates.
|
||||
function _restartUsingFlash(state) {
|
||||
// Remove from the page current iFrame with HTML5 video.
|
||||
state.videoPlayer.player.destroy();
|
||||
|
||||
state.setPlayerMode('flash');
|
||||
|
||||
console.log('[Video info]: Changing YouTube player mode to "flash".');
|
||||
|
||||
// Removed configuration option that requests the HTML5 mode.
|
||||
delete state.videoPlayer.playerVars.html5;
|
||||
|
||||
// Request for the creation of a new Flash player
|
||||
state.videoPlayer.player = new YT.Player(state.id, {
|
||||
playerVars: state.videoPlayer.playerVars,
|
||||
videoId: state.youtubeId(),
|
||||
events: {
|
||||
onReady: state.videoPlayer.onReady,
|
||||
onStateChange: state.videoPlayer.onStateChange,
|
||||
onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange,
|
||||
onError: state.videoPlayer.onError
|
||||
}
|
||||
});
|
||||
|
||||
_updateVcrAndRegion(state, true);
|
||||
state.el.trigger('caption:fetch');
|
||||
state.resizer.setElement(state.el.find('iframe')).align();
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 destroy() {
|
||||
var player = this.videoPlayer.player;
|
||||
this.el.removeClass([
|
||||
'is-unstarted', 'is-playing', 'is-paused', 'is-buffered',
|
||||
'is-ended', 'is-cued'
|
||||
].join(' '));
|
||||
$(window).off('.video');
|
||||
this.el.trigger('destroy');
|
||||
this.el.off();
|
||||
this.videoPlayer.stopTimer();
|
||||
if (this.resizer && this.resizer.destroy) {
|
||||
this.resizer.destroy();
|
||||
}
|
||||
if (player && player.video) {
|
||||
player.video.removeEventListener('loadedmetadata', this.videoPlayer.onLoadMetadataHtml5, false);
|
||||
}
|
||||
if (player && _.isFunction(player.destroy)) {
|
||||
player.destroy();
|
||||
}
|
||||
if (this.canPlayHLS && player.hls) {
|
||||
player.hls.destroy();
|
||||
}
|
||||
delete this.videoPlayer;
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (this.videoPlayer.player.pauseVideo) {
|
||||
this.videoPlayer.player.pauseVideo();
|
||||
}
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (this.videoPlayer.player.playVideo) {
|
||||
if (this.videoPlayer.isEnded()) {
|
||||
// When the video will start playing again from the start, the
|
||||
// start-time and end-time will come back into effect.
|
||||
this.videoPlayer.goToStartTime = true;
|
||||
}
|
||||
|
||||
this.videoPlayer.player.playVideo();
|
||||
}
|
||||
}
|
||||
|
||||
// This function gets the video's current play position in time
|
||||
// (currentTime) and its duration.
|
||||
// It is called at a regular interval when the video is playing.
|
||||
function update(time) {
|
||||
this.videoPlayer.currentTime = time || this.videoPlayer.player.getCurrentTime();
|
||||
|
||||
if (isFinite(this.videoPlayer.currentTime)) {
|
||||
this.videoPlayer.updatePlayTime(this.videoPlayer.currentTime);
|
||||
|
||||
// We need to pause the video if current time is smaller (or equal)
|
||||
// than end-time. Also, we must make sure that this is only done
|
||||
// once per video playing from start to end.
|
||||
if (
|
||||
this.videoPlayer.endTime !== null
|
||||
&& this.videoPlayer.endTime <= this.videoPlayer.currentTime
|
||||
) {
|
||||
this.videoPlayer.pause();
|
||||
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: true
|
||||
});
|
||||
|
||||
this.el.trigger('stop');
|
||||
}
|
||||
this.el.trigger('timeupdate', [this.videoPlayer.currentTime]);
|
||||
}
|
||||
}
|
||||
|
||||
function setPlaybackRate(newSpeed) {
|
||||
this.videoPlayer.player.setPlaybackRate(newSpeed);
|
||||
}
|
||||
|
||||
function onSpeedChange(newSpeed) {
|
||||
var time = this.videoPlayer.currentTime;
|
||||
|
||||
if (this.isFlashMode()) {
|
||||
this.videoPlayer.currentTime = Time.convert(
|
||||
time,
|
||||
parseFloat(this.speed),
|
||||
newSpeed
|
||||
);
|
||||
}
|
||||
|
||||
newSpeed = parseFloat(newSpeed);
|
||||
this.setSpeed(newSpeed);
|
||||
this.videoPlayer.setPlaybackRate(newSpeed);
|
||||
}
|
||||
|
||||
function onAutoAdvanceChange(enabled) {
|
||||
this.setAutoAdvance(enabled);
|
||||
}
|
||||
|
||||
// Every 200 ms, if the video is playing, we call the function update, via
|
||||
// clearInterval. This interval is called updateInterval.
|
||||
// It is created on a onPlay event. Cleared on a onPause event.
|
||||
// Reinitialized on a onSeek event.
|
||||
function onSeek(params) {
|
||||
var time = params.time,
|
||||
type = params.type,
|
||||
oldTime = this.videoPlayer.currentTime;
|
||||
// After the user seeks, the video will start playing from
|
||||
// the sought point, and stop playing at the end.
|
||||
this.videoPlayer.goToStartTime = false;
|
||||
|
||||
this.videoPlayer.seekTo(time);
|
||||
|
||||
this.el.trigger('seek', [time, oldTime, type]);
|
||||
}
|
||||
|
||||
function seekTo(time) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
var duration = this.videoPlayer.duration();
|
||||
|
||||
if ((typeof time !== 'number') || (time > duration) || (time < 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el.off('play.seek');
|
||||
|
||||
if (this.videoPlayer.isPlaying()) {
|
||||
this.videoPlayer.stopTimer();
|
||||
}
|
||||
var isUnplayed = this.videoPlayer.isUnstarted()
|
||||
|| this.videoPlayer.isCued();
|
||||
|
||||
// Use `cueVideoById` method for youtube video that is not played before.
|
||||
if (isUnplayed && this.isYoutubeType()) {
|
||||
this.videoPlayer.player.cueVideoById(this.youtubeId(), time);
|
||||
} else {
|
||||
// Youtube video cannot be rewinded during bufferization, so wait to
|
||||
// finish bufferization and then rewind the video.
|
||||
if (this.isYoutubeType() && this.videoPlayer.isBuffering()) {
|
||||
this.el.on('play.seek', function() {
|
||||
this.videoPlayer.player.seekTo(time, true);
|
||||
}.bind(this));
|
||||
} else {
|
||||
// Otherwise, just seek the video
|
||||
this.videoPlayer.player.seekTo(time, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.videoPlayer.updatePlayTime(time, true);
|
||||
|
||||
// the timer is stopped above; restart it.
|
||||
if (this.videoPlayer.isPlaying()) {
|
||||
this.videoPlayer.runTimer();
|
||||
}
|
||||
// Update the the current time when user seek. (YoutubePlayer)
|
||||
this.videoPlayer.currentTime = time;
|
||||
}
|
||||
|
||||
function runTimer() {
|
||||
if (!this.videoPlayer.updateInterval) {
|
||||
this.videoPlayer.updateInterval = window.setInterval(
|
||||
this.videoPlayer.update, 200
|
||||
);
|
||||
|
||||
this.videoPlayer.update();
|
||||
}
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
window.clearInterval(this.videoPlayer.updateInterval);
|
||||
delete this.videoPlayer.updateInterval;
|
||||
}
|
||||
|
||||
function onEnded() {
|
||||
var time = this.videoPlayer.duration();
|
||||
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: true
|
||||
});
|
||||
|
||||
if (this.videoPlayer.skipOnEndedStartEndReset) {
|
||||
this.videoPlayer.skipOnEndedStartEndReset = undefined;
|
||||
}
|
||||
// Sometimes `onEnded` events fires when `currentTime` not equal
|
||||
// `duration`. In this case, slider doesn't reach the end point of
|
||||
// timeline.
|
||||
this.videoPlayer.updatePlayTime(time);
|
||||
|
||||
// Emit 'pause_video' event when a video ends if Player is of Youtube
|
||||
if (this.isYoutubeType()) {
|
||||
this.el.trigger('pause', arguments);
|
||||
}
|
||||
this.el.trigger('ended', arguments);
|
||||
}
|
||||
|
||||
function onPause() {
|
||||
this.videoPlayer.stopTimer();
|
||||
this.el.trigger('pause', arguments);
|
||||
}
|
||||
|
||||
function onPlay() {
|
||||
this.videoPlayer.runTimer();
|
||||
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
|
||||
end: false
|
||||
});
|
||||
this.videoPlayer.ready();
|
||||
this.el.trigger('play', arguments);
|
||||
}
|
||||
|
||||
function onUnstarted() { }
|
||||
|
||||
function handlePlaybackQualityChange(value) {
|
||||
this.videoPlayer.player.setPlaybackQuality(value);
|
||||
}
|
||||
|
||||
function onPlaybackQualityChange() {
|
||||
var quality;
|
||||
|
||||
quality = this.videoPlayer.player.getPlaybackQuality();
|
||||
|
||||
this.trigger('videoQualityControl.onQualityChange', quality);
|
||||
this.el.trigger('qualitychange', arguments);
|
||||
}
|
||||
|
||||
function onReady() {
|
||||
var _this = this,
|
||||
availablePlaybackRates, baseSpeedSubs,
|
||||
player, videoWidth, videoHeight;
|
||||
|
||||
dfd.resolve();
|
||||
|
||||
this.el.on('speedchange', function(event, speed) {
|
||||
_this.videoPlayer.onSpeedChange(speed);
|
||||
});
|
||||
|
||||
this.el.on('autoadvancechange', function(event, enabled) {
|
||||
_this.videoPlayer.onAutoAdvanceChange(enabled);
|
||||
});
|
||||
|
||||
this.el.on('volumechange volumechange:silent', function(event, volume) {
|
||||
_this.videoPlayer.onVolumeChange(volume);
|
||||
});
|
||||
|
||||
availablePlaybackRates = this.videoPlayer.player
|
||||
.getAvailablePlaybackRates();
|
||||
|
||||
// Because of problems with muting sound outside of range 0.25 and
|
||||
// 5.0, we should filter our available playback rates.
|
||||
// Issues:
|
||||
// https://code.google.com/p/chromium/issues/detail?id=264341
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=840745
|
||||
// https://developer.mozilla.org/en-US/docs/DOM/HTMLMediaElement
|
||||
|
||||
availablePlaybackRates = _.filter(
|
||||
availablePlaybackRates,
|
||||
function(item) {
|
||||
var speed = Number(item);
|
||||
return speed > 0.25 && speed <= 5;
|
||||
}
|
||||
);
|
||||
|
||||
// Because of a recent change in the YouTube API (not documented), sometimes
|
||||
// HTML5 mode loads after Flash mode has been loaded. In this case we have
|
||||
// multiple speeds available but the variable `this.currentPlayerMode` is
|
||||
// set to "flash". This is impossible because in Flash mode we can have
|
||||
// only one speed available. Therefore we must execute the following code
|
||||
// block if we have multiple speeds or if `this.currentPlayerMode` is set to
|
||||
// "html5". If any of the two conditions are true, we then set the variable
|
||||
// `this.currentPlayerMode` to "html5".
|
||||
//
|
||||
// For more information, please see the PR that introduced this change:
|
||||
// https://github.com/openedx/edx-platform/pull/2841
|
||||
if (
|
||||
(this.isHtml5Mode() || availablePlaybackRates.length > 1)
|
||||
&& this.isYoutubeType()
|
||||
) {
|
||||
if (availablePlaybackRates.length === 1 && !this.isTouch) {
|
||||
// This condition is needed in cases when Firefox version is
|
||||
// less than 20. In those versions HTML5 playback could only
|
||||
// happen at 1 speed (no speed changing). Therefore, in this
|
||||
// case, we need to switch back to Flash.
|
||||
//
|
||||
// This might also happen in other browsers, therefore when we
|
||||
// have 1 speed available, we fall back to Flash.
|
||||
|
||||
_restartUsingFlash(this);
|
||||
return false;
|
||||
} else if (availablePlaybackRates.length > 1) {
|
||||
this.setPlayerMode('html5');
|
||||
|
||||
// We need to synchronize available frame rates with the ones
|
||||
// that the user specified.
|
||||
|
||||
baseSpeedSubs = this.videos['1.0'];
|
||||
// this.videos is a dictionary containing various frame rates
|
||||
// and their associated subs.
|
||||
|
||||
// First clear the dictionary.
|
||||
$.each(this.videos, function(index, value) {
|
||||
delete _this.videos[index];
|
||||
});
|
||||
this.speeds = [];
|
||||
// Recreate it with the supplied frame rates.
|
||||
$.each(availablePlaybackRates, function(index, value) {
|
||||
var key = value.toFixed(2).replace(/\.00$/, '.0');
|
||||
|
||||
_this.videos[key] = baseSpeedSubs;
|
||||
_this.speeds.push(key);
|
||||
});
|
||||
|
||||
this.setSpeed(this.speed);
|
||||
this.el.trigger('speed:render', [this.speeds, this.speed]);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isFlashMode()) {
|
||||
this.setSpeed(this.speed);
|
||||
this.el.trigger('speed:set', [this.speed]);
|
||||
}
|
||||
|
||||
if (this.isHtml5Mode()) {
|
||||
this.videoPlayer.player.setPlaybackRate(this.speed);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
var duration = this.videoPlayer.duration(),
|
||||
time = this.videoPlayer.figureOutStartingTime(duration);
|
||||
|
||||
// this.duration will be set initially only if duration is coming from edx-val
|
||||
this.duration = this.duration || duration;
|
||||
|
||||
if (time > 0 && this.videoPlayer.goToStartTime) {
|
||||
this.videoPlayer.seekTo(time);
|
||||
}
|
||||
|
||||
this.el.trigger('ready', arguments);
|
||||
|
||||
if (this.config.autoplay) {
|
||||
this.videoPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
function onStateChange(event) {
|
||||
this.el.removeClass([
|
||||
'is-unstarted', 'is-playing', 'is-paused', 'is-buffered',
|
||||
'is-ended', 'is-cued'
|
||||
].join(' '));
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.data) {
|
||||
case this.videoPlayer.PlayerState.UNSTARTED:
|
||||
this.el.addClass('is-unstarted');
|
||||
this.videoPlayer.onUnstarted();
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.PLAYING:
|
||||
this.el.addClass('is-playing');
|
||||
this.videoPlayer.onPlay();
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.PAUSED:
|
||||
this.el.addClass('is-paused');
|
||||
this.videoPlayer.onPause();
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.BUFFERING:
|
||||
this.el.addClass('is-buffered');
|
||||
this.el.trigger('buffering');
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.ENDED:
|
||||
this.el.addClass('is-ended');
|
||||
this.videoPlayer.onEnded();
|
||||
break;
|
||||
case this.videoPlayer.PlayerState.CUED:
|
||||
this.el.addClass('is-cued');
|
||||
if (this.isFlashMode()) {
|
||||
this.videoPlayer.play();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function onError(code) {
|
||||
this.el.trigger('error', [code]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
function figureOutStartEndTime(duration) {
|
||||
var videoPlayer = this.videoPlayer;
|
||||
|
||||
videoPlayer.startTime = this.config.startTime;
|
||||
if (videoPlayer.startTime >= duration) {
|
||||
videoPlayer.startTime = 0;
|
||||
} else if (this.isFlashMode()) {
|
||||
videoPlayer.startTime /= Number(this.speed);
|
||||
}
|
||||
|
||||
videoPlayer.endTime = this.config.endTime;
|
||||
if (
|
||||
videoPlayer.endTime <= videoPlayer.startTime
|
||||
|| videoPlayer.endTime >= duration
|
||||
) {
|
||||
videoPlayer.endTime = null;
|
||||
} else if (this.isFlashMode()) {
|
||||
videoPlayer.endTime /= Number(this.speed);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
function figureOutStartingTime(duration) {
|
||||
var savedVideoPosition = this.config.savedVideoPosition,
|
||||
|
||||
// Default starting time is 0. This is the case when
|
||||
// there is not start-time, no previously saved position,
|
||||
// or one (or both) of those values is incorrect.
|
||||
time = 0,
|
||||
|
||||
startTime, endTime;
|
||||
|
||||
this.videoPlayer.figureOutStartEndTime(duration);
|
||||
|
||||
startTime = this.videoPlayer.startTime;
|
||||
endTime = this.videoPlayer.endTime;
|
||||
|
||||
if (startTime > 0) {
|
||||
if (
|
||||
startTime < savedVideoPosition
|
||||
&& (endTime > savedVideoPosition || endTime === null)
|
||||
|
||||
// We do not want to jump to the end of the video.
|
||||
// We subtract 1 from the duration for a 1 second
|
||||
// safety net.
|
||||
&& savedVideoPosition < duration - 1
|
||||
) {
|
||||
time = savedVideoPosition;
|
||||
} else {
|
||||
time = startTime;
|
||||
}
|
||||
} else if (
|
||||
savedVideoPosition > 0
|
||||
&& (endTime > savedVideoPosition || endTime === null)
|
||||
|
||||
// We do not want to jump to the end of the video.
|
||||
// We subtract 1 from the duration for a 1 second
|
||||
// safety net.
|
||||
&& savedVideoPosition < duration - 1
|
||||
) {
|
||||
time = savedVideoPosition;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
function updatePlayTime(time, skip_seek) {
|
||||
var videoPlayer = this.videoPlayer,
|
||||
endTime = this.videoPlayer.duration(),
|
||||
youTubeId;
|
||||
|
||||
if (this.config.endTime) {
|
||||
endTime = Math.min(this.config.endTime, endTime);
|
||||
}
|
||||
|
||||
this.trigger(
|
||||
'videoProgressSlider.updatePlayTime',
|
||||
{
|
||||
time: time,
|
||||
duration: endTime
|
||||
}
|
||||
);
|
||||
|
||||
this.trigger(
|
||||
'videoControl.updateVcrVidTime',
|
||||
{
|
||||
time: time,
|
||||
duration: endTime
|
||||
}
|
||||
);
|
||||
|
||||
this.el.trigger('caption:update', [time]);
|
||||
}
|
||||
|
||||
function isEnded() {
|
||||
var playerState = this.videoPlayer.player.getPlayerState(),
|
||||
ENDED = this.videoPlayer.PlayerState.ENDED;
|
||||
|
||||
return playerState === ENDED;
|
||||
}
|
||||
|
||||
function isPlaying() {
|
||||
var playerState = this.videoPlayer.player.getPlayerState();
|
||||
|
||||
return playerState === this.videoPlayer.PlayerState.PLAYING;
|
||||
}
|
||||
|
||||
function isBuffering() {
|
||||
var playerState = this.videoPlayer.player.getPlayerState();
|
||||
|
||||
return playerState === this.videoPlayer.PlayerState.BUFFERING;
|
||||
}
|
||||
|
||||
function isCued() {
|
||||
var playerState = this.videoPlayer.player.getPlayerState();
|
||||
|
||||
return playerState === this.videoPlayer.PlayerState.CUED;
|
||||
}
|
||||
|
||||
function isUnstarted() {
|
||||
var playerState = this.videoPlayer.player.getPlayerState();
|
||||
|
||||
return playerState === this.videoPlayer.PlayerState.UNSTARTED;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the duration of the video in seconds.
|
||||
*
|
||||
* First, try to use the native player API call to get the duration.
|
||||
* If the value returned by the native function is not valid, resort to
|
||||
* the value stored in the metadata for the video. Note that the metadata
|
||||
* is available only for YouTube videos.
|
||||
*
|
||||
* IMPORTANT! It has been observed that sometimes, after initial playback
|
||||
* of the video, when operations "pause" and "play" are performed (in that
|
||||
* sequence), the function will start returning a slightly different value.
|
||||
*
|
||||
* For example: While playing for the first time, the function returns 31.
|
||||
* After pausing the video and then resuming once more, the function will
|
||||
* start returning 31.950656.
|
||||
*
|
||||
* This instability is internal to the player API (or browser internals).
|
||||
*/
|
||||
function duration() {
|
||||
var dur;
|
||||
|
||||
// Sometimes the YouTube API doesn't finish instantiating all of it's
|
||||
// methods, but the execution point arrives here.
|
||||
//
|
||||
// This happens when you have start-time and end-time set, and click "Edit"
|
||||
// in Studio, and then "Save". The Video editor dialog closes, the
|
||||
// video reloads, but the start-end range is not visible.
|
||||
if (this.videoPlayer.player.getDuration) {
|
||||
dur = this.videoPlayer.player.getDuration();
|
||||
}
|
||||
|
||||
// For YouTube videos, before the video starts playing, the API
|
||||
// function player.getDuration() will return 0. This means that the VCR
|
||||
// will show total time as 0 when the page just loads (before the user
|
||||
// clicks the Play button).
|
||||
//
|
||||
// We can do betterin a case when dur is 0 (or less than 0). We can ask
|
||||
// the getDuration() function for total time, which will query the
|
||||
// metadata for a duration.
|
||||
//
|
||||
// Be careful! Often the metadata duration is not very precise. It
|
||||
// might differ by one or two seconds against the actual time as will
|
||||
// be reported later on by the player.getDuration() API function.
|
||||
if (!isFinite(dur) || dur <= 0) {
|
||||
if (this.isYoutubeType()) {
|
||||
dur = this.getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
// Just in case the metadata is garbled, or something went wrong, we
|
||||
// have a final check.
|
||||
if (!isFinite(dur) || dur <= 0) {
|
||||
dur = 0;
|
||||
}
|
||||
|
||||
return Math.floor(dur);
|
||||
}
|
||||
|
||||
function onVolumeChange(volume) {
|
||||
this.videoPlayer.player.setVolume(volume);
|
||||
}
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,169 +0,0 @@
|
||||
(function(requirejs, require, define) {
|
||||
'use strict';
|
||||
|
||||
// VideoControl module.
|
||||
define(
|
||||
'video/04_video_control.js',
|
||||
['time.js'],
|
||||
function(Time) {
|
||||
// VideoControl() function - what this module "exports".
|
||||
return function(state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
state.videoControl = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called, these functions will
|
||||
// get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
destroy: destroy,
|
||||
hideControls: hideControls,
|
||||
show: show,
|
||||
showControls: showControls,
|
||||
focusFirst: focusFirst,
|
||||
updateVcrVidTime: updateVcrVidTime
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoControl, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
this.el.off({
|
||||
mousemove: this.videoControl.showControls,
|
||||
keydown: this.videoControl.showControls,
|
||||
destroy: this.videoControl.destroy,
|
||||
initialize: this.videoControl.focusFirst
|
||||
});
|
||||
|
||||
this.el.off('controls:show');
|
||||
if (this.controlHideTimeout) {
|
||||
clearTimeout(this.controlHideTimeout);
|
||||
}
|
||||
delete this.videoControl;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
state.videoControl.el = state.el.find('.video-controls');
|
||||
state.videoControl.vidTimeEl = state.videoControl.el.find('.vidtime');
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
state.videoControl.fadeOutTimeout = state.config.fadeOutTimeout;
|
||||
|
||||
state.videoControl.el.addClass('html5');
|
||||
state.controlHideTimeout = setTimeout(state.videoControl.hideControls, state.videoControl.fadeOutTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
// function _bindHandlers(state)
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
function _bindHandlers(state) {
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
state.el.on({
|
||||
mousemove: state.videoControl.showControls,
|
||||
keydown: state.videoControl.showControls
|
||||
});
|
||||
}
|
||||
|
||||
if (state.config.focusFirstControl) {
|
||||
state.el.on('initialize', state.videoControl.focusFirst);
|
||||
}
|
||||
state.el.on('destroy', state.videoControl.destroy);
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 focusFirst() {
|
||||
this.videoControl.el.find('.vcr a, .vcr button').first().focus();
|
||||
}
|
||||
|
||||
function show() {
|
||||
this.videoControl.el.removeClass('is-hidden');
|
||||
this.el.trigger('controls:show', arguments);
|
||||
}
|
||||
|
||||
function showControls(event) {
|
||||
if (!this.controlShowLock) {
|
||||
if (!this.captionsHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controlShowLock = true;
|
||||
|
||||
if (this.controlState === 'invisible') {
|
||||
this.videoControl.el.show();
|
||||
this.controlState = 'visible';
|
||||
} else if (this.controlState === 'hiding') {
|
||||
this.videoControl.el.stop(true, false).css('opacity', 1).show();
|
||||
this.controlState = 'visible';
|
||||
} else if (this.controlState === 'visible') {
|
||||
clearTimeout(this.controlHideTimeout);
|
||||
}
|
||||
|
||||
this.controlHideTimeout = setTimeout(this.videoControl.hideControls, this.videoControl.fadeOutTimeout);
|
||||
this.controlShowLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
var _this = this;
|
||||
|
||||
this.controlHideTimeout = null;
|
||||
|
||||
if (!this.captionsHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controlState = 'hiding';
|
||||
this.videoControl.el.fadeOut(this.videoControl.fadeOutTimeout, function() {
|
||||
_this.controlState = 'invisible';
|
||||
// If the focus was on the video control or the volume control,
|
||||
// then we must make sure to close these dialogs. Otherwise, after
|
||||
// next autofocus, these dialogs will be open, but the focus will
|
||||
// not be on them.
|
||||
_this.videoVolumeControl.el.removeClass('open');
|
||||
_this.videoSpeedControl.el.removeClass('open');
|
||||
|
||||
_this.focusGrabber.enableFocusGrabber();
|
||||
});
|
||||
}
|
||||
|
||||
function updateVcrVidTime(params) {
|
||||
var endTime = (this.config.endTime !== null) ? this.config.endTime : params.duration,
|
||||
startTime, currentTime;
|
||||
// in case endTime is accidentally specified as being greater than the video
|
||||
endTime = Math.min(endTime, params.duration);
|
||||
startTime = this.config.startTime > 0 ? this.config.startTime : 0;
|
||||
// if it's a subsection of video, use the clip duration as endTime
|
||||
if (startTime && this.config.endTime) {
|
||||
endTime = this.config.endTime - startTime;
|
||||
}
|
||||
currentTime = startTime ? params.time - startTime : params.time;
|
||||
this.videoControl.vidTimeEl.text(Time.format(currentTime) + ' / ' + Time.format(endTime));
|
||||
}
|
||||
}
|
||||
);
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,313 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/04_video_full_screen.js', ['edx-ui-toolkit/js/utils/html-utils'], function(HtmlUtils) {
|
||||
var template = [
|
||||
'<button class="control add-fullscreen" aria-disabled="false" title="',
|
||||
gettext('Fill browser'),
|
||||
'" aria-label="',
|
||||
gettext('Fill browser'),
|
||||
'">',
|
||||
'<span class="icon fa fa-arrows-alt" aria-hidden="true"></span>',
|
||||
'</button>'
|
||||
].join('');
|
||||
|
||||
// The following properties and functions enable cross-browser use of the
|
||||
// the Fullscreen Web API.
|
||||
//
|
||||
// function getVendorPrefixed(property)
|
||||
// function getFullscreenElement()
|
||||
// function exitFullscreen()
|
||||
// function requestFullscreen(element, options)
|
||||
//
|
||||
// For more information about the Fullscreen Web API see MDN:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
|
||||
var prefixedFullscreenProperties = (function() {
|
||||
if ('fullscreenEnabled' in document) {
|
||||
return {
|
||||
fullscreenElement: 'fullscreenElement',
|
||||
fullscreenEnabled: 'fullscreenEnabled',
|
||||
requestFullscreen: 'requestFullscreen',
|
||||
exitFullscreen: 'exitFullscreen',
|
||||
fullscreenchange: 'fullscreenchange',
|
||||
fullscreenerror: 'fullscreenerror'
|
||||
};
|
||||
}
|
||||
if ('webkitFullscreenEnabled' in document) {
|
||||
return {
|
||||
fullscreenElement: 'webkitFullscreenElement',
|
||||
fullscreenEnabled: 'webkitFullscreenEnabled',
|
||||
requestFullscreen: 'webkitRequestFullscreen',
|
||||
exitFullscreen: 'webkitExitFullscreen',
|
||||
fullscreenchange: 'webkitfullscreenchange',
|
||||
fullscreenerror: 'webkitfullscreenerror'
|
||||
};
|
||||
}
|
||||
if ('mozFullScreenEnabled' in document) {
|
||||
return {
|
||||
fullscreenElement: 'mozFullScreenElement',
|
||||
fullscreenEnabled: 'mozFullScreenEnabled',
|
||||
requestFullscreen: 'mozRequestFullScreen',
|
||||
exitFullscreen: 'mozCancelFullScreen',
|
||||
fullscreenchange: 'mozfullscreenchange',
|
||||
fullscreenerror: 'mozfullscreenerror'
|
||||
};
|
||||
}
|
||||
if ('msFullscreenEnabled' in document) {
|
||||
return {
|
||||
fullscreenElement: 'msFullscreenElement',
|
||||
fullscreenEnabled: 'msFullscreenEnabled',
|
||||
requestFullscreen: 'msRequestFullscreen',
|
||||
exitFullscreen: 'msExitFullscreen',
|
||||
fullscreenchange: 'MSFullscreenChange',
|
||||
fullscreenerror: 'MSFullscreenError'
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}());
|
||||
|
||||
function getVendorPrefixed(property) {
|
||||
return prefixedFullscreenProperties[property];
|
||||
}
|
||||
|
||||
function getFullscreenElement() {
|
||||
return document[getVendorPrefixed('fullscreenElement')];
|
||||
}
|
||||
|
||||
function exitFullscreen() {
|
||||
if (document[getVendorPrefixed('exitFullscreen')]) {
|
||||
return document[getVendorPrefixed('exitFullscreen')]();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function requestFullscreen(element, options) {
|
||||
if (element[getVendorPrefixed('requestFullscreen')]) {
|
||||
return element[getVendorPrefixed('requestFullscreen')](options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
function destroy() {
|
||||
$(document).off('keyup', this.videoFullScreen.exitHandler);
|
||||
this.videoFullScreen.fullScreenEl.remove();
|
||||
this.el.off({
|
||||
destroy: this.videoFullScreen.destroy
|
||||
});
|
||||
document.removeEventListener(
|
||||
getVendorPrefixed('fullscreenchange'),
|
||||
this.videoFullScreen.handleFullscreenChange
|
||||
);
|
||||
if (this.isFullScreen) {
|
||||
this.videoFullScreen.exit();
|
||||
}
|
||||
delete this.videoFullScreen;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
state.videoFullScreen.fullScreenEl = $(template);
|
||||
state.videoFullScreen.sliderEl = state.el.find('.slider');
|
||||
state.videoFullScreen.fullScreenState = false;
|
||||
HtmlUtils.append(state.el.find('.secondary-controls'), HtmlUtils.HTML(state.videoFullScreen.fullScreenEl));
|
||||
state.videoFullScreen.updateControlsHeight();
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
|
||||
// function bindHandlers(state)
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
function bindHandlers(state) {
|
||||
state.videoFullScreen.fullScreenEl.on('click', state.videoFullScreen.toggleHandler);
|
||||
state.el.on({
|
||||
destroy: state.videoFullScreen.destroy
|
||||
});
|
||||
$(document).on('keyup', state.videoFullScreen.exitHandler);
|
||||
document.addEventListener(
|
||||
getVendorPrefixed('fullscreenchange'),
|
||||
state.videoFullScreen.handleFullscreenChange
|
||||
);
|
||||
}
|
||||
|
||||
function getControlsHeight(controls, slider) {
|
||||
return controls.height() + 0.5 * slider.height();
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 handleFullscreenChange() {
|
||||
if (getFullscreenElement() !== this.el[0] && this.isFullScreen) {
|
||||
// The video was fullscreen so this event must relate to this video
|
||||
this.videoFullScreen.handleExit();
|
||||
}
|
||||
}
|
||||
|
||||
function updateControlsHeight() {
|
||||
var controls = this.el.find('.video-controls'),
|
||||
slider = this.videoFullScreen.sliderEl;
|
||||
this.videoFullScreen.height = getControlsHeight(controls, slider);
|
||||
return this.videoFullScreen.height;
|
||||
}
|
||||
|
||||
function notifyParent(fullscreenOpen) {
|
||||
if (window !== window.parent) {
|
||||
// This is used by the Learning MFE to know about changing fullscreen mode.
|
||||
// The MFE is then able to respond appropriately and scroll window to the previous position.
|
||||
window.parent.postMessage({
|
||||
type: 'plugin.videoFullScreen',
|
||||
payload: {
|
||||
open: fullscreenOpen
|
||||
}
|
||||
}, document.referrer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler to toggle fullscreen mode.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
function toggleHandler(event) {
|
||||
event.preventDefault();
|
||||
this.videoCommands.execute('toggleFullScreen');
|
||||
}
|
||||
|
||||
function handleExit() {
|
||||
var fullScreenClassNameEl = this.el.add(document.documentElement);
|
||||
var closedCaptionsEl = this.el.find('.closed-captions');
|
||||
|
||||
if (this.isFullScreen === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
this.videoFullScreen.fullScreenState = this.isFullScreen = false;
|
||||
fullScreenClassNameEl.removeClass('video-fullscreen');
|
||||
$(window).scrollTop(this.scrollPos);
|
||||
this.videoFullScreen.fullScreenEl
|
||||
.attr({title: gettext('Fill browser'), 'aria-label': gettext('Fill browser')})
|
||||
.find('.icon')
|
||||
.removeClass('fa-compress')
|
||||
.addClass('fa-arrows-alt');
|
||||
|
||||
$(closedCaptionsEl).css({top: '70%', left: '5%'});
|
||||
if (this.resizer) {
|
||||
this.resizer.delta.reset().setMode('width');
|
||||
}
|
||||
this.el.trigger('fullscreen', [this.isFullScreen]);
|
||||
|
||||
this.videoFullScreen.notifyParent(false);
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
var fullScreenClassNameEl = this.el.add(document.documentElement);
|
||||
var closedCaptionsEl = this.el.find('.closed-captions');
|
||||
|
||||
if (this.isFullScreen === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoFullScreen.notifyParent(true);
|
||||
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
this.videoFullScreen.fullScreenState = this.isFullScreen = true;
|
||||
fullScreenClassNameEl.addClass('video-fullscreen');
|
||||
this.videoFullScreen.fullScreenEl
|
||||
.attr({title: gettext('Exit full browser'), 'aria-label': gettext('Exit full browser')})
|
||||
.find('.icon')
|
||||
.removeClass('fa-arrows-alt')
|
||||
.addClass('fa-compress');
|
||||
|
||||
$(closedCaptionsEl).css({top: '70%', left: '5%'});
|
||||
if (this.resizer) {
|
||||
this.resizer.delta.substract(this.videoFullScreen.updateControlsHeight(), 'height').setMode('both');
|
||||
}
|
||||
this.el.trigger('fullscreen', [this.isFullScreen]);
|
||||
}
|
||||
|
||||
function exit() {
|
||||
if (getFullscreenElement() === this.el[0]) {
|
||||
exitFullscreen();
|
||||
} else {
|
||||
// Else some other element is fullscreen or the fullscreen api does not exist.
|
||||
this.videoFullScreen.handleExit();
|
||||
}
|
||||
}
|
||||
|
||||
function enter() {
|
||||
this.scrollPos = $(window).scrollTop();
|
||||
this.videoFullScreen.handleEnter();
|
||||
requestFullscreen(this.el[0]);
|
||||
}
|
||||
|
||||
/** Toggle fullscreen mode. */
|
||||
function toggle() {
|
||||
if (this.videoFullScreen.fullScreenState) {
|
||||
this.videoFullScreen.exit();
|
||||
} else {
|
||||
this.videoFullScreen.enter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler to exit from fullscreen mode.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
function exitHandler(event) {
|
||||
if ((this.isFullScreen) && (event.keyCode === 27)) {
|
||||
event.preventDefault();
|
||||
this.videoCommands.execute('toggleFullScreen');
|
||||
}
|
||||
}
|
||||
|
||||
// function makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called, these functions will
|
||||
// get the 'state' object as a context.
|
||||
function makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
destroy: destroy,
|
||||
enter: enter,
|
||||
exit: exit,
|
||||
exitHandler: exitHandler,
|
||||
handleExit: handleExit,
|
||||
handleEnter: handleEnter,
|
||||
handleFullscreenChange: handleFullscreenChange,
|
||||
toggle: toggle,
|
||||
toggleHandler: toggleHandler,
|
||||
updateControlsHeight: updateControlsHeight,
|
||||
notifyParent: notifyParent
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoFullScreen, state);
|
||||
}
|
||||
|
||||
// VideoControl() function - what this module "exports".
|
||||
return function(state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
state.videoFullScreen = {};
|
||||
|
||||
makeFunctionsPublic(state);
|
||||
renderElements(state);
|
||||
bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,181 +0,0 @@
|
||||
(function(requirejs, require, define) {
|
||||
// VideoQualityControl module.
|
||||
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
'video/05_video_quality_control.js',
|
||||
['edx-ui-toolkit/js/utils/html-utils'],
|
||||
function(HtmlUtils) {
|
||||
var template = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<button class="control quality-control is-hidden" aria-disabled="false" title="',
|
||||
'{highDefinition}',
|
||||
'">',
|
||||
'<span class="icon icon-hd" aria-hidden="true">HD</span>',
|
||||
'<span class="sr text-translation">',
|
||||
'{highDefinition}',
|
||||
'</span> ',
|
||||
'<span class="sr control-text">',
|
||||
'{off}',
|
||||
'</span>',
|
||||
'</button>'
|
||||
].join('')),
|
||||
{
|
||||
highDefinition: gettext('High Definition'),
|
||||
off: gettext('off')
|
||||
}
|
||||
);
|
||||
|
||||
// VideoQualityControl() function - what this module "exports".
|
||||
return function(state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
// Changing quality for now only works for YouTube videos.
|
||||
if (state.videoType !== 'youtube') {
|
||||
return;
|
||||
}
|
||||
|
||||
state.videoQualityControl = {};
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called, these functions will
|
||||
// get the 'state' object as a context.
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
destroy: destroy,
|
||||
fetchAvailableQualities: fetchAvailableQualities,
|
||||
onQualityChange: onQualityChange,
|
||||
showQualityControl: showQualityControl,
|
||||
toggleQuality: toggleQuality
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoQualityControl, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
this.videoQualityControl.el.off({
|
||||
click: this.videoQualityControl.toggleQuality,
|
||||
destroy: this.videoQualityControl.destroy
|
||||
});
|
||||
this.el.off('.quality');
|
||||
this.videoQualityControl.el.remove();
|
||||
delete this.videoQualityControl;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
var element = state.videoQualityControl.el = $(template.toString());
|
||||
state.videoQualityControl.quality = 'large';
|
||||
HtmlUtils.append(state.el.find('.secondary-controls'), HtmlUtils.HTML(element));
|
||||
}
|
||||
|
||||
// function _bindHandlers(state)
|
||||
//
|
||||
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
|
||||
function _bindHandlers(state) {
|
||||
state.videoQualityControl.el.on('click',
|
||||
state.videoQualityControl.toggleQuality
|
||||
);
|
||||
state.el.on('play.quality', _.once(
|
||||
state.videoQualityControl.fetchAvailableQualities
|
||||
));
|
||||
|
||||
state.el.on('destroy.quality', state.videoQualityControl.destroy);
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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().
|
||||
// ***************************************************************
|
||||
|
||||
/*
|
||||
* @desc Shows quality control. This function will only be called if HD
|
||||
* qualities are available.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function showQualityControl() {
|
||||
this.videoQualityControl.el.removeClass('is-hidden');
|
||||
}
|
||||
|
||||
// This function can only be called once as _.once has been used.
|
||||
/*
|
||||
* @desc Get the available qualities from YouTube API. Possible values are:
|
||||
['highres', 'hd1080', 'hd720', 'large', 'medium', 'small'].
|
||||
HD are: ['highres', 'hd1080', 'hd720'].
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function fetchAvailableQualities() {
|
||||
var qualities = this.videoPlayer.player.getAvailableQualityLevels();
|
||||
|
||||
this.config.availableHDQualities = _.intersection(
|
||||
qualities, ['highres', 'hd1080', 'hd720']
|
||||
);
|
||||
|
||||
// HD qualities are available, show video quality control.
|
||||
if (this.config.availableHDQualities.length > 0) {
|
||||
this.trigger('videoQualityControl.showQualityControl');
|
||||
this.trigger('videoQualityControl.onQualityChange', this.videoQualityControl.quality);
|
||||
}
|
||||
// On initialization, force the video quality to be 'large' instead of
|
||||
// 'default'. Otherwise, the player will sometimes switch to HD
|
||||
// automatically, for example when the iframe resizes itself.
|
||||
this.trigger('videoPlayer.handlePlaybackQualityChange',
|
||||
this.videoQualityControl.quality
|
||||
);
|
||||
}
|
||||
|
||||
function onQualityChange(value) {
|
||||
var controlStateStr;
|
||||
this.videoQualityControl.quality = value;
|
||||
if (_.contains(this.config.availableHDQualities, value)) {
|
||||
controlStateStr = gettext('on');
|
||||
this.videoQualityControl.el
|
||||
.addClass('active')
|
||||
.find('.control-text')
|
||||
.text(controlStateStr);
|
||||
} else {
|
||||
controlStateStr = gettext('off');
|
||||
this.videoQualityControl.el
|
||||
.removeClass('active')
|
||||
.find('.control-text')
|
||||
.text(controlStateStr);
|
||||
}
|
||||
}
|
||||
|
||||
// This function toggles the quality of video only if HD qualities are
|
||||
// available.
|
||||
function toggleQuality(event) {
|
||||
var newQuality,
|
||||
value = this.videoQualityControl.quality,
|
||||
isHD = _.contains(this.config.availableHDQualities, value);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
newQuality = isHD ? 'large' : 'highres';
|
||||
|
||||
this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality);
|
||||
}
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,367 +0,0 @@
|
||||
(function(requirejs, require, define) {
|
||||
/*
|
||||
"This is as true in everyday life as it is in battle: we are given one life
|
||||
and the decision is ours whether to wait for circumstances to make up our
|
||||
mind, or whether to act, and in acting, to live."
|
||||
— Omar N. Bradley
|
||||
*/
|
||||
|
||||
// VideoProgressSlider module.
|
||||
define(
|
||||
'video/06_video_progress_slider.js',
|
||||
[],
|
||||
function() {
|
||||
var template = [
|
||||
'<div class="slider" role="application" title="',
|
||||
gettext('Video position. Press space to toggle playback'),
|
||||
'"></div>'
|
||||
].join('');
|
||||
|
||||
// VideoProgressSlider() function - what this module "exports".
|
||||
return function(state) {
|
||||
var dfd = $.Deferred();
|
||||
|
||||
state.videoProgressSlider = {};
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
|
||||
dfd.resolve();
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called,
|
||||
// these functions will get the 'state' object as a context.
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
destroy: destroy,
|
||||
buildSlider: buildSlider,
|
||||
getRangeParams: getRangeParams,
|
||||
onSlide: onSlide,
|
||||
onStop: onStop,
|
||||
updatePlayTime: updatePlayTime,
|
||||
updateStartEndTimeRegion: updateStartEndTimeRegion,
|
||||
notifyThroughHandleEnd: notifyThroughHandleEnd,
|
||||
getTimeDescription: getTimeDescription,
|
||||
focusSlider: focusSlider
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoProgressSlider, state);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
this.videoProgressSlider.el.removeAttr('tabindex').slider('destroy');
|
||||
this.el.off('destroy', this.videoProgressSlider.destroy);
|
||||
delete this.videoProgressSlider;
|
||||
}
|
||||
|
||||
function bindHandlers(state) {
|
||||
state.videoProgressSlider.el.on('keypress', sliderToggle.bind(state));
|
||||
state.el.on('destroy', state.videoProgressSlider.destroy);
|
||||
}
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
// 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) {
|
||||
state.videoProgressSlider.el = $(template);
|
||||
|
||||
state.el.find('.video-controls').prepend(state.videoProgressSlider.el);
|
||||
state.videoProgressSlider.buildSlider();
|
||||
_buildHandle(state);
|
||||
bindHandlers(state);
|
||||
}
|
||||
|
||||
function _buildHandle(state) {
|
||||
state.videoProgressSlider.handle = state.videoProgressSlider.el
|
||||
.find('.ui-slider-handle');
|
||||
|
||||
// ARIA
|
||||
// We just want the knob to be selectable with keyboard
|
||||
state.videoProgressSlider.el.attr({
|
||||
tabindex: -1
|
||||
});
|
||||
|
||||
// Let screen readers know that this div, representing the slider
|
||||
// handle, behaves as a slider named 'video position'.
|
||||
state.videoProgressSlider.handle.attr({
|
||||
role: 'slider',
|
||||
'aria-disabled': false,
|
||||
'aria-valuetext': getTimeDescription(state.videoProgressSlider
|
||||
.slider.slider('option', 'value')),
|
||||
'aria-valuemax': state.videoPlayer.duration(),
|
||||
'aria-valuemin': '0',
|
||||
'aria-valuenow': state.videoPlayer.currentTime,
|
||||
tabindex: '0',
|
||||
'aria-label': gettext('Video position. Press space to toggle playback')
|
||||
});
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// 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 buildSlider() {
|
||||
var sliderContents = edx.HtmlUtils.joinHtml(
|
||||
edx.HtmlUtils.HTML('<div class="ui-slider-handle progress-handle"></div>')
|
||||
);
|
||||
|
||||
// xss-lint: disable=javascript-jquery-append
|
||||
this.videoProgressSlider.el.append(sliderContents.text);
|
||||
|
||||
this.videoProgressSlider.slider = this.videoProgressSlider.el
|
||||
.slider({
|
||||
range: 'min',
|
||||
min: this.config.startTime,
|
||||
max: this.config.endTime,
|
||||
slide: this.videoProgressSlider.onSlide,
|
||||
stop: this.videoProgressSlider.onStop,
|
||||
step: 5
|
||||
});
|
||||
|
||||
this.videoProgressSlider.sliderProgress = this.videoProgressSlider
|
||||
.slider
|
||||
.find('.ui-slider-range.ui-widget-header.ui-slider-range-min');
|
||||
}
|
||||
|
||||
// Rebuild the slider start-end range (if it doesn't take up the
|
||||
// whole slider). Remember that endTime === null means the end-time
|
||||
// is set to the end of video by default.
|
||||
function updateStartEndTimeRegion(params) {
|
||||
var start, end, duration, rangeParams;
|
||||
|
||||
// We must have a duration in order to determine the area of range.
|
||||
// It also must be non-zero.
|
||||
if (!params.duration) {
|
||||
return;
|
||||
} else {
|
||||
duration = params.duration;
|
||||
}
|
||||
|
||||
start = this.config.startTime;
|
||||
end = this.config.endTime;
|
||||
|
||||
if (start > duration) {
|
||||
start = 0;
|
||||
} else if (this.isFlashMode()) {
|
||||
start /= Number(this.speed);
|
||||
}
|
||||
|
||||
// If end is set to null, or it is greater than the duration of the
|
||||
// video, then we set it to the end of the video.
|
||||
if (end === null || end > duration) {
|
||||
end = duration;
|
||||
} else if (this.isFlashMode()) {
|
||||
end /= Number(this.speed);
|
||||
}
|
||||
|
||||
// Don't build a range if it takes up the whole slider.
|
||||
if (start === 0 && end === duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Because JavaScript has weird rounding rules when a series of
|
||||
// mathematical operations are performed in a single statement, we will
|
||||
// split everything up into smaller statements.
|
||||
//
|
||||
// This will ensure that visually, the start-end range aligns nicely
|
||||
// with actual starting and ending point of the video.
|
||||
|
||||
rangeParams = getRangeParams(start, end, duration);
|
||||
}
|
||||
|
||||
function getRangeParams(startTime, endTime, duration) {
|
||||
var step = 100 / duration,
|
||||
left = startTime * step,
|
||||
width = endTime * step - left;
|
||||
|
||||
return {
|
||||
left: left + '%',
|
||||
width: width + '%'
|
||||
};
|
||||
}
|
||||
|
||||
function onSlide(event, ui) {
|
||||
var time = ui.value,
|
||||
endTime = this.videoPlayer.duration();
|
||||
|
||||
if (this.config.endTime) {
|
||||
endTime = Math.min(this.config.endTime, endTime);
|
||||
}
|
||||
|
||||
this.videoProgressSlider.frozen = true;
|
||||
|
||||
// Remember the seek to value so that we don't repeat ourselves on the
|
||||
// 'stop' slider event.
|
||||
this.videoProgressSlider.lastSeekValue = time;
|
||||
|
||||
this.trigger(
|
||||
'videoControl.updateVcrVidTime',
|
||||
{
|
||||
time: time,
|
||||
duration: endTime
|
||||
}
|
||||
);
|
||||
|
||||
this.trigger(
|
||||
'videoPlayer.onSlideSeek',
|
||||
{type: 'onSlideSeek', time: time}
|
||||
);
|
||||
|
||||
// ARIA
|
||||
this.videoProgressSlider.handle.attr(
|
||||
'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
|
||||
);
|
||||
}
|
||||
|
||||
function onStop(event, ui) {
|
||||
var _this = this;
|
||||
|
||||
this.videoProgressSlider.frozen = true;
|
||||
|
||||
// Only perform a seek if we haven't made a seek for the new slider value.
|
||||
// This is necessary so that if the user only clicks on the slider, without
|
||||
// dragging it, then only one seek is made, even when a 'slide' and a 'stop'
|
||||
// events are triggered on the slider.
|
||||
if (this.videoProgressSlider.lastSeekValue !== ui.value) {
|
||||
this.trigger(
|
||||
'videoPlayer.onSlideSeek',
|
||||
{type: 'onSlideSeek', time: ui.value}
|
||||
);
|
||||
}
|
||||
|
||||
// ARIA
|
||||
this.videoProgressSlider.handle.attr(
|
||||
'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
|
||||
);
|
||||
|
||||
setTimeout(function() {
|
||||
_this.videoProgressSlider.frozen = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function updatePlayTime(params) {
|
||||
var time = Math.floor(params.time);
|
||||
// params.duration could accidentally be construed as a floating
|
||||
// point double. Since we're displaying this number, round down
|
||||
// to nearest second
|
||||
var endTime = Math.floor(params.duration);
|
||||
|
||||
if (this.config.endTime !== null) {
|
||||
endTime = Math.min(this.config.endTime, endTime);
|
||||
}
|
||||
|
||||
if (
|
||||
this.videoProgressSlider.slider
|
||||
&& !this.videoProgressSlider.frozen
|
||||
) {
|
||||
this.videoProgressSlider.slider
|
||||
.slider('option', 'max', endTime)
|
||||
.slider('option', 'value', time);
|
||||
}
|
||||
|
||||
// Update aria values.
|
||||
this.videoProgressSlider.handle.attr({
|
||||
'aria-valuemax': endTime,
|
||||
'aria-valuenow': time
|
||||
});
|
||||
}
|
||||
|
||||
// When the video stops playing (either because the end was reached, or
|
||||
// because endTime was reached), the screen reader must be notified that
|
||||
// the video is no longer playing. We do this by a little trick. Setting
|
||||
// the title attribute of the slider know to "video ended", and focusing
|
||||
// on it. The screen reader will read the attr text.
|
||||
//
|
||||
// The user can then tab their way forward, landing on the next control
|
||||
// element, the Play button.
|
||||
//
|
||||
// @param params - object with property `end`. If set to true, the
|
||||
// function must set the title attribute to
|
||||
// `video ended`;
|
||||
// if set to false, the function must reset the attr to
|
||||
// it's original state.
|
||||
//
|
||||
// This function will be triggered from VideoPlayer methods onEnded(),
|
||||
// onPlay(), and update() (update method handles endTime).
|
||||
function notifyThroughHandleEnd(params) {
|
||||
if (params.end) {
|
||||
this.videoProgressSlider.handle
|
||||
.attr('title', gettext('Video ended'))
|
||||
.focus();
|
||||
} else {
|
||||
this.videoProgressSlider.handle
|
||||
.attr('title', gettext('Video position'));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a string describing the current time of video in
|
||||
// `%d hours %d minutes %d seconds` format.
|
||||
function getTimeDescription(time) {
|
||||
var seconds = Math.floor(time),
|
||||
minutes = Math.floor(seconds / 60),
|
||||
hours = Math.floor(minutes / 60),
|
||||
i18n = function(value, word) {
|
||||
var msg;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (word) {
|
||||
case 'hour':
|
||||
msg = ngettext('%(value)s hour', '%(value)s hours', value);
|
||||
break;
|
||||
case 'minute':
|
||||
msg = ngettext('%(value)s minute', '%(value)s minutes', value);
|
||||
break;
|
||||
case 'second':
|
||||
msg = ngettext('%(value)s second', '%(value)s seconds', value);
|
||||
break;
|
||||
}
|
||||
return interpolate(msg, {value: value}, true);
|
||||
};
|
||||
|
||||
seconds %= 60;
|
||||
minutes %= 60;
|
||||
|
||||
if (hours) {
|
||||
return i18n(hours, 'hour') + ' '
|
||||
+ i18n(minutes, 'minute') + ' '
|
||||
+ i18n(seconds, 'second');
|
||||
} else if (minutes) {
|
||||
return i18n(minutes, 'minute') + ' '
|
||||
+ i18n(seconds, 'second');
|
||||
}
|
||||
|
||||
return i18n(seconds, 'second');
|
||||
}
|
||||
|
||||
// Shift focus to the progress slider container element.
|
||||
function focusSlider() {
|
||||
this.videoProgressSlider.handle.attr(
|
||||
'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
|
||||
);
|
||||
this.videoProgressSlider.el.trigger('focus');
|
||||
}
|
||||
|
||||
// Toggle video playback when the spacebar is pushed.
|
||||
function sliderToggle(e) {
|
||||
if (e.which === 32) {
|
||||
e.preventDefault();
|
||||
this.videoCommands.execute('togglePlayback');
|
||||
}
|
||||
}
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,554 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
// VideoVolumeControl module.
|
||||
define(
|
||||
'video/07_video_volume_control.js', ['edx-ui-toolkit/js/utils/html-utils'],
|
||||
function(HtmlUtils) {
|
||||
/**
|
||||
* Video volume control module.
|
||||
* @exports video/07_video_volume_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var VolumeControl = function(state, i18n) {
|
||||
if (!(this instanceof VolumeControl)) {
|
||||
return new VolumeControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'keyDownHandler', 'updateVolumeSilently',
|
||||
'onVolumeChangeHandler', 'openMenu', 'closeMenu',
|
||||
'toggleMuteHandler', 'keyDownButtonHandler', 'destroy'
|
||||
);
|
||||
this.state = state;
|
||||
this.state.videoVolumeControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VolumeControl.prototype = {
|
||||
/** Minimum value for the volume slider. */
|
||||
min: 0,
|
||||
/** Maximum value for the volume slider. */
|
||||
max: 100,
|
||||
/** Step to increase/decrease volume level via keyboard. */
|
||||
step: 20,
|
||||
|
||||
videoVolumeControlHtml: HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<div class="volume" role="application">',
|
||||
'<p class="sr instructions">',
|
||||
'{volumeInstructions}',
|
||||
'</p>',
|
||||
'<button class="control" aria-disabled="false"',
|
||||
'" aria-expanded="false" title="',
|
||||
'{adjustVideoVolume}',
|
||||
'" aria-label="',
|
||||
'{adjustVideoVolume}',
|
||||
'">',
|
||||
'<span class="icon fa fa-volume-up" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'<div class="volume-slider-container" aria-hidden="true" title="',
|
||||
'{adjustVideoVolume}',
|
||||
'">',
|
||||
'<div class="volume-slider" ',
|
||||
'role="slider"',
|
||||
'aria-orientation="vertical" ',
|
||||
'aria-valuemin="0" ',
|
||||
'aria-valuemax="100" ',
|
||||
'aria-valuenow="" ',
|
||||
'aria-label="',
|
||||
'{volumeText}',
|
||||
'"></div>',
|
||||
'</div>',
|
||||
'</div>'].join('')),
|
||||
{
|
||||
volumeInstructions: gettext('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.'), // eslint-disable-line max-len
|
||||
adjustVideoVolume: gettext('Adjust video volume'),
|
||||
volumeText: gettext('Volume')
|
||||
}
|
||||
),
|
||||
|
||||
destroy: function() {
|
||||
this.volumeSlider.slider('destroy');
|
||||
this.state.el.find('iframe').removeAttr('tabindex');
|
||||
this.a11y.destroy();
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
this.cookie = this.a11y = null;
|
||||
this.closeMenu();
|
||||
|
||||
this.state.el
|
||||
.off('play.volume')
|
||||
.off({
|
||||
keydown: this.keyDownHandler,
|
||||
volumechange: this.onVolumeChangeHandler
|
||||
});
|
||||
this.el.off({
|
||||
mouseenter: this.openMenu,
|
||||
mouseleave: this.closeMenu
|
||||
});
|
||||
this.button.off({
|
||||
mousedown: this.toggleMuteHandler,
|
||||
keydown: this.keyDownButtonHandler,
|
||||
focus: this.openMenu,
|
||||
blur: this.closeMenu
|
||||
});
|
||||
this.el.remove();
|
||||
delete this.state.videoVolumeControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
var volume;
|
||||
|
||||
if (this.state.isTouch) {
|
||||
// iOS doesn't support volume change
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el = $(this.videoVolumeControlHtml.toString());
|
||||
// Youtube iframe react on key buttons and has his own handlers.
|
||||
// So, we disallow focusing on iframe.
|
||||
this.state.el.find('iframe').attr('tabindex', -1);
|
||||
this.button = this.el.children('.control');
|
||||
this.cookie = new CookieManager(this.min, this.max);
|
||||
this.a11y = new Accessibility(
|
||||
this.button, this.min, this.max, this.i18n
|
||||
);
|
||||
volume = this.cookie.getVolume();
|
||||
this.storedVolume = this.max;
|
||||
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
this.setVolume(volume, true, false);
|
||||
this.checkMuteButtonStatus(volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
var container = this.el.find('.volume-slider'),
|
||||
instructionsId = 'volume-instructions-' + this.state.id;
|
||||
|
||||
HtmlUtils.append(container, HtmlUtils.HTML('<div class="ui-slider-handle volume-handle"></div>'));
|
||||
|
||||
this.volumeSlider = container.slider({
|
||||
orientation: 'vertical',
|
||||
range: 'min',
|
||||
min: this.min,
|
||||
max: this.max,
|
||||
slide: this.onSlideHandler.bind(this)
|
||||
});
|
||||
|
||||
// We provide an independent behavior to adjust volume level.
|
||||
// Therefore, we do not need redundant focusing on slider in TAB
|
||||
// order.
|
||||
container.find('.volume-handle').attr('tabindex', -1);
|
||||
this.state.el.find('.secondary-controls').append(this.el);
|
||||
|
||||
// set dynamic id for instruction element to avoid collisions
|
||||
this.el.find('.instructions').attr('id', instructionsId);
|
||||
this.button.attr('aria-describedby', instructionsId);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.state.el.on({
|
||||
'play.volume': _.once(this.updateVolumeSilently),
|
||||
volumechange: this.onVolumeChangeHandler
|
||||
});
|
||||
this.state.el.find('.volume').on({
|
||||
mouseenter: this.openMenu,
|
||||
mouseleave: this.closeMenu
|
||||
});
|
||||
this.button.on({
|
||||
keydown: this.keyDownHandler,
|
||||
click: false,
|
||||
mousedown: this.toggleMuteHandler,
|
||||
focus: this.openMenu,
|
||||
blur: this.closeMenu
|
||||
});
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates volume level without updating view and triggering
|
||||
* `volumechange` event.
|
||||
*/
|
||||
updateVolumeSilently: function() {
|
||||
this.state.el.trigger(
|
||||
'volumechange:silent', [this.getVolume()]
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns current volume level.
|
||||
* @return {Number}
|
||||
*/
|
||||
getVolume: function() {
|
||||
return this.volume;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets current volume level.
|
||||
* @param {Number} volume Suggested volume level
|
||||
* @param {Boolean} [silent] Sets the new volume level without
|
||||
* triggering `volumechange` event and updating the cookie.
|
||||
* @param {Boolean} [withoutSlider] Disables updating the slider.
|
||||
*/
|
||||
setVolume: function(volume, silent, withoutSlider) {
|
||||
if (volume === this.getVolume()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.volume = volume;
|
||||
this.a11y.update(this.getVolume());
|
||||
|
||||
if (!withoutSlider) {
|
||||
this.updateSliderView(this.getVolume());
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
this.cookie.setVolume(this.getVolume());
|
||||
this.state.el.trigger('volumechange', [this.getVolume()]);
|
||||
}
|
||||
},
|
||||
|
||||
/** Increases current volume level using previously defined step. */
|
||||
increaseVolume: function() {
|
||||
var volume = Math.min(this.getVolume() + this.step, this.max);
|
||||
|
||||
this.setVolume(volume, false, false);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/** Decreases current volume level using previously defined step. */
|
||||
decreaseVolume: function() {
|
||||
var volume = Math.max(this.getVolume() - this.step, this.min);
|
||||
|
||||
this.setVolume(volume, false, false);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/** Updates volume slider view. */
|
||||
updateSliderView: function(volume) {
|
||||
this.volumeSlider.slider('value', volume);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mutes or unmutes volume.
|
||||
* @param {Number} muteStatus Flag to mute/unmute volume.
|
||||
*/
|
||||
mute: function(muteStatus) {
|
||||
var volume;
|
||||
|
||||
this.updateMuteButtonView(muteStatus);
|
||||
|
||||
if (muteStatus) {
|
||||
this.storedVolume = this.getVolume() || this.max;
|
||||
}
|
||||
|
||||
volume = muteStatus ? 0 : this.storedVolume;
|
||||
this.setVolume(volume, false, false);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns current volume state (is it muted or not?).
|
||||
* @return {Boolean}
|
||||
*/
|
||||
getMuteStatus: function() {
|
||||
return this.getVolume() === 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the volume button view.
|
||||
* @param {Boolean} isMuted Flag to use muted or unmuted view.
|
||||
*/
|
||||
updateMuteButtonView: function(isMuted) {
|
||||
var action = isMuted ? 'addClass' : 'removeClass';
|
||||
|
||||
this.el[action]('is-muted');
|
||||
|
||||
if (isMuted) {
|
||||
this.el
|
||||
.find('.control .icon')
|
||||
.removeClass('fa-volume-up')
|
||||
.addClass('fa-volume-off');
|
||||
} else {
|
||||
this.el
|
||||
.find('.control .icon')
|
||||
.removeClass('fa-volume-off')
|
||||
.addClass('fa-volume-up');
|
||||
}
|
||||
},
|
||||
|
||||
/** Toggles the state of the volume button. */
|
||||
toggleMute: function() {
|
||||
this.mute(!this.getMuteStatus());
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks and updates the state of the volume button relatively to
|
||||
* volume level.
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
checkMuteButtonStatus: function(volume) {
|
||||
if (volume <= this.min) {
|
||||
this.updateMuteButtonView(true);
|
||||
this.state.el.off('volumechange.is-muted');
|
||||
this.state.el.on('volumechange.is-muted', _.once(function() {
|
||||
this.updateMuteButtonView(false);
|
||||
}.bind(this)));
|
||||
}
|
||||
},
|
||||
|
||||
/** Opens volume menu. */
|
||||
openMenu: function() {
|
||||
this.el.addClass('is-opened');
|
||||
this.button.attr('aria-expanded', 'true');
|
||||
},
|
||||
|
||||
/** Closes speed menu. */
|
||||
closeMenu: function() {
|
||||
this.el.removeClass('is-opened');
|
||||
this.button.attr('aria-expanded', 'false');
|
||||
},
|
||||
|
||||
/**
|
||||
* Keydown event handler for the video container.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
keyDownHandler: function(event) {
|
||||
// ALT key is used to change (alternate) the function of
|
||||
// other pressed keys. In this case, do nothing.
|
||||
if (event.altKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($(event.target).hasClass('ui-slider-handle')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.UP:
|
||||
// Shift + Arrows keyboard shortcut might be used by
|
||||
// screen readers. In this case, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.increaseVolume();
|
||||
return false;
|
||||
case KEY.DOWN:
|
||||
// Shift + Arrows keyboard shortcut might be used by
|
||||
// screen readers. In this case, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.decreaseVolume();
|
||||
return false;
|
||||
|
||||
case KEY.SPACE:
|
||||
case KEY.ENTER:
|
||||
// Shift + Enter keyboard shortcut might be used by
|
||||
// screen readers. In this case, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.toggleMute();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Keydown event handler for the volume button.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
keyDownButtonHandler: function(event) {
|
||||
// ALT key is used to change (alternate) the function of
|
||||
// other pressed keys. In this case, do nothing.
|
||||
if (event.altKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.toggleMute();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* onSlide callback for the video slider.
|
||||
* @param {jquery Event} event
|
||||
* @param {jqueryuiSlider ui} ui
|
||||
*/
|
||||
onSlideHandler: function(event, ui) {
|
||||
this.setVolume(ui.value, false, true);
|
||||
this.el.find('.volume-slider')
|
||||
.attr('aria-valuenow', ui.volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mousedown event handler for the volume button.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
toggleMuteHandler: function(event) {
|
||||
this.toggleMute();
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Volumechange event handler.
|
||||
* @param {jquery Event} event
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
onVolumeChangeHandler: function(event, volume) {
|
||||
this.checkMuteButtonStatus(volume);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Module responsible for the accessibility of volume controls.
|
||||
* @constructor
|
||||
* @private
|
||||
* @param {jquery $} button The volume button.
|
||||
* @param {Number} min Minimum value for the volume slider.
|
||||
* @param {Number} max Maximum value for the volume slider.
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
*/
|
||||
var Accessibility = function(button, min, max, i18n) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.button = button;
|
||||
this.i18n = i18n;
|
||||
|
||||
this.initialize();
|
||||
};
|
||||
|
||||
Accessibility.prototype = {
|
||||
destroy: function() {
|
||||
this.liveRegion.remove();
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.liveRegion = $('<div />', {
|
||||
class: 'sr video-live-region',
|
||||
'aria-hidden': 'false',
|
||||
'aria-live': 'polite'
|
||||
});
|
||||
|
||||
this.button.after(HtmlUtils.HTML(this.liveRegion).toString());
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates text of the live region.
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
update: function(volume) {
|
||||
this.liveRegion.text([
|
||||
this.getVolumeDescription(volume),
|
||||
this.i18n.Volume + '.'
|
||||
].join(' '));
|
||||
|
||||
$(this.button).parent().find('.volume-slider')
|
||||
.attr('aria-valuenow', volume);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a string describing the level of volume.
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
getVolumeDescription: function(volume) {
|
||||
if (volume === 0) {
|
||||
return this.i18n.Muted;
|
||||
} else if (volume <= 20) {
|
||||
return this.i18n['Very low'];
|
||||
} else if (volume <= 40) {
|
||||
return this.i18n.Low;
|
||||
} else if (volume <= 60) {
|
||||
return this.i18n.Average;
|
||||
} else if (volume <= 80) {
|
||||
return this.i18n.Loud;
|
||||
} else if (volume <= 99) {
|
||||
return this.i18n['Very loud'];
|
||||
}
|
||||
|
||||
return this.i18n.Maximum;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Module responsible for the work with volume cookie.
|
||||
* @constructor
|
||||
* @private
|
||||
* @param {Number} min Minimum value for the volume slider.
|
||||
* @param {Number} max Maximum value for the volume slider.
|
||||
*/
|
||||
var CookieManager = function(min, max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.cookieName = 'video_player_volume_level';
|
||||
};
|
||||
|
||||
CookieManager.prototype = {
|
||||
/**
|
||||
* Returns volume level from the cookie.
|
||||
* @return {Number} Volume level.
|
||||
*/
|
||||
getVolume: function() {
|
||||
var volume = parseInt($.cookie(this.cookieName), 10);
|
||||
|
||||
if (_.isFinite(volume)) {
|
||||
volume = Math.max(volume, this.min);
|
||||
volume = Math.min(volume, this.max);
|
||||
} else {
|
||||
volume = this.max;
|
||||
}
|
||||
|
||||
return volume;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates volume cookie.
|
||||
* @param {Number} volume Volume level.
|
||||
*/
|
||||
setVolume: function(value) {
|
||||
$.cookie(this.cookieName, value, {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return VolumeControl;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,422 +0,0 @@
|
||||
(function(requirejs, require, define) {
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
'video/08_video_speed_control.js', [
|
||||
'video/00_iterator.js',
|
||||
'edx-ui-toolkit/js/utils/html-utils'
|
||||
], function(Iterator, HtmlUtils) {
|
||||
/**
|
||||
* Video speed control module.
|
||||
* @exports video/08_video_speed_control.js
|
||||
* @constructor
|
||||
* @param {object} state The object containing the state of the video player.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var SpeedControl = function(state) {
|
||||
if (!(this instanceof SpeedControl)) {
|
||||
return new SpeedControl(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onSetSpeed', 'onRenderSpeed', 'clickLinkHandler',
|
||||
'keyDownLinkHandler', 'mouseEnterHandler', 'mouseLeaveHandler',
|
||||
'clickMenuHandler', 'keyDownMenuHandler', 'destroy'
|
||||
);
|
||||
this.state = state;
|
||||
this.state.videoSpeedControl = this;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
SpeedControl.prototype = {
|
||||
template: [
|
||||
'<div class="speeds menu-container" role="application">',
|
||||
'<p class="sr instructions">',
|
||||
gettext('Press UP to enter the speed menu then use the UP and DOWN arrow keys to navigate the different speeds, then press ENTER to change to the selected speed.'), // eslint-disable-line max-len, indent
|
||||
'</p>',
|
||||
'<button class="control speed-button" aria-disabled="false" aria-expanded="false"',
|
||||
'title="',
|
||||
gettext('Adjust video speed'),
|
||||
'">',
|
||||
'<span>',
|
||||
'<span class="icon fa fa-caret-right" aria-hidden="true"></span>',
|
||||
'</span>',
|
||||
'<span class="label">',
|
||||
gettext('Speed'),
|
||||
' </span>',
|
||||
'<span class="value"></span>',
|
||||
'</button>',
|
||||
'<ol class="video-speeds menu"></ol>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
|
||||
destroy: function() {
|
||||
this.el.off({
|
||||
mouseenter: this.mouseEnterHandler,
|
||||
mouseleave: this.mouseLeaveHandler,
|
||||
click: this.clickMenuHandler,
|
||||
keydown: this.keyDownMenuHandler
|
||||
});
|
||||
|
||||
this.state.el.off({
|
||||
'speed:set': this.onSetSpeed,
|
||||
'speed:render': this.onRenderSpeed
|
||||
});
|
||||
this.closeMenu(true);
|
||||
this.speedsContainer.remove();
|
||||
this.el.remove();
|
||||
delete this.state.videoSpeedControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
var state = this.state;
|
||||
|
||||
if (!this.isPlaybackRatesSupported(state)) {
|
||||
console.log(
|
||||
'[Video info]: playbackRate is not supported.'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
this.el = $(this.template);
|
||||
this.speedsContainer = this.el.find('.video-speeds');
|
||||
this.speedButton = this.el.find('.speed-button');
|
||||
this.render(state.speeds, state.speed);
|
||||
this.setSpeed(state.speed, true, true);
|
||||
this.bindHandlers();
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
* @param {array} speeds List of speeds available for the player.
|
||||
* @param {string} currentSpeed The current speed set to the player.
|
||||
*/
|
||||
render: function(speeds, currentSpeed) {
|
||||
var speedsContainer = this.speedsContainer,
|
||||
reversedSpeeds = speeds.concat().reverse(),
|
||||
instructionsId = 'speed-instructions-' + this.state.id,
|
||||
speedsList = $.map(reversedSpeeds, function(speed) {
|
||||
return HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<li data-speed="{speed}">',
|
||||
'<button class="control speed-option" tabindex="-1" aria-pressed="false">',
|
||||
'{speed}x',
|
||||
'</button>',
|
||||
'</li>'
|
||||
].join('')
|
||||
),
|
||||
{
|
||||
speed: speed
|
||||
}
|
||||
).toString();
|
||||
});
|
||||
|
||||
HtmlUtils.setHtml(
|
||||
speedsContainer,
|
||||
HtmlUtils.HTML(speedsList)
|
||||
);
|
||||
this.speedLinks = new Iterator(speedsContainer.find('.speed-option'));
|
||||
HtmlUtils.prepend(
|
||||
this.state.el.find('.secondary-controls'),
|
||||
HtmlUtils.HTML(this.el)
|
||||
);
|
||||
this.setActiveSpeed(currentSpeed);
|
||||
|
||||
// set dynamic id for instruction element to avoid collisions
|
||||
this.el.find('.instructions').attr('id', instructionsId);
|
||||
this.speedButton.attr('aria-describedby', instructionsId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind any necessary function callbacks to DOM events (click,
|
||||
* mousemove, etc.).
|
||||
*/
|
||||
bindHandlers: function() {
|
||||
// Attach various events handlers to the speed menu button.
|
||||
this.el.on({
|
||||
mouseenter: this.mouseEnterHandler,
|
||||
mouseleave: this.mouseLeaveHandler,
|
||||
click: this.openMenu,
|
||||
keydown: this.keyDownMenuHandler
|
||||
});
|
||||
|
||||
// Attach click and keydown event handlers to the individual speed
|
||||
// entries.
|
||||
this.speedsContainer.on({
|
||||
click: this.clickLinkHandler,
|
||||
keydown: this.keyDownLinkHandler
|
||||
}, '.speed-option');
|
||||
|
||||
this.state.el.on({
|
||||
'speed:set': this.onSetSpeed,
|
||||
'speed:render': this.onRenderSpeed
|
||||
});
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
onSetSpeed: function(event, speed) {
|
||||
this.setSpeed(speed, true);
|
||||
},
|
||||
|
||||
onRenderSpeed: function(event, speeds, currentSpeed) {
|
||||
this.render(speeds, currentSpeed);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if playbackRate supports by browser. If browser supports, 1.0
|
||||
* should be returned by playbackRate property. In this case, function
|
||||
* return True. Otherwise, False will be returned.
|
||||
* iOS doesn't support speed change.
|
||||
* @param {object} state The object containing the state of the video
|
||||
* player.
|
||||
* @return {boolean}
|
||||
* true: Browser support playbackRate functionality.
|
||||
* false: Browser doesn't support playbackRate functionality.
|
||||
*/
|
||||
isPlaybackRatesSupported: function(state) {
|
||||
var isHtml5 = state.videoType === 'html5',
|
||||
isTouch = state.isTouch,
|
||||
video = document.createElement('video');
|
||||
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
return !isTouch || (isHtml5 && !Boolean(video.playbackRate));
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens speed menu.
|
||||
* @param {boolean} [bindEvent] Click event will be attached on window.
|
||||
*/
|
||||
openMenu: function(bindEvent) {
|
||||
// When speed entries have focus, the menu stays open on
|
||||
// mouseleave. A clickHandler is added to the window
|
||||
// element to have clicks close the menu when they happen
|
||||
// outside of it.
|
||||
if (bindEvent) {
|
||||
$(window).on('click.speedMenu', this.clickMenuHandler);
|
||||
}
|
||||
|
||||
this.el.addClass('is-opened');
|
||||
this.speedButton
|
||||
.attr('tabindex', -1)
|
||||
.attr('aria-expanded', 'true');
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes speed menu.
|
||||
* @param {boolean} [unBindEvent] Click event will be detached from window.
|
||||
*/
|
||||
closeMenu: function(unBindEvent) {
|
||||
// Remove the previously added clickHandler from window element.
|
||||
if (unBindEvent) {
|
||||
$(window).off('click.speedMenu');
|
||||
}
|
||||
|
||||
this.el.removeClass('is-opened');
|
||||
this.speedButton
|
||||
.attr('tabindex', 0)
|
||||
.attr('aria-expanded', 'false');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets new current speed for the speed control and triggers `speedchange`
|
||||
* event if needed.
|
||||
* @param {string|number} speed Speed to be set.
|
||||
* @param {boolean} [silent] Sets the new speed without triggering
|
||||
* `speedchange` event.
|
||||
* @param {boolean} [forceUpdate] Updates the speed even if it's
|
||||
* not differs from current speed.
|
||||
*/
|
||||
setSpeed: function(speed, silent, forceUpdate) {
|
||||
var newSpeed = this.state.speedToString(speed);
|
||||
if (newSpeed !== this.currentSpeed || forceUpdate) {
|
||||
this.speedsContainer
|
||||
.find('li')
|
||||
.siblings("li[data-speed='" + newSpeed + "']");
|
||||
|
||||
this.speedButton.find('.value').text(newSpeed + 'x');
|
||||
this.currentSpeed = newSpeed;
|
||||
|
||||
if (!silent) {
|
||||
this.el.trigger('speedchange', [newSpeed, this.state.speed]);
|
||||
}
|
||||
}
|
||||
|
||||
this.resetActiveSpeed();
|
||||
this.setActiveSpeed(newSpeed);
|
||||
},
|
||||
|
||||
resetActiveSpeed: function() {
|
||||
var speedOptions = this.speedsContainer.find('li');
|
||||
|
||||
$(speedOptions).each(function(index, el) {
|
||||
$(el).removeClass('is-active')
|
||||
.find('.speed-option')
|
||||
.attr('aria-pressed', 'false');
|
||||
});
|
||||
},
|
||||
|
||||
setActiveSpeed: function(speed) {
|
||||
var speedOption = this.speedsContainer.find('li[data-speed="' + this.state.speedToString(speed) + '"]');
|
||||
|
||||
speedOption.addClass('is-active')
|
||||
.find('.speed-option')
|
||||
.attr('aria-pressed', 'true');
|
||||
|
||||
this.speedButton.attr('title', gettext('Video speed: ') + this.state.speedToString(speed) + 'x');
|
||||
},
|
||||
|
||||
/**
|
||||
* Click event handler for the menu.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
clickMenuHandler: function() {
|
||||
this.closeMenu();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Click event handler for speed links.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
clickLinkHandler: function(event) {
|
||||
var el = $(event.currentTarget).parent(),
|
||||
speed = $(el).data('speed');
|
||||
|
||||
this.resetActiveSpeed();
|
||||
this.setActiveSpeed(speed);
|
||||
this.state.videoCommands.execute('speed', speed);
|
||||
this.closeMenu(true);
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mouseenter event handler for the menu.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
mouseEnterHandler: function() {
|
||||
this.openMenu();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mouseleave event handler for the menu.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
mouseLeaveHandler: function() {
|
||||
// Only close the menu is no speed entry has focus.
|
||||
if (!this.speedLinks.list.is(':focus')) {
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Keydown event handler for the menu.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
keyDownMenuHandler: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
// Open menu and focus on last element of list above it.
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
case KEY.UP:
|
||||
this.openMenu(true);
|
||||
this.speedLinks.last().focus();
|
||||
break;
|
||||
// Close menu.
|
||||
case KEY.ESCAPE:
|
||||
this.closeMenu(true);
|
||||
break;
|
||||
}
|
||||
// We do not stop propagation and default behavior on a TAB
|
||||
// keypress.
|
||||
return event.keyCode === KEY.TAB;
|
||||
},
|
||||
|
||||
/**
|
||||
* Keydown event handler for speed links.
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
keyDownLinkHandler: function(event) {
|
||||
// ALT key is used to change (alternate) the function of
|
||||
// other pressed keys. In this, do nothing.
|
||||
if (event.altKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var KEY = $.ui.keyCode,
|
||||
self = this,
|
||||
parent = $(event.currentTarget).parent(),
|
||||
index = parent.index(),
|
||||
speed = parent.data('speed');
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.keyCode) {
|
||||
// Close menu.
|
||||
case KEY.TAB:
|
||||
// Closes menu after 25ms delay to change `tabindex` after
|
||||
// finishing default behavior.
|
||||
setTimeout(function() {
|
||||
self.closeMenu(true);
|
||||
}, 25);
|
||||
|
||||
return true;
|
||||
// Close menu and give focus to speed control.
|
||||
case KEY.ESCAPE:
|
||||
this.closeMenu(true);
|
||||
this.speedButton.focus();
|
||||
|
||||
return false;
|
||||
// Scroll up menu, wrapping at the top. Keep menu open.
|
||||
case KEY.UP:
|
||||
// Shift + Arrows keyboard shortcut might be used by
|
||||
// screen readers. In this, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.speedLinks.prev(index).focus();
|
||||
return false;
|
||||
// Scroll down menu, wrapping at the bottom. Keep menu
|
||||
// open.
|
||||
case KEY.DOWN:
|
||||
// Shift + Arrows keyboard shortcut might be used by
|
||||
// screen readers. In this, do nothing.
|
||||
if (event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.speedLinks.next(index).focus();
|
||||
return false;
|
||||
// Close menu, give focus to speed control and change
|
||||
// speed.
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.closeMenu(true);
|
||||
this.speedButton.focus();
|
||||
this.setSpeed(this.state.speedToString(speed));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return SpeedControl;
|
||||
});
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
@@ -1,687 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
// VideoContextMenu module.
|
||||
define(
|
||||
'video/095_video_context_menu.js',
|
||||
['video/00_component.js'],
|
||||
function(Component) {
|
||||
var AbstractItem, AbstractMenu, Menu, Overlay, Submenu, MenuItem;
|
||||
|
||||
AbstractItem = Component.extend({
|
||||
initialize: function(options) {
|
||||
this.options = $.extend(true, {
|
||||
label: '',
|
||||
prefix: 'edx-',
|
||||
dataAttrs: {menu: this},
|
||||
attrs: {},
|
||||
items: [],
|
||||
callback: $.noop,
|
||||
initialize: $.noop
|
||||
}, options);
|
||||
|
||||
this.id = _.uniqueId();
|
||||
this.element = this.createElement();
|
||||
this.element.attr(this.options.attrs).data(this.options.dataAttrs);
|
||||
this.children = [];
|
||||
this.delegateEvents();
|
||||
this.options.initialize.call(this, this);
|
||||
},
|
||||
destroy: function() {
|
||||
_.invoke(this.getChildren(), 'destroy');
|
||||
this.undelegateEvents();
|
||||
this.getElement().remove();
|
||||
},
|
||||
open: function() {
|
||||
this.getElement().addClass('is-opened');
|
||||
return this;
|
||||
},
|
||||
close: function() { },
|
||||
closeSiblings: function() {
|
||||
_.invoke(this.getSiblings(), 'close');
|
||||
return this;
|
||||
},
|
||||
getElement: function() {
|
||||
return this.element;
|
||||
},
|
||||
addChild: function(child) {
|
||||
var firstChild = null,
|
||||
lastChild = null;
|
||||
if (this.hasChildren()) {
|
||||
lastChild = this.getLastChild();
|
||||
lastChild.next = child;
|
||||
firstChild = this.getFirstChild();
|
||||
firstChild.prev = child;
|
||||
}
|
||||
child.parent = this;
|
||||
child.next = firstChild;
|
||||
child.prev = lastChild;
|
||||
this.children.push(child);
|
||||
return this;
|
||||
},
|
||||
getChildren: function() {
|
||||
// Returns the copy.
|
||||
return this.children.concat();
|
||||
},
|
||||
hasChildren: function() {
|
||||
return this.getChildren().length > 0;
|
||||
},
|
||||
getFirstChild: function() {
|
||||
return _.first(this.children);
|
||||
},
|
||||
getLastChild: function() {
|
||||
return _.last(this.children);
|
||||
},
|
||||
bindEvent: function(element, events, handler) {
|
||||
$(element).on(this.addNamespace(events), handler);
|
||||
return this;
|
||||
},
|
||||
getNext: function() {
|
||||
var item = this.next;
|
||||
while (item.isHidden() && this.id !== item.id) { item = item.next; }
|
||||
return item;
|
||||
},
|
||||
getPrev: function() {
|
||||
var item = this.prev;
|
||||
while (item.isHidden() && this.id !== item.id) { item = item.prev; }
|
||||
return item;
|
||||
},
|
||||
createElement: function() {
|
||||
return null;
|
||||
},
|
||||
getRoot: function() {
|
||||
var item = this;
|
||||
while (item.parent) { item = item.parent; }
|
||||
return item;
|
||||
},
|
||||
populateElement: function() { },
|
||||
focus: function() {
|
||||
this.getElement().focus();
|
||||
this.closeSiblings();
|
||||
return this;
|
||||
},
|
||||
isHidden: function() {
|
||||
return this.getElement().is(':hidden');
|
||||
},
|
||||
getSiblings: function() {
|
||||
var items = [],
|
||||
item = this;
|
||||
while (item.next && item.next.id !== this.id) {
|
||||
item = item.next;
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
select: function() { },
|
||||
unselect: function() { },
|
||||
setLabel: function() { },
|
||||
itemHandler: function() { },
|
||||
keyDownHandler: function() { },
|
||||
delegateEvents: function() { },
|
||||
undelegateEvents: function() {
|
||||
this.getElement().off('.' + this.id);
|
||||
},
|
||||
addNamespace: function(events) {
|
||||
return _.map(events.split(/\s+/), function(event) {
|
||||
return event + '.' + this.id;
|
||||
}, this).join(' ');
|
||||
}
|
||||
});
|
||||
|
||||
AbstractMenu = AbstractItem.extend({
|
||||
delegateEvents: function() {
|
||||
this.bindEvent(this.getElement(), 'keydown mouseleave mouseover', this.itemHandler.bind(this))
|
||||
.bindEvent(this.getElement(), 'contextmenu', function(event) { event.preventDefault(); });
|
||||
return this;
|
||||
},
|
||||
|
||||
populateElement: function() {
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
_.each(this.getChildren(), function(child) {
|
||||
fragment.appendChild(child.populateElement()[0]);
|
||||
}, this);
|
||||
|
||||
this.appendContent([fragment]);
|
||||
this.isRendered = true;
|
||||
return this.getElement();
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.closeChildren();
|
||||
this.getElement().removeClass('is-opened');
|
||||
return this;
|
||||
},
|
||||
|
||||
closeChildren: function() {
|
||||
_.invoke(this.getChildren(), 'close');
|
||||
return this;
|
||||
},
|
||||
|
||||
itemHandler: function(event) {
|
||||
event.preventDefault();
|
||||
var item = $(event.target).data('menu');
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
this.keyDownHandler.call(this, event, item);
|
||||
break;
|
||||
case 'mouseover':
|
||||
this.mouseOverHandler.call(this, event, item);
|
||||
break;
|
||||
case 'mouseleave':
|
||||
this.mouseLeaveHandler.call(this, event, item);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
keyDownHandler: function() { },
|
||||
mouseOverHandler: function() { },
|
||||
mouseLeaveHandler: function() { }
|
||||
});
|
||||
|
||||
Menu = AbstractMenu.extend({
|
||||
initialize: function(options, contextmenuElement, container) {
|
||||
this.contextmenuElement = $(contextmenuElement);
|
||||
this.container = $(container);
|
||||
this.overlay = this.getOverlay();
|
||||
AbstractMenu.prototype.initialize.apply(this, arguments);
|
||||
this.build(this, this.options.items);
|
||||
},
|
||||
|
||||
createElement: function() {
|
||||
return $('<ol />', {
|
||||
class: ['contextmenu', this.options.prefix + 'contextmenu'].join(' '),
|
||||
role: 'menu',
|
||||
tabindex: -1
|
||||
});
|
||||
},
|
||||
|
||||
delegateEvents: function() {
|
||||
AbstractMenu.prototype.delegateEvents.call(this);
|
||||
this.bindEvent(this.contextmenuElement, 'contextmenu', this.contextmenuHandler.bind(this))
|
||||
.bindEvent(window, 'resize', _.debounce(this.close.bind(this), 100));
|
||||
return this;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
AbstractMenu.prototype.destroy.call(this);
|
||||
this.overlay.destroy();
|
||||
this.contextmenuElement.removeData('contextmenu');
|
||||
return this;
|
||||
},
|
||||
|
||||
undelegateEvents: function() {
|
||||
AbstractMenu.prototype.undelegateEvents.call(this);
|
||||
this.contextmenuElement.off(this.addNamespace('contextmenu'));
|
||||
this.overlay.undelegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
appendContent: function(content) {
|
||||
var $content = $(content);
|
||||
this.getElement().append($content);
|
||||
return this;
|
||||
},
|
||||
|
||||
addChild: function() {
|
||||
AbstractMenu.prototype.addChild.apply(this, arguments);
|
||||
this.next = this.getFirstChild();
|
||||
this.prev = this.getLastChild();
|
||||
return this;
|
||||
},
|
||||
|
||||
build: function(container, items) {
|
||||
_.each(items, function(item) {
|
||||
var child;
|
||||
if (_.has(item, 'items')) {
|
||||
child = this.build((new Submenu(item, this.contextmenuElement)), item.items);
|
||||
} else {
|
||||
child = new MenuItem(item);
|
||||
}
|
||||
container.addChild(child);
|
||||
}, this);
|
||||
return container;
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
this.getElement().focus();
|
||||
return this;
|
||||
},
|
||||
|
||||
open: function() {
|
||||
var $menu = (this.isRendered) ? this.getElement() : this.populateElement();
|
||||
this.container.append($menu);
|
||||
AbstractItem.prototype.open.call(this);
|
||||
this.overlay.show(this.container);
|
||||
return this;
|
||||
},
|
||||
|
||||
close: function() {
|
||||
AbstractMenu.prototype.close.call(this);
|
||||
this.getElement().detach();
|
||||
this.overlay.hide();
|
||||
return this;
|
||||
},
|
||||
|
||||
position: function(event) {
|
||||
this.getElement().position({
|
||||
my: 'left top',
|
||||
of: event,
|
||||
collision: 'flipfit flipfit',
|
||||
within: this.contextmenuElement
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
pointInContainerBox: function(x, y) {
|
||||
var containerOffset = this.contextmenuElement.offset(),
|
||||
containerBox = {
|
||||
x0: containerOffset.left,
|
||||
y0: containerOffset.top,
|
||||
x1: containerOffset.left + this.contextmenuElement.outerWidth(),
|
||||
y1: containerOffset.top + this.contextmenuElement.outerHeight()
|
||||
};
|
||||
return containerBox.x0 <= x && x <= containerBox.x1 && containerBox.y0 <= y && y <= containerBox.y1;
|
||||
},
|
||||
|
||||
getOverlay: function() {
|
||||
return new Overlay(
|
||||
this.close.bind(this),
|
||||
function(event) {
|
||||
event.preventDefault();
|
||||
if (this.pointInContainerBox(event.pageX, event.pageY)) {
|
||||
this.position(event).focus();
|
||||
this.closeChildren();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
contextmenuHandler: function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.open().position(event).focus();
|
||||
},
|
||||
|
||||
keyDownHandler: function(event, item) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.UP:
|
||||
item.getPrev().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.DOWN:
|
||||
item.getNext().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.TAB:
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.ESCAPE:
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Overlay = Component.extend({
|
||||
ns: '.overlay',
|
||||
initialize: function(clickHandler, contextmenuHandler) {
|
||||
this.element = $('<div />', {
|
||||
class: 'overlay'
|
||||
});
|
||||
this.clickHandler = clickHandler;
|
||||
this.contextmenuHandler = contextmenuHandler;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.getElement().remove();
|
||||
this.undelegateEvents();
|
||||
},
|
||||
|
||||
getElement: function() {
|
||||
return this.element;
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.getElement().detach();
|
||||
this.undelegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function(container) {
|
||||
var $elem = $(this.getElement());
|
||||
$(container).append($elem);
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
|
||||
delegateEvents: function() {
|
||||
var self = this;
|
||||
$(document)
|
||||
.on('click' + this.ns, function() {
|
||||
if (_.isFunction(self.clickHandler)) {
|
||||
self.clickHandler.apply(this, arguments);
|
||||
}
|
||||
self.hide();
|
||||
})
|
||||
.on('contextmenu' + this.ns, function() {
|
||||
if (_.isFunction(self.contextmenuHandler)) {
|
||||
self.contextmenuHandler.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
undelegateEvents: function() {
|
||||
$(document).off(this.ns);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
Submenu = AbstractMenu.extend({
|
||||
initialize: function(options, contextmenuElement) {
|
||||
this.contextmenuElement = contextmenuElement;
|
||||
AbstractMenu.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
|
||||
createElement: function() {
|
||||
var $spanElem,
|
||||
$listElem,
|
||||
$element = $('<li />', {
|
||||
class: ['submenu-item', 'menu-item', this.options.prefix + 'submenu-item'].join(' '),
|
||||
'aria-expanded': 'false',
|
||||
'aria-haspopup': 'true',
|
||||
'aria-labelledby': 'submenu-item-label-' + this.id,
|
||||
role: 'menuitem',
|
||||
tabindex: -1
|
||||
});
|
||||
|
||||
$spanElem = $('<span />', {
|
||||
id: 'submenu-item-label-' + this.id,
|
||||
text: this.options.label
|
||||
});
|
||||
this.label = $spanElem.appendTo($element);
|
||||
|
||||
$listElem = $('<ol />', {
|
||||
class: ['submenu', this.options.prefix + 'submenu'].join(' '),
|
||||
role: 'menu'
|
||||
});
|
||||
|
||||
this.list = $listElem.appendTo($element);
|
||||
|
||||
return $element;
|
||||
},
|
||||
|
||||
appendContent: function(content) {
|
||||
var $content = $(content);
|
||||
this.list.append($content);
|
||||
return this;
|
||||
},
|
||||
|
||||
setLabel: function(label) {
|
||||
this.label.text(label);
|
||||
return this;
|
||||
},
|
||||
|
||||
openKeyboard: function() {
|
||||
if (this.hasChildren()) {
|
||||
this.open();
|
||||
this.getFirstChild().focus();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
keyDownHandler: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.LEFT:
|
||||
this.close().focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.RIGHT:
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.openKeyboard();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
open: function() {
|
||||
AbstractMenu.prototype.open.call(this);
|
||||
this.getElement().attr({'aria-expanded': 'true'});
|
||||
this.position();
|
||||
return this;
|
||||
},
|
||||
|
||||
close: function() {
|
||||
AbstractMenu.prototype.close.call(this);
|
||||
this.getElement().attr({'aria-expanded': 'false'});
|
||||
return this;
|
||||
},
|
||||
|
||||
position: function() {
|
||||
this.list.position({
|
||||
my: 'left top',
|
||||
at: 'right top',
|
||||
of: this.getElement(),
|
||||
collision: 'flipfit flipfit',
|
||||
within: this.contextmenuElement
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
mouseOverHandler: function() {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(this.open.bind(this), 200);
|
||||
this.focus();
|
||||
},
|
||||
|
||||
mouseLeaveHandler: function() {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(this.close.bind(this), 200);
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
|
||||
MenuItem = AbstractItem.extend({
|
||||
createElement: function() {
|
||||
var classNames = [
|
||||
'menu-item', this.options.prefix + 'menu-item',
|
||||
this.options.isSelected ? 'is-selected' : ''
|
||||
].join(' ');
|
||||
|
||||
return $('<li />', {
|
||||
class: classNames,
|
||||
'aria-selected': this.options.isSelected ? 'true' : 'false',
|
||||
role: 'menuitem',
|
||||
tabindex: -1,
|
||||
text: this.options.label
|
||||
});
|
||||
},
|
||||
|
||||
populateElement: function() {
|
||||
return this.getElement();
|
||||
},
|
||||
|
||||
delegateEvents: function() {
|
||||
this.bindEvent(this.getElement(), 'click keydown contextmenu mouseover', this.itemHandler.bind(this));
|
||||
return this;
|
||||
},
|
||||
|
||||
setLabel: function(label) {
|
||||
this.getElement().text(label);
|
||||
return this;
|
||||
},
|
||||
|
||||
select: function(event) {
|
||||
this.options.callback.call(this, event, this, this.options);
|
||||
this.getElement()
|
||||
.addClass('is-selected')
|
||||
.attr({'aria-selected': 'true'});
|
||||
_.invoke(this.getSiblings(), 'unselect');
|
||||
// Hide the menu.
|
||||
this.getRoot().close();
|
||||
return this;
|
||||
},
|
||||
|
||||
unselect: function() {
|
||||
this.getElement()
|
||||
.removeClass('is-selected')
|
||||
.attr({'aria-selected': 'false'});
|
||||
return this;
|
||||
},
|
||||
|
||||
itemHandler: function(event) {
|
||||
event.preventDefault();
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.type) {
|
||||
case 'contextmenu':
|
||||
case 'click':
|
||||
this.select();
|
||||
break;
|
||||
case 'mouseover':
|
||||
this.focus();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case 'keydown':
|
||||
this.keyDownHandler.call(this, event, this);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
keyDownHandler: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (keyCode) {
|
||||
case KEY.RIGHT:
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
this.select();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// VideoContextMenu() function - what this module 'exports'.
|
||||
return function(state, i18n) {
|
||||
var speedCallback = function(event, menuitem, options) {
|
||||
var speed = parseFloat(options.label);
|
||||
state.videoCommands.execute('speed', speed);
|
||||
},
|
||||
options = {
|
||||
items: [{
|
||||
label: i18n.Play,
|
||||
callback: function() {
|
||||
state.videoCommands.execute('togglePlayback');
|
||||
},
|
||||
initialize: function(menuitem) {
|
||||
state.el.on({
|
||||
play: function() {
|
||||
menuitem.setLabel(i18n.Pause);
|
||||
},
|
||||
pause: function() {
|
||||
menuitem.setLabel(i18n.Play);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: state.videoVolumeControl.getMuteStatus() ? i18n.Unmute : i18n.Mute,
|
||||
callback: function() {
|
||||
state.videoCommands.execute('toggleMute');
|
||||
},
|
||||
initialize: function(menuitem) {
|
||||
state.el.on({
|
||||
volumechange: function() {
|
||||
if (state.videoVolumeControl.getMuteStatus()) {
|
||||
menuitem.setLabel(i18n.Unmute);
|
||||
} else {
|
||||
menuitem.setLabel(i18n.Mute);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: i18n['Fill browser'],
|
||||
callback: function() {
|
||||
state.videoCommands.execute('toggleFullScreen');
|
||||
},
|
||||
initialize: function(menuitem) {
|
||||
state.el.on({
|
||||
fullscreen: function(event, isFullscreen) {
|
||||
if (isFullscreen) {
|
||||
menuitem.setLabel(i18n['Exit full browser']);
|
||||
} else {
|
||||
menuitem.setLabel(i18n['Fill browser']);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: i18n.Speed,
|
||||
items: _.map(state.speeds, function(speed) {
|
||||
var isSelected = parseFloat(speed) === state.speed;
|
||||
return {
|
||||
label: speed + 'x', callback: speedCallback, speed: speed, isSelected: isSelected
|
||||
};
|
||||
}),
|
||||
initialize: function(menuitem) {
|
||||
state.el.on({
|
||||
speedchange: function(event, speed) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
var item = menuitem.getChildren().filter(function(item) {
|
||||
return item.options.speed === speed;
|
||||
})[0];
|
||||
if (item) {
|
||||
item.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
$.fn.contextmenu = function(container, options) {
|
||||
return this.each(function() {
|
||||
$(this).data('contextmenu', new Menu(options, this, container));
|
||||
});
|
||||
};
|
||||
|
||||
if (!state.isYoutubeType()) {
|
||||
state.el.find('video').contextmenu(state.el, options);
|
||||
state.el.on('destroy', function() {
|
||||
var contextmenu = $(this).find('video').data('contextmenu');
|
||||
if (contextmenu) {
|
||||
contextmenu.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,112 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/09_bumper.js', [], function() {
|
||||
/**
|
||||
* VideoBumper module.
|
||||
* @exports video/09_bumper.js
|
||||
* @constructor
|
||||
* @param {Object} player The player factory.
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var VideoBumper = function(player, state) {
|
||||
if (!(this instanceof VideoBumper)) {
|
||||
return new VideoBumper(player, state);
|
||||
}
|
||||
|
||||
_.bindAll(
|
||||
this, 'showMainVideoHandler', 'destroy', 'skipByDuration', 'destroyAndResolve'
|
||||
);
|
||||
this.dfd = $.Deferred();
|
||||
this.element = state.el;
|
||||
this.element.addClass('is-bumper');
|
||||
this.player = player;
|
||||
this.state = state;
|
||||
this.doNotShowAgain = false;
|
||||
this.state.videoBumper = this;
|
||||
this.bindHandlers();
|
||||
this.initialize();
|
||||
this.maxBumperDuration = 35; // seconds
|
||||
};
|
||||
|
||||
VideoBumper.prototype = {
|
||||
initialize: function() {
|
||||
this.player();
|
||||
},
|
||||
|
||||
getPromise: function() {
|
||||
return this.dfd.promise();
|
||||
},
|
||||
|
||||
showMainVideoHandler: function() {
|
||||
this.state.storage.setItem('isBumperShown', true);
|
||||
setTimeout(function() {
|
||||
this.saveState();
|
||||
this.showMainVideo();
|
||||
}.bind(this), 20);
|
||||
},
|
||||
|
||||
destroyAndResolve: function() {
|
||||
this.destroy();
|
||||
this.dfd.resolve();
|
||||
},
|
||||
|
||||
showMainVideo: function() {
|
||||
if (this.state.videoPlayer) {
|
||||
this.destroyAndResolve();
|
||||
} else {
|
||||
this.state.el.on('initialize', this.destroyAndResolve);
|
||||
}
|
||||
},
|
||||
|
||||
skip: function() {
|
||||
this.element.trigger('skip', [this.doNotShowAgain]);
|
||||
this.showMainVideoHandler();
|
||||
},
|
||||
|
||||
skipAndDoNotShowAgain: function() {
|
||||
this.doNotShowAgain = true;
|
||||
this.skip();
|
||||
},
|
||||
|
||||
skipByDuration: function(event, time) {
|
||||
if (time > this.maxBumperDuration) {
|
||||
this.element.trigger('ended');
|
||||
}
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
var events = ['ended', 'error'].join(' ');
|
||||
this.element.on(events, this.showMainVideoHandler);
|
||||
this.element.on('timeupdate', this.skipByDuration);
|
||||
},
|
||||
|
||||
saveState: function() {
|
||||
var info = {bumper_last_view_date: true};
|
||||
if (this.doNotShowAgain) {
|
||||
_.extend(info, {bumper_do_not_show_again: true});
|
||||
}
|
||||
if (this.state.videoSaveStatePlugin) {
|
||||
this.state.videoSaveStatePlugin.saveState(true, info);
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
var events = ['ended', 'error'].join(' ');
|
||||
this.element.off(events, this.showMainVideoHandler);
|
||||
this.element.off({
|
||||
timeupdate: this.skipByDuration,
|
||||
initialize: this.destroyAndResolve
|
||||
});
|
||||
this.element.removeClass('is-bumper');
|
||||
if (_.isFunction(this.state.videoPlayer.destroy)) {
|
||||
this.state.videoPlayer.destroy();
|
||||
}
|
||||
delete this.state.videoBumper;
|
||||
}
|
||||
};
|
||||
|
||||
return VideoBumper;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,202 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Completion handler
|
||||
* @exports video/09_completion.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
define('video/09_completion.js', [], function() {
|
||||
var VideoCompletionHandler = function(state) {
|
||||
if (!(this instanceof VideoCompletionHandler)) {
|
||||
return new VideoCompletionHandler(state);
|
||||
}
|
||||
this.state = state;
|
||||
this.state.completionHandler = this;
|
||||
this.initialize();
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VideoCompletionHandler.prototype = {
|
||||
|
||||
/** Tears down the VideoCompletionHandler.
|
||||
*
|
||||
* * Removes backreferences from this.state to this.
|
||||
* * Turns off signal handlers.
|
||||
*/
|
||||
destroy: function() {
|
||||
this.el.remove();
|
||||
this.el.off('timeupdate.completion');
|
||||
this.el.off('ended.completion');
|
||||
delete this.state.completionHandler;
|
||||
},
|
||||
|
||||
/** Initializes the VideoCompletionHandler.
|
||||
*
|
||||
* This sets all the instance variables needed to perform
|
||||
* completion calculations.
|
||||
*/
|
||||
initialize: function() {
|
||||
// Attributes with "Time" in the name refer to the number of seconds since
|
||||
// the beginning of the video, except for lastSentTime, which refers to a
|
||||
// timestamp in seconds since the Unix epoch.
|
||||
this.lastSentTime = undefined;
|
||||
this.isComplete = false;
|
||||
this.completionPercentage = this.state.config.completionPercentage;
|
||||
this.startTime = this.state.config.startTime;
|
||||
this.endTime = this.state.config.endTime;
|
||||
this.isEnabled = this.state.config.completionEnabled;
|
||||
if (this.endTime) {
|
||||
this.completeAfterTime = this.calculateCompleteAfterTime(this.startTime, this.endTime);
|
||||
}
|
||||
if (this.isEnabled) {
|
||||
this.bindHandlers();
|
||||
}
|
||||
},
|
||||
|
||||
/** Bind event handler callbacks.
|
||||
*
|
||||
* When ended is triggered, mark the video complete
|
||||
* unconditionally.
|
||||
*
|
||||
* When timeupdate is triggered, check to see if the user has
|
||||
* passed the completeAfterTime in the video, and if so, mark the
|
||||
* video complete.
|
||||
*
|
||||
* When destroy is triggered, clean up outstanding resources.
|
||||
*/
|
||||
bindHandlers: function() {
|
||||
var self = this;
|
||||
|
||||
/** Event handler to check if the video is complete, and submit
|
||||
* a completion if it is.
|
||||
*
|
||||
* If the timeupdate handler doesn't fire after the required
|
||||
* percentage, this will catch any fully complete videos.
|
||||
*/
|
||||
this.state.el.on('ended.completion', function() {
|
||||
self.handleEnded();
|
||||
});
|
||||
|
||||
/** Event handler to check video progress, and mark complete if
|
||||
* greater than completionPercentage
|
||||
*/
|
||||
this.state.el.on('timeupdate.completion', function(ev, currentTime) {
|
||||
self.handleTimeUpdate(currentTime);
|
||||
});
|
||||
|
||||
/** Event handler to receive youtube metadata (if we even are a youtube link),
|
||||
* and mark complete, if youtube will insist on hosting the video itself.
|
||||
*/
|
||||
this.state.el.on('metadata_received', function() {
|
||||
self.checkMetadata();
|
||||
});
|
||||
|
||||
/** Event handler to clean up resources when the video player
|
||||
* is destroyed.
|
||||
*/
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
},
|
||||
|
||||
/** Handler to call when the ended event is triggered */
|
||||
handleEnded: function() {
|
||||
if (this.isComplete) {
|
||||
return;
|
||||
}
|
||||
this.markCompletion();
|
||||
},
|
||||
|
||||
/** Handler to call when a timeupdate event is triggered */
|
||||
handleTimeUpdate: function(currentTime) {
|
||||
var duration;
|
||||
if (this.isComplete) {
|
||||
return;
|
||||
}
|
||||
if (this.lastSentTime !== undefined && currentTime - this.lastSentTime < this.repostDelaySeconds()) {
|
||||
// Throttle attempts to submit in case of network issues
|
||||
return;
|
||||
}
|
||||
if (this.completeAfterTime === undefined) {
|
||||
// Duration is not available at initialization time
|
||||
duration = this.state.videoPlayer.duration();
|
||||
if (!duration) {
|
||||
// duration is not yet set. Wait for another event,
|
||||
// or fall back to 'ended' handler.
|
||||
return;
|
||||
}
|
||||
this.completeAfterTime = this.calculateCompleteAfterTime(this.startTime, duration);
|
||||
}
|
||||
|
||||
if (currentTime > this.completeAfterTime) {
|
||||
this.markCompletion(currentTime);
|
||||
}
|
||||
},
|
||||
|
||||
/** Handler to call when youtube metadata is received */
|
||||
checkMetadata: function() {
|
||||
var metadata = this.state.metadata[this.state.youtubeId()];
|
||||
|
||||
// https://developers.google.com/youtube/v3/docs/videos#contentDetails.contentRating.ytRating
|
||||
if (metadata && metadata.contentRating && metadata.contentRating.ytRating === 'ytAgeRestricted') {
|
||||
// Age-restricted videos won't play in embedded players. Instead, they ask you to watch it on
|
||||
// youtube itself. Which means we can't notice if they complete it. Rather than leaving an
|
||||
// incompletable video in the course, let's just mark it complete right now.
|
||||
if (!this.isComplete) {
|
||||
this.markCompletion();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** Submit completion to the LMS */
|
||||
markCompletion: function(currentTime) {
|
||||
var self = this;
|
||||
var errmsg;
|
||||
this.isComplete = true;
|
||||
this.lastSentTime = currentTime;
|
||||
this.state.el.trigger('complete');
|
||||
if (this.state.config.publishCompletionUrl) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: this.state.config.publishCompletionUrl,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({completion: 1.0}),
|
||||
success: function() {
|
||||
self.state.el.off('timeupdate.completion');
|
||||
self.state.el.off('ended.completion');
|
||||
},
|
||||
error: function(xhr) {
|
||||
/* eslint-disable no-console */
|
||||
self.complete = false;
|
||||
errmsg = 'Failed to submit completion';
|
||||
if (xhr.responseJSON !== undefined) {
|
||||
errmsg += ': ' + xhr.responseJSON.error;
|
||||
}
|
||||
console.warn(errmsg);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
});
|
||||
} else {
|
||||
/* eslint-disable no-console */
|
||||
console.warn('publishCompletionUrl not defined');
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
|
||||
/** Determine what point in the video (in seconds from the
|
||||
* beginning) counts as complete.
|
||||
*/
|
||||
calculateCompleteAfterTime: function(startTime, endTime) {
|
||||
return startTime + (endTime - startTime) * this.completionPercentage;
|
||||
},
|
||||
|
||||
/** How many seconds to wait after a POST fails to try again. */
|
||||
repostDelaySeconds: function() {
|
||||
return 3.0;
|
||||
}
|
||||
};
|
||||
return VideoCompletionHandler;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,113 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/09_events_bumper_plugin.js', [], function() {
|
||||
/**
|
||||
* Events module.
|
||||
* @exports video/09_events_bumper_plugin.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @param {Object} options
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var EventsBumperPlugin = function(state, i18n, options) {
|
||||
if (!(this instanceof EventsBumperPlugin)) {
|
||||
return new EventsBumperPlugin(state, i18n, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onReady', 'onPlay', 'onEnded', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip',
|
||||
'onShowCaptions', 'onHideCaptions', 'destroy');
|
||||
this.state = state;
|
||||
this.options = _.extend({}, options);
|
||||
this.state.videoEventsBumperPlugin = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
EventsBumperPlugin.moduleName = 'EventsBumperPlugin';
|
||||
EventsBumperPlugin.prototype = {
|
||||
destroy: function() {
|
||||
this.state.el.off(this.events);
|
||||
delete this.state.videoEventsBumperPlugin;
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.events = {
|
||||
ready: this.onReady,
|
||||
play: this.onPlay,
|
||||
'ended stop': this.onEnded,
|
||||
skip: this.onSkip,
|
||||
'language_menu:show': this.onShowLanguageMenu,
|
||||
'language_menu:hide': this.onHideLanguageMenu,
|
||||
'captions:show': this.onShowCaptions,
|
||||
'captions:hide': this.onHideCaptions,
|
||||
destroy: this.destroy
|
||||
};
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
this.state.el.on(this.events);
|
||||
},
|
||||
|
||||
onReady: function() {
|
||||
this.log('edx.video.bumper.loaded');
|
||||
},
|
||||
|
||||
onPlay: function() {
|
||||
this.log('edx.video.bumper.played', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onEnded: function() {
|
||||
this.log('edx.video.bumper.stopped', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onSkip: function(event, doNotShowAgain) {
|
||||
var info = {currentTime: this.getCurrentTime()},
|
||||
eventName = 'edx.video.bumper.' + (doNotShowAgain ? 'dismissed' : 'skipped');
|
||||
this.log(eventName, info);
|
||||
},
|
||||
|
||||
onShowLanguageMenu: function() {
|
||||
this.log('edx.video.bumper.transcript.menu.shown');
|
||||
},
|
||||
|
||||
onHideLanguageMenu: function() {
|
||||
this.log('edx.video.bumper.transcript.menu.hidden');
|
||||
},
|
||||
|
||||
onShowCaptions: function() {
|
||||
this.log('edx.video.bumper.transcript.shown', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onHideCaptions: function() {
|
||||
this.log('edx.video.bumper.transcript.hidden', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
getCurrentTime: function() {
|
||||
var player = this.state.videoPlayer;
|
||||
return player ? player.currentTime : 0;
|
||||
},
|
||||
|
||||
getDuration: function() {
|
||||
var player = this.state.videoPlayer;
|
||||
return player ? player.duration() : 0;
|
||||
},
|
||||
|
||||
log: function(eventName, data) {
|
||||
var logInfo = _.extend({
|
||||
host_component_id: this.state.id,
|
||||
bumper_id: this.state.config.sources[0] || '',
|
||||
duration: this.getDuration(),
|
||||
code: 'html5'
|
||||
}, data, this.options.data);
|
||||
Logger.log(eventName, logInfo);
|
||||
}
|
||||
};
|
||||
|
||||
return EventsBumperPlugin;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,179 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/09_events_plugin.js', [], function() {
|
||||
/**
|
||||
* Events module.
|
||||
* @exports video/09_events_plugin.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @param {Object} options
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var EventsPlugin = function(state, i18n, options) {
|
||||
if (!(this instanceof EventsPlugin)) {
|
||||
return new EventsPlugin(state, i18n, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onComplete', 'onEnded', 'onSeek',
|
||||
'onSpeedChange', 'onAutoAdvanceChange', 'onShowLanguageMenu', 'onHideLanguageMenu',
|
||||
'onSkip', 'onShowTranscript', 'onHideTranscript', 'onShowCaptions', 'onHideCaptions',
|
||||
'destroy');
|
||||
|
||||
this.state = state;
|
||||
this.options = _.extend({}, options);
|
||||
this.state.videoEventsPlugin = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
EventsPlugin.moduleName = 'EventsPlugin';
|
||||
EventsPlugin.prototype = {
|
||||
destroy: function() {
|
||||
this.state.el.off(this.events);
|
||||
delete this.state.videoEventsPlugin;
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.events = {
|
||||
ready: this.onReady,
|
||||
play: this.onPlay,
|
||||
pause: this.onPause,
|
||||
complete: this.onComplete,
|
||||
'ended stop': this.onEnded,
|
||||
seek: this.onSeek,
|
||||
skip: this.onSkip,
|
||||
speedchange: this.onSpeedChange,
|
||||
autoadvancechange: this.onAutoAdvanceChange,
|
||||
'language_menu:show': this.onShowLanguageMenu,
|
||||
'language_menu:hide': this.onHideLanguageMenu,
|
||||
'transcript:show': this.onShowTranscript,
|
||||
'transcript:hide': this.onHideTranscript,
|
||||
'captions:show': this.onShowCaptions,
|
||||
'captions:hide': this.onHideCaptions,
|
||||
destroy: this.destroy
|
||||
};
|
||||
this.bindHandlers();
|
||||
this.emitPlayVideoEvent = true;
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
this.state.el.on(this.events);
|
||||
},
|
||||
|
||||
onReady: function() {
|
||||
this.log('load_video');
|
||||
},
|
||||
|
||||
onPlay: function() {
|
||||
if (this.emitPlayVideoEvent) {
|
||||
this.log('play_video', {currentTime: this.getCurrentTime()});
|
||||
this.emitPlayVideoEvent = false;
|
||||
}
|
||||
},
|
||||
|
||||
onPause: function() {
|
||||
this.log('pause_video', {currentTime: this.getCurrentTime()});
|
||||
this.emitPlayVideoEvent = true;
|
||||
},
|
||||
|
||||
onComplete: function() {
|
||||
this.log('complete_video', {currentTime: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onEnded: function() {
|
||||
this.log('stop_video', {currentTime: this.getCurrentTime()});
|
||||
this.emitPlayVideoEvent = true;
|
||||
},
|
||||
|
||||
onSkip: function(event, doNotShowAgain) {
|
||||
var info = {currentTime: this.getCurrentTime()},
|
||||
eventName = doNotShowAgain ? 'do_not_show_again_video' : 'skip_video';
|
||||
this.log(eventName, info);
|
||||
},
|
||||
|
||||
onSeek: function(event, time, oldTime, type) {
|
||||
this.log('seek_video', {
|
||||
old_time: oldTime,
|
||||
new_time: time,
|
||||
type: type
|
||||
});
|
||||
this.emitPlayVideoEvent = true;
|
||||
},
|
||||
|
||||
onSpeedChange: function(event, newSpeed, oldSpeed) {
|
||||
this.log('speed_change_video', {
|
||||
current_time: this.getCurrentTime(),
|
||||
old_speed: this.state.speedToString(oldSpeed),
|
||||
new_speed: this.state.speedToString(newSpeed)
|
||||
});
|
||||
},
|
||||
|
||||
onAutoAdvanceChange: function(event, enabled) {
|
||||
this.log('auto_advance_change_video', {
|
||||
enabled: enabled
|
||||
});
|
||||
},
|
||||
|
||||
onShowLanguageMenu: function() {
|
||||
this.log('edx.video.language_menu.shown');
|
||||
},
|
||||
|
||||
onHideLanguageMenu: function() {
|
||||
this.log('edx.video.language_menu.hidden', {language: this.getCurrentLanguage()});
|
||||
},
|
||||
|
||||
onShowTranscript: function() {
|
||||
this.log('show_transcript', {current_time: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onHideTranscript: function() {
|
||||
this.log('hide_transcript', {current_time: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onShowCaptions: function() {
|
||||
this.log('edx.video.closed_captions.shown', {current_time: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
onHideCaptions: function() {
|
||||
this.log('edx.video.closed_captions.hidden', {current_time: this.getCurrentTime()});
|
||||
},
|
||||
|
||||
getCurrentTime: function() {
|
||||
var player = this.state.videoPlayer,
|
||||
startTime = this.state.config.startTime,
|
||||
currentTime;
|
||||
currentTime = player ? player.currentTime : 0;
|
||||
// if video didn't start from 0(it's a subsection of video), subtract the additional time at start
|
||||
if (startTime) {
|
||||
currentTime = currentTime ? currentTime - startTime : 0;
|
||||
}
|
||||
return currentTime;
|
||||
},
|
||||
|
||||
getCurrentLanguage: function() {
|
||||
var language = this.state.lang;
|
||||
return language;
|
||||
},
|
||||
|
||||
log: function(eventName, data) {
|
||||
// use startTime and endTime to calculate the duration to handle the case where only a subsection of video is used
|
||||
var endTime = this.state.config.endTime || this.state.duration,
|
||||
startTime = this.state.config.startTime;
|
||||
|
||||
var logInfo = _.extend({
|
||||
id: this.state.id,
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
code: this.state.isYoutubeType() ? this.state.youtubeId() : this.state.canPlayHLS ? 'hls' : 'html5',
|
||||
duration: endTime - startTime
|
||||
}, data, this.options.data);
|
||||
Logger.log(eventName, logInfo);
|
||||
}
|
||||
};
|
||||
|
||||
return EventsPlugin;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,97 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/09_play_pause_control.js', [], function() {
|
||||
/**
|
||||
* Play/pause control module.
|
||||
* @exports video/09_play_pause_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var PlayPauseControl = function(state, i18n) {
|
||||
if (!(this instanceof PlayPauseControl)) {
|
||||
return new PlayPauseControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'play', 'pause', 'onClick', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoPlayPauseControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
PlayPauseControl.prototype = {
|
||||
template: [
|
||||
'<button class="control video_control play" aria-disabled="false" aria-label="',
|
||||
gettext('Play'),
|
||||
'">',
|
||||
'<span class="icon fa fa-play" aria-hidden="true"></span>',
|
||||
'</button>'
|
||||
].join(''),
|
||||
|
||||
destroy: function() {
|
||||
this.el.remove();
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
delete this.state.videoPlayPauseControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.el = $(this.template);
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
this.state.el.find('.vcr').prepend(this.el);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on({
|
||||
click: this.onClick
|
||||
});
|
||||
this.state.el.on({
|
||||
play: this.play,
|
||||
'pause ended': this.pause,
|
||||
destroy: this.destroy
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(event) {
|
||||
event.preventDefault();
|
||||
this.state.videoCommands.execute('togglePlayback');
|
||||
},
|
||||
|
||||
play: function() {
|
||||
this.el
|
||||
.addClass('pause')
|
||||
.removeClass('play')
|
||||
.attr({title: gettext('Pause'), 'aria-label': gettext('Pause')})
|
||||
.find('.icon')
|
||||
.removeClass('fa-play')
|
||||
.addClass('fa-pause');
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
this.el
|
||||
.removeClass('pause')
|
||||
.addClass('play')
|
||||
.attr({title: gettext('Play'), 'aria-label': gettext('Play')})
|
||||
.find('.icon')
|
||||
.removeClass('fa-pause')
|
||||
.addClass('fa-play');
|
||||
}
|
||||
};
|
||||
|
||||
return PlayPauseControl;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,88 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/09_play_placeholder.js', [], function() {
|
||||
/**
|
||||
* Play placeholder control module.
|
||||
* @exports video/09_play_placeholder.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var PlayPlaceholder = function(state, i18n) {
|
||||
if (!(this instanceof PlayPlaceholder)) {
|
||||
return new PlayPlaceholder(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onClick', 'hide', 'show', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoPlayPlaceholder = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
PlayPlaceholder.prototype = {
|
||||
destroy: function() {
|
||||
this.el.off('click', this.onClick);
|
||||
this.state.el.on({
|
||||
destroy: this.destroy,
|
||||
play: this.hide,
|
||||
'ended pause': this.show
|
||||
});
|
||||
this.hide();
|
||||
delete this.state.videoPlayPlaceholder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates whether the placeholder should be shown. We display it
|
||||
* for html5 videos on iPad and Android devices.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
shouldBeShown: function() {
|
||||
return /iPad|Android/i.test(this.state.isTouch[0]) && !this.state.isYoutubeType();
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
if (!this.shouldBeShown()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.el = this.state.el.find('.btn-play');
|
||||
this.bindHandlers();
|
||||
this.show();
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.state.el.on({
|
||||
destroy: this.destroy,
|
||||
play: this.hide,
|
||||
'ended pause': this.show
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
this.state.videoCommands.execute('play');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.el
|
||||
.addClass('is-hidden')
|
||||
.attr({'aria-hidden': 'true', tabindex: -1});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.el
|
||||
.removeClass('is-hidden')
|
||||
.attr({'aria-hidden': 'false', tabindex: 0});
|
||||
}
|
||||
};
|
||||
|
||||
return PlayPlaceholder;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,90 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/09_play_skip_control.js', [], function() {
|
||||
/**
|
||||
* Play/skip control module.
|
||||
* @exports video/09_play_skip_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var PlaySkipControl = function(state, i18n) {
|
||||
if (!(this instanceof PlaySkipControl)) {
|
||||
return new PlaySkipControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'play', 'onClick', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoPlaySkipControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
PlaySkipControl.prototype = {
|
||||
template: [
|
||||
'<button class="control video_control play play-skip-control" title="',
|
||||
gettext('Play'),
|
||||
'">',
|
||||
'<span class="icon fa fa-play" aria-hidden="true"></span>',
|
||||
'</button>'
|
||||
].join(''),
|
||||
|
||||
destroy: function() {
|
||||
this.el.remove();
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
delete this.state.videoPlaySkipControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.el = $(this.template);
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
this.state.el.find('.vcr').prepend(this.el);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.state.el.on({
|
||||
play: this.play,
|
||||
destroy: this.destroy
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(event) {
|
||||
event.preventDefault();
|
||||
if (this.state.videoPlayer.isPlaying()) {
|
||||
this.state.videoCommands.execute('skip');
|
||||
} else {
|
||||
this.state.videoCommands.execute('play');
|
||||
}
|
||||
},
|
||||
|
||||
play: function() {
|
||||
this.el
|
||||
.removeClass('play')
|
||||
.addClass('skip')
|
||||
.attr('title', gettext('Skip'))
|
||||
.find('.icon')
|
||||
.removeClass('fa-play')
|
||||
.addClass('fa-step-forward');
|
||||
// Disable possibility to pause the video.
|
||||
this.state.el.find('video').off('click');
|
||||
}
|
||||
};
|
||||
|
||||
return PlaySkipControl;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,70 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/09_poster.js', [], function() {
|
||||
/**
|
||||
* Poster module.
|
||||
* @exports video/09_poster.js
|
||||
* @constructor
|
||||
* @param {jquery Element} element
|
||||
* @param {Object} options
|
||||
*/
|
||||
var VideoPoster = function(element, options) {
|
||||
if (!(this instanceof VideoPoster)) {
|
||||
return new VideoPoster(element, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onClick', 'destroy');
|
||||
this.element = element;
|
||||
this.container = element.find('.video-player');
|
||||
this.options = options || {};
|
||||
this.initialize();
|
||||
};
|
||||
|
||||
VideoPoster.moduleName = 'Poster';
|
||||
VideoPoster.prototype = {
|
||||
template: _.template([
|
||||
'<div class="video-pre-roll is-<%- type %> poster" ',
|
||||
'style="background-image: url(<%- url %>)">',
|
||||
'<button class="btn-play btn-pre-roll">',
|
||||
'<img src="/static/images/play.png" alt="">',
|
||||
'<span class="sr">', gettext('Play video'), '</span>',
|
||||
'</button>',
|
||||
'</div>'
|
||||
].join('')),
|
||||
|
||||
initialize: function() {
|
||||
this.el = $(this.template({
|
||||
url: this.options.poster.url,
|
||||
type: this.options.poster.type
|
||||
}));
|
||||
this.element.addClass('is-pre-roll');
|
||||
this.render();
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.element.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.container.append(this.el);
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
if (_.isFunction(this.options.onClick)) {
|
||||
this.options.onClick();
|
||||
}
|
||||
this.destroy();
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.element.off('destroy', this.destroy).removeClass('is-pre-roll');
|
||||
this.el.remove();
|
||||
}
|
||||
};
|
||||
|
||||
return VideoPoster;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,131 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/09_save_state_plugin.js', ['underscore', 'time.js'], function(_, Time) {
|
||||
/**
|
||||
* Save state module.
|
||||
* @exports video/09_save_state_plugin.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @param {Object} options
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var SaveStatePlugin = function(state, i18n, options) {
|
||||
if (!(this instanceof SaveStatePlugin)) {
|
||||
return new SaveStatePlugin(state, i18n, options);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onSpeedChange', 'onAutoAdvanceChange', 'saveStateHandler', 'bindUnloadHandler', 'onUnload',
|
||||
'onYoutubeAvailability', 'onLanguageChange', 'destroy');
|
||||
this.state = state;
|
||||
this.options = _.extend({events: []}, options);
|
||||
this.state.videoSaveStatePlugin = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
SaveStatePlugin.moduleName = 'SaveStatePlugin';
|
||||
SaveStatePlugin.prototype = {
|
||||
destroy: function() {
|
||||
this.state.el.off(this.events).off('destroy', this.destroy);
|
||||
$(window).off('unload', this.onUnload);
|
||||
delete this.state.videoSaveStatePlugin;
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.events = {
|
||||
speedchange: this.onSpeedChange,
|
||||
autoadvancechange: this.onAutoAdvanceChange,
|
||||
play: this.bindUnloadHandler,
|
||||
'pause destroy': this.saveStateHandler,
|
||||
'language_menu:change': this.onLanguageChange,
|
||||
youtube_availability: this.onYoutubeAvailability
|
||||
};
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
bindHandlers: function() {
|
||||
if (this.options.events.length) {
|
||||
_.each(this.options.events, function(eventName) {
|
||||
var callback;
|
||||
if (_.has(this.events, eventName)) {
|
||||
callback = this.events[eventName];
|
||||
this.state.el.on(eventName, callback);
|
||||
}
|
||||
}, this);
|
||||
} else {
|
||||
this.state.el.on(this.events);
|
||||
}
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
bindUnloadHandler: _.once(function() {
|
||||
$(window).on('unload.video', this.onUnload);
|
||||
}),
|
||||
|
||||
onSpeedChange: function(event, newSpeed) {
|
||||
this.saveState(true, {speed: newSpeed});
|
||||
this.state.storage.setItem('speed', newSpeed, true);
|
||||
this.state.storage.setItem('general_speed', newSpeed);
|
||||
},
|
||||
|
||||
onAutoAdvanceChange: function(event, enabled) {
|
||||
this.saveState(true, {auto_advance: enabled});
|
||||
this.state.storage.setItem('auto_advance', enabled);
|
||||
},
|
||||
|
||||
saveStateHandler: function() {
|
||||
this.saveState(true);
|
||||
},
|
||||
|
||||
onUnload: function() {
|
||||
this.saveState();
|
||||
},
|
||||
|
||||
onLanguageChange: function(event, langCode) {
|
||||
this.state.storage.setItem('language', langCode);
|
||||
},
|
||||
|
||||
onYoutubeAvailability: function(event, youtubeIsAvailable) {
|
||||
// Compare what the client-side code has determined Youtube
|
||||
// availability to be (true/false) vs. what the LMS recorded for
|
||||
// this user. The LMS will assume YouTube is available by default.
|
||||
if (youtubeIsAvailable !== this.state.config.recordedYoutubeIsAvailable) {
|
||||
this.saveState(true, {youtube_is_available: youtubeIsAvailable});
|
||||
}
|
||||
},
|
||||
|
||||
saveState: function(async, data) {
|
||||
if (this.state.config.saveStateEnabled) {
|
||||
if (!($.isPlainObject(data))) {
|
||||
data = {
|
||||
saved_video_position: this.state.videoPlayer.currentTime
|
||||
};
|
||||
}
|
||||
|
||||
if (data.speed) {
|
||||
this.state.storage.setItem('speed', data.speed, true);
|
||||
}
|
||||
|
||||
if (_.has(data, 'saved_video_position')) {
|
||||
this.state.storage.setItem('savedVideoPosition', data.saved_video_position, true);
|
||||
data.saved_video_position = Time.formatFull(data.saved_video_position);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: this.state.config.saveStateUrl,
|
||||
type: 'POST',
|
||||
async: !!async,
|
||||
dataType: 'json',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return SaveStatePlugin;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,76 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
// VideoSkipControl module.
|
||||
define(
|
||||
'video/09_skip_control.js', [],
|
||||
function() {
|
||||
/**
|
||||
* Video skip control module.
|
||||
* @exports video/09_skip_control.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
var SkipControl = function(state, i18n) {
|
||||
if (!(this instanceof SkipControl)) {
|
||||
return new SkipControl(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'onClick', 'render', 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoSkipControl = this;
|
||||
this.i18n = i18n;
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
SkipControl.prototype = {
|
||||
template: [
|
||||
'<button class="control video_control skip skip-control" aria-disabled="false" title="',
|
||||
gettext('Do not show again'),
|
||||
'">',
|
||||
'<span class="icon fa fa-step-forward" aria-hidden="true"></span>',
|
||||
'</button>'
|
||||
].join(''),
|
||||
|
||||
destroy: function() {
|
||||
this.el.remove();
|
||||
this.state.el.off('.skip');
|
||||
delete this.state.videoSkipControl;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.el = $(this.template);
|
||||
this.bindHandlers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates any necessary DOM elements, attach them, and set their,
|
||||
* initial configuration.
|
||||
*/
|
||||
render: function() {
|
||||
this.state.el.find('.vcr .control').after(this.el);
|
||||
},
|
||||
|
||||
/** Bind any necessary function callbacks to DOM events. */
|
||||
bindHandlers: function() {
|
||||
this.el.on('click', this.onClick);
|
||||
this.state.el.on({
|
||||
'play.skip': _.once(this.render),
|
||||
'destroy.skip': this.destroy
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(event) {
|
||||
event.preventDefault();
|
||||
this.state.videoCommands.execute('skip', true);
|
||||
}
|
||||
};
|
||||
|
||||
return SkipControl;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,1463 +0,0 @@
|
||||
(function(define) {
|
||||
// VideoCaption module.
|
||||
|
||||
'use strict';
|
||||
|
||||
define('video/09_video_caption.js', [
|
||||
'video/00_sjson.js',
|
||||
'video/00_async_process.js',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'draggabilly',
|
||||
'time.js',
|
||||
'underscore'
|
||||
], function(Sjson, AsyncProcess, HtmlUtils, Draggabilly, Time, _) {
|
||||
/**
|
||||
* @desc VideoCaption module exports a function.
|
||||
*
|
||||
* @type {function}
|
||||
* @access public
|
||||
*
|
||||
* @param {object} state - The object containing the state of the video
|
||||
* player. All other modules, their parameters, public variables, etc.
|
||||
* are available via this object.
|
||||
*
|
||||
* @this {object} The global window object.
|
||||
*
|
||||
* @returns {jquery Promise}
|
||||
*/
|
||||
var VideoCaption = function(state) {
|
||||
if (!(this instanceof VideoCaption)) {
|
||||
return new VideoCaption(state);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'toggleTranscript', 'onMouseEnter', 'onMouseLeave', 'onMovement',
|
||||
'onContainerMouseEnter', 'onContainerMouseLeave', 'fetchCaption',
|
||||
'onResize', 'pause', 'play', 'onCaptionUpdate', 'onCaptionHandler', 'destroy',
|
||||
'handleKeypress', 'handleKeypressLink', 'openLanguageMenu', 'closeLanguageMenu',
|
||||
'previousLanguageMenuItem', 'nextLanguageMenuItem', 'handleCaptionToggle',
|
||||
'showClosedCaptions', 'hideClosedCaptions', 'toggleClosedCaptions',
|
||||
'updateCaptioningCookie', 'handleCaptioningCookie', 'handleTranscriptToggle',
|
||||
'listenForDragDrop', 'setTranscriptVisibility', 'updateTranscriptCookie',
|
||||
'updateGoogleDisclaimer', 'toggleGoogleDisclaimer', 'updateProblematicCaptionsContent'
|
||||
);
|
||||
|
||||
this.state = state;
|
||||
this.state.videoCaption = this;
|
||||
this.renderElements();
|
||||
this.handleCaptioningCookie();
|
||||
this.setTranscriptVisibility();
|
||||
this.listenForDragDrop();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VideoCaption.prototype = {
|
||||
|
||||
destroy: function() {
|
||||
this.state.el
|
||||
.off({
|
||||
'caption:fetch': this.fetchCaption,
|
||||
'caption:resize': this.onResize,
|
||||
'caption:update': this.onCaptionUpdate,
|
||||
ended: this.pause,
|
||||
fullscreen: this.onResize,
|
||||
pause: this.pause,
|
||||
play: this.play,
|
||||
destroy: this.destroy
|
||||
})
|
||||
.removeClass('is-captions-rendered');
|
||||
if (this.fetchXHR && this.fetchXHR.abort) {
|
||||
this.fetchXHR.abort();
|
||||
}
|
||||
if (this.availableTranslationsXHR && this.availableTranslationsXHR.abort) {
|
||||
this.availableTranslationsXHR.abort();
|
||||
}
|
||||
this.subtitlesEl.remove();
|
||||
this.container.remove();
|
||||
delete this.state.videoCaption;
|
||||
},
|
||||
/**
|
||||
* @desc Initiate rendering of elements, and set their initial configuration.
|
||||
*
|
||||
*/
|
||||
renderElements: function() {
|
||||
var languages = this.state.config.transcriptLanguages;
|
||||
|
||||
var langHtml = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<div class="grouped-controls">',
|
||||
'<button class="control toggle-captions" aria-disabled="false">',
|
||||
'<span class="icon fa fa-cc" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'<button class="control toggle-transcript" aria-disabled="false">',
|
||||
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'<div class="lang menu-container" role="application">',
|
||||
'<p class="sr instructions" id="lang-instructions-{courseId}"></p>',
|
||||
'<button class="control language-menu" aria-disabled="false"',
|
||||
'aria-describedby="lang-instructions-{courseId}" ',
|
||||
'title="{langTitle}">',
|
||||
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join('')),
|
||||
{
|
||||
langTitle: gettext('Open language menu'),
|
||||
courseId: this.state.id
|
||||
}
|
||||
);
|
||||
|
||||
var subtitlesHtml = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
[
|
||||
'<div class="subtitles" role="region" id="transcript-{courseId}">',
|
||||
'<h3 id="transcript-label-{courseId}" class="transcript-title sr"></h3>',
|
||||
'<ol id="transcript-captions-{courseId}" class="subtitles-menu" lang="{courseLang}"></ol>',
|
||||
'</div>'
|
||||
].join('')),
|
||||
{
|
||||
courseId: this.state.id,
|
||||
courseLang: this.state.lang
|
||||
}
|
||||
);
|
||||
|
||||
this.loaded = false;
|
||||
this.subtitlesEl = $(HtmlUtils.ensureHtml(subtitlesHtml).toString());
|
||||
this.subtitlesMenuEl = this.subtitlesEl.find('.subtitles-menu');
|
||||
this.container = $(HtmlUtils.ensureHtml(langHtml).toString());
|
||||
this.captionControlEl = this.container.find('.toggle-captions');
|
||||
this.captionDisplayEl = this.state.el.find('.closed-captions');
|
||||
this.transcriptControlEl = this.container.find('.toggle-transcript');
|
||||
this.languageChooserEl = this.container.find('.lang');
|
||||
this.menuChooserEl = this.languageChooserEl.parent();
|
||||
|
||||
if (_.keys(languages).length) {
|
||||
this.renderLanguageMenu(languages);
|
||||
this.fetchCaption();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Bind any necessary function callbacks to DOM events (click,
|
||||
* mousemove, etc.).
|
||||
*
|
||||
*/
|
||||
bindHandlers: function() {
|
||||
var state = this.state,
|
||||
events = [
|
||||
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
|
||||
'keydown'
|
||||
].join(' ');
|
||||
|
||||
this.captionControlEl.on({
|
||||
click: this.toggleClosedCaptions,
|
||||
keydown: this.handleCaptionToggle
|
||||
});
|
||||
this.transcriptControlEl.on({
|
||||
click: this.toggleTranscript,
|
||||
keydown: this.handleTranscriptToggle
|
||||
});
|
||||
this.subtitlesMenuEl.on({
|
||||
mouseenter: this.onMouseEnter,
|
||||
mouseleave: this.onMouseLeave,
|
||||
mousemove: this.onMovement,
|
||||
mousewheel: this.onMovement,
|
||||
DOMMouseScroll: this.onMovement
|
||||
})
|
||||
.on(events, 'span[data-index]', this.onCaptionHandler);
|
||||
this.container.on({
|
||||
mouseenter: this.onContainerMouseEnter,
|
||||
mouseleave: this.onContainerMouseLeave
|
||||
});
|
||||
|
||||
if (this.showLanguageMenu) {
|
||||
this.languageChooserEl.on({
|
||||
keydown: this.handleKeypress
|
||||
}, '.language-menu');
|
||||
|
||||
this.languageChooserEl.on({
|
||||
keydown: this.handleKeypressLink
|
||||
}, '.control-lang');
|
||||
}
|
||||
|
||||
state.el
|
||||
.on({
|
||||
'caption:fetch': this.fetchCaption,
|
||||
'caption:resize': this.onResize,
|
||||
'caption:update': this.onCaptionUpdate,
|
||||
ended: this.pause,
|
||||
fullscreen: this.onResize,
|
||||
pause: this.pause,
|
||||
play: this.play,
|
||||
destroy: this.destroy
|
||||
});
|
||||
|
||||
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
|
||||
this.subtitlesMenuEl.on('scroll', state.videoControl.showControls);
|
||||
}
|
||||
},
|
||||
|
||||
onCaptionUpdate: function(event, time) {
|
||||
this.updatePlayTime(time);
|
||||
},
|
||||
|
||||
handleCaptionToggle: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.SPACE:
|
||||
case KEY.ENTER:
|
||||
event.preventDefault();
|
||||
this.toggleClosedCaptions(event);
|
||||
// no default
|
||||
}
|
||||
},
|
||||
|
||||
handleTranscriptToggle: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.SPACE:
|
||||
case KEY.ENTER:
|
||||
event.preventDefault();
|
||||
this.toggleTranscript(event);
|
||||
// no default
|
||||
}
|
||||
},
|
||||
|
||||
handleKeypressLink: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode,
|
||||
focused, index, total;
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY.UP:
|
||||
event.preventDefault();
|
||||
focused = $(':focus').parent();
|
||||
index = this.languageChooserEl.find('li').index(focused);
|
||||
total = this.languageChooserEl.find('li').size() - 1;
|
||||
|
||||
this.previousLanguageMenuItem(event, index);
|
||||
break;
|
||||
|
||||
case KEY.DOWN:
|
||||
event.preventDefault();
|
||||
focused = $(':focus').parent();
|
||||
index = this.languageChooserEl.find('li').index(focused);
|
||||
total = this.languageChooserEl.find('li').size() - 1;
|
||||
|
||||
this.nextLanguageMenuItem(event, index, total);
|
||||
break;
|
||||
|
||||
case KEY.ESCAPE:
|
||||
this.closeLanguageMenu(event);
|
||||
break;
|
||||
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
return true;
|
||||
// no default
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
handleKeypress: function(event) {
|
||||
var KEY = $.ui.keyCode,
|
||||
keyCode = event.keyCode;
|
||||
|
||||
switch (keyCode) {
|
||||
// Handle keypresses
|
||||
case KEY.ENTER:
|
||||
case KEY.SPACE:
|
||||
case KEY.UP:
|
||||
event.preventDefault();
|
||||
this.openLanguageMenu(event);
|
||||
break;
|
||||
|
||||
case KEY.ESCAPE:
|
||||
this.closeLanguageMenu(event);
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
return event.keyCode === KEY.TAB;
|
||||
},
|
||||
|
||||
nextLanguageMenuItem: function(event, index, total) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.altKey || event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index === total) {
|
||||
this.languageChooserEl
|
||||
.find('.control-lang').first()
|
||||
.focus();
|
||||
} else {
|
||||
this.languageChooserEl
|
||||
.find('li:eq(' + index + ')')
|
||||
.next()
|
||||
.find('.control-lang')
|
||||
.focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
previousLanguageMenuItem: function(event, index) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.altKey || event.shiftKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
this.languageChooserEl
|
||||
.find('.control-lang').last()
|
||||
.focus();
|
||||
} else {
|
||||
this.languageChooserEl
|
||||
.find('li:eq(' + index + ')')
|
||||
.prev()
|
||||
.find('.control-lang')
|
||||
.focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
openLanguageMenu: function(event) {
|
||||
var button = this.languageChooserEl,
|
||||
menu = button.parent().find('.menu');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
button
|
||||
.addClass('is-opened');
|
||||
|
||||
menu
|
||||
.find('.control-lang').last()
|
||||
.focus();
|
||||
},
|
||||
|
||||
closeLanguageMenu: function(event) {
|
||||
var button = this.languageChooserEl;
|
||||
event.preventDefault();
|
||||
|
||||
button
|
||||
.removeClass('is-opened')
|
||||
.find('.language-menu')
|
||||
.focus();
|
||||
},
|
||||
|
||||
onCaptionHandler: function(event) {
|
||||
switch (event.type) {
|
||||
case 'mouseover':
|
||||
case 'mouseout':
|
||||
this.captionMouseOverOut(event);
|
||||
break;
|
||||
case 'mousedown':
|
||||
this.captionMouseDown(event);
|
||||
break;
|
||||
case 'click':
|
||||
this.captionClick(event);
|
||||
break;
|
||||
case 'focusin':
|
||||
this.captionFocus(event);
|
||||
break;
|
||||
case 'focusout':
|
||||
this.captionBlur(event);
|
||||
break;
|
||||
case 'keydown':
|
||||
this.captionKeyDown(event);
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Opens language menu.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onContainerMouseEnter: function(event) {
|
||||
event.preventDefault();
|
||||
$(event.currentTarget).find('.lang').addClass('is-opened');
|
||||
|
||||
// We only want to fire the analytics event if a menu is
|
||||
// present instead of on the container hover, since it wraps
|
||||
// the "CC" and "Transcript" buttons as well.
|
||||
if ($(event.currentTarget).find('.lang').length) {
|
||||
this.state.el.trigger('language_menu:show');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Closes language menu.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onContainerMouseLeave: function(event) {
|
||||
event.preventDefault();
|
||||
$(event.currentTarget).find('.lang').removeClass('is-opened');
|
||||
|
||||
// We only want to fire the analytics event if a menu is
|
||||
// present instead of on the container hover, since it wraps
|
||||
// the "CC" and "Transcript" buttons as well.
|
||||
if ($(event.currentTarget).find('.lang').length) {
|
||||
this.state.el.trigger('language_menu:hide');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Freezes moving of captions when mouse is over them.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMouseEnter: function() {
|
||||
if (this.frozen) {
|
||||
clearTimeout(this.frozen);
|
||||
}
|
||||
|
||||
this.frozen = setTimeout(
|
||||
this.onMouseLeave,
|
||||
this.state.config.captionsFreezeTime
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Unfreezes moving of captions when mouse go out.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMouseLeave: function() {
|
||||
if (this.frozen) {
|
||||
clearTimeout(this.frozen);
|
||||
}
|
||||
|
||||
this.frozen = null;
|
||||
|
||||
if (this.playing) {
|
||||
this.scrollCaption();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Freezes moving of captions when mouse is moving over them.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*/
|
||||
onMovement: function() {
|
||||
this.onMouseEnter();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Gets the correct start and end times from the state configuration
|
||||
*
|
||||
* @returns {array} if [startTime, endTime] are defined
|
||||
*/
|
||||
getStartEndTimes: function() {
|
||||
// due to the way config.startTime/endTime are
|
||||
// processed in 03_video_player.js, we assume
|
||||
// endTime can be an integer or null,
|
||||
// and startTime is an integer > 0
|
||||
var config = this.state.config;
|
||||
var startTime = config.startTime * 1000;
|
||||
var endTime = (config.endTime !== null) ? config.endTime * 1000 : null;
|
||||
return [startTime, endTime];
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Gets captions within the start / end times stored within this.state.config
|
||||
*
|
||||
* @returns {object} {start, captions} parallel arrays of
|
||||
* start times and corresponding captions
|
||||
*/
|
||||
getBoundedCaptions: function() {
|
||||
// get start and caption. If startTime and endTime
|
||||
// are specified, filter by that range.
|
||||
var times = this.getStartEndTimes();
|
||||
// eslint-disable-next-line prefer-spread
|
||||
var results = this.sjson.filter.apply(this.sjson, times);
|
||||
var start = results.start;
|
||||
var captions = results.captions;
|
||||
|
||||
return {
|
||||
start: start,
|
||||
captions: captions
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sets whether or not the Google disclaimer should be shown based on captions
|
||||
* being AI generated, and shows/hides based on the above and if ClosedCaptions are being shown.
|
||||
*
|
||||
* @param {array} captions List of captions for the video.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
updateGoogleDisclaimer: function(captions) {
|
||||
const aIGeneratedSpanText = '<span id="captions-ai-generated"',
|
||||
aiProviderRegexp = /data\-provider=["'](?<provider>\w+)["']/;
|
||||
var self = this,
|
||||
state = this.state,
|
||||
aiGeneratedSpan = captions.find(caption => caption.includes(aIGeneratedSpanText)),
|
||||
captionsAIGenerated = !(aiGeneratedSpan === undefined),
|
||||
aiCaptionProviderIsGoogle = true;
|
||||
|
||||
if (captionsAIGenerated) {
|
||||
const providerMatch = aiProviderRegexp.exec(aiGeneratedSpan);
|
||||
if (providerMatch !== null) {
|
||||
aiCaptionProviderIsGoogle = providerMatch.groups['provider'] === 'gcp';
|
||||
}
|
||||
// If there is no provider tag, it was generated before we added those,
|
||||
// so it must be Google
|
||||
}
|
||||
// This field is whether or not, in general, this video should show the google disclaimer
|
||||
self.shouldShowGoogleDisclaimer = captionsAIGenerated && aiCaptionProviderIsGoogle;
|
||||
// Should we, right now, on load, show the google disclaimer
|
||||
self.toggleGoogleDisclaimer(!self.hideCaptionsOnLoad && !state.captionsHidden);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Show or hide the google translate disclaimer based on the passed param
|
||||
* and whether or not we are currently showing a google translated transcript.
|
||||
* @param {boolean} [show] Show if true, hide if false - if we are showing a google
|
||||
* translated transcript. If not, this will always hide.
|
||||
*/
|
||||
toggleGoogleDisclaimer: function(show) {
|
||||
var self = this,
|
||||
state = this.state;
|
||||
if (show && self.shouldShowGoogleDisclaimer) {
|
||||
state.el.find('.google-disclaimer').show();
|
||||
} else {
|
||||
state.el.find('.google-disclaimer').hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Replaces content in a caption
|
||||
*
|
||||
* @param {array} captions List of captions for the video.
|
||||
* @param {string} content content to be replaced
|
||||
* @param {string} replacementContent the replace string
|
||||
*
|
||||
* @returns {array} captions List of captions for the video.
|
||||
*/
|
||||
updateProblematicCaptionsContent: function(captions, content = '', replacementContent = '') {
|
||||
var updatedCaptions = captions.map(caption => caption.replace(content, replacementContent));
|
||||
|
||||
return updatedCaptions;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Fetch the caption file specified by the user. Upon successful
|
||||
* receipt of the file, the captions will be rendered.
|
||||
* @param {boolean} [fetchWithYoutubeId] Fetch youtube captions if true.
|
||||
* @returns {boolean}
|
||||
* true: The user specified a caption file. NOTE: if an error happens
|
||||
* while the specified file is being retrieved (for example the
|
||||
* file is missing on the server), this function will still return
|
||||
* true.
|
||||
* false: No caption file was specified, or an empty string was
|
||||
* specified for the Youtube type player.
|
||||
*/
|
||||
fetchCaption: function(fetchWithYoutubeId) {
|
||||
var self = this,
|
||||
state = this.state,
|
||||
language = state.getCurrentLanguage(),
|
||||
url = state.config.transcriptTranslationUrl.replace('__lang__', language),
|
||||
data, youtubeId;
|
||||
|
||||
if (this.loaded) {
|
||||
this.hideCaptions(false);
|
||||
}
|
||||
|
||||
if (this.fetchXHR && this.fetchXHR.abort) {
|
||||
this.fetchXHR.abort();
|
||||
}
|
||||
|
||||
if (state.videoType === 'youtube' || fetchWithYoutubeId) {
|
||||
try {
|
||||
youtubeId = state.youtubeId('1.0');
|
||||
} catch (err) {
|
||||
youtubeId = null;
|
||||
}
|
||||
|
||||
if (!youtubeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data = {videoId: youtubeId};
|
||||
}
|
||||
|
||||
state.el.removeClass('is-captions-rendered');
|
||||
// Fetch the captions file. If no file was specified, or if an error
|
||||
// occurred, then we hide the captions panel, and the "Transcript" button
|
||||
this.fetchXHR = $.ajaxWithPrefix({
|
||||
url: url,
|
||||
notifyOnError: false,
|
||||
data: data,
|
||||
success: function(sjson) {
|
||||
var results, start, captions;
|
||||
self.sjson = new Sjson(sjson);
|
||||
results = self.getBoundedCaptions();
|
||||
start = results.start;
|
||||
captions = results.captions;
|
||||
var contentToReplace = CAPTIONS_CONTENT_TO_REPLACE,
|
||||
replacementContent = CAPTIONS_CONTENT_REPLACEMENT;
|
||||
|
||||
captions = self.updateProblematicCaptionsContent(captions, contentToReplace, replacementContent);
|
||||
|
||||
self.updateGoogleDisclaimer(captions);
|
||||
|
||||
if (self.loaded) {
|
||||
if (self.rendered) {
|
||||
self.renderCaption(start, captions);
|
||||
self.updatePlayTime(state.videoPlayer.currentTime);
|
||||
}
|
||||
} else {
|
||||
if (state.isTouch) {
|
||||
HtmlUtils.setHtml(
|
||||
self.subtitlesEl.find('.subtitles-menu'),
|
||||
HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<li>'),
|
||||
gettext('Transcript will be displayed when you start playing the video.'),
|
||||
HtmlUtils.HTML('</li>')
|
||||
)
|
||||
);
|
||||
} else {
|
||||
self.renderCaption(start, captions);
|
||||
}
|
||||
self.hideCaptions(self.hideCaptionsOnLoad);
|
||||
HtmlUtils.append(
|
||||
self.state.el.find('.video-wrapper').parent(),
|
||||
HtmlUtils.HTML(self.subtitlesEl)
|
||||
);
|
||||
HtmlUtils.append(
|
||||
self.state.el.find('.secondary-controls'),
|
||||
HtmlUtils.HTML(self.container)
|
||||
);
|
||||
self.bindHandlers();
|
||||
}
|
||||
|
||||
self.loaded = true;
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var canFetchWithYoutubeId;
|
||||
console.log('[Video info]: ERROR while fetching captions.');
|
||||
console.log(
|
||||
'[Video info]: STATUS:', textStatus
|
||||
+ ', MESSAGE:', '' + errorThrown
|
||||
);
|
||||
// If initial list of languages has more than 1 item, check
|
||||
// for availability other transcripts.
|
||||
// If player mode is html5 and there are no initial languages
|
||||
// then try to fetch youtube version of transcript with
|
||||
// youtubeId.
|
||||
if (_.keys(state.config.transcriptLanguages).length > 1) {
|
||||
self.fetchAvailableTranslations();
|
||||
} else if (!fetchWithYoutubeId && state.videoType === 'html5') {
|
||||
canFetchWithYoutubeId = self.fetchCaption(true);
|
||||
if (canFetchWithYoutubeId) {
|
||||
console.log('[Video info]: Html5 mode fetching caption with youtubeId.'); // eslint-disable-line max-len, no-console
|
||||
} else {
|
||||
self.hideCaptions(true);
|
||||
self.languageChooserEl.hide();
|
||||
self.hideClosedCaptions();
|
||||
}
|
||||
} else {
|
||||
self.hideCaptions(true);
|
||||
self.languageChooserEl.hide();
|
||||
self.hideClosedCaptions();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Fetch the list of available language codes. Upon successful receipt
|
||||
* the list of available languages will be updated.
|
||||
*
|
||||
* @returns {jquery Promise}
|
||||
*/
|
||||
fetchAvailableTranslations: function() {
|
||||
var self = this,
|
||||
state = this.state;
|
||||
|
||||
this.availableTranslationsXHR = $.ajaxWithPrefix({
|
||||
url: state.config.transcriptAvailableTranslationsUrl,
|
||||
notifyOnError: false,
|
||||
success: function(response) {
|
||||
var currentLanguages = state.config.transcriptLanguages,
|
||||
newLanguages = _.pick(currentLanguages, response);
|
||||
|
||||
// Update property with available currently translations.
|
||||
state.config.transcriptLanguages = newLanguages;
|
||||
// Remove an old language menu.
|
||||
self.container.find('.langs-list').remove();
|
||||
|
||||
if (_.keys(newLanguages).length) {
|
||||
self.renderLanguageMenu(newLanguages);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.hideCaptions(true);
|
||||
self.languageChooserEl.hide();
|
||||
}
|
||||
});
|
||||
|
||||
return this.availableTranslationsXHR;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Recalculates and updates the height of the container of captions.
|
||||
*
|
||||
*/
|
||||
onResize: function() {
|
||||
this.subtitlesEl
|
||||
.find('.spacing').first()
|
||||
.height(this.topSpacingHeight());
|
||||
|
||||
this.subtitlesEl
|
||||
.find('.spacing').last()
|
||||
.height(this.bottomSpacingHeight());
|
||||
|
||||
this.scrollCaption();
|
||||
this.setSubtitlesHeight();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Create any necessary DOM elements, attach them, and set their
|
||||
* initial configuration for the Language menu.
|
||||
*
|
||||
* @param {object} languages Dictionary where key is language code,
|
||||
* value - language label
|
||||
*
|
||||
*/
|
||||
renderLanguageMenu: function(languages) {
|
||||
var self = this,
|
||||
state = this.state,
|
||||
$menu = $('<ol class="langs-list menu">'),
|
||||
currentLang = state.getCurrentLanguage(),
|
||||
$li, $link, linkHtml;
|
||||
|
||||
if (_.keys(languages).length < 2) {
|
||||
// Remove the menu toggle button
|
||||
self.container.find('.lang').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLanguageMenu = true;
|
||||
|
||||
$.each(languages, function(code, label) {
|
||||
$li = $('<li />', {'data-lang-code': code});
|
||||
linkHtml = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<button class="control control-lang">'),
|
||||
label,
|
||||
HtmlUtils.HTML('</button>')
|
||||
);
|
||||
$link = $(linkHtml.toString());
|
||||
|
||||
if (currentLang === code) {
|
||||
$li.addClass('is-active');
|
||||
$link.attr('aria-pressed', 'true');
|
||||
}
|
||||
|
||||
$li.append($link);
|
||||
$menu.append($li);
|
||||
});
|
||||
|
||||
HtmlUtils.append(
|
||||
this.languageChooserEl,
|
||||
HtmlUtils.HTML($menu)
|
||||
);
|
||||
|
||||
$menu.on('click', '.control-lang', function(e) {
|
||||
var el = $(e.currentTarget).parent(),
|
||||
captionState = self.state,
|
||||
langCode = el.data('lang-code');
|
||||
|
||||
if (captionState.lang !== langCode) {
|
||||
captionState.lang = langCode;
|
||||
el.addClass('is-active')
|
||||
.siblings('li')
|
||||
.removeClass('is-active')
|
||||
.find('.control-lang')
|
||||
.attr('aria-pressed', 'false');
|
||||
|
||||
$(e.currentTarget).attr('aria-pressed', 'true');
|
||||
|
||||
captionState.el.trigger('language_menu:change', [langCode]);
|
||||
self.fetchCaption();
|
||||
|
||||
// update the closed-captions lang attribute
|
||||
self.captionDisplayEl.attr('lang', langCode);
|
||||
|
||||
// update the transcript lang attribute
|
||||
self.subtitlesMenuEl.attr('lang', langCode);
|
||||
self.closeLanguageMenu(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Create any necessary DOM elements, attach them, and set their
|
||||
* initial configuration.
|
||||
*
|
||||
* @param {jQuery element} container Element in which captions will be
|
||||
* inserted.
|
||||
* @param {array} start List of start times for the video.
|
||||
* @param {array} captions List of captions for the video.
|
||||
* @returns {object} jQuery's Promise object
|
||||
*
|
||||
*/
|
||||
buildCaptions: function(container, start, captions) {
|
||||
var process = function(text, index) {
|
||||
var $spanEl = $('<span>', {
|
||||
role: 'link',
|
||||
'data-index': index,
|
||||
'data-start': start[index],
|
||||
tabindex: 0
|
||||
});
|
||||
|
||||
HtmlUtils.setHtml($($spanEl), HtmlUtils.HTML(text.toString()));
|
||||
|
||||
return $spanEl.wrap('<li>').parent()[0]; // xss-lint: disable=javascript-jquery-insertion
|
||||
};
|
||||
|
||||
return AsyncProcess.array(captions, process).done(function(list) {
|
||||
HtmlUtils.append(
|
||||
container,
|
||||
HtmlUtils.HTML(list)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Initiates creating of captions and set their initial configuration.
|
||||
*
|
||||
* @param {array} start List of start times for the video.
|
||||
* @param {array} captions List of captions for the video.
|
||||
*
|
||||
*/
|
||||
renderCaption: function(start, captions) {
|
||||
var self = this;
|
||||
|
||||
var onRender = function() {
|
||||
self.addPaddings();
|
||||
// Enables or disables automatic scrolling of the captions when the
|
||||
// video is playing. This feature has to be disabled when tabbing
|
||||
// through them as it interferes with that action. Initially, have
|
||||
// this flag enabled as we assume mouse use. Then, if the first
|
||||
// caption (through forward tabbing) or the last caption (through
|
||||
// backwards tabbing) gets the focus, disable that feature.
|
||||
// Re-enable it if tabbing then cycles out of the the captions.
|
||||
self.autoScrolling = true;
|
||||
// Keeps track of where the focus is situated in the array of
|
||||
// captions. Used to implement the automatic scrolling behavior and
|
||||
// decide if the outline around a caption has to be hidden or shown
|
||||
// on a mouseenter or mouseleave. Initially, no caption has the
|
||||
// focus, set the index to -1.
|
||||
self.currentCaptionIndex = -1;
|
||||
// Used to track if the focus is coming from a click or tabbing. This
|
||||
// has to be known to decide if, when a caption gets the focus, an
|
||||
// outline has to be drawn (tabbing) or not (mouse click).
|
||||
self.isMouseFocus = false;
|
||||
self.rendered = true;
|
||||
self.state.el.addClass('is-captions-rendered');
|
||||
|
||||
self.subtitlesEl
|
||||
.attr('aria-label', gettext('Activating a link in this group will skip to the corresponding point in the video.')); // eslint-disable-line max-len
|
||||
|
||||
self.subtitlesEl.find('.transcript-title')
|
||||
.text(gettext('Video transcript'));
|
||||
|
||||
self.subtitlesEl.find('.transcript-start')
|
||||
.text(gettext('Start of transcript. Skip to the end.'))
|
||||
.attr('lang', $('html').attr('lang'));
|
||||
|
||||
self.subtitlesEl.find('.transcript-end')
|
||||
.text(gettext('End of transcript. Skip to the start.'))
|
||||
.attr('lang', $('html').attr('lang'));
|
||||
|
||||
self.container.find('.menu-container .instructions')
|
||||
.text(gettext('Press the UP arrow key to enter the language menu then use UP and DOWN arrow keys to navigate language options. Press ENTER to change to the selected language.')); // eslint-disable-line max-len
|
||||
};
|
||||
|
||||
this.rendered = false;
|
||||
this.subtitlesMenuEl.empty();
|
||||
this.setSubtitlesHeight();
|
||||
this.buildCaptions(this.subtitlesMenuEl, start, captions).done(onRender);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sets top and bottom spacing height and make sure they are taken
|
||||
* out of the tabbing order.
|
||||
*
|
||||
*/
|
||||
addPaddings: function() {
|
||||
var topSpacer = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<li class="spacing" style="height: {height}px">',
|
||||
'<a href="#transcript-end-{id}" id="transcript-start-{id}" class="transcript-start"></a>', // eslint-disable-line max-len, indent
|
||||
'</li>'
|
||||
].join('')),
|
||||
{
|
||||
id: this.state.id,
|
||||
height: this.topSpacingHeight()
|
||||
}
|
||||
);
|
||||
|
||||
var bottomSpacer = HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<li class="spacing" style="height: {height}px">',
|
||||
'<a href="#transcript-start-{id}" id="transcript-end-{id}" class="transcript-end"></a>', // eslint-disable-line max-len, indent
|
||||
'</li>'
|
||||
].join('')),
|
||||
{
|
||||
id: this.state.id,
|
||||
height: this.bottomSpacingHeight()
|
||||
}
|
||||
);
|
||||
|
||||
HtmlUtils.prepend(
|
||||
this.subtitlesMenuEl,
|
||||
topSpacer
|
||||
);
|
||||
|
||||
HtmlUtils.append(
|
||||
this.subtitlesMenuEl,
|
||||
bottomSpacer
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc
|
||||
* On mouseOver: Hides the outline of a caption that has been tabbed to.
|
||||
* On mouseOut: Shows the outline of a caption that has been tabbed to.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionMouseOverOut: function(event) {
|
||||
var $caption = $(event.target),
|
||||
captionIndex = parseInt($caption.attr('data-index'), 10);
|
||||
|
||||
if (captionIndex === this.currentCaptionIndex) {
|
||||
if (event.type === 'mouseover') {
|
||||
$caption.removeClass('focused');
|
||||
} else { // mouseout
|
||||
$caption.addClass('focused');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles mousedown event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionMouseDown: function(event) {
|
||||
var $caption = $(event.target);
|
||||
|
||||
this.isMouseFocus = true;
|
||||
this.autoScrolling = true;
|
||||
$caption.removeClass('focused');
|
||||
this.currentCaptionIndex = -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles click event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionClick: function(event) {
|
||||
this.seekPlayer(event);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles focus event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionFocus: function(event) {
|
||||
var $caption = $(event.target),
|
||||
container = $caption.parent(),
|
||||
captionIndex = parseInt($caption.attr('data-index'), 10);
|
||||
// If the focus comes from a mouse click, hide the outline, turn on
|
||||
// automatic scrolling and set currentCaptionIndex to point outside of
|
||||
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
|
||||
if (this.isMouseFocus) {
|
||||
this.autoScrolling = true;
|
||||
container.removeClass('focused');
|
||||
this.currentCaptionIndex = -1;
|
||||
} else {
|
||||
// If the focus comes from tabbing, show the outline and turn off
|
||||
// automatic scrolling.
|
||||
|
||||
this.currentCaptionIndex = captionIndex;
|
||||
container.addClass('focused');
|
||||
// The second and second to last elements turn automatic scrolling
|
||||
// off again as it may have been enabled in captionBlur.
|
||||
if (
|
||||
captionIndex <= 1
|
||||
|| captionIndex >= this.sjson.getSize() - 2
|
||||
) {
|
||||
this.autoScrolling = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles blur event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionBlur: function(event) {
|
||||
var $caption = $(event.target),
|
||||
container = $caption.parent(),
|
||||
captionIndex = parseInt($caption.attr('data-index'), 10);
|
||||
|
||||
container.removeClass('focused');
|
||||
// If we are on first or last index, we have to turn automatic scroll
|
||||
// on again when losing focus. There is no way to know in what
|
||||
// direction we are tabbing. So we could be on the first element and
|
||||
// tabbing back out of the captions or on the last element and tabbing
|
||||
// forward out of the captions.
|
||||
if (captionIndex === 0
|
||||
|| captionIndex === this.sjson.getSize() - 1) {
|
||||
this.autoScrolling = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Handles keydown event on concrete caption.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
captionKeyDown: function(event) {
|
||||
this.isMouseFocus = false;
|
||||
if (event.which === 13) { // Enter key
|
||||
this.seekPlayer(event);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Scrolls caption container to make active caption visible.
|
||||
*
|
||||
*/
|
||||
scrollCaption: function() {
|
||||
var el = this.subtitlesEl.find('.current:first');
|
||||
|
||||
// Automatic scrolling gets disabled if one of the captions has
|
||||
// received focus through tabbing.
|
||||
if (
|
||||
!this.frozen
|
||||
&& el.length
|
||||
&& this.autoScrolling
|
||||
) {
|
||||
this.subtitlesEl.scrollTo(
|
||||
el,
|
||||
{
|
||||
offset: -1 * this.calculateOffset(el)
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates flags on play
|
||||
*
|
||||
*/
|
||||
play: function() {
|
||||
var captions, startAndCaptions, start;
|
||||
if (this.loaded) {
|
||||
if (!this.rendered) {
|
||||
startAndCaptions = this.getBoundedCaptions();
|
||||
start = startAndCaptions.start;
|
||||
captions = startAndCaptions.captions;
|
||||
this.renderCaption(start, captions);
|
||||
}
|
||||
|
||||
this.playing = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates flags on pause
|
||||
*
|
||||
*/
|
||||
pause: function() {
|
||||
if (this.loaded) {
|
||||
this.playing = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Updates captions UI on paying.
|
||||
*
|
||||
* @param {number} time Time in seconds.
|
||||
*
|
||||
*/
|
||||
updatePlayTime: function(time) {
|
||||
var state = this.state,
|
||||
params, newIndex, times;
|
||||
|
||||
if (this.loaded) {
|
||||
if (state.isFlashMode()) {
|
||||
time = Time.convert(time, state.speed, '1.0');
|
||||
}
|
||||
|
||||
time = Math.round(time * 1000 + 100);
|
||||
times = this.getStartEndTimes();
|
||||
// if start and end times are defined, limit search.
|
||||
// else, use the entire list of video captions
|
||||
params = [time].concat(times);
|
||||
// eslint-disable-next-line prefer-spread
|
||||
newIndex = this.sjson.search.apply(this.sjson, params);
|
||||
|
||||
if (
|
||||
typeof newIndex !== 'undefined'
|
||||
&& newIndex !== -1
|
||||
&& this.currentIndex !== newIndex
|
||||
) {
|
||||
if (typeof this.currentIndex !== 'undefined') {
|
||||
this.subtitlesEl
|
||||
.find('li.current')
|
||||
.attr('aria-current', 'false')
|
||||
.removeClass('current');
|
||||
}
|
||||
this.subtitlesEl
|
||||
.find("span[data-index='" + newIndex + "']")
|
||||
.parent()
|
||||
.attr('aria-current', 'true')
|
||||
.addClass('current');
|
||||
|
||||
this.currentIndex = newIndex;
|
||||
this.captionDisplayEl.text(this.subtitlesEl.find("span[data-index='" + newIndex + "']").text());
|
||||
this.scrollCaption();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sends log to the server on caption seek.
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
seekPlayer: function(event) {
|
||||
var state = this.state,
|
||||
time = parseInt($(event.target).data('start'), 10);
|
||||
|
||||
if (state.isFlashMode()) {
|
||||
time = Math.round(Time.convert(time, '1.0', state.speed));
|
||||
}
|
||||
|
||||
state.trigger(
|
||||
'videoPlayer.onCaptionSeek',
|
||||
{
|
||||
type: 'onCaptionSeek',
|
||||
time: time / 1000
|
||||
}
|
||||
);
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for paddings.
|
||||
*
|
||||
* @param {jquery element} element Top or bottom padding element.
|
||||
* @returns {number} Offset for the passed padding element.
|
||||
*
|
||||
*/
|
||||
calculateOffset: function(element) {
|
||||
return this.captionHeight() / 2 - element.height() / 2;
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for the top padding element.
|
||||
*
|
||||
* @returns {number} Offset for the passed top padding element.
|
||||
*
|
||||
*/
|
||||
topSpacingHeight: function() {
|
||||
return this.calculateOffset(
|
||||
this.subtitlesEl.find('li:not(.spacing)').first()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Calculates offset for the bottom padding element.
|
||||
*
|
||||
* @returns {number} Offset for the passed bottom padding element.
|
||||
*
|
||||
*/
|
||||
bottomSpacingHeight: function() {
|
||||
return this.calculateOffset(
|
||||
this.subtitlesEl.find('li:not(.spacing)').last()
|
||||
);
|
||||
},
|
||||
|
||||
handleCaptioningCookie: function() {
|
||||
if ($.cookie('show_closed_captions') === 'true') {
|
||||
this.state.showClosedCaptions = true;
|
||||
this.showClosedCaptions();
|
||||
|
||||
// keep it going until turned off
|
||||
$.cookie('show_closed_captions', 'true', {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
this.hideClosedCaptions();
|
||||
}
|
||||
},
|
||||
|
||||
toggleClosedCaptions: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.el.hasClass('has-captions')) {
|
||||
this.state.showClosedCaptions = false;
|
||||
this.updateCaptioningCookie(false);
|
||||
this.hideClosedCaptions();
|
||||
this.toggleGoogleDisclaimer(false);
|
||||
} else {
|
||||
this.state.showClosedCaptions = true;
|
||||
this.updateCaptioningCookie(true);
|
||||
this.showClosedCaptions();
|
||||
this.toggleGoogleDisclaimer(true);
|
||||
}
|
||||
},
|
||||
|
||||
showClosedCaptions: function() {
|
||||
var text = gettext('Hide closed captions');
|
||||
this.state.el.addClass('has-captions');
|
||||
|
||||
this.captionDisplayEl
|
||||
.show()
|
||||
.addClass('is-visible')
|
||||
.attr('lang', this.state.lang);
|
||||
|
||||
this.captionControlEl
|
||||
.addClass('is-active')
|
||||
.attr('title', text)
|
||||
.attr('aria-label', text);
|
||||
|
||||
if (this.subtitlesEl.find('.current').text()) {
|
||||
this.captionDisplayEl
|
||||
.text(this.subtitlesEl.find('.current').text());
|
||||
} else {
|
||||
this.captionDisplayEl
|
||||
.text(gettext('(Caption will be displayed when you start playing the video.)'));
|
||||
}
|
||||
|
||||
this.state.el.trigger('captions:show');
|
||||
},
|
||||
|
||||
hideClosedCaptions: function() {
|
||||
var text = gettext('Turn on closed captioning');
|
||||
this.state.el.removeClass('has-captions');
|
||||
|
||||
this.captionDisplayEl
|
||||
.hide()
|
||||
.removeClass('is-visible');
|
||||
|
||||
this.captionControlEl
|
||||
.removeClass('is-active')
|
||||
.attr('title', text)
|
||||
.attr('aria-label', text);
|
||||
|
||||
this.state.el.trigger('captions:hide');
|
||||
},
|
||||
|
||||
updateCaptioningCookie: function(method) {
|
||||
if (method) {
|
||||
$.cookie('show_closed_captions', 'true', {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
$.cookie('show_closed_captions', null, {
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This runs when the video block is first rendered and sets the initial visibility
|
||||
* of the transcript panel based on the value of the 'show_transcript' cookie and/or
|
||||
* the block's showCaptions setting.
|
||||
*/
|
||||
setTranscriptVisibility: function() {
|
||||
var hideCaptionsOnRender = !this.state.config.showCaptions;
|
||||
|
||||
if ($.cookie('show_transcript') === 'true') {
|
||||
this.hideCaptionsOnLoad = false;
|
||||
// Keep it going until turned off.
|
||||
this.updateTranscriptCookie(true);
|
||||
} else if ($.cookie('show_transcript') === 'false') {
|
||||
hideCaptionsOnRender = true;
|
||||
this.hideCaptionsOnLoad = true;
|
||||
} else {
|
||||
this.hideCaptionsOnLoad = !this.state.config.showCaptions;
|
||||
}
|
||||
|
||||
if (hideCaptionsOnRender) {
|
||||
this.state.el.addClass('closed');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Shows/Hides transcript on click `transcript` button
|
||||
*
|
||||
* @param {jquery Event} event
|
||||
*
|
||||
*/
|
||||
toggleTranscript: function(event) {
|
||||
event.preventDefault();
|
||||
if (this.state.el.hasClass('closed')) {
|
||||
this.hideCaptions(false, true);
|
||||
this.updateTranscriptCookie(true);
|
||||
} else {
|
||||
this.hideCaptions(true, true);
|
||||
this.updateTranscriptCookie(false);
|
||||
}
|
||||
},
|
||||
|
||||
updateTranscriptCookie: function(showTranscript) {
|
||||
if (showTranscript) {
|
||||
$.cookie('show_transcript', 'true', {
|
||||
expires: 3650,
|
||||
path: '/'
|
||||
});
|
||||
} else {
|
||||
$.cookie('show_transcript', 'false', {
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
listenForDragDrop: function() {
|
||||
var captions = this.captionDisplayEl['0'];
|
||||
|
||||
if (typeof Draggabilly === 'function') {
|
||||
// eslint-disable-next-line no-new
|
||||
new Draggabilly(captions, {containment: true});
|
||||
} else {
|
||||
console.log('Closed captioning available but not draggable');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Shows/Hides the transcript panel.
|
||||
*
|
||||
* @param {boolean} hideCaptions if `true` hides the transcript panel,
|
||||
* otherwise - show.
|
||||
*/
|
||||
hideCaptions: function(hideCaptions, triggerEvent) {
|
||||
var transcriptControlEl = this.transcriptControlEl,
|
||||
self = this,
|
||||
state = this.state,
|
||||
text;
|
||||
|
||||
if (hideCaptions) {
|
||||
state.captionsHidden = true;
|
||||
state.el.addClass('closed');
|
||||
text = gettext('Turn on transcripts');
|
||||
if (triggerEvent) {
|
||||
this.state.el.trigger('transcript:hide');
|
||||
}
|
||||
|
||||
self.toggleGoogleDisclaimer(false);
|
||||
|
||||
transcriptControlEl
|
||||
.removeClass('is-active')
|
||||
.attr('title', gettext(text))
|
||||
.attr('aria-label', text);
|
||||
} else {
|
||||
state.captionsHidden = false;
|
||||
state.el.removeClass('closed');
|
||||
this.scrollCaption();
|
||||
text = gettext('Turn off transcripts');
|
||||
if (triggerEvent) {
|
||||
this.state.el.trigger('transcript:show');
|
||||
}
|
||||
|
||||
self.toggleGoogleDisclaimer(true);
|
||||
|
||||
transcriptControlEl
|
||||
.addClass('is-active')
|
||||
.attr('title', gettext(text))
|
||||
.attr('aria-label', text);
|
||||
}
|
||||
|
||||
if (state.resizer) {
|
||||
if (state.isFullScreen) {
|
||||
state.resizer.setMode('both');
|
||||
} else {
|
||||
state.resizer.alignByWidthOnly();
|
||||
}
|
||||
}
|
||||
|
||||
this.setSubtitlesHeight();
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Return the caption container height.
|
||||
*
|
||||
* @returns {number} event Height of the container in pixels.
|
||||
*
|
||||
*/
|
||||
captionHeight: function() {
|
||||
var state = this.state;
|
||||
if (state.isFullScreen) {
|
||||
return state.container.height() - state.videoFullScreen.height;
|
||||
} else {
|
||||
return state.container.height();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @desc Sets the height of the caption container element.
|
||||
*
|
||||
*/
|
||||
setSubtitlesHeight: function() {
|
||||
var height = 0,
|
||||
state = this.state;
|
||||
// on page load captionHidden = undefined
|
||||
if ((state.captionsHidden === undefined && this.hideCaptionsOnLoad)
|
||||
|| state.captionsHidden === true
|
||||
) {
|
||||
// In case of html5 autoshowing subtitles, we adjust height of
|
||||
// subs, by height of scrollbar.
|
||||
height = state.el.find('.video-controls').height()
|
||||
+ 0.5 * state.el.find('.slider').height();
|
||||
// Height of videoControl does not contain height of slider.
|
||||
// css is set to absolute, to avoid yanking when slider
|
||||
// autochanges its height.
|
||||
}
|
||||
|
||||
this.subtitlesEl.css({
|
||||
maxHeight: this.captionHeight() - height
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return VideoCaption;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,111 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define('video/10_commands.js', [], function() {
|
||||
var VideoCommands, Command, playCommand, pauseCommand, togglePlaybackCommand,
|
||||
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand, skipCommand;
|
||||
/**
|
||||
* Video commands module.
|
||||
* @exports video/10_commands.js
|
||||
* @constructor
|
||||
* @param {Object} state The object containing the state of the video
|
||||
* @param {Object} i18n The object containing strings with translations.
|
||||
* @return {jquery Promise}
|
||||
*/
|
||||
VideoCommands = function(state, i18n) {
|
||||
if (!(this instanceof VideoCommands)) {
|
||||
return new VideoCommands(state, i18n);
|
||||
}
|
||||
|
||||
_.bindAll(this, 'destroy');
|
||||
this.state = state;
|
||||
this.state.videoCommands = this;
|
||||
this.i18n = i18n;
|
||||
this.commands = [];
|
||||
this.initialize();
|
||||
|
||||
return $.Deferred().resolve().promise();
|
||||
};
|
||||
|
||||
VideoCommands.prototype = {
|
||||
destroy: function() {
|
||||
this.state.el.off('destroy', this.destroy);
|
||||
delete this.state.videoCommands;
|
||||
},
|
||||
|
||||
/** Initializes the module. */
|
||||
initialize: function() {
|
||||
this.commands = this.getCommands();
|
||||
this.state.el.on('destroy', this.destroy);
|
||||
},
|
||||
|
||||
execute: function(command) {
|
||||
var args = [].slice.call(arguments, 1) || [];
|
||||
|
||||
if (_.has(this.commands, command)) {
|
||||
this.commands[command].execute.apply(this, [this.state].concat(args));
|
||||
} else {
|
||||
console.log('Command "' + command + '" is not available.');
|
||||
}
|
||||
},
|
||||
|
||||
getCommands: function() {
|
||||
var commands = {},
|
||||
commandsList = [
|
||||
playCommand, pauseCommand, togglePlaybackCommand,
|
||||
toggleMuteCommand, toggleFullScreenCommand, setSpeedCommand,
|
||||
skipCommand
|
||||
];
|
||||
|
||||
_.each(commandsList, function(command) {
|
||||
commands[command.name] = command;
|
||||
}, this);
|
||||
|
||||
return commands;
|
||||
}
|
||||
};
|
||||
|
||||
Command = function(name, execute) {
|
||||
this.name = name;
|
||||
this.execute = execute;
|
||||
};
|
||||
|
||||
playCommand = new Command('play', function(state) {
|
||||
state.videoPlayer.play();
|
||||
});
|
||||
|
||||
pauseCommand = new Command('pause', function(state) {
|
||||
state.videoPlayer.pause();
|
||||
});
|
||||
|
||||
togglePlaybackCommand = new Command('togglePlayback', function(state) {
|
||||
if (state.videoPlayer.isPlaying()) {
|
||||
pauseCommand.execute(state);
|
||||
} else {
|
||||
playCommand.execute(state);
|
||||
}
|
||||
});
|
||||
|
||||
toggleMuteCommand = new Command('toggleMute', function(state) {
|
||||
state.videoVolumeControl.toggleMute();
|
||||
});
|
||||
|
||||
toggleFullScreenCommand = new Command('toggleFullScreen', function(state) {
|
||||
state.videoFullScreen.toggle();
|
||||
});
|
||||
|
||||
setSpeedCommand = new Command('speed', function(state, speed) {
|
||||
state.videoSpeedControl.setSpeed(state.speedToString(speed));
|
||||
});
|
||||
|
||||
skipCommand = new Command('skip', function(state, doNotShowAgain) {
|
||||
if (doNotShowAgain) {
|
||||
state.videoBumper.skipAndDoNotShowAgain();
|
||||
} else {
|
||||
state.videoBumper.skip();
|
||||
}
|
||||
});
|
||||
|
||||
return VideoCommands;
|
||||
});
|
||||
}(RequireJS.define));
|
||||
@@ -1,198 +0,0 @@
|
||||
/* globals _ */
|
||||
/* RequireJS */
|
||||
(function(require, $) {
|
||||
'use strict';
|
||||
|
||||
// In the case when the Video constructor will be called before RequireJS finishes loading all of the Video
|
||||
// dependencies, we will have a mock function that will collect all the elements that must be initialized as
|
||||
// Video elements.
|
||||
//
|
||||
// Once RequireJS will load all of the necessary dependencies, main code will invoke the mock function with
|
||||
// the second parameter set to truthy value. This will trigger the actual Video constructor on all elements
|
||||
// that are stored in a temporary list.
|
||||
window.Video = (function() {
|
||||
// Temporary storage place for elements that must be initialized as Video elements.
|
||||
var tempCallStack = [];
|
||||
|
||||
return function(element, processTempCallStack) {
|
||||
// If mock function was called with second parameter set to truthy value, we invoke the real `window.Video`
|
||||
// on all the stored elements so far.
|
||||
if (processTempCallStack) {
|
||||
$.each(tempCallStack, function(index, el) {
|
||||
// By now, `window.Video` is the real constructor.
|
||||
window.Video(el);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// If normal call to `window.Video` constructor, store the element for later initializing.
|
||||
tempCallStack.push(element);
|
||||
|
||||
// Real Video constructor returns the `state` object. The mock function will return an empty object.
|
||||
return {};
|
||||
};
|
||||
}());
|
||||
|
||||
// Main module.
|
||||
require(
|
||||
/* End RequireJS */
|
||||
/* Webpack
|
||||
define(
|
||||
/* End Webpack */
|
||||
[
|
||||
'video/00_video_storage.js',
|
||||
'video/01_initialize.js',
|
||||
'video/025_focus_grabber.js',
|
||||
'video/035_video_accessible_menu.js',
|
||||
'video/04_video_control.js',
|
||||
'video/04_video_full_screen.js',
|
||||
'video/05_video_quality_control.js',
|
||||
'video/06_video_progress_slider.js',
|
||||
'video/07_video_volume_control.js',
|
||||
'video/08_video_speed_control.js',
|
||||
'video/08_video_auto_advance_control.js',
|
||||
'video/09_video_caption.js',
|
||||
'video/09_play_placeholder.js',
|
||||
'video/09_play_pause_control.js',
|
||||
'video/09_play_skip_control.js',
|
||||
'video/09_skip_control.js',
|
||||
'video/09_bumper.js',
|
||||
'video/09_save_state_plugin.js',
|
||||
'video/09_events_plugin.js',
|
||||
'video/09_events_bumper_plugin.js',
|
||||
'video/09_poster.js',
|
||||
'video/09_completion.js',
|
||||
'video/10_commands.js',
|
||||
'video/095_video_context_menu.js',
|
||||
'video/036_video_social_sharing.js',
|
||||
'video/037_video_transcript_feedback.js'
|
||||
],
|
||||
function(
|
||||
VideoStorage, initialize, FocusGrabber, VideoAccessibleMenu, VideoControl, VideoFullScreen,
|
||||
VideoQualityControl, VideoProgressSlider, VideoVolumeControl, VideoSpeedControl, VideoAutoAdvanceControl,
|
||||
VideoCaption, VideoPlayPlaceholder, VideoPlayPauseControl, VideoPlaySkipControl, VideoSkipControl,
|
||||
VideoBumper, VideoSaveStatePlugin, VideoEventsPlugin, VideoEventsBumperPlugin, VideoPoster,
|
||||
VideoCompletionHandler, VideoCommands, VideoContextMenu, VideoSocialSharing, VideoTranscriptFeedback
|
||||
) {
|
||||
/* RequireJS */
|
||||
var youtubeXhr = null,
|
||||
oldVideo = window.Video;
|
||||
/* End RequireJS */
|
||||
/* Webpack
|
||||
var youtubeXhr = null;
|
||||
/* End Webpack */
|
||||
|
||||
window.Video = function(element) {
|
||||
var el = $(element).find('.video'),
|
||||
id = el.attr('id').replace(/video_/, ''),
|
||||
storage = VideoStorage('VideoState', id),
|
||||
bumperMetadata = el.data('bumper-metadata'),
|
||||
autoAdvanceEnabled = el.data('autoadvance-enabled') === 'True',
|
||||
mainVideoModules = [
|
||||
FocusGrabber, VideoControl, VideoPlayPlaceholder,
|
||||
VideoPlayPauseControl, VideoProgressSlider, VideoSpeedControl,
|
||||
VideoVolumeControl, VideoQualityControl, VideoFullScreen, VideoCaption, VideoCommands,
|
||||
VideoContextMenu, VideoSaveStatePlugin, VideoEventsPlugin, VideoCompletionHandler, VideoTranscriptFeedback
|
||||
].concat(autoAdvanceEnabled ? [VideoAutoAdvanceControl] : []),
|
||||
bumperVideoModules = [VideoControl, VideoPlaySkipControl, VideoSkipControl,
|
||||
VideoVolumeControl, VideoCaption, VideoCommands, VideoSaveStatePlugin, VideoTranscriptFeedback,
|
||||
VideoEventsBumperPlugin, VideoCompletionHandler],
|
||||
state = {
|
||||
el: el,
|
||||
id: id,
|
||||
metadata: el.data('metadata'),
|
||||
storage: storage,
|
||||
options: {},
|
||||
youtubeXhr: youtubeXhr,
|
||||
modules: mainVideoModules
|
||||
};
|
||||
|
||||
var getBumperState = function(metadata) {
|
||||
var bumperState = $.extend(true, {
|
||||
el: el,
|
||||
id: id,
|
||||
storage: storage,
|
||||
options: {},
|
||||
youtubeXhr: youtubeXhr
|
||||
}, {metadata: metadata});
|
||||
|
||||
bumperState.modules = bumperVideoModules;
|
||||
bumperState.options = {
|
||||
SaveStatePlugin: {events: ['language_menu:change']}
|
||||
};
|
||||
return bumperState;
|
||||
};
|
||||
|
||||
var player = function(innerState) {
|
||||
return function() {
|
||||
_.extend(innerState.metadata, {autoplay: true, focusFirstControl: true});
|
||||
initialize(innerState, element);
|
||||
};
|
||||
};
|
||||
var onSequenceChange;
|
||||
|
||||
VideoAccessibleMenu(el, {
|
||||
storage: storage,
|
||||
saveStateUrl: state.metadata.saveStateUrl
|
||||
});
|
||||
|
||||
VideoSocialSharing(el);
|
||||
|
||||
if (bumperMetadata) {
|
||||
VideoPoster(el, {
|
||||
poster: el.data('poster'),
|
||||
onClick: _.once(function() {
|
||||
var mainVideoPlayer = player(state);
|
||||
var bumper, bumperState;
|
||||
if (storage.getItem('isBumperShown')) {
|
||||
mainVideoPlayer();
|
||||
} else {
|
||||
bumperState = getBumperState(bumperMetadata);
|
||||
bumper = new VideoBumper(player(bumperState), bumperState);
|
||||
state.bumperState = bumperState;
|
||||
bumper.getPromise().done(function() {
|
||||
delete state.bumperState;
|
||||
mainVideoPlayer();
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
initialize(state, element);
|
||||
}
|
||||
|
||||
if (!youtubeXhr) {
|
||||
youtubeXhr = state.youtubeXhr;
|
||||
}
|
||||
|
||||
el.data('video-player-state', state);
|
||||
onSequenceChange = function() {
|
||||
if (state && state.videoPlayer) {
|
||||
state.videoPlayer.destroy();
|
||||
}
|
||||
$('.sequence').off('sequence:change', onSequenceChange);
|
||||
};
|
||||
$('.sequence').on('sequence:change', onSequenceChange);
|
||||
|
||||
// Because the 'state' object is only available inside this closure, we will also make it available to
|
||||
// the caller by returning it. This is necessary so that we can test Video with Jasmine.
|
||||
return state;
|
||||
};
|
||||
|
||||
window.Video.clearYoutubeXhr = function() {
|
||||
youtubeXhr = null;
|
||||
};
|
||||
|
||||
window.Video.loadYouTubeIFrameAPI = initialize.prototype.loadYouTubeIFrameAPI;
|
||||
|
||||
/* RequireJS */
|
||||
// Invoke the mock Video constructor so that the elements stored within it can be processed by the real
|
||||
// `window.Video` constructor.
|
||||
oldVideo(null, true);
|
||||
/* End RequireJS */
|
||||
}
|
||||
);
|
||||
/* RequireJS */
|
||||
}(window.RequireJS.require, window.jQuery));
|
||||
/* End RequireJS */
|
||||
@@ -247,7 +247,7 @@ class _BuiltInVideoBlock(
|
||||
fragment = Fragment(self.get_html(context=context))
|
||||
add_css_to_fragment(fragment, 'VideoBlockDisplay.css')
|
||||
add_webpack_js_to_fragment(fragment, 'VideoBlockDisplay')
|
||||
shim_xmodule_js(fragment, 'Video')
|
||||
fragment.initialize_js('Video')
|
||||
return fragment
|
||||
|
||||
def author_view(self, context):
|
||||
@@ -280,8 +280,8 @@ class _BuiltInVideoBlock(
|
||||
|
||||
fragment = Fragment(self.get_html(view=PUBLIC_VIEW, context=context))
|
||||
add_css_to_fragment(fragment, 'VideoBlockDisplay.css')
|
||||
add_webpack_js_to_fragment(fragment, 'VideoBlockDisplay')
|
||||
shim_xmodule_js(fragment, 'Video')
|
||||
add_webpack_js_to_fragment(fragment, 'VideoBlockMain')
|
||||
fragment.initialize_js('Video')
|
||||
return fragment
|
||||
|
||||
def get_html(self, view=STUDENT_VIEW, context=None): # lint-amnesty, pylint: disable=arguments-differ, too-many-statements
|
||||
|
||||
Reference in New Issue
Block a user