Commit db5eee87 by jmclaus

Merge pull request #3151 from edx/jmclaus/disable_hd_control_when_hd_qualities_not_available

Disable HD control when HD is not available [BLD-937]
parents 2eb997cd f05c0aa8
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,8 @@ 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: Show the HD button only if there is an HD version available. BLD-937.
Studio: Add edit button to leaf xblocks on the container page. STUD-1306. Studio: Add edit button to leaf xblocks on the container page. STUD-1306.
Blades: Add LTI context_id parameter. BLD-584. Blades: Add LTI context_id parameter. BLD-584.
......
...@@ -320,7 +320,7 @@ div.video { ...@@ -320,7 +320,7 @@ div.video {
a.speed-button, a.speed-button,
div.volume > a, div.volume > a,
a.add-fullscreen, a.add-fullscreen,
a.quality_control, a.quality-control,
a.hide-subtitles { a.hide-subtitles {
// overflow is used to bypass Firefox CSS :focus outline bug // overflow is used to bypass Firefox CSS :focus outline bug
// http://johndoesdesign.com/blog/2012/css/firefox-and-its-css-focus-outline-bug/ // http://johndoesdesign.com/blog/2012/css/firefox-and-its-css-focus-outline-bug/
...@@ -536,11 +536,10 @@ div.video { ...@@ -536,11 +536,10 @@ div.video {
} }
a.quality_control { a.quality-control {
@extend %video-button; @extend %video-button;
background: url(../images/hd.png) center no-repeat; background: url(../images/hd.png) center no-repeat;
border-left: none; border-left: none;
display: none;
float: left; float: left;
padding: 0 11px; padding: 0 11px;
width: 30px; width: 30px;
...@@ -550,6 +549,10 @@ div.video { ...@@ -550,6 +549,10 @@ div.video {
color: #0ff; color: #0ff;
text-decoration: none; text-decoration: none;
} }
&.is-hidden {
display: none !important;
}
} }
div.lang { div.lang {
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
</div> </div>
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a> <a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
<div class="lang menu-container"> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div> </div>
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
</div> </div>
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a> <a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
<div class="lang menu-container"> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div> </div>
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
</div> </div>
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser" role="button" aria-disabled="false">Fill Browser</a>
<a href="#" class="quality_control" title="HD off" role="button" aria-disabled="false">HD off</a> <a href="#" class="quality-control is-hidden" title="HD off" role="button" aria-disabled="false">HD off</a>
<div class="lang menu-container"> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div> </div>
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
</div> </div>
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a> <a href="#" class="quality-control is-hidden" title="HD">HD</a>
<div class="lang menu-container"> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div> </div>
...@@ -186,7 +186,7 @@ ...@@ -186,7 +186,7 @@
</div> </div>
</div> </div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a> <a href="#" class="quality-control is-hidden" title="HD">HD</a>
<div class="lang menu-container"> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a> <a href="#" class="hide-subtitles" title="Turn off captions" role="button" aria-disabled="false">Captions</a>
</div> </div>
......
...@@ -8,12 +8,16 @@ ...@@ -8,12 +8,16 @@
'getPlayerState', 'getVolume', 'setVolume', 'getPlayerState', 'getVolume', 'setVolume',
'loadVideoById', 'getAvailablePlaybackRates', 'playVideo', 'loadVideoById', 'getAvailablePlaybackRates', 'playVideo',
'pauseVideo', 'seekTo', 'getDuration', 'setPlaybackRate', 'pauseVideo', 'seekTo', 'getDuration', 'setPlaybackRate',
'getPlaybackQuality', 'destroy' 'getAvailableQualityLevels', 'getPlaybackQuality',
'setPlaybackQuality', 'destroy'
] ]
); );
Player.getDuration.andReturn(60); Player.getDuration.andReturn(60);
Player.getAvailablePlaybackRates.andReturn([0.50, 1.0, 1.50, 2.0]); Player.getAvailablePlaybackRates.andReturn([0.50, 1.0, 1.50, 2.0]);
Player.getAvailableQualityLevels.andReturn(
['highres', 'hd1080', 'hd720', 'large', 'medium', 'small']
);
return Player; return Player;
}, },
......
(function (undefined) { (function (undefined) {
describe('VideoQualityControl', function () { describe('VideoQualityControl', function () {
var state, oldOTBD; var state, qualityControl, qualityControlEl, videoPlayer, player;
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
.andReturn(null);
});
afterEach(function () { afterEach(function () {
$('source').remove(); $('source').remove();
window.onTouchBasedDevice = oldOTBD; if (state.storage) {
state.storage.clear(); state.storage.clear();
}
}); });
describe('constructor', function () { describe('constructor, YouTube mode', function () {
beforeEach(function () { beforeEach(function () {
state = jasmine.initializePlayer('video.html'); state = jasmine.initializePlayerYouTube();
}); qualityControl = state.videoQualityControl;
videoPlayer = state.videoPlayer;
it('render the quality control', function () { player = videoPlayer.player;
waitsFor(function () {
return state.videoControl;
}, 'videoControl is present', 5000);
runs(function () { // Define empty methods in YouTube stub
var container = state.videoControl.secondaryControlsEl; player.quality = 'large';
player.setPlaybackQuality.andCallFake(function (quality){
expect(container).toContain('a.quality_control'); player.quality = quality;
}); });
}); });
it('add ARIA attributes to quality control', function () { it('contains the quality control and is initially hidden',
var qualityControl = $('a.quality_control'); function () {
expect(qualityControl.el).toHaveClass(
'quality-control is-hidden'
);
});
expect(qualityControl).toHaveAttrs({ it('add ARIA attributes to quality control', function () {
expect(qualityControl.el).toHaveAttrs({
'role': 'button', 'role': 'button',
'title': 'HD off', 'title': 'HD off',
'aria-disabled': 'false' 'aria-disabled': 'false'
...@@ -43,9 +39,60 @@ ...@@ -43,9 +39,60 @@
}); });
it('bind the quality control', function () { it('bind the quality control', function () {
var handler = state.videoQualityControl.toggleQuality; expect(qualityControl.el).toHandleWith('click',
qualityControl.toggleQuality
);
expect(state.el).toHandle('play');
});
it('calls fetchAvailableQualities only once', function () {
expect(player.getAvailableQualityLevels.calls.length)
.toEqual(0);
videoPlayer.onPlay();
videoPlayer.onPlay();
expect(player.getAvailableQualityLevels.calls.length)
.toEqual(1);
});
it('shows the quality control on play if HD is available',
function () {
videoPlayer.onPlay();
expect(qualityControl.el).not.toHaveClass('is-hidden');
});
it('leaves quality control hidden on play if HD is not available',
function () {
player.getAvailableQualityLevels.andReturn(
['large', 'medium', 'small']
);
videoPlayer.onPlay();
expect(qualityControl.el).toHaveClass('is-hidden');
});
it('switch to HD if it is available', function () {
videoPlayer.onPlay();
qualityControl.quality = 'large';
qualityControl.el.click();
expect(player.setPlaybackQuality)
.toHaveBeenCalledWith('highres');
qualityControl.quality = 'highres';
qualityControl.el.click();
expect(player.setPlaybackQuality).toHaveBeenCalledWith('large');
});
});
describe('constructor, HTML5 mode', function () {
it('does not contain the quality control', function () {
state = jasmine.initializePlayer();
expect($('a.quality_control')).toHandleWith('click', handler); expect(state.el.find('a.quality-control').length).toBe(0);
}); });
}); });
}); });
......
...@@ -511,8 +511,10 @@ function (VideoPlayer, VideoStorage) { ...@@ -511,8 +511,10 @@ function (VideoPlayer, VideoStorage) {
element: element, element: element,
fadeOutTimeout: 1400, fadeOutTimeout: 1400,
captionsFreezeTime: 10000, captionsFreezeTime: 10000,
availableQualities: ['hd720', 'hd1080', 'highres'], mode: $.cookie('edX_video_player_mode'),
mode: $.cookie('edX_video_player_mode') // Available HD qualities will only be accessible once the video has
// been played once, via player.getAvailableQualityLevels.
availableHDQualities: []
}); });
if (this.config.endTime < this.config.startTime) { if (this.config.endTime < this.config.startTime) {
......
...@@ -12,7 +12,7 @@ function () { ...@@ -12,7 +12,7 @@ function () {
// 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') {
state.el.find('a.quality_control').remove(); state.el.find('a.quality-control').remove();
return; return;
} }
...@@ -36,7 +36,9 @@ function () { ...@@ -36,7 +36,9 @@ function () {
// get the 'state' object as a context. // get the 'state' object as a context.
function _makeFunctionsPublic(state) { function _makeFunctionsPublic(state) {
var methodsDict = { var methodsDict = {
fetchAvailableQualities: fetchAvailableQualities,
onQualityChange: onQualityChange, onQualityChange: onQualityChange,
showQualityControl: showQualityControl,
toggleQuality: toggleQuality toggleQuality: toggleQuality
}; };
...@@ -49,17 +51,22 @@ function () { ...@@ -49,17 +51,22 @@ function () {
// make the created DOM elements available via the 'state' object. Much easier to work this // 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. // way - you don't have to do repeated jQuery element selects.
function _renderElements(state) { function _renderElements(state) {
state.videoQualityControl.el = state.el.find('a.quality_control'); state.videoQualityControl.el = state.el.find('a.quality-control');
state.videoQualityControl.el.show(); state.videoQualityControl.el.show();
state.videoQualityControl.quality = null; state.videoQualityControl.quality = 'large';
} }
// function _bindHandlers(state) // function _bindHandlers(state)
// //
// Bind any necessary function callbacks to DOM events (click, mousemove, etc.). // Bind any necessary function callbacks to DOM events (click, mousemove, etc.).
function _bindHandlers(state) { function _bindHandlers(state) {
state.videoQualityControl.el.on('click', state.videoQualityControl.toggleQuality); state.videoQualityControl.el.on('click',
state.videoQualityControl.toggleQuality
);
state.el.on('play',
_.once(state.videoQualityControl.fetchAvailableQualities)
);
} }
// *************************************************************** // ***************************************************************
...@@ -68,11 +75,42 @@ function () { ...@@ -68,11 +75,42 @@ function () {
// The magic private function that makes them available and sets up their context is makeFunctionsPublic(). // The magic private function that makes them available and sets up their context is makeFunctionsPublic().
// *************************************************************** // ***************************************************************
/*
* @desc Shows quality control. This function will only be called if HD
* qualities are available.
*
* @public
*/
function showQualityControl() {
this.videoQualityControl.el.removeClass('is-hidden');
}
// This function can only be called once as _.once has been used.
/*
* @desc Get the available qualities from YouTube API. Possible values are:
['highres', 'hd1080', 'hd720', 'large', 'medium', 'small'].
HD are: ['highres', 'hd1080', 'hd720'].
*
* @public
*/
function fetchAvailableQualities() {
var qualities = this.videoPlayer.player.getAvailableQualityLevels();
this.config.availableHDQualities = _.intersection(
qualities, ['highres', 'hd1080', 'hd720']
);
// HD qualities are available, show video quality control.
if (this.config.availableHDQualities.length > 0) {
this.trigger('videoQualityControl.showQualityControl');
}
}
function onQualityChange(value) { function onQualityChange(value) {
var controlStateStr; var controlStateStr;
this.videoQualityControl.quality = value; this.videoQualityControl.quality = value;
if (_.indexOf(this.config.availableQualities, value) !== -1) { if (_.contains(this.config.availableHDQualities, value)) {
controlStateStr = gettext('HD on'); controlStateStr = gettext('HD on');
this.videoQualityControl.el this.videoQualityControl.el
.addClass('active') .addClass('active')
...@@ -88,24 +126,15 @@ function () { ...@@ -88,24 +126,15 @@ function () {
} }
} }
// This function change quality of video. // This function toggles the quality of video only if HD qualities are
// Right now we haven't ability to choose quality of HD video, // available.
// 'hd720' will be played by default as HD video(this thing is hardcoded).
// If suggested quality level is not available for the video,
// then the quality will be set to the next lowest level that is available.
// (large -> medium)
function toggleQuality(event) { function toggleQuality(event) {
var newQuality, var newQuality, value = this.videoQualityControl.quality,
value = this.videoQualityControl.quality; isHD = _.contains(this.config.availableHDQualities, value);
event.preventDefault(); event.preventDefault();
if (_.indexOf(this.config.availableQualities, value) !== -1) { newQuality = isHD ? 'large' : 'highres';
newQuality = 'large';
} else {
newQuality = 'hd720';
}
this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality); this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality);
} }
......
...@@ -297,3 +297,20 @@ Feature: LMS.Video component ...@@ -297,3 +297,20 @@ Feature: LMS.Video component
# Then I select the "1.25" speed # Then I select the "1.25" speed
# And I click video button "pause" # And I click video button "pause"
# And I click on caption line "2", video module shows elapsed time "4" # And I click on caption line "2", video module shows elapsed time "4"
# 27
@skip_firefox
Scenario: Quality button appears on play
Given the course has a Video component in "Youtube" mode
Then I see video button "quality" is hidden
And I click video button "play"
Then I see video button "quality" is visible
# 28
@skip_firefox
Scenario: Quality button works correctly
Given the course has a Video component in "Youtube" mode
And I click video button "play"
And I see video button "quality" is inactive
And I click video button "quality"
Then I see video button "quality" is active
...@@ -6,7 +6,7 @@ import json ...@@ -6,7 +6,7 @@ import json
import os import os
import time import time
import requests import requests
from nose.tools import assert_less, assert_equal from nose.tools import assert_less, assert_equal, assert_true, assert_false
from common import i_am_registered_for_the_course, visit_scenario_item from common import i_am_registered_for_the_course, visit_scenario_item
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
...@@ -44,6 +44,7 @@ VIDEO_BUTTONS = { ...@@ -44,6 +44,7 @@ VIDEO_BUTTONS = {
'pause': '.video_control.pause', 'pause': '.video_control.pause',
'fullscreen': '.add-fullscreen', 'fullscreen': '.add-fullscreen',
'download_transcript': '.video-tracks > a', 'download_transcript': '.video-tracks > a',
'quality': '.quality-control',
} }
VIDEO_MENUS = { VIDEO_MENUS = {
...@@ -542,11 +543,6 @@ def upload_to_assets(_step, filename): ...@@ -542,11 +543,6 @@ def upload_to_assets(_step, filename):
upload_file(filename, world.scenario_dict['COURSE'].location) upload_file(filename, world.scenario_dict['COURSE'].location)
@step('button "([^"]*)" is hidden$')
def is_hidden_button(_step, button):
assert not world.css_visible(VIDEO_BUTTONS[button])
@step('menu "([^"]*)" doesn\'t exist$') @step('menu "([^"]*)" doesn\'t exist$')
def is_hidden_menu(_step, menu): def is_hidden_menu(_step, menu):
assert world.is_css_not_present(VIDEO_MENUS[menu]) assert world.is_css_not_present(VIDEO_MENUS[menu])
...@@ -627,3 +623,30 @@ def click_on_the_caption(_step, index, expected_time): ...@@ -627,3 +623,30 @@ def click_on_the_caption(_step, index, expected_time):
find_caption_line_by_data_index(int(index)).click() find_caption_line_by_data_index(int(index)).click()
actual_time = elapsed_time() actual_time = elapsed_time()
assert int(expected_time) == actual_time assert int(expected_time) == actual_time
@step('button "([^"]*)" is (hidden|visible)$')
def is_hidden_button(_step, button, state):
selector = VIDEO_BUTTONS[button]
if state == 'hidden':
world.wait_for_invisible(selector)
assert_false(
world.css_visible(selector),
'Button {0} is invisible, but should be visible'.format(button)
)
else:
world.wait_for_visible(selector)
assert_true(
world.css_visible(selector),
'Button {0} is visible, but should be invisible'.format(button)
)
@step('button "([^"]*)" is (active|inactive)$')
def i_see_active_button(_step, button, state):
selector = VIDEO_BUTTONS[button]
if state == 'active':
assert world.css_has_class(selector, 'active')
else:
assert not world.css_has_class(selector, 'active')
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
</div> </div>
</div> </div>
<a href="#" class="add-fullscreen" title="${_('Fill browser')}" role="button" aria-disabled="false">${_('Fill browser')}</a> <a href="#" class="add-fullscreen" title="${_('Fill browser')}" role="button" aria-disabled="false">${_('Fill browser')}</a>
<a href="#" class="quality_control" title="${_('HD off')}" role="button" aria-disabled="false">${_('HD off')}</a> <a href="#" class="quality-control is-hidden" title="${_('HD off')}" role="button" aria-disabled="false">${_('HD off')}</a>
<div class="lang menu-container"> <div class="lang menu-container">
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria- <a href="#" class="hide-subtitles" title="${_('Turn off captions')}" role="button" aria-
......
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