')
.height(this.topSpacingHeight())
@@ -936,6 +980,7 @@
.addClass('current');
this.currentIndex = newIndex;
+ this.captionDisplayEl.text(this.subtitlesEl.find("li[data-index='" + newIndex + "']").text());
this.scrollCaption();
}
}
@@ -1017,6 +1062,82 @@
}
},
+ handleCaptioningCookie: function() {
+ if ($.cookie('show_closed_captions') === 'true') {
+ this.state.showClosedCaptions = true;
+ this.showClosedCaptions();
+
+ // keep it going until turned off
+ $.cookie('show_closed_captions', 'true', {
+ expires: 3650,
+ path: '/'
+ });
+ } else {
+ this.hideClosedCaptions();
+ }
+ },
+
+ toggleClosedCaptions: function(event) {
+ event.preventDefault();
+
+ if (this.state.el.hasClass('has-captions')) {
+ this.state.showClosedCaptions = false;
+ this.updateCaptioningCookie(false);
+ this.hideClosedCaptions();
+ } else {
+ this.state.showClosedCaptions = true;
+ this.updateCaptioningCookie(true);
+ this.showClosedCaptions();
+ }
+ },
+
+ showClosedCaptions: function() {
+ this.state.el.addClass('has-captions');
+
+ this.captionDisplayEl
+ .show()
+ .addClass('is-visible');
+
+ this.captionControlEl
+ .addClass('is-active')
+ .find('.control-text')
+ .text(gettext('Hide closed captions'));
+
+ if (this.subtitlesEl.find('.current').text()) {
+ this.captionDisplayEl
+ .text(this.subtitlesEl.find('.current').text());
+ } else {
+ this.captionDisplayEl
+ .text(gettext('(Caption will be displayed when you start playing the video.)'));
+ }
+ },
+
+ hideClosedCaptions: function() {
+ this.state.el.removeClass('has-captions');
+
+ this.captionDisplayEl
+ .hide()
+ .removeClass('is-visible');
+
+ this.captionControlEl
+ .removeClass('is-active')
+ .find('.control-text')
+ .text(gettext('Turn on closed captioning'));
+ },
+
+ updateCaptioningCookie: function(method) {
+ if (method) {
+ $.cookie('show_closed_captions', 'true', {
+ expires: 3650,
+ path: '/'
+ });
+ } else {
+ $.cookie('show_closed_captions', null, {
+ path: '/'
+ });
+ }
+ },
+
/**
* @desc Shows/Hides captions and updates the cookie.
*
diff --git a/common/test/acceptance/pages/lms/video/video.py b/common/test/acceptance/pages/lms/video/video.py
index e2c61a1b88..cfd84899a2 100644
--- a/common/test/acceptance/pages/lms/video/video.py
+++ b/common/test/acceptance/pages/lms/video/video.py
@@ -16,6 +16,7 @@ log = logging.getLogger('VideoPage')
VIDEO_BUTTONS = {
'transcript': '.lang',
'transcript_button': '.toggle-transcript',
+ 'cc_button': '.toggle-captions',
'volume': '.volume',
'play': '.video_control.play',
'pause': '.video_control.pause',
@@ -28,10 +29,12 @@ VIDEO_BUTTONS = {
}
CSS_CLASS_NAMES = {
- 'closed_captions': '.video.closed',
+ 'captions_closed': '.video.closed',
'captions_rendered': '.video.is-captions-rendered',
'captions': '.subtitles',
- 'captions_text': '.subtitles > li',
+ 'captions_text': '.subtitles li',
+ 'captions_text_getter': '.subtitles li[role="link"][data-index="1"]',
+ 'closed_captions': '.closed-captions',
'error_message': '.video .video-player h3',
'video_container': '.video',
'video_sources': '.video-player video source',
@@ -293,6 +296,18 @@ class VideoPage(PageObject):
"""
self._captions_visibility(False)
+ def show_closed_captions(self):
+ """
+ Make closed captions visible.
+ """
+ self._closed_captions_visibility(True)
+
+ def hide_closed_captions(self):
+ """
+ Make closed captions invisible.
+ """
+ self._closed_captions_visibility(False)
+
def is_captions_visible(self):
"""
Get current visibility sate of captions.
@@ -302,8 +317,20 @@ class VideoPage(PageObject):
"""
self.wait_for_ajax()
- caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
- return not self.q(css=caption_state_selector).present
+ caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['captions'])
+ return self.q(css=caption_state_selector).visible
+
+ def is_closed_captions_visible(self):
+ """
+ Get current visibility sate of closed captions.
+
+ Returns:
+ bool: True means captions are visible, False means captions are not visible
+
+ """
+ self.wait_for_ajax()
+ closed_caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
+ return self.q(css=closed_caption_state_selector).visible
@wait_for_js
def _captions_visibility(self, captions_new_state):
@@ -327,7 +354,24 @@ class VideoPage(PageObject):
# Verify that captions state is toggled/changed
EmptyPromise(lambda: self.is_captions_visible() == captions_new_state,
- "Captions are {state}".format(state=state)).fulfill()
+ "Transcripts are {state}".format(state=state)).fulfill()
+
+ @wait_for_js
+ def _closed_captions_visibility(self, closed_captions_new_state):
+ """
+ Set the video closed captioning visibility state.
+
+ Arguments:
+ closed_captions_new_state (bool): True means show closed captioning
+ """
+ states = {True: 'shown', False: 'hidden'}
+ state = states[closed_captions_new_state]
+
+ self.click_player_button('cc_button')
+
+ # Make sure that the captions are visible
+ EmptyPromise(lambda: self.is_closed_captions_visible() == closed_captions_new_state,
+ "Closed captions are {state}".format(state=state)).fulfill()
@property
def captions_text(self):
@@ -345,6 +389,31 @@ class VideoPage(PageObject):
return ' '.join(subs)
+ @property
+ def closed_captions_text(self):
+ """
+ Extract closed captioning text.
+
+ Returns:
+ str: closed captions Text.
+
+ """
+ self.wait_for_closed_captions()
+
+ closed_captions_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
+ subs = self.q(css=closed_captions_selector).html
+
+ return ' '.join(subs)
+
+ def click_first_line_in_transcript(self):
+ """
+ Clicks a line in the transcript updating the current caption.
+ """
+
+ self.wait_for_captions()
+ captions_selector = self.q(css=CSS_CLASS_NAMES['captions_text_getter'])
+ captions_selector.click()
+
@property
def speed(self):
"""
@@ -494,6 +563,12 @@ class VideoPage(PageObject):
response = requests.get(url, **kwargs)
return response.status_code < 400, response.headers, response.content
+ def get_cookie(self, cookie_name):
+ """
+ Searches for and returns `cookie_name`
+ """
+ return self.browser.get_cookie(cookie_name)
+
def downloaded_transcript_contains_text(self, transcript_format, text_to_search):
"""
Download the transcript in format `transcript_format` and check that it contains the text `text_to_search`
@@ -837,6 +912,20 @@ class VideoPage(PageObject):
captions_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['captions_rendered'])
self.wait_for_element_presence(captions_rendered_selector, 'Captions Rendered')
+ def wait_for_closed_captions(self):
+ """
+ Wait until closed captions are rendered completely.
+ """
+ cc_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
+ self.wait_for_element_visibility(cc_rendered_selector, 'Closed captions rendered')
+
+ def wait_for_closed_captions_to_be_hidden(self):
+ """
+ Waits for the closed captions to be turned off completely.
+ """
+ cc_rendered_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
+ self.wait_for_element_invisibility(cc_rendered_selector, 'Closed captions hidden')
+
def _parse_time_str(time_str):
"""
diff --git a/common/test/acceptance/pages/studio/video/video.py b/common/test/acceptance/pages/studio/video/video.py
index 3c4ae03559..1732cf1b04 100644
--- a/common/test/acceptance/pages/studio/video/video.py
+++ b/common/test/acceptance/pages/studio/video/video.py
@@ -13,11 +13,11 @@ from selenium.webdriver.common.keys import Keys
CLASS_SELECTORS = {
- 'video_container': 'div.video',
+ 'video_container': '.video',
'video_init': '.is-initialized',
'video_xmodule': '.xmodule_VideoModule',
'video_spinner': '.video-wrapper .spinner',
- 'video_controls': 'section.video-controls',
+ 'video_controls': '.video-controls',
'attach_asset': '.upload-dialog > input[type="file"]',
'upload_dialog': '.wrapper-modal-window-assetupload',
'xblock': '.add-xblock-component',
@@ -264,7 +264,7 @@ class VideoComponentPage(VideoPage):
line_number (int): caption line number
"""
- caption_line_selector = ".subtitles > li[data-index='{index}']".format(index=line_number - 1)
+ caption_line_selector = ".subtitles li[data-index='{index}']".format(index=line_number - 1)
self.q(css=caption_line_selector).results[0].send_keys(Keys.ENTER)
def is_caption_line_focused(self, line_number):
@@ -275,7 +275,7 @@ class VideoComponentPage(VideoPage):
line_number (int): caption line number
"""
- caption_line_selector = ".subtitles > li[data-index='{index}']".format(index=line_number - 1)
+ caption_line_selector = ".subtitles li[data-index='{index}']".format(index=line_number - 1)
attributes = self.q(css=caption_line_selector).attrs('class')
return 'focused' in attributes
@@ -504,7 +504,7 @@ class VideoComponentPage(VideoPage):
As all the captions lines are exactly same so only getting partial lines will work.
"""
self.wait_for_captions()
- selector = '.subtitles > li:nth-child({})'
+ selector = '.subtitles li:nth-child({})'
return ' '.join([self.q(css=selector.format(i)).text[0] for i in range(1, 6)])
def set_url_field(self, url, field_number):
diff --git a/common/test/acceptance/tests/video/test_studio_video_editor.py b/common/test/acceptance/tests/video/test_studio_video_editor.py
index ad15488a90..e2764dd21c 100644
--- a/common/test/acceptance/tests/video/test_studio_video_editor.py
+++ b/common/test/acceptance/tests/video/test_studio_video_editor.py
@@ -142,7 +142,7 @@ class VideoEditorTest(CMSVideoBaseTest):
self.open_advanced_tab()
self.video.upload_translation('1mb_transcripts.srt', 'uk')
self.save_unit_settings()
- self.assertTrue(self.video.is_captions_visible())
+ self.video.wait_for(self.video.is_captions_visible, 'Captions are visible', timeout=10)
unicode_text = "Привіт, edX вітає вас.".decode('utf-8')
self.assertIn(unicode_text, self.video.captions_lines())
diff --git a/common/test/acceptance/tests/video/test_studio_video_module.py b/common/test/acceptance/tests/video/test_studio_video_module.py
index 7a693166bb..d38036e064 100644
--- a/common/test/acceptance/tests/video/test_studio_video_module.py
+++ b/common/test/acceptance/tests/video/test_studio_video_module.py
@@ -255,7 +255,6 @@ class CMSVideoTest(CMSVideoBaseTest):
Then when I view the video it does show the captions
"""
self._create_course_unit(subtitles=True)
-
self.assertTrue(self.video.is_captions_visible())
def test_captions_toggling(self):
diff --git a/common/test/acceptance/tests/video/test_video_module.py b/common/test/acceptance/tests/video/test_video_module.py
index db0169685f..979b6d1bef 100644
--- a/common/test/acceptance/tests/video/test_video_module.py
+++ b/common/test/acceptance/tests/video/test_video_module.py
@@ -212,9 +212,9 @@ class YouTubeVideoTest(VideoBaseTest):
# Verify that video has rendered in "Youtube" mode
self.assertTrue(self.video.is_video_rendered('youtube'))
- def test_cc_button_wo_english_transcript(self):
+ def test_transcript_button_wo_english_transcript(self):
"""
- Scenario: CC button works correctly w/o english transcript in Youtube mode
+ Scenario: Transcript button works correctly w/o english transcript in Youtube mode
Given the course has a Video component in "Youtube" mode
And I have defined a non-english transcript for the video
And I have uploaded a non-english transcript file to assets
@@ -226,13 +226,38 @@ class YouTubeVideoTest(VideoBaseTest):
self.navigate_to_video()
self.video.show_captions()
- # Verify that we see "好 各位同学" text in the captions
+ # Verify that we see "好 各位同学" text in the transcript
unicode_text = "好 各位同学".decode('utf-8')
self.assertIn(unicode_text, self.video.captions_text)
- def test_cc_button_transcripts_and_sub_fields_empty(self):
+ def test_cc_button(self):
"""
- Scenario: CC button works correctly if transcripts and sub fields are empty,
+ Scenario: CC button works correctly with transcript in YouTube mode
+ Given the course has a video component in "Youtube" mode
+ And I have defined a transcript for the video
+ Then I see the closed captioning element over the video
+ """
+ data = {'transcripts': {'zh': 'chinese_transcripts.srt'}}
+ self.metadata = self.metadata_for_mode('youtube', data)
+ self.assets.append('chinese_transcripts.srt')
+ self.navigate_to_video()
+
+ # Show captions and make sure they're visible and cookie is set
+ self.video.show_closed_captions()
+ self.video.wait_for_closed_captions()
+ self.assertTrue(self.video.is_closed_captions_visible)
+ self.video.reload_page()
+ self.assertTrue(self.video.is_closed_captions_visible)
+
+ # Hide captions and make sure they're hidden and cookie is unset
+ self.video.hide_closed_captions()
+ self.video.wait_for_closed_captions_to_be_hidden()
+ self.video.reload_page()
+ self.video.wait_for_closed_captions_to_be_hidden()
+
+ def test_transcript_button_transcripts_and_sub_fields_empty(self):
+ """
+ Scenario: Transcript button works correctly if transcripts and sub fields are empty,
but transcript file exists in assets (Youtube mode of Video component)
Given the course has a Video component in "Youtube" mode
And I have uploaded a .srt.sjson file to assets
@@ -247,11 +272,11 @@ class YouTubeVideoTest(VideoBaseTest):
# Verify that we see "Welcome to edX." text in the captions
self.assertIn('Welcome to edX.', self.video.captions_text)
- def test_cc_button_hidden_no_translations(self):
+ def test_transcript_button_hidden_no_translations(self):
"""
- Scenario: CC button is hidden if no translations
+ Scenario: Transcript button is hidden if no translations
Given the course has a Video component in "Youtube" mode
- Then the "CC" button is hidden
+ Then the "Transcript" button is hidden
"""
self.navigate_to_video()
self.assertFalse(self.video.is_button_shown('transcript_button'))
@@ -522,6 +547,16 @@ class YouTubeVideoTest(VideoBaseTest):
timeout=5
)
+ def _verify_closed_caption_text(self, text):
+ """
+ Scenario: returns True if the captions are visible, False is else
+ """
+ self.video.wait_for(
+ lambda: (text in self.video.closed_captions_text),
+ u'Closed captions contain "{}" text'.format(text),
+ timeout=5
+ )
+
def test_video_language_menu_working(self):
"""
Scenario: Language menu works correctly in Video component
@@ -554,6 +589,43 @@ class YouTubeVideoTest(VideoBaseTest):
self.video.select_language('en')
self._verify_caption_text('Welcome to edX.')
+ def test_video_language_menu_working_closed_captions(self):
+ """
+ Scenario: Language menu works correctly in Video component, checks closed captions
+ Given the course has a Video component in "Youtube" mode
+ And I have defined multiple language transcripts for the videos
+ And I make sure captions are closed
+ And I see video menu "language" with correct items
+ And I select language with code "en"
+ Then I see "Welcome to edX." text in the closed captions
+ And I select language with code "zh"
+ Then I see "我们今天要讲的题目是" text in the closed captions
+ """
+ self.assets.extend(['chinese_transcripts.srt', 'subs_3_yD_cEKoCk.srt.sjson'])
+ data = {'transcripts': {"zh": "chinese_transcripts.srt"}, 'sub': '3_yD_cEKoCk'}
+ self.metadata = self.metadata_for_mode('youtube', additional_data=data)
+
+ # go to video
+ self.navigate_to_video()
+ self.video.show_closed_captions()
+
+ correct_languages = {'en': 'English', 'zh': 'Chinese'}
+ self.assertEqual(self.video.caption_languages, correct_languages)
+
+ # we start the video, then pause it to activate the transcript
+ self.video.click_player_button('play')
+ self.video.wait_for_position('0:01')
+ self.video.click_player_button('pause')
+
+ self.video.select_language('en')
+ self.video.click_first_line_in_transcript()
+ self._verify_closed_caption_text('Welcome to edX.')
+
+ self.video.select_language('zh')
+ unicode_text = "我们今天要讲的题目是".decode('utf-8')
+ self.video.click_first_line_in_transcript()
+ self._verify_closed_caption_text(unicode_text)
+
def test_multiple_videos_in_sequentials_load_and_work(self):
"""
Scenario: Multiple videos in sequentials all load and work, switching between sequentials
diff --git a/lms/envs/common.py b/lms/envs/common.py
index ef0528df20..e86ff3a23d 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1257,7 +1257,7 @@ main_vendor_js = base_vendor_js + [
'js/vendor/jquery.ba-bbq.min.js',
'js/vendor/afontgarde/modernizr.fontface-generatedcontent.js',
'js/vendor/afontgarde/afontgarde.js',
- 'js/vendor/afontgarde/edx-icons.js',
+ 'js/vendor/afontgarde/edx-icons.js'
]
# Common files used by both RequireJS code and non-RequireJS code
diff --git a/lms/templates/video.html b/lms/templates/video.html
index 48c069a873..8df4b34895 100644
--- a/lms/templates/video.html
+++ b/lms/templates/video.html
@@ -26,6 +26,7 @@