Merge pull request #1642 from edx/anton/video-player-improvements
Video player: improvements.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from static_replace import replace_static_urls
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
154
common/static/sass/assets/_anims.scss
Normal file
154
common/static/sass/assets/_anims.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -45,17 +45,15 @@
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<a href="#before-transcript" class="nav-skip">${_("Skip to a navigable version of this video's transcript.")}</a>
|
||||
|
||||
<article class="video-wrapper">
|
||||
<div class="video-player-pre"></div>
|
||||
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<div class="video-player-pre"></div>
|
||||
<section class="video-player">
|
||||
<div id="${id}"></div>
|
||||
<h3 class="hidden">${_('ERROR: No playable video sources found!')}</h3>
|
||||
</section>
|
||||
|
||||
<div class="video-player-post"></div>
|
||||
|
||||
<section class="video-controls">
|
||||
<div class="slider" title="Video position"></div>
|
||||
|
||||
@@ -87,14 +85,14 @@
|
||||
</section>
|
||||
<a class="nav-skip" id="before-transcript" href="#after-transcript">${_('Skip to end of transcript.')}</a>
|
||||
</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.')}">
|
||||
<li></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
<a class="nav-skip" id="after-transcript" href="#before-transcript">${_('Go back to start of transcript.')}</a>
|
||||
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
<ul class="wrapper-downloads">
|
||||
% if sources.get('main'):
|
||||
|
||||
Reference in New Issue
Block a user