From 2f572a86ea08e2f17bbca431de1820edb90b7ed1 Mon Sep 17 00:00:00 2001 From: polesye Date: Mon, 31 Mar 2014 13:40:50 +0300 Subject: [PATCH] Refactor video caption module. --- .../js/spec/video/video_caption_spec.js | 26 +- .../js/spec/video/video_player_spec.js | 30 +- .../xmodule/js/src/video/03_video_player.js | 26 +- .../xmodule/js/src/video/04_video_control.js | 1 - .../xmodule/js/src/video/09_video_caption.js | 1526 +++++++++-------- 5 files changed, 849 insertions(+), 760 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js index 34ae9e9b66..3bc7b5c858 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js @@ -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(); }); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js index b5811e01e7..0b569faf6f 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js @@ -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(); diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js index 19fad9bd1c..5d6ca56c94 100644 --- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js @@ -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() { diff --git a/common/lib/xmodule/xmodule/js/src/video/04_video_control.js b/common/lib/xmodule/xmodule/js/src/video/04_video_control.js index 205d062551..4d2b3adcd9 100644 --- a/common/lib/xmodule/xmodule/js/src/video/04_video_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/04_video_control.js @@ -277,7 +277,6 @@ function () { .attr('title', text) .text(text); - this.trigger('videoCaption.resize', null); this.el.trigger('fullscreen', [this.isFullScreen]); } diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js index 3fd5832f8a..71e7155c7f 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js @@ -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. - // *************************************************************** + VideoCaption.prototype = { + /** + * @desc Initiate rendering of elements, and set their initial configuration. + * + */ + renderElements: function () { + var state = this.state, + languages = this.state.config.transcriptLanguages; - // 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 - }; + this.loaded = false; + this.subtitlesEl = state.el.find('ol.subtitles'); + this.container = state.el.find('.lang'); + this.hideSubtitlesEl = state.el.find('a.hide-subtitles'); - state.bindTo(methodsDict, state.videoCaption, state); - } + if (_.keys(languages).length) { + this.renderLanguageMenu(languages); - // *************************************************************** - // 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; + 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'); - } - - function onMouseEnter() { - if (this.videoCaption.frozen) { - clearTimeout(this.videoCaption.frozen); - } - - this.videoCaption.frozen = setTimeout( - this.videoCaption.onMouseLeave, - this.config.captionsFreezeTime - ); - } - - function onMouseLeave() { - if (this.videoCaption.frozen) { - clearTimeout(this.videoCaption.frozen); - } - - this.videoCaption.frozen = null; - - if (this.videoCaption.playing) { - this.videoCaption.scrollCaption(); - } - } - - function onMovement() { - this.videoCaption.onMouseEnter(); - } - - /** - * @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 (Caption.fetchXHR && Caption.fetchXHR.abort) { - Caption.fetchXHR.abort(); - } - - if (this.videoType === 'youtube') { - data = { - videoId: this.youtubeId('1.0') - }; - } - - // 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.' - ) - ); - } else { - Caption.renderCaption(start, captions); - } - - Caption.bindHandlers(); - } - - 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); - } - }, - error: function (jqXHR, textStatus, errorThrown) { - Caption.hideCaptions(true, false); - Caption.hideSubtitlesEl.hide(); - } - }); - } - - 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 = $('