diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c958366b7d..00ab686c20 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
+Blades: Allow multiple transcripts with video. BLD-642.
+
CMS: Add feature to allow exporting a course to a git repository by
specifying the giturl in the course settings.
diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
index b194e356bd..9a590572a3 100644
--- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
+++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
@@ -145,7 +145,7 @@ def verify_setting_entry(setting, display_name, value, explicitly_set):
# Check if the web object is a list type
# If so, we use a slightly different mechanism for determining its value
- if setting.has_class('metadata-list-enum'):
+ if setting.has_class('metadata-list-enum') or setting.has_class('metadata-dict'):
list_value = ', '.join(ele.value for ele in setting.find_by_css('.list-settings-item'))
assert_equal(value, list_value)
elif setting.has_class('metadata-videolist-enum'):
diff --git a/cms/djangoapps/contentstore/features/transcripts.feature b/cms/djangoapps/contentstore/features/transcripts.feature
index 72f2beff3a..bd0a6efc1b 100644
--- a/cms/djangoapps/contentstore/features/transcripts.feature
+++ b/cms/djangoapps/contentstore/features/transcripts.feature
@@ -1,6 +1,6 @@
@shard_3
-Feature: CMS.Transcripts
- As a course author, I want to be able to create video components.
+Feature: CMS Transcripts
+ As a course author, I want to be able to create video components
# For transcripts acceptance tests there are 3 available caption
# files. They can be used to test various transcripts features. Two of
@@ -72,7 +72,7 @@ Feature: CMS.Transcripts
And I remove "t_not_exist" transcripts id from store
And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I see status message "not found"
- And I see value "" in the field "HTML5 Transcript"
+ And I see value "" in the field "Transcript (primary)"
# Import: w/o local but with server subs
And I remove "t__eq_exist" transcripts id from store
@@ -83,7 +83,7 @@ Feature: CMS.Transcripts
Then I see status message "found"
And I see button "upload_new_timed_transcripts"
And I see button "download_to_edit"
- And I see value "t__eq_exist" in the field "HTML5 Transcript"
+ And I see value "t__eq_exist" in the field "Transcript (primary)"
#4
Scenario: Youtube id only: check "Found" state
@@ -92,7 +92,7 @@ Feature: CMS.Transcripts
And I enter a "http://youtu.be/t_not_exist" source to field number 1
Then I see status message "found"
- And I see value "t_not_exist" in the field "HTML5 Transcript"
+ And I see value "t_not_exist" in the field "Transcript (primary)"
#5
Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are equal
@@ -102,7 +102,7 @@ Feature: CMS.Transcripts
And I enter a "http://youtu.be/t__eq_exist" source to field number 1
And I see status message "found"
- And I see value "t__eq_exist" in the field "HTML5 Transcript"
+ And I see value "t__eq_exist" in the field "Transcript (primary)"
#6
Scenario: Youtube id only: check "Found" state when user sets youtube_id with local and server subs and they are not equal
@@ -114,7 +114,7 @@ Feature: CMS.Transcripts
And I see button "replace"
And I click transcript button "replace"
And I see status message "found"
- And I see value "t_neq_exist" in the field "HTML5 Transcript"
+ And I see value "t_neq_exist" in the field "Transcript (primary)"
#7
Scenario: html5 source only: check "Not Found" state
@@ -123,7 +123,7 @@ Feature: CMS.Transcripts
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "not found"
- And I see value "" in the field "HTML5 Transcript"
+ And I see value "" in the field "Transcript (primary)"
#8
Scenario: html5 source only: check "Found" state
@@ -132,7 +132,7 @@ Feature: CMS.Transcripts
And I enter a "t_not_exist.mp4" source to field number 1
Then I see status message "found"
- And I see value "t_not_exist" in the field "HTML5 Transcript"
+ And I see value "t_not_exist" in the field "Transcript (primary)"
#9
Scenario: User sets youtube_id w/o server but with local subs and one html5 link w/o subs
@@ -144,7 +144,7 @@ Feature: CMS.Transcripts
And I enter a "test_video_name.mp4" source to field number 2
Then I see status message "found"
- And I see value "t_not_exist" in the field "HTML5 Transcript"
+ And I see value "t_not_exist" in the field "Transcript (primary)"
# Disabled 1/29/14 due to flakiness observed in master
#10
@@ -160,7 +160,7 @@ Feature: CMS.Transcripts
#
# And I enter a "t_not_exist.mp4" source to field number 2
# Then I see status message "found"
- # And I see value "t__eq_exist" in the field "HTML5 Transcript"
+ # And I see value "t__eq_exist" in the field "Transcript (primary)"
#11
Scenario: User sets youtube_id w/o local but with server subs and one html5 link w/o transcripts w/o import action, then another one html5 link w/o transcripts
@@ -338,7 +338,7 @@ Feature: CMS.Transcripts
Then I see status message "uploaded_successfully"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
- And I see value "t__eq_exist" in the field "HTML5 Transcript"
+ And I see value "t__eq_exist" in the field "Transcript (primary)"
And I enter a "http://youtu.be/t_not_exist" source to field number 2
Then I see status message "found"
@@ -359,7 +359,7 @@ Feature: CMS.Transcripts
And I see button "upload_new_timed_transcripts"
And I upload the transcripts file "test_transcripts.srt"
Then I see status message "uploaded_successfully"
- And I see value "test_transcripts" in the field "HTML5 Transcript"
+ And I see value "test_transcripts" in the field "Transcript (primary)"
And I enter a "t_not_exist.webm" source to field number 2
Then I see status message "replace"
@@ -367,7 +367,7 @@ Feature: CMS.Transcripts
And I see choose button "test_transcripts.mp4" number 1
And I see choose button "t_not_exist.webm" number 2
And I click transcript button "choose" number 2
- And I see value "test_transcripts|t_not_exist" in the field "HTML5 Transcript"
+ And I see value "test_transcripts|t_not_exist" in the field "Transcript (primary)"
#21
Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save - > change it to another one HTML5 source w/o transcripts - click on use existing - > change it to another one HTML5 source w/o transcripts - click on use existing
@@ -378,7 +378,7 @@ Feature: CMS.Transcripts
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
- And I see value "t_not_exist" in the field "HTML5 Transcript"
+ And I see value "t_not_exist" in the field "Transcript (primary)"
And I save changes
And I edit the component
@@ -387,13 +387,13 @@ Feature: CMS.Transcripts
Then I see status message "use existing"
And I see button "use_existing"
And I click transcript button "use_existing"
- And I see value "video_name_2" in the field "HTML5 Transcript"
+ And I see value "video_name_2" in the field "Transcript (primary)"
And I enter a "video_name_3.mp4" source to field number 1
Then I see status message "use existing"
And I see button "use_existing"
And I click transcript button "use_existing"
- And I see value "video_name_3" in the field "HTML5 Transcript"
+ And I see value "video_name_3" in the field "Transcript (primary)"
#22
Scenario: Work with 1 field only: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - click on use existing -> change it to another one HTML5 source w/o transcripts - do not click on use existing -> change it to another one HTML5 source w/o transcripts - click on use existing
@@ -404,7 +404,7 @@ Feature: CMS.Transcripts
Then I see status message "found"
And I see button "download_to_edit"
And I see button "upload_new_timed_transcripts"
- And I see value "t_not_exist" in the field "HTML5 Transcript"
+ And I see value "t_not_exist" in the field "Transcript (primary)"
And I save changes
And I edit the component
@@ -413,7 +413,7 @@ Feature: CMS.Transcripts
Then I see status message "use existing"
And I see button "use_existing"
And I click transcript button "use_existing"
- And I see value "video_name_2" in the field "HTML5 Transcript"
+ And I see value "video_name_2" in the field "Transcript (primary)"
And I enter a "video_name_3.mp4" source to field number 1
Then I see status message "use existing"
@@ -423,7 +423,7 @@ Feature: CMS.Transcripts
Then I see status message "use existing"
And I see button "use_existing"
And I click transcript button "use_existing"
- And I see value "video_name_4" in the field "HTML5 Transcript"
+ And I see value "video_name_4" in the field "Transcript (primary)"
#23
Scenario: Work with 2 fields: Enter HTML5 source with transcripts - save -> change it to another one HTML5 source w/o transcripts - do not click on use existing -> add another one HTML5 source w/o transcripts - click on use existing
@@ -446,7 +446,7 @@ Feature: CMS.Transcripts
Then I see status message "use existing"
And I see button "use_existing"
And I click transcript button "use_existing"
- And I see value "video_name_2|video_name_3" in the field "HTML5 Transcript"
+ And I see value "video_name_2|video_name_3" in the field "Transcript (primary)"
#24 Uploading subtitles with different file name than file
Scenario: File name and name of subs are different
@@ -457,7 +457,7 @@ Feature: CMS.Transcripts
And I see status message "not found"
And I upload the transcripts file "test_transcripts.srt"
Then I see status message "uploaded_successfully"
- And I see value "video_name_1" in the field "HTML5 Transcript"
+ And I see value "video_name_1" in the field "Transcript (primary)"
And I save changes
Then when I view the video it does show the captions
@@ -488,14 +488,14 @@ Feature: CMS.Transcripts
And I see status message "not found"
And I upload the transcripts file "test_transcripts.srt"
Then I see status message "uploaded_successfully"
- And I see value "video_name_1|video_name_2" in the field "HTML5 Transcript"
+ And I see value "video_name_1|video_name_2" in the field "Transcript (primary)"
And I clear field number 1
Then I see status message "found"
- And I see value "video_name_2" in the field "HTML5 Transcript"
+ And I see value "video_name_2" in the field "Transcript (primary)"
#27
- Scenario: Upload button for single youtube id.
+ Scenario: Upload button for single youtube id
Given I have created a Video component
And I edit the component
@@ -512,7 +512,7 @@ Feature: CMS.Transcripts
Then I see status message "found"
#28
- Scenario: Upload button for youtube id with html5 ids.
+ Scenario: Upload button for youtube id with html5 ids
Given I have created a Video component
And I edit the component
@@ -528,7 +528,7 @@ Feature: CMS.Transcripts
Then I see status message "uploaded_successfully"
And I clear field number 1
Then I see status message "found"
- And I see value "video_name_1" in the field "HTML5 Transcript"
+ And I see value "video_name_1" in the field "Transcript (primary)"
And I save changes
Then when I view the video it does show the captions
@@ -544,14 +544,14 @@ Feature: CMS.Transcripts
Then I see status message "not found"
And I open tab "Advanced"
- And I set value "t_not_exist" to the field "HTML5 Transcript"
+ And I set value "t_not_exist" to the field "Transcript (primary)"
And I save changes
Then when I view the video it does show the captions
And I edit the component
Then I see status message "found"
- And I see value "video_name_1" in the field "HTML5 Transcript"
+ And I see value "video_name_1" in the field "Transcript (primary)"
#30
Scenario: Check non-ascii (chinise) transcripts
@@ -576,7 +576,7 @@ Feature: CMS.Transcripts
Then I see status message "not found"
And I open tab "Advanced"
- And I set value "t_not_exist" to the field "HTML5 Transcript"
+ And I set value "t_not_exist" to the field "Transcript (primary)"
And I open tab "Basic"
Then I see status message "found"
@@ -585,18 +585,20 @@ Feature: CMS.Transcripts
And I edit the component
Then I see status message "found"
- And I see value "video_name_1" in the field "HTML5 Transcript"
+ And I see value "video_name_1" in the field "Transcript (primary)"
#32
Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible w/o saving
- Given I have created a Video component with subtitles "t_not_exist"
+ Given I have created a Video component
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
- Then I see status message "found"
+ Then I see status message "not found"
+ And I upload the transcripts file "chinese_transcripts.srt"
+ Then I see status message "uploaded_successfully"
And I open tab "Advanced"
- And I set value "" to the field "HTML5 Transcript"
+ And I set value "" to the field "Transcript (primary)"
And I open tab "Basic"
Then I see status message "not found"
@@ -605,21 +607,24 @@ Feature: CMS.Transcripts
And I edit the component
Then I see status message "not found"
- And I see value "" in the field "HTML5 Transcript"
+ And I see value "" in the field "Transcript (primary)"
#33
Scenario: After clearing Transcripts field in the Advanced tab "not found" message should be visible with saving
- Given I have created a Video component with subtitles "t_not_exist"
+ Given I have created a Video component
And I edit the component
And I enter a "t_not_exist.mp4" source to field number 1
- Then I see status message "found"
+ Then I see status message "not found"
+ And I upload the transcripts file "chinese_transcripts.srt"
+ Then I see status message "uploaded_successfully"
And I save changes
+ Then I see "好 各位同学" text in the captions
And I edit the component
And I open tab "Advanced"
- And I set value "" to the field "HTML5 Transcript"
+ And I set value "" to the field "Transcript (primary)"
And I open tab "Basic"
Then I see status message "not found"
@@ -628,7 +633,7 @@ Feature: CMS.Transcripts
And I edit the component
Then I see status message "not found"
- And I see value "" in the field "HTML5 Transcript"
+ And I see value "" in the field "Transcript (primary)"
#34
Scenario: Video with existing subs - Advanced tab - change to another one subs - Basic tab - Found message - Save - see correct subs
@@ -647,7 +652,7 @@ Feature: CMS.Transcripts
And I edit the component
And I open tab "Advanced"
- And I set value "t_not_exist" to the field "HTML5 Transcript"
+ And I set value "t_not_exist" to the field "Transcript (primary)"
And I open tab "Basic"
Then I see status message "found"
@@ -670,7 +675,7 @@ Feature: CMS.Transcripts
And I edit the component
And I open tab "Advanced"
- And I revert the transcript field "HTML5 Transcript"
+ And I revert the transcript field "Transcript (primary)"
And I save changes
Then when I view the video it does not show the captions
@@ -686,7 +691,7 @@ Feature: CMS.Transcripts
And I see status message "not found"
And I upload the transcripts file "test_transcripts.srt"
Then I see status message "uploaded_successfully"
- And I see value "video_name_1.1.2" in the field "HTML5 Transcript"
+ And I see value "video_name_1.1.2" in the field "Transcript (primary)"
And I save changes
Then when I view the video it does show the captions
diff --git a/cms/djangoapps/contentstore/features/transcripts.py b/cms/djangoapps/contentstore/features/transcripts.py
index 695c96c8b8..41903afccf 100644
--- a/cms/djangoapps/contentstore/features/transcripts.py
+++ b/cms/djangoapps/contentstore/features/transcripts.py
@@ -103,7 +103,7 @@ def i_do_not_see_error_message(_step):
@step('I see error message "([^"]*)"$')
def i_see_error_message(_step, error):
- assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error.strip()])
+ assert world.css_has_text(SELECTORS['error_bar'], ERROR_MESSAGES[error])
@step('I do not see status message$')
@@ -114,7 +114,7 @@ def i_do_not_see_status_message(_step):
@step('I see status message "([^"]*)"$')
def i_see_status_message(_step, status):
assert not world.css_visible(SELECTORS['error_bar'])
- assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status.strip()])
+ assert world.css_has_text(SELECTORS['status_bar'], STATUSES[status])
DOWNLOAD_BUTTON = TRANSCRIPTS_BUTTONS["download_to_edit"][0]
if world.is_css_present(DOWNLOAD_BUTTON, wait_time=1) \
diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature
index 07c298b0d2..1c12915e91 100644
--- a/cms/djangoapps/contentstore/features/video-editor.feature
+++ b/cms/djangoapps/contentstore/features/video-editor.feature
@@ -1,6 +1,6 @@
@shard_3
-Feature: CMS.Video Component Editor
- As a course author, I want to be able to create video components.
+Feature: CMS Video Component Editor
+ As a course author, I want to be able to create video components
Scenario: User can view Video metadata
Given I have created a Video component
@@ -17,14 +17,14 @@ Feature: CMS.Video Component Editor
# Sauce Labs cannot delete cookies
@skip_sauce
- Scenario: Captions are hidden when "show captions" is false
+ Scenario: Captions are hidden when "transcript display" is false
Given I have created a Video component with subtitles
- And I have set "show transcript" to False
+ And I have set "transcript display" to False
Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies
@skip_sauce
- Scenario: Captions are shown when "show captions" is true
+ Scenario: Captions are shown when "transcript display" is true
Given I have created a Video component with subtitles
- And I have set "show transcript" to True
+ And I have set "transcript display" to True
Then when I view the video it does show the captions
diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py
index b8c74be80a..ea90993d67 100644
--- a/cms/djangoapps/contentstore/features/video-editor.py
+++ b/cms/djangoapps/contentstore/features/video-editor.py
@@ -5,7 +5,7 @@ from lettuce import world, step
from terrain.steps import reload_the_page
-@step('I have set "show transcript" to (.*)$')
+@step('I have set "transcript display" to (.*)$')
def set_show_captions(step, setting):
# Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions')
@@ -13,7 +13,7 @@ def set_show_captions(step, setting):
world.css_click('a.edit-button')
world.wait_for(lambda _driver: world.css_visible('a.save-button'))
world.click_link_by_text('Advanced')
- world.browser.select('Show Transcript', setting)
+ world.browser.select('Transcript Display', setting)
world.css_click('a.save-button')
@@ -42,10 +42,11 @@ def correct_video_settings(_step):
['Display Name', 'Video', False],
['Download Transcript', '', False],
['End Time', '00:00:00', False],
- ['HTML5 Transcript', '', False],
- ['Show Transcript', 'True', False],
['Start Time', '00:00:00', False],
+ ['Transcript (primary)', '', False],
+ ['Transcript Display', 'True', False],
['Transcript Download Allowed', 'False', False],
+ ['Transcript Translations', '', False],
['Video Download Allowed', 'False', False],
['Video Sources', '', False],
['Youtube ID', 'OEoXaMPEzfM', False],
diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature
index 2ead8987e9..fc70ee3c14 100644
--- a/cms/djangoapps/contentstore/features/video.feature
+++ b/cms/djangoapps/contentstore/features/video.feature
@@ -1,6 +1,6 @@
@shard_3
-Feature: CMS.Video Component
- As a course author, I want to be able to view my created videos in Studio.
+Feature: CMS Video Component
+ As a course author, I want to be able to view my created videos in Studio
# 1
# Video Alpha Features will work in Firefox only when Firefox is the active window
@@ -43,38 +43,6 @@ Feature: CMS.Video Component
Then the correct Youtube video is shown
# 7
- Scenario: Closed captions become visible when the mouse hovers over CC button
- Given I have created a Video component with subtitles
- And Make sure captions are closed
- Then Captions become "invisible"
- And I hover over button "CC"
- Then Captions become "visible"
- And I hover over button "volume"
- Then Captions become "invisible"
-
- # 8
- # Disabled 11/26 due to flakiness in master.
- # Enabled back on 11/29.
- Scenario: Open captions never become invisible
- Given I have created a Video component with subtitles
- And Make sure captions are open
- Then Captions are "visible"
- And I hover over button "CC"
- Then Captions are "visible"
- And I hover over button "volume"
- Then Captions are "visible"
-
- # 9
- # Disabled 11/26 due to flakiness in master.
- # Enabled back on 11/29.
- Scenario: Closed captions are invisible when mouse doesn't hover on CC button
- Given I have created a Video component with subtitles
- And Make sure captions are closed
- Then Captions become "invisible"
- And I hover over button "volume"
- Then Captions are "invisible"
-
- # 10
# Disabled 11/26 due to flakiness in master.
# Enabled back on 11/29.
Scenario: When enter key is pressed on a caption shows an outline around it
@@ -84,7 +52,7 @@ Feature: CMS.Video Component
Then I press "enter" button on caption line with data-index "0"
And I see caption line with data-index "0" has class "focused"
- # 11
+ # 8
Scenario: When start end end times are specified, a range on slider is shown
Given I have created a Video component with subtitles
And Make sure captions are closed
diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py
index 2dc4bccfb3..b3777aa57d 100644
--- a/cms/djangoapps/contentstore/features/video.py
+++ b/cms/djangoapps/contentstore/features/video.py
@@ -56,6 +56,13 @@ def i_created_a_video_with_subs_with_name(_step, sub_id):
world.visit(video_url)
world.wait_for_xmodule()
+
+ # update .sub filed with proper subs name (which mimics real Studio/XML behavior)
+ # this is needed only for that videos which are created in acceptance tests.
+ _step.given('I edit the component')
+ world.wait_for_ajax_complete()
+ _step.given('I save changes')
+
world.disable_jquery_animations()
world.wait_for_present('.is-initialized')
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index c19c4d198c..8d44b98d95 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -492,15 +492,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, 'i4x://edX/toy/video/video_with_end_time')
self.assertContains(resp, 'i4x://edX/toy/poll_question/T1_changemind_poll_foo_2')
- def test_video_module_caption_asset_path(self):
- """
- This verifies that a video caption url is as we expect it to be
- """
- resp = self._test_preview(Location('i4x', 'edX', 'toy', 'video', 'sample_video', None))
- self.assertEquals(resp.status_code, 200)
- content = json.loads(resp.content)
- self.assertIn('data-caption-asset-path="/c4x/edX/toy/asset/subs_"', content['html'])
-
def _test_preview(self, location):
""" Preview test case. """
direct_store = modulestore('direct')
diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
index a34927e3ef..c0c179abb8 100644
--- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py
@@ -3,11 +3,13 @@ import unittest
from uuid import uuid4
import copy
import textwrap
+from mock import patch, Mock
from pymongo import MongoClient
from django.test.utils import override_settings
from django.conf import settings
+from django.utils import translation
from nose.plugins.skip import SkipTest
@@ -16,7 +18,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.exceptions import NotFoundError
from xmodule.contentstore.django import contentstore, _CONTENTSTORE
-from contentstore import transcripts_utils
+from xmodule.video_module import transcripts_utils
from contentstore.tests.modulestore_config import TEST_MODULESTORE
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
@@ -188,20 +190,29 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
def test_success_downloading_subs(self):
- # Disabled 11/14/13
- # This test is flakey because it performs an HTTP request on an external service
- # Re-enable when `requests.get` is patched using `mock.patch`
- raise SkipTest
-
+ response = textwrap.dedent("""
+
+
+ Test text 1.
+ Test text 2.
+ Test text 3.
+
+ """)
good_youtube_subs = {
- 0.5: 'JMD_ifUUfsU',
- 1.0: 'hI10vDNYz4M',
- 2.0: 'AKqURZnYqpk'
+ 0.5: 'good_id_1',
+ 1.0: 'good_id_2',
+ 2.0: 'good_id_3'
}
self.clear_subs_content(good_youtube_subs)
- # Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
- transcripts_utils.download_youtube_subs(good_youtube_subs, self.course)
+ with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get:
+ mock_get.return_value = Mock(status_code=200, text=response, content=response)
+ # Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
+ transcripts_utils.download_youtube_subs(good_youtube_subs, self.course, settings)
+
+ mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_1'})
+ mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_2'})
+ mock_get.assert_any_call('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_id_3'})
# Check assets status after importing subtitles.
for subs_id in good_youtube_subs.values():
@@ -226,12 +237,10 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
self.assertEqual(html5_ids[2], 'baz.1.4')
self.assertEqual(html5_ids[3], 'foo')
- def test_fail_downloading_subs(self):
+ @patch('xmodule.video_module.transcripts_utils.requests.get')
+ def test_fail_downloading_subs(self, mock_get):
- # Disabled 11/14/13
- # This test is flakey because it performs an HTTP request on an external service
- # Re-enable when `requests.get` is patched using `mock.patch`
- raise SkipTest
+ mock_get.return_value = Mock(status_code=404, text='Error 404')
bad_youtube_subs = {
0.5: 'BAD_YOUTUBE_ID1',
@@ -239,9 +248,8 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
2.0: 'BAD_YOUTUBE_ID3'
}
self.clear_subs_content(bad_youtube_subs)
-
with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
- transcripts_utils.download_youtube_subs(bad_youtube_subs, self.course)
+ transcripts_utils.download_youtube_subs(bad_youtube_subs, self.course, settings)
# Check assets status after importing subtitles.
for subs_id in bad_youtube_subs.values():
@@ -267,7 +275,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
self.clear_subs_content(good_youtube_subs)
# Check transcripts_utils.GetTranscriptsFromYouTubeException not thrown
- transcripts_utils.download_youtube_subs(good_youtube_subs, self.course)
+ transcripts_utils.download_youtube_subs(good_youtube_subs, self.course, settings)
# Check assets status after importing subtitles.
for subs_id in good_youtube_subs.values():
@@ -438,3 +446,43 @@ class TestGenerateSrtFromSjson(TestDownloadYoutubeSubs):
}
srt_subs = transcripts_utils.generate_srt_from_sjson(sjson_subs, 1)
self.assertFalse(srt_subs)
+
+
+class TestYoutubeTranscripts(unittest.TestCase):
+ """
+ Tests for checking right datastructure returning when using youtube api.
+ """
+ @patch('xmodule.video_module.transcripts_utils.requests.get')
+ def test_youtube_bad_status_code(self, mock_get):
+ mock_get.return_value = Mock(status_code=404, text='test')
+ youtube_id = 'bad_youtube_id'
+ with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
+ transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
+
+ @patch('xmodule.video_module.transcripts_utils.requests.get')
+ def test_youtube_empty_text(self, mock_get):
+ mock_get.return_value = Mock(status_code=200, text='')
+ youtube_id = 'bad_youtube_id'
+ with self.assertRaises(transcripts_utils.GetTranscriptsFromYouTubeException):
+ transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
+
+ def test_youtube_good_result(self):
+ response = textwrap.dedent("""
+
+
+ Test text 1.
+ Test text 2.
+ Test text 3.
+
+ """)
+ expected_transcripts = {
+ 'start': [270, 2720, 5430],
+ 'end': [2720, 2720, 7160],
+ 'text': ['Test text 1.', 'Test text 2.', 'Test text 3.']
+ }
+ youtube_id = 'good_youtube_id'
+ with patch('xmodule.video_module.transcripts_utils.requests.get') as mock_get:
+ mock_get.return_value = Mock(status_code=200, text=response, content=response)
+ transcripts = transcripts_utils.get_transcripts_from_youtube(youtube_id, settings, translation)
+ self.assertEqual(transcripts, expected_transcripts)
+ mock_get.assert_called_with('http://video.google.com/timedtext', params={'lang': 'en', 'v': 'good_youtube_id'})
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 80548a27a7..4945a0e6f2 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -24,12 +24,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore import Location
+from xmodule.video_module import manage_video_subtitles_save
from util.json_request import expect_json, JsonResponse
from util.string_utils import str_to_bool
-from ..transcripts_utils import manage_video_subtitles_save
-
from ..utils import get_modulestore
from .access import has_course_access
@@ -251,6 +250,8 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location: " + str(item_location)}, 404)
+ old_metadata = own_metadata(existing_item)
+
if publish:
if publish == 'make_private':
_xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
@@ -299,7 +300,7 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
field.write_to(existing_item, value)
if existing_item.category == 'video':
- manage_video_subtitles_save(existing_item, existing_item, request.user)
+ manage_video_subtitles_save(existing_item, request.user, old_metadata, generate_translation=True)
# commit to datastore
store.update_item(existing_item, request.user.id)
diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py
index f92e8e7f21..9722bbfa06 100644
--- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py
+++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py
@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from django.conf import settings
-from contentstore import transcripts_utils
+from xmodule.video_module import transcripts_utils
from contentstore.tests.utils import CourseTestCase
from cache_toolbox.core import del_cached_content
from xmodule.modulestore.django import modulestore
diff --git a/cms/djangoapps/contentstore/views/transcripts_ajax.py b/cms/djangoapps/contentstore/views/transcripts_ajax.py
index 01535f6021..d30e2a29c4 100644
--- a/cms/djangoapps/contentstore/views/transcripts_ajax.py
+++ b/cms/djangoapps/contentstore/views/transcripts_ajax.py
@@ -26,12 +26,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationErr
from util.json_request import JsonResponse
from xmodule.modulestore.locator import BlockUsageLocator
-from ..transcripts_utils import (
+from xmodule.video_module.transcripts_utils import (
generate_subs_from_source,
generate_srt_from_sjson, remove_subs_from_store,
download_youtube_subs, get_transcripts_from_youtube,
copy_or_rename_transcript,
- save_module,
manage_video_subtitles_save,
TranscriptsGenerationException,
GetTranscriptsFromYouTubeException,
@@ -136,7 +135,7 @@ def upload_transcripts(request):
return error_response(response, "Can't find transcripts in storage for {}".format(sub_attr))
item.sub = selected_name # write one of new subtitles names to item.sub attribute.
- save_module(item, request.user)
+ item.save_with_metadata(request.user)
response['subs'] = item.sub
response['status'] = 'Success'
else:
@@ -272,7 +271,11 @@ def check_transcripts(request):
#check youtube local and server transcripts for equality
if transcripts_presence['youtube_server'] and transcripts_presence['youtube_local']:
try:
- youtube_server_subs = get_transcripts_from_youtube(youtube_id)
+ youtube_server_subs = get_transcripts_from_youtube(
+ youtube_id,
+ settings,
+ item.runtime.service(item, "i18n")
+ )
if json.loads(local_transcripts) == youtube_server_subs: # check transcripts for equality
transcripts_presence['youtube_diff'] = False
except GetTranscriptsFromYouTubeException:
@@ -389,7 +392,7 @@ def choose_transcripts(request):
if item.sub != html5_id: # update sub value
item.sub = html5_id
- save_module(item, request.user)
+ item.save_with_metadata(request.user)
response = {'status': 'Success', 'subs': item.sub}
return JsonResponse(response)
@@ -415,12 +418,12 @@ def replace_transcripts(request):
return error_response(response, 'YouTube id {} is not presented in request data.'.format(youtube_id))
try:
- download_youtube_subs({1.0: youtube_id}, item)
+ download_youtube_subs({1.0: youtube_id}, item, settings)
except GetTranscriptsFromYouTubeException as e:
return error_response(response, e.message)
item.sub = youtube_id
- save_module(item, request.user)
+ item.save_with_metadata(request.user)
response = {'status': 'Success', 'subs': item.sub}
return JsonResponse(response)
@@ -519,10 +522,10 @@ def save_transcripts(request):
for metadata_key, value in metadata.items():
setattr(item, metadata_key, value)
- save_module(item, request.user) # item becomes updated with new values
+ item.save_with_metadata(request.user) # item becomes updated with new values
if new_sub:
- manage_video_subtitles_save(None, item, request.user)
+ manage_video_subtitles_save(item, request.user)
else:
# If `new_sub` is empty, it means that user explicitly does not want to use
# transcripts for current video ids and we remove all transcripts from storage.
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 23b7de10a5..8b61901327 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -26,7 +26,9 @@ Longer TODO:
import sys
import lms.envs.common
-from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, enable_microsites
+from lms.envs.common import (
+ USE_TZ, TECH_SUPPORT_EMAIL, PLATFORM_NAME, BUGS_EMAIL, DOC_STORE_CONFIG, enable_microsites, ALL_LANGUAGES
+)
from path import path
from lms.lib.xblock.mixin import LmsBlockMixin
diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss
index 5f4e9d1063..5fc6486698 100644
--- a/common/lib/xmodule/xmodule/css/video/display.scss
+++ b/common/lib/xmodule/xmodule/css/video/display.scss
@@ -164,6 +164,37 @@ div.video {
}
}
+ %video-button {
+ @include transition(none);
+ display: block;
+ font-weight: 800;
+ line-height: 46px;
+ margin: 0;
+ padding: 0 0 0 15px;
+ text-indent: -9999px;
+ -webkit-font-smoothing: antialiased;
+ box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
+ color: #fff;
+ cursor: pointer;
+ border-width: 0 1px;
+ border-style: solid;
+ border-color: #000;
+
+ &:hover {
+ background-color: #444;
+ color: #fff;
+ text-decoration: none;
+ outline: 0;
+ }
+
+ &:active,
+ &:focus {
+ color: #fff;
+ background-color: #444;
+ text-decoration: none;
+ }
+ }
+
div.slider {
@include clearfix();
@include transform(scaleY(0.5) translate3d(0, 50%, 0));
@@ -230,48 +261,33 @@ div.video {
margin-bottom: 0;
a {
- border-bottom: none;
- border-right: 1px solid #000;
+ @extend %video-button;
+ background-image: url('../images/vcr.png');
+ background-position: 15px 15px ;
+ background-repeat: no-repeat;
+ border-left: none;
box-shadow: 1px 0 0 #555;
- cursor: pointer;
- display: block;
- line-height: 46px;
padding: 0 lh(.75);
- text-indent: -9999px;
width: 14px;
- background: url('../images/vcr.png') 15px 15px no-repeat;
&:focus {
position: relative;
z-index: 10000;
outline: #fff dotted thin;
outline-offset: -2px;
- background: #333;
- }
-
- &:hover {
- outline: 0;
}
&:empty {
height: 46px;
- background: url('../images/vcr.png') 15px 15px no-repeat;
+ background-position: 15px 15px;
}
&.play {
background-position: 17px -114px;
-
- &:hover {
- background-color: #444;
- }
}
&.pause {
background-position: 16px -50px;
-
- &:hover {
- background-color: #444;
- }
}
}
@@ -301,16 +317,12 @@ div.video {
}
}
- div.speeds {
+ .menu-container {
float: left;
position: relative;
&.open {
- & > a {
- background: url('../images/open-arrow.png') 10px center no-repeat;
- }
-
- ol.video_speeds {
+ .menu {
display: block;
opacity: 1;
padding: 0;
@@ -319,22 +331,77 @@ div.video {
}
}
- & > a {
- @include clearfix();
+ .menu {
@include transition(none);
- background: url('../images/closed-arrow.png') 10px center no-repeat;
- border-left: 1px solid #000;
- border-right: 1px solid #000;
- box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
- color: #fff;
- cursor: pointer;
- display: block;
- line-height: 46px; //height of play pause buttons
- margin-right: 0;
- padding-left: 15px;
- position: relative;
- -webkit-font-smoothing: antialiased;
+ box-shadow: inset 1px 0 0 #555, 0 1px 0 #444;
+ background-color: #444;
+ border: 1px solid #000;
+ bottom: 46px;
+ display: none;
+ opacity: 0;
+ position: absolute;
+ z-index: 10;
+
+ li {
+ box-shadow: 0 1px 0 #555;
+ border-bottom: 1px solid #000;
+ color: #fff;
+ cursor: pointer;
+
+ a {
+ border: 0;
+ color: #fff;
+ display: block;
+ padding: lh(.5);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &:hover {
+ background-color: #666;
+ color: #aaa;
+ outline-offset: -4px;
+ }
+ }
+
+ &.active{
+ a {
+ font-weight: bold;
+ }
+ }
+
+ &:last-child {
+ box-shadow: none;
+ border-bottom: 0;
+ margin-top: 0;
+ }
+ }
+ }
+ }
+
+ div.speeds {
+ &.open {
+ & > a {
+ background-image: url('../images/open-arrow.png');
+ }
+ }
+
+ .menu{
+ width: 131px;
+
+ @media (max-width: 1024px) {
+ width: 101px;
+ }
+ }
+
+ & > a {
+ @extend %video-button;
+ @include clearfix();
+ background-image: url('../images/closed-arrow.png');
+ background-position: 10px center;
+ background-repeat: no-repeat;
min-width: 116px;
+ text-indent: 0;
@media (max-width: 1024px) {
min-width: 0;
@@ -342,26 +409,6 @@ div.video {
}
h3 {
- display: block;
-
- @media (max-width: 1024px) {
- display: none;
- }
- }
-
- &:hover {
- outline: 0;
- opacity: 1;
- background-color: #444;
- }
-
- &:active {
- opacity: 1;
- background-color: #444;
- }
-
- h3 {
- color: #999;
float: left;
font-size: em(14);
font-weight: normal;
@@ -369,6 +416,11 @@ div.video {
padding: 0 lh(.25) 0 lh(.5);
line-height: 46px;
text-transform: uppercase;
+ color: #999;
+
+ @media (max-width: 1024px) {
+ display: none;
+ }
}
p.active {
@@ -385,55 +437,6 @@ div.video {
color: #fff;
}
}
-
- // fix for now
- ol.video_speeds {
- @include transition(none);
- box-shadow: inset 1px 0 0 #555, 0 4px 0 #444;
- background-color: #444;
- border: 1px solid #000;
- bottom: 46px;
- display: none;
- opacity: 0;
- position: absolute;
- width: 131px;
-
- @media (max-width: 1024px) {
- width: 101px;
- }
-
- z-index: 10;
-
- li {
- box-shadow: 0 1px 0 #555;
- border-bottom: 1px solid #000;
- color: #fff;
- cursor: pointer;
-
- a {
- border: 0;
- color: #fff;
- display: block;
- padding: lh(.5);
-
- &:hover {
- background-color: #666;
- color: #aaa;
- outline-offset: -4px;
- }
- }
-
- &.active {
- font-weight: bold;
- }
-
- &:last-child {
- box-shadow: none;
- border-bottom: 0;
- margin-top: 0;
- }
- }
- }
}
div.volume {
@@ -454,29 +457,14 @@ div.video {
}
& > a {
+ @extend %video-button;
@include clearfix();
- @include transition(none);
background-image: url('../images/volume.png');
background-position: 10px center;
background-repeat: no-repeat;
- border-right: 1px solid #000;
- box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
- color: #fff;
- cursor: pointer;
- display: block;
- height: 46px;
- margin-right: 0;
- padding-left: 15px;
- position: relative;
- -webkit-font-smoothing: antialiased;
+ border-left: none;
width: 30px;
-
- &:hover, &:active {
- background-color: #444;
- color: #fff;
- text-decoration: none;
- outline: 0;
- }
+ height: 46px;
}
.volume-slider-container {
@@ -523,49 +511,24 @@ div.video {
}
a.add-fullscreen {
- @include transition(none);
+ @extend %video-button;
background: url(../images/fullscreen.png) center no-repeat;
- border-right: 1px solid #000;
- box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
- color: #797979;
- display: block;
+ border-left: none;
float: left;
- line-height: 46px; //height of play pause buttons
- margin-left: 0;
- padding: 0 lh(.5);
- text-indent: -9999px;
+ padding: 0 11px;
width: 30px;
-
- &:hover, &:active {
- background-color: #444;
- color: #fff;
- text-decoration: none;
- outline: 0;
- }
}
a.quality_control {
- @include transition(none);
+ @extend %video-button;
background: url(../images/hd.png) center no-repeat;
- border-right: 1px solid #000;
- box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
- color: #797979;
+ border-left: none;
display: none;
float: left;
- line-height: 46px; //height of play pause buttons
- margin-left: 0;
- padding: 0 lh(.5);
- text-indent: -9999px;
+ padding: 0 11px;
width: 30px;
- &:hover {
- background-color: #444;
- color: #fff;
- text-decoration: none;
- outline: 0;
- }
-
&.active {
background-color: #F44;
color: #0ff;
@@ -574,33 +537,26 @@ div.video {
}
}
+ div.lang {
+ & > a.hide-subtitles {
+ @extend %video-button;
+ @include transition(none);
+ box-shadow: inset 1px 0 0 #555;
+ background: url('../images/cc.png') center no-repeat;
+ border-left: none;
+ border-right: none;
+ padding: 0 11px;
+ width: 30px;
- a.hide-subtitles {
- @include transition(none);
- background: url('../images/cc.png') center no-repeat;
- float: left;
- font-weight: 800;
- line-height: 46px; //height of play pause buttons
- margin-left: 0;
- opacity: 1;
- padding: 0 lh(.5);
- position: relative;
- text-indent: -9999px;
- -webkit-font-smoothing: antialiased;
- width: 30px;
-
- &:hover {
- background-color: #444;
- color: #fff;
- text-decoration: none;
- outline: 0;
+ &.off {
+ opacity: 0.7;
+ }
}
- &.off {
- opacity: 0.7;
+ .menu.langs-list {
+ right: -1px;
+ width: 150px;
}
-
- color: #797979;
}
}
}
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html
index 89b4e360c6..c08b544bc1 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video.html
@@ -11,7 +11,10 @@
data-start=""
data-end=""
data-saved-video-position="0"
- data-caption-asset-path="/static/subs/"
+ data-transcript-language="en"
+ data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+ data-transcript-translation-url="/transcript/translation"
+ data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
@@ -51,7 +54,9 @@
Fill Browser
HD off
- Captions
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html
index 982aca0232..7a19c951a6 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html
@@ -10,7 +10,10 @@
data-start=""
data-end=""
data-saved-video-position="0"
- data-caption-asset-path="/static/subs/"
+ data-transcript-language="en"
+ data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+ data-transcript-translation-url="/transcript/translation"
+ data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y"
data-mp4-source="xmodule/include/fixtures/test.mp4"
data-webm-source="xmodule/include/fixtures/test.webm"
@@ -54,7 +57,9 @@
Fill Browser
HD off
- Captions
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
index c3c61ba398..c330a0fb8f 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
@@ -10,7 +10,10 @@
data-start=""
data-end=""
data-saved-video-position="0"
- data-caption-asset-path="/static/subs/"
+ data-transcript-language="en"
+ data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+ data-transcript-translation-url="/transcript/translation"
+ data-transcript-available-translations-url="/transcript/available_translations"
data-sub="Z5KLxerq05Y"
data-mp4-source="xmodule/include/fixtures/test.mp4"
data-webm-source="xmodule/include/fixtures/test.webm"
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 ee98a3992e..1a28833f56 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
@@ -11,7 +11,10 @@
data-start=""
data-end=""
data-saved-video-position="0"
- data-caption-asset-path="/static/subs/"
+ data-transcript-language="en"
+ data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+ data-transcript-translation-url="/transcript/translation"
+ data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
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 33d304688b..df512bd7ab 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
@@ -11,7 +11,10 @@
data-start=""
data-end=""
data-saved-video-position="0"
- data-caption-asset-path="/static/subs/"
+ data-transcript-language="en"
+ data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+ data-transcript-translation-url="/transcript/translation"
+ data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
@@ -51,7 +54,9 @@
Fill Browser
HD off
- Captions
+
@@ -73,9 +78,13 @@
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
+ data-speed="1.0"
data-start=""
data-end=""
- data-caption-asset-path="/static/subs/"
+ data-transcript-language="en"
+ data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+ data-transcript-translation-url="/transcript/translation"
+ data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
@@ -112,7 +121,9 @@
Fill Browser
HD
- Captions
+
@@ -132,9 +143,13 @@
class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
+ data-speed="1.0"
data-start=""
data-end=""
- data-caption-asset-path="/static/subs/"
+ data-transcript-language="en"
+ data-transcript-languages='{"en": "English", "de": "Deutsch", "zh": "普通话"}'
+ data-transcript-translation-url="/transcript/translation"
+ data-transcript-available-translations-url="/transcript/available_translations"
data-autoplay="False"
data-yt-test-timeout="1500"
data-yt-test-url="https://gdata.youtube.com/feeds/api/videos/"
@@ -171,7 +186,9 @@
Fill Browser
HD
- Captions
+
diff --git a/common/lib/xmodule/xmodule/js/spec/helper.js b/common/lib/xmodule/xmodule/js/spec/helper.js
index 59fe9af25e..9847f4f27b 100644
--- a/common/lib/xmodule/xmodule/js/spec/helper.js
+++ b/common/lib/xmodule/xmodule/js/spec/helper.js
@@ -1,6 +1,4 @@
(function ($, undefined) {
- var oldAjaxWithPrefix = $.ajaxWithPrefix;
-
// Stub YouTube API.
window.YT = {
Player: function () {
@@ -63,42 +61,6 @@
]
};
- // For our purposes, we need to make sure that the function
- // $.ajaxWithPrefix does not fail when during tests a captions file is
- // requested. It is originally defined in file:
- //
- // common/static/coffee/src/ajax_prefix.js
- //
- // We will replace it with a function that does:
- //
- // 1.) Return a hard coded captions object if the file name contains
- // 'Z5KLxerq05Y'.
- // 2.) Behaves the same a as the original function in all other cases.
- $.ajaxWithPrefix = function (url, settings) {
- var data, success;
-
- if (!settings) {
- settings = url;
- url = settings.url;
- success = settings.success;
- data = settings.data;
- }
-
- if (
- url.match(/Z5KLxerq05Y/g) ||
- url.match(/7tqY6eQzVhE/g) ||
- url.match(/cogebirgzzM/g)
- ) {
- if ($.isFunction(success)) {
- return success(jasmine.stubbedCaption);
- } else if ($.isFunction(data)) {
- return data(jasmine.stubbedCaption);
- }
- } else {
- return oldAjaxWithPrefix.apply(this, arguments);
- }
- };
-
// Time waitsFor() should wait for before failing a test.
window.WAIT_TIMEOUT = 5000;
@@ -145,13 +107,16 @@
jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50'];
jasmine.stubRequests = function () {
- return spyOn($, 'ajax').andCallFake(function (settings) {
- var match, status, callCallback;
+ var spy = $.ajax;
- if (
- match = settings.url
- .match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/)
- ) {
+ if (!($.ajax.isSpy)) {
+ spy = spyOn($, 'ajax');
+ }
+ return spy.andCallFake(function (settings) {
+ var match = settings.url
+ .match(/youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/),
+ status, callCallback;
+ if (match) {
status = match[1].split('_');
if (status && status[0] === 'status') {
callCallback = function (callback) {
@@ -177,11 +142,10 @@
}
};
}
- } else if (
- match = settings.url
- .match(/static(\/.*)?\/subs\/(.+)\.srt\.sjson/)
- ) {
+ } else if (settings.url == '/transcript/translation') {
return settings.success(jasmine.stubbedCaption);
+ } else if (settings.url == '/transcript/available_translations') {
+ return settings.success(['uk', 'de']);
} else if (settings.url.match(/.+\/problem_get$/)) {
return settings.success({
html: readFixtures('problem_content.html')
@@ -265,6 +229,7 @@
.data(params);
}
+ jasmine.stubRequests();
state = new Video('#example');
state.resizer = (function () {
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 995c89627e..b0cf54bb97 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
@@ -181,31 +181,6 @@
});
});
- describe('youtubeId', function () {
- beforeEach(function () {
- loadFixtures('video.html');
- $.cookie.andReturn('1.0');
- state = new Video('#example');
- });
-
- describe('with speed', function () {
- it('return the video id for given speed', function () {
- expect(state.youtubeId('0.50'))
- .toEqual('7tqY6eQzVhE');
- expect(state.youtubeId('1.0'))
- .toEqual('cogebirgzzM');
- expect(state.youtubeId('1.50'))
- .toEqual('abcdefghijkl');
- });
- });
-
- describe('without speed', function () {
- it('return the video id for current speed', function () {
- expect(state.youtubeId()).toEqual('abcdefghijkl');
- });
- });
- });
-
describe('YouTube video in FireFox will cue first', function () {
var oldUserAgent;
@@ -368,84 +343,6 @@
});
});
- describe('setSpeed', function () {
-
- describe('YT', function () {
- beforeEach(function () {
- loadFixtures('video.html');
- state = new Video('#example');
- });
-
- it('check mapping', function () {
- var map = {
- '0.75': '0.50',
- '1.25': '1.50'
- };
-
- $.each(map, function(key, expected) {
- state.setSpeed(key, true);
- expect(state.speed).toBe(expected);
- });
- });
- });
- describe('HTML5', function () {
- beforeEach(function () {
- loadFixtures('video_html5.html');
- state = new Video('#example');
- });
-
- describe('when new speed is available', function () {
- beforeEach(function () {
- state.setSpeed('0.75', true);
- });
-
- it('set new speed', function () {
- expect(state.speed).toEqual('0.75');
- });
-
- it('save setting for new speed', function () {
-
- expect(state.storage.getItem('general_speed')).toBe('0.75');
- expect(state.storage.getItem('speed', true)).toBe('0.75');
- });
- });
-
- describe('when new speed is not available', function () {
- beforeEach(function () {
- state.setSpeed('1.75');
- });
-
- it('set speed to 1.0x', function () {
- expect(state.speed).toEqual('1.0');
- });
- });
-
- it('check mapping', function () {
- var map = {
- '0.25': '0.75',
- '0.50': '0.75',
- '2.0': '1.50'
- };
-
- $.each(map, function(key, expected) {
- state.setSpeed(key, true);
- expect(state.speed).toBe(expected);
- });
- });
- });
- });
-
- describe('getDuration', function () {
- beforeEach(function () {
- loadFixtures('video.html');
- state = new Video('#example');
- });
-
- it('return duration for current video', function () {
- expect(state.getDuration()).toEqual(400);
- });
- });
-
describe('log', function () {
beforeEach(function () {
loadFixtures('video_html5.html');
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 6177d4303c..020de1f384 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js
@@ -6,8 +6,14 @@ require(
['video/01_initialize.js'],
function (Initialize) {
describe('Initialize', function () {
+ var state = {};
+
+ afterEach(function () {
+ state = {};
+ });
+
describe('saveState function', function () {
- var state, videoPlayerCurrentTime, newCurrentTime, speed;
+ var videoPlayerCurrentTime, newCurrentTime, speed;
// We make sure that `currentTime` is a float. We need to test
// that Math.round() is called.
@@ -40,10 +46,6 @@ function (Initialize) {
spyOn(Time, 'formatFull').andCallThrough();
});
- afterEach(function () {
- state = undefined;
- });
-
it('data is not an object, async is true', function () {
itSpec({
asyncVal: true,
@@ -161,8 +163,211 @@ function (Initialize) {
});
}
});
- });
+ describe('getCurrentLanguage', function () {
+ var msg;
+
+ beforeEach(function () {
+ state.config = {};
+ state.config.transcriptLanguages = {
+ 'de': 'German',
+ 'en': 'English',
+ 'uk': 'Ukrainian',
+ };
+ });
+
+ it ('returns current language', function () {
+ var expected;
+
+ state.lang = 'de';
+ expected = Initialize.prototype.getCurrentLanguage.call(state);
+ expect(expected).toBe('de');
+ });
+
+ msg = 'returns `en`, if language isn\'t available for the video';
+ it (msg, function () {
+ var expected;
+
+ state.lang = 'zh';
+ expected = Initialize.prototype.getCurrentLanguage.call(state);
+ expect(expected).toBe('en');
+ });
+
+ msg = 'returns any available language, if current and `en` ' +
+ 'languages aren\'t available for the video';
+ it (msg, function () {
+ var expected;
+
+ state.lang = 'zh';
+ state.config.transcriptLanguages = {
+ 'de': 'German',
+ 'uk': 'Ukrainian',
+ };
+ expected = Initialize.prototype.getCurrentLanguage.call(state);
+ expect(expected).toBe('uk');
+ });
+
+ it ('returns `null`, if transcript unavailable', function () {
+ var expected;
+
+ state.lang = 'zh';
+ state.config.transcriptLanguages = {};
+ expected = Initialize.prototype.getCurrentLanguage.call(state);
+ expect(expected).toBeNull();
+ });
+ });
+
+ describe('getDuration', function () {
+ beforeEach(function () {
+ state = {
+ speed: '1.50',
+ metadata: {
+ 'testId': {
+ duration: 400
+ },
+ 'videoId': {
+ duration: 100
+ }
+ },
+ videos: {
+ '1.0': 'testId',
+ '1.50': 'videoId'
+ },
+ youtubeId: Initialize.prototype.youtubeId
+ };
+ });
+
+ it('returns duration for current video', function () {
+ var expected = Initialize.prototype.getDuration.call(state);
+
+ expect(expected).toEqual(100);
+ });
+
+ var msg = 'returns duration for the 1.0 speed as a fallback';
+ it(msg, function () {
+ var expected;
+
+ state.speed = '2.0';
+ expected = Initialize.prototype.getDuration.call(state);
+
+ expect(expected).toEqual(400);
+ });
+ });
+
+ describe('youtubeId', function () {
+ beforeEach(function () {
+ state = {
+ speed: '1.50',
+ videos: {
+ '0.50': '7tqY6eQzVhE',
+ '1.0': 'cogebirgzzM',
+ '1.50': 'abcdefghijkl'
+ }
+ };
+ });
+
+ describe('with speed', function () {
+ it('return the video id for given speed', function () {
+ $.each(state.videos, function(speed, videoId) {
+ var expected = Initialize.prototype.youtubeId.call(
+ state, speed
+ );
+
+ expect(videoId).toBe(expected);
+ });
+ });
+ });
+
+ describe('without speed', function () {
+ it('return the video id for current speed', function () {
+ var expected = Initialize.prototype.youtubeId.call(state);
+
+ expect(expected).toEqual('abcdefghijkl');
+ });
+ });
+
+ describe('speed is absent in the list of video speeds', function () {
+ it('return the video id for 1.0x speed', function () {
+ var expected = Initialize.prototype.youtubeId.call(state, '0.0');
+
+ expect(expected).toEqual('cogebirgzzM');
+ });
+ });
+ });
+
+ describe('setSpeed', function () {
+ describe('YT', function () {
+ beforeEach(function () {
+ state = {
+ speeds: ['0.25', '0.50', '1.0', '1.50', '2.0'],
+ storage: jasmine.createSpyObj('storage', ['setItem'])
+ };
+ });
+
+ it('check mapping', function () {
+ var map = {
+ '0.75': '0.50',
+ '1.25': '1.50'
+ };
+
+ $.each(map, function(key, expected) {
+ Initialize.prototype.setSpeed.call(state, key);
+ expect(state.speed).toBe(expected);
+ });
+ });
+ });
+
+ describe('HTML5', function () {
+ beforeEach(function () {
+ state = {
+ speeds: ['0.75', '1.0', '1.25', '1.50'],
+ storage: jasmine.createSpyObj('storage', ['setItem'])
+ };
+ });
+
+ describe('when new speed is available', function () {
+ beforeEach(function () {
+ Initialize.prototype.setSpeed.call(state, '0.75', true);
+ });
+
+ it('set new speed', function () {
+ expect(state.speed).toEqual('0.75');
+ });
+
+ it('save setting for new speed', function () {
+ expect(state.storage.setItem.calls[0].args)
+ .toEqual(['speed', '0.75', true]);
+
+ expect(state.storage.setItem.calls[1].args)
+ .toEqual(['general_speed', '0.75']);
+ });
+ });
+
+ describe('when new speed is not available', function () {
+ beforeEach(function () {
+ Initialize.prototype.setSpeed.call(state, '1.75');
+ });
+
+ it('set speed to 1.0x', function () {
+ expect(state.speed).toEqual('1.0');
+ });
+ });
+
+ it('check mapping', function () {
+ var map = {
+ '0.25': '0.75',
+ '0.50': '0.75',
+ '2.0': '1.50'
+ };
+
+ $.each(map, function(key, expected) {
+ Initialize.prototype.setSpeed.call(state, key, true);
+ expect(state.speed).toBe(expected);
+ });
+ });
+ });
+ });
+ });
});
}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
index 6ad591d74e..a9e5236c93 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
@@ -9,6 +9,7 @@
state = jasmine.initializePlayer();
videoControl = state.videoControl;
+ $.fn.scrollTo.reset();
});
afterEach(function () {
@@ -28,12 +29,7 @@
describe('always', function () {
beforeEach(function () {
spyOn($, 'ajaxWithPrefix').andCallThrough();
-
state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
});
it('create the caption element', function () {
@@ -64,8 +60,12 @@
runs(function () {
expect($.ajaxWithPrefix).toHaveBeenCalledWith({
- url: state.videoCaption.captionURL(),
+ url: '/transcript/translation',
notifyOnError: false,
+ data: {
+ videoId: 'Z5KLxerq05Y',
+ language: 'en'
+ },
success: jasmine.any(Function),
error: jasmine.any(Function)
});
@@ -100,23 +100,98 @@
expect($('.subtitles')).toHandleWith(
'DOMMouseScroll', state.videoCaption.onMovement
);
+ });
+
+ it('bind the scroll', function () {
+ expect($('.subtitles'))
+ .toHandleWith('scroll', state.videoControl.showControls);
+ });
+
+ });
+
+ describe('renderLanguageMenu', function () {
+ describe('is rendered', function () {
+ it('if languages more than 1', function () {
+ state = jasmine.initializePlayer();
+ var transcripts = state.config.transcriptLanguages,
+ langCodes = _.keys(transcripts),
+ langLabels = _.values(transcripts);
+
+ expect($('.langs-list')).toExist();
+ expect($('.langs-list')).toHandle('click');
+
+
+ $('.langs-list li').each(function(index) {
+ var code = $(this).data('lang-code'),
+ link = $(this).find('a'),
+ label = link.text();
+
+ expect(code).toBeInArray(langCodes);
+ expect(label).toBeInArray(langLabels);
+ });
+ });
+
+ it('when clicking on link with new language', function () {
+ state = jasmine.initializePlayer();
+ var Caption = state.videoCaption,
+ link = $('.langs-list li[data-lang-code="de"] a');
+
+ spyOn(Caption, 'fetchCaption');
+ spyOn(state.storage, 'setItem');
+
+ state.lang = 'en';
+ link.trigger('click');
+
+ expect(Caption.fetchCaption).toHaveBeenCalled();
+ expect(state.lang).toBe('de');
+ expect(state.storage.setItem)
+ .toHaveBeenCalledWith('language', 'de');
+ expect($('.langs-list li.active').length).toBe(1);
+ });
+
+ it('when clicking on link with current language', function () {
+ state = jasmine.initializePlayer();
+ var Caption = state.videoCaption,
+ link = $('.langs-list li[data-lang-code="en"] a');
+
+ spyOn(Caption, 'fetchCaption');
+ spyOn(state.storage, 'setItem');
+
+ state.lang = 'en';
+ link.trigger('click');
+
+ expect(Caption.fetchCaption).not.toHaveBeenCalled();
+ expect(state.lang).toBe('en');
+ expect(state.storage.setItem)
+ .not.toHaveBeenCalledWith('language', 'en');
+ expect($('.langs-list li.active').length).toBe(1);
+ });
+
+ it('open the language toggle on hover', function () {
+ state = jasmine.initializePlayer();
+ $('.lang').mouseenter();
+ expect($('.lang')).toHaveClass('open');
+ $('.lang').mouseleave();
+ expect($('.lang')).not.toHaveClass('open');
+ });
});
- it('bind the scroll', function () {
- expect($('.subtitles'))
- .toHandleWith('scroll', state.videoCaption.autoShowCaptions);
- expect($('.subtitles'))
- .toHandleWith('scroll', videoControl.showControls);
+ describe('is not rendered', function () {
+ it('if just 1 language', function () {
+ state = jasmine.initializePlayer(null, {
+ 'transcriptLanguages': {"en": "English"}
+ });
+
+ expect($('.langs-list')).not.toExist();
+ expect($('.lang')).not.toHandle('mouseenter');
+ expect($('.lang')).not.toHandle('mouseleave');
+ });
});
});
describe('when on a non touch-based device', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
});
it('render the caption', function () {
@@ -142,35 +217,46 @@
.toBe(true);
});
+
it('bind all the caption link', function () {
+ var handlerList = ['captionMouseOverOut', 'captionClick',
+ 'captionMouseDown', 'captionFocus', 'captionBlur',
+ 'captionKeyDown'
+ ];
+
+ $.each(handlerList, function(index, handler) {
+ spyOn(state.videoCaption, handler);
+ });
$('.subtitles li[data-index]').each(
function (index, link) {
- expect($(link)).toHandleWith(
- 'mouseover', state.videoCaption.captionMouseOverOut
- );
- expect($(link)).toHandleWith(
- 'mouseout', state.videoCaption.captionMouseOverOut
- );
- expect($(link)).toHandleWith(
- 'mousedown', state.videoCaption.captionMouseDown
- );
- expect($(link)).toHandleWith(
- 'click', state.videoCaption.captionClick
- );
- expect($(link)).toHandleWith(
- 'focus', state.videoCaption.captionFocus
- );
- expect($(link)).toHandleWith(
- 'blur', state.videoCaption.captionBlur
- );
- expect($(link)).toHandleWith(
- 'keydown', state.videoCaption.captionKeyDown
- );
+
+ $(link).trigger('mouseover');
+ expect(state.videoCaption.captionMouseOverOut).toHaveBeenCalled();
+
+ state.videoCaption.captionMouseOverOut.reset();
+ $(link).trigger('mouseout');
+ expect(state.videoCaption.captionMouseOverOut).toHaveBeenCalled();
+
+ $(this).click();
+ expect(state.videoCaption.captionClick).toHaveBeenCalled();
+
+ $(this).trigger('mousedown');
+ expect(state.videoCaption.captionMouseDown).toHaveBeenCalled();
+
+ $(this).trigger('focus');
+ expect(state.videoCaption.captionFocus).toHaveBeenCalled();
+
+ $(this).trigger('blur');
+ expect(state.videoCaption.captionBlur).toHaveBeenCalled();
+
+ $(this).trigger('keydown');
+ expect(state.videoCaption.captionKeyDown).toHaveBeenCalled();
});
});
it('set rendered to true', function () {
+ state = jasmine.initializePlayer();
expect(state.videoCaption.rendered).toBeTruthy();
});
});
@@ -180,9 +266,6 @@
window.onTouchBasedDevice.andReturn(['iPad']);
state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
$.fn.scrollTo.reset();
});
@@ -200,12 +283,9 @@
describe('when no captions file was specified', function () {
beforeEach(function () {
- loadFixtures('video_all.html');
-
- // Unspecify the captions file.
- $('#example').find('#video_id').data('sub', '');
-
- state = new Video('#example');
+ state = jasmine.initializePlayer('video_all.html', {
+ 'sub': ''
+ });
});
it('captions panel is not shown', function () {
@@ -218,6 +298,7 @@
beforeEach(function () {
jasmine.Clock.useMock();
spyOn(window, 'clearTimeout');
+ state = jasmine.initializePlayer();
});
describe('when cursor is outside of the caption box', function () {
@@ -313,8 +394,254 @@
});
});
+ it('reRenderCaption', function () {
+ var Caption = state.videoCaption,
+ li;
+
+ Caption.captions = ['test'];
+ Caption.start = [500];
+
+ spyOn(Caption, 'addPaddings');
+
+ Caption.reRenderCaption();
+ li = $('ol.subtitles li');
+
+ expect(Caption.addPaddings).toHaveBeenCalled();
+ expect(li.length).toBe(1);
+ expect(li).toHaveData('start', '500');
+ });
+
+ describe('fetchCaption', function () {
+ var Caption, msg;
+
+ beforeEach(function () {
+ state = jasmine.initializePlayer();
+ Caption = state.videoCaption;
+ spyOn($, 'ajaxWithPrefix').andCallThrough();
+ spyOn(Caption, 'reRenderCaption');
+ spyOn(Caption, 'renderCaption');
+ spyOn(Caption, 'bindHandlers');
+ spyOn(Caption, 'updatePlayTime');
+ spyOn(Caption, 'hideCaptions');
+ spyOn(state, 'youtubeId').andReturn('Z5KLxerq05Y');
+ });
+
+ it('do not fetch captions, if 1.0 speed is absent', function () {
+ state.youtubeId.andReturn(void(0));
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).not.toHaveBeenCalled();
+ expect(Caption.hideCaptions).not.toHaveBeenCalled();
+ });
+
+ it('show caption on language change', function () {
+ Caption.loaded = true;
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.hideCaptions).toHaveBeenCalledWith(false);
+ });
+
+ msg = 'use cookie to show/hide captions if they have not been ' +
+ 'loaded yet';
+ it(msg, function () {
+ Caption.loaded = false;
+ state.hide_captions = false;
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.hideCaptions).toHaveBeenCalledWith(false, false);
+
+ Caption.loaded = false;
+ Caption.hideCaptions.reset();
+ state.hide_captions = true;
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.hideCaptions).toHaveBeenCalledWith(true, false);
+ });
+
+ it('on success: on touch devices', function () {
+ state.isTouch = true;
+ Caption.loaded = false;
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.bindHandlers).toHaveBeenCalled();
+ expect(Caption.renderCaption).not.toHaveBeenCalled();
+ expect(Caption.updatePlayTime).not.toHaveBeenCalled();
+ expect(Caption.reRenderCaption).not.toHaveBeenCalled();
+ expect(Caption.loaded).toBeTruthy();
+ });
+
+ msg = 'on success: change language on touch devices when ' +
+ 'captions have not been rendered yet';
+ it(msg, function () {
+ state.isTouch = true;
+ Caption.loaded = true;
+ Caption.rendered = false;
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.bindHandlers).not.toHaveBeenCalled();
+ expect(Caption.renderCaption).not.toHaveBeenCalled();
+ expect(Caption.updatePlayTime).not.toHaveBeenCalled();
+ expect(Caption.reRenderCaption).not.toHaveBeenCalled();
+ expect(Caption.loaded).toBeTruthy();
+ });
+
+ it('on success: re-render on touch devices', function () {
+ state.isTouch = true;
+ Caption.loaded = true;
+ Caption.rendered = true;
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.bindHandlers).not.toHaveBeenCalled();
+ expect(Caption.renderCaption).not.toHaveBeenCalled();
+ expect(Caption.updatePlayTime).toHaveBeenCalled();
+ expect(Caption.reRenderCaption).toHaveBeenCalled();
+ expect(Caption.loaded).toBeTruthy();
+ });
+
+ it('on success: rendered correct', function () {
+ Caption.loaded = false;
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.bindHandlers).toHaveBeenCalled();
+ expect(Caption.renderCaption).toHaveBeenCalled();
+ expect(Caption.updatePlayTime).not.toHaveBeenCalled();
+ expect(Caption.reRenderCaption).not.toHaveBeenCalled();
+ expect(Caption.loaded).toBeTruthy();
+ });
+
+ it('on success: re-rendered correct', function () {
+ Caption.loaded = true;
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.bindHandlers).not.toHaveBeenCalled();
+ expect(Caption.renderCaption).not.toHaveBeenCalled();
+ expect(Caption.updatePlayTime).toHaveBeenCalled();
+ expect(Caption.reRenderCaption).toHaveBeenCalled();
+ expect(Caption.loaded).toBeTruthy();
+ });
+
+ msg = 'on error: captions are hidden if there are no transcripts';
+ it(msg, function () {
+ spyOn(Caption, 'fetchAvailableTranslations');
+ $.ajax.andCallFake(function (settings) {
+ settings.error([]);
+ });
+
+ state.config.transcriptLanguages = {};
+
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.fetchAvailableTranslations).not.toHaveBeenCalled();
+ expect(Caption.hideCaptions.mostRecentCall.args)
+ .toEqual([true, false]);
+ expect(Caption.hideSubtitlesEl).toBeHidden();
+ });
+
+ msg = 'on error: fetch available translations if there are ' +
+ 'additional transcripts';
+ xit(msg, function () {
+ $.ajax
+ .andCallFake(function (settings) {
+ settings.error([]);
+ });
+
+ state.config.transcriptLanguages = {
+ 'en': 'English',
+ 'uk': 'Ukrainian',
+ };
+
+ spyOn(Caption, 'fetchAvailableTranslations');
+ Caption.fetchCaption();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.fetchAvailableTranslations).toHaveBeenCalled();
+ expect(Caption.hideCaptions).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('fetchAvailableTranslations', function () {
+ var Caption, msg;
+
+ beforeEach(function () {
+ state = jasmine.initializePlayer();
+ Caption = state.videoCaption;
+ spyOn($, 'ajaxWithPrefix').andCallThrough();
+ spyOn(Caption, 'hideCaptions');
+ spyOn(Caption, 'fetchCaption');
+ spyOn(Caption, 'renderLanguageMenu');
+ });
+
+ it('request created with correct parameters', function () {
+ Caption.fetchAvailableTranslations();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalledWith({
+ url: '/transcript/available_translations',
+ notifyOnError: false,
+ success: jasmine.any(Function),
+ error: jasmine.any(Function)
+ });
+ });
+
+ msg = 'on succes: language menu is rendered if translations available';
+ it(msg, function () {
+ state.config.transcriptLanguages = {
+ 'en': 'English',
+ 'uk': 'Ukrainian',
+ 'de': 'German'
+ };
+ Caption.fetchAvailableTranslations();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.fetchCaption).toHaveBeenCalled();
+ expect(state.config.transcriptLanguages).toEqual({
+ 'uk': 'Ukrainian',
+ 'de': 'German'
+ });
+ expect(Caption.renderLanguageMenu).toHaveBeenCalledWith({
+ 'uk': 'Ukrainian',
+ 'de': 'German'
+ });
+ });
+
+ msg = 'on succes: language menu isn\'t rendered if translations unavailable';
+ it(msg, function () {
+ state.config.transcriptLanguages = {
+ 'en': 'English',
+ 'ru': 'Russian'
+ };
+ Caption.fetchAvailableTranslations();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.fetchCaption).not.toHaveBeenCalled();
+ expect(state.config.transcriptLanguages).toEqual({});
+ expect(Caption.renderLanguageMenu).not.toHaveBeenCalled();
+ });
+
+ msg = 'on error: captions are hidden if there are no transcript';
+ it(msg, function () {
+ $.ajax.andCallFake(function (settings) {
+ settings.error();
+ });
+ Caption.fetchAvailableTranslations();
+
+ expect($.ajaxWithPrefix).toHaveBeenCalled();
+ expect(Caption.hideCaptions).toHaveBeenCalledWith(true, false);
+ expect(Caption.hideSubtitlesEl).toBeHidden();
+ });
+ });
+
describe('search', function () {
it('return a correct caption index', function () {
+ state = jasmine.initializePlayer();
expect(state.videoCaption.search(0)).toEqual(-1);
expect(state.videoCaption.search(3120)).toEqual(1);
expect(state.videoCaption.search(6270)).toEqual(2);
@@ -328,13 +655,7 @@
describe('when the caption was not rendered', function () {
beforeEach(function () {
window.onTouchBasedDevice.andReturn(['iPad']);
-
state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
-
state.videoCaption.play();
});
@@ -359,34 +680,6 @@
expect($('.subtitles li:last')).toBe('.spacing');
});
- it('bind all the caption link', function () {
- $('.subtitles li[data-index]').each(
- function (index, link) {
-
- expect($(link)).toHandleWith(
- 'mouseover', state.videoCaption.captionMouseOverOut
- );
- expect($(link)).toHandleWith(
- 'mouseout', state.videoCaption.captionMouseOverOut
- );
- expect($(link)).toHandleWith(
- 'mousedown', state.videoCaption.captionMouseDown
- );
- expect($(link)).toHandleWith(
- 'click', state.videoCaption.captionClick
- );
- expect($(link)).toHandleWith(
- 'focus', state.videoCaption.captionFocus
- );
- expect($(link)).toHandleWith(
- 'blur', state.videoCaption.captionBlur
- );
- expect($(link)).toHandleWith(
- 'keydown', state.videoCaption.captionKeyDown
- );
- });
- });
-
it('set rendered to true', function () {
expect(state.videoCaption.rendered).toBeTruthy();
});
@@ -399,6 +692,7 @@
describe('pause', function () {
beforeEach(function () {
+ state = jasmine.initializePlayer();
state.videoCaption.playing = true;
state.videoCaption.pause();
});
@@ -409,6 +703,10 @@
});
describe('updatePlayTime', function () {
+ beforeEach(function () {
+ state = jasmine.initializePlayer();
+ });
+
describe('when the video speed is 1.0x', function () {
beforeEach(function () {
state.videoSpeedControl.currentSpeed = '1.0';
@@ -475,11 +773,7 @@
describe('resize', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
-
videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
-
$('.subtitles li[data-index=1]').addClass('current');
state.videoCaption.resize();
});
@@ -542,10 +836,6 @@
xdescribe('scrollCaption', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
});
describe('when frozen', function () {
@@ -590,6 +880,10 @@
// Disabled 10/9/13 due to flakiness in master
xdescribe('seekPlayer', function () {
+ beforeEach(function () {
+ state = jasmine.initializePlayer();
+ });
+
describe('when the video speed is 1.0x', function () {
beforeEach(function () {
state.videoSpeedControl.currentSpeed = '1.0';
@@ -603,12 +897,6 @@
describe('when the video speed is not 1.0x', function () {
beforeEach(function () {
- state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
-
state.videoSpeedControl.currentSpeed = '0.75';
$('.subtitles li[data-start="14910"]').trigger('click');
});
@@ -622,12 +910,6 @@
function () {
beforeEach(function () {
- state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
-
state.videoSpeedControl.currentSpeed = '0.75';
state.currentPlayerMode = 'flash';
$('.subtitles li[data-start="14910"]').trigger('click');
@@ -642,11 +924,6 @@
describe('toggle', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
-
spyOn(state.videoPlayer, 'log');
$('.subtitles li[data-index=1]').addClass('current');
});
@@ -722,10 +999,6 @@
describe('caption accessibility', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
-
- videoControl = state.videoControl;
-
- $.fn.scrollTo.reset();
});
describe('when getting focus through TAB key', 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 3dfba3008d..ec4f62f7e2 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
@@ -1,4 +1,10 @@
-(function (undefined) {
+(function (requirejs, require, define, undefined) {
+
+'use strict';
+
+require(
+['video/03_video_player.js'],
+function (VideoPlayer) {
describe('VideoPlayer', function () {
var state, oldOTBD;
@@ -11,7 +17,9 @@
afterEach(function () {
$('source').remove();
window.onTouchBasedDevice = oldOTBD;
- state.storage.clear();
+ if (state.storage) {
+ state.storage.clear();
+ }
});
describe('constructor', function () {
@@ -39,8 +47,8 @@
expect(state.videoCaption).toBeDefined();
expect(state.youtubeId('1.0')).toEqual('Z5KLxerq05Y');
expect(state.speed).toEqual('1.50');
- expect(state.config.captionAssetPath)
- .toEqual('/static/subs/');
+ expect(state.config.transcriptTranslationUrl)
+ .toEqual('/transcript/translation');
});
it('create video speed control', function () {
@@ -307,7 +315,7 @@
});
waitsFor(function () {
- duration = state.videoPlayer.duration();
+ var duration = state.videoPlayer.duration();
return duration > 0 && state.videoPlayer.isPlaying();
}, 'video begins playing', WAIT_TIMEOUT);
@@ -379,85 +387,33 @@
});
});
- describe('onSpeedChange', function () {
+ describe('when the video is not playing', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
- state.videoEl = $('video, iframe');
-
spyOn(state.videoPlayer, 'updatePlayTime').andCallThrough();
spyOn(state, 'setSpeed').andCallThrough();
spyOn(state.videoPlayer, 'log').andCallThrough();
spyOn(state.videoPlayer.player, 'setPlaybackRate').andCallThrough();
+ spyOn(state.videoPlayer, 'setPlaybackRate').andCallThrough();
});
- describe('always', function () {
- beforeEach(function () {
-
- state.videoPlayer.currentTime = 60;
- state.videoPlayer.onSpeedChange('0.75', false);
- });
-
- it('check if speed_change_video is logged', function () {
- expect(state.videoPlayer.log).toHaveBeenCalledWith(
- 'speed_change_video',
- {
- current_time: state.videoPlayer.currentTime,
- old_speed: '1.50',
- new_speed: '0.75'
- }
- );
- });
-
- it('convert the current time to the new speed', function () {
- expect(state.videoPlayer.currentTime).toEqual(60);
- });
-
- it('set video speed to the new speed', function () {
- expect(state.setSpeed).toHaveBeenCalledWith('0.75', true);
- });
+ it('video has a correct speed', function () {
+ state.speed = '2.0';
+ state.videoPlayer.onPlay();
+ expect(state.videoPlayer.setPlaybackRate)
+ .toHaveBeenCalledWith('2.0');
+ state.videoPlayer.onPlay();
+ expect(state.videoPlayer.setPlaybackRate.calls.length)
+ .toEqual(1);
});
- describe('when the video is playing', function () {
- beforeEach(function () {
- state.videoPlayer.currentTime = 60;
- state.videoPlayer.play();
- state.videoPlayer.onSpeedChange('0.75', false);
- });
-
- it('trigger updatePlayTime event', function () {
- expect(state.videoPlayer.player.setPlaybackRate)
- .toHaveBeenCalledWith('0.75');
- });
- });
-
- describe('when the video is not playing', function () {
- beforeEach(function () {
- state.videoPlayer.onSpeedChange('0.75', false);
- });
-
- it('trigger updatePlayTime event', function () {
- expect(state.videoPlayer.player.setPlaybackRate)
- .toHaveBeenCalledWith('0.75');
- });
-
- it('video has a correct speed', function () {
- spyOn(state.videoPlayer, 'onSpeedChange');
- state.speed = '2.0';
- state.videoPlayer.onPlay();
- expect(state.videoPlayer.onSpeedChange)
- .toHaveBeenCalledWith('2.0');
- state.videoPlayer.onPlay();
- expect(state.videoPlayer.onSpeedChange.calls.length).toEqual(1);
- });
-
- it('video has a correct volume', function () {
- spyOn(state.videoPlayer.player, 'setVolume');
- state.currentVolume = '0.26';
- state.videoPlayer.onPlay();
- expect(state.videoPlayer.player.setVolume)
- .toHaveBeenCalledWith('0.26');
- });
+ it('video has a correct volume', function () {
+ spyOn(state.videoPlayer.player, 'setVolume');
+ state.currentVolume = '0.26';
+ state.videoPlayer.onPlay();
+ expect(state.videoPlayer.player.setVolume)
+ .toHaveBeenCalledWith('0.26');
});
});
@@ -789,7 +745,7 @@
state.el.addClass('video-fullscreen');
state.videoControl.fullScreenState = true;
- isFullScreen = true;
+ state.videoControl.isFullScreen = true;
state.videoControl.fullScreenEl.attr('title', 'Exit-fullscreen');
state.videoControl.toggleFullScreen(jQuery.Event('click'));
@@ -931,20 +887,6 @@
});
});
- describe('playback rate', function () {
- beforeEach(function () {
- state = jasmine.initializePlayer();
-
- state.videoEl = $('video, iframe');
-
- state.videoPlayer.player.setPlaybackRate(1.5);
- });
-
- it('set the player playback rate', function () {
- expect(state.videoPlayer.player.video.playbackRate).toEqual(1.5);
- });
- });
-
describe('volume', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
@@ -1023,7 +965,7 @@
});
waitsFor(function () {
- duration = state.videoPlayer.duration();
+ var duration = state.videoPlayer.duration();
return duration > 0 && state.videoPlayer.isPlaying();
},'Video does not play.' , WAIT_TIMEOUT);
@@ -1034,6 +976,108 @@
});
});
});
- });
-}).call(this);
+ describe('onSpeedChange', function () {
+ beforeEach(function () {
+ state = {
+ el: $(document),
+ speed: '1.50',
+ setSpeed: jasmine.createSpy(),
+ saveState: jasmine.createSpy(),
+ videoPlayer: {
+ currentTime: 60,
+ log: jasmine.createSpy(),
+ updatePlayTime: jasmine.createSpy(),
+ setPlaybackRate: jasmine.createSpy(),
+ player: jasmine.createSpyObj('player', ['setPlaybackRate'])
+ }
+ };
+ });
+
+ describe('always', function () {
+ it('check if speed_change_video is logged', function () {
+ VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
+ expect(state.videoPlayer.log).toHaveBeenCalledWith(
+ 'speed_change_video',
+ {
+ current_time: state.videoPlayer.currentTime,
+ old_speed: '1.50',
+ new_speed: '0.75'
+ }
+ );
+ });
+
+ it('convert the current time to the new speed', function () {
+ state.currentPlayerMode = 'flash';
+ VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
+ expect(state.videoPlayer.currentTime).toBe('120.000');
+ });
+
+ it('set video speed to the new speed', function () {
+ VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false);
+ expect(state.setSpeed).toHaveBeenCalledWith('0.75', true);
+ expect(state.saveState).toHaveBeenCalledWith(true, {
+ speed: '0.75'
+ });
+ expect(state.videoPlayer.setPlaybackRate)
+ .toHaveBeenCalledWith('0.75');
+ });
+ });
+ });
+
+ describe('setPlaybackRate', function () {
+ beforeEach(function () {
+ state = {
+ youtubeId: jasmine.createSpy().andReturn('videoId'),
+ videoPlayer: {
+ currentTime: 60,
+ isPlaying: jasmine.createSpy(),
+ updatePlayTime: jasmine.createSpy(),
+ setPlaybackRate: jasmine.createSpy(),
+ player: jasmine.createSpyObj('player', [
+ 'setPlaybackRate', 'loadVideoById', 'cueVideoById'
+ ])
+ }
+ };
+ });
+
+ it('in Flash mode and video is playing', function () {
+ state.currentPlayerMode = 'flash';
+ state.videoPlayer.isPlaying.andReturn(true);
+ VideoPlayer.prototype.setPlaybackRate.call(state, '0.75');
+ expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
+ expect(state.videoPlayer.player.loadVideoById)
+ .toHaveBeenCalledWith('videoId', 60);
+ });
+
+ it('in Flash mode and video not started', function () {
+ state.currentPlayerMode = 'flash';
+ state.videoPlayer.isPlaying.andReturn(false);
+ VideoPlayer.prototype.setPlaybackRate.call(state, '0.75');
+ expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
+ expect(state.videoPlayer.player.cueVideoById)
+ .toHaveBeenCalledWith('videoId', 60);
+ });
+
+ 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;
+
+ state.videoPlayer.isPlaying.andReturn(false);
+ VideoPlayer.prototype.setPlaybackRate.call(state, '1.0');
+ expect(state.videoPlayer.updatePlayTime).toHaveBeenCalledWith(60);
+ expect(state.videoPlayer.player.cueVideoById)
+ .toHaveBeenCalledWith('videoId', 60);
+ });
+ });
+ });
+});
+
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
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 8ab4e0b785..7cea30aa9e 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -57,9 +57,11 @@ function (VideoPlayer, VideoStorage) {
});
});
},
+
methodsDict = {
bindTo: bindTo,
fetchMetadata: fetchMetadata,
+ getCurrentLanguage: getCurrentLanguage,
getDuration: getDuration,
getVideoMetadata: getVideoMetadata,
initialize: initialize,
@@ -305,6 +307,11 @@ function (VideoPlayer, VideoStorage) {
value ||
'1.0';
},
+ 'transcriptLanguage': function (value) {
+ return storage.getItem('language') ||
+ value ||
+ 'en';
+ },
'ytTestTimeout': function (value) {
value = parseInt(value, 10);
@@ -432,6 +439,7 @@ function (VideoPlayer, VideoStorage) {
this.config.endTime = null;
}
+ this.lang = this.config.transcriptLanguage;
this.speed = Number(
this.config.speed || this.config.generalSpeed
).toFixed(2).replace(/\.00$/, '.0');
@@ -631,17 +639,16 @@ function (VideoPlayer, VideoStorage) {
function setSpeed(newSpeed, updateStorage) {
// Possible speeds for each player type.
- // flash = [0.75, 1, 1.25, 1.5]
- // html5 = [0.75, 1, 1.25, 1.5]
- // youtube html5 = [0.25, 0.5, 1, 1.5, 2]
+ // HTML5 = [0.75, 1, 1.25, 1.5]
+ // Youtube Flash = [0.75, 1, 1.25, 1.5]
+ // Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2]
var map = {
- '0.25': '0.75',
- '0.50': '0.75',
- '0.75': '0.50',
- '1.25': '1.50',
- '2.0': '1.50'
- },
- useSession = true;
+ '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
+ '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash
+ '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5
+ '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5
+ '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash
+ };
if (_.contains(this.speeds, newSpeed)) {
this.speed = newSpeed;
@@ -712,6 +719,24 @@ function (VideoPlayer, VideoStorage) {
}
}
+ function getCurrentLanguage() {
+ var keys = _.keys(this.config.transcriptLanguages);
+
+ if (keys.length) {
+ if (!_.contains(keys, this.lang)) {
+ if (_.contains(keys, 'en')) {
+ this.lang = 'en';
+ } else {
+ this.lang = keys.pop();
+ }
+ }
+ } else {
+ return null;
+ }
+
+ return this.lang;
+ }
+
/*
* The trigger() function will assume that the @objChain is a complete
* chain with a method (function) at the end. It will call this function.
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 bd80c92aea..b225d0783e 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
@@ -5,30 +5,16 @@ define(
'video/03_video_player.js',
['video/02_html5_video.js', 'video/00_resizer.js'],
function (HTML5Video, Resizer) {
- var dfd = $.Deferred();
+ var dfd = $.Deferred(),
+ VideoPlayer = function (state) {
+ state.videoPlayer = {};
+ _makeFunctionsPublic(state);
+ _initialize(state);
+ // No callbacks to DOM events (click, mousemove, etc.).
- // VideoPlayer() function - what this module "exports".
- return function (state) {
-
- state.videoPlayer = {};
-
- _makeFunctionsPublic(state);
- _initialize(state);
- // No callbacks to DOM events (click, mousemove, etc.).
-
- return dfd.promise();
- };
-
- // ***************************************************************
- // Private functions start here.
- // ***************************************************************
-
- // function _makeFunctionsPublic(state)
- //
- // Functions which will be accessible via 'state' object. When called,
- // these functions will get the 'state' object as a context.
- function _makeFunctionsPublic(state) {
- var methodsDict = {
+ return dfd.promise();
+ },
+ methodsDict = {
duration: duration,
handlePlaybackQualityChange: handlePlaybackQualityChange,
isPlaying: isPlaying,
@@ -46,10 +32,25 @@ function (HTML5Video, Resizer) {
onVolumeChange: onVolumeChange,
pause: pause,
play: play,
+ setPlaybackRate: setPlaybackRate,
update: update,
updatePlayTime: updatePlayTime
};
+ VideoPlayer.prototype = methodsDict;
+
+ // VideoPlayer() function - what this module "exports".
+ return VideoPlayer;
+
+ // ***************************************************************
+ // Private functions start here.
+ // ***************************************************************
+
+ // function _makeFunctionsPublic(state)
+ //
+ // Functions which will be accessible via 'state' object. When called,
+ // these functions will get the 'state' object as a context.
+ function _makeFunctionsPublic(state) {
state.bindTo(methodsDict, state.videoPlayer, state);
}
@@ -70,7 +71,7 @@ function (HTML5Video, Resizer) {
$(window).on('unload', state.saveState);
if (state.currentPlayerMode !== 'flash') {
- state.videoPlayer.onSpeedChange(state.speed);
+ state.videoPlayer.setPlaybackRate(state.speed);
}
state.videoPlayer.player.setVolume(state.currentVolume);
});
@@ -325,33 +326,10 @@ function (HTML5Video, Resizer) {
}
}
- function onSpeedChange(newSpeed) {
+ function setPlaybackRate(newSpeed) {
var time = this.videoPlayer.currentTime,
methodName, youtubeId;
- if (this.currentPlayerMode === 'flash') {
- this.videoPlayer.currentTime = Time.convert(
- time,
- parseFloat(this.speed),
- newSpeed
- );
- }
-
- newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0');
-
- if (this.speed != newSpeed) {
- this.videoPlayer.log(
- 'speed_change_video',
- {
- current_time: time,
- old_speed: this.speed,
- new_speed: newSpeed
- }
- );
- }
-
- this.setSpeed(newSpeed, true);
-
if (
this.currentPlayerMode === 'html5' &&
!(
@@ -377,7 +355,33 @@ function (HTML5Video, Resizer) {
this.videoPlayer.player[methodName](youtubeId, time);
this.videoPlayer.updatePlayTime(time);
}
+ }
+ function onSpeedChange(newSpeed) {
+ var time = this.videoPlayer.currentTime,
+ isFlash = this.currentPlayerMode === 'flash';
+
+ if (isFlash) {
+ this.videoPlayer.currentTime = Time.convert(
+ time,
+ parseFloat(this.speed),
+ newSpeed
+ );
+ }
+
+ newSpeed = parseFloat(newSpeed).toFixed(2).replace(/\.00$/, '.0');
+
+ this.videoPlayer.log(
+ 'speed_change_video',
+ {
+ current_time: time,
+ old_speed: this.speed,
+ new_speed: newSpeed
+ }
+ );
+
+ this.setSpeed(newSpeed, true);
+ this.videoPlayer.setPlaybackRate(newSpeed);
this.el.trigger('speedchange', arguments);
this.saveState(true, { speed: newSpeed });
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 edea7bcdba..06ff56e180 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
@@ -43,8 +43,7 @@ function () {
// these functions will get the 'state' object as a context.
function _makeFunctionsPublic(state) {
var methodsDict = {
- autoHideCaptions: autoHideCaptions,
- autoShowCaptions: autoShowCaptions,
+ addPaddings: addPaddings,
bindHandlers: bindHandlers,
bottomSpacingHeight: bottomSpacingHeight,
calculateOffset: calculateOffset,
@@ -55,8 +54,8 @@ function () {
captionKeyDown: captionKeyDown,
captionMouseDown: captionMouseDown,
captionMouseOverOut: captionMouseOverOut,
- captionURL: captionURL,
fetchCaption: fetchCaption,
+ fetchAvailableTranslations: fetchAvailableTranslations,
hideCaptions: hideCaptions,
onMouseEnter: onMouseEnter,
onMouseLeave: onMouseLeave,
@@ -65,6 +64,8 @@ function () {
play: play,
renderCaption: renderCaption,
renderElements: renderElements,
+ renderLanguageMenu: renderLanguageMenu,
+ reRenderCaption: reRenderCaption,
resize: resize,
scrollCaption: scrollCaption,
search: search,
@@ -105,14 +106,24 @@ function () {
* and the CC button will be hidden.
*/
function renderElements() {
- this.videoCaption.loaded = false;
+ var Caption = this.videoCaption,
+ languages = this.config.transcriptLanguages;
- this.videoCaption.subtitlesEl = this.el.find('ol.subtitles');
- this.videoCaption.hideSubtitlesEl = this.el.find('a.hide-subtitles');
+ Caption.loaded = false;
+ Caption.subtitlesEl = this.el.find('ol.subtitles');
+ Caption.container = this.el.find('.lang');
+ Caption.hideSubtitlesEl = this.el.find('a.hide-subtitles');
- if (!this.videoCaption.fetchCaption()) {
- this.videoCaption.hideCaptions(true);
- this.videoCaption.hideSubtitlesEl.hide();
+ if (_.keys(languages).length) {
+ Caption.renderLanguageMenu(languages);
+
+ if (!Caption.fetchCaption()) {
+ Caption.hideCaptions(true);
+ Caption.hideSubtitlesEl.hide();
+ }
+ } else {
+ Caption.hideCaptions(true, false);
+ Caption.hideSubtitlesEl.hide();
}
}
@@ -121,203 +132,50 @@ function () {
// Bind any necessary function callbacks to DOM events (click,
// mousemove, etc.).
function bindHandlers() {
- $(window).bind('resize', this.videoCaption.resize);
- this.videoCaption.hideSubtitlesEl.on(
- 'click', this.videoCaption.toggle
- );
+ var self = this,
+ Caption = this.videoCaption;
- this.videoCaption.subtitlesEl
- .on(
- 'mouseenter',
- this.videoCaption.onMouseEnter
- ).on(
- 'mouseleave',
- this.videoCaption.onMouseLeave
- ).on(
- 'mousemove',
- this.videoCaption.onMovement
- ).on(
- 'mousewheel',
- this.videoCaption.onMovement
- ).on(
- 'DOMMouseScroll',
- this.videoCaption.onMovement
- );
+ $(window).bind('resize', Caption.resize);
+ Caption.hideSubtitlesEl.on({
+ 'click': Caption.toggle
+ });
- if ((this.videoType === 'html5') && (this.config.autohideHtml5)) {
- this.el.on({
- mousemove: this.videoCaption.autoShowCaptions,
- keydown: this.videoCaption.autoShowCaptions
- });
+ Caption.subtitlesEl.on({
+ mouseenter: Caption.onMouseEnter,
+ mouseleave: Caption.onMouseLeave,
+ mousemove: Caption.onMovement,
+ mousewheel: Caption.onMovement,
+ DOMMouseScroll: Caption.onMovement
+ });
- // Moving slider on subtitles is not a mouse move, but captions and
- // controls should be shown.
- this.videoCaption.subtitlesEl
- .on(
- 'scroll', this.videoCaption.autoShowCaptions
- )
- .on(
- 'scroll', this.videoControl.showControls
- );
- } else if (!this.config.autohideHtml5) {
- this.videoCaption.subtitlesEl.on({
- keydown: this.videoCaption.autoShowCaptions,
- focus: this.videoCaption.autoShowCaptions,
-
- // Moving slider on subtitles is not a mouse move, but captions
- // should not be auto-hidden.
- scroll: this.videoCaption.autoShowCaptions,
-
- mouseout: this.videoCaption.autoHideCaptions,
- blur: this.videoCaption.autoHideCaptions
- });
-
- this.videoCaption.hideSubtitlesEl.on({
- mousemove: this.videoCaption.autoShowCaptions,
-
- mouseout: this.videoCaption.autoHideCaptions,
- blur: this.videoCaption.autoHideCaptions
+ if (Caption.showLanguageMenu) {
+ Caption.container.on({
+ mouseenter: onContainerMouseEnter,
+ mouseleave: onContainerMouseLeave
});
}
- }
- /**
- * @desc Fetch the caption file specified by the user. Upn successful
- * receival of the file, the captions will be rendered.
- *
- * @type {function}
- * @access public
- *
- * @this {object} - The object containg the state of the video
- * player. All other modules, their parameters, public variables, etc.
- * are available via this object.
- *
- * @returns {boolean}
- * true: The user specified a caption file. NOTE: if an error happens
- * while the specified file is being retrieved (for example the
- * file is missing on the server), this function will still return
- * true.
- * false: No caption file was specified, or an empty string was
- * specified.
- */
- function fetchCaption() {
- var _this = this;
-
- // Check whether the captions file was specified. This is the point
- // where we either stop with the caption panel (so that a white empty
- // panel to the right of the video will not be shown), or carry on
- // further.
- if (!this.youtubeId('1.0')) {
- return false;
- }
-
- this.videoCaption.hideCaptions(this.hide_captions);
-
- // Fetch the captions file. If no file was specified, or if an error
- // occurred, then we hide the captions panel, and the "CC" button
- $.ajaxWithPrefix({
- url: _this.videoCaption.captionURL(),
- notifyOnError: false,
- success: function (captions) {
- _this.videoCaption.captions = captions.text;
- _this.videoCaption.start = captions.start;
- _this.videoCaption.loaded = true;
-
- if (_this.isTouch) {
- _this.videoCaption.subtitlesEl.find('li').html(
- gettext(
- 'Caption will be displayed when ' +
- 'you start playing the video.'
- )
- );
- } else {
- _this.videoCaption.renderCaption();
- }
-
- _this.videoCaption.bindHandlers();
- },
- error: function (jqXHR, textStatus, errorThrown) {
- console.log('[Video info]: ERROR while fetching captions.');
- console.log(
- '[Video info]: STATUS:', textStatus +
- ', MESSAGE:', '' + errorThrown
- );
-
- _this.videoCaption.hideCaptions(true, false);
- _this.videoCaption.hideSubtitlesEl.hide();
+ this.el.on('speedchange', function () {
+ if (self.currentPlayerMode === 'flash') {
+ Caption.fetchCaption();
}
});
- return true;
- }
-
- function captionURL() {
- return '' + this.config.captionAssetPath +
- this.youtubeId('1.0') + '.srt.sjson';
- }
-
- function autoShowCaptions(event) {
- if (!this.captionsShowLock) {
- if (!this.captionsHidden) {
- return;
- }
-
- this.captionsShowLock = true;
-
- if (this.captionState === 'invisible') {
- this.videoCaption.subtitlesEl.show();
- this.captionState = 'visible';
- } else if (this.captionState === 'hiding') {
- this.videoCaption.subtitlesEl
- .stop(true, false).css('opacity', 1).show();
- this.captionState = 'visible';
- } else if (this.captionState === 'visible') {
- clearTimeout(this.captionHideTimeout);
- }
-
- if (this.config.autohideHtml5) {
- this.captionHideTimeout = setTimeout(
- this.videoCaption.autoHideCaptions,
- this.videoCaption.fadeOutTimeout
- );
- }
-
- this.captionsShowLock = false;
+ if ((this.videoType === 'html5') && (this.config.autohideHtml5)) {
+ Caption.subtitlesEl.on('scroll', this.videoControl.showControls);
}
}
- function autoHideCaptions() {
- var _this;
+ function onContainerMouseEnter(event) {
+ event.preventDefault();
- this.captionHideTimeout = null;
-
- if (!this.captionsHidden) {
- return;
- }
-
- this.captionState = 'hiding';
-
- _this = this;
-
- this.videoCaption.subtitlesEl
- .fadeOut(
- this.videoCaption.fadeOutTimeout,
- function () {
- _this.captionState = 'invisible';
- }
- );
+ $(event.currentTarget).addClass('open');
}
- function resize() {
- this.videoCaption.subtitlesEl
- .find('.spacing:first')
- .height(this.videoCaption.topSpacingHeight())
- .find('.spacing:last')
- .height(this.videoCaption.bottomSpacingHeight());
+ function onContainerMouseLeave(event) {
+ event.preventDefault();
- this.videoCaption.scrollCaption();
-
- this.videoCaption.setSubtitlesHeight();
+ $(event.currentTarget).removeClass('open');
}
function onMouseEnter() {
@@ -344,76 +202,275 @@ function () {
}
function onMovement() {
- if (!this.config.autohideHtml5) {
- this.videoCaption.autoShowCaptions();
- }
-
this.videoCaption.onMouseEnter();
}
- function renderCaption() {
- var container = $(''),
- _this = this,
- autohideHtml5 = this.config.autohideHtml5;
-
- this.container.after(this.videoCaption.subtitlesEl);
- this.el.find('.video-controls .secondary-controls')
- .append(this.videoCaption.hideSubtitlesEl);
-
- this.videoCaption.setSubtitlesHeight();
-
- if ((this.videoType === 'html5' && autohideHtml5) || !autohideHtml5) {
- this.videoCaption.fadeOutTimeout = this.config.fadeOutTimeout;
- this.videoCaption.subtitlesEl.addClass('html5');
+ /**
+ * @desc Fetch the caption file specified by the user. Upn successful
+ * receival of the file, the captions will be rendered.
+ *
+ * @type {function}
+ * @access public
+ *
+ * @this {object} - The object containg the state of the video
+ * player. All other modules, their parameters, public variables, etc.
+ * are available via this object.
+ *
+ * @returns {boolean}
+ * true: The user specified a caption file. NOTE: if an error happens
+ * while the specified file is being retrieved (for example the
+ * file is missing on the server), this function will still return
+ * true.
+ * false: No caption file was specified, or an empty string was
+ * specified.
+ */
+ function fetchCaption() {
+ var self = this,
+ Caption = self.videoCaption;
+ // Check whether the captions file was specified. This is the point
+ // where we either stop with the caption panel (so that a white empty
+ // panel to the right of the video will not be shown), or carry on
+ // further.
+ if (!this.youtubeId('1.0')) {
+ return false;
}
- $.each(this.videoCaption.captions, function(index, text) {
+ if (Caption.loaded) {
+ Caption.hideCaptions(false);
+ } else {
+ Caption.hideCaptions(this.hide_captions, false);
+ }
+
+ if (Caption.fetchXHR && Caption.fetchXHR.abort) {
+ Caption.fetchXHR.abort();
+ }
+
+ // Fetch the captions file. If no file was specified, or if an error
+ // occurred, then we hide the captions panel, and the "CC" button
+ Caption.fetchXHR = $.ajaxWithPrefix({
+ url: self.config.transcriptTranslationUrl,
+ notifyOnError: false,
+ data: {
+ videoId: this.youtubeId(),
+ language: this.getCurrentLanguage()
+ },
+ success: function (captions) {
+ Caption.captions = captions.text;
+ Caption.start = captions.start;
+
+ if (Caption.loaded) {
+ if (Caption.rendered) {
+ Caption.reRenderCaption();
+ Caption.updatePlayTime(self.videoPlayer.currentTime);
+ }
+ } else {
+ if (self.isTouch) {
+ Caption.subtitlesEl.find('li').html(
+ gettext(
+ 'Caption will be displayed when ' +
+ 'you start playing the video.'
+ )
+ );
+ } else {
+ Caption.renderCaption();
+ }
+
+ Caption.bindHandlers();
+ }
+
+ Caption.loaded = true;
+ },
+ error: function (jqXHR, textStatus, errorThrown) {
+ console.log('[Video info]: ERROR while fetching captions.');
+ console.log(
+ '[Video info]: STATUS:', textStatus +
+ ', MESSAGE:', '' + errorThrown
+ );
+ // If initial list of languages has more than 1 item, check
+ // for availability other transcripts.
+ if (_.keys(self.config.transcriptLanguages).length > 1) {
+ Caption.fetchAvailableTranslations();
+ } else {
+ Caption.hideCaptions(true, false);
+ Caption.hideSubtitlesEl.hide();
+ }
+ }
+ });
+
+ return true;
+ }
+
+ function fetchAvailableTranslations() {
+ var self = this,
+ Caption = this.videoCaption;
+
+ return $.ajaxWithPrefix({
+ url: self.config.transcriptAvailableTranslationsUrl,
+ notifyOnError: false,
+ success: function (response) {
+ var currentLanguages = self.config.transcriptLanguages,
+ newLanguages = _.pick(currentLanguages, response);
+
+ // Update property with available currently translations.
+ self.config.transcriptLanguages = newLanguages;
+ // Remove an old language menu.
+ Caption.container.find('.langs-list').remove();
+
+ if (_.keys(newLanguages).length) {
+ // And try again to fetch transcript.
+ Caption.fetchCaption();
+ Caption.renderLanguageMenu(newLanguages);
+ }
+ },
+ error: function (jqXHR, textStatus, errorThrown) {
+ Caption.hideCaptions(true, false);
+ Caption.hideSubtitlesEl.hide();
+ }
+ });
+ }
+
+ function resize() {
+ this.videoCaption.subtitlesEl
+ .find('.spacing:first')
+ .height(this.videoCaption.topSpacingHeight())
+ .find('.spacing:last')
+ .height(this.videoCaption.bottomSpacingHeight());
+
+ this.videoCaption.scrollCaption();
+
+ this.videoCaption.setSubtitlesHeight();
+ }
+
+ function renderLanguageMenu(languages) {
+ var self = this,
+ menu = $('