Merge pull request #746 from edx/anton/support_failover_from_yt
Add supporting failover from Youtube.
This commit is contained in:
@@ -90,7 +90,13 @@ jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']
|
||||
jasmine.stubRequests = ->
|
||||
spyOn($, 'ajax').andCallFake (settings) ->
|
||||
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
|
||||
settings.success data: jasmine.stubbedMetadata[match[1]]
|
||||
if settings.success
|
||||
# match[1] - it's video ID
|
||||
settings.success data: jasmine.stubbedMetadata[match[1]]
|
||||
else {
|
||||
always: (callback) ->
|
||||
callback.call(window, {}, 'success');
|
||||
}
|
||||
else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/
|
||||
settings.success jasmine.stubbedCaption
|
||||
else if settings.url.match /.+\/problem_get$/
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
beforeEach(function () {
|
||||
jasmine.stubRequests();
|
||||
oldOTBD = window.onTouchBasedDevice;
|
||||
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
|
||||
this.videosDefinition = '0.75:7tqY6eQzVhE,1.0:cogebirgzzM';
|
||||
this['7tqY6eQzVhE'] = '7tqY6eQzVhE';
|
||||
this['cogebirgzzM'] = 'cogebirgzzM';
|
||||
@@ -16,7 +14,6 @@
|
||||
window.onYouTubePlayerAPIReady = undefined;
|
||||
window.onHTML5PlayerAPIReady = undefined;
|
||||
$('source').remove();
|
||||
window.onTouchBasedDevice = oldOTBD;
|
||||
});
|
||||
|
||||
describe('constructor', function () {
|
||||
@@ -58,6 +55,46 @@
|
||||
expect(this.state.speed).toEqual('0.75');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Check Youtube link existence', function () {
|
||||
var statusList = {
|
||||
error: 'html5',
|
||||
timeout: 'html5',
|
||||
abort: 'html5',
|
||||
parsererror: 'html5',
|
||||
success: 'youtube',
|
||||
notmodified: 'youtube'
|
||||
};
|
||||
|
||||
function stubDeffered(data, status) {
|
||||
return {
|
||||
always: function(callback) {
|
||||
callback.call(window, data, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkPlayer(videoType, data, status) {
|
||||
this.state = new window.Video('#example');
|
||||
spyOn(this.state , 'getVideoMetadata')
|
||||
.andReturn(stubDeffered(data, status));
|
||||
this.state.initialize('#example');
|
||||
|
||||
expect(this.state.videoType).toEqual(videoType);
|
||||
}
|
||||
|
||||
it('if video id is incorrect', function () {
|
||||
checkPlayer('html5', { error: {} }, 'success');
|
||||
});
|
||||
|
||||
$.each(statusList, function(status, mode){
|
||||
it('Status:' + status + ', mode:' + mode, function () {
|
||||
checkPlayer(mode, {}, status);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('HTML5', function () {
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
it('create Youtube player', function() {
|
||||
var oldYT = window.YT;
|
||||
|
||||
jasmine.stubRequests();
|
||||
|
||||
window.YT = {
|
||||
Player: function () { },
|
||||
PlayerState: oldYT.PlayerState
|
||||
|
||||
@@ -30,8 +30,7 @@ function (VideoPlayer) {
|
||||
*/
|
||||
return function (state, element) {
|
||||
_makeFunctionsPublic(state);
|
||||
_initialize(state, element);
|
||||
_renderElements(state);
|
||||
state.initialize(element);
|
||||
};
|
||||
|
||||
// ***************************************************************
|
||||
@@ -56,59 +55,12 @@ function (VideoPlayer) {
|
||||
// Old private functions. Now also public so that can be
|
||||
// tested by Jasmine.
|
||||
|
||||
state.initialize = _.bind(initialize, state);
|
||||
state.parseSpeed = _.bind(parseSpeed, state);
|
||||
state.fetchMetadata = _.bind(fetchMetadata, state);
|
||||
state.parseYoutubeStreams = _.bind(parseYoutubeStreams, state);
|
||||
state.parseVideoSources = _.bind(parseVideoSources, state);
|
||||
}
|
||||
|
||||
// function _initialize(element)
|
||||
// The function set initial configuration and preparation.
|
||||
|
||||
function _initialize(state, element) {
|
||||
// This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'.
|
||||
state.isFullScreen = false;
|
||||
|
||||
// The parent element of the video, and the ID.
|
||||
state.el = $(element).find('.video');
|
||||
state.id = state.el.attr('id').replace(/video_/, '');
|
||||
|
||||
// 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.
|
||||
state.config = {
|
||||
element: element,
|
||||
|
||||
start: state.el.data('start'),
|
||||
end: state.el.data('end'),
|
||||
|
||||
caption_data_dir: state.el.data('caption-data-dir'),
|
||||
caption_asset_path: state.el.data('caption-asset-path'),
|
||||
show_captions: (state.el.data('show-captions').toString().toLowerCase() === 'true'),
|
||||
youtubeStreams: state.el.data('streams'),
|
||||
|
||||
sub: state.el.data('sub'),
|
||||
mp4Source: state.el.data('mp4-source'),
|
||||
webmSource: state.el.data('webm-source'),
|
||||
oggSource: state.el.data('ogg-source'),
|
||||
|
||||
fadeOutTimeout: 1400,
|
||||
|
||||
availableQualities: ['hd720', 'hd1080', 'highres']
|
||||
};
|
||||
|
||||
if (!(_parseYouTubeIDs(state))) {
|
||||
// If we do not have YouTube ID's, try parsing HTML5 video sources.
|
||||
_prepareHTML5Video(state);
|
||||
}
|
||||
|
||||
_configureCaptions(state);
|
||||
_setPlayerMode(state);
|
||||
|
||||
// Possible value are: 'visible', 'hiding', and 'invisible'.
|
||||
state.controlState = 'visible';
|
||||
state.controlHideTimeout = null;
|
||||
state.captionState = 'visible';
|
||||
state.captionHideTimeout = null;
|
||||
state.getVideoMetadata = _.bind(getVideoMetadata, state);
|
||||
}
|
||||
|
||||
// function _renderElements(state)
|
||||
@@ -228,12 +180,83 @@ function (VideoPlayer) {
|
||||
state.setSpeed($.cookie('video_speed'));
|
||||
}
|
||||
|
||||
function _setConfigurations(state) {
|
||||
_configureCaptions(state);
|
||||
_setPlayerMode(state);
|
||||
|
||||
// Possible value are: 'visible', 'hiding', and 'invisible'.
|
||||
state.controlState = 'visible';
|
||||
state.controlHideTimeout = null;
|
||||
state.captionState = 'visible';
|
||||
state.captionHideTimeout = null;
|
||||
}
|
||||
|
||||
// ***************************************************************
|
||||
// Public functions start here.
|
||||
// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
|
||||
// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
|
||||
// ***************************************************************
|
||||
|
||||
// function initialize(element)
|
||||
// The function set initial configuration and preparation.
|
||||
|
||||
function initialize(element) {
|
||||
var _this = this;
|
||||
// This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'.
|
||||
this.isFullScreen = false;
|
||||
|
||||
// The parent element of the video, and the ID.
|
||||
this.el = $(element).find('.video');
|
||||
this.id = this.el.attr('id').replace(/video_/, '');
|
||||
|
||||
// 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.
|
||||
this.config = {
|
||||
element: element,
|
||||
|
||||
start: this.el.data('start'),
|
||||
end: this.el.data('end'),
|
||||
|
||||
caption_data_dir: this.el.data('caption-data-dir'),
|
||||
caption_asset_path: this.el.data('caption-asset-path'),
|
||||
show_captions: (this.el.data('show-captions').toString().toLowerCase() === 'true'),
|
||||
youtubeStreams: this.el.data('streams'),
|
||||
|
||||
sub: this.el.data('sub'),
|
||||
mp4Source: this.el.data('mp4-source'),
|
||||
webmSource: this.el.data('webm-source'),
|
||||
oggSource: this.el.data('ogg-source'),
|
||||
|
||||
fadeOutTimeout: 1400,
|
||||
|
||||
availableQualities: ['hd720', 'hd1080', 'highres']
|
||||
};
|
||||
|
||||
if (!(_parseYouTubeIDs(this))) {
|
||||
// If we do not have YouTube ID's, try parsing HTML5 video sources.
|
||||
_prepareHTML5Video(this);
|
||||
_setConfigurations(this);
|
||||
_renderElements(this);
|
||||
} else {
|
||||
this.getVideoMetadata()
|
||||
.always(function(json, status) {
|
||||
var err = $.isPlainObject(json.error) ||
|
||||
(status !== "success" && status !== "notmodified");
|
||||
|
||||
if (err){
|
||||
// When the youtube link doesn't work for any reason
|
||||
// (for example, the great firewall in china) any
|
||||
// alternate sources should automatically play.
|
||||
_prepareHTML5Video(_this);
|
||||
_this.el.find('a.quality_control').hide();
|
||||
}
|
||||
|
||||
_setConfigurations(_this);
|
||||
_renderElements(_this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// function parseYoutubeStreams(state, youtubeStreams)
|
||||
//
|
||||
// Take a string in the form:
|
||||
@@ -297,9 +320,9 @@ function (VideoPlayer) {
|
||||
this.metadata = {};
|
||||
|
||||
$.each(this.videos, function (speed, url) {
|
||||
$.get('https://gdata.youtube.com/feeds/api/videos/' + url + '?v=2&alt=jsonc', (function(data) {
|
||||
_this.getVideoMetadata(url, function(data) {
|
||||
_this.metadata[data.data.id] = data.data;
|
||||
}), 'jsonp');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,6 +352,24 @@ function (VideoPlayer) {
|
||||
}
|
||||
}
|
||||
|
||||
function getVideoMetadata(url, callback) {
|
||||
var successHandler, xhr;
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
url = this.videos['1.0'] || '';
|
||||
}
|
||||
|
||||
successHandler = ($.isFunction(callback)) ? callback : null;
|
||||
xhr = $.ajax({
|
||||
url: 'https://gdata.youtube.com/feeds/api/videos/' + url + '?v=2&alt=jsonc',
|
||||
timeout: 500,
|
||||
dataType: 'jsonp',
|
||||
success: successHandler
|
||||
});
|
||||
|
||||
return xhr;
|
||||
}
|
||||
|
||||
function stopBuffering() {
|
||||
var video;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user