diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 80a00b0e1099..a97c04792fdc 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -118,8 +118,8 @@ def seed(): pass # Point the URL used to test YouTube availability to our stub YouTube server -YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) -YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) +YOUTUBE['API'] = "http://127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) +YOUTUBE['METADATA_URL'] = "http://127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT) # Generate a random UUID so that different runs of acceptance tests don't break each other diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 85396530fe2e..fdfaf9cca8e1 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -348,3 +348,4 @@ XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False) +XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index 84916debe96b..8c3fc54f9ea7 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -103,8 +103,8 @@ # Point the URL used to test YouTube availability to our stub YouTube server YOUTUBE_PORT = 9080 -YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) -YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) +YOUTUBE['API'] = "http://127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) +YOUTUBE['METADATA_URL'] = "http://127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT) FEATURES['ENABLE_COURSEWARE_INDEX'] = True diff --git a/cms/envs/common.py b/cms/envs/common.py index a683dce91208..65ae5ec2316d 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -44,7 +44,7 @@ PROFILE_IMAGE_SECRET_KEY, PROFILE_IMAGE_MIN_BYTES, PROFILE_IMAGE_MAX_BYTES, # The following setting is included as it is used to check whether to # display credit eligibility table on the CMS or not. - ENABLE_CREDIT_ELIGIBILITY + ENABLE_CREDIT_ELIGIBILITY, YOUTUBE_API_KEY ) from path import path from warnings import simplefilter @@ -652,10 +652,10 @@ YOUTUBE = { # YouTube JavaScript API - 'API': 'www.youtube.com/iframe_api', + 'API': 'https://www.youtube.com/iframe_api', - # URL to test YouTube availability - 'TEST_URL': 'gdata.youtube.com/feeds/api/videos/', + # URL to get YouTube metadata + 'METADATA_URL': 'https://www.googleapis.com/youtube/v3/videos', # Current youtube api for requesting transcripts. # For example: http://video.google.com/timedtext?lang=en&v=j_jEn79vS3g. @@ -994,6 +994,9 @@ XBLOCK_SETTINGS = { "VideoDescriptor": { "licensing_enabled": FEATURES.get("LICENSING", False) + }, + 'VideoModule': { + 'YOUTUBE_API_KEY': YOUTUBE_API_KEY } } diff --git a/cms/templates/ux/reference/container.html b/cms/templates/ux/reference/container.html index 19ab1db780e4..569e37910578 100644 --- a/cms/templates/ux/reference/container.html +++ b/cms/templates/ux/reference/container.html @@ -137,7 +137,7 @@

Page Actions

Video

