diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b7d1d7a5ce..0c636e4390 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,16 @@ is enabled. Studio: Added improvements to Course Creation: richer error messaging, tip text, and fourth field for course run. +Blades: New features for VideoAlpha player: +1.) Controls are auto hidden after a delay of mouse inactivity - the full video +becomes visible. +2.) When captions (CC) button is pressed, captions stick (not auto hidden after +a delay of mouse inactivity). The video player size does not change - the video +is down-sized and placed in the middle of the black area. +3.) All source code of Video Alpha 2 is written in JavaScript. It is not a basic +conversion from CoffeeScript. The structure of the player has been changed. +4.) A lot of additional unit tests. + LMS: Added user preferences (arbitrary user/key/value tuples, for which which user/key is unique) and a REST API for reading users and preferences. Access to the REST API is restricted by use of the diff --git a/common/lib/xmodule/.gitignore b/common/lib/xmodule/.gitignore deleted file mode 100644 index cc2973bc1e..0000000000 --- a/common/lib/xmodule/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -test.mp4 -test.ogv -test.webm -jasmine_test_runner.html diff --git a/common/lib/xmodule/test_files/test.mp4 b/common/lib/xmodule/test_files/test.mp4 new file mode 100644 index 0000000000..81d11df5f2 Binary files /dev/null and b/common/lib/xmodule/test_files/test.mp4 differ diff --git a/common/lib/xmodule/test_files/test.ogv b/common/lib/xmodule/test_files/test.ogv new file mode 100644 index 0000000000..e128d82b5c Binary files /dev/null and b/common/lib/xmodule/test_files/test.ogv differ diff --git a/common/lib/xmodule/test_files/test.webm b/common/lib/xmodule/test_files/test.webm new file mode 100644 index 0000000000..42e2130ca4 Binary files /dev/null and b/common/lib/xmodule/test_files/test.webm differ diff --git a/common/lib/xmodule/xmodule/css/videoalpha/display.scss b/common/lib/xmodule/xmodule/css/videoalpha/display.scss index fc746fa846..4db641a99a 100644 --- a/common/lib/xmodule/xmodule/css/videoalpha/display.scss +++ b/common/lib/xmodule/xmodule/css/videoalpha/display.scss @@ -12,6 +12,7 @@ div.videoalpha { div.tc-wrapper { position: relative; + @include clearfix; } article.video-wrapper { @@ -82,7 +83,7 @@ div.videoalpha { -moz-transition: -moz-transform 0.7s ease-in-out; -ms-transition: -ms-transform 0.7s ease-in-out; transition: transform 0.7s ease-in-out; - @include transform(scaleY(0.5) translateY(50%)); + @include transform(scaleY(0.5) translate3d(0, 50%, 0)); div.ui-widget-header { background: #777; @@ -244,7 +245,7 @@ div.videoalpha { // fix for now ol.video_speeds { - box-shadow: inset 1px 0 0 #555, 0 3px 0 #444; + box-shadow: inset 1px 0 0 #555, 0 4px 0 #444; @include transition(none); background-color: #444; border: 1px solid #000; @@ -252,7 +253,7 @@ div.videoalpha { display: none; opacity: 0.0; position: absolute; - width: 133px; + width: 131px; z-index: 10; li { @@ -454,7 +455,7 @@ div.videoalpha { } div.slider { - @include transform(scaleY(1) translateY(0)); + @include transform(scaleY(1) translate3d(0, 0, 0)); a.ui-slider-handle { @include transform(scale(1) translate3d(-50%, -15%, 0)); @@ -509,6 +510,7 @@ div.videoalpha { left: 0px; right: 0px; position: absolute; + z-index: 1; } article.video-wrapper section.video-controls div.secondary-controls a.hide-subtitles { @@ -528,11 +530,14 @@ div.videoalpha { ol.subtitles.html5 { background-color: rgba(243, 243, 243, 0.8); - height: 380px; + height: 100%; position: absolute; right: 0; + bottom: 0; + top: 0; width: 275px; - margin-top: 20px; + padding: 0 20px; + z-index: 0; } } diff --git a/common/lib/xmodule/xmodule/js/fixtures/videoalpha_all.html b/common/lib/xmodule/xmodule/js/fixtures/videoalpha_all.html index 1dd7e7c44e..f26cbde151 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/videoalpha_all.html +++ b/common/lib/xmodule/xmodule/js/fixtures/videoalpha_all.html @@ -9,9 +9,9 @@ data-end="" data-caption-asset-path="/static/subs/" data-sub="test_name_of_the_subtitles" - data-mp4-source="test.mp4" - data-webm-source="test.webm" - data-ogg-source="test.ogv" + data-mp4-source="test_files/test.mp4" + data-webm-source="test_files/test.webm" + data-ogg-source="test_files/test.ogv" data-autoplay="False" >
diff --git a/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html b/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html index 0663541bac..f44e0ca66e 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html +++ b/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html @@ -9,9 +9,9 @@ data-end="" data-caption-asset-path="/static/subs/" data-sub="test_name_of_the_subtitles" - data-mp4-source="test.mp4" - data-webm-source="test.webm" - data-ogg-source="test.ogv" + data-mp4-source="test_files/test.mp4" + data-webm-source="test_files/test.webm" + data-ogg-source="test_files/test.ogv" data-autoplay="False" >
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/general_spec.js b/common/lib/xmodule/xmodule/js/spec/videoalpha/general_spec.js index 00f47dacaf..353480268a 100644 --- a/common/lib/xmodule/xmodule/js/spec/videoalpha/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/general_spec.js @@ -117,9 +117,9 @@ it('parse Html5 sources', function () { var html5Sources = { - mp4: 'test.mp4', - webm: 'test.webm', - ogg: 'test.ogv' + mp4: 'test_files/test.mp4', + webm: 'test_files/test.webm', + ogg: 'test_files/test.ogv' }; expect(state.html5Sources).toEqual(html5Sources); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/01_initialize.js b/common/lib/xmodule/xmodule/js/src/videoalpha/01_initialize.js index 19a7de4cc8..a1222d084d 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/01_initialize.js @@ -134,8 +134,8 @@ function (VideoPlayer) { ) { VideoPlayer(state); } else { - onPlayerReadyFunc = (this.videoType === 'youtube') ? 'onYouTubePlayerAPIReady' : 'onHTML5PlayerAPIReady'; - window[onPlayerReadyFunc] = _.bind(window.VideoPlayer, state); + onPlayerReadyFunc = (state.videoType === 'youtube') ? 'onYouTubePlayerAPIReady' : 'onHTML5PlayerAPIReady'; + window[onPlayerReadyFunc] = _.bind(VideoPlayer, window, state); } } @@ -341,70 +341,30 @@ function (VideoPlayer) { return this.metadata[this.youtubeId()].duration; } - /* The function .trigger() expects the parameter @callType one of - * - * 'event' - * 'method' - * - * The default value (if @callType and @eventName are not specified) is 'method'. Based on this parameter, this - * function can be used in two ways. - * - * - * - * First use: A safe way to trigger jQuery events. - * ----------------------------------------------- - * - * @callType === 'event' - * - * Because jQuery events can be triggered on some jQuery object, we must make sure that - * we don't trigger an event on an undefined object. For this we will have an in-between - * method that will check for the existance of an object before triggering an event on it. - * - * @objChain is an array that contains the chain of properties on the 'state' object. For - * example, if - * - * objChain = ['videoPlayer', 'stopVideo']; - * - * then we will check for the existance of the - * - * state.videoPlayer.stopVideo - * - * object, and, if found to be present, will trigger the specified event on this object. - * - * @eventName is a string the name of the event to trigger on the specified object. - * - * @extraParameters is an object or an array that should be passed to the triggered method. - * - * - * Second use: A safe way to call methods. - * --------------------------------------- - * - * @callType === 'method' - * - * Parameter @eventName is NOT necessary. - * + /* * 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}, 'method'); + * state.trigger('videoPlayer.pause', {'param1': 10}); * * Then trigger() will execute: * * state.videoPlayer.pause({'param1': 10}); */ - function trigger(objChain, extraParameters, callType, eventName) { - var i, tmpObj; + function trigger(objChain, extraParameters) { + var 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 'objChain' chain of object is + // 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 (objChain.length) { - i = objChain.shift(); + while (chain.length) { + i = chain.shift(); if (tmpObj.hasOwnProperty(i)) { tmpObj = tmpObj[i]; @@ -415,18 +375,7 @@ function (VideoPlayer) { } } - if ((typeof callType === 'undefined') && (typeof eventName === 'undefined')) { - callType = 'method'; - } - - // Based on the type, either trigger, or invoke. - if (callType === 'event') { - tmpObj.trigger(eventName, extraParameters); - } else if (callType === 'method') { - tmpObj(extraParameters); - } else { - return false; - } + tmpObj(extraParameters); return true; } diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/02_html5_video.js b/common/lib/xmodule/xmodule/js/src/videoalpha/02_html5_video.js index ed76065efb..824b199c8f 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/02_html5_video.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/02_html5_video.js @@ -218,6 +218,7 @@ function () { // The player state is used by other parts of the VideoPlayer to detrermine what the video is // currently doing. this.video = this.videoEl[0]; + this.video.load(); this.playerState = HTML5Video.PlayerState.UNSTARTED; // this.callStateChangeCallback(); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/03_video_player.js b/common/lib/xmodule/xmodule/js/src/videoalpha/03_video_player.js index fbbdb20e58..1b9a14cf73 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/03_video_player.js @@ -225,7 +225,7 @@ function (HTML5Video) { } } - // Every 200 ms, if the video is playing, we call the function update, via + // 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. @@ -252,10 +252,10 @@ function (HTML5Video) { } function onEnded() { - this.trigger(['videoControl','pause'], null); + this.trigger('videoControl.pause', null); if (this.config.show_captions) { - this.trigger(['videoCaption','pause'], null); + this.trigger('videoCaption.pause', null); } } @@ -270,10 +270,10 @@ function (HTML5Video) { clearInterval(this.videoPlayer.updateInterval); delete this.videoPlayer.updateInterval; - this.trigger(['videoControl','pause'], null); + this.trigger('videoControl.pause', null); if (this.config.show_captions) { - this.trigger(['videoCaption','pause'], null); + this.trigger('videoCaption.pause', null); } } @@ -289,10 +289,10 @@ function (HTML5Video) { this.videoPlayer.updateInterval = setInterval(this.videoPlayer.update, 200); } - this.trigger(['videoControl','play'], null); + this.trigger('videoControl.play', null); if (this.config.show_captions) { - this.trigger(['videoCaption','play'], null); + this.trigger('videoCaption.play', null); } } @@ -307,7 +307,7 @@ function (HTML5Video) { quality = this.videoPlayer.player.getPlaybackQuality(); - this.trigger(['videoQualityControl', 'onQualityChange'], quality); + this.trigger('videoQualityControl.onQualityChange', quality); } function onReady() { @@ -327,7 +327,7 @@ function (HTML5Video) { baseSpeedSubs = this.videos['1.0']; _this = this; - // this.videos is a dictionary containing various frame rates + // this.videos is a dictionary containing various frame rates // and their associated subs. // First clear the dictionary. @@ -342,7 +342,7 @@ function (HTML5Video) { _this.speeds.push(value.toFixed(2).replace(/\.00$/, '.0')); }); - this.trigger(['videoSpeedControl', 'reRender'], {'newSpeeds': this.speeds, 'currentSpeed': this.speed}); + this.trigger('videoSpeedControl.reRender', {'newSpeeds': this.speeds, 'currentSpeed': this.speed}); this.setSpeed($.cookie('video_speed')); } @@ -379,9 +379,9 @@ function (HTML5Video) { duration = this.videoPlayer.duration(); - this.trigger(['videoProgressSlider', 'updatePlayTime'], {'time': time, 'duration': duration}); - this.trigger(['videoControl', 'updateVcrVidTime'], {'time': time, 'duration': duration}); - this.trigger(['videoCaption', 'updatePlayTime'], time); + this.trigger('videoProgressSlider.updatePlayTime', {'time': time, 'duration': duration}); + this.trigger('videoControl.updateVcrVidTime', {'time': time, 'duration': duration}); + this.trigger('videoCaption.updatePlayTime', time); } function isPlaying() { diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/04_video_control.js b/common/lib/xmodule/xmodule/js/src/videoalpha/04_video_control.js index fe72d55b6d..85e5c27131 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/04_video_control.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/04_video_control.js @@ -146,9 +146,9 @@ function () { event.preventDefault(); if (this.videoControl.isPlaying) { - this.trigger(['videoPlayer', 'pause'], null); + this.trigger('videoPlayer.pause', null); } else { - this.trigger(['videoPlayer', 'play'], null); + this.trigger('videoPlayer.play', null); } } @@ -167,7 +167,7 @@ function () { this.videoControl.fullScreenEl.attr('title', 'Exit fullscreen'); } - this.trigger(['videoCaption', 'resize'], null); + this.trigger('videoCaption.resize', null); } function exitFullScreen(event) { diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/05_video_quality_control.js b/common/lib/xmodule/xmodule/js/src/videoalpha/05_video_quality_control.js index 6ea48cea9f..597531b01a 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/05_video_quality_control.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/05_video_quality_control.js @@ -90,7 +90,7 @@ function () { newQuality = 'hd720'; } - this.trigger(['videoPlayer', 'handlePlaybackQualityChange'], newQuality); + this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality); } }); diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/06_video_progress_slider.js b/common/lib/xmodule/xmodule/js/src/videoalpha/06_video_progress_slider.js index 219d5b73a5..e2d9f8dd1f 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/06_video_progress_slider.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/06_video_progress_slider.js @@ -19,7 +19,7 @@ function () { makeFunctionsPublic(state); renderElements(state); - bindHandlers(state); + // No callbacks to DOM events (click, mousemove, etc.). }; // *************************************************************** @@ -54,13 +54,6 @@ function () { } } - // function bindHandlers(state) - // - // Bind any necessary function callbacks to DOM events (click, mousemove, etc.). - function bindHandlers(state) { - - } - function buildSlider(state) { state.videoProgressSlider.slider = state.videoProgressSlider.el.slider({ range: 'min', @@ -100,7 +93,7 @@ function () { this.videoProgressSlider.frozen = true; this.videoProgressSlider.updateTooltip(ui.value); - this.trigger(['videoPlayer', 'onSlideSeek'], {'type': 'onSlideSeek', 'time': ui.value}); + this.trigger('videoPlayer.onSlideSeek', {'type': 'onSlideSeek', 'time': ui.value}); } function onChange(event, ui) { @@ -112,7 +105,7 @@ function () { this.videoProgressSlider.frozen = true; - this.trigger(['videoPlayer', 'onSlideSeek'], {'type': 'onSlideSeek', 'time': ui.value}); + this.trigger('videoPlayer.onSlideSeek', {'type': 'onSlideSeek', 'time': ui.value}); setTimeout(function() { _this.videoProgressSlider.frozen = false; diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/07_video_volume_control.js b/common/lib/xmodule/xmodule/js/src/videoalpha/07_video_volume_control.js index 64c80b8324..5e8663265f 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/07_video_volume_control.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/07_video_volume_control.js @@ -94,7 +94,7 @@ function () { path: '/' }); - this.trigger(['videoPlayer', 'onVolumeChange'], ui.value); + this.trigger('videoPlayer.onVolumeChange', ui.value); } function toggleMute(event) { diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/videoalpha/08_video_speed_control.js index a6fc90d2d0..3185ac1bab 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/08_video_speed_control.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/08_video_speed_control.js @@ -105,7 +105,7 @@ function () { parseFloat(this.videoSpeedControl.currentSpeed).toFixed(2).replace(/\.00$/, '.0') ); - this.trigger(['videoPlayer', 'onSpeedChange'], this.videoSpeedControl.currentSpeed); + this.trigger('videoPlayer.onSpeedChange', this.videoSpeedControl.currentSpeed); } } diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/videoalpha/09_video_caption.js index dc0dc440d1..bfe6023eb3 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/09_video_caption.js @@ -65,7 +65,7 @@ function () { this.el.find('.video-controls .secondary-controls').append(this.videoCaption.hideSubtitlesEl); this.el.find('.subtitles').css({ - maxHeight: this.el.find('.video-wrapper').height() - 5 + maxHeight: this.el.find('.video-wrapper').height() }); this.videoCaption.fetchCaption(); @@ -329,7 +329,7 @@ function () { event.preventDefault(); time = Math.round(Time.convert($(event.target).data('start'), '1.0', this.speed) / 1000); - this.trigger(['videoPlayer', 'onCaptionSeek'], {'type': 'onCaptionSeek', 'time': time}); + this.trigger('videoPlayer.onCaptionSeek', {'type': 'onCaptionSeek', 'time': time}); } function calculateOffset(element) {