\n');
+ expect($('.volume')).toExist();
});
it('create the slider', function () {
@@ -292,7 +292,7 @@ describe('VideoVolumeControl', function () {
shiftKey: true
});
});
- })
+ });
describe('keyDownButtonHandler', function () {
beforeEach(function () {
@@ -308,6 +308,6 @@ describe('VideoVolumeControl', function () {
}));
expect(volumeControl.getMuteStatus()).toEqual(isMuted);
});
- })
+ });
});
}).call(this);
diff --git a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js
index cbf7df47ee..f0c1debcd0 100644
--- a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js
+++ b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js
@@ -177,9 +177,8 @@ function () {
}
};
- var cleanDelta = function () {
- delta['height'] = 0;
- delta['width'] = 0;
+ var resetDelta = function () {
+ delta['height'] = delta['width'] = 0;
return module;
};
@@ -200,12 +199,23 @@ function () {
return module;
};
+ var destroy = function () {
+ var data = getData();
+ data.element.css({
+ 'height': '', 'width': '', 'top': '', 'left': ''
+ });
+ removeCallbacks();
+ resetDelta();
+ mode = null;
+ };
+
initialize.apply(module, arguments);
return $.extend(true, module, {
align: align,
alignByWidthOnly: alignByWidthOnly,
alignByHeightOnly: alignByHeightOnly,
+ destroy: destroy,
setParams: initialize,
setMode: setMode,
setElement: setElement,
@@ -218,7 +228,7 @@ function () {
delta: {
add: addDelta,
substract: substractDelta,
- reset: cleanDelta
+ reset: resetDelta
}
});
};
diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
index 05654966f5..ad7edc8b56 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -14,8 +14,8 @@
define(
'video/01_initialize.js',
-['video/03_video_player.js', 'video/00_video_storage.js', 'video/00_i18n.js'],
-function (VideoPlayer, VideoStorage, i18n) {
+['video/03_video_player.js', 'video/00_i18n.js'],
+function (VideoPlayer, i18n) {
/**
* @function
*
@@ -71,7 +71,6 @@ function (VideoPlayer, VideoStorage, i18n) {
isYoutubeType: isYoutubeType,
parseSpeed: parseSpeed,
parseYoutubeStreams: parseYoutubeStreams,
- saveState: saveState,
setPlayerMode: setPlayerMode,
setSpeed: setSpeed,
speedToString: speedToString,
@@ -145,9 +144,7 @@ function (VideoPlayer, VideoStorage, i18n) {
_youtubeApiDeferred.resolve();
}
- window.YT.ready(function () {
- onYTApiReady();
- });
+ window.YT.ready(onYTApiReady);
} else {
// There is only one global variable window.onYouTubeIframeAPIReady which
// is supposed to be a function that will be called by the YouTube API
@@ -191,9 +188,7 @@ function (VideoPlayer, VideoStorage, i18n) {
// Attach a callback to our Deferred object to be called once the
// YouTube API loads.
window.onYouTubeIframeAPIReady.done(function () {
- window.YT.ready(function () {
- onYTApiReady();
- });
+ window.YT.ready(onYTApiReady);
});
}
} else {
@@ -212,20 +207,15 @@ function (VideoPlayer, VideoStorage, i18n) {
// callback, which will set `state.youtubeApiAvailable` to `true`.
// If something goes wrong at this stage, `state.youtubeApiAvailable` is
// `false`.
- _reportToServer(state, state.youtubeApiAvailable);
+ if (!state.youtubeIsAvailable) {
+ console.log('[Video info]: YouTube API is not available.');
+ }
+ state.el.trigger('youtube_availability', [state.youtubeIsAvailable]);
}, state.config.ytTestTimeout);
$.getScript(document.location.protocol + '//' + state.config.ytApiUrl);
}
- function _reportToServer(state, youtubeIsAvailable) {
- if (!youtubeIsAvailable) {
- console.log('[Video info]: YouTube API is not available.');
- }
-
- state.saveState(true, { youtube_is_available: youtubeIsAvailable });
- }
-
// function _configureCaptions(state)
// Configure displaying of captions.
//
@@ -296,8 +286,7 @@ function (VideoPlayer, VideoStorage, i18n) {
state.videoType = 'html5';
- if (!state.config.sub || !state.config.sub.length) {
- state.config.sub = '';
+ if (!_.keys(state.config.transcriptLanguages).length) {
state.config.showCaptions = false;
}
state.setSpeed(state.speed);
@@ -328,8 +317,9 @@ function (VideoPlayer, VideoStorage, i18n) {
function _initializeModules(state, i18n) {
var dfd = $.Deferred(),
modulesList = $.map(state.modules, function(module) {
- if ($.isFunction(module)) {
- return module(state, i18n);
+ var options = state.options[module.moduleName] || {};
+ if (_.isFunction(module)) {
+ return module(state, i18n, options);
} else if ($.isPlainObject(module)) {
return module;
}
@@ -388,7 +378,6 @@ function (VideoPlayer, VideoStorage, i18n) {
},
'startTime': function (value) {
value = parseInt(value, 10);
-
if (!isFinite(value) || value < 0) {
return 0;
}
@@ -407,6 +396,13 @@ function (VideoPlayer, VideoStorage, i18n) {
},
config = {};
+ data = _.extend({
+ startTime: 0,
+ endTime: null,
+ sub: '',
+ streams: ''
+ }, data);
+
$.each(data, function(option, value) {
// Extract option that is in `extractKeys`.
if ($.inArray(option, extractKeys) !== -1) {
@@ -420,7 +416,7 @@ function (VideoPlayer, VideoStorage, i18n) {
// Pre-process data.
if (conversions[option]) {
- if ($.isFunction(conversions[option])) {
+ if (_.isFunction(conversions[option])) {
value = conversions[option].call(this, value);
} else {
throw new TypeError(option + ' is not a function.');
@@ -463,12 +459,11 @@ function (VideoPlayer, VideoStorage, i18n) {
function initialize(element) {
var self = this,
- el = $(element).find('.video'),
+ el = this.el,
+ id = this.id,
container = el.find('.video-wrapper'),
- id = el.attr('id').replace(/video_/, ''),
__dfd__ = $.Deferred(),
- isTouch = onTouchBasedDevice() || '',
- storage = VideoStorage('VideoState', id);
+ isTouch = onTouchBasedDevice() || '';
if (isTouch) {
el.addClass('is-touch');
@@ -476,23 +471,18 @@ function (VideoPlayer, VideoStorage, i18n) {
$.extend(this, {
__dfd__: __dfd__,
- el: el,
container: container,
- id: id,
isFullScreen: false,
- isTouch: isTouch,
- storage: storage
+ isTouch: isTouch
});
- console.log(
- '[Video info]: Initializing video with id "' + id + '".'
- );
+ console.log('[Video info]: Initializing video with id "%s".', id);
// We store all settings passed to us by the server in one place. These
// are "read only", so don't modify them. All variable content lives in
// 'state' object.
// jQuery .data() return object with keys in lower camelCase format.
- this.config = $.extend({}, _getConfiguration(el.data(), storage), {
+ this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), {
element: element,
fadeOutTimeout: 1400,
captionsFreezeTime: 10000,
@@ -602,26 +592,18 @@ function (VideoPlayer, VideoStorage, i18n) {
// true: Parsing of YouTube video IDs went OK, and we can proceed
// onwards to play YouTube videos.
function parseYoutubeStreams(youtubeStreams) {
- var _this;
-
- if (
- typeof youtubeStreams === 'undefined' ||
- youtubeStreams.length === 0
- ) {
+ if (_.isUndefined(youtubeStreams) || !youtubeStreams.length) {
return false;
}
- _this = this;
this.videos = {};
- $.each(youtubeStreams.split(/,/), function (index, video) {
+ _.each(youtubeStreams.split(/,/), function (video) {
var speed;
-
video = video.split(/:/);
- speed = _this.speedToString(video[0]);
-
- _this.videos[speed] = video[1];
- });
+ speed = this.speedToString(video[0]);
+ this.videos[speed] = video[1];
+ }, this);
return _.isString(this.videos['1.0']);
}
@@ -633,23 +615,21 @@ function (VideoPlayer, VideoStorage, i18n) {
// example the length of the video can be determined from the meta
// data.
function fetchMetadata() {
- var _this = this,
+ var self = this,
metadataXHRs = [];
this.metadata = {};
- $.each(this.videos, function (speed, url) {
- var xhr = _this.getVideoMetadata(url, function (data) {
+ metadataXHRs = _.map(this.videos, function (url, speed) {
+ return self.getVideoMetadata(url, function (data) {
if (data.data) {
- _this.metadata[data.data.id] = data.data;
+ self.metadata[data.data.id] = data.data;
}
});
-
- metadataXHRs.push(xhr);
});
$.when.apply(this, metadataXHRs).done(function () {
- _this.el.trigger('metadata_received');
+ self.el.trigger('metadata_received');
// Not only do we trigger the "metadata_received" event, we also
// set a flag to notify that metadata has been received. This
@@ -657,7 +637,7 @@ function (VideoPlayer, VideoStorage, i18n) {
// to know that metadata has been received. This is important in
// cases when some code will subscribe to the "metadata_received"
// event after it has been triggered.
- _this.youtubeMetadataReceived = true;
+ self.youtubeMetadataReceived = true;
});
}
@@ -666,23 +646,21 @@ function (VideoPlayer, VideoStorage, i18n) {
//
// Create a separate array of available speeds.
function parseSpeed() {
- this.speeds = ($.map(this.videos, function (url, speed) {
- return speed;
- })).sort();
+ this.speeds = _.keys(this.videos).sort();
}
- function setSpeed(newSpeed, updateStorage) {
+ function setSpeed(newSpeed) {
// Possible speeds for each player type.
// HTML5 = [0.75, 1, 1.25, 1.5]
// Youtube Flash = [0.75, 1, 1.25, 1.5]
// Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2]
var map = {
- '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
- '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
- '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
- '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
- '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash
- };
+ '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
+ '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
+ '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
+ '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
+ '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash
+ };
if (_.contains(this.speeds, newSpeed)) {
this.speed = newSpeed;
@@ -690,57 +668,21 @@ function (VideoPlayer, VideoStorage, i18n) {
newSpeed = map[newSpeed];
this.speed = _.contains(this.speeds, newSpeed) ? newSpeed : '1.0';
}
-
- if (updateStorage) {
- this.storage.setItem('speed', this.speed, true);
- this.storage.setItem('general_speed', this.speed);
- }
}
function getVideoMetadata(url, callback) {
- var successHandler, xhr;
-
- if (typeof url !== 'string') {
+ if (!(_.isString(url))) {
url = this.videos['1.0'] || '';
}
- successHandler = ($.isFunction(callback)) ? callback : null;
- xhr = $.ajax({
+
+ return $.ajax({
url: [
document.location.protocol, '//', this.config.ytTestUrl, url,
'?v=2&alt=jsonc'
].join(''),
dataType: 'jsonp',
timeout: this.config.ytTestTimeout,
- success: successHandler
- });
-
- return xhr;
- }
-
- function saveState(async, data) {
-
- if (!($.isPlainObject(data))) {
- data = {
- saved_video_position: this.videoPlayer.currentTime
- };
- }
-
- if (data.speed) {
- this.storage.setItem('speed', data.speed, true);
- }
-
- if (data.hasOwnProperty('saved_video_position')) {
- this.storage.setItem('savedVideoPosition', data.saved_video_position, true);
-
- data.saved_video_position = Time.formatFull(data.saved_video_position);
- }
-
- $.ajax({
- url: this.config.saveStateUrl,
- type: 'POST',
- async: async ? true : false,
- dataType: 'json',
- data: data,
+ success: _.isFunction(callback) ? callback : null
});
}
diff --git a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js
index d055b85d62..dc3fd7974b 100644
--- a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js
+++ b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js
@@ -110,6 +110,54 @@ function () {
});
};
+ Player.prototype.onError = function (event) {
+ if ($.isFunction(this.config.events.onError)) {
+ this.config.events.onError();
+ }
+ };
+
+ Player.prototype.destroy = function () {
+ this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false);
+ this.video.removeEventListener('play', this.onPlay, false);
+ this.video.removeEventListener('playing', this.onPlaying, false);
+ this.video.removeEventListener('pause', this.onPause, false);
+ this.video.removeEventListener('ended', this.onEnded, false);
+ this.el
+ .find('.video-player div').removeClass('hidden')
+ .end()
+ .find('.video-player h3').addClass('hidden')
+ .end().removeClass('is-initialized')
+ .find('.spinner').attr({'aria-hidden': 'false'});
+ this.videoEl.remove();
+ };
+
+ Player.prototype.onLoadedMetadata = function () {
+ this.playerState = HTML5Video.PlayerState.PAUSED;
+ if ($.isFunction(this.config.events.onReady)) {
+ this.config.events.onReady(null);
+ }
+ };
+
+ Player.prototype.onPlay = function () {
+ this.playerState = HTML5Video.PlayerState.BUFFERING;
+ this.callStateChangeCallback();
+ };
+
+ Player.prototype.onPlaying = function () {
+ this.playerState = HTML5Video.PlayerState.PLAYING;
+ this.callStateChangeCallback();
+ };
+
+ Player.prototype.onPause = function () {
+ this.playerState = HTML5Video.PlayerState.PAUSED;
+ this.callStateChangeCallback();
+ };
+
+ Player.prototype.onEnded = function () {
+ this.playerState = HTML5Video.PlayerState.ENDED;
+ this.callStateChangeCallback();
+ };
+
return Player;
/*
@@ -152,6 +200,7 @@ function () {
var isTouch = onTouchBasedDevice() || '',
sourceList, _this, errorMessage, lastSource;
+ _.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded');
this.logs = [];
// Initially we assume that el is a DOM element. If jQuery selector
// fails to select something, we assume that el is an ID of a DOM
@@ -226,6 +275,8 @@ function () {
lastSource = this.videoEl.find('source').last();
lastSource.on('error', this.showErrorMessage.bind(this));
+ lastSource.on('error', this.onError.bind(this));
+ this.videoEl.on('error', this.onError.bind(this));
if (/iP(hone|od)/i.test(isTouch[0])) {
this.videoEl.prop('controls', true);
@@ -280,35 +331,11 @@ function () {
// When the
- """.format(os.path.split(non_en_file.name)[1])
+ """.format(os.path.split(srt_file.name)[1])
MODEL_DATA = {
'data': DATA
@@ -311,37 +400,41 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
super(TestTranscriptTranslationGetDispatch, self).setUp()
self.item_descriptor.render(STUDENT_VIEW)
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
+ self.item.video_bumper = {"transcripts": {"en": ""}}
- def test_translation_fails(self):
+ @ddt.data(
# No language
- request = Request.blank('/translation')
- response = self.item.transcript(request=request, dispatch='translation')
- self.assertEqual(response.status, '400 Bad Request')
-
+ ('/translation', 'translation', '400 Bad Request'),
# No videoId - HTML5 video with language that is not in available languages
- request = Request.blank('/translation/ru')
- response = self.item.transcript(request=request, dispatch='translation/ru')
- self.assertEqual(response.status, '404 Not Found')
-
+ ('/translation/ru', 'translation/ru', '404 Not Found'),
# Language is not in available languages
- request = Request.blank('/translation/ru?videoId=12345')
- response = self.item.transcript(request=request, dispatch='translation/ru')
- self.assertEqual(response.status, '404 Not Found')
-
+ ('/translation/ru?videoId=12345', 'translation/ru', '404 Not Found'),
# Youtube_id is invalid or does not exist
- request = Request.blank('/translation/uk?videoId=9855256955511225')
- response = self.item.transcript(request=request, dispatch='translation/uk')
- self.assertEqual(response.status, '404 Not Found')
+ ('/translation/uk?videoId=9855256955511225', 'translation/uk', '404 Not Found'),
+ ('/translation?is_bumper=1', 'translation', '400 Bad Request'),
+ ('/translation/ru?is_bumper=1', 'translation/ru', '404 Not Found'),
+ ('/translation/ru?videoId=12345&is_bumper=1', 'translation/ru', '404 Not Found'),
+ ('/translation/uk?videoId=9855256955511225&is_bumper=1', 'translation/uk', '404 Not Found'),
+ )
+ @ddt.unpack
+ def test_translation_fails(self, url, dispatch, status_code):
+ request = Request.blank(url)
+ response = self.item.transcript(request=request, dispatch=dispatch)
+ self.assertEqual(response.status, status_code)
- def test_translaton_en_youtube_success(self):
+ @ddt.data(
+ ('translation/en?videoId={}', 'translation/en', attach_sub),
+ ('translation/en?videoId={}&is_bumper=1', 'translation/en', attach_bumper_transcript))
+ @ddt.unpack
+ def test_translaton_en_youtube_success(self, url, dispatch, attach):
subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
good_sjson = _create_file(json.dumps(subs))
_upload_sjson_file(good_sjson, self.item_descriptor.location)
subs_id = _get_subs_id(good_sjson.name)
- self.item.sub = subs_id
- request = Request.blank('/translation/en?videoId={}'.format(subs_id))
- response = self.item.transcript(request=request, dispatch='translation/en')
+ attach(self.item, subs_id)
+ request = Request.blank(url.format(subs_id))
+ response = self.item.transcript(request=request, dispatch=dispatch)
self.assertDictEqual(json.loads(response.body), subs)
def test_translation_non_en_youtube_success(self):
@@ -352,9 +445,9 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
]
}
- self.non_en_file.seek(0)
- _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
- subs_id = _get_subs_id(self.non_en_file.name)
+ self.srt_file.seek(0)
+ _upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1])
+ subs_id = _get_subs_id(self.srt_file.name)
# youtube 1_0 request, will generate for all speeds for existing ids
self.item.youtube_id_1_0 = subs_id
@@ -387,16 +480,19 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
}
self.assertDictEqual(json.loads(response.body), calculated_1_5)
- def test_translaton_en_html5_success(self):
- subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
- good_sjson = _create_file(json.dumps(subs))
+ @ddt.data(
+ ('translation/en', 'translation/en', attach_sub),
+ ('translation/en?is_bumper=1', 'translation/en', attach_bumper_transcript))
+ @ddt.unpack
+ def test_translaton_en_html5_success(self, url, dispatch, attach):
+ good_sjson = _create_file(json.dumps(TRANSCRIPT))
_upload_sjson_file(good_sjson, self.item_descriptor.location)
subs_id = _get_subs_id(good_sjson.name)
- self.item.sub = subs_id
- request = Request.blank('/translation/en')
- response = self.item.transcript(request=request, dispatch='translation/en')
- self.assertDictEqual(json.loads(response.body), subs)
+ attach(self.item, subs_id)
+ request = Request.blank(url)
+ response = self.item.transcript(request=request, dispatch=dispatch)
+ self.assertDictEqual(json.loads(response.body), TRANSCRIPT)
def test_translaton_non_en_html5_success(self):
subs = {
@@ -406,8 +502,8 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.'
]
}
- self.non_en_file.seek(0)
- _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1])
+ self.srt_file.seek(0)
+ _upload_file(self.srt_file, self.item_descriptor.location, os.path.split(self.srt_file.name)[1])
# manually clean youtube_id_1_0, as it has default value
self.item.youtube_id_1_0 = ""
@@ -453,7 +549,22 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
response = self.item.transcript(request=request, dispatch='translation/uk')
self.assertEqual(response.status, '404 Not Found')
- def test_translation_static_transcript(self):
+ @ddt.data(
+ # Test youtube style en
+ ('/translation/en?videoId=12345', 'translation/en', '307 Temporary Redirect', '12345'),
+ # Test html5 style en
+ ('/translation/en', 'translation/en', '307 Temporary Redirect', 'OEoXaMPEzfM', attach_sub),
+ # Test different language to ensure we are just ignoring it since we can't
+ # translate with static fallback
+ ('/translation/uk', 'translation/uk', '404 Not Found'),
+ (
+ '/translation/en?is_bumper=1', 'translation/en', '307 Temporary Redirect', 'OEoXaMPEzfM',
+ attach_bumper_transcript
+ ),
+ ('/translation/uk?is_bumper=1', 'translation/uk', '404 Not Found'),
+ )
+ @ddt.unpack
+ def test_translation_static_transcript(self, url, dispatch, status_code, sub=None, attach=None):
"""
Set course static_asset_path and ensure we get redirected to that path
if it isn't found in the contentstore
@@ -464,30 +575,16 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
store.update_item(self.course, self.user.id)
- # Test youtube style en
- request = Request.blank('/translation/en?videoId=12345')
- response = self.item.transcript(request=request, dispatch='translation/en')
- self.assertEqual(response.status, '307 Temporary Redirect')
- self.assertIn(
- ('Location', '/static/dummy/static/subs_12345.srt.sjson'),
- response.headerlist
- )
-
- # Test HTML5 video style
- self.item.sub = 'OEoXaMPEzfM'
- request = Request.blank('/translation/en')
- response = self.item.transcript(request=request, dispatch='translation/en')
- self.assertEqual(response.status, '307 Temporary Redirect')
- self.assertIn(
- ('Location', '/static/dummy/static/subs_OEoXaMPEzfM.srt.sjson'),
- response.headerlist
- )
-
- # Test different language to ensure we are just ignoring it since we can't
- # translate with static fallback
- request = Request.blank('/translation/uk')
- response = self.item.transcript(request=request, dispatch='translation/uk')
- self.assertEqual(response.status, '404 Not Found')
+ if attach:
+ attach(self.item, sub)
+ request = Request.blank(url)
+ response = self.item.transcript(request=request, dispatch=dispatch)
+ self.assertEqual(response.status, status_code)
+ if sub:
+ self.assertIn(
+ ('Location', '/static/dummy/static/subs_{}.srt.sjson'.format(sub)),
+ response.headerlist
+ )
@attr('shard_1')
@@ -497,7 +594,7 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo):
Tests for `translation` dispatch GET HTTP method.
"""
- non_en_file = _create_srt_file()
+ srt_file = _create_srt_file()
DATA = """
- """.format(os.path.split(non_en_file.name)[1], u"塞.srt".encode('utf8'))
+ """.format(os.path.split(srt_file.name)[1], u"塞.srt".encode('utf8'))
MODEL_DATA = {'data': DATA}
@@ -523,12 +620,12 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo):
self.assertEqual(response.status, '400 Bad Request')
# Correct case:
- filename = os.path.split(self.non_en_file.name)[1]
- _upload_file(self.non_en_file, self.item_descriptor.location, filename)
- self.non_en_file.seek(0)
+ filename = os.path.split(self.srt_file.name)[1]
+ _upload_file(self.srt_file, self.item_descriptor.location, filename)
+ self.srt_file.seek(0)
request = Request.blank(u'translation/uk?filename={}'.format(filename))
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/uk')
- self.assertEqual(response.body, self.non_en_file.read())
+ self.assertEqual(response.body, self.srt_file.read())
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
self.assertEqual(
response.headers['Content-Disposition'],
@@ -537,12 +634,12 @@ class TestStudioTranscriptTranslationGetDispatch(TestVideo):
self.assertEqual(response.headers['Content-Language'], 'uk')
# Non ascii file name download:
- self.non_en_file.seek(0)
- _upload_file(self.non_en_file, self.item_descriptor.location, u'塞.srt')
- self.non_en_file.seek(0)
+ self.srt_file.seek(0)
+ _upload_file(self.srt_file, self.item_descriptor.location, u'塞.srt')
+ self.srt_file.seek(0)
request = Request.blank('translation/zh?filename={}'.format(u'塞.srt'.encode('utf8')))
response = self.item_descriptor.studio_transcript(request=request, dispatch='translation/zh')
- self.assertEqual(response.body, self.non_en_file.read())
+ self.assertEqual(response.body, self.srt_file.read())
self.assertEqual(response.headers['Content-Type'], 'application/x-subrip; charset=utf-8')
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename="塞.srt"')
self.assertEqual(response.headers['Content-Language'], 'zh')
@@ -614,7 +711,7 @@ class TestGetTranscript(TestVideo):
"""
Make sure that `get_transcript` method works correctly
"""
- non_en_file = _create_srt_file()
+ srt_file = _create_srt_file()
DATA = """
- """.format(os.path.split(non_en_file.name)[1], u"塞.srt".encode('utf8'))
+ """.format(os.path.split(srt_file.name)[1], u"塞.srt".encode('utf8'))
MODEL_DATA = {
'data': DATA
@@ -660,7 +757,8 @@ class TestGetTranscript(TestVideo):
_upload_sjson_file(good_sjson, self.item.location)
self.item.sub = _get_subs_id(good_sjson.name)
- text, filename, mime_type = self.item.get_transcript()
+ transcripts = self.item.get_transcripts_info()
+ text, filename, mime_type = self.item.get_transcript(transcripts)
expected_text = textwrap.dedent("""\
0
@@ -697,7 +795,8 @@ class TestGetTranscript(TestVideo):
_upload_sjson_file(good_sjson, self.item.location)
self.item.sub = _get_subs_id(good_sjson.name)
- text, filename, mime_type = self.item.get_transcript("txt")
+ transcripts = self.item.get_transcripts_info()
+ text, filename, mime_type = self.item.get_transcript(transcripts, transcript_format="txt")
expected_text = textwrap.dedent("""\
Hi, welcome to Edx.
Let's start with what is on your screen right now.""")
@@ -708,14 +807,15 @@ class TestGetTranscript(TestVideo):
def test_en_with_empty_sub(self):
+ transcripts = {"transcripts": {}, "sub": ""}
# no self.sub, self.youttube_1_0 exist, but no file in assets
with self.assertRaises(NotFoundError):
- self.item.get_transcript()
+ self.item.get_transcript(transcripts)
- # no self.sub and no self.youtube_1_0
+ # no self.sub and no self.youtube_1_0, no non-en transcritps
self.item.youtube_id_1_0 = None
with self.assertRaises(ValueError):
- self.item.get_transcript()
+ self.item.get_transcript(transcripts)
# no self.sub but youtube_1_0 exists with file in assets
good_sjson = _create_file(content=textwrap.dedent("""\
@@ -737,7 +837,7 @@ class TestGetTranscript(TestVideo):
_upload_sjson_file(good_sjson, self.item.location)
self.item.youtube_id_1_0 = _get_subs_id(good_sjson.name)
- text, filename, mime_type = self.item.get_transcript()
+ text, filename, mime_type = self.item.get_transcript(transcripts)
expected_text = textwrap.dedent("""\
0
00:00:00,270 --> 00:00:02,720
@@ -755,10 +855,11 @@ class TestGetTranscript(TestVideo):
def test_non_en_with_non_ascii_filename(self):
self.item.transcript_language = 'zh'
- self.non_en_file.seek(0)
- _upload_file(self.non_en_file, self.item_descriptor.location, u"塞.srt")
+ self.srt_file.seek(0)
+ _upload_file(self.srt_file, self.item_descriptor.location, u"塞.srt")
- text, filename, mime_type = self.item.get_transcript()
+ transcripts = self.item.get_transcripts_info()
+ text, filename, mime_type = self.item.get_transcript(transcripts)
expected_text = textwrap.dedent("""
0
00:00:00,12 --> 00:00:00,100
@@ -774,8 +875,9 @@ class TestGetTranscript(TestVideo):
_upload_sjson_file(good_sjson, self.item.location)
self.item.sub = _get_subs_id(good_sjson.name)
+ transcripts = self.item.get_transcripts_info()
with self.assertRaises(ValueError):
- self.item.get_transcript()
+ self.item.get_transcript(transcripts)
def test_key_error(self):
good_sjson = _create_file(content="""
@@ -794,5 +896,6 @@ class TestGetTranscript(TestVideo):
_upload_sjson_file(good_sjson, self.item.location)
self.item.sub = _get_subs_id(good_sjson.name)
+ transcripts = self.item.get_transcripts_info()
with self.assertRaises(KeyError):
- self.item.get_transcript()
+ self.item.get_transcript(transcripts)
diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py
index c9b02696f3..36c1eacb2e 100644
--- a/lms/djangoapps/courseware/tests/test_video_mongo.py
+++ b/lms/djangoapps/courseware/tests/test_video_mongo.py
@@ -9,8 +9,9 @@ from nose.plugins.attrib import attr
from django.conf import settings
from django.test import TestCase
+from django.test.utils import override_settings
-from xmodule.video_module import create_youtube_string, VideoDescriptor
+from xmodule.video_module import VideoDescriptor, bumper_utils, video_utils
from xmodule.x_module import STUDENT_VIEW
from xmodule.tests.test_video import VideoDescriptorTestBase
from xmodule.tests.test_import import DummySystem
@@ -31,43 +32,51 @@ class TestVideoYouTube(TestVideo):
def test_video_constructor(self):
"""Make sure that all parameters extracted correctly from xml"""
context = self.item_descriptor.render(STUDENT_VIEW).content
- sources = json.dumps([u'example.mp4', u'example.webm'])
+ sources = [u'example.mp4', u'example.webm']
expected_context = {
- 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
- 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False),
'branding_info': None,
'license': None,
+ 'bumper_metadata': 'null',
'cdn_eval': False,
'cdn_exp_group': None,
- 'data_dir': getattr(self, 'data_dir', None),
'display_name': u'A Name',
- 'end': 3610.0,
- 'id': self.item_descriptor.location.html_id(),
- 'show_captions': 'true',
- 'handout': None,
'download_video_link': u'example.mp4',
- 'sources': sources,
- 'speed': 'null',
- 'general_speed': 1.0,
- 'start': 3603.0,
- 'saved_video_position': 0.0,
- 'sub': u'a_sub_file.srt.sjson',
+ 'handout': None,
+ 'id': self.item_descriptor.location.html_id(),
+ 'metadata': json.dumps(OrderedDict({
+ "saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state",
+ "autoplay": False,
+ "streams": "0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg",
+ "sub": "a_sub_file.srt.sjson",
+ "sources": sources,
+ "captionDataDir": None,
+ "showCaptions": "true",
+ "generalSpeed": 1.0,
+ "speed": None,
+ "savedVideoPosition": 0.0,
+ "start": 3603.0,
+ "end": 3610.0,
+ "transcriptLanguage": "en",
+ "transcriptLanguages": OrderedDict({"en": "English", "uk": u"Українська"}),
+ "ytTestTimeout": 1500,
+ "ytApiUrl": "www.youtube.com/iframe_api",
+ "ytTestUrl": "gdata.youtube.com/feeds/api/videos/",
+ "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
+ ).rstrip('/?'),
+ "transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'available_translations'
+ ).rstrip('/?'),
+ "autohideHtml5": False,
+ })),
'track': None,
- 'youtube_streams': create_youtube_string(self.item_descriptor),
- 'yt_test_timeout': 1500,
- 'yt_api_url': 'www.youtube.com/iframe_api',
- 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
- 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
- 'transcript_language': u'en',
- 'transcript_languages': json.dumps(OrderedDict({"en": "English", "uk": u"Українська"})),
- 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'translation'
- ).rstrip('/?'),
- 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'available_translations'
- ).rstrip('/?'),
+ 'transcript_download_formats_list': [
+ {'display_name': 'SubRip (.srt) file', 'value': 'srt'},
+ {'display_name': 'Text (.txt) file', 'value': 'txt'}
+ ],
+ 'poster': 'null',
}
self.assertEqual(
@@ -100,43 +109,51 @@ class TestVideoNonYouTube(TestVideo):
the template generates an empty string for the YouTube streams.
"""
context = self.item_descriptor.render(STUDENT_VIEW).content
- sources = json.dumps([u'example.mp4', u'example.webm'])
+ sources = [u'example.mp4', u'example.webm']
expected_context = {
- 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'branding_info': None,
'license': None,
+ 'bumper_metadata': 'null',
'cdn_eval': False,
'cdn_exp_group': None,
- 'data_dir': getattr(self, 'data_dir', None),
- 'show_captions': 'true',
- 'handout': None,
'display_name': u'A Name',
'download_video_link': u'example.mp4',
- 'end': 3610.0,
+ 'handout': None,
'id': self.item_descriptor.location.html_id(),
- 'sources': sources,
- 'speed': 'null',
- 'general_speed': 1.0,
- 'start': 3603.0,
- 'saved_video_position': 0.0,
- 'sub': u'a_sub_file.srt.sjson',
+ 'metadata': json.dumps(OrderedDict({
+ "saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state",
+ "autoplay": False,
+ "streams": "1.00:3_yD_cEKoCk",
+ "sub": "a_sub_file.srt.sjson",
+ "sources": sources,
+ "captionDataDir": None,
+ "showCaptions": "true",
+ "generalSpeed": 1.0,
+ "speed": None,
+ "savedVideoPosition": 0.0,
+ "start": 3603.0,
+ "end": 3610.0,
+ "transcriptLanguage": "en",
+ "transcriptLanguages": OrderedDict({"en": "English"}),
+ "ytTestTimeout": 1500,
+ "ytApiUrl": "www.youtube.com/iframe_api",
+ "ytTestUrl": "gdata.youtube.com/feeds/api/videos/",
+ "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
+ ).rstrip('/?'),
+ "transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'available_translations'
+ ).rstrip('/?'),
+ "autohideHtml5": False,
+ })),
'track': None,
- 'youtube_streams': '1.00:3_yD_cEKoCk',
- 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
- 'yt_test_timeout': 1500,
- 'yt_api_url': 'www.youtube.com/iframe_api',
- 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
- 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
- 'transcript_language': u'en',
- 'transcript_languages': '{"en": "English"}',
- 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'translation'
- ).rstrip('/?'),
- 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'available_translations'
- ).rstrip('/?')
+ 'transcript_download_formats_list': [
+ {'display_name': 'SubRip (.srt) file', 'value': 'srt'},
+ {'display_name': 'Text (.txt) file', 'value': 'txt'}
+ ],
+ 'poster': 'null',
}
self.assertEqual(
@@ -157,6 +174,32 @@ class TestGetHtmlMethod(BaseTestXmodule):
def setUp(self):
super(TestGetHtmlMethod, self).setUp()
self.setup_course()
+ self.default_metadata_dict = OrderedDict({
+ "saveStateUrl": "",
+ "autoplay": settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
+ "streams": "1.00:3_yD_cEKoCk",
+ "sub": "a_sub_file.srt.sjson",
+ "sources": '[]',
+ "captionDataDir": None,
+ "showCaptions": "true",
+ "generalSpeed": 1.0,
+ "speed": None,
+ "savedVideoPosition": 0.0,
+ "start": 3603.0,
+ "end": 3610.0,
+ "transcriptLanguage": "en",
+ "transcriptLanguages": OrderedDict({"en": "English"}),
+ "ytTestTimeout": 1500,
+ "ytApiUrl": "www.youtube.com/iframe_api",
+ "ytTestUrl": "gdata.youtube.com/feeds/api/videos/",
+ "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
+ ).rstrip('/?'),
+ "transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'available_translations'
+ ).rstrip('/?'),
+ "autohideHtml5": False,
+ })
def test_get_html_track(self):
SOURCE_XML = """
@@ -209,36 +252,31 @@ class TestGetHtmlMethod(BaseTestXmodule):
'transcripts': '
',
},
]
- sources = json.dumps([u'example.mp4', u'example.webm'])
+ sources = [u'example.mp4', u'example.webm']
expected_context = {
'branding_info': None,
'license': None,
+ 'bumper_metadata': 'null',
'cdn_eval': False,
'cdn_exp_group': None,
- 'data_dir': getattr(self, 'data_dir', None),
- 'show_captions': 'true',
- 'handout': None,
'display_name': u'A Name',
'download_video_link': u'example.mp4',
- 'end': 3610.0,
- 'id': None,
- 'sources': sources,
- 'start': 3603.0,
- 'saved_video_position': 0.0,
- 'sub': u'a_sub_file.srt.sjson',
- 'speed': 'null',
- 'general_speed': 1.0,
- 'track': u'http://www.example.com/track',
- 'youtube_streams': '1.00:3_yD_cEKoCk',
- 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
- 'yt_test_timeout': 1500,
- 'yt_api_url': 'www.youtube.com/iframe_api',
- 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
- 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
+ 'handout': None,
+ 'id': self.item_descriptor.location.html_id(),
+ 'metadata': '',
+ 'track': None,
+ 'transcript_download_format': 'srt',
+ 'transcript_download_formats_list': [
+ {'display_name': 'SubRip (.srt) file', 'value': 'srt'},
+ {'display_name': 'Text (.txt) file', 'value': 'txt'}
+ ],
+ 'poster': 'null',
}
for data in cases:
+ metadata = self.default_metadata_dict
+ metadata['sources'] = sources
DATA = SOURCE_XML.format(
download_track=data['download_track'],
track=data['track'],
@@ -252,22 +290,29 @@ class TestGetHtmlMethod(BaseTestXmodule):
).rstrip('/?')
context = self.item_descriptor.render(STUDENT_VIEW).content
-
- expected_context.update({
- 'transcript_download_format': None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt',
- 'transcript_languages': '{"en": "English"}' if not data['transcripts'] else json.dumps({"uk": u'Українська'}),
- 'transcript_language': u'en' if not data['transcripts'] or data.get('sub') else u'uk',
- 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'translation'
+ metadata.update({
+ 'transcriptLanguages': {"en": "English"} if not data['transcripts'] else {"uk": u'Українська'},
+ 'transcriptLanguage': u'en' if not data['transcripts'] or data.get('sub') else u'uk',
+ 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
).rstrip('/?'),
- 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+ 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'),
- 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
- 'track': track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url'],
+ 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
'sub': data['sub'],
- 'id': self.item_descriptor.location.html_id(),
})
+ expected_context.update({
+ 'transcript_download_format': (
+ None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt'
+ ),
+ 'track': (
+ track_url if data['expected_track_url'] == u'a_sub_file.srt.sjson' else data['expected_track_url']
+ ),
+ 'id': self.item_descriptor.location.html_id(),
+ 'metadata': json.dumps(metadata)
+ })
+
self.assertEqual(
context,
self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context),
@@ -295,7 +340,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
""",
'result': {
'download_video_link': u'example_source.mp4',
- 'sources': json.dumps([u'example.mp4', u'example.webm']),
+ 'sources': [u'example.mp4', u'example.webm'],
},
},
{
@@ -307,7 +352,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
""",
'result': {
'download_video_link': u'example.mp4',
- 'sources': json.dumps([u'example.mp4', u'example.webm']),
+ 'sources': [u'example.mp4', u'example.webm'],
},
},
{
@@ -326,7 +371,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
""",
'result': {
- 'sources': json.dumps([u'example.mp4', u'example.webm']),
+ 'sources': [u'example.mp4', u'example.webm'],
},
},
]
@@ -334,31 +379,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
initial_context = {
'branding_info': None,
'license': None,
+ 'bumper_metadata': 'null',
'cdn_eval': False,
'cdn_exp_group': None,
- 'data_dir': getattr(self, 'data_dir', None),
- 'show_captions': 'true',
- 'handout': None,
'display_name': u'A Name',
- 'download_video_link': None,
- 'end': 3610.0,
- 'id': None,
- 'sources': '[]',
- 'speed': 'null',
- 'general_speed': 1.0,
- 'start': 3603.0,
- 'saved_video_position': 0.0,
- 'sub': u'a_sub_file.srt.sjson',
+ 'download_video_link': u'example.mp4',
+ 'handout': None,
+ 'id': self.item_descriptor.location.html_id(),
+ 'metadata': self.default_metadata_dict,
'track': None,
- 'youtube_streams': '1.00:3_yD_cEKoCk',
- 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
- 'yt_test_timeout': 1500,
- 'yt_api_url': 'www.youtube.com/iframe_api',
- 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
- 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
- 'transcript_language': u'en',
- 'transcript_languages': '{"en": "English"}',
+ 'transcript_download_formats_list': [
+ {'display_name': 'SubRip (.srt) file', 'value': 'srt'},
+ {'display_name': 'Text (.txt) file', 'value': 'txt'}
+ ],
+ 'poster': 'null',
}
for data in cases:
@@ -371,17 +406,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
context = self.item_descriptor.render(STUDENT_VIEW).content
expected_context = dict(initial_context)
- expected_context.update({
- 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'translation'
+ expected_context['metadata'].update({
+ 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
).rstrip('/?'),
- 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+ 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'),
- 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
- 'id': self.item_descriptor.location.html_id(),
+ 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
+ 'sources': data['result'].get('sources', []),
+ })
+ expected_context.update({
+ 'id': self.item_descriptor.location.html_id(),
+ 'download_video_link': data['result'].get('download_video_link'),
+ 'metadata': json.dumps(expected_context['metadata'])
})
- expected_context.update(data['result'])
self.assertEqual(
context,
@@ -413,7 +452,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
'edx_video_id': "meow",
'result': {
'download_video_link': u'example_source.mp4',
- 'sources': json.dumps([u'example.mp4', u'example.webm']),
+ 'sources': [u'example.mp4', u'example.webm'],
}
}
DATA = SOURCE_XML.format(
@@ -469,39 +508,32 @@ class TestGetHtmlMethod(BaseTestXmodule):
'result': {
'download_video_link': None,
# make sure the desktop_mp4 url is included as part of the alternative sources.
- 'sources': json.dumps([u'example.mp4', u'example.webm', u'http://www.meowmix.com']),
+ 'sources': [u'example.mp4', u'example.webm', u'http://www.meowmix.com'],
}
}
# Video found for edx_video_id
+ metadata = self.default_metadata_dict
+ metadata['autoplay'] = False
+ metadata['sources'] = ""
initial_context = {
'branding_info': None,
'license': None,
+ 'bumper_metadata': 'null',
'cdn_eval': False,
'cdn_exp_group': None,
- 'data_dir': getattr(self, 'data_dir', None),
- 'show_captions': 'true',
- 'handout': None,
'display_name': u'A Name',
- 'download_video_link': None,
- 'end': 3610.0,
- 'id': None,
- 'sources': '[]',
- 'speed': 'null',
- 'general_speed': 1.0,
- 'start': 3603.0,
- 'saved_video_position': 0.0,
- 'sub': u'a_sub_file.srt.sjson',
+ 'download_video_link': u'example.mp4',
+ 'handout': None,
+ 'id': self.item_descriptor.location.html_id(),
'track': None,
- 'youtube_streams': '1.00:3_yD_cEKoCk',
- 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
- 'yt_test_timeout': 1500,
- 'yt_api_url': 'www.youtube.com/iframe_api',
- 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
- 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
- 'transcript_language': u'en',
- 'transcript_languages': '{"en": "English"}',
+ 'transcript_download_formats_list': [
+ {'display_name': 'SubRip (.srt) file', 'value': 'srt'},
+ {'display_name': 'Text (.txt) file', 'value': 'txt'}
+ ],
+ 'poster': 'null',
+ 'metadata': metadata
}
DATA = SOURCE_XML.format(
@@ -514,17 +546,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
context = self.item_descriptor.render(STUDENT_VIEW).content
expected_context = dict(initial_context)
- expected_context.update({
- 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'translation'
+ expected_context['metadata'].update({
+ 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
).rstrip('/?'),
- 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+ 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'),
- 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
- 'id': self.item_descriptor.location.html_id(),
+ 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
+ 'sources': data['result']['sources'],
+ })
+ expected_context.update({
+ 'id': self.item_descriptor.location.html_id(),
+ 'download_video_link': data['result']['download_video_link'],
+ 'metadata': json.dumps(expected_context['metadata'])
})
- expected_context.update(data['result'])
self.assertEqual(
context,
@@ -579,42 +615,32 @@ class TestGetHtmlMethod(BaseTestXmodule):
'result': {
'download_video_link': u'http://fake-video.edx.org/thundercats.mp4',
# make sure the urls for the various encodings are included as part of the alternative sources.
- 'sources': json.dumps(
- [u'example.mp4', u'example.webm'] +
- [video['url'] for video in encoded_videos]
- ),
+ 'sources': [u'example.mp4', u'example.webm'] +
+ [video['url'] for video in encoded_videos],
}
}
# Video found for edx_video_id
+ metadata = self.default_metadata_dict
+ metadata['sources'] = ""
initial_context = {
'branding_info': None,
'license': None,
+ 'bumper_metadata': 'null',
'cdn_eval': False,
'cdn_exp_group': None,
- 'data_dir': getattr(self, 'data_dir', None),
- 'show_captions': 'true',
- 'handout': None,
'display_name': u'A Name',
- 'download_video_link': None,
- 'end': 3610.0,
- 'id': None,
- 'sources': '[]',
- 'speed': 'null',
- 'general_speed': 1.0,
- 'start': 3603.0,
- 'saved_video_position': 0.0,
- 'sub': u'a_sub_file.srt.sjson',
+ 'download_video_link': u'example.mp4',
+ 'handout': None,
+ 'id': self.item_descriptor.location.html_id(),
'track': None,
- 'youtube_streams': '1.00:3_yD_cEKoCk',
- 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
- 'yt_test_timeout': 1500,
- 'yt_api_url': 'www.youtube.com/iframe_api',
- 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
- 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
- 'transcript_language': u'en',
- 'transcript_languages': '{"en": "English"}',
+ 'transcript_download_formats_list': [
+ {'display_name': 'SubRip (.srt) file', 'value': 'srt'},
+ {'display_name': 'Text (.txt) file', 'value': 'txt'}
+ ],
+ 'poster': 'null',
+ 'metadata': metadata,
}
DATA = SOURCE_XML.format(
@@ -627,17 +653,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
context = self.item_descriptor.render(STUDENT_VIEW).content
expected_context = dict(initial_context)
- expected_context.update({
- 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'translation'
+ expected_context['metadata'].update({
+ 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
).rstrip('/?'),
- 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+ 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'),
- 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
- 'id': self.item_descriptor.location.html_id(),
+ 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
+ 'sources': data['result']['sources'],
+ })
+ expected_context.update({
+ 'id': self.item_descriptor.location.html_id(),
+ 'download_video_link': data['result']['download_video_link'],
+ 'metadata': json.dumps(expected_context['metadata'])
})
- expected_context.update(data['result'])
self.assertEqual(
context,
@@ -690,12 +720,10 @@ class TestGetHtmlMethod(BaseTestXmodule):
""",
'result': {
'download_video_link': u'example_source.mp4',
- 'sources': json.dumps(
- [
- u'http://cdn_example.com/example.mp4',
- u'http://cdn_example.com/example.webm'
- ]
- ),
+ 'sources': [
+ u'http://cdn_example.com/example.mp4',
+ u'http://cdn_example.com/example.webm'
+ ],
},
}
@@ -712,31 +740,21 @@ class TestGetHtmlMethod(BaseTestXmodule):
'url': 'http://www.xuetangx.com'
},
'license': None,
+ 'bumper_metadata': 'null',
'cdn_eval': False,
'cdn_exp_group': None,
- 'data_dir': getattr(self, 'data_dir', None),
- 'show_captions': 'true',
- 'handout': None,
'display_name': u'A Name',
'download_video_link': None,
- 'end': 3610.0,
+ 'handout': None,
'id': None,
- 'sources': '[]',
- 'speed': 'null',
- 'general_speed': 1.0,
- 'start': 3603.0,
- 'saved_video_position': 0.0,
- 'sub': u'a_sub_file.srt.sjson',
+ 'metadata': self.default_metadata_dict,
'track': None,
- 'youtube_streams': '1.00:3_yD_cEKoCk',
- 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', True),
- 'yt_test_timeout': 1500,
- 'yt_api_url': 'www.youtube.com/iframe_api',
- 'yt_test_url': 'gdata.youtube.com/feeds/api/videos/',
'transcript_download_format': 'srt',
- 'transcript_download_formats_list': [{'display_name': 'SubRip (.srt) file', 'value': 'srt'}, {'display_name': 'Text (.txt) file', 'value': 'txt'}],
- 'transcript_language': u'en',
- 'transcript_languages': '{"en": "English"}',
+ 'transcript_download_formats_list': [
+ {'display_name': 'SubRip (.srt) file', 'value': 'srt'},
+ {'display_name': 'Text (.txt) file', 'value': 'txt'}
+ ],
+ 'poster': 'null',
}
for data in cases:
@@ -748,21 +766,23 @@ class TestGetHtmlMethod(BaseTestXmodule):
)
self.initialize_module(data=DATA)
self.item_descriptor.xmodule_runtime.user_location = 'CN'
-
context = self.item_descriptor.render('student_view').content
-
expected_context = dict(initial_context)
- expected_context.update({
- 'transcript_translation_url': self.item_descriptor.xmodule_runtime.handler_url(
- self.item_descriptor, 'transcript', 'translation'
+ expected_context['metadata'].update({
+ 'transcriptTranslationUrl': self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
).rstrip('/?'),
- 'transcript_available_translations_url': self.item_descriptor.xmodule_runtime.handler_url(
+ 'transcriptAvailableTranslationsUrl': self.item_descriptor.xmodule_runtime.handler_url(
self.item_descriptor, 'transcript', 'available_translations'
).rstrip('/?'),
- 'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
- 'id': self.item_descriptor.location.html_id(),
+ 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
+ 'sources': data['result'].get('sources', []),
+ })
+ expected_context.update({
+ 'id': self.item_descriptor.location.html_id(),
+ 'download_video_link': data['result'].get('download_video_link'),
+ 'metadata': json.dumps(expected_context['metadata'])
})
- expected_context.update(data['result'])
self.assertEqual(
context,
@@ -948,3 +968,125 @@ class VideoDescriptorTest(TestCase, VideoDescriptorTestBase):
VideoDescriptor.from_xml(xml_data, module_system, id_generator=Mock())
with self.assertRaises(ValVideoNotFoundError):
get_video_info("test_edx_video_id")
+
+
+class TestVideoWithBumper(TestVideo):
+ """
+ Tests rendered content in presence of video bumper.
+ """
+ CATEGORY = "video"
+ METADATA = {}
+ FEATURES = settings.FEATURES
+
+ @patch('xmodule.video_module.bumper_utils.get_bumper_settings')
+ def test_is_bumper_enabled(self, get_bumper_settings):
+ """
+ Check that bumper is (not)shown if ENABLE_VIDEO_BUMPER is (False)True
+
+ Assume that bumper settings are correct.
+ """
+ self.FEATURES.update({
+ "SHOW_BUMPER_PERIODICITY": 1,
+ "ENABLE_VIDEO_BUMPER": True,
+ })
+
+ get_bumper_settings.return_value = {
+ "video_id": "edx_video_id",
+ "transcripts": {},
+ }
+ with override_settings(FEATURES=self.FEATURES):
+ self.assertTrue(bumper_utils.is_bumper_enabled(self.item_descriptor))
+
+ self.FEATURES.update({"ENABLE_VIDEO_BUMPER": False})
+
+ with override_settings(FEATURES=self.FEATURES):
+ self.assertFalse(bumper_utils.is_bumper_enabled(self.item_descriptor))
+
+ @patch('xmodule.video_module.bumper_utils.is_bumper_enabled')
+ @patch('xmodule.video_module.bumper_utils.get_bumper_settings')
+ @patch('edxval.api.get_urls_for_profiles')
+ def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bumper_enabled):
+ """
+ Test content with rendered bumper metadata.
+ """
+ get_url_for_profiles.return_value = {
+ "desktop_mp4": "http://test_bumper.mp4",
+ "desktop_webm": "",
+ }
+
+ get_bumper_settings.return_value = {
+ "video_id": "edx_video_id",
+ "transcripts": {},
+ }
+
+ is_bumper_enabled.return_value = True
+
+ content = self.item_descriptor.render(STUDENT_VIEW).content
+ sources = [u'example.mp4', u'example.webm']
+ expected_context = {
+ 'branding_info': None,
+ 'license': None,
+ 'bumper_metadata': json.dumps(OrderedDict({
+ 'saveStateUrl': self.item_descriptor.xmodule_runtime.ajax_url + '/save_user_state',
+ "showCaptions": "true",
+ "sources": ["http://test_bumper.mp4"],
+ 'streams': '',
+ "transcriptLanguage": "en",
+ "transcriptLanguages": {"en": "English"},
+ "transcriptTranslationUrl": video_utils.set_query_parameter(
+ self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
+ ).rstrip('/?'), 'is_bumper', 1
+ ),
+ "transcriptAvailableTranslationsUrl": video_utils.set_query_parameter(
+ self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'available_translations'
+ ).rstrip('/?'), 'is_bumper', 1
+ ),
+ })),
+ 'cdn_eval': False,
+ 'cdn_exp_group': None,
+ 'display_name': u'A Name',
+ 'download_video_link': u'example.mp4',
+ 'handout': None,
+ 'id': self.item_descriptor.location.html_id(),
+ 'metadata': json.dumps(OrderedDict({
+ "saveStateUrl": self.item_descriptor.xmodule_runtime.ajax_url + "/save_user_state",
+ "autoplay": False,
+ "streams": "0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg",
+ "sub": "a_sub_file.srt.sjson",
+ "sources": sources,
+ "captionDataDir": None,
+ "showCaptions": "true",
+ "generalSpeed": 1.0,
+ "speed": None,
+ "savedVideoPosition": 0.0,
+ "start": 3603.0,
+ "end": 3610.0,
+ "transcriptLanguage": "en",
+ "transcriptLanguages": OrderedDict({"en": "English", "uk": u"Українська"}),
+ "ytTestTimeout": 1500,
+ "ytApiUrl": "www.youtube.com/iframe_api",
+ "ytTestUrl": "gdata.youtube.com/feeds/api/videos/",
+ "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'translation/__lang__'
+ ).rstrip('/?'),
+ "transcriptAvailableTranslationsUrl": self.item_descriptor.xmodule_runtime.handler_url(
+ self.item_descriptor, 'transcript', 'available_translations'
+ ).rstrip('/?'),
+ "autohideHtml5": False,
+ })),
+ 'track': None,
+ 'transcript_download_format': 'srt',
+ 'transcript_download_formats_list': [
+ {'display_name': 'SubRip (.srt) file', 'value': 'srt'},
+ {'display_name': 'Text (.txt) file', 'value': 'txt'}
+ ],
+ 'poster': json.dumps(OrderedDict({
+ "url": "http://img.youtube.com/vi/ZwkTiUPN0mg/0.jpg",
+ "type": "youtube"
+ }))
+ }
+
+ expected_content = self.item_descriptor.xmodule_runtime.render_template('video.html', expected_context)
+ self.assertEqual(content, expected_content)
diff --git a/lms/djangoapps/mobile_api/video_outlines/serializers.py b/lms/djangoapps/mobile_api/video_outlines/serializers.py
index 73f7089df6..167abf3e23 100644
--- a/lms/djangoapps/mobile_api/video_outlines/serializers.py
+++ b/lms/djangoapps/mobile_api/video_outlines/serializers.py
@@ -206,7 +206,8 @@ def video_summary(video_profiles, course_id, video_descriptor, request, local_ca
size = default_encoded_video.get('file_size', 0)
# Transcripts...
- transcript_langs = video_descriptor.available_translations(verify_assets=False)
+ transcripts_info = video_descriptor.get_transcripts_info()
+ transcript_langs = video_descriptor.available_translations(transcripts_info, verify_assets=False)
transcripts = {
lang: reverse(
@@ -227,7 +228,7 @@ def video_summary(video_profiles, course_id, video_descriptor, request, local_ca
"duration": duration,
"size": size,
"transcripts": transcripts,
- "language": video_descriptor.get_default_transcript_language(),
+ "language": video_descriptor.get_default_transcript_language(transcripts_info),
"encoded_videos": video_data.get('profiles')
}
ret.update(always_available_data)
diff --git a/lms/djangoapps/mobile_api/video_outlines/views.py b/lms/djangoapps/mobile_api/video_outlines/views.py
index ae1d7361c6..f0a25a77af 100644
--- a/lms/djangoapps/mobile_api/video_outlines/views.py
+++ b/lms/djangoapps/mobile_api/video_outlines/views.py
@@ -119,7 +119,8 @@ class VideoTranscripts(generics.RetrieveAPIView):
)
try:
video_descriptor = modulestore().get_item(usage_key)
- content, filename, mimetype = video_descriptor.get_transcript(lang=lang)
+ transcripts = video_descriptor.get_transcripts_info()
+ content, filename, mimetype = video_descriptor.get_transcript(transcripts, lang=lang)
except (NotFoundError, ValueError, KeyError):
raise Http404(u"Transcript not found for {}, lang: {}".format(block_id, lang))
diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py
index 3a1c1a450e..ce93aac8da 100644
--- a/lms/envs/bok_choy.py
+++ b/lms/envs/bok_choy.py
@@ -123,6 +123,11 @@ FEATURES['ENABLE_MAX_FAILED_LOGIN_ATTEMPTS'] = False
FEATURES['SQUELCH_PII_IN_LOGS'] = False
FEATURES['PREVENT_CONCURRENT_LOGINS'] = False
FEATURES['ADVANCED_SECURITY'] = False
+
+FEATURES['ENABLE_MOBILE_REST_API'] = True # Show video bumper in LMS
+FEATURES['ENABLE_VIDEO_BUMPER'] = True # Show video bumper in LMS
+FEATURES['SHOW_BUMPER_PERIODICITY'] = 1
+
PASSWORD_MIN_LENGTH = None
PASSWORD_COMPLEXITY = {}
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 3e0d69b7d0..74dac03426 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -392,6 +392,13 @@ FEATURES = {
# Teams feature
'ENABLE_TEAMS': False,
+
+ # Show video bumper in LMS
+ 'ENABLE_VIDEO_BUMPER': False,
+
+ # How many seconds to show the bumper again, default is 7 days:
+ 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,
+
}
# Ignore static asset files on import which match this pattern
@@ -1665,6 +1672,8 @@ YOUTUBE = {
'v': 'set_youtube_id_of_11_symbols_here',
},
},
+
+ 'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080
}
################################### APPS ######################################
diff --git a/lms/templates/video.html b/lms/templates/video.html
index 9936b97b7d..48c069a873 100644
--- a/lms/templates/video.html
+++ b/lms/templates/video.html
@@ -7,47 +7,9 @@
${_('Go back to start of transcript.')}
@@ -116,8 +50,8 @@
% if transcript_download_format:
${_('Download transcript')}