Fix multiple video bug
This commit is contained in:
committed by
Alexander Kryklia
parent
14708aa30b
commit
a09e610403
@@ -40,6 +40,12 @@ div.video {
|
||||
padding-bottom: 56.25%;
|
||||
position: relative;
|
||||
|
||||
div {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
object, iframe {
|
||||
border: none;
|
||||
height: 100%;
|
||||
@@ -48,6 +54,15 @@ div.video {
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-align: center;
|
||||
color: white;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.video-controls {
|
||||
@@ -516,6 +531,12 @@ div.video {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
article.video-wrapper section.video-player {
|
||||
h3 {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
ol.subtitles {
|
||||
width: 0;
|
||||
height: 0;
|
||||
@@ -563,6 +584,12 @@ div.video {
|
||||
position: static;
|
||||
}
|
||||
|
||||
article.video-wrapper section.video-player {
|
||||
h3 {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
div.tc-wrapper {
|
||||
@include clearfix;
|
||||
display: table;
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
data-end=""
|
||||
data-caption-asset-path="/static/subs/"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
data-webm-source="test_files/test.webm"
|
||||
data-ogg-source="test_files/test.ogv"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
@@ -55,4 +57,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
data-webm-source="test_files/test.webm"
|
||||
data-ogg-source="test_files/test.ogv"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
@@ -27,4 +29,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
data-end=""
|
||||
data-caption-asset-path="/static/subs/"
|
||||
data-autoplay="False"
|
||||
data-yt-test-timeout="1500"
|
||||
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
|
||||
@@ -90,12 +90,24 @@ jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']
|
||||
jasmine.stubRequests = ->
|
||||
spyOn($, 'ajax').andCallFake (settings) ->
|
||||
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
|
||||
if settings.success
|
||||
status = match[1].split('_')
|
||||
if status and status[0] is 'status'
|
||||
{
|
||||
always: (callback) ->
|
||||
callback.call(window, {}, status[1])
|
||||
error: (callback) ->
|
||||
callback.call(window, {}, status[1])
|
||||
done: (callback) ->
|
||||
callback.call(window, {}, status[1])
|
||||
}
|
||||
else if settings.success
|
||||
# match[1] - it's video ID
|
||||
settings.success data: jasmine.stubbedMetadata[match[1]]
|
||||
else {
|
||||
always: (callback) ->
|
||||
callback.call(window, {}, 'success');
|
||||
callback.call(window, {}, 'success')
|
||||
done: (callback) ->
|
||||
callback.call(window, {}, 'success')
|
||||
}
|
||||
else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/
|
||||
settings.success jasmine.stubbedCaption
|
||||
|
||||
@@ -55,46 +55,6 @@
|
||||
expect(this.state.speed).toEqual('0.75');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check Youtube link existence', function () {
|
||||
var statusList = {
|
||||
error: 'html5',
|
||||
timeout: 'html5',
|
||||
abort: 'html5',
|
||||
parsererror: 'html5',
|
||||
success: 'youtube',
|
||||
notmodified: 'youtube'
|
||||
};
|
||||
|
||||
function stubDeffered(data, status) {
|
||||
return {
|
||||
always: function(callback) {
|
||||
callback.call(window, data, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkPlayer(videoType, data, status) {
|
||||
this.state = new window.Video('#example');
|
||||
spyOn(this.state , 'getVideoMetadata')
|
||||
.andReturn(stubDeffered(data, status));
|
||||
this.state.initialize('#example');
|
||||
|
||||
expect(this.state.videoType).toEqual(videoType);
|
||||
}
|
||||
|
||||
it('if video id is incorrect', function () {
|
||||
checkPlayer('html5', { error: {} }, 'success');
|
||||
});
|
||||
|
||||
$.each(statusList, function(status, mode){
|
||||
it('Status:' + status + ', mode:' + mode, function () {
|
||||
checkPlayer(mode, {}, status);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('HTML5', function () {
|
||||
@@ -154,10 +114,22 @@
|
||||
|
||||
it('parse Html5 sources', function () {
|
||||
var html5Sources = {
|
||||
mp4: 'test_files/test.mp4',
|
||||
webm: 'test_files/test.webm',
|
||||
ogg: 'test_files/test.ogv'
|
||||
};
|
||||
mp4: null,
|
||||
webm: null,
|
||||
ogg: null
|
||||
}, v = document.createElement('video');
|
||||
|
||||
if (!!(v.canPlayType && v.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''))) {
|
||||
html5Sources['webm'] = 'xmodule/include/fixtures/test.webm';
|
||||
}
|
||||
|
||||
if (!!(v.canPlayType && v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''))) {
|
||||
html5Sources['mp4'] = 'xmodule/include/fixtures/test.mp4';
|
||||
}
|
||||
|
||||
if (!!(v.canPlayType && v.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''))) {
|
||||
html5Sources['ogg'] = 'xmodule/include/fixtures/test.ogv';
|
||||
}
|
||||
|
||||
expect(state.html5Sources).toEqual(html5Sources);
|
||||
});
|
||||
|
||||
@@ -143,8 +143,6 @@ function (VideoPlayer) {
|
||||
if (state.parseYoutubeStreams(state.config.youtubeStreams)) {
|
||||
state.videoType = 'youtube';
|
||||
|
||||
state.fetchMetadata();
|
||||
state.parseSpeed();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -153,9 +151,7 @@ function (VideoPlayer) {
|
||||
// function _prepareHTML5Video(state)
|
||||
// The function prepare HTML5 video, parse HTML5
|
||||
// video sources etc.
|
||||
function _prepareHTML5Video(state) {
|
||||
state.videoType = 'html5';
|
||||
|
||||
function _prepareHTML5Video(state, html5Mode) {
|
||||
state.parseVideoSources(
|
||||
{
|
||||
mp4: state.config.mp4Source,
|
||||
@@ -164,20 +160,39 @@ function (VideoPlayer) {
|
||||
}
|
||||
);
|
||||
|
||||
if (html5Mode) {
|
||||
state.speeds = ['0.75', '1.0', '1.25', '1.50'];
|
||||
state.videos = {
|
||||
'0.75': state.config.sub,
|
||||
'1.0': state.config.sub,
|
||||
'1.25': state.config.sub,
|
||||
'1.5': state.config.sub
|
||||
};
|
||||
}
|
||||
|
||||
// We must have at least one non-YouTube video source available.
|
||||
// Otherwise, return a negative.
|
||||
if (
|
||||
state.html5Sources.webm === null &&
|
||||
state.html5Sources.mp4 === null &&
|
||||
state.html5Sources.ogg === null
|
||||
) {
|
||||
state.el.find('.video-player div').addClass('hidden');
|
||||
state.el.find('.video-player h3').removeClass('hidden');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
state.videoType = 'html5';
|
||||
|
||||
if (!state.config.sub || !state.config.sub.length) {
|
||||
state.config.sub = '';
|
||||
state.config.show_captions = false;
|
||||
}
|
||||
|
||||
state.speeds = ['0.75', '1.0', '1.25', '1.50'];
|
||||
state.videos = {
|
||||
'0.75': state.config.sub,
|
||||
'1.0': state.config.sub,
|
||||
'1.25': state.config.sub,
|
||||
'1.5': state.config.sub
|
||||
};
|
||||
|
||||
state.setSpeed($.cookie('video_speed'));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function _setConfigurations(state) {
|
||||
@@ -201,7 +216,7 @@ function (VideoPlayer) {
|
||||
// The function set initial configuration and preparation.
|
||||
|
||||
function initialize(element) {
|
||||
var _this = this;
|
||||
var _this = this, tempYtTestTimeout;
|
||||
// This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'.
|
||||
this.isFullScreen = false;
|
||||
|
||||
@@ -227,28 +242,61 @@ function (VideoPlayer) {
|
||||
webmSource: this.el.data('webm-source'),
|
||||
oggSource: this.el.data('ogg-source'),
|
||||
|
||||
ytTestUrl: this.el.data('yt-test-url'),
|
||||
|
||||
fadeOutTimeout: 1400,
|
||||
|
||||
availableQualities: ['hd720', 'hd1080', 'highres']
|
||||
};
|
||||
|
||||
// Check if the YT test timeout has been set. If not, or it is in
|
||||
// improper format, then set to default value.
|
||||
tempYtTestTimeout = parseInt(this.el.data('yt-test-timeout'), 10);
|
||||
if (!isFinite(tempYtTestTimeout)) {
|
||||
tempYtTestTimeout = 1500;
|
||||
}
|
||||
this.config.ytTestTimeout = tempYtTestTimeout;
|
||||
|
||||
if (!(_parseYouTubeIDs(this))) {
|
||||
// If we do not have YouTube ID's, try parsing HTML5 video sources.
|
||||
_prepareHTML5Video(this);
|
||||
if (!_prepareHTML5Video(this, true)) {
|
||||
// Non-YouTube sources were not found either.
|
||||
return;
|
||||
}
|
||||
|
||||
_setConfigurations(this);
|
||||
_renderElements(this);
|
||||
} else {
|
||||
this.getVideoMetadata()
|
||||
if (!this.youtubeXhr) {
|
||||
this.youtubeXhr = this.getVideoMetadata();
|
||||
}
|
||||
|
||||
this.youtubeXhr
|
||||
.always(function(json, status) {
|
||||
var err = $.isPlainObject(json.error) ||
|
||||
(status !== "success" && status !== "notmodified");
|
||||
|
||||
if (err){
|
||||
(status !== 'success' && status !== 'notmodified');
|
||||
if (err) {
|
||||
// When the youtube link doesn't work for any reason
|
||||
// (for example, the great firewall in china) any
|
||||
// alternate sources should automatically play.
|
||||
_prepareHTML5Video(_this);
|
||||
_this.el.find('a.quality_control').hide();
|
||||
if (!_prepareHTML5Video(_this)) {
|
||||
// Non-YouTube sources were not found either.
|
||||
|
||||
_this.el.find('.video-player div').removeClass('hidden');
|
||||
_this.el.find('.video-player h3').addClass('hidden');
|
||||
|
||||
// If in reality the timeout was to short, try to
|
||||
// continue loading the YouTube video anyways.
|
||||
_this.fetchMetadata();
|
||||
_this.parseSpeed();
|
||||
} else {
|
||||
// In-browser HTML5 player does not support quality
|
||||
// control.
|
||||
_this.el.find('a.quality_control').hide();
|
||||
}
|
||||
} else {
|
||||
_this.fetchMetadata();
|
||||
_this.parseSpeed();
|
||||
}
|
||||
|
||||
_setConfigurations(_this);
|
||||
@@ -294,7 +342,13 @@ function (VideoPlayer) {
|
||||
// Take the HTML5 sources (URLs of videos), and make them available explictly for each type
|
||||
// of video format (mp4, webm, ogg).
|
||||
function parseVideoSources(sources) {
|
||||
var _this = this;
|
||||
var _this = this,
|
||||
v = document.createElement('video'),
|
||||
sourceCodecs = {
|
||||
mp4: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
|
||||
webm: 'video/webm; codecs="vp8, vorbis"',
|
||||
ogg: 'video/ogg; codecs="theora"'
|
||||
};
|
||||
|
||||
this.html5Sources = {
|
||||
mp4: null,
|
||||
@@ -304,7 +358,14 @@ function (VideoPlayer) {
|
||||
|
||||
$.each(sources, function (name, source) {
|
||||
if (source && source.length) {
|
||||
_this.html5Sources[name] = source;
|
||||
if (
|
||||
Boolean(
|
||||
v.canPlayType &&
|
||||
v.canPlayType(sourceCodecs[name]).replace(/no/, '')
|
||||
)
|
||||
) {
|
||||
_this.html5Sources[name] = source;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -321,7 +382,9 @@ function (VideoPlayer) {
|
||||
|
||||
$.each(this.videos, function (speed, url) {
|
||||
_this.getVideoMetadata(url, function(data) {
|
||||
_this.metadata[data.data.id] = data.data;
|
||||
if (data.data) {
|
||||
_this.metadata[data.data.id] = data.data;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -358,12 +421,11 @@ function (VideoPlayer) {
|
||||
if (typeof url !== 'string') {
|
||||
url = this.videos['1.0'] || '';
|
||||
}
|
||||
|
||||
successHandler = ($.isFunction(callback)) ? callback : null;
|
||||
xhr = $.ajax({
|
||||
url: 'https://gdata.youtube.com/feeds/api/videos/' + url + '?v=2&alt=jsonc',
|
||||
timeout: 500,
|
||||
url: this.config.ytTestUrl + url + '?v=2&alt=jsonc',
|
||||
dataType: 'jsonp',
|
||||
timeout: this.config.ytTestTimeout,
|
||||
success: successHandler
|
||||
});
|
||||
|
||||
|
||||
@@ -10,21 +10,31 @@ function () {
|
||||
return function (state) {
|
||||
state.videoSpeedControl = {};
|
||||
|
||||
if (state.videoType === 'html5') {
|
||||
_initialize(state);
|
||||
} else if (state.videoType === 'youtube' && state.youtubeXhr) {
|
||||
state.youtubeXhr.done(function () {
|
||||
_initialize(state);
|
||||
});
|
||||
}
|
||||
|
||||
if (state.videoType === 'html5' && !(_checkPlaybackRates())) {
|
||||
_hideSpeedControl(state);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
// Private functions start here.
|
||||
// ***************************************************************
|
||||
|
||||
function _initialize(state) {
|
||||
_makeFunctionsPublic(state);
|
||||
_renderElements(state);
|
||||
_bindHandlers(state);
|
||||
}
|
||||
|
||||
// function _makeFunctionsPublic(state)
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called,
|
||||
|
||||
@@ -20,7 +20,8 @@ function (
|
||||
VideoSpeedControl,
|
||||
VideoCaption
|
||||
) {
|
||||
var previousState;
|
||||
var previousState,
|
||||
youtubeXhr = null;
|
||||
|
||||
// Because this constructor can be called multiple times on a single page (when
|
||||
// the user switches verticals, the page doesn't reload, but the content changes), we must
|
||||
@@ -53,7 +54,11 @@ function (
|
||||
state = {};
|
||||
previousState = state;
|
||||
|
||||
state.youtubeXhr = youtubeXhr;
|
||||
Initialize(state, element);
|
||||
if (!youtubeXhr) {
|
||||
youtubeXhr = state.youtubeXhr;
|
||||
}
|
||||
|
||||
VideoControl(state);
|
||||
VideoQualityControl(state);
|
||||
@@ -67,6 +72,10 @@ function (
|
||||
// Video with Jasmine.
|
||||
return state;
|
||||
};
|
||||
|
||||
window.Video.clearYoutubeXhr = function () {
|
||||
youtubeXhr = null;
|
||||
};
|
||||
});
|
||||
|
||||
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
|
||||
|
||||
@@ -167,6 +167,12 @@ class VideoModule(VideoFields, XModule):
|
||||
sources = {get_ext(src): src for src in self.html5_sources}
|
||||
sources['main'] = self.source
|
||||
|
||||
# for testing Youtube timeout in acceptance tests
|
||||
if getattr(settings, 'VIDEO_PORT', None):
|
||||
yt_test_url = "http://127.0.0.1:" + str(settings.VIDEO_PORT) + '/test_youtube/'
|
||||
else:
|
||||
yt_test_url = 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
|
||||
return self.system.render_template('video.html', {
|
||||
'youtube_streams': _create_youtube_string(self),
|
||||
'id': self.location.html_id(),
|
||||
@@ -181,7 +187,11 @@ class VideoModule(VideoFields, XModule):
|
||||
'show_captions': json.dumps(self.show_captions),
|
||||
'start': self.start_time,
|
||||
'end': self.end_time,
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
# TODO: Later on the value 1500 should be taken from some global
|
||||
# configuration setting field.
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': yt_test_url
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
Feature: Video component
|
||||
As a student, I want to view course videos in LMS.
|
||||
|
||||
|
||||
Scenario: Video component is fully rendered in the LMS in HTML5 mode
|
||||
Given the course has a Video component in HTML5 mode
|
||||
Then when I view the video it has rendered in HTML5 mode
|
||||
And all sources are correct
|
||||
|
||||
Scenario: Video component is fully rendered in the LMS in Youtube mode
|
||||
Given the course has a Video component in Youtube mode
|
||||
Then when I view the video it has rendered in Youtube mode
|
||||
|
||||
# Firefox doesn't have HTML5
|
||||
# Firefox doesn't have HTML5 (only mp4 - fix here)
|
||||
@skip_firefox
|
||||
Scenario: Autoplay is enabled in LMS for a Video component
|
||||
Given the course has a Video component in HTML5 mode
|
||||
Then when I view the video it has autoplay enabled
|
||||
|
||||
# Youtube testing
|
||||
Scenario: Video component is fully rendered in the LMS in Youtube mode with HTML5 sources
|
||||
Given youtube server is up and response time is 0.4 seconds
|
||||
And the course has a Video component in Youtube_HTML5 mode
|
||||
Then when I view the video it has rendered in Youtube mode
|
||||
|
||||
Scenario: Video component is not rendered in the LMS in Youtube mode with HTML5 sources
|
||||
Given youtube server is up and response time is 2 seconds
|
||||
And the course has a Video component in Youtube_HTML5 mode
|
||||
Then when I view the video it has rendered in HTML5 mode
|
||||
|
||||
Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources
|
||||
Given youtube server is up and response time is 2 seconds
|
||||
And the course has a Video component in Youtube mode
|
||||
Then when I view the video it has rendered in Youtube mode
|
||||
|
||||
Scenario: Video component is rendered in the LMS in Youtube mode with HTML5 sources that doesn't supported by browser
|
||||
Given youtube server is up and response time is 2 seconds
|
||||
And the course has a Video component in Youtube_HTML5_Unsupported_Video mode
|
||||
Then when I view the video it has rendered in Youtube mode
|
||||
|
||||
Scenario: Video component is rendered in the LMS in HTML5 mode with HTML5 sources that doesn't supported by browser
|
||||
Given the course has a Video component in HTML5_Unsupported_Video mode
|
||||
Then error message is shown
|
||||
And error message has correct text
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from common import i_am_registered_for_the_course, section_location
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
@@ -11,6 +12,9 @@ HTML5_SOURCES = [
|
||||
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm',
|
||||
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv'
|
||||
]
|
||||
HTML5_SOURCES_INCORRECT = [
|
||||
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99'
|
||||
]
|
||||
|
||||
@step('when I view the (.*) it has autoplay enabled$')
|
||||
def does_autoplay_video(_step, video_type):
|
||||
@@ -51,10 +55,37 @@ def add_video_to_course(course, player_mode):
|
||||
'html5_sources': HTML5_SOURCES
|
||||
}
|
||||
})
|
||||
if player_mode == 'youtube_html5':
|
||||
kwargs.update({
|
||||
'metadata': {
|
||||
'html5_sources': HTML5_SOURCES
|
||||
}
|
||||
})
|
||||
if player_mode == 'youtube_html5_unsupported_video':
|
||||
kwargs.update({
|
||||
'metadata': {
|
||||
'html5_sources': HTML5_SOURCES_INCORRECT
|
||||
}
|
||||
})
|
||||
if player_mode == 'html5_unsupported_video':
|
||||
kwargs.update({
|
||||
'metadata': {
|
||||
'youtube_id_1_0': '',
|
||||
'youtube_id_0_75': '',
|
||||
'youtube_id_1_25': '',
|
||||
'youtube_id_1_5': '',
|
||||
'html5_sources': HTML5_SOURCES_INCORRECT
|
||||
}
|
||||
})
|
||||
|
||||
world.ItemFactory.create(**kwargs)
|
||||
|
||||
|
||||
@step('youtube server is up and response time is (.*) seconds$')
|
||||
def set_youtube_response_timeout(_step, time):
|
||||
world.youtube_server.time_to_response = time
|
||||
|
||||
|
||||
@step('when I view the video it has rendered in (.*) mode$')
|
||||
def video_is_rendered(_step, mode):
|
||||
modes = {
|
||||
@@ -64,9 +95,23 @@ def video_is_rendered(_step, mode):
|
||||
html_tag = modes[mode.lower()]
|
||||
assert world.css_find('.video {0}'.format(html_tag)).first
|
||||
|
||||
|
||||
@step('all sources are correct$')
|
||||
def all_sources_are_correct(_step):
|
||||
sources = world.css_find('.video video source')
|
||||
assert set(source['src'] for source in sources) == set(HTML5_SOURCES)
|
||||
|
||||
|
||||
@step('error message is shown$')
|
||||
def error_message_is_shown(_step):
|
||||
selector = '.video .video-player h3'
|
||||
assert world.css_visible(selector)
|
||||
|
||||
|
||||
@step('error message has correct text$')
|
||||
def error_message_has_correct_text(_step):
|
||||
selector = '.video .video-player h3'
|
||||
text = _('ERROR: No playable video sources found!')
|
||||
assert world.css_has_text(selector, text)
|
||||
|
||||
|
||||
|
||||
45
lms/djangoapps/courseware/features/youtube_setup.py
Normal file
45
lms/djangoapps/courseware/features/youtube_setup.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from courseware.mock_youtube_server.mock_youtube_server import MockYoutubeServer
|
||||
from lettuce import before, after, world
|
||||
from django.conf import settings
|
||||
import threading
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@before.all
|
||||
def setup_mock_youtube_server():
|
||||
# import ipdb; ipdb.set_trace()
|
||||
server_host = '127.0.0.1'
|
||||
|
||||
server_port = settings.VIDEO_PORT
|
||||
|
||||
address = (server_host, server_port)
|
||||
|
||||
# Create the mock server instance
|
||||
server = MockYoutubeServer(address)
|
||||
logger.debug("Youtube server started at {} port".format(str(server_port)))
|
||||
|
||||
server.time_to_response = 1 # seconds
|
||||
|
||||
# Start the server running in a separate daemon thread
|
||||
# Because the thread is a daemon, it will terminate
|
||||
# when the main thread terminates.
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# Store the server instance in lettuce's world
|
||||
# so that other steps can access it
|
||||
# (and we can shut it down later)
|
||||
world.youtube_server = server
|
||||
|
||||
|
||||
@after.all
|
||||
def teardown_mock_youtube_server(total):
|
||||
|
||||
# Stop the LTI server and free up the port
|
||||
world.youtube_server.shutdown()
|
||||
@@ -0,0 +1,81 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import urlparse
|
||||
from requests.packages.oauthlib.oauth1.rfc5849 import signature
|
||||
import mock
|
||||
import threading
|
||||
import json
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
import time
|
||||
|
||||
class MockYoutubeRequestHandler(BaseHTTPRequestHandler):
|
||||
'''
|
||||
A handler for Youtube GET requests.
|
||||
'''
|
||||
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def do_HEAD(self):
|
||||
self._send_head()
|
||||
|
||||
def do_GET(self):
|
||||
'''
|
||||
Handle a GET request from the client and sends response back.
|
||||
'''
|
||||
self._send_head()
|
||||
|
||||
logger.debug("Youtube provider received GET request to path {}".format(
|
||||
self.path)
|
||||
) # Log the request
|
||||
|
||||
status_message = "I'm youtube."
|
||||
response_timeout = float(self.server.time_to_response)
|
||||
|
||||
# threading timer produces TypeError: 'NoneType' object is not callable here
|
||||
# so we use time.sleep, as we already in separate thread.
|
||||
time.sleep(response_timeout)
|
||||
self._send_response(status_message)
|
||||
|
||||
def _send_head(self):
|
||||
'''
|
||||
Send the response code and MIME headers
|
||||
'''
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
|
||||
def _send_response(self, message):
|
||||
'''
|
||||
Send message back to the client
|
||||
'''
|
||||
callback = urlparse.parse_qs(self.path)['callback'][0]
|
||||
response = callback + '({})'.format(json.dumps({'message': message}))
|
||||
# Log the response
|
||||
logger.debug("Youtube: sent response {}".format(message))
|
||||
|
||||
self.wfile.write(response)
|
||||
|
||||
|
||||
class MockYoutubeServer(HTTPServer):
|
||||
'''
|
||||
A mock Youtube provider server that responds
|
||||
to GET requests to localhost.
|
||||
'''
|
||||
|
||||
def __init__(self, address):
|
||||
'''
|
||||
Initialize the mock XQueue server instance.
|
||||
|
||||
*address* is the (host, host's port to listen to) tuple.
|
||||
'''
|
||||
handler = MockYoutubeRequestHandler
|
||||
HTTPServer.__init__(self, address, handler)
|
||||
|
||||
def shutdown(self):
|
||||
'''
|
||||
Stop the server and free up the port
|
||||
'''
|
||||
# First call superclass shutdown()
|
||||
HTTPServer.shutdown(self)
|
||||
# We also need to manually close the socket
|
||||
self.socket.close()
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Test for Mock_Youtube_Server
|
||||
"""
|
||||
import unittest
|
||||
import threading
|
||||
import urllib
|
||||
from mock_youtube_server import MockYoutubeServer
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class MockYoutubeServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the Youtube provider server that listens on a local
|
||||
port and responds with jsonp.
|
||||
|
||||
Used for lettuce BDD tests in lms/courseware/features/video.feature
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# This is a test of the test setup,
|
||||
# so it does not need to run as part of the unit test suite
|
||||
# You can re-enable it by commenting out the line below
|
||||
raise SkipTest
|
||||
|
||||
# Create the server
|
||||
server_port = 8034
|
||||
server_host = '127.0.0.1'
|
||||
address = (server_host, server_port)
|
||||
self.server = MockYoutubeServer(address, )
|
||||
self.server.time_to_response = 0.5
|
||||
# Start the server in a separate daemon thread
|
||||
server_thread = threading.Thread(target=self.server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
# Stop the server, freeing up the port
|
||||
self.server.shutdown()
|
||||
|
||||
def test_request(self):
|
||||
"""
|
||||
Tests that Youtube server processes request with right program
|
||||
path, and responses with incorrect signature.
|
||||
"""
|
||||
# GET request
|
||||
response_handle = urllib.urlopen(
|
||||
'http://127.0.0.1:8034/feeds/api/videos/OEoXaMPEzfM?v=2&alt=jsonc&callback=callback_func',
|
||||
)
|
||||
response = response_handle.read()
|
||||
self.assertEqual("""callback_func({"message": "I\'m youtube."})""", response)
|
||||
@@ -64,7 +64,9 @@ class TestVideo(BaseTestXmodule):
|
||||
'sub': u'a_sub_file.srt.sjson',
|
||||
'track': '',
|
||||
'youtube_streams': _create_youtube_string(self.item_module),
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
}
|
||||
|
||||
self.maxDiff = None
|
||||
@@ -114,7 +116,9 @@ class TestVideoNonYouTube(TestVideo):
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
'track': '',
|
||||
'youtube_streams': '1.00:OEoXaMPEzfM',
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected_context)
|
||||
|
||||
@@ -92,7 +92,9 @@ class VideoModuleUnitTest(unittest.TestCase):
|
||||
'sources': sources,
|
||||
'youtube_streams': _create_youtube_string(module),
|
||||
'track': '',
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
|
||||
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True),
|
||||
'yt_test_timeout': 1500,
|
||||
'yt_test_url': 'https://gdata.youtube.com/feeds/api/videos/'
|
||||
}
|
||||
|
||||
self.assertEqual(module.get_html(), expected_context)
|
||||
|
||||
@@ -82,6 +82,11 @@ XQUEUE_INTERFACE = {
|
||||
"basic_auth": ('anant', 'agarwal'),
|
||||
}
|
||||
|
||||
|
||||
# Set up Video information so that the lms will send
|
||||
# requests to a mock Youtube server running locally
|
||||
VIDEO_PORT = XQUEUE_PORT + 2
|
||||
|
||||
# Forums are disabled in test.py to speed up unit tests, but we do not have
|
||||
# per-test control for acceptance tests
|
||||
MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
|
||||
|
||||
@@ -70,6 +70,10 @@ XQUEUE_INTERFACE = {
|
||||
"basic_auth": ('anant', 'agarwal'),
|
||||
}
|
||||
|
||||
# Set up Video information so that the lms will send
|
||||
# requests to a mock Youtube server running locally
|
||||
VIDEO_PORT = XQUEUE_PORT + 2
|
||||
|
||||
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('courseware',)
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
data-end="${end}"
|
||||
data-caption-asset-path="${caption_asset_path}"
|
||||
data-autoplay="${autoplay}"
|
||||
data-yt-test-timeout="${yt_test_timeout}"
|
||||
data-yt-test-url="${yt_test_url}"
|
||||
>
|
||||
<div class="tc-wrapper">
|
||||
<article class="video-wrapper">
|
||||
@@ -30,6 +32,7 @@
|
||||
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user