-
+
diff --git a/common/djangoapps/terrain/setup_prereqs.py b/common/djangoapps/terrain/setup_prereqs.py index 98f5c6798373..90337c386775 100644 --- a/common/djangoapps/terrain/setup_prereqs.py +++ b/common/djangoapps/terrain/setup_prereqs.py @@ -28,8 +28,7 @@ YOUTUBE_API_URLS = { 'main': 'https://www.youtube.com/', - 'player': 'http://www.youtube.com/iframe_api', - 'metadata': 'http://gdata.youtube.com/feeds/api/videos/', + 'player': 'https://www.youtube.com/iframe_api', # For transcripts, you need to check an actual video, so we will # just specify our default video and see if that one is available. 'transcript': 'http://video.google.com/timedtext?lang=en&v=OEoXaMPEzfM', diff --git a/common/djangoapps/terrain/stubs/youtube.py b/common/djangoapps/terrain/stubs/youtube.py index af3a87f5ecdd..27f9d5ef2e1d 100644 --- a/common/djangoapps/terrain/stubs/youtube.py +++ b/common/djangoapps/terrain/stubs/youtube.py @@ -95,6 +95,9 @@ def do_GET(self): if self.server.config.get('youtube_api_blocked'): self.send_response(404, content='', headers={'Content-type': 'text/plain'}) else: + # Delay the response to simulate network latency + time.sleep(self.server.config.get('time_to_response', self.DEFAULT_DELAY_SEC)) + # Get the response to send from YouTube. # We need to do this every time because Google sometimes sends different responses # as part of their own experiments, which has caused our tests to become "flaky" @@ -117,17 +120,16 @@ def _send_video_response(self, youtube_id, message): # Construct the response content callback = self.get_params['callback'] - youtube_metadata = json.loads( - requests.get( - "http://gdata.youtube.com/feeds/api/videos/{id}?v=2&alt=jsonc".format(id=youtube_id) - ).text - ) + data = OrderedDict({ - 'data': OrderedDict({ - 'id': youtube_id, - 'message': message, - 'duration': youtube_metadata['data']['duration'], - }) + 'items': list( + OrderedDict({ + 'contentDetails': OrderedDict({ + 'id': youtube_id, + 'duration': 'PT2M20S', + }) + }) + ) }) response = "{cb}({data})".format(cb=callback, data=json.dumps(data)) diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html index dabb3801b97f..f7d04cce65cc 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video.html @@ -4,7 +4,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html index 617d95835760..5b20840630c8 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html @@ -4,7 +4,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html index 47be4f04fcb9..f82451e377cf 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html @@ -4,7 +4,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html index 77017d403dd1..267897e5988f 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html @@ -4,7 +4,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html b/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html index 22bd2062689f..efada9d13fc4 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html @@ -4,7 +4,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html index 8842b1e5926f..0af562049f5f 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html @@ -4,7 +4,7 @@
@@ -38,7 +38,7 @@
@@ -68,7 +68,7 @@
diff --git a/common/lib/xmodule/xmodule/js/js_test.yml b/common/lib/xmodule/xmodule/js/js_test.yml index 6af10abaa553..6c72d4b64930 100644 --- a/common/lib/xmodule/xmodule/js/js_test.yml +++ b/common/lib/xmodule/xmodule/js/js_test.yml @@ -59,6 +59,7 @@ lib_paths: - common_static/js/src/utility.js - public/js/split_test_staff.js - common_static/js/src/accessibility_tools.js + - common_static/js/vendor/moment.min.js # Paths to spec (test) JavaScript files spec_paths: diff --git a/common/lib/xmodule/xmodule/js/spec/helper.js b/common/lib/xmodule/xmodule/js/spec/helper.js index 97d422d5d891..9c83fd006306 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.js +++ b/common/lib/xmodule/xmodule/js/spec/helper.js @@ -36,7 +36,7 @@ return f(); } }; - + jasmine.YT = stubbedYT; // Stub YouTube API. window.YT = stubbedYT; @@ -76,19 +76,27 @@ jasmine.stubbedMetadata = { '7tqY6eQzVhE': { - id: '7tqY6eQzVhE', - duration: 300 + contentDetails : { + id: '7tqY6eQzVhE', + duration: 'PT5M0S' + } }, 'cogebirgzzM': { - id: 'cogebirgzzM', - duration: 200 + contentDetails : { + id: 'cogebirgzzM', + duration: 'PT3M20S' + } }, 'abcdefghijkl': { - id: 'abcdefghijkl', - duration: 400 + contentDetails : { + id: 'abcdefghijkl', + duration: 'PT6M40S' + } }, bogus: { - duration: 100 + contentDetails : { + duration: 'PT1M40S' + } } }; @@ -122,7 +130,7 @@ } return spy.andCallFake(function (settings) { var match = settings.url - .match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/), + .match(/googleapis\.com\/.+\/videos\/\?id=(.+)&part=contentDetails/), status, callCallback; if (match) { status = match[1].split('_'); @@ -138,7 +146,7 @@ }; } else if (settings.success) { return settings.success({ - data: jasmine.stubbedMetadata[match[1]] + items: jasmine.stubbedMetadata[match[1]] }); } else { return { @@ -167,15 +175,6 @@ // Do nothing. return; } else if (settings.url === '/save_user_state') { - return {success: true}; - } else if (settings.url === 'http://www.youtube.com/iframe_api') { - // Stub YouTube API. - window.YT = stubbedYT; - - // Call the callback that must be called when YouTube API is - // loaded. By specification. - window.onYouTubeIframeAPIReady(); - return {success: true}; } else { throw 'External request attempted for ' + @@ -224,6 +223,19 @@ // Stub jQuery.scrollTo module. $.fn.scrollTo = jasmine.createSpy('jQuery.scrollTo'); + // Stub window.Video.loadYouTubeIFrameAPI() + window.Video.loadYouTubeIFrameAPI = jasmine.createSpy('window.Video.loadYouTubeIFrameAPI').andReturn( + function (scriptTag) { + var event = document.createEvent('Event'); + if (fixture === "video.html") { + event.initEvent('load', false, false); + } else { + event.initEvent('error', false, false); + } + scriptTag.dispatchEvent(event); + } + ); + jasmine.initializePlayer = function (fixture, params) { var state; 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 7cefab5e4999..f9c0eabf2dfa 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -115,6 +115,12 @@ return state.youtubeApiAvailable === true; }, 'YouTube API is loaded', 3000); + window.YT = jasmine.YT; + + // Call the callback that must be called when YouTube API is + // loaded. By specification. + window.onYouTubeIframeAPIReady(); + runs(function () { // If YouTube API is not loaded, then the code will should create // a global callback that will be called by API once it is loaded. diff --git a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js index f3194b8bce3a..040400514472 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js @@ -71,10 +71,10 @@ function (Initialize) { speed: '1.50', metadata: { 'testId': { - duration: 400 + duration: 'PT6M40S' }, 'videoId': { - duration: 100 + duration: 'PT1M40S' } }, videos: { 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 2a30ceb45452..cc43a2335674 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -16,6 +16,8 @@ define( 'video/01_initialize.js', ['video/03_video_player.js', 'video/00_i18n.js'], function (VideoPlayer, i18n) { + var moment = window.moment; + /** * @function * @@ -31,6 +33,9 @@ function (VideoPlayer, i18n) { state.initialize(element) .done(function () { + if (state.isYoutubeType()) { + state.parseSpeed(); + } // On iPhones and iPods native controls are used. if (/iP(hone|od)/i.test(state.isTouch[0])) { _hideWaitPlaceholder(state); @@ -75,7 +80,10 @@ function (VideoPlayer, i18n) { setSpeed: setSpeed, speedToString: speedToString, trigger: trigger, - youtubeId: youtubeId + youtubeId: youtubeId, + loadHtmlPlayer: loadHtmlPlayer, + loadYoutubePlayer: loadYoutubePlayer, + loadYouTubeIFrameAPI: loadYouTubeIFrameAPI }, _youtubeApiDeferred = null, @@ -126,6 +134,9 @@ function (VideoPlayer, i18n) { onYTApiReady = function () { console.log('[Video info]: YouTube API is available and is loaded.'); + if (state.htmlPlayerLoaded) { return; } + + console.log('[Video info]: Starting YouTube player.'); video = VideoPlayer(state); state.modules.push(video); @@ -176,7 +187,6 @@ function (VideoPlayer, i18n) { if (!_youtubeApiDeferred) { _youtubeApiDeferred = $.Deferred(); setupOnYouTubeIframeAPIReady(); - _loadYoutubeApi(state); } else if (!window.onYouTubeIframeAPIReady || !window.onYouTubeIframeAPIReady.done) { // The Deferred object could have been already defined in a previous // initialization of the video module. However, since then the global variable @@ -196,24 +206,32 @@ function (VideoPlayer, i18n) { state.modules.push(video); state.__dfd__.resolve(); + state.htmlPlayerLoaded = true; } } - function _loadYoutubeApi(state) { - console.log('[Video info]: YouTube API is not loaded. Will try to load...'); + function _waitForYoutubeApi(state) { + console.log('[Video info]: Starting to wait for YouTube API to load.'); window.setTimeout(function () { // If YouTube API will load OK, it will run `onYouTubeIframeAPIReady` // callback, which will set `state.youtubeApiAvailable` to `true`. // If something goes wrong at this stage, `state.youtubeApiAvailable` is // `false`. - if (!state.youtubeIsAvailable) { + if (!state.youtubeApiAvailable) { console.log('[Video info]: YouTube API is not available.'); + if (!state.htmlPlayerLoaded) { + state.loadHtmlPlayer(); + } } - state.el.trigger('youtube_availability', [state.youtubeIsAvailable]); + state.el.trigger('youtube_availability', [state.youtubeApiAvailable]); }, state.config.ytTestTimeout); - $.getScript(document.location.protocol + '//' + state.config.ytApiUrl); + } + + function loadYouTubeIFrameAPI(scriptTag) { + var firstScriptTag = document.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); } // function _configureCaptions(state) @@ -454,6 +472,50 @@ function (VideoPlayer, i18n) { }); } + function loadYoutubePlayer() { + if (this.htmlPlayerLoaded) { return; } + + console.log( + '[Video info]: Fetch metadata for YouTube video.' + ); + + this.fetchMetadata(); + this.parseSpeed(); + } + + function loadHtmlPlayer() { + + // When the youtube link doesn't work for any reason + // (for example, firewall) any + // alternate sources should automatically play. + if (!_prepareHTML5Video(this)) { + console.log( + '[Video info]: Continue loading ' + + 'YouTube video.' + ); + + // Non-YouTube sources were not found either. + + this.el.find('.video-player div') + .removeClass('hidden'); + this.el.find('.video-player h3') + .addClass('hidden'); + + // If in reality the timeout was to short, try to + // continue loading the YouTube video anyways. + this.loadYoutubePlayer(); + } else { + console.log( + '[Video info]: Start HTML5 player.' + ); + + // In-browser HTML5 player does not support quality + // control. + this.el.find('a.quality_control').hide(); + _renderElements(this); + } + } + // function initialize(element) // The function set initial configuration and preparation. @@ -484,7 +546,7 @@ function (VideoPlayer, i18n) { // jQuery .data() return object with keys in lower camelCase format. this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), { element: element, - fadeOutTimeout: 1400, + fadeOutTimeout: 1400, captionsFreezeTime: 10000, mode: $.cookie('edX_video_player_mode'), // Available HD qualities will only be accessible once the video has @@ -500,6 +562,9 @@ function (VideoPlayer, i18n) { this.speed = this.speedToString( this.config.speed || this.config.generalSpeed ); + this.htmlPlayerLoaded = false; + + _setConfigurations(this); if (!(_parseYouTubeIDs(this))) { @@ -512,67 +577,30 @@ function (VideoPlayer, i18n) { } console.log('[Video info]: Start player in HTML5 mode.'); - - _setConfigurations(this); _renderElements(this); } else { - if (!this.youtubeXhr) { - this.youtubeXhr = this.getVideoMetadata(); - } + _renderElements(this); - this.youtubeXhr - .always(function (json, status) { - // It will work for both if statusCode is 200 or 410. - var didSucceed = (json.error && json.error.code === 410) || status === 'success' || status === 'notmodified'; - if (!didSucceed) { - console.log( - '[Video info]: YouTube returned an error for ' + - 'video with id "' + id + '".' - ); - - // When the youtube link doesn't work for any reason - // (for example, the great firewall in china) any - // alternate sources should automatically play. - if (!_prepareHTML5Video(self)) { - console.log( - '[Video info]: Continue loading ' + - 'YouTube video.' - ); - - // Non-YouTube sources were not found either. - - el.find('.video-player div') - .removeClass('hidden'); - el.find('.video-player h3') - .addClass('hidden'); - - // If in reality the timeout was to short, try to - // continue loading the YouTube video anyways. - self.fetchMetadata(); - self.parseSpeed(); - } else { - console.log( - '[Video info]: Change player mode to HTML5.' - ); + _waitForYoutubeApi(this); - // In-browser HTML5 player does not support quality - // control. - el.find('a.quality_control').hide(); - } - } else { - console.log( - '[Video info]: Start player in YouTube mode.' - ); + var scriptTag = document.createElement('script'); - self.fetchMetadata(); - self.parseSpeed(); - } + scriptTag.src = this.config.ytApiUrl; + scriptTag.async = true; - _setConfigurations(self); - _renderElements(self); - }); - } + $(scriptTag).on('load', function() { + self.loadYoutubePlayer(); + }); + $(scriptTag).on('error', function() { + console.log( + '[Video info]: YouTube returned an error for ' + + 'video with id "' + self.id + '".' + ); + self.loadHtmlPlayer(); + }); + window.Video.loadYouTubeIFrameAPI(scriptTag); + } return __dfd__.promise(); } @@ -619,8 +647,9 @@ function (VideoPlayer, i18n) { metadataXHRs = _.map(this.videos, function (url, speed) { return self.getVideoMetadata(url, function (data) { - if (data.data) { - self.metadata[data.data.id] = data.data; + if (data.items.length > 0) { + var metaDataItem = data.items[0]; + self.metadata[metaDataItem.id] = metaDataItem.contentDetails; } }); }); @@ -671,16 +700,16 @@ function (VideoPlayer, i18n) { if (!(_.isString(url))) { url = this.videos['1.0'] || ''; } - - return $.ajax({ - url: [ - document.location.protocol, '//', this.config.ytTestUrl, url, - '?v=2&alt=jsonc' - ].join(''), - dataType: 'jsonp', - timeout: this.config.ytTestTimeout, - success: _.isFunction(callback) ? callback : null - }); + // Will hit the API URL iF YT key is defined in settings. + if (this.config.ytKey) { + return $.ajax({ + url: [this.config.ytMetadataUrl, '?id=', url, '&part=contentDetails&key=', this.config.ytKey].join(''), + timeout: this.config.ytTestTimeout, + success: _.isFunction(callback) ? callback : null + }); + } else { + return $.Deferred().reject().promise(); + } } function youtubeId(speed) { @@ -693,7 +722,7 @@ function (VideoPlayer, i18n) { function getDuration() { try { - return this.metadata[this.youtubeId()].duration; + return moment.duration(this.metadata[this.youtubeId()].duration, moment.ISO_8601).asSeconds(); } catch (err) { return _.result(this.metadata[this.youtubeId('1.0')], 'duration') || 0; } diff --git a/common/lib/xmodule/xmodule/js/src/video/10_main.js b/common/lib/xmodule/xmodule/js/src/video/10_main.js index be3bbc8ae87f..23763aaf1c2d 100644 --- a/common/lib/xmodule/xmodule/js/src/video/10_main.js +++ b/common/lib/xmodule/xmodule/js/src/video/10_main.js @@ -160,6 +160,8 @@ youtubeXhr = null; }; + window.Video.loadYouTubeIFrameAPI = initialize.prototype.loadYouTubeIFrameAPI; + // Invoke the mock Video constructor so that the elements stored within it can be processed by the real // `window.Video` constructor. oldVideo(null, true); diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py index 269adf6109bf..5b0ae3b2fe8c 100644 --- a/common/lib/xmodule/xmodule/tests/test_video.py +++ b/common/lib/xmodule/xmodule/tests/test_video.py @@ -736,8 +736,8 @@ def setUp(self): # YouTube JavaScript API 'API': 'www.youtube.com/iframe_api', - # URL to test YouTube availability - 'TEST_URL': 'gdata.youtube.com/feeds/api/videos/', + # URL to get YouTube metadata + 'METADATA_URL': 'www.googleapis.com/youtube/v3/videos/', # Current youtube api for requesting transcripts. # For example: http://video.google.com/timedtext?lang=en&v=j_jEn79vS3g. diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index f9134c820cbe..a85e14f9e098 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -86,6 +86,7 @@ _ = lambda text: text +@XBlock.wants('settings') class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers, XModule, LicenseMixin): """ XML source example: @@ -261,6 +262,15 @@ def get_html(self): cdn_exp_group = None self.youtube_streams = youtube_streams or create_youtube_string(self) # pylint: disable=W0201 + + settings_service = self.runtime.service(self, 'settings') + + yt_api_key = None + if settings_service: + xblock_settings = settings_service.get_settings_bucket(self) + if xblock_settings and 'YOUTUBE_API_KEY' in xblock_settings: + yt_api_key = xblock_settings['YOUTUBE_API_KEY'] + metadata = { 'saveStateUrl': self.system.ajax_url + '/save_user_state', 'autoplay': settings.FEATURES.get('AUTOPLAY_VIDEOS', False), @@ -286,7 +296,9 @@ def get_html(self): 'ytTestTimeout': 1500, 'ytApiUrl': settings.YOUTUBE['API'], - 'ytTestUrl': settings.YOUTUBE['TEST_URL'], + 'ytMetadataUrl': settings.YOUTUBE['METADATA_URL'], + 'ytKey': yt_api_key, + 'transcriptTranslationUrl': self.runtime.handler_url( self, 'transcript', 'translation/__lang__' ).rstrip('/?'), diff --git a/common/test/acceptance/tests/helpers.py b/common/test/acceptance/tests/helpers.py index 1b8b597520d0..b521ddfca0db 100644 --- a/common/test/acceptance/tests/helpers.py +++ b/common/test/acceptance/tests/helpers.py @@ -67,8 +67,7 @@ def is_youtube_available(): youtube_api_urls = { 'main': 'https://www.youtube.com/', - 'player': 'http://www.youtube.com/iframe_api', - 'metadata': 'http://gdata.youtube.com/feeds/api/videos/', + 'player': 'https://www.youtube.com/iframe_api', # For transcripts, you need to check an actual video, so we will # just specify our default video and see if that one is available. 'transcript': 'http://video.google.com/timedtext?lang=en&v=3_yD_cEKoCk', diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 7eead5c995d8..8e3581888bf7 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -3,7 +3,7 @@ Feature: LMS.Video component As a student, I want to view course videos in LMS # 1 - Scenario: Verify that each video in each sub-section includes a transcript for non-Youtube countries + Scenario: Verify that each video in sub-section includes a transcript for Youtube and non-Youtube countries Given youtube server is up and response time is 2 seconds And I am registered for the course "test_course" And I have a "subs_3_yD_cEKoCk.srt.sjson" transcript file in assets @@ -24,9 +24,9 @@ Feature: LMS.Video component | Welcome to edX. | | Equal transcripts | When I open video "C" - Then the video has rendered in "HTML5" mode + Then the video has rendered in "YOUTUBE" mode And I make sure captions are opened And I see "好 各位同学" text in the captions When I open video "D" - Then the video has rendered in "HTML5" mode - And the video does not show the captions + Then the video has rendered in "YOUTUBE" mode + And I make sure captions are opened diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index cb06a2cbedc1..701f244167cd 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -62,8 +62,9 @@ def test_video_constructor(self): "transcriptLanguage": "en", "transcriptLanguages": OrderedDict({"en": "English", "uk": u"Українська"}), "ytTestTimeout": 1500, - "ytApiUrl": "www.youtube.com/iframe_api", - "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", + "ytApiUrl": "https://www.youtube.com/iframe_api", + "ytMetadataUrl": "https://www.googleapis.com/youtube/v3/videos/", + "ytKey": None, "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), @@ -139,8 +140,9 @@ def test_video_constructor(self): "transcriptLanguage": "en", "transcriptLanguages": OrderedDict({"en": "English"}), "ytTestTimeout": 1500, - "ytApiUrl": "www.youtube.com/iframe_api", - "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", + "ytApiUrl": "https://www.youtube.com/iframe_api", + "ytMetadataUrl": "https://www.googleapis.com/youtube/v3/videos/", + "ytKey": None, "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), @@ -192,8 +194,9 @@ def setUp(self): "transcriptLanguage": "en", "transcriptLanguages": OrderedDict({"en": "English"}), "ytTestTimeout": 1500, - "ytApiUrl": "www.youtube.com/iframe_api", - "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", + "ytApiUrl": "https://www.youtube.com/iframe_api", + "ytMetadataUrl": "https://www.googleapis.com/youtube/v3/videos/", + "ytKey": None, "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), @@ -1181,8 +1184,9 @@ def test_bumper_metadata(self, get_url_for_profiles, get_bumper_settings, is_bum "transcriptLanguage": "en", "transcriptLanguages": OrderedDict({"en": "English", "uk": u"Українська"}), "ytTestTimeout": 1500, - "ytApiUrl": "www.youtube.com/iframe_api", - "ytTestUrl": "gdata.youtube.com/feeds/api/videos/", + "ytApiUrl": "https://www.youtube.com/iframe_api", + "ytMetadataUrl": "https://www.googleapis.com/youtube/v3/videos/", + "ytKey": None, "transcriptTranslationUrl": self.item_descriptor.xmodule_runtime.handler_url( self.item_descriptor, 'transcript', 'translation/__lang__' ).rstrip('/?'), diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py index e932dbf34aa8..c7ac6e811f84 100644 --- a/lms/envs/acceptance.py +++ b/lms/envs/acceptance.py @@ -175,8 +175,8 @@ def seed(): } # Point the URL used to test YouTube availability to our stub YouTube server -YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) -YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) +YOUTUBE['API'] = "http://127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) +YOUTUBE['METADATA_URL'] = "http://127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT) if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or \ diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 5a497f425f00..d878a1e78340 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -625,6 +625,7 @@ XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False) +XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) ##### CDN EXPERIMENT/MONITORING FLAGS ##### CDN_VIDEO_URLS = ENV_TOKENS.get('CDN_VIDEO_URLS', CDN_VIDEO_URLS) diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py index 897029a772d4..bc4141ea537f 100644 --- a/lms/envs/bok_choy.py +++ b/lms/envs/bok_choy.py @@ -115,8 +115,8 @@ # Point the URL used to test YouTube availability to our stub YouTube server YOUTUBE_PORT = 9080 -YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) -YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) +YOUTUBE['API'] = "http://127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT) +YOUTUBE['METADATA_URL'] = "http://127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT) YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT) ############################# SECURITY SETTINGS ################################ diff --git a/lms/envs/common.py b/lms/envs/common.py index 5b6d67d20b75..cffb5a299c55 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1737,10 +1737,10 @@ YOUTUBE = { # YouTube JavaScript API - 'API': 'www.youtube.com/iframe_api', + 'API': 'https://www.youtube.com/iframe_api', - # URL to test YouTube availability - 'TEST_URL': 'gdata.youtube.com/feeds/api/videos/', + # URL to get YouTube metadata + 'METADATA_URL': 'https://www.googleapis.com/youtube/v3/videos/', # Current youtube api for requesting transcripts. # For example: http://video.google.com/timedtext?lang=en&v=j_jEn79vS3g. @@ -1754,6 +1754,7 @@ 'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080 } +YOUTUBE_API_KEY = None ################################### APPS ###################################### INSTALLED_APPS = ( diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index dab385edde83..82b875da5046 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -61,6 +61,7 @@ lib_paths: - xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min.js - xmodule_js/common_static/js/test/i18n.js - xmodule_js/common_static/js/vendor/date.js + - xmodule_js/common_static/js/vendor/moment.min.js # Paths to source JavaScript files src_paths: