Commit ec6388b8 by Anton Stupak

Merge pull request #3153 from edx/anton/caption_refactor

Refactor video caption module.
parents dd7bae43 2f572a86
......@@ -123,28 +123,16 @@
it('bind the hide caption button', function () {
state = jasmine.initializePlayer();
expect($('.hide-subtitles')).toHandleWith(
'click', state.videoCaption.toggle
);
expect($('.hide-subtitles')).toHandle('click');
});
it('bind the mouse movement', function () {
state = jasmine.initializePlayer();
expect($('.subtitles')).toHandleWith(
'mouseover', state.videoCaption.onMouseEnter
);
expect($('.subtitles')).toHandleWith(
'mouseout', state.videoCaption.onMouseLeave
);
expect($('.subtitles')).toHandleWith(
'mousemove', state.videoCaption.onMovement
);
expect($('.subtitles')).toHandleWith(
'mousewheel', state.videoCaption.onMovement
);
expect($('.subtitles')).toHandleWith(
'DOMMouseScroll', state.videoCaption.onMovement
);
expect($('.subtitles')).toHandle('mouseover');
expect($('.subtitles')).toHandle('mouseout');
expect($('.subtitles')).toHandle('mousemove');
expect($('.subtitles')).toHandle('mousewheel');
expect($('.subtitles')).toHandle('DOMMouseScroll');
});
it('bind the scroll', function () {
......@@ -859,7 +847,7 @@
runs(function () {
videoControl = state.videoControl;
$('.subtitles li[data-index=1]').addClass('current');
state.videoCaption.resize();
state.videoCaption.onResize();
});
});
......
......@@ -26,7 +26,6 @@ function (VideoPlayer) {
describe('always', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
state.videoEl = $('video, iframe');
});
......@@ -211,7 +210,7 @@ function (VideoPlayer) {
state.videoEl = $('video, iframe');
spyOn(state.videoControl, 'pause').andCallThrough();
spyOn(state.videoCaption, 'pause').andCallThrough();
spyOn($.fn, 'trigger').andCallThrough();
state.videoPlayer.onStateChange({
data: YT.PlayerState.PAUSED
......@@ -223,7 +222,7 @@ function (VideoPlayer) {
});
it('pause the video caption', function () {
expect(state.videoCaption.pause).toHaveBeenCalled();
expect($.fn.trigger).toHaveBeenCalledWith('pause', {});
});
});
......@@ -245,7 +244,7 @@ function (VideoPlayer) {
spyOn(state.videoPlayer, 'log').andCallThrough();
spyOn(window, 'setInterval').andReturn(100);
spyOn(state.videoControl, 'play');
spyOn(state.videoCaption, 'play');
spyOn($.fn, 'trigger').andCallThrough();
state.videoPlayer.onStateChange({
data: YT.PlayerState.PLAYING
......@@ -281,7 +280,7 @@ function (VideoPlayer) {
});
it('play the video caption', function () {
expect(state.videoCaption.play).toHaveBeenCalled();
expect($.fn.trigger).toHaveBeenCalledWith('play', {});
});
});
......@@ -295,7 +294,7 @@ function (VideoPlayer) {
spyOn(state.videoPlayer, 'log').andCallThrough();
spyOn(state.videoControl, 'pause').andCallThrough();
spyOn(state.videoCaption, 'pause').andCallThrough();
spyOn($.fn, 'trigger').andCallThrough();
state.videoPlayer.onStateChange({
data: YT.PlayerState.PLAYING
......@@ -323,7 +322,7 @@ function (VideoPlayer) {
});
it('pause the video caption', function () {
expect(state.videoCaption.pause).toHaveBeenCalled();
expect($.fn.trigger).toHaveBeenCalledWith('pause', {});
});
});
......@@ -334,7 +333,7 @@ function (VideoPlayer) {
state.videoEl = $('video, iframe');
spyOn(state.videoControl, 'pause').andCallThrough();
spyOn(state.videoCaption, 'pause').andCallThrough();
spyOn($.fn, 'trigger').andCallThrough();
state.videoPlayer.onStateChange({
data: YT.PlayerState.ENDED
......@@ -346,7 +345,7 @@ function (VideoPlayer) {
});
it('pause the video caption', function () {
expect(state.videoCaption.pause).toHaveBeenCalled();
expect($.fn.trigger).toHaveBeenCalledWith('ended', {});
});
});
});
......@@ -709,6 +708,7 @@ function (VideoPlayer) {
describe('updatePlayTime with invalid endTime', function () {
beforeEach(function () {
state = {
el: $('#video_id'),
videoPlayer: {
duration: function () {
// The video will be 60 seconds long.
......@@ -756,10 +756,7 @@ function (VideoPlayer) {
describe('when the video player is not full screen', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
state.videoEl = $('video, iframe');
spyOn(state.videoCaption, 'resize').andCallThrough();
spyOn($.fn, 'trigger').andCallThrough();
state.videoControl.toggleFullScreen(jQuery.Event('click'));
});
......@@ -774,7 +771,7 @@ function (VideoPlayer) {
});
it('tell VideoCaption to resize', function () {
expect(state.videoCaption.resize).toHaveBeenCalled();
expect($.fn.trigger).toHaveBeenCalledWith('fullscreen', [true]);
expect(state.resizer.setMode).toHaveBeenCalledWith('both');
expect(state.resizer.delta.substract).toHaveBeenCalled();
});
......@@ -783,11 +780,8 @@ function (VideoPlayer) {
describe('when the video player already full screen', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
state.videoEl = $('video, iframe');
spyOn(state.videoCaption, 'resize').andCallThrough();
spyOn($.fn, 'trigger').andCallThrough();
state.el.addClass('video-fullscreen');
state.videoControl.fullScreenState = true;
state.videoControl.isFullScreen = true;
......@@ -806,7 +800,7 @@ function (VideoPlayer) {
});
it('tell VideoCaption to resize', function () {
expect(state.videoCaption.resize).toHaveBeenCalled();
expect($.fn.trigger).toHaveBeenCalledWith('fullscreen', [false]);
expect(state.resizer.setMode)
.toHaveBeenCalledWith('width');
expect(state.resizer.delta.reset).toHaveBeenCalled();
......
......@@ -223,20 +223,20 @@ function (HTML5Video, Resizer) {
container: state.container
})
.callbacks.once(function() {
state.trigger('videoCaption.resize', null);
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.trigger('videoCaption.resize', null);
state.el.trigger('caption:resize');
});
}
$(window).on('resize', _.debounce(function () {
state.trigger('videoControl.updateControlsHeight', null);
state.trigger('videoCaption.resize', null);
state.el.trigger('caption:resize');
state.resizer.align();
}, 100));
}
......@@ -271,7 +271,7 @@ function (HTML5Video, Resizer) {
});
_updateVcrAndRegion(state, true);
state.trigger('videoCaption.fetchCaption', null);
state.el.trigger('caption:fetch');
state.resizer.setElement(state.el.find('iframe')).align();
}
......@@ -447,10 +447,6 @@ function (HTML5Video, Resizer) {
end: true
});
if (this.config.showCaptions) {
this.trigger('videoCaption.pause', null);
}
if (this.videoPlayer.skipOnEndedStartEndReset) {
this.videoPlayer.skipOnEndedStartEndReset = undefined;
}
......@@ -475,11 +471,6 @@ function (HTML5Video, Resizer) {
delete this.videoPlayer.updateInterval;
this.trigger('videoControl.pause', null);
if (this.config.showCaptions) {
this.trigger('videoCaption.pause', null);
}
this.saveState(true);
this.el.trigger('pause', arguments);
}
......@@ -501,17 +492,10 @@ function (HTML5Video, Resizer) {
}
this.trigger('videoControl.play', null);
this.trigger('videoProgressSlider.notifyThroughHandleEnd', {
end: false
});
if (this.config.showCaptions) {
this.trigger('videoCaption.play', null);
}
this.videoPlayer.ready();
this.el.trigger('play', arguments);
}
......@@ -803,7 +787,7 @@ function (HTML5Video, Resizer) {
}
);
this.trigger('videoCaption.updatePlayTime', time);
this.el.trigger('caption:update', [time]);
}
function isEnded() {
......
......@@ -277,7 +277,6 @@ function () {
.attr('title', text)
.text(text);
this.trigger('videoCaption.resize', null);
this.el.trigger('fullscreen', [this.isFullScreen]);
}
......
......@@ -5,734 +5,858 @@ define(
'video/09_video_caption.js',
['video/00_sjson.js', 'video/00_async_process.js'],
function (Sjson, AsyncProcess) {
/**
* @desc VideoCaption module exports a function.
*
* @type {function}
* @access public
*
* @param {object} state - The object containg the state of the video
* @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 {undefined}
* @returns {jquery Promise}
*/
return function (state) {
state.videoCaption = {};
_makeFunctionsPublic(state);
state.videoCaption.renderElements();
var VideoCaption = function (state) {
if (!(this instanceof VideoCaption)) {
return new VideoCaption(state);
}
this.state = state;
this.state.videoCaption = this;
this.renderElements();
return $.Deferred().resolve().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 = {
addPaddings: addPaddings,
bindHandlers: bindHandlers,
bottomSpacingHeight: bottomSpacingHeight,
calculateOffset: calculateOffset,
captionBlur: captionBlur,
captionClick: captionClick,
captionFocus: captionFocus,
captionHeight: captionHeight,
captionKeyDown: captionKeyDown,
captionMouseDown: captionMouseDown,
captionMouseOverOut: captionMouseOverOut,
fetchCaption: fetchCaption,
fetchAvailableTranslations: fetchAvailableTranslations,
hideCaptions: hideCaptions,
onMouseEnter: onMouseEnter,
onMouseLeave: onMouseLeave,
onMovement: onMovement,
pause: pause,
play: play,
renderCaption: renderCaption,
renderElements: renderElements,
renderLanguageMenu: renderLanguageMenu,
resize: resize,
scrollCaption: scrollCaption,
seekPlayer: seekPlayer,
setSubtitlesHeight: setSubtitlesHeight,
toggle: toggle,
topSpacingHeight: topSpacingHeight,
updatePlayTime: updatePlayTime
};
state.bindTo(methodsDict, state.videoCaption, state);
}
// ***************************************************************
// 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 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.
*
* @type {function}
* @access public
*
* @this {object} - The object containg the state of the video
* player. All other modules, their parameters, public variables, etc.
* are available via this object.
*
* @returns {boolean}
* true: The function fethched captions successfully, and compltely
* rendered everything related to captions.
* false: The captions were not fetched. Nothing will be rendered,
* and the CC button will be hidden.
*/
function renderElements() {
var Caption = this.videoCaption,
languages = this.config.transcriptLanguages;
Caption.loaded = false;
Caption.subtitlesEl = this.el.find('ol.subtitles');
Caption.container = this.el.find('.lang');
Caption.hideSubtitlesEl = this.el.find('a.hide-subtitles');
if (_.keys(languages).length) {
Caption.renderLanguageMenu(languages);
if (!Caption.fetchCaption()) {
Caption.hideCaptions(true);
Caption.hideSubtitlesEl.hide();
}
} else {
Caption.hideCaptions(true, false);
Caption.hideSubtitlesEl.hide();
}
}
// function bindHandlers()
//
// Bind any necessary function callbacks to DOM events (click,
// mousemove, etc.).
function bindHandlers() {
var self = this,
Caption = this.videoCaption,
events = [
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
'keydown'
].join(' ');
Caption.hideSubtitlesEl.on({
'click': Caption.toggle
});
Caption.subtitlesEl
.on({
mouseenter: Caption.onMouseEnter,
mouseleave: Caption.onMouseLeave,
mousemove: Caption.onMovement,
mousewheel: Caption.onMovement,
DOMMouseScroll: Caption.onMovement
})
.on(events, 'li[data-index]', function (event) {
switch (event.type) {
case 'mouseover':
case 'mouseout':
Caption.captionMouseOverOut(event);
break;
case 'mousedown':
Caption.captionMouseDown(event);
break;
case 'click':
Caption.captionClick(event);
break;
case 'focusin':
Caption.captionFocus(event);
break;
case 'focusout':
Caption.captionBlur(event);
break;
case 'keydown':
Caption.captionKeyDown(event);
break;
VideoCaption.prototype = {
/**
* @desc Initiate rendering of elements, and set their initial configuration.
*
*/
renderElements: function () {
var state = this.state,
languages = this.state.config.transcriptLanguages;
this.loaded = false;
this.subtitlesEl = state.el.find('ol.subtitles');
this.container = state.el.find('.lang');
this.hideSubtitlesEl = state.el.find('a.hide-subtitles');
if (_.keys(languages).length) {
this.renderLanguageMenu(languages);
if (!this.fetchCaption()) {
this.hideCaptions(true);
this.hideSubtitlesEl.hide();
}
});
if (Caption.showLanguageMenu) {
Caption.container.on({
mouseenter: onContainerMouseEnter,
mouseleave: onContainerMouseLeave
});
}
if ((this.videoType === 'html5') && (this.config.autohideHtml5)) {
Caption.subtitlesEl.on('scroll', this.videoControl.showControls);
}
}
function onContainerMouseEnter(event) {
event.preventDefault();
$(event.currentTarget).addClass('open');
}
function onContainerMouseLeave(event) {
event.preventDefault();
$(event.currentTarget).removeClass('open');
}
} else {
this.hideCaptions(true, false);
this.hideSubtitlesEl.hide();
}
},
/**
* @desc Bind any necessary function callbacks to DOM events (click,
* mousemove, etc.).
*
*/
bindHandlers: function () {
var self = this,
state = this.state,
events = [
'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur',
'keydown'
].join(' ');
// Change context to VideoCaption of event handlers using `bind`.
this.hideSubtitlesEl.on('click', this.toggle.bind(this));
this.subtitlesEl
.on({
mouseenter: this.onMouseEnter.bind(this),
mouseleave: this.onMouseLeave.bind(this),
mousemove: this.onMovement.bind(this),
mousewheel: this.onMovement.bind(this),
DOMMouseScroll: this.onMovement.bind(this)
})
.on(events, 'li[data-index]', function (event) {
switch (event.type) {
case 'mouseover':
case 'mouseout':
self.captionMouseOverOut(event);
break;
case 'mousedown':
self.captionMouseDown(event);
break;
case 'click':
self.captionClick(event);
break;
case 'focusin':
self.captionFocus(event);
break;
case 'focusout':
self.captionBlur(event);
break;
case 'keydown':
self.captionKeyDown(event);
break;
}
});
function onMouseEnter() {
if (this.videoCaption.frozen) {
clearTimeout(this.videoCaption.frozen);
}
if (this.showLanguageMenu) {
this.container.on({
mouseenter: this.onContainerMouseEnter,
mouseleave: this.onContainerMouseLeave
});
}
this.videoCaption.frozen = setTimeout(
this.videoCaption.onMouseLeave,
this.config.captionsFreezeTime
);
}
state.el
.on({
'caption:fetch': this.fetchCaption.bind(this),
'caption:resize': this.onResize.bind(this),
'caption:update': function (event, time) {
self.updatePlayTime(time);
},
'ended': this.pause,
'fullscreen': this.onResize.bind(this),
'pause': this.pause,
'play': this.play,
});
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
this.subtitlesEl.on('scroll', state.videoControl.showControls);
}
},
/**
* @desc Opens language menu.
*
* @param {jquery Event} event
*/
onContainerMouseEnter: function (event) {
event.preventDefault();
$(event.currentTarget).addClass('open');
},
/**
* @desc Closes language menu.
*
* @param {jquery Event} event
*/
onContainerMouseLeave: function (event) {
event.preventDefault();
$(event.currentTarget).removeClass('open');
},
/**
* @desc Freezes moving of captions when mouse is over them.
*
* @param {jquery Event} event
*/
onMouseEnter: function (event) {
if (this.frozen) {
clearTimeout(this.frozen);
}
function onMouseLeave() {
if (this.videoCaption.frozen) {
clearTimeout(this.videoCaption.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 (event) {
if (this.frozen) {
clearTimeout(this.frozen);
}
this.videoCaption.frozen = null;
this.frozen = null;
if (this.videoCaption.playing) {
this.videoCaption.scrollCaption();
}
}
if (this.playing) {
this.scrollCaption();
}
},
/**
* @desc Freezes moving of captions when mouse is moving over them.
*
* @param {jquery Event} event
*/
onMovement: function (event) {
this.onMouseEnter();
},
/**
* @desc Fetch the caption file specified by the user. Upon successful
* receipt of the file, the captions will be rendered.
*
* @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 () {
var self = this,
state = this.state,
language = state.getCurrentLanguage(),
data, youtubeId;
if (this.loaded) {
this.hideCaptions(false);
} else {
this.hideCaptions(state.hide_captions, false);
}
function onMovement() {
this.videoCaption.onMouseEnter();
}
if (this.fetchXHR && this.fetchXHR.abort) {
this.fetchXHR.abort();
}
/**
* @desc Fetch the caption file specified by the user. Upn successful
* receival of the file, the captions will be rendered.
*
* @type {function}
* @access public
*
* @this {object} - The object containg the state of the video
* player. All other modules, their parameters, public variables, etc.
* are available via this object.
*
* @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.
*/
function fetchCaption() {
var self = this,
Caption = self.videoCaption,
language = this.getCurrentLanguage(),
data;
if (Caption.loaded) {
Caption.hideCaptions(false);
} else {
Caption.hideCaptions(this.hide_captions, false);
}
if (state.videoType === 'youtube') {
youtubeId = state.youtubeId('1.0');
if (Caption.fetchXHR && Caption.fetchXHR.abort) {
Caption.fetchXHR.abort();
}
if (!youtubeId) {
return false;
}
if (this.videoType === 'youtube') {
data = {
videoId: this.youtubeId('1.0')
};
}
data = {
videoId: youtubeId
};
}
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
Caption.fetchXHR = $.ajaxWithPrefix({
url: self.config.transcriptTranslationUrl + '/' + language,
notifyOnError: false,
data: data,
success: function (response) {
Caption.sjson = new Sjson(response);
var start = Caption.sjson.getStartTimes(),
captions = Caption.sjson.getCaptions();
if (Caption.loaded) {
if (Caption.rendered) {
Caption.renderCaption(start, captions);
Caption.updatePlayTime(self.videoPlayer.currentTime);
}
} else {
if (self.isTouch) {
Caption.subtitlesEl.find('li').html(
gettext(
'Caption will be displayed when ' +
'you start playing the video.'
)
);
// Fetch the captions file. If no file was specified, or if an error
// occurred, then we hide the captions panel, and the "CC" button
this.fetchXHR = $.ajaxWithPrefix({
url: state.config.transcriptTranslationUrl + '/' + language,
notifyOnError: false,
data: data,
success: function (sjson) {
self.sjson = new Sjson(sjson);
var start = self.sjson.getStartTimes(),
captions = self.sjson.getCaptions();
if (self.loaded) {
if (self.rendered) {
self.renderCaption(start, captions);
self.updatePlayTime(state.videoPlayer.currentTime);
}
} else {
Caption.renderCaption(start, captions);
if (state.isTouch) {
self.subtitlesEl.find('li').html(
gettext(
'Caption will be displayed when ' +
'you start playing the video.'
)
);
} else {
self.renderCaption(start, captions);
}
self.bindHandlers();
}
Caption.bindHandlers();
self.loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
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 (_.keys(state.config.transcriptLanguages).length > 1) {
self.fetchAvailableTranslations();
} else {
self.hideCaptions(true, false);
self.hideSubtitlesEl.hide();
}
}
});
Caption.loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
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 (_.keys(self.config.transcriptLanguages).length > 1) {
Caption.fetchAvailableTranslations();
} else {
Caption.hideCaptions(true, false);
Caption.hideSubtitlesEl.hide();
}
}
});
return true;
}
function fetchAvailableTranslations() {
var self = this,
Caption = this.videoCaption;
return $.ajaxWithPrefix({
url: self.config.transcriptAvailableTranslationsUrl,
notifyOnError: false,
success: function (response) {
var currentLanguages = self.config.transcriptLanguages,
newLanguages = _.pick(currentLanguages, response);
// Update property with available currently translations.
self.config.transcriptLanguages = newLanguages;
// Remove an old language menu.
Caption.container.find('.langs-list').remove();
if (_.keys(newLanguages).length) {
// And try again to fetch transcript.
Caption.fetchCaption();
Caption.renderLanguageMenu(newLanguages);
return true;
},
/**
* @desc Fetch the list of available translations. Upon successful receipt,
* the list of available translations will be updated.
*
* @returns {jquery Promise}
*/
fetchAvailableTranslations: function () {
var self = this,
state = this.state;
return $.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) {
// And try again to fetch transcript.
self.fetchCaption();
self.renderLanguageMenu(newLanguages);
}
},
error: function (jqXHR, textStatus, errorThrown) {
self.hideCaptions(true, false);
self.hideSubtitlesEl.hide();
}
},
error: function (jqXHR, textStatus, errorThrown) {
Caption.hideCaptions(true, false);
Caption.hideSubtitlesEl.hide();
});
},
/**
* @desc Recalculates and updates the height of the container of captions.
*
*/
onResize: function () {
this.subtitlesEl
.find('.spacing').first()
.height(this.topSpacingHeight()).end()
.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();
if (_.keys(languages).length < 2) {
return false;
}
});
}
function resize() {
this.videoCaption.subtitlesEl
.find('.spacing:first')
.height(this.videoCaption.topSpacingHeight())
.find('.spacing:last')
.height(this.videoCaption.bottomSpacingHeight());
this.videoCaption.scrollCaption();
this.videoCaption.setSubtitlesHeight();
}
function renderLanguageMenu(languages) {
var self = this,
menu = $('<ol class="langs-list menu">'),
currentLang = this.getCurrentLanguage();
if (_.keys(languages).length < 2) {
return false;
}
this.videoCaption.showLanguageMenu = true;
this.showLanguageMenu = true;
$.each(languages, function(code, label) {
var li = $('<li data-lang-code="' + code + '" />'),
link = $('<a href="javascript:void(0);">' + label + '</a>');
$.each(languages, function(code, label) {
var li = $('<li data-lang-code="' + code + '" />'),
link = $('<a href="javascript:void(0);">' + label + '</a>');
if (currentLang === code) {
li.addClass('active');
}
if (currentLang === code) {
li.addClass('active');
}
li.append(link);
menu.append(li);
});
li.append(link);
menu.append(li);
});
this.videoCaption.container.append(menu);
this.container.append(menu);
menu.on('click', 'a', function (e) {
var el = $(e.currentTarget).parent(),
Caption = self.videoCaption,
langCode = el.data('lang-code');
menu.on('click', 'a', function (e) {
var el = $(e.currentTarget).parent(),
state = self.state,
langCode = el.data('lang-code');
if (self.lang !== langCode) {
self.lang = langCode;
self.storage.setItem('language', langCode);
el .addClass('active')
.siblings('li')
.removeClass('active');
if (state.lang !== langCode) {
state.lang = langCode;
state.storage.setItem('language', langCode);
el .addClass('active')
.siblings('li')
.removeClass('active');
Caption.fetchCaption();
}
});
}
function buildCaptions (container, start, captions) {
var process = function(text, index) {
var liEl = $('<li>', {
'data-index': index,
'data-start': start[index],
'tabindex': 0
}).html(text);
return liEl[0];
self.fetchCaption();
}
});
},
/**
* @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 liEl = $('<li>', {
'data-index': index,
'data-start': start[index],
'tabindex': 0
}).html(text);
return liEl[0];
};
return AsyncProcess.array(captions, process).done(function (list) {
container.append(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;
};
return AsyncProcess.array(captions, process).done(function (list) {
container.append(list);
});
}
function renderCaption(start, captions) {
var Caption = this.videoCaption;
var onRender = function () {
Caption.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.
Caption.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.
Caption.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).
Caption.isMouseFocus = false;
Caption.rendered = true;
};
Caption.rendered = false;
Caption.subtitlesEl.empty();
Caption.setSubtitlesHeight();
buildCaptions(Caption.subtitlesEl, start, captions).done(onRender);
}
function addPaddings() {
// Set top and bottom spacing height and make sure they are taken out of
// the tabbing order.
this.videoCaption.subtitlesEl
.prepend(
$('<li class="spacing">')
.height(this.videoCaption.topSpacingHeight())
.attr('tabindex', -1)
)
.append(
$('<li class="spacing">')
.height(this.videoCaption.bottomSpacingHeight())
.attr('tabindex', -1)
);
}
// On mouseOver, hide the outline of a caption that has been tabbed to.
// On mouseOut, show the outline of a caption that has been tabbed to.
function captionMouseOverOut(event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
if (captionIndex === this.videoCaption.currentCaptionIndex) {
if (event.type === 'mouseover') {
caption.removeClass('focused');
}
else { // mouseout
caption.addClass('focused');
this.rendered = false;
this.subtitlesEl.empty();
this.setSubtitlesHeight();
this.buildCaptions(this.subtitlesEl, start, captions).done(onRender);
},
/**
* @desc Sets top and bottom spacing height and make sure they are taken
* out of the tabbing order.
*
*/
addPaddings: function () {
this.subtitlesEl
.prepend(
$('<li class="spacing">')
.height(this.topSpacingHeight())
.attr('tabindex', -1)
)
.append(
$('<li class="spacing">')
.height(this.bottomSpacingHeight())
.attr('tabindex', -1)
);
},
/**
* @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');
}
}
}
}
function captionMouseDown(event) {
var caption = $(event.target);
this.videoCaption.isMouseFocus = true;
this.videoCaption.autoScrolling = true;
caption.removeClass('focused');
this.videoCaption.currentCaptionIndex = -1;
}
function captionClick(event) {
this.videoCaption.seekPlayer(event);
}
function captionFocus(event) {
var caption = $(event.target),
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.videoCaption.isMouseFocus) {
this.videoCaption.autoScrolling = true;
},
/**
* @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.videoCaption.currentCaptionIndex = -1;
}
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
else {
this.videoCaption.currentCaptionIndex = captionIndex;
caption.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.videoCaption.sjson.getSize() - 2
) {
this.videoCaption.autoScrolling = false;
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),
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;
caption.removeClass('focused');
this.currentCaptionIndex = -1;
}
}
}
function captionBlur(event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
caption.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.videoCaption.sjson.getSize() - 1) {
this.videoCaption.autoScrolling = true;
}
}
function captionKeyDown(event) {
this.videoCaption.isMouseFocus = false;
if (event.which === 13) { //Enter key
this.videoCaption.seekPlayer(event);
}
}
function scrollCaption() {
var el = this.videoCaption.subtitlesEl.find('.current:first');
// Automatic scrolling gets disabled if one of the captions has
// received focus through tabbing.
if (
!this.videoCaption.frozen &&
el.length &&
this.videoCaption.autoScrolling
) {
this.videoCaption.subtitlesEl.scrollTo(
el,
{
offset: -this.videoCaption.calculateOffset(el)
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
else {
this.currentCaptionIndex = captionIndex;
caption.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;
}
);
}
}
function play() {
if (this.videoCaption.loaded) {
if (!this.videoCaption.rendered) {
var start = this.videoCaption.sjson.getStartTimes(),
captions = this.videoCaption.sjson.getCaptions();
this.videoCaption.renderCaption(start, captions);
}
},
/**
* @desc Handles blur event on concrete caption.
*
* @param {jquery Event} event
*
*/
captionBlur: function (event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
this.videoCaption.playing = true;
}
}
function pause() {
if (this.videoCaption.loaded) {
this.videoCaption.playing = false;
}
}
function updatePlayTime(time) {
var newIndex;
if (this.videoCaption.loaded) {
if (this.isFlashMode()) {
time = Time.convert(time, this.speed, '1.0');
caption.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);
}
},
time = Math.round(time * 1000 + 100);
newIndex = this.videoCaption.sjson.search(time);
/**
* @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 (
typeof newIndex !== 'undefined' &&
newIndex !== -1 &&
this.videoCaption.currentIndex !== newIndex
!this.frozen &&
el.length &&
this.autoScrolling
) {
if (typeof this.videoCaption.currentIndex !== 'undefined') {
this.videoCaption.subtitlesEl
.find('li.current')
.removeClass('current');
this.subtitlesEl.scrollTo(
el,
{
offset: -1 * this.calculateOffset(el)
}
);
}
},
/**
* @desc Updates flags on play
*
*/
play: function () {
if (this.loaded) {
if (!this.rendered) {
var start = this.sjson.getStartTimes(),
captions = this.sjson.getCaptions();
this.renderCaption(start, captions);
}
this.videoCaption.subtitlesEl
.find("li[data-index='" + newIndex + "']")
.addClass('current');
this.videoCaption.currentIndex = newIndex;
this.videoCaption.scrollCaption();
this.playing = true;
}
}
}
function seekPlayer(event) {
var time = parseInt($(event.target).data('start'), 10);
if (this.isFlashMode()) {
time = Math.round(Time.convert(time, '1.0', this.speed));
}
this.trigger(
'videoPlayer.onCaptionSeek',
{
'type': 'onCaptionSeek',
'time': time/1000
},
/**
* @desc Updates flags on pause
*
*/
pause: function () {
if (this.loaded) {
this.playing = false;
}
);
event.preventDefault();
}
function calculateOffset(element) {
return this.videoCaption.captionHeight() / 2 - element.height() / 2;
}
function topSpacingHeight() {
return this.videoCaption.calculateOffset(
this.videoCaption.subtitlesEl.find('li:not(.spacing):first')
);
}
function bottomSpacingHeight() {
return this.videoCaption.calculateOffset(
this.videoCaption.subtitlesEl.find('li:not(.spacing):last')
);
}
function toggle(event) {
event.preventDefault();
if (this.el.hasClass('closed')) {
this.videoCaption.hideCaptions(false);
} else {
this.videoCaption.hideCaptions(true);
}
}
},
/**
* @desc Updates captions UI on paying.
*
* @param {number} time Time in seconds.
*
*/
updatePlayTime: function (time) {
var state = this.state,
newIndex;
if (this.loaded) {
if (state.isFlashMode()) {
time = Time.convert(time, state.speed, '1.0');
}
function hideCaptions(hide_captions, update_cookie) {
var hideSubtitlesEl = this.videoCaption.hideSubtitlesEl,
type, text;
time = Math.round(time * 1000 + 100);
newIndex = this.sjson.search(time);
if (
typeof newIndex !== 'undefined' &&
newIndex !== -1 &&
this.currentIndex !== newIndex
) {
if (typeof this.currentIndex !== 'undefined') {
this.subtitlesEl
.find('li.current')
.removeClass('current');
}
if (typeof update_cookie === 'undefined') {
update_cookie = true;
}
this.subtitlesEl
.find("li[data-index='" + newIndex + "']")
.addClass('current');
if (hide_captions) {
type = 'hide_transcript';
this.captionsHidden = true;
this.currentIndex = newIndex;
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));
}
this.el.addClass('closed');
state.trigger(
'videoPlayer.onCaptionSeek',
{
'type': 'onCaptionSeek',
'time': time/1000
}
);
text = gettext('Turn on captions');
} else {
type = 'show_transcript';
this.captionsHidden = false;
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()
);
},
/**
* @desc Shows/Hides captions on click `CC` button
*
* @param {jquery Event} event
*
*/
toggle: function (event) {
event.preventDefault();
if (this.state.el.hasClass('closed')) {
this.hideCaptions(false);
} else {
this.hideCaptions(true);
}
},
/**
* @desc Shows/Hides captions and updates the cookie.
*
* @param {boolean} hide_captions if `true` hides the caption,
* otherwise - show.
* @param {boolean} update_cookie Flag to update or not the cookie.
*
*/
hideCaptions: function (hide_captions, update_cookie) {
var hideSubtitlesEl = this.hideSubtitlesEl,
state = this.state,
type, text;
if (typeof update_cookie === 'undefined') {
update_cookie = true;
}
this.el.removeClass('closed');
this.videoCaption.scrollCaption();
if (hide_captions) {
type = 'hide_transcript';
state.captionsHidden = true;
state.el.addClass('closed');
text = gettext('Turn on captions');
} else {
type = 'show_transcript';
state.captionsHidden = false;
state.el.removeClass('closed');
this.scrollCaption();
text = gettext('Turn off captions');
}
text = gettext('Turn off captions');
}
hideSubtitlesEl
.attr('title', text)
.text(gettext(text));
hideSubtitlesEl
.attr('title', text)
.text(gettext(text));
if (state.videoPlayer) {
state.videoPlayer.log(type, {
currentTime: state.videoPlayer.currentTime
});
}
if (this.videoPlayer) {
this.videoPlayer.log(type, {
currentTime: this.videoPlayer.currentTime
});
}
if (state.resizer) {
if (state.isFullScreen) {
state.resizer.setMode('both');
} else {
state.resizer.alignByWidthOnly();
}
}
if (this.resizer) {
if (this.isFullScreen) {
this.resizer.setMode('both');
this.setSubtitlesHeight();
if (update_cookie) {
$.cookie('hide_captions', hide_captions, {
expires: 3650,
path: '/'
});
}
},
/**
* @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.videoControl.height;
} else {
this.resizer.alignByWidthOnly();
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 && state.hide_captions) ||
state.captionsHidden === true
) {
// In case of html5 autoshowing subtitles, we adjust height of
// subs, by height of scrollbar.
height = state.videoControl.el.height() +
0.5 * state.videoControl.sliderEl.height();
// Height of videoControl does not contain height of slider.
// css is set to absolute, to avoid yanking when slider
// autochanges its height.
}
}
this.videoCaption.setSubtitlesHeight();
if (update_cookie) {
$.cookie('hide_captions', hide_captions, {
expires: 3650,
path: '/'
this.subtitlesEl.css({
maxHeight: this.captionHeight() - height
});
}
}
function captionHeight() {
if (this.isFullScreen) {
return this.container.height() - this.videoControl.height;
} else {
return this.container.height();
}
}
function setSubtitlesHeight() {
var height = 0;
// on page load captionHidden = undefined
if ((this.captionsHidden === undefined && this.hide_captions) ||
this.captionsHidden === true
) {
// In case of html5 autoshowing subtitles, we adjust height of
// subs, by height of scrollbar.
height = this.videoControl.el.height() +
0.5 * this.videoControl.sliderEl.height();
// Height of videoControl does not contain height of slider.
// css is set to absolute, to avoid yanking when slider
// autochanges its height.
}
};
this.videoCaption.subtitlesEl.css({
maxHeight: this.videoCaption.captionHeight() - height
});
}
return VideoCaption;
});
}(RequireJS.define));
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment