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,
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;
};
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,
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
......
// 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">
<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'):
......
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