From 63c5ec5277422f67015f4cadec821b4da2460dae Mon Sep 17 00:00:00 2001 From: Anton Stupak Date: Mon, 19 Aug 2013 15:48:24 +0300 Subject: [PATCH 1/3] Add supporting failover from Youtube. --- .../lib/xmodule/xmodule/js/spec/helper.coffee | 7 +- .../xmodule/js/spec/video/general_spec.js | 40 +++++ .../js/spec/video/video_player_spec.js | 2 + .../xmodule/js/src/video/01_initialize.js | 147 +++++++++++------- 4 files changed, 142 insertions(+), 54 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee index 1dfccdf521..5fbbd27fad 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.coffee +++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee @@ -90,7 +90,12 @@ 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 + 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$/ diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index accfba0dbe..f4dd1db206 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -58,6 +58,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 () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js index e92f251f70..0873426aa9 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js @@ -79,6 +79,8 @@ it('create Youtube player', function() { var oldYT = window.YT; + jasmine.stubRequests(); + window.YT = { Player: function () { }, PlayerState: oldYT.PlayerState 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 9a6e20421d..62c9e88141 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -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 state = this; + // 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); + _setConfigurations(state); + _renderElements(state); + } else { + state.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(state); + state.el.find('a.quality_control').hide(); + } + + _setConfigurations(state); + _renderElements(state); + }); + } + } + // 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; From 22b5decc95aabbe67c21479b6d772f2026903b05 Mon Sep 17 00:00:00 2001 From: Anton Stupak Date: Tue, 20 Aug 2013 16:49:33 +0300 Subject: [PATCH 2/3] Small fixes. --- .../lib/xmodule/xmodule/js/spec/helper.coffee | 1 + .../xmodule/js/spec/video/general_spec.js | 12 ++--- .../xmodule/js/src/video/01_initialize.js | 48 +++++++++---------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee index 5fbbd27fad..f3cecf71cb 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.coffee +++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee @@ -91,6 +91,7 @@ jasmine.stubRequests = -> spyOn($, 'ajax').andCallFake (settings) -> if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ if settings.success + # match[1] - it's video ID settings.success data: jasmine.stubbedMetadata[match[1]] else { always: (callback) -> diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index f4dd1db206..33d7277bb4 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -61,12 +61,12 @@ describe('Check Youtube link existence', function () { var statusList = { - 'error': 'html5', - 'timeout': 'html5', - 'abort': 'html5', - 'parsererror': 'html5', - 'success': 'youtube', - 'notmodified': 'youtube' + error: 'html5', + timeout: 'html5', + abort: 'html5', + parsererror: 'html5', + success: 'youtube', + notmodified: 'youtube' }; function stubDeffered(data, status) { 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 62c9e88141..79bc16dbda 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -201,44 +201,44 @@ function (VideoPlayer) { // The function set initial configuration and preparation. function initialize(element) { - var state = this; + var _this = this; // This is used in places where we instead would have to check if an element has a CSS class 'fullscreen'. - state.isFullScreen = false; + this.isFullScreen = false; // The parent element of the video, and the ID. - state.el = $(element).find('.video'); - state.id = state.el.attr('id').replace(/video_/, ''); + 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. - state.config = { + this.config = { element: element, - start: state.el.data('start'), - end: state.el.data('end'), + start: this.el.data('start'), + end: this.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'), + 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: state.el.data('sub'), - mp4Source: state.el.data('mp4-source'), - webmSource: state.el.data('webm-source'), - oggSource: state.el.data('ogg-source'), + 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(state))) { + if (!(_parseYouTubeIDs(this))) { // If we do not have YouTube ID's, try parsing HTML5 video sources. - _prepareHTML5Video(state); - _setConfigurations(state); - _renderElements(state); + _prepareHTML5Video(this); + _setConfigurations(this); + _renderElements(this); } else { - state.getVideoMetadata() + this.getVideoMetadata() .always(function(json, status) { var err = $.isPlainObject(json.error) || (status !== "success" && status !== "notmodified"); @@ -247,12 +247,12 @@ function (VideoPlayer) { // When the youtube link doesn't work for any reason // (for example, the great firewall in china) any // alternate sources should automatically play. - _prepareHTML5Video(state); - state.el.find('a.quality_control').hide(); + _prepareHTML5Video(_this); + _this.el.find('a.quality_control').hide(); } - _setConfigurations(state); - _renderElements(state); + _setConfigurations(_this); + _renderElements(_this); }); } } From 03111cb98ed4a335288193c79a6fe8a75b9e6669 Mon Sep 17 00:00:00 2001 From: Anton Stupak Date: Tue, 20 Aug 2013 16:52:51 +0300 Subject: [PATCH 3/3] Remove onTouchBasedDevice function. --- common/lib/xmodule/xmodule/js/spec/video/general_spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js index 33d7277bb4..9194106fff 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -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 () {