Commit 7b2d67f2 by polesye

BLD-1049: Allow the video player to work with redirected links.

parent 7228d41a
...@@ -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: Fix bug with incorrect link format and redirection. BLD-1049
Blades: Fix bug with incorrect RelativeTime value after XML serialization. BLD-1060 Blades: Fix bug with incorrect RelativeTime value after XML serialization. BLD-1060
LMS: Update bulk email implementation to lessen load on the database LMS: Update bulk email implementation to lessen load on the database
......
...@@ -34,14 +34,22 @@ Feature: CMS Transcripts ...@@ -34,14 +34,22 @@ Feature: CMS Transcripts
And I expect inputs are enabled And I expect inputs are enabled
#User input URL with incorrect format #User input URL with incorrect format
And I enter a "htt://link.c" source to field number 1 And I enter a "http://link.c" source to field number 1
Then I see error message "url_format" Then I see error message "url_format"
# Currently we are working with 1st field. It means, that if 1st field # Currently we are working with 1st field. It means, that if 1st field
# contain incorrect value, 2nd and 3rd fields should be disabled until # contain incorrect value, 2nd and 3rd fields should be disabled until
# 1st field will be filled by correct correct value # 1st field will be filled by correct correct value
And I expect 2, 3 inputs are disabled And I expect 2, 3 inputs are disabled
# We are not clearing fields here,
# Because we changing same field. #User input URL with incorrect format
And I enter a "http://goo.gl/pxxZrg" source to field number 1
And I enter a "http://goo.gl/pxxZrg" source to field number 2
Then I see error message "links_duplication"
And I expect 1, 3 inputs are disabled
And I clear fields
And I expect inputs are enabled
And I enter a "http://youtu.be/t_not_exist" source to field number 1 And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I do not see error message Then I do not see error message
And I expect inputs are enabled And I expect inputs are enabled
...@@ -699,3 +707,37 @@ Feature: CMS Transcripts ...@@ -699,3 +707,37 @@ Feature: CMS Transcripts
And I edit the component And I edit the component
Then I see status message "found" Then I see status message "found"
#37 Uploading subtitles with different file name than file
Scenario: Shortened link: File name and name of subs are different
Given I have created a Video component
And I edit the component
And I enter a "http://goo.gl/pxxZrg" source to field number 1
And I see status message "not found"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see value "pxxZrg" in the field "Default Timed Transcript"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
#38 Uploading subtitles with different file name than file
Scenario: Relative link: File name and name of subs are different
Given I have created a Video component
And I edit the component
And I enter a "/gizmo.webm" source to field number 1
And I see status message "not found"
And I upload the transcripts file "uk_transcripts.srt"
Then I see status message "uploaded_successfully"
And I see value "gizmo" in the field "Default Timed Transcript"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
...@@ -19,6 +19,7 @@ DELAY = 0.5 ...@@ -19,6 +19,7 @@ DELAY = 0.5
ERROR_MESSAGES = { ERROR_MESSAGES = {
'url_format': u'Incorrect url format.', 'url_format': u'Incorrect url format.',
'file_type': u'Link types should be unique.', 'file_type': u'Link types should be unique.',
'links_duplication': u'Links should be unique.',
} }
STATUSES = { STATUSES = {
...@@ -43,7 +44,7 @@ TRANSCRIPTS_BUTTONS = { ...@@ -43,7 +44,7 @@ TRANSCRIPTS_BUTTONS = {
'import': ('.setting-import', 'Import YouTube Transcript'), 'import': ('.setting-import', 'Import YouTube Transcript'),
'download_to_edit': ('.setting-download', 'Download Transcript for Editing'), 'download_to_edit': ('.setting-download', 'Download Transcript for Editing'),
'disabled_download_to_edit': ('.setting-download.is-disabled', 'Download Transcript for Editing'), 'disabled_download_to_edit': ('.setting-download.is-disabled', 'Download Transcript for Editing'),
'upload_new_timed_transcripts': ('.setting-upload', 'Upload New Transcript'), 'upload_new_timed_transcripts': ('.setting-upload', 'Upload New Transcript'),
'replace': ('.setting-replace', 'Yes, replace the edX transcript with the YouTube transcript'), 'replace': ('.setting-replace', 'Yes, replace the edX transcript with the YouTube transcript'),
'choose': ('.setting-choose', 'Timed Transcript from {}'), 'choose': ('.setting-choose', 'Timed Transcript from {}'),
'use_existing': ('.setting-use-existing', 'Use Current Transcript'), 'use_existing': ('.setting-use-existing', 'Use Current Transcript'),
...@@ -118,8 +119,7 @@ def i_see_status_message(_step, status): ...@@ -118,8 +119,7 @@ def i_see_status_message(_step, status):
assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status]) assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status])
DOWNLOAD_BUTTON = TRANSCRIPTS_BUTTONS["download_to_edit"][0] DOWNLOAD_BUTTON = TRANSCRIPTS_BUTTONS["download_to_edit"][0]
if world.is_css_present(DOWNLOAD_BUTTON, wait_time=1) \ if world.is_css_present(DOWNLOAD_BUTTON, wait_time=1) and not world.css_find(DOWNLOAD_BUTTON)[0].has_class('is-disabled'):
and not world.css_find(DOWNLOAD_BUTTON)[0].has_class('is-disabled'):
assert _transcripts_are_downloaded() assert _transcripts_are_downloaded()
...@@ -210,7 +210,7 @@ def check_text_in_the_captions(_step, text): ...@@ -210,7 +210,7 @@ def check_text_in_the_captions(_step, text):
@step('I see value "([^"]*)" in the field "([^"]*)"$') @step('I see value "([^"]*)" in the field "([^"]*)"$')
def check_transcripts_field(_step, values, field_name): def check_transcripts_field(_step, values, field_name):
world.select_editor_tab('Advanced') world.select_editor_tab('Advanced')
tab = world.css_find('#settings-tab').first; tab = world.css_find('#settings-tab').first
field_id = '#' + tab.find_by_xpath('.//label[text()="%s"]' % field_name.strip())[0]['for'] field_id = '#' + tab.find_by_xpath('.//label[text()="%s"]' % field_name.strip())[0]['for']
values_list = [i.strip() == world.css_value(field_id) for i in values.split('|')] values_list = [i.strip() == world.css_value(field_id) for i in values.split('|')]
assert any(values_list) assert any(values_list)
...@@ -229,19 +229,19 @@ def open_tab(_step, tab_name): ...@@ -229,19 +229,19 @@ def open_tab(_step, tab_name):
@step('I set value "([^"]*)" to the field "([^"]*)"$') @step('I set value "([^"]*)" to the field "([^"]*)"$')
def set_value_transcripts_field(_step, value, field_name): def set_value_transcripts_field(_step, value, field_name):
tab = world.css_find('#settings-tab').first; tab = world.css_find('#settings-tab').first
XPATH = './/label[text()="{name}"]'.format(name=field_name) XPATH = './/label[text()="{name}"]'.format(name=field_name)
SELECTOR = '#' + tab.find_by_xpath(XPATH)[0]['for'] SELECTOR = '#' + tab.find_by_xpath(XPATH)[0]['for']
element = world.css_find(SELECTOR).first element = world.css_find(SELECTOR).first
if element['type'] == 'text': if element['type'] == 'text':
SCRIPT = '$("{selector}").val("{value}").change()'.format( SCRIPT = '$("{selector}").val("{value}").change()'.format(
selector=SELECTOR, selector=SELECTOR,
value=value value=value
) )
world.browser.execute_script(SCRIPT) world.browser.execute_script(SCRIPT)
assert world.css_has_value(SELECTOR, value) assert world.css_has_value(SELECTOR, value)
else: else:
assert False, 'Incorrect element type.'; assert False, 'Incorrect element type.'
world.wait_for_ajax_complete() world.wait_for_ajax_complete()
......
...@@ -26,7 +26,7 @@ describe('Transcripts.Utils', function () { ...@@ -26,7 +26,7 @@ describe('Transcripts.Utils', function () {
} (videoId)), } (videoId)),
html5FileName = 'file_name', html5FileName = 'file_name',
html5LinksList = (function (videoName) { html5LinksList = (function (videoName) {
var videoTypes = ['mp4', 'webm'], var videoTypes = ['mp4', 'webm', 'm4v', 'ogv'],
links = [ links = [
'http://somelink.com/%s.%s?param=1&param=2#hash', 'http://somelink.com/%s.%s?param=1&param=2#hash',
'http://somelink.com/%s.%s#hash', 'http://somelink.com/%s.%s#hash',
...@@ -34,6 +34,7 @@ describe('Transcripts.Utils', function () { ...@@ -34,6 +34,7 @@ describe('Transcripts.Utils', function () {
'http://somelink.com/%s.%s', 'http://somelink.com/%s.%s',
'ftp://somelink.com/%s.%s', 'ftp://somelink.com/%s.%s',
'https://somelink.com/%s.%s', 'https://somelink.com/%s.%s',
'https://somelink.com/sub/sub/%s.%s',
'http://cdn.somecdn.net/v/%s.%s', 'http://cdn.somecdn.net/v/%s.%s',
'somelink.com/%s.%s', 'somelink.com/%s.%s',
'%s.%s' '%s.%s'
...@@ -48,7 +49,25 @@ describe('Transcripts.Utils', function () { ...@@ -48,7 +49,25 @@ describe('Transcripts.Utils', function () {
return data; return data;
} (html5FileName)); } (html5FileName)),
otherLinkId = 'other_link_id',
otherLinksList = (function (linkId) {
var links = [
'http://goo.gl/%s?param=1&param=2#hash',
'http://goo.gl/%s?param=1&param=2',
'http://goo.gl/%s#hash',
'http://goo.gl/%s',
'http://goo.gl/%s',
'ftp://goo.gl/%s',
'https://goo.gl/%s',
'%s'
];
return $.map(links, function (link) {
return _str.sprintf(link, linkId);
});
} (otherLinkId));
describe('Method: getField', function (){ describe('Method: getField', function (){
var collection, var collection,
...@@ -107,7 +126,6 @@ describe('Transcripts.Utils', function () { ...@@ -107,7 +126,6 @@ describe('Transcripts.Utils', function () {
}); });
describe('Wrong arguments ', function () { describe('Wrong arguments ', function () {
beforeEach(function(){ beforeEach(function(){
spyOn(console, 'log'); spyOn(console, 'log');
}); });
...@@ -124,18 +142,9 @@ describe('Transcripts.Utils', function () { ...@@ -124,18 +142,9 @@ describe('Transcripts.Utils', function () {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it('videoId is wrong', function () {
var videoId = 'wrong_id',
link = 'http://youtu.be/' + videoId,
result = Utils.parseYoutubeLink(link);
expect(result).toBeUndefined();
});
var wrongUrls = [ var wrongUrls = [
'http://youtu.bee/' + videoId,
'http://youtu.be/', 'http://youtu.be/',
'example.com', '/static/example',
'http://google.com/somevideo.mp4' 'http://google.com/somevideo.mp4'
]; ];
...@@ -163,10 +172,20 @@ describe('Transcripts.Utils', function () { ...@@ -163,10 +172,20 @@ describe('Transcripts.Utils', function () {
}); });
}); });
}); });
$.each(otherLinksList, function (index, link) {
it(link, function () {
var result = Utils.parseHTML5Link(link);
expect(result).toEqual({
video: otherLinkId,
type: 'other'
});
});
});
}); });
describe('Wrong arguments ', function () { describe('Wrong arguments ', function () {
beforeEach(function(){ beforeEach(function(){
spyOn(console, 'log'); spyOn(console, 'log');
}); });
...@@ -184,15 +203,11 @@ describe('Transcripts.Utils', function () { ...@@ -184,15 +203,11 @@ describe('Transcripts.Utils', function () {
}); });
var html5WrongUrls = [ var html5WrongUrls = [
'http://youtu.bee/' + videoId,
'http://youtu.be/', 'http://youtu.be/',
'example.com', 'http://example.com/.mp4',
'http://google.com/somevideo.mp1', 'http://example.com/video_name.',
'http://google.com/somevideomp4', 'http://example.com/',
'http://google.com/somevideo_mp4', 'http://example.com'
'http://google.com/somevideo:mp4',
'http://google.com/somevideo',
'http://google.com/somevideo.webm_'
]; ];
$.each(html5WrongUrls, function (index, link) { $.each(html5WrongUrls, function (index, link) {
...@@ -248,6 +263,13 @@ describe('Transcripts.Utils', function () { ...@@ -248,6 +263,13 @@ describe('Transcripts.Utils', function () {
}); });
describe('Wrong arguments ', function () { describe('Wrong arguments ', function () {
it('youtube videoId is wrong', function () {
var videoId = 'wrong_id',
link = 'http://youtu.be/' + videoId,
result = Utils.parseLink(link);
expect(result).toEqual({ mode : 'incorrect' });
});
it('no arguments', function () { it('no arguments', function () {
var result = Utils.parseLink(); var result = Utils.parseLink();
......
...@@ -15,9 +15,7 @@ ...@@ -15,9 +15,7 @@
data-transcript-translation-url="/transcript/translation" data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations" data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y" data-sub="Z5KLxerq05Y"
data-mp4-source="xmodule/include/fixtures/test.mp4" data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]'
data-webm-source="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api" data-yt-api-url="www.youtube.com/iframe_api"
......
...@@ -15,9 +15,7 @@ ...@@ -15,9 +15,7 @@
data-transcript-translation-url="/transcript/translation" data-transcript-translation-url="/transcript/translation"
data-transcript-available-translations-url="/transcript/available_translations" data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y" data-sub="Z5KLxerq05Y"
data-mp4-source="xmodule/include/fixtures/test.mp4" data-sources='["xmodule/include/fixtures/test.mp4","xmodule/include/fixtures/test.webm","xmodule/include/fixtures/test.ogv"]'
data-webm-source="xmodule/include/fixtures/test.webm"
data-ogg-source="xmodule/include/fixtures/test.ogv"
data-autoplay="False" data-autoplay="False"
data-yt-test-timeout="1500" data-yt-test-timeout="1500"
data-yt-api-url="www.youtube.com/iframe_api" data-yt-api-url="www.youtube.com/iframe_api"
......
...@@ -79,53 +79,6 @@ ...@@ -79,53 +79,6 @@
expect(state.videos).toBeUndefined(); expect(state.videos).toBeUndefined();
}); });
it('parse Html5 sources', function () {
var html5Sources = {
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);
});
it('parse available video speeds', function () { it('parse available video speeds', function () {
var speeds = jasmine.stubbedHtml5Speeds; var speeds = jasmine.stubbedHtml5Speeds;
......
...@@ -70,7 +70,6 @@ function (VideoPlayer, VideoStorage, i18n) { ...@@ -70,7 +70,6 @@ function (VideoPlayer, VideoStorage, i18n) {
isFlashMode: isFlashMode, isFlashMode: isFlashMode,
isYoutubeType: isYoutubeType, isYoutubeType: isYoutubeType,
parseSpeed: parseSpeed, parseSpeed: parseSpeed,
parseVideoSources: parseVideoSources,
parseYoutubeStreams: parseYoutubeStreams, parseYoutubeStreams: parseYoutubeStreams,
saveState: saveState, saveState: saveState,
setPlayerMode: setPlayerMode, setPlayerMode: setPlayerMode,
...@@ -280,32 +279,17 @@ function (VideoPlayer, VideoStorage, i18n) { ...@@ -280,32 +279,17 @@ function (VideoPlayer, VideoStorage, i18n) {
// The function prepare HTML5 video, parse HTML5 // The function prepare HTML5 video, parse HTML5
// video sources etc. // video sources etc.
function _prepareHTML5Video(state) { function _prepareHTML5Video(state) {
state.parseVideoSources(
{
mp4: state.config.mp4Source,
webm: state.config.webmSource,
ogg: state.config.oggSource
}
);
state.speeds = ['0.75', '1.0', '1.25', '1.50']; state.speeds = ['0.75', '1.0', '1.25', '1.50'];
// If none of the supported video formats can be played and there is no
// We must have at least one non-YouTube video source available. // short-hand video links, than hide the spinner and show error message.
// Otherwise, return a negative. if (!state.config.sources.length) {
if (
state.html5Sources.webm === null &&
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');
_hideWaitPlaceholder(state); _hideWaitPlaceholder(state);
state.el
console.log( .find('.video-player div')
'[Video info]: Non-youtube video sources aren\'t available.' .addClass('hidden')
); .end()
.find('.video-player h3')
.removeClass('hidden');
return false; return false;
} }
...@@ -642,48 +626,6 @@ function (VideoPlayer, VideoStorage, i18n) { ...@@ -642,48 +626,6 @@ function (VideoPlayer, VideoStorage, i18n) {
return _.isString(this.videos['1.0']); return _.isString(this.videos['1.0']);
} }
// function parseVideoSources(, mp4Source, webmSource, oggSource)
//
// 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,
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,
webm: null,
ogg: null
};
$.each(sources, function (name, source) {
if (source && source.length) {
if (
Boolean(
v.canPlayType &&
v.canPlayType(sourceCodecs[name]).replace(/no/, '')
)
) {
_this.html5Sources[name] = source;
}
}
});
// None of the supported video formats can be played. Hide the spinner.
if (!(_.compact(_.values(this.html5Sources)))) {
_hideWaitPlaceholder(state);
console.log(
'[Video info]: This browser cannot play .mp4, .ogg, or .webm ' +
'files'
);
}
}
// function fetchMetadata() // function fetchMetadata()
// //
// When dealing with YouTube videos, we must fetch meta data that has // When dealing with YouTube videos, we must fetch meta data that has
...@@ -763,7 +705,10 @@ function (VideoPlayer, VideoStorage, i18n) { ...@@ -763,7 +705,10 @@ function (VideoPlayer, VideoStorage, i18n) {
} }
successHandler = ($.isFunction(callback)) ? callback : null; successHandler = ($.isFunction(callback)) ? callback : null;
xhr = $.ajax({ xhr = $.ajax({
url: document.location.protocol + '//' + this.config.ytTestUrl + url + '?v=2&alt=jsonc', url: [
document.location.protocol, '//', this.config.ytTestUrl, url,
'?v=2&alt=jsonc'
].join(''),
dataType: 'jsonp', dataType: 'jsonp',
timeout: this.config.ytTestTimeout, timeout: this.config.ytTestTimeout,
success: successHandler success: successHandler
......
...@@ -94,6 +94,22 @@ function () { ...@@ -94,6 +94,22 @@ function () {
return this.logs; return this.logs;
}; };
Player.prototype.showErrorMessage = function () {
this.el
.find('.video-player div')
.addClass('hidden')
.end()
.find('.video-player h3')
.removeClass('hidden')
.end()
.addClass('is-initialized')
.find('.spinner')
.attr({
'aria-hidden': 'true',
'tabindex': -1
});
};
return Player; return Player;
/* /*
...@@ -113,7 +129,7 @@ function () { ...@@ -113,7 +129,7 @@ function () {
* *
* config = { * config = {
* *
* videoSources: {}, // An object with properties being video * videoSources: [], // An array with properties being video
* // sources. The property name is the * // sources. The property name is the
* // video format of the source. Supported * // video format of the source. Supported
* // video formats are: 'mp4', 'webm', and * // video formats are: 'mp4', 'webm', and
...@@ -134,7 +150,7 @@ function () { ...@@ -134,7 +150,7 @@ function () {
*/ */
function Player(el, config) { function Player(el, config) {
var isTouch = onTouchBasedDevice() || '', var isTouch = onTouchBasedDevice() || '',
sourceStr, _this, errorMessage; sourceList, _this, errorMessage, lastSource;
this.logs = []; this.logs = [];
// Initially we assume that el is a DOM element. If jQuery selector // Initially we assume that el is a DOM element. If jQuery selector
...@@ -167,63 +183,50 @@ function () { ...@@ -167,63 +183,50 @@ function () {
// We should have at least one video source. Otherwise there is no // We should have at least one video source. Otherwise there is no
// point to continue. // point to continue.
if (!config.videoSources) { if (!config.videoSources && !config.videoSources.length) {
return; return;
} }
// From the start, all sources are empty. We will populate this
// object below.
sourceStr = {
mp4: ' ',
webm: ' ',
ogg: ' '
};
// Will be used in inner functions to point to the current object. // Will be used in inner functions to point to the current object.
_this = this; _this = this;
// Create HTML markup for individual sources of the HTML5 <video> // Create HTML markup for individual sources of the HTML5 <video>
// element. // element.
$.each(sourceStr, function (videoType, videoSource) { sourceList = $.map(config.videoSources, function (source) {
var url = _this.config.videoSources[videoType]; return [
if (url && url.length) { '<source ',
sourceStr[videoType] = 'src="', source,
'<source ' + // Following hack allows to open the same video twice
'src="' + url + // https://code.google.com/p/chromium/issues/detail?id=31014
// Following hack allows to open the same video twice // Check whether the url already has a '?' inside, and if so,
// https://code.google.com/p/chromium/issues/detail?id=31014 // use '&' instead of '?' to prevent breaking the url's integrity.
// Check whether the url already has a '?' inside, and if so, (source.indexOf('?') === -1 ? '?' : '&'),
// use '&' instead of '?' to prevent breaking the url's integrity. (new Date()).getTime(), '" />'
(url.indexOf('?') == -1 ? '?' : '&') + (new Date()).getTime() + ].join('');
'" ' + 'type="video/' + videoType + '" ' +
'/> ';
}
}); });
// We should have at least one video source. Otherwise there is no
// point to continue.
if (
sourceStr.mp4 === ' ' &&
sourceStr.webm === ' ' &&
sourceStr.ogg === ' '
) {
return;
}
// Create HTML markup for the <video> element, populating it with // Create HTML markup for the <video> element, populating it with
// sources from previous step. Because of problems with creating // sources from previous step. Because of problems with creating
// video element via jquery (http://bugs.jquery.com/ticket/9174) we // video element via jquery (http://bugs.jquery.com/ticket/9174) we
// create it using native JS. // create it using native JS.
this.video = document.createElement('video'); this.video = document.createElement('video');
errorMessage = gettext('This browser cannot play .mp4, .ogg, or .webm files.')
+ gettext('Try using a different browser, such as Google Chrome.'); errorMessage = [
this.video.innerHTML = _.values(sourceStr).join('') + errorMessage; gettext('This browser cannot play .mp4, .ogg, or .webm files.'),
gettext('Try using a different browser, such as Google Chrome.')
].join('');
this.video.innerHTML = sourceList.join('') + errorMessage;
// Get the jQuery object, and set the player state to UNSTARTED. // Get the jQuery object, and set the player state to UNSTARTED.
// The player state is used by other parts of the VideoPlayer to // The player state is used by other parts of the VideoPlayer to
// determine what the video is currently doing. // determine what the video is currently doing.
this.videoEl = $(this.video); this.videoEl = $(this.video);
lastSource = this.videoEl.find('source').last();
lastSource.on('error', this.showErrorMessage.bind(this));
if (/iP(hone|od)/i.test(isTouch[0])) { if (/iP(hone|od)/i.test(isTouch[0])) {
this.videoEl.prop('controls', true); this.videoEl.prop('controls', true);
} }
...@@ -253,6 +256,7 @@ function () { ...@@ -253,6 +256,7 @@ function () {
'durationchange', 'volumechange' 'durationchange', 'volumechange'
]; ];
this.debug = false;
$.each(events, function(index, eventName) { $.each(events, function(index, eventName) {
_this.video.addEventListener(eventName, function () { _this.video.addEventListener(eventName, function () {
_this.logs.push({ _this.logs.push({
...@@ -260,6 +264,15 @@ function () { ...@@ -260,6 +264,15 @@ function () {
'state': _this.playerState 'state': _this.playerState
}); });
if (_this.debug) {
console.log(
'event name:', eventName,
'state:', _this.playerState,
'readyState:', _this.video.readyState,
'networkState:', _this.video.networkState
);
}
el.trigger('html5:' + eventName, arguments); el.trigger('html5:' + eventName, arguments);
}); });
}); });
......
...@@ -142,7 +142,7 @@ function (HTML5Video, Resizer) { ...@@ -142,7 +142,7 @@ function (HTML5Video, Resizer) {
if (state.videoType === 'html5') { if (state.videoType === 'html5') {
state.videoPlayer.player = new HTML5Video.Player(state.el, { state.videoPlayer.player = new HTML5Video.Player(state.el, {
playerVars: state.videoPlayer.playerVars, playerVars: state.videoPlayer.playerVars,
videoSources: state.html5Sources, videoSources: state.config.sources,
events: { events: {
onReady: state.videoPlayer.onReady, onReady: state.videoPlayer.onReady,
onStateChange: state.videoPlayer.onStateChange onStateChange: state.videoPlayer.onStateChange
......
...@@ -20,7 +20,7 @@ from mock import Mock ...@@ -20,7 +20,7 @@ from mock import Mock
from . import LogicTest from . import LogicTest
from lxml import etree from lxml import etree
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.video_module import VideoDescriptor, create_youtube_string, get_ext from xmodule.video_module import VideoDescriptor, create_youtube_string
from .test_import import DummySystem from .test_import import DummySystem
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
...@@ -107,18 +107,6 @@ class VideoModuleTest(LogicTest): ...@@ -107,18 +107,6 @@ class VideoModuleTest(LogicTest):
'1.50': ''} '1.50': ''}
) )
def test_get_ext(self):
"""Test get the file's extension in a url without query string."""
filename_str = 'http://www.example.com/path/video.mp4'
output = get_ext(filename_str)
self.assertEqual(output, 'mp4')
def test_get_ext_with_query_string(self):
"""Test get the file's extension in a url with query string."""
filename_str = 'http://www.example.com/path/video.mp4?param1=1&p2=2'
output = get_ext(filename_str)
self.assertEqual(output, 'mp4')
class VideoDescriptorTest(unittest.TestCase): class VideoDescriptorTest(unittest.TestCase):
"""Test for VideoDescriptor""" """Test for VideoDescriptor"""
......
...@@ -35,14 +35,6 @@ from .video_utils import create_youtube_string ...@@ -35,14 +35,6 @@ from .video_utils import create_youtube_string
from .video_xfields import VideoFields from .video_xfields import VideoFields
from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers
from urlparse import urlparse
def get_ext(filename):
# Prevent incorrectly parsing urls like 'http://abc.com/path/video.mp4?xxxx'.
path = urlparse(filename).path
return path.rpartition('.')[-1]
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_ = lambda text: text _ = lambda text: text
...@@ -97,15 +89,15 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule): ...@@ -97,15 +89,15 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
def get_html(self): def get_html(self):
track_url = None track_url = None
download_video_link = None
transcript_download_format = self.transcript_download_format transcript_download_format = self.transcript_download_format
sources = filter(None, self.html5_sources)
sources = {get_ext(src): src for src in self.html5_sources}
if self.download_video: if self.download_video:
if self.source: if self.source:
sources['main'] = self.source download_video_link = self.source
elif self.html5_sources: elif self.html5_sources:
sources['main'] = self.html5_sources[0] download_video_link = self.html5_sources[0]
if self.download_track: if self.download_track:
if self.track: if self.track:
...@@ -149,7 +141,8 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule): ...@@ -149,7 +141,8 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule):
'handout': self.handout, 'handout': self.handout,
'id': self.location.html_id(), 'id': self.location.html_id(),
'show_captions': json.dumps(self.show_captions), 'show_captions': json.dumps(self.show_captions),
'sources': sources, 'download_video_link': download_video_link,
'sources': json.dumps(sources),
'speed': json.dumps(self.speed), 'speed': json.dumps(self.speed),
'general_speed': self.global_speed, 'general_speed': self.global_speed,
'saved_video_position': self.saved_video_position.total_seconds(), 'saved_video_position': self.saved_video_position.total_seconds(),
......
...@@ -26,12 +26,7 @@ class TestVideoYouTube(TestVideo): ...@@ -26,12 +26,7 @@ class TestVideoYouTube(TestVideo):
def test_video_constructor(self): def test_video_constructor(self):
"""Make sure that all parameters extracted correctly from xml""" """Make sure that all parameters extracted correctly from xml"""
context = self.item_descriptor.render('student_view').content context = self.item_descriptor.render('student_view').content
sources = json.dumps([u'example.mp4', u'example.webm'])
sources = {
'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
}
expected_context = { expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
...@@ -42,6 +37,7 @@ class TestVideoYouTube(TestVideo): ...@@ -42,6 +37,7 @@ class TestVideoYouTube(TestVideo):
'id': self.item_descriptor.location.html_id(), 'id': self.item_descriptor.location.html_id(),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
'download_video_link': u'example.mp4',
'sources': sources, 'sources': sources,
'speed': 'null', 'speed': 'null',
'general_speed': 1.0, 'general_speed': 1.0,
...@@ -56,7 +52,7 @@ class TestVideoYouTube(TestVideo): ...@@ -56,7 +52,7 @@ class TestVideoYouTube(TestVideo):
'transcript_download_format': 'srt', 'transcript_download_format': 'srt',
'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}], 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
'transcript_language': u'en', 'transcript_language': u'en',
'transcript_languages': json.dumps(OrderedDict({"en": "English", "uk": u"Українська"})), 'transcript_languages': json.dumps(OrderedDict({"en": "English", "uk": u"Українська"})),
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'translation' self.item_descriptor, 'transcript', 'translation'
).rstrip('/?'), ).rstrip('/?'),
...@@ -93,13 +89,8 @@ class TestVideoNonYouTube(TestVideo): ...@@ -93,13 +89,8 @@ class TestVideoNonYouTube(TestVideo):
"""Make sure that if the 'youtube' attribute is omitted in XML, then """Make sure that if the 'youtube' attribute is omitted in XML, then
the template generates an empty string for the YouTube streams. the template generates an empty string for the YouTube streams.
""" """
sources = {
'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
}
context = self.item_descriptor.render('student_view').content context = self.item_descriptor.render('student_view').content
sources = json.dumps([u'example.mp4', u'example.webm'])
expected_context = { expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
...@@ -107,6 +98,7 @@ class TestVideoNonYouTube(TestVideo): ...@@ -107,6 +98,7 @@ class TestVideoNonYouTube(TestVideo):
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
'display_name': u'A Name', 'display_name': u'A Name',
'download_video_link': u'example.mp4',
'end': 3610.0, 'end': 3610.0,
'id': self.item_descriptor.location.html_id(), 'id': self.item_descriptor.location.html_id(),
'sources': sources, 'sources': sources,
...@@ -148,7 +140,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -148,7 +140,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
METADATA = {} METADATA = {}
def setUp(self): def setUp(self):
self.setup_course(); self.setup_course()
def test_get_html_track(self): def test_get_html_track(self):
SOURCE_XML = """ SOURCE_XML = """
...@@ -201,19 +193,17 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -201,19 +193,17 @@ class TestGetHtmlMethod(BaseTestXmodule):
'transcripts': '<transcript language="uk" src="ukrainian.srt" />', 'transcripts': '<transcript language="uk" src="ukrainian.srt" />',
}, },
] ]
sources = json.dumps([u'example.mp4', u'example.webm'])
expected_context = { expected_context = {
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
'display_name': u'A Name', 'display_name': u'A Name',
'download_video_link': u'example.mp4',
'end': 3610.0, 'end': 3610.0,
'id': None, 'id': None,
'sources': { 'sources': sources,
'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm'
},
'start': 3603.0, 'start': 3603.0,
'saved_video_position': 0.0, 'saved_video_position': 0.0,
'sub': u'a_sub_file.srt.sjson', 'sub': u'a_sub_file.srt.sjson',
...@@ -284,9 +274,8 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -284,9 +274,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
<source src="example.webm"/> <source src="example.webm"/>
""", """,
'result': { 'result': {
'main': u'example_source.mp4', 'download_video_link': u'example_source.mp4',
u'mp4': u'example.mp4', 'sources': json.dumps([u'example.mp4', u'example.webm']),
u'webm': u'example.webm',
}, },
}, },
{ {
...@@ -297,9 +286,8 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -297,9 +286,8 @@ class TestGetHtmlMethod(BaseTestXmodule):
<source src="example.webm"/> <source src="example.webm"/>
""", """,
'result': { 'result': {
'main': u'example.mp4', 'download_video_link': u'example.mp4',
u'mp4': u'example.mp4', 'sources': json.dumps([u'example.mp4', u'example.webm']),
u'webm': u'example.webm',
}, },
}, },
{ {
...@@ -318,20 +306,20 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -318,20 +306,20 @@ class TestGetHtmlMethod(BaseTestXmodule):
<source src="example.webm"/> <source src="example.webm"/>
""", """,
'result': { 'result': {
u'mp4': u'example.mp4', 'sources': json.dumps([u'example.mp4', u'example.webm']),
u'webm': u'example.webm',
}, },
}, },
] ]
expected_context = { initial_context = {
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'show_captions': 'true', 'show_captions': 'true',
'handout': None, 'handout': None,
'display_name': u'A Name', 'display_name': u'A Name',
'download_video_link': None,
'end': 3610.0, 'end': 3610.0,
'id': None, 'id': None,
'sources': None, 'sources': '[]',
'speed': 'null', 'speed': 'null',
'general_speed': 1.0, 'general_speed': 1.0,
'start': 3603.0, 'start': 3603.0,
...@@ -358,6 +346,7 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -358,6 +346,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.initialize_module(data=DATA) self.initialize_module(data=DATA)
context = self.item_descriptor.render('student_view').content context = self.item_descriptor.render('student_view').content
expected_context = dict(initial_context)
expected_context.update({ expected_context.update({
'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url( 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'translation' self.item_descriptor, 'transcript', 'translation'
...@@ -366,9 +355,9 @@ class TestGetHtmlMethod(BaseTestXmodule): ...@@ -366,9 +355,9 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.item_descriptor, 'transcript', 'available_translations' self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'), ).rstrip('/?'),
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state', 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'sources': data['result'],
'id': self.item_descriptor.location.html_id(), 'id': self.item_descriptor.location.html_id(),
}) })
expected_context.update(data['result'])
self.assertEqual( self.assertEqual(
context, context,
...@@ -385,7 +374,7 @@ class TestVideoDescriptorInitialization(BaseTestXmodule): ...@@ -385,7 +374,7 @@ class TestVideoDescriptorInitialization(BaseTestXmodule):
METADATA = {} METADATA = {}
def setUp(self): def setUp(self):
self.setup_course(); self.setup_course()
def test_source_not_in_html5sources(self): def test_source_not_in_html5sources(self):
metadata = { metadata = {
......
...@@ -13,10 +13,7 @@ ...@@ -13,10 +13,7 @@
${'data-sub="{}"'.format(sub) if sub else ''} ${'data-sub="{}"'.format(sub) if sub else ''}
${'data-autoplay="{}"'.format(autoplay) if autoplay else ''} ${'data-autoplay="{}"'.format(autoplay) if autoplay else ''}
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''} data-sources='${sources}'
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
data-save-state-url="${ajax_url}" data-save-state-url="${ajax_url}"
data-caption-data-dir="${data_dir}" data-caption-data-dir="${data_dir}"
data-show-captions="${show_captions}" data-show-captions="${show_captions}"
...@@ -106,9 +103,9 @@ ...@@ -106,9 +103,9 @@
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
<ul class="wrapper-downloads"> <ul class="wrapper-downloads">
% if sources.get('main'): % if download_video_link:
<li class="video-sources video-download-button"> <li class="video-sources video-download-button">
${('<a href="%s">' + _('Download video') + '</a>') % sources.get('main')} ${('<a href="%s">' + _('Download video') + '</a>') % download_video_link}
</li> </li>
% endif % endif
% if track: % if track:
......
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