Commit b883d527 by Chris Rodriguez

Adding closed captions (not draggable)

parent dcc86a3b
......@@ -283,7 +283,7 @@ require.config({
"osda":{
exports: "osda",
deps: ["annotator", "annotator-harvardx", "video.dev", "vjs.youtube", "rangeslider", "share-annotator", "richText-annotator", "reply-annotator", "tags-annotator", "flagging-annotator", "grouping-annotator", "diacritic-annotator", "openseadragon", "jquery-Watch", "catch", "handlebars", "URI"]
},
}
// end of annotation tool files
}
});
......@@ -247,6 +247,22 @@ html:not('.afontgarde') .icon-fallback-img {
}
}
.closed-captions {
position: absolute;
width: 85%;
left: 5%;
top: 70%;
text-align: center;
}
.closed-captions.is-visible {
max-height: ($baseline * 3);
border-radius: ($baseline / 5);
padding: 8px ($baseline / 2) 8px ($baseline * 1.5);
background: rgba(0, 0, 0, .75);
color: $yellow;
}
.video-player {
overflow: hidden;
min-height: 300px;
......@@ -701,39 +717,44 @@ html:not('.afontgarde') .icon-fallback-img {
.subtitles {
@include float(left);
overflow: auto;
margin: 0;
max-height: 460px;
width: flex-grid(3, 9);
padding: 0;
font-size: 14px;
list-style: none;
visibility: visible;
li {
@extend %ui-fake-link;
margin-bottom: 8px;
border: 0;
.subtitles-menu {
height: 100%;
margin: 0;
padding: 0;
color: #0074b5; // AA compliant
line-height: lh();
list-style: none;
&.current {
color: #333;
font-weight: 700;
}
li {
@extend %ui-fake-link;
margin-bottom: 8px;
border: 0;
padding: 0;
color: #0074b5; // AA compliant
line-height: lh();
&.focused {
outline: #000 dotted thin;
outline-offset: -1px;
}
&.current {
color: #333;
font-weight: 700;
}
&:hover,
&:focus {
text-decoration: underline;
}
&.focused {
outline: #000 dotted thin;
outline-offset: -1px;
}
&:hover,
&:focus {
text-decoration: underline;
}
&:empty {
margin-bottom: 0;
&:empty {
margin-bottom: 0;
}
}
}
}
......
......@@ -17,6 +17,7 @@
<div id="id"></div>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div class="slider"></div>
<div>
......
......@@ -48,6 +48,12 @@
});
});
it('adds the captioning control to the video player', function() {
state = jasmine.initializePlayer();
expect($('.video')).toContain('.toggle-captions');
expect($('.video')).toContain('.closed-captions');
});
it('fetch the transcript in HTML5 mode', function () {
runs(function () {
state = jasmine.initializePlayer();
......@@ -122,16 +128,16 @@
it('bind the mouse movement', function () {
state = jasmine.initializePlayer();
expect($('.subtitles')).toHandle('mouseover');
expect($('.subtitles')).toHandle('mouseout');
expect($('.subtitles')).toHandle('mousemove');
expect($('.subtitles')).toHandle('mousewheel');
expect($('.subtitles')).toHandle('DOMMouseScroll');
expect($('.subtitles-menu')).toHandle('mouseover');
expect($('.subtitles-menu')).toHandle('mouseout');
expect($('.subtitles-menu')).toHandle('mousemove');
expect($('.subtitles-menu')).toHandle('mousewheel');
expect($('.subtitles-menu')).toHandle('DOMMouseScroll');
});
it('bind the scroll', function () {
state = jasmine.initializePlayer();
expect($('.subtitles'))
expect($('.subtitles-menu'))
.toHandleWith('scroll', state.videoControl.showControls);
});
......@@ -158,14 +164,61 @@
});
});
describe('renderCaptions', function() {
describe('is rendered', function() {
var KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', { keyCode: key });
};
it('toggles the captions on control click', function() {
state = jasmine.initializePlayer();
$('.toggle-captions').click();
expect($('.toggle-captions')).toHaveClass('is-active');
expect($('.closed-captions')).toHaveClass('is-visible');
$('.toggle-captions').click();
expect($('.toggle-captions')).not.toHaveClass('is-active');
expect($('.closed-captions')).not.toHaveClass('is-visible');
});
it('toggles the captions on keypress ENTER', function() {
state = jasmine.initializePlayer();
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.ENTER));
expect($('.toggle-captions')).toHaveClass('is-active');
expect($('.closed-captions')).toHaveClass('is-visible');
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.ENTER));
expect($('.toggle-captions')).not.toHaveClass('is-active');
expect($('.closed-captions')).not.toHaveClass('is-visible');
});
it('toggles the captions on keypress SPACE', function() {
state = jasmine.initializePlayer();
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.SPACE));
expect($('.toggle-captions')).toHaveClass('is-active');
expect($('.closed-captions')).toHaveClass('is-visible');
$('.toggle-captions').focus().trigger(keyPressEvent(KEY.SPACE));
expect($('.toggle-captions')).not.toHaveClass('is-active');
expect($('.closed-captions')).not.toHaveClass('is-visible');
});
});
});
describe('renderLanguageMenu', function () {
describe('is rendered', function () {
var KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', { keyCode: key });
};
keyPressEvent = function(key) {
return $.Event('keydown', { keyCode: key });
};
it('if languages more than 1', function () {
state = jasmine.initializePlayer();
......@@ -364,9 +417,8 @@
});
it('show explanation message', function () {
expect($('.subtitles li')).toHaveHtml(
'Caption will be displayed when you start playing ' +
'the video.'
expect($('.subtitles-menu li')).toHaveHtml(
'Transcript will be displayed when you start playing the video.'
);
});
......@@ -444,7 +496,7 @@
runs(function () {
$(window).trigger(jQuery.Event('mousemove'));
jasmine.Clock.tick(state.config.captionsFreezeTime);
$('.subtitles').trigger(jQuery.Event('mouseenter'));
$('.subtitles-menu').trigger(jQuery.Event('mouseenter'));
jasmine.Clock.tick(state.config.captionsFreezeTime);
});
});
......@@ -459,7 +511,7 @@
describe('when the cursor is moving', function () {
it('reset the freezing timeout', function () {
runs(function () {
$('.subtitles').trigger(jQuery.Event('mousemove'));
$('.subtitles-menu').trigger(jQuery.Event('mousemove'));
expect(window.clearTimeout).toHaveBeenCalled();
});
});
......@@ -468,7 +520,7 @@
describe('when the mouse is scrolling', function () {
it('reset the freezing timeout', function () {
runs(function () {
$('.subtitles').trigger(jQuery.Event('mousewheel'));
$('.subtitles-menu').trigger(jQuery.Event('mousewheel'));
expect(window.clearTimeout).toHaveBeenCalled();
});
});
......@@ -486,7 +538,7 @@
describe('always', function () {
beforeEach(function () {
$('.subtitles').trigger(jQuery.Event('mouseout'));
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
});
it('reset the freezing timeout', function () {
......@@ -501,9 +553,9 @@
describe('when the player is playing', function () {
beforeEach(function () {
state.videoCaption.playing = true;
$('.subtitles li[data-index]:first')
$('.subtitles-menu li[data-index]:first')
.addClass('current');
$('.subtitles').trigger(jQuery.Event('mouseout'));
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
});
it('scroll the transcript', function () {
......@@ -514,7 +566,7 @@
describe('when the player is not playing', function () {
beforeEach(function () {
state.videoCaption.playing = false;
$('.subtitles').trigger(jQuery.Event('mouseout'));
$('.subtitles-menu').trigger(jQuery.Event('mouseout'));
});
it('does not scroll the transcript', function () {
......
......@@ -92,7 +92,8 @@ function (VideoPlayer) {
showinfo: 0,
enablejsapi: 1,
modestbranding: 1,
html5: 1
html5: 1,
cc_load_policy: 0
},
videoId: 'cogebirgzzM',
events: events
......@@ -118,7 +119,8 @@ function (VideoPlayer) {
rel: 0,
showinfo: 0,
enablejsapi: 1,
modestbranding: 1
modestbranding: 1,
cc_load_policy: 0
},
videoId: 'abcdefghijkl',
events: jasmine.any(Object)
......
......@@ -139,7 +139,8 @@ function (HTML5Video, Resizer) {
rel: 0,
showinfo: 0,
enablejsapi: 1,
modestbranding: 1
modestbranding: 1,
cc_load_policy: 0
};
if (!state.isFlashMode()) {
......
......@@ -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):
......@@ -346,6 +390,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):
"""
Get current video speed value.
......@@ -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):
"""
......
......@@ -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):
......
......@@ -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())
......
......@@ -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):
......
......@@ -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 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
"""
Scenario: CC button works correctly if transcripts and sub fields are empty,
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
......
......@@ -1284,7 +1284,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
......
......@@ -26,6 +26,7 @@
<h3 class="hidden">${_('No playable video sources found.')}</h3>
</section>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<section class="video-controls is-hidden">
<div>
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment