Commit 64c0a8c9 by polesye

Merge pull request #1642 from edx/anton/video-player-improvements

Video player: improvements.
parents 50250d82 153bc25d
...@@ -5,6 +5,13 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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). 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 Blades: Fix answer choices rearranging if user tries to stylize something in the
......
...@@ -46,11 +46,11 @@ Feature: CMS.Video Component ...@@ -46,11 +46,11 @@ Feature: CMS.Video Component
Scenario: Closed captions become visible when the mouse hovers over CC button Scenario: Closed captions become visible when the mouse hovers over CC button
Given I have created a Video component with subtitles Given I have created a Video component with subtitles
And Make sure captions are closed And Make sure captions are closed
Then Captions become "invisible" after 3 seconds Then Captions become "invisible"
And I hover over button "CC" And I hover over button "CC"
Then Captions become "visible" Then Captions become "visible"
And I hover over button "volume" And I hover over button "volume"
Then Captions become "invisible" after 3 seconds Then Captions become "invisible"
# 8 # 8
Scenario: Open captions never become invisible Scenario: Open captions never become invisible
...@@ -66,7 +66,7 @@ Feature: CMS.Video Component ...@@ -66,7 +66,7 @@ Feature: CMS.Video Component
Scenario: Closed captions are invisible when mouse doesn't hover on CC button Scenario: Closed captions are invisible when mouse doesn't hover on CC button
Given I have created a Video component with subtitles Given I have created a Video component with subtitles
And Make sure captions are closed And Make sure captions are closed
Then Captions become "invisible" after 3 seconds Then Captions become "invisible"
And I hover over button "volume" And I hover over button "volume"
Then Captions are "invisible" Then Captions are "invisible"
...@@ -74,9 +74,9 @@ Feature: CMS.Video Component ...@@ -74,9 +74,9 @@ Feature: CMS.Video Component
Scenario: When enter key is pressed on a caption shows an outline around it Scenario: When enter key is pressed on a caption shows an outline around it
Given I have created a Video component with subtitles Given I have created a Video component with subtitles
And Make sure captions are opened And Make sure captions are opened
Then I focus on caption line with data-index 0 Then I focus on caption line with data-index "0"
Then I press "enter" button 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" And I see caption line with data-index "0" has class "focused"
# 11 # 11
# Disabled until we come up with a more solid test, as this one is brittle. # Disabled until we come up with a more solid test, as this one is brittle.
......
...@@ -11,6 +11,11 @@ VIDEO_BUTTONS = { ...@@ -11,6 +11,11 @@ VIDEO_BUTTONS = {
'Play': '.video_control.play', '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. # We should wait 300 ms for event handler invocation + 200ms for safety.
DELAY = 0.5 DELAY = 0.5
...@@ -23,6 +28,13 @@ def i_created_a_video_component(step): ...@@ -23,6 +28,13 @@ def i_created_a_video_component(step):
category='video', 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$') @step('I have created a Video component with subtitles$')
def i_created_a_video_with_subs(_step): def i_created_a_video_with_subs(_step):
...@@ -41,7 +53,13 @@ def i_created_a_video_with_subs_with_name(_step, sub_id): ...@@ -41,7 +53,13 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
# Return to the video # Return to the video
world.visit(video_url) world.visit(video_url)
world.wait_for_xmodule() 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 "([^"]*)"$') @step('I have uploaded subtitles "([^"]*)"$')
...@@ -52,7 +70,6 @@ def i_have_uploaded_subtitles(_step, sub_id): ...@@ -52,7 +70,6 @@ def i_have_uploaded_subtitles(_step, sub_id):
@step('when I view the (.*) it does not have autoplay enabled$') @step('when I view the (.*) it does not have autoplay enabled$')
def does_not_autoplay(_step, video_type): 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_find('.%s' % video_type)[0]['data-autoplay'] == 'False'
assert world.css_has_class('.video_control', 'play') assert world.css_has_class('.video_control', 'play')
...@@ -73,7 +90,6 @@ def i_edit_the_component(_step): ...@@ -73,7 +90,6 @@ def i_edit_the_component(_step):
@step('I have (hidden|toggled) captions$') @step('I have (hidden|toggled) captions$')
def hide_or_show_captions(step, shown): def hide_or_show_captions(step, shown):
world.wait_for_xmodule()
button_css = 'a.hide-subtitles' button_css = 'a.hide-subtitles'
if shown == 'hidden': if shown == 'hidden':
world.css_click(button_css) world.css_click(button_css)
...@@ -118,18 +134,18 @@ def xml_only_video(step): ...@@ -118,18 +134,18 @@ def xml_only_video(step):
@step('The correct Youtube video is shown$') @step('The correct Youtube video is shown$')
def the_youtube_video_is_shown(_step): def the_youtube_video_is_shown(_step):
world.wait_for_xmodule()
ele = world.css_find('.video').first ele = world.css_find('.video').first
assert ele['data-streams'].split(':')[1] == world.scenario_dict['YOUTUBE_ID'] assert ele['data-streams'].split(':')[1] == world.scenario_dict['YOUTUBE_ID']
@step('Make sure captions are (.+)$') @step('Make sure captions are (.+)$')
def set_captions_visibility_state(_step, captions_state): def set_captions_visibility_state(_step, captions_state):
SELECTOR = '.closed .subtitles'
if captions_state == 'closed': if captions_state == 'closed':
if world.css_visible('.subtitles'): if not world.is_css_present(SELECTOR):
world.browser.find_by_css('.hide-subtitles').click() world.browser.find_by_css('.hide-subtitles').click()
else: else:
if not world.css_visible('.subtitles'): if world.is_css_present(SELECTOR):
world.browser.find_by_css('.hide-subtitles').click() world.browser.find_by_css('.hide-subtitles').click()
...@@ -139,18 +155,7 @@ def hover_over_button(_step, button): ...@@ -139,18 +155,7 @@ def hover_over_button(_step, button):
@step('Captions (?:are|become) "([^"]*)"$') @step('Captions (?:are|become) "([^"]*)"$')
def are_captions_visibile(_step, visibility_state): def check_captions_visibility_state(_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)
if visibility_state == 'visible': if visibility_state == 'visible':
assert world.css_visible('.subtitles') assert world.css_visible('.subtitles')
else: else:
...@@ -162,17 +167,17 @@ def find_caption_line_by_data_index(index): ...@@ -162,17 +167,17 @@ def find_caption_line_by_data_index(index):
return world.css_find(SELECTOR).first 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): def focus_on_caption_line(_step, index):
find_caption_line_by_data_index(int(index.strip()))._element.send_keys(Keys.TAB) 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+)$') @step('I press "enter" button on caption line with data-index "([^"]*)"$')
def focus_on_caption_line(_step, index): def click_on_the_caption(_step, index):
find_caption_line_by_data_index(int(index.strip()))._element.send_keys(Keys.ENTER) 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): def caption_line_has_class(_step, index, className):
SELECTOR = ".subtitles > li[data-index='{index}']".format(index=int(index.strip())) SELECTOR = ".subtitles > li[data-index='{index}']".format(index=int(index.strip()))
world.css_has_class(SELECTOR, className.strip()) world.css_has_class(SELECTOR, className.strip())
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import logging import logging
from uuid import uuid4 from uuid import uuid4
from static_replace import replace_static_urls from static_replace import replace_static_urls
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
......
...@@ -15,9 +15,17 @@ div.video { ...@@ -15,9 +15,17 @@ div.video {
border: 0; border: 0;
} }
&.is-initialized {
article.video-wrapper {
.spinner {
display: none;
}
}
}
div.tc-wrapper { div.tc-wrapper {
position: relative;
@include clearfix; @include clearfix;
position: relative;
} }
div.focus_grabber { div.focus_grabber {
...@@ -58,21 +66,38 @@ div.video { ...@@ -58,21 +66,38 @@ div.video {
float: left; float: left;
margin-right: flex-gutter(9); margin-right: flex-gutter(9);
width: flex-grid(6, 9); width: flex-grid(6, 9);
background-color: black; background-color: black;
position: relative; position: relative;
div.video-player-pre { div.video-player-pre, div.video-player-post {
height: 50px; height: 50px;
background-color: black; background-color: black;
} }
div.video-player-post { .spinner {
height: 50px; @include transform(translate(-50%, -50%));
background-color: black; 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 { section.video-player {
overflow: hidden; overflow: hidden;
min-height: 300px; min-height: 300px;
...@@ -85,7 +110,6 @@ div.video { ...@@ -85,7 +110,6 @@ div.video {
object, iframe, video { object, iframe, video {
border: none; border: none;
height: 100%;
width: 100%; width: 100%;
} }
...@@ -109,12 +133,13 @@ div.video { ...@@ -109,12 +133,13 @@ div.video {
&:hover { &:hover {
ul, div { ul, div {
opacity: 1.0; opacity: 1;
} }
} }
div.slider { div.slider {
@include clearfix(); @include clearfix();
@include transform(scaleY(0.5) translate3d(0, 50%, 0));
background: #c2c2c2; background: #c2c2c2;
border: 1px solid #000; border: 1px solid #000;
border-radius: 0; border-radius: 0;
...@@ -132,7 +157,6 @@ div.video { ...@@ -132,7 +157,6 @@ div.video {
-moz-transition: -moz-transform 0.7s ease-in-out; -moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out; -ms-transition: -ms-transform 0.7s ease-in-out;
transition: 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 { div.ui-widget-header {
background: #777; background: #777;
...@@ -141,23 +165,11 @@ div.video { ...@@ -141,23 +165,11 @@ div.video {
div.ui-corner-all.slider-range { div.ui-corner-all.slider-range {
background-color: #1e91d3; 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; opacity: 0.3;
} }
a.ui-slider-handle { 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: $pink url(../images/slider-handle.png) center center no-repeat;
background-size: 50%; background-size: 50%;
border: 1px solid darken($pink, 20%); border: 1px solid darken($pink, 20%);
...@@ -171,7 +183,6 @@ div.video { ...@@ -171,7 +183,6 @@ div.video {
-moz-transition: -moz-transform 0.7s ease-in-out; -moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out; -ms-transition: -ms-transform 0.7s ease-in-out;
transition: 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; width: 20px;
&:focus, &:hover { &:focus, &:hover {
...@@ -268,25 +279,26 @@ div.video { ...@@ -268,25 +279,26 @@ div.video {
position: relative; position: relative;
&.open { &.open {
&>a { & > a {
background: url('../images/open-arrow.png') 10px center no-repeat; background: url('../images/open-arrow.png') 10px center no-repeat;
} }
ol.video_speeds { ol.video_speeds {
display: block; display: block;
opacity: 1.0; opacity: 1;
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
} }
} }
&>a { & > a {
@include clearfix();
@include transition(none);
background: url('../images/closed-arrow.png') 10px center no-repeat; background: url('../images/closed-arrow.png') 10px center no-repeat;
border-left: 1px solid #000; border-left: 1px solid #000;
border-right: 1px solid #000; border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
@include clearfix();
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
display: block; display: block;
...@@ -294,7 +306,6 @@ div.video { ...@@ -294,7 +306,6 @@ div.video {
margin-right: 0; margin-right: 0;
padding-left: 15px; padding-left: 15px;
position: relative; position: relative;
@include transition(none);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
width: 116px; width: 116px;
...@@ -312,12 +323,12 @@ div.video { ...@@ -312,12 +323,12 @@ div.video {
&:hover { &:hover {
outline: 0; outline: 0;
opacity: 1.0; opacity: 1;
background-color: #444; background-color: #444;
} }
&:active { &:active {
opacity: 1.0; opacity: 1;
background-color: #444; background-color: #444;
} }
...@@ -349,13 +360,13 @@ div.video { ...@@ -349,13 +360,13 @@ div.video {
// fix for now // fix for now
ol.video_speeds { ol.video_speeds {
box-shadow: inset 1px 0 0 #555, 0 4px 0 #444;
@include transition(none); @include transition(none);
box-shadow: inset 1px 0 0 #555, 0 4px 0 #444;
background-color: #444; background-color: #444;
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0.0; opacity: 0;
position: absolute; position: absolute;
width: 131px; width: 131px;
...@@ -404,24 +415,24 @@ div.video { ...@@ -404,24 +415,24 @@ div.video {
&.open { &.open {
.volume-slider-container { .volume-slider-container {
display: block; display: block;
opacity: 1.0; opacity: 1;
} }
} }
&.muted { &.muted {
&>a { & > a {
background-image: url('../images/mute.png'); background-image: url('../images/mute.png');
} }
} }
> a { & > a {
@include clearfix();
@include transition(none);
background-image: url('../images/volume.png'); background-image: url('../images/volume.png');
background-position: 10px center; background-position: 10px center;
background-repeat: no-repeat; background-repeat: no-repeat;
border-right: 1px solid #000; border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
@include clearfix();
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
display: block; display: block;
...@@ -429,7 +440,6 @@ div.video { ...@@ -429,7 +440,6 @@ div.video {
margin-right: 0; margin-right: 0;
padding-left: 15px; padding-left: 15px;
position: relative; position: relative;
@include transition(none);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
width: 30px; width: 30px;
...@@ -442,13 +452,13 @@ div.video { ...@@ -442,13 +452,13 @@ div.video {
} }
.volume-slider-container { .volume-slider-container {
box-shadow: inset 1px 0 0 #555, 0 3px 0 #444;
@include transition(none); @include transition(none);
box-shadow: inset 1px 0 0 #555, 0 3px 0 #444;
background-color: #444; background-color: #444;
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0.0; opacity: 0;
position: absolute; position: absolute;
width: 45px; width: 45px;
height: 125px; height: 125px;
...@@ -465,6 +475,7 @@ div.video { ...@@ -465,6 +475,7 @@ div.video {
box-shadow: 0 1px 0 #333; box-shadow: 0 1px 0 #333;
a.ui-slider-handle { 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: $pink url(../images/slider-handle.png) center center no-repeat;
background-size: 50%; background-size: 50%;
border: 1px solid darken($pink, 20%); border: 1px solid darken($pink, 20%);
...@@ -473,7 +484,6 @@ div.video { ...@@ -473,7 +484,6 @@ div.video {
cursor: pointer; cursor: pointer;
height: 15px; height: 15px;
left: -6px; left: -6px;
@include transition(height 2.0s ease-in-out 0s, width 2.0s ease-in-out 0s);
width: 15px; width: 15px;
} }
...@@ -485,6 +495,7 @@ div.video { ...@@ -485,6 +495,7 @@ div.video {
} }
a.add-fullscreen { a.add-fullscreen {
@include transition(none);
background: url(../images/fullscreen.png) center no-repeat; background: url(../images/fullscreen.png) center no-repeat;
border-right: 1px solid #000; border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
...@@ -495,7 +506,6 @@ div.video { ...@@ -495,7 +506,6 @@ div.video {
margin-left: 0; margin-left: 0;
padding: 0 lh(.5); padding: 0 lh(.5);
text-indent: -9999px; text-indent: -9999px;
@include transition(none);
width: 30px; width: 30px;
&:hover, &:active { &:hover, &:active {
...@@ -508,6 +518,7 @@ div.video { ...@@ -508,6 +518,7 @@ div.video {
a.quality_control { a.quality_control {
@include transition(none);
background: url(../images/hd.png) center no-repeat; background: url(../images/hd.png) center no-repeat;
border-right: 1px solid #000; border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
...@@ -518,7 +529,6 @@ div.video { ...@@ -518,7 +529,6 @@ div.video {
margin-left: 0; margin-left: 0;
padding: 0 lh(.5); padding: 0 lh(.5);
text-indent: -9999px; text-indent: -9999px;
@include transition(none);
width: 30px; width: 30px;
&:hover { &:hover {
...@@ -538,16 +548,16 @@ div.video { ...@@ -538,16 +548,16 @@ div.video {
a.hide-subtitles { a.hide-subtitles {
@include transition(none);
background: url('../images/cc.png') center no-repeat; background: url('../images/cc.png') center no-repeat;
float: left; float: left;
font-weight: 800; font-weight: 800;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
margin-left: 0; margin-left: 0;
opacity: 1.0; opacity: 1;
padding: 0 lh(.5); padding: 0 lh(.5);
position: relative; position: relative;
text-indent: -9999px; text-indent: -9999px;
@include transition(none);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
width: 30px; width: 30px;
...@@ -569,7 +579,7 @@ div.video { ...@@ -569,7 +579,7 @@ div.video {
&:hover section.video-controls { &:hover section.video-controls {
ul, div { ul, div {
opacity: 1.0; opacity: 1;
} }
div.slider { div.slider {
...@@ -732,6 +742,7 @@ div.video { ...@@ -732,6 +742,7 @@ div.video {
} }
ol.subtitles { ol.subtitles {
@include transition(none);
background: rgba(#000, .8); background: rgba(#000, .8);
bottom: 0; bottom: 0;
height: 100%; height: 100%;
...@@ -742,7 +753,6 @@ div.video { ...@@ -742,7 +753,6 @@ div.video {
right: 0; right: 0;
top: 0; top: 0;
visibility: visible; visibility: visible;
@include transition(none);
li { li {
color: #aaa; color: #aaa;
...@@ -754,3 +764,5 @@ div.video { ...@@ -754,3 +764,5 @@ div.video {
} }
} }
} }
...@@ -97,6 +97,85 @@ function (Resizer) { ...@@ -97,6 +97,85 @@ function (Resizer) {
expect(realWidth).toBe(expectedWidth); 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();
});
});
});
}); });
}); });
......
...@@ -12,6 +12,8 @@ function () { ...@@ -12,6 +12,8 @@ function () {
containerRatio: null, containerRatio: null,
elementRatio: null elementRatio: null
}, },
callbacksList = [],
module = {},
mode = null, mode = null,
config; config;
...@@ -28,7 +30,7 @@ function () { ...@@ -28,7 +30,7 @@ function () {
); );
} }
return this; return module;
}; };
var getData = function () { var getData = function () {
...@@ -79,7 +81,9 @@ function () { ...@@ -79,7 +81,9 @@ function () {
break; break;
} }
return this; fireCallbacks();
return module;
}; };
var alignByWidthOnly = function () { var alignByWidthOnly = function () {
...@@ -93,7 +97,7 @@ function () { ...@@ -93,7 +97,7 @@ function () {
'left': 0 'left': 0
}); });
return this; return module;
}; };
var alignByHeightOnly = function () { var alignByHeightOnly = function () {
...@@ -107,7 +111,7 @@ function () { ...@@ -107,7 +111,7 @@ function () {
'left': 0.5*(data.containerWidth - width) 'left': 0.5*(data.containerWidth - width)
}); });
return this; return module;
}; };
var setMode = function (param) { var setMode = function (param) {
...@@ -116,18 +120,69 @@ function () { ...@@ -116,18 +120,69 @@ function () {
align(); align();
} }
return this; return module;
};
var addCallback = function (func) {
if ($.isFunction(func)) {
callbacksList.push(func);
} else {
console.error('TypeError: Argument is not a function.');
}
return module;
};
var addOnceCallback = function (func) {
if ($.isFunction(func)) {
var decorator = function () {
func();
removeCallback(func);
};
addCallback(decorator);
} else {
console.error('TypeError: Argument is not a function.');
}
return module;
}; };
initialize.apply(this, arguments); 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);
return { if (index !== -1) {
return callbacksList.splice(index, 1);
}
};
initialize.apply(module, arguments);
return $.extend(true, module, {
align: align, align: align,
alignByWidthOnly: alignByWidthOnly, alignByWidthOnly: alignByWidthOnly,
alignByHeightOnly: alignByHeightOnly, alignByHeightOnly: alignByHeightOnly,
setParams: initialize, setParams: initialize,
setMode: setMode setMode: setMode,
}; callbacks: {
add: addCallback,
once: addOnceCallback,
remove: removeCallback,
removeAll: removeCallbacks
}
});
}; };
return Resizer; return Resizer;
......
...@@ -16,7 +16,6 @@ define( ...@@ -16,7 +16,6 @@ define(
'video/01_initialize.js', 'video/01_initialize.js',
['video/03_video_player.js'], ['video/03_video_player.js'],
function (VideoPlayer) { function (VideoPlayer) {
// window.console.log() is expected to be available. We do not support // window.console.log() is expected to be available. We do not support
// browsers which lack this functionality. // browsers which lack this functionality.
...@@ -42,7 +41,20 @@ function (VideoPlayer) { ...@@ -42,7 +41,20 @@ function (VideoPlayer) {
*/ */
return function (state, element) { return function (state, element) {
_makeFunctionsPublic(state); _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) { ...@@ -94,12 +106,20 @@ function (VideoPlayer) {
// Require JS. At the time when we reach this code, the stand alone // 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 // HTML5 player is already loaded, so no further testing in that case
// is required. // is required.
var video;
if(state.videoType === 'youtube') { if(state.videoType === 'youtube') {
YT.ready(function() { YT.ready(function() {
VideoPlayer(state); video = VideoPlayer(state);
state.modules.push(video);
state.__dfd__.resolve();
}); });
} else { } else {
VideoPlayer(state); video = VideoPlayer(state);
state.modules.push(video);
state.__dfd__.resolve();
} }
} }
...@@ -191,6 +211,8 @@ function (VideoPlayer) { ...@@ -191,6 +211,8 @@ function (VideoPlayer) {
state.html5Sources.mp4 === null && state.html5Sources.mp4 === null &&
state.html5Sources.ogg === 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 div').addClass('hidden');
state.el.find('.video-player h3').removeClass('hidden'); state.el.find('.video-player h3').removeClass('hidden');
...@@ -224,6 +246,22 @@ function (VideoPlayer) { ...@@ -224,6 +246,22 @@ function (VideoPlayer) {
state.captionHideTimeout = null; 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. // Public functions start here.
// These are available via the 'state' object. Their context ('this' // These are available via the 'state' object. Their context ('this'
...@@ -259,6 +297,7 @@ function (VideoPlayer) { ...@@ -259,6 +297,7 @@ function (VideoPlayer) {
data, tempYtTestTimeout; data, tempYtTestTimeout;
// This is used in places where we instead would have to check if an // This is used in places where we instead would have to check if an
// element has a CSS class 'fullscreen'. // element has a CSS class 'fullscreen'.
this.__dfd__ = $.Deferred();
this.isFullScreen = false; this.isFullScreen = false;
// The parent element of the video, and the ID. // The parent element of the video, and the ID.
...@@ -313,8 +352,9 @@ function (VideoPlayer) { ...@@ -313,8 +352,9 @@ function (VideoPlayer) {
// If we do not have YouTube ID's, try parsing HTML5 video sources. // If we do not have YouTube ID's, try parsing HTML5 video sources.
if (!_prepareHTML5Video(this)) { if (!_prepareHTML5Video(this)) {
this.__dfd__.reject();
// Non-YouTube sources were not found either. // Non-YouTube sources were not found either.
return; return this.__dfd__.promise();
} }
console.log('[Video info]: Start player in HTML5 mode.'); console.log('[Video info]: Start player in HTML5 mode.');
...@@ -381,6 +421,8 @@ function (VideoPlayer) { ...@@ -381,6 +421,8 @@ function (VideoPlayer) {
_renderElements(_this); _renderElements(_this);
}); });
} }
return this.__dfd__.promise();
} }
/* /*
......
...@@ -33,11 +33,16 @@ define( ...@@ -33,11 +33,16 @@ define(
[], [],
function () { function () {
return function (state) { return function (state) {
var dfd = $.Deferred();
state.focusGrabber = {}; state.focusGrabber = {};
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
_renderElements(state); _renderElements(state);
_bindHandlers(state); _bindHandlers(state);
dfd.resolve();
return dfd.promise();
}; };
......
...@@ -5,14 +5,18 @@ define( ...@@ -5,14 +5,18 @@ define(
'video/03_video_player.js', 'video/03_video_player.js',
['video/02_html5_video.js', 'video/00_resizer.js' ], ['video/02_html5_video.js', 'video/00_resizer.js' ],
function (HTML5Video, Resizer) { function (HTML5Video, Resizer) {
var dfd = $.Deferred();
// VideoPlayer() function - what this module "exports". // VideoPlayer() function - what this module "exports".
return function (state) { return function (state) {
state.videoPlayer = {}; state.videoPlayer = {};
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
_initialize(state); _initialize(state);
// No callbacks to DOM events (click, mousemove, etc.). // No callbacks to DOM events (click, mousemove, etc.).
return dfd.promise();
}; };
// *************************************************************** // ***************************************************************
...@@ -56,7 +60,7 @@ function (HTML5Video, Resizer) { ...@@ -56,7 +60,7 @@ function (HTML5Video, Resizer) {
// via the 'state' object. Much easier to work this way - you don't // 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 _initialize(state) { function _initialize(state) {
var youTubeId; var youTubeId, player, videoWidth, videoHeight;
// The function is called just once to apply pre-defined configurations // The function is called just once to apply pre-defined configurations
// by student before video starts playing. Waits until the video's // by student before video starts playing. Waits until the video's
...@@ -138,9 +142,28 @@ function (HTML5Video, Resizer) { ...@@ -138,9 +142,28 @@ function (HTML5Video, Resizer) {
.onPlaybackQualityChange .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) // function _restartUsingFlash(state)
// //
// When we are about to play a YouTube video in HTML5 mode and discover // When we are about to play a YouTube video in HTML5 mode and discover
...@@ -393,6 +416,16 @@ function (HTML5Video, Resizer) { ...@@ -393,6 +416,16 @@ function (HTML5Video, Resizer) {
var availablePlaybackRates, baseSpeedSubs, _this, var availablePlaybackRates, baseSpeedSubs, _this,
player, videoWidth, videoHeight; 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'); this.videoPlayer.log('load_video');
availablePlaybackRates = this.videoPlayer.player availablePlaybackRates = this.videoPlayer.player
...@@ -468,27 +501,6 @@ function (HTML5Video, Resizer) { ...@@ -468,27 +501,6 @@ function (HTML5Video, Resizer) {
this.videoPlayer.player.setPlaybackRate(this.speed); 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 /* The following has been commented out to make sure autoplay is
disabled for students. disabled for students.
if ( if (
......
...@@ -8,11 +8,16 @@ function () { ...@@ -8,11 +8,16 @@ function () {
// VideoControl() function - what this module "exports". // VideoControl() function - what this module "exports".
return function (state) { return function (state) {
var dfd = $.Deferred();
state.videoControl = {}; state.videoControl = {};
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
_renderElements(state); _renderElements(state);
_bindHandlers(state); _bindHandlers(state);
dfd.resolve();
return dfd.promise();
}; };
// *************************************************************** // ***************************************************************
......
...@@ -8,6 +8,8 @@ function () { ...@@ -8,6 +8,8 @@ function () {
// VideoQualityControl() function - what this module "exports". // VideoQualityControl() function - what this module "exports".
return function (state) { return function (state) {
var dfd = $.Deferred();
// Changing quality for now only works for YouTube videos. // Changing quality for now only works for YouTube videos.
if (state.videoType !== 'youtube') { if (state.videoType !== 'youtube') {
return; return;
...@@ -18,6 +20,9 @@ function () { ...@@ -18,6 +20,9 @@ function () {
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
_renderElements(state); _renderElements(state);
_bindHandlers(state); _bindHandlers(state);
dfd.resolve();
return dfd.promise();
}; };
// *************************************************************** // ***************************************************************
......
...@@ -12,14 +12,18 @@ define( ...@@ -12,14 +12,18 @@ define(
'video/06_video_progress_slider.js', 'video/06_video_progress_slider.js',
[], [],
function () { function () {
// VideoProgressSlider() function - what this module "exports". // VideoProgressSlider() function - what this module "exports".
return function (state) { return function (state) {
var dfd = $.Deferred();
state.videoProgressSlider = {}; state.videoProgressSlider = {};
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
_renderElements(state); _renderElements(state);
// No callbacks to DOM events (click, mousemove, etc.). // No callbacks to DOM events (click, mousemove, etc.).
dfd.resolve();
return dfd.promise();
}; };
// *************************************************************** // ***************************************************************
...@@ -47,7 +51,7 @@ function () { ...@@ -47,7 +51,7 @@ function () {
// Create any necessary DOM elements, attach them, and set their // Create any necessary DOM elements, attach them, and set their
// initial configuration. Also make the created DOM elements available // initial configuration. Also make the created DOM elements available
// via the 'state' object. Much easier to work this way - you don't // 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) { function _renderElements(state) {
if (!onTouchBasedDevice()) { if (!onTouchBasedDevice()) {
state.videoProgressSlider.el = state.videoControl.sliderEl; state.videoProgressSlider.el = state.videoControl.sliderEl;
......
...@@ -8,11 +8,16 @@ function () { ...@@ -8,11 +8,16 @@ function () {
// VideoVolumeControl() function - what this module "exports". // VideoVolumeControl() function - what this module "exports".
return function (state) { return function (state) {
var dfd = $.Deferred();
state.videoVolumeControl = {}; state.videoVolumeControl = {};
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
_renderElements(state); _renderElements(state);
_bindHandlers(state); _bindHandlers(state);
dfd.resolve();
return dfd.promise();
}; };
// *************************************************************** // ***************************************************************
......
...@@ -8,15 +8,12 @@ function () { ...@@ -8,15 +8,12 @@ function () {
// VideoSpeedControl() function - what this module "exports". // VideoSpeedControl() function - what this module "exports".
return function (state) { return function (state) {
var dfd = $.Deferred();
state.videoSpeedControl = {}; state.videoSpeedControl = {};
if (state.videoType === 'html5') { _initialize(state);
_initialize(state); dfd.resolve();
} else if (state.videoType === 'youtube' && state.youtubeXhr) {
state.youtubeXhr.always(function () {
_initialize(state);
});
}
if (state.videoType === 'html5' && !(_checkPlaybackRates())) { if (state.videoType === 'html5' && !(_checkPlaybackRates())) {
console.log( console.log(
...@@ -24,9 +21,9 @@ function () { ...@@ -24,9 +21,9 @@ function () {
); );
_hideSpeedControl(state); _hideSpeedControl(state);
return;
} }
return dfd.promise();
}; };
// *************************************************************** // ***************************************************************
......
...@@ -21,11 +21,16 @@ function () { ...@@ -21,11 +21,16 @@ function () {
* @returns {undefined} * @returns {undefined}
*/ */
return function (state) { return function (state) {
var dfd = $.Deferred();
state.videoCaption = {}; state.videoCaption = {};
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
state.videoCaption.renderElements(); state.videoCaption.renderElements();
dfd.resolve();
return dfd.promise();
}; };
// *************************************************************** // ***************************************************************
...@@ -725,7 +730,7 @@ function () { ...@@ -725,7 +730,7 @@ function () {
}); });
} }
if (this.resizer) { if (this.resizer && !this.isFullScreen) {
this.resizer.alignByWidthOnly(); this.resizer.alignByWidthOnly();
} }
......
...@@ -94,20 +94,22 @@ function ( ...@@ -94,20 +94,22 @@ function (
state = {}; state = {};
previousState = state; previousState = state;
state.modules = [
FocusGrabber,
VideoControl,
VideoQualityControl,
VideoProgressSlider,
VideoVolumeControl,
VideoSpeedControl,
VideoCaption
];
state.youtubeXhr = youtubeXhr; state.youtubeXhr = youtubeXhr;
Initialize(state, element); Initialize(state, element);
if (!youtubeXhr) { if (!youtubeXhr) {
youtubeXhr = state.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 // 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 // it available to the caller by returning it. This is necessary so that we can test
// Video with Jasmine. // Video with Jasmine.
......
...@@ -98,6 +98,7 @@ def _write_styles(selector, output_root, classes): ...@@ -98,6 +98,7 @@ def _write_styles(selector, output_root, classes):
module_styles_lines = [] module_styles_lines = []
module_styles_lines.append("@import 'bourbon/bourbon';") module_styles_lines.append("@import 'bourbon/bourbon';")
module_styles_lines.append("@import 'bourbon/addons/button';") module_styles_lines.append("@import 'bourbon/addons/button';")
module_styles_lines.append("@import 'assets/anims';")
for class_, fragment_names in css_imports.items(): for class_, fragment_names in css_imports.items():
module_styles_lines.append("""{selector}.xmodule_{class_} {{""".format( module_styles_lines.append("""{selector}.xmodule_{class_} {{""".format(
class_=class_, selector=selector class_=class_, selector=selector
......
// 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;
}
}
...@@ -45,17 +45,15 @@ ...@@ -45,17 +45,15 @@
<div class="tc-wrapper"> <div class="tc-wrapper">
<a href="#before-transcript" class="nav-skip">${_("Skip to a navigable version of this video's transcript.")}</a> <a href="#before-transcript" class="nav-skip">${_("Skip to a navigable version of this video's transcript.")}</a>
<article class="video-wrapper"> <article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<div class="video-player-pre"></div> <div class="video-player-pre"></div>
<section class="video-player"> <section class="video-player">
<div id="${id}"></div> <div id="${id}"></div>
<h3 class="hidden">${_('ERROR: No playable video sources found!')}</h3> <h3 class="hidden">${_('ERROR: No playable video sources found!')}</h3>
</section> </section>
<div class="video-player-post"></div> <div class="video-player-post"></div>
<section class="video-controls"> <section class="video-controls">
<div class="slider" title="Video position"></div> <div class="slider" title="Video position"></div>
...@@ -87,14 +85,14 @@ ...@@ -87,14 +85,14 @@
</section> </section>
<a class="nav-skip" id="before-transcript" href="#after-transcript">${_('Skip to end of transcript.')}</a> <a class="nav-skip" id="before-transcript" href="#after-transcript">${_('Skip to end of transcript.')}</a>
</article> </article>
<ol id="transcript-captions" class="subtitles" tabindex="0" title="${_('Captions')}" role="group" aria-label="${_('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.')}"> <ol id="transcript-captions" class="subtitles" tabindex="0" title="${_('Captions')}" role="group" aria-label="${_('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.')}">
<li></li> <li></li>
</ol> </ol>
</div> </div>
<a class="nav-skip" id="after-transcript" href="#before-transcript">${_('Go back to start of transcript.')}</a> <a class="nav-skip" id="after-transcript" href="#before-transcript">${_('Go back to start of transcript.')}</a>
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
<ul class="wrapper-downloads"> <ul class="wrapper-downloads">
% if sources.get('main'): % if sources.get('main'):
......
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