diff --git a/cms/djangoapps/contentstore/features/transcripts.py b/cms/djangoapps/contentstore/features/transcripts.py index f6413cb924..e198ee3f32 100644 --- a/cms/djangoapps/contentstore/features/transcripts.py +++ b/cms/djangoapps/contentstore/features/transcripts.py @@ -201,7 +201,8 @@ def upload_file(_step, file_name): @step('I see "([^"]*)" text in the captions') def check_text_in_the_captions(_step, text): - world.wait_for(lambda _: world.css_text('.subtitles'), 30) + world.wait_for_present('.video.is-captions-rendered') + world.wait_for(lambda _: world.css_text('.subtitles'), timeout=30) actual_text = world.css_text('.subtitles') assert (text in actual_text) diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py index d2e7c81b58..b8287047af 100644 --- a/cms/djangoapps/contentstore/features/video.py +++ b/cms/djangoapps/contentstore/features/video.py @@ -197,11 +197,15 @@ def find_caption_line_by_data_index(index): @step('I focus on caption line with data-index "([^"]*)"$') def focus_on_caption_line(_step, index): + world.wait_for_present('.video.is-captions-rendered') + world.wait_for(lambda _: world.css_text('.subtitles'), timeout=30) find_caption_line_by_data_index(int(index.strip()))._element.send_keys(Keys.TAB) @step('I press "enter" button on caption line with data-index "([^"]*)"$') def click_on_the_caption(_step, index): + world.wait_for_present('.video.is-captions-rendered') + world.wait_for(lambda _: world.css_text('.subtitles'), timeout=30) find_caption_line_by_data_index(int(index.strip()))._element.send_keys(Keys.ENTER) @@ -214,7 +218,6 @@ def caption_line_has_class(_step, index, className): @step('I see a range on slider$') def see_a_range_slider_with_proper_range(_step): world.wait_for_visible(VIDEO_BUTTONS['pause']) - assert world.css_visible(".slider-range") 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 31ffe79a11..e08ba56a75 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js @@ -396,6 +396,89 @@ function (Initialize) { }); }); }); + + describe('setPlayerMode', function () { + beforeEach(function () { + state = { + currentPlayerMode: 'flash', + }; + }); + + it('updates player mode', function () { + var setPlayerMode = Initialize.prototype.setPlayerMode; + + setPlayerMode.call(state, 'html5'); + expect(state.currentPlayerMode).toBe('html5'); + setPlayerMode.call(state, 'flash'); + expect(state.currentPlayerMode).toBe('flash'); + }); + + it('sets default mode if passed is not supported', function () { + var setPlayerMode = Initialize.prototype.setPlayerMode; + + setPlayerMode.call(state, '77html77'); + expect(state.currentPlayerMode).toBe('html5'); + }); + }); + + describe('getPlayerMode', function () { + beforeEach(function () { + state = { + currentPlayerMode: 'flash', + }; + }); + + it('returns current player mode', function () { + var getPlayerMode = Initialize.prototype.getPlayerMode, + actual = getPlayerMode.call(state); + + expect(actual).toBe(state.currentPlayerMode); + }); + }); + + describe('isFlashMode', function () { + it('returns `true` if player in `flash` mode', function () { + var state = { + getPlayerMode: jasmine.createSpy().andReturn('flash'), + }, + isFlashMode = Initialize.prototype.isFlashMode, + actual = isFlashMode.call(state); + + expect(actual).toBeTruthy(); + }); + + it('returns `false` if player is not in `flash` mode', function () { + var state = { + getPlayerMode: jasmine.createSpy().andReturn('html5'), + }, + isFlashMode = Initialize.prototype.isFlashMode, + actual = isFlashMode.call(state); + + expect(actual).toBeFalsy(); + }); + }); + + describe('isHtml5Mode', function () { + it('returns `true` if player in `html5` mode', function () { + var state = { + getPlayerMode: jasmine.createSpy().andReturn('html5'), + }, + isHtml5Mode = Initialize.prototype.isHtml5Mode, + actual = isHtml5Mode.call(state); + + expect(actual).toBeTruthy(); + }); + + it('returns `false` if player is not in `html5` mode', function () { + var state = { + getPlayerMode: jasmine.createSpy().andReturn('flash'), + }, + isHtml5Mode = Initialize.prototype.isHtml5Mode, + actual = isHtml5Mode.call(state); + + expect(actual).toBeFalsy(); + }); + }); }); }); 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 0b569faf6f..6c6685e4fc 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 @@ -1070,6 +1070,9 @@ function (VideoPlayer) { beforeEach(function () { state = { youtubeId: jasmine.createSpy().andReturn('videoId'), + isFlashMode: jasmine.createSpy().andReturn(false), + isHtml5Mode: jasmine.createSpy().andReturn(true), + setPlayerMode: jasmine.createSpy(), videoPlayer: { currentTime: 60, isPlaying: jasmine.createSpy(), @@ -1083,7 +1086,8 @@ function (VideoPlayer) { }); it('in Flash mode and video is playing', function () { - state.currentPlayerMode = 'flash'; + state.isFlashMode.andReturn(true); + state.isHtml5Mode.andReturn(false); state.videoPlayer.isPlaying.andReturn(true); VideoPlayer.prototype.setPlaybackRate.call(state, '0.75'); expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60); @@ -1092,7 +1096,8 @@ function (VideoPlayer) { }); it('in Flash mode and video not started', function () { - state.currentPlayerMode = 'flash'; + state.isFlashMode.andReturn(true); + state.isHtml5Mode.andReturn(false); state.videoPlayer.isPlaying.andReturn(false); VideoPlayer.prototype.setPlaybackRate.call(state, '0.75'); expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60); @@ -1101,13 +1106,11 @@ function (VideoPlayer) { }); it('in HTML5 mode', function () { - state.currentPlayerMode = 'html5'; VideoPlayer.prototype.setPlaybackRate.call(state, '0.75'); expect(state.videoPlayer.player.setPlaybackRate).toHaveBeenCalledWith('0.75'); }); it('Youtube video in FF, with new speed equal 1.0', function () { - state.currentPlayerMode = 'html5'; state.videoType = 'youtube'; state.browserIsFirefox = true; diff --git a/common/lib/xmodule/xmodule/js/src/video/00_async_process.js b/common/lib/xmodule/xmodule/js/src/video/00_async_process.js index 55a2cb1448..faffff7ca0 100644 --- a/common/lib/xmodule/xmodule/js/src/video/00_async_process.js +++ b/common/lib/xmodule/xmodule/js/src/video/00_async_process.js @@ -10,7 +10,7 @@ function() { * @param {array} list Array to process. * @param {function} process Calls this function on each item in the list. * @return {array} Returns a Promise object to observe when all actions of a - certain type bound to the collection, queued or not, have finished. + * certain type bound to the collection, queued or not, have finished. */ var AsyncProcess = { array: function (list, process) { 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 cf37e68e79..79a1d76af0 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -63,13 +63,16 @@ function (VideoPlayer, VideoStorage) { fetchMetadata: fetchMetadata, getCurrentLanguage: getCurrentLanguage, getDuration: getDuration, + getPlayerMode: getPlayerMode, getVideoMetadata: getVideoMetadata, initialize: initialize, + isHtml5Mode: isHtml5Mode, isFlashMode: isFlashMode, parseSpeed: parseSpeed, parseVideoSources: parseVideoSources, parseYoutubeStreams: parseYoutubeStreams, saveState: saveState, + setPlayerMode: setPlayerMode, setSpeed: setSpeed, trigger: trigger, youtubeId: youtubeId @@ -250,18 +253,6 @@ function (VideoPlayer, VideoStorage) { } } - // function _setPlayerMode(state) - // By default we will be forcing HTML5 player mode. Only in the case - // when, after initializtion, we will get one available playback rate, - // we will change to Flash player mode. There is a need to store this - // setting in cookies because otherwise we will have to change from - // HTML5 to Flash on every page load in a browser that doesn't fully - // support HTML5. When we have this setting in cookies, we can select - // the proper mode from the start (not having to change mode later on). - function _setPlayerMode(state) { - state.currentPlayerMode = 'html5'; - } - // function _parseYouTubeIDs(state) // The function parse YouTube stream ID's. // @return @@ -339,8 +330,7 @@ function (VideoPlayer, VideoStorage) { function _setConfigurations(state) { _configureCaptions(state); - _setPlayerMode(state); - + state.setPlayerMode(state.config.mode); // Possible value are: 'visible', 'hiding', and 'invisible'. state.controlState = 'visible'; state.controlHideTimeout = null; @@ -520,7 +510,8 @@ function (VideoPlayer, VideoStorage) { element: element, fadeOutTimeout: 1400, captionsFreezeTime: 10000, - availableQualities: ['hd720', 'hd1080', 'highres'] + availableQualities: ['hd720', 'hd1080', 'highres'], + mode: $.cookie('edX_video_player_mode') }); if (this.config.endTime < this.config.startTime) { @@ -811,8 +802,46 @@ function (VideoPlayer, VideoStorage) { } } + /** + * Sets player mode. + * + * @param {string} mode Mode to set for the video player if it is supported. + * Otherwise, `html5` is used by default. + */ + function setPlayerMode(mode) { + var supportedModes = ['html5', 'flash']; + + mode = _.contains(supportedModes, mode) ? mode : 'html5'; + this.currentPlayerMode = mode; + } + + /** + * Returns current player mode. + * + * @return {string} Returns string that describes player mode + */ + function getPlayerMode() { + return this.currentPlayerMode; + } + + /** + * Checks if current player mode is Flash. + * + * @return {boolean} Returns `true` if current mode is `flash`, otherwise + * it returns `false` + */ function isFlashMode() { - return this.currentPlayerMode === 'flash'; + return this.getPlayerMode() === 'flash'; + } + + /** + * Checks if current player mode is Html5. + * + * @return {boolean} Returns `true` if current mode is `html5`, otherwise + * it returns `false` + */ + function isHtml5Mode() { + return this.getPlayerMode() === 'html5'; } function getCurrentLanguage() { diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js index 5d6ca56c94..8623f67045 100644 --- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js @@ -251,7 +251,7 @@ function (HTML5Video, Resizer) { // Remove from the page current iFrame with HTML5 video. state.videoPlayer.player.destroy(); - state.currentPlayerMode = 'flash'; + state.setPlayerMode('flash'); console.log('[Video info]: Changing YouTube player mode to "flash".'); @@ -334,7 +334,7 @@ function (HTML5Video, Resizer) { methodName, youtubeId; if ( - this.currentPlayerMode === 'html5' && + this.isHtml5Mode() && !( this.browserIsFirefox && newSpeed === '1.0' && @@ -554,7 +554,7 @@ function (HTML5Video, Resizer) { // For more information, please see the PR that introduced this change: // https://github.com/edx/edx-platform/pull/2841 if ( - (this.currentPlayerMode === 'html5' || availablePlaybackRates.length > 1) && + (this.isHtml5Mode() || availablePlaybackRates.length > 1) && this.videoType === 'youtube' ) { if (availablePlaybackRates.length === 1 && !this.isTouch) { @@ -568,7 +568,7 @@ function (HTML5Video, Resizer) { _restartUsingFlash(this); } else if (availablePlaybackRates.length > 1) { - this.currentPlayerMode = 'html5'; + this.setPlayerMode('html5'); // We need to synchronize available frame rates with the ones // that the user specified. @@ -607,7 +607,7 @@ function (HTML5Video, Resizer) { this.trigger('videoSpeedControl.setSpeed', this.speed); } - if (this.currentPlayerMode === 'html5') { + if (this.isHtml5Mode()) { this.videoPlayer.player.setPlaybackRate(this.speed); } diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js index 71e7155c7f..bfb4fab566 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js @@ -234,6 +234,7 @@ function (Sjson, AsyncProcess) { }; } + state.el.removeClass('is-captions-rendered'); // Fetch the captions file. If no file was specified, or if an error // occurred, then we hide the captions panel, and the "CC" button this.fetchXHR = $.ajaxWithPrefix({ @@ -447,9 +448,9 @@ function (Sjson, AsyncProcess) { // outline has to be drawn (tabbing) or not (mouse click). self.isMouseFocus = false; self.rendered = true; + self.state.el.addClass('is-captions-rendered'); }; - this.rendered = false; this.subtitlesEl.empty(); this.setSubtitlesHeight(); diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index b95f6f4bf7..f3916810c1 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -10,7 +10,6 @@ in-browser HTML5 video method (when in HTML5 mode). - Navigational subtitles can be disabled altogether via an attribute in XML. """ -import os import json import logging from operator import itemgetter @@ -36,8 +35,6 @@ from .video_utils import create_youtube_string from .video_xfields import VideoFields from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers -from xmodule.modulestore.inheritance import InheritanceKeyValueStore -from xblock.runtime import KvsFieldData from urlparse import urlparse def get_ext(filename): diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 4193944370..4e87688f1a 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -310,3 +310,34 @@ Feature: LMS Video component When I open video "D" Then the video has rendered in "HTML5" mode And the video does not show the captions + + # 27 + Scenario: Transcripts are available on different speeds of Flash mode + Given I am registered for the course "test_course" + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "Flash" mode + Then the video has rendered in "Flash" mode + And I make sure captions are opened + And I see "Hi, welcome to Edx." text in the captions + Then I select the "1.50" speed + And I see "Hi, welcome to Edx." text in the captions + Then I select the "0.75" speed + And I see "Hi, welcome to Edx." text in the captions + Then I select the "1.25" speed + And I see "Hi, welcome to Edx." text in the captions + + # 28 + Scenario: Elapsed time calculates correctly on different speeds of Flash mode + Given I am registered for the course "test_course" + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "Flash" mode + And I make sure captions are opened + Then I select the "1.50" speed + And I click video button "pause" + And I click on caption line "4", video module shows elapsed time "7" + Then I select the "0.75" speed + And I click video button "pause" + And I click on caption line "3", video module shows elapsed time "9" + Then I select the "1.25" speed + And I click video button "pause" + And I click on caption line "2", video module shows elapsed time "4" diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index ac2c8c126e..c62f1ad300 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # pylint: disable=C0111 -from lettuce import world, step, before +from lettuce import world, step, before, after import json import os import time @@ -26,6 +26,13 @@ HTML5_SOURCES = [ 'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv', ] +FLASH_SOURCES = { + 'youtube_id_1_0': 'OEoXaMPEzfM', + 'youtube_id_0_75': 'JMD_ifUUfsU', + 'youtube_id_1_25': 'AKqURZnYqpk', + 'youtube_id_1_5': 'DYpADpL7jAY', +} + HTML5_SOURCES_INCORRECT = [ 'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99', ] @@ -52,6 +59,11 @@ def setUp(scenario): world.video_sequences = {} +@after.each_scenario +def tearDown(scenario): + world.browser.cookies.delete('edX_video_player_mode') + + class RequestHandlerWithSessionId(object): def get(self, url): """ @@ -98,19 +110,6 @@ def get_metadata(parent_location, player_mode, data, display_name='Video'): 'metadata': {}, } - if data: - conversions = { - 'transcripts': json.loads, - 'download_track': json.loads, - 'download_video': json.loads, - } - - for key in data: - if key in conversions: - data[key] = conversions[key](data[key]) - - kwargs['metadata'].update(data) - if player_mode == 'html5': kwargs['metadata'].update({ 'youtube_id_1_0': '', @@ -136,6 +135,23 @@ def get_metadata(parent_location, player_mode, data, display_name='Video'): 'html5_sources': HTML5_SOURCES_INCORRECT }) + if player_mode == 'flash': + kwargs['metadata'].update(FLASH_SOURCES) + world.browser.cookies.add({'edX_video_player_mode': 'flash'}) + + if data: + conversions = { + 'transcripts': json.loads, + 'download_track': json.loads, + 'download_video': json.loads, + } + + for key in data: + if key in conversions: + data[key] = conversions[key](data[key]) + + kwargs['metadata'].update(data) + return kwargs @@ -251,6 +267,14 @@ def duration(): return duration +def elapsed_time(): + """ + Elapsed time of the video, in seconds. + """ + elapsed_time, duration = video_time() + return elapsed_time + + def video_time(): """ Return a tuple `(elapsed_time, duration)`, each in seconds. @@ -273,6 +297,11 @@ def parse_time_str(time_str): return time_obj.tm_min * 60 + time_obj.tm_sec +def find_caption_line_by_data_index(index): + SELECTOR = ".subtitles > li[data-index='{index}']".format(index=index) + return world.css_find(SELECTOR).first + + @step('youtube stub server (.*) YouTube API') def configure_youtube_api(_step, action): action=action.strip() @@ -349,7 +378,8 @@ def set_youtube_response_timeout(_step, time): def video_is_rendered(_step, mode): modes = { 'html5': 'video', - 'youtube': 'iframe' + 'youtube': 'iframe', + 'flash': 'iframe', } html_tag = modes[mode.lower()] assert world.css_find('.video {0}'.format(html_tag)).first @@ -360,7 +390,8 @@ def video_is_rendered(_step, mode): def videos_are_rendered(_step, mode): modes = { 'html5': 'video', - 'youtube': 'iframe' + 'youtube': 'iframe', + 'flash': 'iframe', } html_tag = modes[mode.lower()] @@ -423,6 +454,7 @@ def i_see_menu(_step, menu): @step('I see "([^"]*)" text in the captions$') def check_text_in_the_captions(_step, text): + world.wait_for_present('.video.is-captions-rendered') world.wait_for(lambda _: world.css_text('.subtitles')) actual_text = world.css_text('.subtitles') assert (text in actual_text) @@ -430,6 +462,7 @@ def check_text_in_the_captions(_step, text): @step('I see text in the captions:') def check_captions(_step): + world.wait_for_present('.video.is-captions-rendered') for index, video in enumerate(_step.hashes): assert (video.get('text') in world.css_text('.subtitles', index=index)) @@ -439,12 +472,12 @@ def select_language(_step, code): # Make sure that all ajax requests that affects the language menu are finished. # For example, request to get new translation etc. world.wait_for_ajax_complete() - selector = VIDEO_MENUS["language"] + ' li[data-lang-code="{code}"]'.format( code=code ) world.css_find(VIDEO_BUTTONS["CC"])[0].mouse_over() + world.wait_for_present('.lang.open') world.css_click(selector) assert world.css_has_class(selector, 'active') @@ -454,6 +487,7 @@ def select_language(_step, code): # For example, request to get new translation etc. world.wait_for_ajax_complete() world.wait_for_visible('.subtitles') + world.wait_for_present('.video.is-captions-rendered') @step('I click video button "([^"]*)"$') @@ -472,10 +506,12 @@ def start_playing_video_from_n_seconds(_step, position): @step('I see duration "([^"]*)"$') def i_see_duration(_step, position): world.wait_for( - func=lambda _: duration() == parse_time_str(position), + func=lambda _: duration() > 0, timeout=30 ) + assert duration() == parse_time_str(position) + @step('I seek video to "([^"]*)" seconds$') def seek_video_to_n_seconds(_step, seconds): @@ -507,14 +543,11 @@ def video_alignment(_step, transcript_visibility): set_window_dimensions(300, 600) real, expected = get_all_dimensions() - width = round(100 * real['width']/expected['width']) == wrapper_width set_window_dimensions(600, 300) real, expected = get_all_dimensions() - height = abs(expected['height'] - real['height']) <= 5 - # Restore initial window size set_window_dimensions(initial['width'], initial['height']) @@ -569,3 +602,12 @@ def shows_captions(_step, show_captions): assert world.is_css_present('div.video.closed') else: assert world.is_css_not_present('div.video.closed') + + +@step('I click on caption line "([^"]*)", video module shows elapsed time "([^"]*)"$') +def click_on_the_caption(_step, index, expected_time): + world.wait_for_present('.video.is-captions-rendered') + find_caption_line_by_data_index(int(index)).click() + actual_time = elapsed_time() + assert int(expected_time) == actual_time +