From 153bc25d8c155315e9a774b7082c2dacd44a7207 Mon Sep 17 00:00:00 2001 From: polesye Date: Wed, 6 Nov 2013 17:23:01 +0200 Subject: [PATCH] BLD-502: Add improvements to Video player. --- CHANGELOG.rst | 7 + .../contentstore/features/video.feature | 12 +- cms/djangoapps/contentstore/features/video.py | 47 +++--- cms/djangoapps/contentstore/views/item.py | 1 + .../xmodule/xmodule/css/video/display.scss | 112 +++++++------ .../xmodule/js/spec/video/resizer_spec.js | 79 +++++++++ .../xmodule/js/src/video/00_resizer.js | 73 ++++++++- .../xmodule/js/src/video/01_initialize.js | 52 +++++- .../xmodule/js/src/video/025_focus_grabber.js | 5 + .../xmodule/js/src/video/03_video_player.js | 56 ++++--- .../xmodule/js/src/video/04_video_control.js | 5 + .../js/src/video/05_video_quality_control.js | 5 + .../js/src/video/06_video_progress_slider.js | 8 +- .../js/src/video/07_video_volume_control.js | 5 + .../js/src/video/08_video_speed_control.js | 15 +- .../xmodule/js/src/video/09_video_caption.js | 7 +- .../xmodule/xmodule/js/src/video/10_main.js | 18 +- common/lib/xmodule/xmodule/static_content.py | 1 + common/static/sass/assets/_anims.scss | 154 ++++++++++++++++++ lms/templates/video.html | 14 +- 20 files changed, 535 insertions(+), 141 deletions(-) create mode 100644 common/static/sass/assets/_anims.scss diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dbb096a60d..dc86b63d05 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,13 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Blades: Video player: + - Add spinner; + - Improve initialization of modules; + - Speed up video resizing during page loading; + - Speed up acceptance tests. (BLD-502) + - Fix transcripts bug - when show_captions is set to false. BLD-467. + Studio: change create_item, delete_item, and save_item to RESTful API (STUD-847). Blades: Fix answer choices rearranging if user tries to stylize something in the diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature index 521b4f4403..af8580cab5 100644 --- a/cms/djangoapps/contentstore/features/video.feature +++ b/cms/djangoapps/contentstore/features/video.feature @@ -46,11 +46,11 @@ Feature: CMS.Video Component Scenario: Closed captions become visible when the mouse hovers over CC button Given I have created a Video component with subtitles And Make sure captions are closed - Then Captions become "invisible" after 3 seconds + Then Captions become "invisible" And I hover over button "CC" Then Captions become "visible" And I hover over button "volume" - Then Captions become "invisible" after 3 seconds + Then Captions become "invisible" # 8 Scenario: Open captions never become invisible @@ -66,7 +66,7 @@ Feature: CMS.Video Component Scenario: Closed captions are invisible when mouse doesn't hover on CC button Given I have created a Video component with subtitles And Make sure captions are closed - Then Captions become "invisible" after 3 seconds + Then Captions become "invisible" And I hover over button "volume" Then Captions are "invisible" @@ -74,9 +74,9 @@ Feature: CMS.Video Component Scenario: When enter key is pressed on a caption shows an outline around it Given I have created a Video component with subtitles And Make sure captions are opened - Then I focus on caption line with data-index 0 - Then I press "enter" button on caption line with data-index 0 - And I see caption line with data-index 0 has class "focused" + Then I focus on caption line with data-index "0" + Then I press "enter" button on caption line with data-index "0" + And I see caption line with data-index "0" has class "focused" # 11 # Disabled until we come up with a more solid test, as this one is brittle. diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py index 87252a9dc3..a7c4fc69ab 100644 --- a/cms/djangoapps/contentstore/features/video.py +++ b/cms/djangoapps/contentstore/features/video.py @@ -11,6 +11,11 @@ VIDEO_BUTTONS = { 'Play': '.video_control.play', } +SELECTORS = { + 'spinner': '.video-wrapper .spinner', + 'controls': 'section.video-controls', +} + # We should wait 300 ms for event handler invocation + 200ms for safety. DELAY = 0.5 @@ -23,6 +28,13 @@ def i_created_a_video_component(step): category='video', ) + world.wait_for_xmodule() + world.disable_jquery_animations() + + world.wait_for_present('.is-initialized') + world.wait(DELAY) + assert not world.css_visible(SELECTORS['spinner']) + @step('I have created a Video component with subtitles$') def i_created_a_video_with_subs(_step): @@ -41,7 +53,13 @@ def i_created_a_video_with_subs_with_name(_step, sub_id): # Return to the video world.visit(video_url) + world.wait_for_xmodule() + world.disable_jquery_animations() + + world.wait_for_present('.is-initialized') + world.wait(DELAY) + assert not world.css_visible(SELECTORS['spinner']) @step('I have uploaded subtitles "([^"]*)"$') @@ -52,7 +70,6 @@ def i_have_uploaded_subtitles(_step, sub_id): @step('when I view the (.*) it does not have autoplay enabled$') def does_not_autoplay(_step, video_type): - world.wait_for_xmodule() assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False' assert world.css_has_class('.video_control', 'play') @@ -73,7 +90,6 @@ def i_edit_the_component(_step): @step('I have (hidden|toggled) captions$') def hide_or_show_captions(step, shown): - world.wait_for_xmodule() button_css = 'a.hide-subtitles' if shown == 'hidden': world.css_click(button_css) @@ -118,18 +134,18 @@ def xml_only_video(step): @step('The correct Youtube video is shown$') def the_youtube_video_is_shown(_step): - world.wait_for_xmodule() ele = world.css_find('.video').first assert ele['data-streams'].split(':')[1] == world.scenario_dict['YOUTUBE_ID'] @step('Make sure captions are (.+)$') def set_captions_visibility_state(_step, captions_state): + SELECTOR = '.closed .subtitles' if captions_state == 'closed': - if world.css_visible('.subtitles'): + if not world.is_css_present(SELECTOR): world.browser.find_by_css('.hide-subtitles').click() else: - if not world.css_visible('.subtitles'): + if world.is_css_present(SELECTOR): world.browser.find_by_css('.hide-subtitles').click() @@ -139,18 +155,7 @@ def hover_over_button(_step, button): @step('Captions (?:are|become) "([^"]*)"$') -def are_captions_visibile(_step, visibility_state): - _step.given('Captions become "{0}" after 0 seconds'.format(visibility_state)) - - -@step('Captions (?:are|become) "([^"]*)" after (.+) seconds$') -def check_captions_visibility_state(_step, visibility_state, timeout): - timeout = int(timeout.strip()) - - # Captions become invisible by fading out. We must wait by a specified - # time. - world.wait(timeout) - +def check_captions_visibility_state(_step, visibility_state): if visibility_state == 'visible': assert world.css_visible('.subtitles') else: @@ -162,17 +167,17 @@ def find_caption_line_by_data_index(index): return world.css_find(SELECTOR).first -@step('I focus on caption line with data-index (\d+)$') +@step('I focus on caption line with data-index "([^"]*)"$') def focus_on_caption_line(_step, index): find_caption_line_by_data_index(int(index.strip()))._element.send_keys(Keys.TAB) -@step('I press "enter" button on caption line with data-index (\d+)$') -def focus_on_caption_line(_step, index): +@step('I press "enter" button on caption line with data-index "([^"]*)"$') +def click_on_the_caption(_step, index): find_caption_line_by_data_index(int(index.strip()))._element.send_keys(Keys.ENTER) -@step('I see caption line with data-index (\d+) has class "([^"]*)"$') +@step('I see caption line with data-index "([^"]*)" has class "([^"]*)"$') def caption_line_has_class(_step, index, className): SELECTOR = ".subtitles > li[data-index='{index}']".format(index=int(index.strip())) world.css_has_class(SELECTOR, className.strip()) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 448da9d8f4..97bfde3b82 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -2,6 +2,7 @@ import logging from uuid import uuid4 + from static_replace import replace_static_urls from django.core.exceptions import PermissionDenied diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index 7b3d12b2ba..84074958d8 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -15,9 +15,17 @@ div.video { border: 0; } + &.is-initialized { + article.video-wrapper { + .spinner { + display: none; + } + } + } + div.tc-wrapper { - position: relative; @include clearfix; + position: relative; } div.focus_grabber { @@ -58,21 +66,38 @@ div.video { float: left; margin-right: flex-gutter(9); width: flex-grid(6, 9); - background-color: black; - position: relative; - div.video-player-pre { - height: 50px; - background-color: black; + div.video-player-pre, div.video-player-post { + height: 50px; + background-color: black; } - div.video-player-post { - height: 50px; - background-color: black; + .spinner { + @include transform(translate(-50%, -50%)); + position: absolute; + z-index: 1; + background: rgba(0, 0, 0, 0.7); + top: 50%; + left: 50%; + padding: 30px; + border-radius: 25%; + + &:after{ + @include animation(rotateCW 3s infinite linear); + content: ''; + display: block; + width: 30px; + height: 30px; + border: 7px solid white; + border-top-color: transparent; + border-radius: 100%; + position: relative; + } } + section.video-player { overflow: hidden; min-height: 300px; @@ -85,7 +110,6 @@ div.video { object, iframe, video { border: none; - height: 100%; width: 100%; } @@ -109,12 +133,13 @@ div.video { &:hover { ul, div { - opacity: 1.0; + opacity: 1; } } div.slider { @include clearfix(); + @include transform(scaleY(0.5) translate3d(0, 50%, 0)); background: #c2c2c2; border: 1px solid #000; border-radius: 0; @@ -132,7 +157,6 @@ div.video { -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) translate3d(0, 50%, 0)); div.ui-widget-header { background: #777; @@ -141,23 +165,11 @@ div.video { div.ui-corner-all.slider-range { background-color: #1e91d3; - - /* We add opacity so that we can discern the amount of video that has - * been played. The progress will advance as a gray-shaded area. When - * it will overlap with the range, you will see a different shade of - * the range for part that has been played, and for part that is - * still to be played. - * - * For CSS opacity, different browsers are handled differently. - */ - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; - filter: alpha(opacity=30); - -moz-opacity: 0.3; - -khtml-opacity: 0.3; opacity: 0.3; } a.ui-slider-handle { + @include transform(scale(.7, 1.3) translate3d(-80%, -15%, 0)); background: $pink url(../images/slider-handle.png) center center no-repeat; background-size: 50%; border: 1px solid darken($pink, 20%); @@ -171,7 +183,6 @@ div.video { -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(scale(.7, 1.3) translate3d(-80%, -15%, 0)); width: 20px; &:focus, &:hover { @@ -268,25 +279,26 @@ div.video { position: relative; &.open { - &>a { + & > a { background: url('../images/open-arrow.png') 10px center no-repeat; } ol.video_speeds { display: block; - opacity: 1.0; + opacity: 1; padding: 0; margin: 0; list-style: none; } } - &>a { + & > a { + @include clearfix(); + @include transition(none); background: url('../images/closed-arrow.png') 10px center no-repeat; border-left: 1px solid #000; border-right: 1px solid #000; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; - @include clearfix(); color: #fff; cursor: pointer; display: block; @@ -294,7 +306,6 @@ div.video { margin-right: 0; padding-left: 15px; position: relative; - @include transition(none); -webkit-font-smoothing: antialiased; width: 116px; @@ -312,12 +323,12 @@ div.video { &:hover { outline: 0; - opacity: 1.0; + opacity: 1; background-color: #444; } &:active { - opacity: 1.0; + opacity: 1; background-color: #444; } @@ -349,13 +360,13 @@ div.video { // fix for now ol.video_speeds { - box-shadow: inset 1px 0 0 #555, 0 4px 0 #444; @include transition(none); + box-shadow: inset 1px 0 0 #555, 0 4px 0 #444; background-color: #444; border: 1px solid #000; bottom: 46px; display: none; - opacity: 0.0; + opacity: 0; position: absolute; width: 131px; @@ -404,24 +415,24 @@ div.video { &.open { .volume-slider-container { display: block; - opacity: 1.0; + opacity: 1; } } &.muted { - &>a { + & > a { background-image: url('../images/mute.png'); } } - > a { + & > a { + @include clearfix(); + @include transition(none); background-image: url('../images/volume.png'); background-position: 10px center; background-repeat: no-repeat; - border-right: 1px solid #000; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; - @include clearfix(); color: #fff; cursor: pointer; display: block; @@ -429,7 +440,6 @@ div.video { margin-right: 0; padding-left: 15px; position: relative; - @include transition(none); -webkit-font-smoothing: antialiased; width: 30px; @@ -442,13 +452,13 @@ div.video { } .volume-slider-container { - box-shadow: inset 1px 0 0 #555, 0 3px 0 #444; @include transition(none); + box-shadow: inset 1px 0 0 #555, 0 3px 0 #444; background-color: #444; border: 1px solid #000; bottom: 46px; display: none; - opacity: 0.0; + opacity: 0; position: absolute; width: 45px; height: 125px; @@ -465,6 +475,7 @@ div.video { box-shadow: 0 1px 0 #333; a.ui-slider-handle { + @include transition(height 2.0s ease-in-out 0s, width 2.0s ease-in-out 0s); background: $pink url(../images/slider-handle.png) center center no-repeat; background-size: 50%; border: 1px solid darken($pink, 20%); @@ -473,7 +484,6 @@ div.video { cursor: pointer; height: 15px; left: -6px; - @include transition(height 2.0s ease-in-out 0s, width 2.0s ease-in-out 0s); width: 15px; } @@ -485,6 +495,7 @@ div.video { } a.add-fullscreen { + @include transition(none); background: url(../images/fullscreen.png) center no-repeat; border-right: 1px solid #000; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; @@ -495,7 +506,6 @@ div.video { margin-left: 0; padding: 0 lh(.5); text-indent: -9999px; - @include transition(none); width: 30px; &:hover, &:active { @@ -508,6 +518,7 @@ div.video { a.quality_control { + @include transition(none); background: url(../images/hd.png) center no-repeat; border-right: 1px solid #000; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; @@ -518,7 +529,6 @@ div.video { margin-left: 0; padding: 0 lh(.5); text-indent: -9999px; - @include transition(none); width: 30px; &:hover { @@ -538,16 +548,16 @@ div.video { a.hide-subtitles { + @include transition(none); background: url('../images/cc.png') center no-repeat; float: left; font-weight: 800; line-height: 46px; //height of play pause buttons margin-left: 0; - opacity: 1.0; + opacity: 1; padding: 0 lh(.5); position: relative; text-indent: -9999px; - @include transition(none); -webkit-font-smoothing: antialiased; width: 30px; @@ -569,7 +579,7 @@ div.video { &:hover section.video-controls { ul, div { - opacity: 1.0; + opacity: 1; } div.slider { @@ -732,6 +742,7 @@ div.video { } ol.subtitles { + @include transition(none); background: rgba(#000, .8); bottom: 0; height: 100%; @@ -742,7 +753,6 @@ div.video { right: 0; top: 0; visibility: visible; - @include transition(none); li { color: #aaa; @@ -754,3 +764,5 @@ div.video { } } } + + diff --git a/common/lib/xmodule/xmodule/js/spec/video/resizer_spec.js b/common/lib/xmodule/xmodule/js/spec/video/resizer_spec.js index 788f37e3ba..eea22f5047 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/resizer_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/resizer_spec.js @@ -97,6 +97,85 @@ function (Resizer) { expect(realWidth).toBe(expectedWidth); }); + describe('Callbacks', function () { + var resizer, + spiesList = []; + + beforeEach(function () { + var spiesCount = _.range(3); + + spiesList = $.map(spiesCount, function() { + return jasmine.createSpy(); + }); + + resizer = new Resizer(config); + }); + + + it('callbacks are called', function () { + $.each(spiesList, function(index, spy) { + resizer.callbacks.add(spy); + }); + + 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.length).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 = '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.reset(); + }); + + }); + + }); + }); }); diff --git a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js index bbe75b0aeb..c9fde305ac 100644 --- a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js +++ b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js @@ -12,6 +12,8 @@ function () { containerRatio: null, elementRatio: null }, + callbacksList = [], + module = {}, mode = null, config; @@ -28,7 +30,7 @@ function () { ); } - return this; + return module; }; var getData = function () { @@ -79,7 +81,9 @@ function () { break; } - return this; + fireCallbacks(); + + return module; }; var alignByWidthOnly = function () { @@ -93,7 +97,7 @@ function () { 'left': 0 }); - return this; + return module; }; var alignByHeightOnly = function () { @@ -107,7 +111,7 @@ function () { 'left': 0.5*(data.containerWidth - width) }); - return this; + return module; }; var setMode = function (param) { @@ -116,18 +120,69 @@ function () { align(); } - return this; + return module; }; - initialize.apply(this, arguments); + var addCallback = function (func) { + if ($.isFunction(func)) { + callbacksList.push(func); + } else { + console.error('TypeError: Argument is not a function.'); + } - return { + 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); + } + }; + + initialize.apply(module, arguments); + + return $.extend(true, module, { align: align, alignByWidthOnly: alignByWidthOnly, alignByHeightOnly: alignByHeightOnly, setParams: initialize, - setMode: setMode - }; + setMode: setMode, + callbacks: { + add: addCallback, + once: addOnceCallback, + remove: removeCallback, + removeAll: removeCallbacks + } + }); }; return Resizer; diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js index aa28df9dcd..4ced648483 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -16,7 +16,6 @@ define( 'video/01_initialize.js', ['video/03_video_player.js'], function (VideoPlayer) { - // window.console.log() is expected to be available. We do not support // browsers which lack this functionality. @@ -42,7 +41,20 @@ function (VideoPlayer) { */ return function (state, element) { _makeFunctionsPublic(state); - state.initialize(element); + + state.initialize(element) + .done(function () { + _initializeModules(state) + .done(function () { + state.el + .addClass('is-initialized') + .find('.spinner') + .attr({ + 'aria-hidden': 'true', + 'tabindex': -1 + }); + }); + }); }; // *************************************************************** @@ -94,12 +106,20 @@ function (VideoPlayer) { // 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; + if(state.videoType === 'youtube') { YT.ready(function() { - VideoPlayer(state); + video = VideoPlayer(state); + + state.modules.push(video); + state.__dfd__.resolve(); }); } else { - VideoPlayer(state); + video = VideoPlayer(state); + + state.modules.push(video); + state.__dfd__.resolve(); } } @@ -191,6 +211,8 @@ function (VideoPlayer) { state.html5Sources.mp4 === null && state.html5Sources.ogg === null ) { + + // TODO: use 1 class to work with. state.el.find('.video-player div').addClass('hidden'); state.el.find('.video-player h3').removeClass('hidden'); @@ -224,6 +246,22 @@ function (VideoPlayer) { state.captionHideTimeout = null; } + function _initializeModules(state) { + var dfd = $.Deferred(), + modulesList = $.map(state.modules, function(module) { + if ($.isFunction(module)) { + return module(state); + } else if ($.isPlainObject(module)) { + return module; + } + }); + + $.when.apply(null, modulesList) + .done(dfd.resolve); + + return dfd.promise(); + } + // *************************************************************** // Public functions start here. // These are available via the 'state' object. Their context ('this' @@ -259,6 +297,7 @@ function (VideoPlayer) { data, tempYtTestTimeout; // This is used in places where we instead would have to check if an // element has a CSS class 'fullscreen'. + this.__dfd__ = $.Deferred(); this.isFullScreen = false; // The parent element of the video, and the ID. @@ -313,8 +352,9 @@ function (VideoPlayer) { // If we do not have YouTube ID's, try parsing HTML5 video sources. if (!_prepareHTML5Video(this)) { + this.__dfd__.reject(); // Non-YouTube sources were not found either. - return; + return this.__dfd__.promise(); } console.log('[Video info]: Start player in HTML5 mode.'); @@ -381,6 +421,8 @@ function (VideoPlayer) { _renderElements(_this); }); } + + return this.__dfd__.promise(); } /* diff --git a/common/lib/xmodule/xmodule/js/src/video/025_focus_grabber.js b/common/lib/xmodule/xmodule/js/src/video/025_focus_grabber.js index 341590cdae..65b85efebc 100644 --- a/common/lib/xmodule/xmodule/js/src/video/025_focus_grabber.js +++ b/common/lib/xmodule/xmodule/js/src/video/025_focus_grabber.js @@ -33,11 +33,16 @@ define( [], function () { return function (state) { + var dfd = $.Deferred(); + state.focusGrabber = {}; _makeFunctionsPublic(state); _renderElements(state); _bindHandlers(state); + + dfd.resolve(); + return dfd.promise(); }; 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 187403431c..c5dab0af85 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 @@ -5,14 +5,18 @@ define( 'video/03_video_player.js', ['video/02_html5_video.js', 'video/00_resizer.js' ], function (HTML5Video, Resizer) { + var dfd = $.Deferred(); // VideoPlayer() function - what this module "exports". return function (state) { + state.videoPlayer = {}; _makeFunctionsPublic(state); _initialize(state); // No callbacks to DOM events (click, mousemove, etc.). + + return dfd.promise(); }; // *************************************************************** @@ -56,7 +60,7 @@ function (HTML5Video, Resizer) { // via the 'state' object. Much easier to work this way - you don't // have to do repeated jQuery element selects. function _initialize(state) { - var youTubeId; + var youTubeId, player, videoWidth, videoHeight; // The function is called just once to apply pre-defined configurations // by student before video starts playing. Waits until the video's @@ -138,9 +142,28 @@ function (HTML5Video, Resizer) { .onPlaybackQualityChange } }); + player = state.videoEl = state.el.find('iframe'); + videoWidth = player.attr('width') || player.width(); + videoHeight = player.attr('height') || player.height(); + + _resize(state, videoWidth, videoHeight); } } + function _resize (state, videoWidth, videoHeight) { + state.resizer = new Resizer({ + element: state.videoEl, + elementRatio: videoWidth/videoHeight, + container: state.videoEl.parent() + }) + .setMode('width') + .callbacks.once(function() { + state.trigger('videoCaption.resize', null); + }); + + $(window).bind('resize', _.debounce(state.resizer.align, 100)); + } + // function _restartUsingFlash(state) // // When we are about to play a YouTube video in HTML5 mode and discover @@ -393,6 +416,16 @@ function (HTML5Video, Resizer) { var availablePlaybackRates, baseSpeedSubs, _this, player, videoWidth, videoHeight; + dfd.resolve(); + + if (this.videoType === 'html5') { + player = this.videoEl = this.videoPlayer.player.videoEl; + videoWidth = player[0].videoWidth || player.width(); + videoHeight = player[0].videoHeight || player.height(); + + _resize(this, videoWidth, videoHeight); + } + this.videoPlayer.log('load_video'); availablePlaybackRates = this.videoPlayer.player @@ -468,27 +501,6 @@ function (HTML5Video, Resizer) { this.videoPlayer.player.setPlaybackRate(this.speed); } - if (this.videoType === 'html5') { - player = this.videoEl = this.videoPlayer.player.videoEl; - videoWidth = player[0].videoWidth || player.width(); - videoHeight = player[0].videoHeight || player.height(); - } else { - player = this.videoEl = this.el.find('iframe'); - videoWidth = player.attr('width') || player.width(); - videoHeight = player.attr('height') || player.height(); - } - - this.resizer = new Resizer({ - element: this.videoEl, - elementRatio: videoWidth/videoHeight, - container: this.videoEl.parent() - }) - .setMode('width'); - - this.trigger('videoCaption.resize', null); - $(window).bind('resize', _.debounce(this.resizer.align, 100)); - - /* The following has been commented out to make sure autoplay is disabled for students. if ( 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 5909e23bf6..f9eae8e383 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 @@ -8,11 +8,16 @@ function () { // 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(); }; // *************************************************************** diff --git a/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js b/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js index 5566b54268..6052622128 100644 --- a/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js @@ -8,6 +8,8 @@ function () { // 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; @@ -18,6 +20,9 @@ function () { _makeFunctionsPublic(state); _renderElements(state); _bindHandlers(state); + + dfd.resolve(); + return dfd.promise(); }; // *************************************************************** diff --git a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js index 76c69a0134..e0911bca74 100644 --- a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js +++ b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js @@ -12,14 +12,18 @@ define( 'video/06_video_progress_slider.js', [], function () { - // VideoProgressSlider() function - what this module "exports". return function (state) { + var dfd = $.Deferred(); + state.videoProgressSlider = {}; _makeFunctionsPublic(state); _renderElements(state); // No callbacks to DOM events (click, mousemove, etc.). + + dfd.resolve(); + return dfd.promise(); }; // *************************************************************** @@ -47,7 +51,7 @@ function () { // 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. + // have to do repeated jQuery element selects. function _renderElements(state) { if (!onTouchBasedDevice()) { state.videoProgressSlider.el = state.videoControl.sliderEl; diff --git a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js index 6ed107e1b2..dbabe5a15e 100644 --- a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js @@ -8,11 +8,16 @@ function () { // VideoVolumeControl() function - what this module "exports". return function (state) { + var dfd = $.Deferred(); + state.videoVolumeControl = {}; _makeFunctionsPublic(state); _renderElements(state); _bindHandlers(state); + + dfd.resolve(); + return dfd.promise(); }; // *************************************************************** diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js index e69817721f..9ba2da055e 100644 --- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js @@ -8,15 +8,12 @@ function () { // VideoSpeedControl() function - what this module "exports". return function (state) { + var dfd = $.Deferred(); + state.videoSpeedControl = {}; - if (state.videoType === 'html5') { - _initialize(state); - } else if (state.videoType === 'youtube' && state.youtubeXhr) { - state.youtubeXhr.always(function () { - _initialize(state); - }); - } + _initialize(state); + dfd.resolve(); if (state.videoType === 'html5' && !(_checkPlaybackRates())) { console.log( @@ -24,9 +21,9 @@ function () { ); _hideSpeedControl(state); - - return; } + + return dfd.promise(); }; // *************************************************************** 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 657435182d..8092abe794 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 @@ -21,11 +21,16 @@ function () { * @returns {undefined} */ return function (state) { + var dfd = $.Deferred(); + state.videoCaption = {}; _makeFunctionsPublic(state); state.videoCaption.renderElements(); + + dfd.resolve(); + return dfd.promise(); }; // *************************************************************** @@ -725,7 +730,7 @@ function () { }); } - if (this.resizer) { + if (this.resizer && !this.isFullScreen) { this.resizer.alignByWidthOnly(); } diff --git a/common/lib/xmodule/xmodule/js/src/video/10_main.js b/common/lib/xmodule/xmodule/js/src/video/10_main.js index 9f20a1c87e..224f0f4316 100644 --- a/common/lib/xmodule/xmodule/js/src/video/10_main.js +++ b/common/lib/xmodule/xmodule/js/src/video/10_main.js @@ -94,20 +94,22 @@ function ( state = {}; previousState = state; + state.modules = [ + FocusGrabber, + VideoControl, + VideoQualityControl, + VideoProgressSlider, + VideoVolumeControl, + VideoSpeedControl, + VideoCaption + ]; + state.youtubeXhr = youtubeXhr; Initialize(state, element); if (!youtubeXhr) { youtubeXhr = state.youtubeXhr; } - FocusGrabber(state); - VideoControl(state); - VideoQualityControl(state); - VideoProgressSlider(state); - VideoVolumeControl(state); - VideoSpeedControl(state); - VideoCaption(state); - // 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. diff --git a/common/lib/xmodule/xmodule/static_content.py b/common/lib/xmodule/xmodule/static_content.py index 2cadd34df1..aabddf721a 100755 --- a/common/lib/xmodule/xmodule/static_content.py +++ b/common/lib/xmodule/xmodule/static_content.py @@ -98,6 +98,7 @@ def _write_styles(selector, output_root, classes): module_styles_lines = [] module_styles_lines.append("@import 'bourbon/bourbon';") module_styles_lines.append("@import 'bourbon/addons/button';") + module_styles_lines.append("@import 'assets/anims';") for class_, fragment_names in css_imports.items(): module_styles_lines.append("""{selector}.xmodule_{class_} {{""".format( class_=class_, selector=selector diff --git a/common/static/sass/assets/_anims.scss b/common/static/sass/assets/_anims.scss new file mode 100644 index 0000000000..ad709bc4ca --- /dev/null +++ b/common/static/sass/assets/_anims.scss @@ -0,0 +1,154 @@ +// animations & keyframes +// ==================== + +// fade in +@include keyframes(fadeIn) { + 0% { + opacity: 0.0; + } + + 50% { + opacity: 0.5; + } + + 100% { + opacity: 1.0; + } +} + +// fade out +@include keyframes(fadeOut) { + 0% { + opacity: 1.0; + } + + 50% { + opacity: 0.5; + } + + 100% { + opacity: 0.0; + } +} + +// ==================== + + +// rotate up +@include keyframes(rotateUp) { + 0% { + @include transform(rotate(0deg)); + } + + 50% { + @include transform(rotate(-90deg)); + } + + 100% { + @include transform(rotate(-180deg)); + } +} + +// rotate up +@include keyframes(rotateDown) { + 0% { + @include transform(rotate(0deg)); + } + + 50% { + @include transform(rotate(90deg)); + } + + 100% { + @include transform(rotate(180deg)); + } +} + +// rotate clockwise +@include keyframes(rotateCW) { + 0% { + @include transform(rotate(0deg)); + } + + 50% { + @include transform(rotate(180deg)); + } + + 100% { + @include transform(rotate(360deg)); + } +} + +// rotate counter-clockwise +@include keyframes(rotateCCW) { + 0% { + @include transform(rotate(0deg)); + } + + 50% { + @include transform(rotate(-180deg)); + } + + 100% { + @include transform(rotate(-360deg)); + } +} + +// bounce in +@include keyframes(bounceIn) { + 0% { + opacity: 0.0; + @include transform(scale(0.3)); + } + + 50% { + opacity: 1.0; + @include transform(scale(1.05)); + } + + 100% { + @include transform(scale(1)); + } +} + +// bounce out +@include keyframes(bounceOut) { + 0% { + @include transform(scale(1)); + } + + 50% { + opacity: 1.0; + @include transform(scale(1.05)); + } + + 100% { + opacity: 0.0; + @include transform(scale(0.3)); + } +} + +// ==================== + + +// flash +@include keyframes(flash) { + 0%, 100% { + opacity: 1.0; + } + + 50% { + opacity: 0.0; + } +} + +// flash - double +@include keyframes(flashDouble) { +0%, 50%, 100% { + opacity: 1.0; +} + +25%, 75% { + opacity: 0.0; + } +} diff --git a/lms/templates/video.html b/lms/templates/video.html index 9d54f68ea8..571c82c4fc 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -45,17 +45,15 @@
${_("Skip to a navigable version of this video's transcript.")} - -
- + ${_('Go back to start of transcript.')} - +